From bd3341269151368eabe83198456cffdd08323771 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 16:57:19 +0300 Subject: [PATCH 01/19] feat(frontend): migrate HomePage and SearchBar to design system components - HomePage: replace inline styles with , , - SearchBar: replace with ,
    with ,
  • with ++, hardcoded hex with theme tokens - ESLint no-restricted-imports respected via @mui/material/Box deep import - Logic unchanged, all 111 tests pass --- apps/frontend/src/pages/home/ui/HomePage.tsx | 21 ++-- .../src/widgets/search-bar/ui/SearchBar.tsx | 115 +++++++++--------- docs/features/pilot-migration/plan.md | 60 +++++++++ docs/features/pilot-migration/spec.md | 31 +++++ docs/features/pilot-migration/tasks.md | 27 ++++ 5 files changed, 187 insertions(+), 67 deletions(-) create mode 100644 docs/features/pilot-migration/plan.md create mode 100644 docs/features/pilot-migration/spec.md create mode 100644 docs/features/pilot-migration/tasks.md diff --git a/apps/frontend/src/pages/home/ui/HomePage.tsx b/apps/frontend/src/pages/home/ui/HomePage.tsx index a0f2c52..1977544 100644 --- a/apps/frontend/src/pages/home/ui/HomePage.tsx +++ b/apps/frontend/src/pages/home/ui/HomePage.tsx @@ -1,14 +1,15 @@ +import Box from '@mui/material/Box'; +import { Heading, Text } from '@moex-vibe/design-system'; + export function HomePage() { return ( -
    -

    MoexVibe

    -

    - Анализ акций и облигаций Московской биржи -

    -

    Введите название или тикер в строку поиска выше

    -

    - Данные задерживаются на 15 минут · Бесплатный API MOEX ISS -

    -
    + + + MoexVibe + + Анализ акций и облигаций Московской биржи + Введите название или тикер в строку поиска выше + Данные задерживаются на 15 минут · Бесплатный API MOEX ISS + ); } diff --git a/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx b/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx index 240f04b..f41d370 100644 --- a/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx +++ b/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx @@ -1,5 +1,7 @@ import { useState, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import Box from '@mui/material/Box'; +import { Chip, Surface, Text, TextField } from '@moex-vibe/design-system'; import { useSearch } from '@/entities/search'; export function SearchBar() { @@ -32,82 +34,81 @@ export function SearchBar() { const showResults = open && debounced.length >= 2; return ( -
    - + { setQuery(e.target.value); setOpen(true); }} onFocus={() => setOpen(true)} - style={{ - width: '100%', - padding: '8px 12px', - border: '1px solid #ccc', - borderRadius: 6, - fontSize: 14, - }} /> {showResults && ( -
      - {isLoading &&
    • Загрузка...
    • } - {!isLoading && results && results.length === 0 && ( -
    • Ничего не найдено
    • - )} - {!isLoading && - results?.map((item) => ( -
    • { - setOpen(false); - setQuery(''); - navigate( - item.type === 'share' ? `/stocks/${item.secid}` : `/bonds/${item.secid}`, - ); - }} - style={{ - padding: '10px 12px', - cursor: 'pointer', - borderBottom: '1px solid #f0f0f0', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - }} - onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')} - onMouseLeave={(e) => (e.currentTarget.style.background = '')} - > - - {item.shortName} - {item.secid} - - + {isLoading && ( + + Загрузка... + + )} + {!isLoading && results && results.length === 0 && ( + + Ничего не найдено + + )} + {!isLoading && + results?.map((item) => ( + { + setOpen(false); + setQuery(''); + navigate( + item.type === 'share' ? `/stocks/${item.secid}` : `/bonds/${item.secid}`, + ); + }} + sx={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + px: 1.5, + py: 1.25, + cursor: 'pointer', + borderBottom: 1, + borderColor: 'divider', + '&:hover': { bgcolor: 'action.hover' }, + '&:last-child': { borderBottom: 0 }, + }} > - {item.type === 'share' ? 'Акция' : 'Облигация'} - -
    • - ))} -
    + + {item.shortName} + + {item.secid} + + + + + ))} + + )} -
    +
    ); } diff --git a/docs/features/pilot-migration/plan.md b/docs/features/pilot-migration/plan.md new file mode 100644 index 0000000..f53557e --- /dev/null +++ b/docs/features/pilot-migration/plan.md @@ -0,0 +1,60 @@ +# Pilot Migration — Implementation Plan + +## Architecture + +Миграция слоя представления без изменения логики. HomePage меняет JSX на DS-компоненты. SearchBar заменяет inline-стили на DS-компоненты + MUI `Box` (разрешён layout utility). + +## Files + +### Modify +- `apps/frontend/src/pages/home/ui/HomePage.tsx` — замена JSX +- `apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx` — замена JSX + +### No changes +- `apps/frontend/src/widgets/search-bar/ui/SearchBar.test.tsx` — тесты не меняются (те же тексты) +- `apps/frontend/src/styles.css` — CSS-переменные всё ещё используются broker-страницами + +## Step-by-step + +### Step 1: Migrate HomePage + +```tsx +// До +
    +

    MoexVibe

    +

    + Анализ акций и облигаций Московской биржи +

    +

    Введите название или тикер в строку поиска выше

    +

    + Данные задерживаются на 15 минут · Бесплатный API MOEX ISS +

    +
    + +// После +import { Box } from '@mui/material'; +import { Heading, Text } from '@moex-vibe/design-system'; + + + MoexVibe + Анализ акций и облигаций Московской биржи + Введите название или тикер в строку поиска выше + Данные задерживаются на 15 минут · Бесплатный API MOEX ISS + +``` + +### Step 2: Migrate SearchBar + +- `` → `` +- `
      ` dropdown wrapper → `` (с inline-стилями для absolute positioning) +- `
    • ` loading → `Загрузка...` +- `
    • ` empty → `Ничего не найдено` +- `
    • ` result item → `` + `` + `` +- Hover effect (`onMouseEnter/onMouseLeave`) → MUI `sx={{ '&:hover': { bgcolor: 'action.hover' } }}` +- Hardcoded hex colors → MUI theme tokens (`text.disabled`, `divider`, etc.) + +### Step 3: Verify + +- `npm run test:frontend` — все тесты проходят +- `npm run lint -w apps/frontend` — lint чист, нет неразрешённых MUI-импортов +- `npm run build:frontend` — сборка проходит diff --git a/docs/features/pilot-migration/spec.md b/docs/features/pilot-migration/spec.md new file mode 100644 index 0000000..050b42c --- /dev/null +++ b/docs/features/pilot-migration/spec.md @@ -0,0 +1,31 @@ +# Pilot Migration — Design System Adoption + +## Status + +Approved — утверждена пользователем 2026-06-21. + +## Goal + +Пилотная миграция существующих страниц на компоненты `@moex-vibe/design-system`. Подтвердить, что API дизайн-системы покрывает реальные сценарии, и зафиксировать gaps для следующей итерации. + +## Scope + +- `apps/frontend/src/pages/home/ui/HomePage.tsx` — миграция на ``, ``, `` +- `apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx` — миграция на ``, ``, ``, ``, `` + +## Out of scope + +- Рефакторинг логики SearchBar (debounce, click outside, keyboard nav, navigate) +- Миграция других страниц/виджетов +- Добавление новых компонентов в DS +- Удаление всего `styles.css` (только orphaned CSS variables после миграции) +- Изменение поведения компонентов (все acceptance criteria остаются теми же) + +## Acceptance Criteria + +- [ ] HomePage не содержит inline-стилей; все стили — через DS-компоненты и MUI layout utilities +- [ ] SearchBar не содержит hardcoded hex-цветов; палитра — через DS tokens +- [ ] SearchBar states (loading, empty, results) используют соответствующие DS-компоненты +- [ ] Логика SearchBar не изменена — все тесты проходят +- [ ] ESLint `no-restricted-imports` не нарушен (только `Box` из MUI напрямую) +- [ ] `npm run test:frontend && npm run lint -w apps/frontend && npm run build:frontend` проходят diff --git a/docs/features/pilot-migration/tasks.md b/docs/features/pilot-migration/tasks.md new file mode 100644 index 0000000..1d40751 --- /dev/null +++ b/docs/features/pilot-migration/tasks.md @@ -0,0 +1,27 @@ +# Pilot Migration — Tasks + +## 1. HomePage + +- [x] Заменить h1 на `` +- [x] Заменить p с text-secondary на `` +- [x] Заменить p с #888/#aaa на `` +- [x] Заменить div-wrapper на `` +- [x] Удалить все inline-стили + +## 2. SearchBar + +- [x] Заменить `` на `` +- [x] Заменить `
        ` на `` с positioning styles +- [x] Заменить loading text на `` +- [x] Заменить empty text на `` +- [x] Заменить `
      • ` results на `` с flex layout +- [x] Заменить hardcoded hex на theme tokens +- [x] Заменить JS hover handlers на MUI `sx={{ '&:hover': { bgcolor: 'action.hover' } }}` +- [x] Заменить тип "Акция"/"Облигация" на `` + +## 3. Verification + +- [x] `npm run test:frontend` — 23 suites, 111 passed +- [x] `npm run lint -w apps/frontend` — чист, 0 warnings +- [x] `npm run build:frontend` — проходит +- [x] ESLint `no-restricted-imports` не нарушен (использован `@mui/material/Box` deep import) -- 2.47.2 From a8741cfeab061a82c4b9fa1df15ad6ec24319470 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 17:15:45 +0300 Subject: [PATCH 02/19] fix: resolve Vite MUI deep import by moving to barrel import + updated ESLint allowlist - Changed @mui/material/Box deep imports to { Box } from '@mui/material' - Updated no-restricted-imports to only restrict DS-covered components, allowing Box/Stack/Grid for layout --- apps/frontend/.eslintrc.cjs | 11 ++++++++++- apps/frontend/src/pages/home/ui/HomePage.tsx | 2 +- apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/frontend/.eslintrc.cjs b/apps/frontend/.eslintrc.cjs index 22f9c0a..ec4f261 100644 --- a/apps/frontend/.eslintrc.cjs +++ b/apps/frontend/.eslintrc.cjs @@ -32,7 +32,16 @@ module.exports = { 'no-restricted-imports': ['warn', { paths: [{ name: '@mui/material', - message: 'Import from @moex-vibe/design-system instead.', + importNames: [ + // DS-covered: import from @moex-vibe/design-system + 'Typography', 'Button', 'TextField', 'Select', 'Checkbox', + 'Paper', 'Chip', 'Badge', 'Alert', 'Dialog', 'Skeleton', + 'CircularProgress', 'Link', 'IconButton', + 'Table', 'TableBody', 'TableCell', 'TableContainer', + 'TableHead', 'TableRow', 'TableSortLabel', + 'TablePagination', 'Pagination', + ], + message: 'Import from @moex-vibe/design-system instead, or use Box/Stack/Grid for layout.', }], }], '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], diff --git a/apps/frontend/src/pages/home/ui/HomePage.tsx b/apps/frontend/src/pages/home/ui/HomePage.tsx index 1977544..9f7f049 100644 --- a/apps/frontend/src/pages/home/ui/HomePage.tsx +++ b/apps/frontend/src/pages/home/ui/HomePage.tsx @@ -1,4 +1,4 @@ -import Box from '@mui/material/Box'; +import { Box } from '@mui/material'; import { Heading, Text } from '@moex-vibe/design-system'; export function HomePage() { diff --git a/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx b/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx index f41d370..e1b75d4 100644 --- a/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx +++ b/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import Box from '@mui/material/Box'; +import { Box } from '@mui/material'; import { Chip, Surface, Text, TextField } from '@moex-vibe/design-system'; import { useSearch } from '@/entities/search'; -- 2.47.2 From 62a3389bfb1a7b3c35c24a9493447b7bc78670dc Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 17:23:04 +0300 Subject: [PATCH 03/19] feat(frontend): migrate LoginPage, RegisterPage, ProfilePage to design system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoginPage: / + ); } - -const inputStyle: React.CSSProperties = { - width: '100%', - padding: '10px 12px', - border: '1px solid #e0e0e0', - borderRadius: 'var(--border-radius)', - fontSize: 16, - outline: 'none', - boxSizing: 'border-box', -}; - -const buttonStyle: React.CSSProperties = { - padding: '12px 24px', - background: 'var(--color-primary)', - color: '#fff', - border: 'none', - borderRadius: 'var(--border-radius)', - fontSize: 16, - fontWeight: 600, - cursor: 'pointer', -}; diff --git a/apps/frontend/src/pages/profile/ProfilePage.test.tsx b/apps/frontend/src/pages/profile/ProfilePage.test.tsx index da3d8fe..907bc0c 100644 --- a/apps/frontend/src/pages/profile/ProfilePage.test.tsx +++ b/apps/frontend/src/pages/profile/ProfilePage.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; import { server } from '@/shared/lib/test/server'; @@ -57,6 +57,8 @@ describe('ProfilePage', () => { await user.type(input, 'New Name'); await user.click(screen.getByRole('button', { name: 'Сохранить' })); - expect(await screen.findByText('Сохранение...')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Сохранить' })).toBeDisabled(); + }); }); }); diff --git a/apps/frontend/src/pages/profile/ui/ProfilePage.tsx b/apps/frontend/src/pages/profile/ui/ProfilePage.tsx index a804be9..ddce145 100644 --- a/apps/frontend/src/pages/profile/ui/ProfilePage.tsx +++ b/apps/frontend/src/pages/profile/ui/ProfilePage.tsx @@ -1,4 +1,6 @@ import { useState, type FormEvent } from 'react'; +import { Box } from '@mui/material'; +import { Button, Heading, Surface, Text, TextField } from '@moex-vibe/design-system'; import { useSession } from '@/entities/session'; export function ProfilePage() { @@ -24,82 +26,37 @@ export function ProfilePage() { if (!user) return null; return ( -
        -

        Профиль

        -
        -
        - Почта -

        {user.email}

        -
        -
        - Роль -

        {user.role}

        -
        -
        -
        - - setName(e.target.value)} - style={{ - width: '100%', - padding: '10px 12px', - border: '1px solid #e0e0e0', - borderRadius: 'var(--border-radius)', - fontSize: 16, - outline: 'none', - boxSizing: 'border-box', - }} - /> -
        + + + Профиль + + + + + Почта + + {user.email} + + + + Роль + + {user.role} + + + setName(e.target.value)} /> {message && ( -
        - {message} -
        + {message} )} - - -
        -
        + + + + ); } diff --git a/apps/frontend/src/pages/register/ui/RegisterPage.tsx b/apps/frontend/src/pages/register/ui/RegisterPage.tsx index 77d3b7f..7811b12 100644 --- a/apps/frontend/src/pages/register/ui/RegisterPage.tsx +++ b/apps/frontend/src/pages/register/ui/RegisterPage.tsx @@ -1,5 +1,7 @@ import { useState, type FormEvent } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { Box } from '@mui/material'; +import { Button, Heading, Text, TextField } from '@moex-vibe/design-system'; import { useSession } from '@/entities/session'; export function RegisterPage() { @@ -36,124 +38,57 @@ export function RegisterPage() { } return ( -
        -

        Регистрация

        -
        - {error &&
        {error}
        } -
        - - setName(e.target.value)} - style={inputStyle} - placeholder="Иван Иванов" - /> -
        -
        - - setEmail(e.target.value)} - required - style={inputStyle} - placeholder="email@example.com" - /> -
        -
        - - setPassword(e.target.value)} - required - minLength={6} - style={inputStyle} - placeholder="Минимум 6 символов" - /> -
        -
        - - setConfirmPassword(e.target.value)} - required - style={inputStyle} - placeholder="Повторите пароль" - /> -
        - -

        + + + Регистрация + + + {error && {error}} + setName(e.target.value)} + placeholder="Иван Иванов" + /> + setEmail(e.target.value)} + required + placeholder="email@example.com" + /> + setPassword(e.target.value)} + required + placeholder="Минимум 6 символов" + slotProps={{ htmlInput: { minLength: 6 } }} + /> + setConfirmPassword(e.target.value)} + required + placeholder="Повторите пароль" + /> + + Уже есть аккаунт?{' '} - + Войти -

        -
        -
        + + + ); } - -const inputStyle: React.CSSProperties = { - width: '100%', - padding: '10px 12px', - border: '1px solid #e0e0e0', - borderRadius: 'var(--border-radius)', - fontSize: 16, - outline: 'none', - boxSizing: 'border-box', -}; - -const buttonStyle: React.CSSProperties = { - padding: '12px 24px', - background: 'var(--color-primary)', - color: '#fff', - border: 'none', - borderRadius: 'var(--border-radius)', - fontSize: 16, - fontWeight: 600, - cursor: 'pointer', -}; -- 2.47.2 From 860bed159c2bd908919d379cb6cc0f2fdad89b6a Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 17:42:05 +0300 Subject: [PATCH 04/19] feat(frontend): migrate BrokerAccountsPage and widgets to design system --- .../broker-accounts/ui/BrokerAccountsPage.tsx | 76 ++-- .../BrokerAllocationBar.tsx | 61 ++- apps/frontend/src/styles.css | 357 ------------------ .../ui/BrokerAccountCard.tsx | 183 ++++++--- .../ui/BrokerAccountsSummary.tsx | 170 ++++++--- .../broker-accounts-page-migration/plan.md | 94 +++++ .../broker-accounts-page-migration/spec.md | 54 +++ .../broker-accounts-page-migration/tasks.md | 42 +++ 8 files changed, 508 insertions(+), 529 deletions(-) create mode 100644 docs/features/broker-accounts-page-migration/plan.md create mode 100644 docs/features/broker-accounts-page-migration/spec.md create mode 100644 docs/features/broker-accounts-page-migration/tasks.md diff --git a/apps/frontend/src/pages/broker-accounts/ui/BrokerAccountsPage.tsx b/apps/frontend/src/pages/broker-accounts/ui/BrokerAccountsPage.tsx index ba3bff0..ed655e5 100644 --- a/apps/frontend/src/pages/broker-accounts/ui/BrokerAccountsPage.tsx +++ b/apps/frontend/src/pages/broker-accounts/ui/BrokerAccountsPage.tsx @@ -1,3 +1,5 @@ +import { Box } from '@mui/material'; +import { EmptyState, Heading, Text } from '@moex-vibe/design-system'; import { aggregateBrokerAccounts, useBrokerAccounts, @@ -8,20 +10,20 @@ import { BrokerAccountsSummary } from '@/widgets/broker-accounts-summary'; function BrokerAccountsPageSkeleton() { return ( -
        -
        -
        -

        T-Bank broker overview

        -

        Брокерские счета

        -
        -
        + + + + T-Bank broker overview + + Брокерские счета + -
        + {['1', '2', '3'].map((id) => ( undefined} /> ))} -
        -
        + + ); } @@ -53,25 +55,23 @@ export function BrokerAccountsPage() { } if (error) { - return

        Не удалось загрузить счета

        ; + return Не удалось загрузить счета; } if (safeAccounts.length === 0) { return ( -
        -
        -
        -

        T-Bank broker overview

        -

        Брокерские счета

        -
        -
        -
        -

        Пока нет подключённых счетов

        -

        - После подключения T-Bank здесь появятся брокерские счета и ИИС со сводкой по капиталу. -

        -
        -
        + + + + T-Bank broker overview + + Брокерские счета + + + ); } @@ -85,16 +85,16 @@ export function BrokerAccountsPage() { const aggregate = aggregateBrokerAccounts(successfulPortfolios); return ( -
        -
        -
        -

        T-Bank broker overview

        -

        Брокерские счета

        -
        -

        - {safeAccounts.length} счетов под наблюдением -

        -
        + + + + + T-Bank broker overview + + Брокерские счета + + {safeAccounts.length} счетов под наблюдением + 0} /> -
        + {accountQueries.map(({ account, query }) => ( ))} -
        -
        + + ); } diff --git a/apps/frontend/src/shared/ui/broker-allocation-bar/BrokerAllocationBar.tsx b/apps/frontend/src/shared/ui/broker-allocation-bar/BrokerAllocationBar.tsx index 1de966c..6df64ac 100644 --- a/apps/frontend/src/shared/ui/broker-allocation-bar/BrokerAllocationBar.tsx +++ b/apps/frontend/src/shared/ui/broker-allocation-bar/BrokerAllocationBar.tsx @@ -1,3 +1,6 @@ +import { Box } from '@mui/material'; +import { Text } from '@moex-vibe/design-system'; + type AllocationBarItem = { key: string; label: string; @@ -16,34 +19,58 @@ export function BrokerAllocationBar({ const positiveItems = items.filter((item) => item.value > 0); if (positiveItems.length === 0) { - return

        Нет данных для распределения

        ; + return Нет данных для распределения; } return ( -
        -
        + + {positiveItems.map((item) => ( -
        -
          + + {positiveItems.map((item) => ( -
        • - +
        • + {item.label} + {item.percent.toFixed(0)}% +
          ))} -
        -
        + + ); } diff --git a/apps/frontend/src/styles.css b/apps/frontend/src/styles.css index 343b42d..d6ba3bc 100644 --- a/apps/frontend/src/styles.css +++ b/apps/frontend/src/styles.css @@ -225,294 +225,7 @@ a { color: var(--color-negative); } -.broker-accounts-page { - display: grid; - gap: 24px; -} - -.broker-accounts-page__header { - display: flex; - justify-content: space-between; - align-items: end; - gap: 16px; -} - -.broker-accounts-page__eyebrow, -.broker-account-card__eyebrow, -.broker-accounts-summary__eyebrow { - font-size: 12px; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--color-text-secondary); -} - -.broker-accounts-page__title, -.broker-account-card__title, -.broker-accounts-summary__title { - font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif; - line-height: 1.05; -} - -.broker-accounts-page__title { - font-size: clamp(2.2rem, 3vw, 3rem); -} - -.broker-accounts-page__caption { - color: var(--color-text-secondary); -} - -.broker-accounts-summary { - padding: 28px; - border-radius: 28px; - background: var(--broker-overview-bg); - box-shadow: - 0 24px 60px rgba(15, 52, 35, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.55); - border: 1px solid rgba(255, 255, 255, 0.7); - display: grid; - gap: 20px; -} - -.broker-accounts-summary__hero { - display: grid; - gap: 12px; - padding: 24px; - border-radius: 22px; - background: - radial-gradient(circle at top right, rgba(152, 196, 132, 0.3), transparent 28%), - linear-gradient(135deg, var(--broker-overview-panel) 0%, #173f2d 100%); - color: #f8f5ec; -} - -.broker-accounts-summary__hero .broker-accounts-summary__eyebrow { - color: rgba(248, 245, 236, 0.72); -} - -.broker-accounts-summary__title { - font-size: clamp(1.9rem, 2.4vw, 2.6rem); -} - -.broker-accounts-summary__status { - display: flex; - flex-wrap: wrap; - gap: 10px; - color: rgba(248, 245, 236, 0.78); -} - -.broker-accounts-summary__status span { - padding: 6px 10px; - border-radius: 999px; - background: var(--broker-overview-panel-soft); -} - -.broker-accounts-summary__currency-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 16px; -} - -.broker-accounts-summary__currency-card, -.broker-account-card, -.broker-accounts-empty { - border-radius: 24px; - border: 1px solid var(--broker-overview-border); - background: rgba(255, 255, 255, 0.92); - box-shadow: 0 20px 45px rgba(31, 48, 39, 0.08); -} - -.broker-accounts-summary__currency-card { - padding: 22px; - display: grid; - gap: 16px; -} - -.broker-accounts-summary__currency-header { - display: grid; - gap: 6px; -} - -.broker-accounts-summary__currency { - font-size: 13px; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--color-text-secondary); -} - -.broker-accounts-summary__total { - font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif; - font-size: clamp(1.8rem, 2vw, 2.4rem); -} - -.broker-accounts-summary__metrics { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; -} - -.broker-accounts-summary__metric { - display: grid; - gap: 8px; - padding: 14px; - border-radius: 18px; - background: rgba(255, 255, 255, 0.7); -} - -.broker-accounts-summary__metric dt, -.broker-account-card__stat span { - font-size: 13px; - color: var(--color-text-secondary); -} - -.broker-accounts-summary__metric dd, -.broker-account-card__stat strong { - font-size: 1.05rem; - font-weight: 700; -} - -.broker-allocation-bar { - display: grid; - gap: 12px; -} - -.broker-allocation-bar__track { - min-height: 12px; - border-radius: 999px; - overflow: hidden; - display: flex; - background: rgba(19, 54, 38, 0.08); -} - -.broker-allocation-bar__segment { - min-width: 8px; -} - -.broker-allocation-bar__legend { - list-style: none; - display: flex; - flex-wrap: wrap; - gap: 8px 12px; -} - -.broker-allocation-bar__legend-item { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 10px; - border-radius: 999px; - background: rgba(19, 54, 38, 0.05); -} - -.broker-allocation-bar__swatch { - width: 9px; - height: 9px; - border-radius: 999px; -} - -.broker-allocation-bar__empty { - color: var(--color-text-secondary); -} - -.broker-accounts-page__cards { - display: grid; - gap: 16px; -} - -.broker-account-card { - padding: 22px; -} - -.broker-account-card--link { - display: grid; - gap: 18px; - color: inherit; - text-decoration: none; - transition: - transform 0.22s ease, - box-shadow 0.22s ease, - border-color 0.22s ease; -} - -.broker-account-card--link:hover { - transform: translateY(-2px); - box-shadow: 0 26px 50px rgba(31, 48, 39, 0.11); - border-color: rgba(38, 92, 55, 0.22); -} - -.broker-account-card--link:focus-visible { - outline: 3px solid rgba(59, 128, 74, 0.3); - outline-offset: 3px; -} - -.broker-account-card__header { - display: flex; - justify-content: space-between; - gap: 12px; - align-items: start; -} - -.broker-account-card__title { - font-size: clamp(1.4rem, 2vw, 1.8rem); -} - -.broker-account-card__opened { - color: var(--color-text-secondary); - text-align: right; -} - -.broker-account-card__grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; -} - -.broker-account-card__stat { - display: grid; - gap: 8px; - padding: 14px; - border-radius: 18px; - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(241, 245, 239, 0.88)); - border: 1px solid rgba(31, 48, 39, 0.08); -} - -.broker-account-card__stat--wide { - grid-column: span 2; -} - -.broker-account-card__alert { - display: flex; - justify-content: space-between; - align-items: center; - gap: 16px; - padding: 16px 18px; - border-radius: 18px; - background: rgba(198, 40, 40, 0.08); - color: #8e2525; -} - -.broker-account-card__retry { - border: none; - border-radius: 999px; - background: #163f2c; - color: #fff; - padding: 10px 16px; - font: inherit; - cursor: pointer; -} - -.broker-account-card__retry:focus-visible { - outline: 3px solid rgba(59, 128, 74, 0.3); - outline-offset: 3px; -} - -.broker-accounts-empty { - padding: 28px; - display: grid; - gap: 10px; -} - @media (prefers-reduced-motion: reduce) { - .broker-account-card--link, .loading-spinner, .skeleton { transition: none; @@ -520,76 +233,6 @@ a { } } -@media (max-width: 720px) { - .broker-account__header { - padding: 0 0 8px; - } - - .broker-account__workspace { - gap: 16px; - grid-template-columns: minmax(0, 1fr); - } - - .broker-account__navigation { - flex-direction: row; - overflow-x: auto; - scrollbar-width: thin; - } - - .broker-account__link { - flex: none; - white-space: nowrap; - } - - .broker-overview__summary, - .broker-overview__assets { - grid-template-columns: 1fr; - } - - .broker-allocation { - align-items: stretch; - flex-direction: column; - } - - .broker-allocation svg { - max-width: 180px; - width: 100%; - align-self: center; - } - - .broker-accounts-page__header, - .broker-account-card__header { - grid-template-columns: minmax(0, 1fr); - display: grid; - } - - .broker-accounts-summary, - .broker-account-card, - .broker-accounts-empty { - padding: 18px; - border-radius: 20px; - } - - .broker-accounts-summary__hero, - .broker-accounts-summary__metrics, - .broker-account-card__grid { - grid-template-columns: 1fr; - } - - .broker-account-card__stat--wide { - grid-column: auto; - } - - .broker-account-card__opened { - text-align: left; - } - - .broker-account-card__alert { - align-items: stretch; - flex-direction: column; - } -} - .broker-operations__toolbar { display: flex; align-items: end; diff --git a/apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx b/apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx index 82a2d42..dd80bad 100644 --- a/apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx +++ b/apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; -import { SkeletonBlock } from '@/shared/ui/SkeletonBlock'; +import { Box } from '@mui/material'; +import { Alert, Button, Heading, Skeleton, Text } from '@moex-vibe/design-system'; import type { BrokerAccount, BrokerPortfolio } from '@/shared/api/responses'; import { buildBrokerAllocation } from '@/entities/broker-position'; import { BrokerAllocationBar } from '@/shared/ui/broker-allocation-bar'; @@ -14,26 +15,49 @@ function brokerAccountTypeLabel(type: 'brokerage' | 'iis'): string { return type === 'iis' ? 'ИИС' : 'Брокерский счёт'; } +const cardSx = { + p: 2.75, + borderRadius: 3, + border: '1px solid', + borderColor: 'rgba(21, 61, 43, 0.12)', + bgcolor: 'rgba(255, 255, 255, 0.92)', + boxShadow: '0 20px 45px rgba(31, 48, 39, 0.08)', +}; + +const statSx = { + display: 'grid', + gap: 1, + p: 1.75, + borderRadius: '18px', + background: 'linear-gradient(135deg, rgba(255,255,255,0.92), rgba(241,245,239,0.88))', + border: '1px solid', + borderColor: 'rgba(31,48,39,0.08)', +}; + function BrokerAccountCardSkeleton({ name, typeLabel }: { name: string; typeLabel: string }) { return ( -
        -
        -
        -

        {typeLabel}

        -

        {name}

        -
        -
        -
        + + + + + {typeLabel} + + {name} + + + {[1, 2, 3, 4].map((item) => ( -
        - - -
        + + + + ))} -
        - - -
        + + + + ); } @@ -45,20 +69,26 @@ function BrokerAccountCardError({ onRetry: () => void; }) { return ( -
        -
        -
        -

        {brokerAccountTypeLabel(account.type)}

        -

        {account.name}

        -
        -
        -
        -

        Не удалось загрузить данные счёта

        - -
        -
        + + + + + {brokerAccountTypeLabel(account.type)} + + {account.name} + + + + Повторить + + } + > + Не удалось загрузить данные счёта + + ); } @@ -74,44 +104,77 @@ function BrokerAccountCardSuccess({ const allocation = buildBrokerAllocation(portfolio); return ( - -
        -
        -

        {typeLabel}

        -

        {account.name}

        -
        - {openedAt ?

        Открыт {openedAt}

        : null} -
        + + + + {typeLabel} + + {account.name} + + {openedAt ? ( + + Открыт {openedAt} + + ) : null} + -
        -
        - Стоимость - {formatBrokerMoney(portfolio.totals.portfolio)} -
        -
        - За день - + + + + Стоимость + + {formatBrokerMoney(portfolio.totals.portfolio)} + + + + За день + + {formatBrokerSignedCurrencyValue( portfolio.totals.portfolio?.currency ?? 'RUB', portfolio.yields.daily?.value ?? null, )} - -
        -
        - Дневная динамика - {formatBrokerSignedPercent(portfolio.yields.dailyPercent)} -
        -
        - Ожидаемая доходность - {formatBrokerSignedPercent(portfolio.yields.expectedPercent)} -
        -
        + + + + + Дневная динамика + + {formatBrokerSignedPercent(portfolio.yields.dailyPercent)} + + + + Ожидаемая доходность + + {formatBrokerSignedPercent(portfolio.yields.expectedPercent)} + + - + ); } diff --git a/apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx b/apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx index 266c0d6..7ac60ea 100644 --- a/apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx +++ b/apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx @@ -1,4 +1,5 @@ -import { SkeletonBlock } from '@/shared/ui/SkeletonBlock'; +import { Box } from '@mui/material'; +import { Skeleton, Text } from '@moex-vibe/design-system'; import type { BrokerAccountsAggregate } from '@/entities/broker-account'; import { buildBrokerAllocation } from '@/entities/broker-position'; import { BrokerAllocationBar } from '@/shared/ui/broker-allocation-bar'; @@ -21,42 +22,80 @@ export function BrokerAccountsSummary({ }) { if (isLoading) { return ( -
        -
        - - - -
        -
        + + + + + + + {[1, 2, 3].map((item) => ( -
        - - -
        + + + + ))} -
        -
        + + ); } return ( -
        -
        -
        -

        Финансовый обзор

        -

        Счета в одном кадре

        -
        -
        - {totalCount} счетов - {availableCount !== totalCount ? ( - - Доступно по {availableCount} из {totalCount} счетов - - ) : null} -
        -
        + + + + + + Финансовый обзор + + + Счета в одном кадре + + + + + {totalCount} счетов + + {availableCount !== totalCount ? ( + + Доступно по {availableCount} из {totalCount} счетов + + ) : null} + + + -
        + {aggregate.portfolios.map((portfolioSummary) => { const allocation = buildBrokerAllocation({ account: { @@ -114,51 +153,68 @@ export function BrokerAccountsSummary({ }); return ( -
        -
        - + + {portfolioSummary.currency} - - + + {formatBrokerCurrencyValue(portfolioSummary.currency, portfolioSummary.total)} - -
        -
        -
        -
        За день
        -
        + + + + + + За день + + {formatBrokerSignedCurrencyValue( portfolioSummary.currency, portfolioSummary.daily, )} -
        -
        -
        -
        Динамика
        -
        {formatBrokerSignedPercent(portfolioSummary.dailyPercent)}
        -
        -
        -
        Свободные деньги
        -
        + + + + + Динамика + + {formatBrokerSignedPercent(portfolioSummary.dailyPercent)} + + + + Свободные деньги + + {formatBrokerCurrencyValue( portfolioSummary.currency, aggregate.cash.find((cash) => cash.currency === portfolioSummary.currency) ?.value ?? 0, )} -
        -
        -
        + + + -
        +
        ); })} -
        -
        + + ); } diff --git a/docs/features/broker-accounts-page-migration/plan.md b/docs/features/broker-accounts-page-migration/plan.md new file mode 100644 index 0000000..26eb9be --- /dev/null +++ b/docs/features/broker-accounts-page-migration/plan.md @@ -0,0 +1,94 @@ +# Broker Accounts Page — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate broker-accounts page area to use `@moex-vibe/design-system` components. + +**Architecture:** Replace legacy CSS classes, inline styles, and shared/components with DS equivalents. Keep business logic and data layer unchanged. The hero gradient section is intentionally left as custom CSS. + +**Tech Stack:** React 18, MUI 6.5, `@moex-vibe/design-system` 0.1.0 + +--- + +## Files + +### Modify +- `apps/frontend/src/pages/broker-accounts/ui/BrokerAccountsPage.tsx` +- `apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx` +- `apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx` +- `apps/frontend/src/shared/ui/broker-allocation-bar/BrokerAllocationBar.tsx` +- `apps/frontend/src/styles.css` — удалить осиротевшие классы + +### No changes +- Entity layer (`entities/broker-account/`, `entities/broker-position/`) — бизнес-логика не меняется +- Shared formatters (`shared/lib/formatters.ts`) — используются для форматирования значений +- Shared API layer (`shared/api/`) — не меняется + +--- + +## Task 1: BrokerAllocationBar + +Replace inline HTML + CSS classes with `` + ``. + +- Empty: `

        ` → `` +- Track: `

        ` → `` with `sx` +- Segment: `` → `` with `sx` +- Legend: `
          ` / `
        • ` → `` with `sx` flex layout +- Swatch: `` with inline style → `` with `sx` + +Segment width/background и swatch colors остаются как inline styles (dynamic per item, from ALLOCATION_CONFIG). + +## Task 2: BrokerAccountCard + +Replace legacy shared components and CSS with DS equivalents. + +- Skeleton block: `` → `` from DS +- Eyebrow: `

          ` → `` +- Title: `

          ` → `` +- Stat labels/values: `` + `` → `` + `` +- Card wrapper: `
          ` → `` with `component={Link}` or `` with `sx` +- Error: `
          ` → `` + `` + `

        +
        + + + + {[1, 2, 3, 4].map((row) => ( + + + + + + + + + + + + + + + + + + ))} + + + ) : positions.length === 0 && !isFetching ? ( -

        {emptyMessage}

        + + {emptyMessage} + ) : ( -
        -
        - - - - - - - - - - - + + + + {positions.map((position) => ( - - - - - - - + + ))} - -
        + + + + + + Тикер - + + Название - + + Количество - + + Цена - + + Стоимость -
        + - - - {position.name || '-'} - - + + + {position.name || '-'} + + {formatQuantity(position.quantity)} - + + {formatMoney(position.currentPrice)} - + + {formatMoney(position.currentValue)} -
        -
        + + + {isFetching && ( -
        -
        - + + + Загрузка страницы {pageNumber}… - -
        + + )} -
        + )} ); diff --git a/apps/frontend/src/widgets/broker-positions-table/ui/PositionTicker.tsx b/apps/frontend/src/widgets/broker-positions-table/ui/PositionTicker.tsx index 16a61fe..fbc1f66 100644 --- a/apps/frontend/src/widgets/broker-positions-table/ui/PositionTicker.tsx +++ b/apps/frontend/src/widgets/broker-positions-table/ui/PositionTicker.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; +import { Box } from '@mui/material'; import type { BrokerPosition } from '@/shared/api/responses'; import { getBrokerInstrumentPath } from '@/entities/broker-position'; @@ -11,12 +12,18 @@ export function PositionTicker({ position }: { position: BrokerPosition }) { }); if (!path || label === '-') { - return {label}; + return ( + + {label} + + ); } return ( - - {label} + + + {label} + ); } -- 2.47.2 From e044b64de4b3851d815dceeaf2963ff8d6ba24c5 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 18:59:58 +0300 Subject: [PATCH 11/19] feat: migrate BrokerOperationsTable to design system --- .../ui/BrokerOperationsTable.tsx | 264 +++++++++--------- 1 file changed, 135 insertions(+), 129 deletions(-) diff --git a/apps/frontend/src/widgets/broker-operations-table/ui/BrokerOperationsTable.tsx b/apps/frontend/src/widgets/broker-operations-table/ui/BrokerOperationsTable.tsx index 06e0ef4..4854290 100644 --- a/apps/frontend/src/widgets/broker-operations-table/ui/BrokerOperationsTable.tsx +++ b/apps/frontend/src/widgets/broker-operations-table/ui/BrokerOperationsTable.tsx @@ -1,6 +1,8 @@ import { Link } from 'react-router-dom'; import type { ReactNode } from 'react'; import type { BrokerOperation, BrokerOperationsPage } from '@/shared/api/responses'; +import { Box } from '@mui/material'; +import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system'; import { TableSkeleton } from '@/shared/ui/TableSkeleton'; import { getBrokerOperationImpact, @@ -10,42 +12,33 @@ import { import { getBrokerInstrumentPath } from '@/entities/broker-position'; import { formatBrokerSignedMoney } from '@/shared/lib/formatters'; -const tableStyle = { +const tableSx = { width: '100%', borderCollapse: 'collapse', fontSize: 14, -} satisfies React.CSSProperties; +} as const; -const thStyle = { - borderBottom: '1px solid #e0e0e0', - color: 'var(--color-text-secondary)', +const thSx = { + borderBottom: '1px solid', + borderColor: 'divider', + color: 'text.secondary', fontWeight: 600, - padding: '10px 8px', -} satisfies React.CSSProperties; + p: 1, + textAlign: 'left', +} as const; -const tdStyle = { - borderBottom: '1px solid #eeeeee', - padding: '10px 8px', +const tdSx = { + borderBottom: '1px solid', + borderColor: 'divider', + p: 1, verticalAlign: 'top', -} satisfies React.CSSProperties; + textAlign: 'left', +} as const; -const pagButtonStyle = { - padding: '6px 14px', - borderRadius: 6, - border: '1px solid #e0e0e0', - background: 'var(--color-surface)', - color: 'var(--color-text)', - fontSize: 14, - fontWeight: 600, - cursor: 'pointer', - lineHeight: 1.4, -} satisfies React.CSSProperties; - -const pagButtonDisabledStyle = { - ...pagButtonStyle, - opacity: 0.35, - cursor: 'not-allowed', -} satisfies React.CSSProperties; +const tdSxRight = { + ...tdSx, + textAlign: 'right', +} as const; function formatDate(value: string | null) { if (!value) return '-'; @@ -53,11 +46,10 @@ function formatDate(value: string | null) { return new Date(value).toLocaleString('ru-RU'); } -function moneyColor(impact: BrokerOperationImpact): string { - if (impact === 'adds') return 'var(--color-positive)'; - if (impact === 'reduces') return 'var(--color-negative)'; - - return 'var(--color-text)'; +function operationTone(impact: BrokerOperationImpact) { + if (impact === 'adds') return 'positive' as const; + if (impact === 'reduces') return 'negative' as const; + return 'primary' as const; } function OperationInstrument({ operation }: { operation: BrokerOperation }) { @@ -69,19 +61,23 @@ function OperationInstrument({ operation }: { operation: BrokerOperation }) { }); const name = operation.name || operation.description; - if (!path && !name) return -; - if (!path) return {name}; + if (!path && !name) return -; + if (!path) return {name}; if (!ticker || ticker === '-') return {name}; return ( -
        - - {ticker} + + + + {ticker} + {name && name !== ticker && ( - {name} + + {name} + )} -
        + ); } @@ -103,146 +99,156 @@ export function BrokerOperationsTable({ return (
        -
        -

        {title}

        + {title} {headerAction} {pagination && ( -
        - - : '←'} + + {pageNumber} - - -
        + {isFetching ? : '→'} + + )} -
        + {isLoading ? ( -
        - - - - - - - - - + + + -
        + + + + + Дата - + + Тип - + + Инструмент - + + Сумма -
        -
        + + ) : operations.length === 0 && !isFetching ? ( -

        {emptyMessage}

        + + {emptyMessage} + ) : ( -
        -
        - - - - - - - - - - + + + + {operations.map((operation) => { const impact = getBrokerOperationImpact(operation); return ( - - - - - - + + ); })} - -
        + + + + + + Дата - + + Тип - + + Инструмент - + + Сумма -
        {formatDate(operation.date)} - {getBrokerOperationTypeLabel(operation)} - + + + {formatDate(operation.date)} + + + {getBrokerOperationTypeLabel(operation)} + + - + {formatBrokerSignedMoney(operation.payment)} -
        -
        + + + {isFetching && ( -
        - + + )} -
        + )}
        ); -- 2.47.2 From 8ad5f3c70cb068ffffab816b292f126d1bf7223f Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 20:21:00 +0300 Subject: [PATCH 12/19] feat: migrate BrokerPositionsPage and BrokerOperationsPage --- .../ui/BrokerOperationsPage.tsx | 26 ++++++++++++------- .../ui/BrokerPositionsPage.tsx | 14 +++++----- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/apps/frontend/src/pages/broker-operations/ui/BrokerOperationsPage.tsx b/apps/frontend/src/pages/broker-operations/ui/BrokerOperationsPage.tsx index 2a52d29..f6e7135 100644 --- a/apps/frontend/src/pages/broker-operations/ui/BrokerOperationsPage.tsx +++ b/apps/frontend/src/pages/broker-operations/ui/BrokerOperationsPage.tsx @@ -1,5 +1,7 @@ import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { Box } from '@mui/material'; +import { Heading, Text } from '@moex-vibe/design-system'; import { BROKER_OPERATION_TYPE_OPTIONS, isBrokerOperationType, @@ -32,7 +34,9 @@ export function BrokerOperationsPage() { } const history = operations.error ? ( -

        Не удалось загрузить историю операций

        + + Не удалось загрузить историю операций + ) : ( -
        -

        + + + Операции -

        - -
        + + {history} - + ); } diff --git a/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx b/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx index 5586b0b..7e75cc8 100644 --- a/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx +++ b/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx @@ -1,3 +1,5 @@ +import { Box } from '@mui/material'; +import { Heading, Text } from '@moex-vibe/design-system'; import { useBrokerPositions } from '@/entities/broker-position'; import { useBrokerAccountContext } from '@/widgets/broker-account-layout'; import { BrokerPositionTable } from '@/widgets/broker-positions-table'; @@ -15,14 +17,14 @@ export function BrokerPositionsPage({ type, title }: BrokerPositionsPageProps) { if (positions.error) { return ( -
        -

        + + {title} -

        -

        + + {type === 'share' ? 'Не удалось загрузить акции' : 'Не удалось загрузить облигации'} -

        -
        + + ); } -- 2.47.2 From aa56c8a48d8e09eb827817f3ed0e9b6d8dbf2812 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 20:21:15 +0300 Subject: [PATCH 13/19] chore: remove legacy SkeletonBlock component --- apps/frontend/src/shared/ui/SkeletonBlock.tsx | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 apps/frontend/src/shared/ui/SkeletonBlock.tsx diff --git a/apps/frontend/src/shared/ui/SkeletonBlock.tsx b/apps/frontend/src/shared/ui/SkeletonBlock.tsx deleted file mode 100644 index 0419c7f..0000000 --- a/apps/frontend/src/shared/ui/SkeletonBlock.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export function SkeletonBlock({ - width, - height, - borderRadius = 4, -}: { - width?: string | number; - height?: string | number; - borderRadius?: number; -}) { - return ( -
        - ); -} -- 2.47.2 From 85fbfcdb0c424dafe0363dca0c9d276eee3aacc6 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 20:24:03 +0300 Subject: [PATCH 14/19] chore: remove SkeletonBlock from shared/ui exports --- apps/frontend/src/shared/ui/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/src/shared/ui/index.ts b/apps/frontend/src/shared/ui/index.ts index 977fbbb..acc0989 100644 --- a/apps/frontend/src/shared/ui/index.ts +++ b/apps/frontend/src/shared/ui/index.ts @@ -1,2 +1 @@ -export { SkeletonBlock } from './SkeletonBlock'; export { TableSkeleton } from './TableSkeleton'; -- 2.47.2 From b41ee9fce4491591273200636b446149b5aea2ec Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 20:30:39 +0300 Subject: [PATCH 15/19] chore: remove all orphaned broker-* CSS classes and variables --- apps/frontend/src/styles.css | 85 ------------------------------------ 1 file changed, 85 deletions(-) diff --git a/apps/frontend/src/styles.css b/apps/frontend/src/styles.css index b64e8cc..600402b 100644 --- a/apps/frontend/src/styles.css +++ b/apps/frontend/src/styles.css @@ -16,12 +16,6 @@ --color-negative: #c62828; --border-radius: 8px; --shadow: 0 1px 3px rgba(0, 0, 0, 0.12); - --broker-overview-bg: linear-gradient(180deg, #f1f5ef 0%, #f7f2e8 100%); - --broker-overview-panel: rgba(15, 51, 36, 0.93); - --broker-overview-panel-soft: rgba(255, 255, 255, 0.09); - --broker-overview-border: rgba(21, 61, 43, 0.12); - --broker-overview-accent: #98c484; - --broker-overview-gold: #d7b268; } .pnl-cell { @@ -94,47 +88,6 @@ a { z-index: 1; } -.broker-allocation ul { - list-style: none; - display: grid; - gap: 8px; -} - -.broker-allocation { - display: flex; - align-items: center; - gap: 20px; -} - -.broker-allocation svg { - width: 160px; - max-width: 40%; - flex: 0 0 auto; -} - -.broker-allocation figcaption { - display: grid; - gap: 12px; -} - -.broker-allocation li { - display: flex; - align-items: center; - gap: 8px; -} - -.broker-allocation__swatch { - width: 12px; - height: 12px; - border: 1px solid color-mix(in srgb, var(--color-text) 20%, transparent); - border-radius: 2px; - flex: 0 0 auto; -} - -.broker-allocation__negative { - color: var(--color-negative); -} - @media (prefers-reduced-motion: reduce) { .loading-spinner, .skeleton { @@ -143,42 +96,4 @@ a { } } -.broker-operations__toolbar { - display: flex; - align-items: end; - justify-content: space-between; - gap: 16px; - margin-bottom: 20px; -} -.broker-operations__toolbar label { - display: grid; - gap: 6px; - color: var(--color-text-secondary); - font-size: 13px; -} - -.broker-operations__toolbar select { - min-width: 240px; - padding: 8px 10px; - border: 1px solid #d8d8d8; - border-radius: var(--border-radius); - background: var(--color-surface); - color: var(--color-text); -} - -.broker-operations__toolbar select:focus-visible { - outline: 3px solid color-mix(in srgb, var(--color-primary) 35%, transparent); - outline-offset: 2px; -} - -@media (max-width: 720px) { - .broker-operations__toolbar { - align-items: stretch; - flex-direction: column; - } - .broker-operations__toolbar select { - width: 100%; - min-width: 0; - } -} -- 2.47.2 From 6debf66ac8a6061cd3f2f9c2fb53d9d4b0737291 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 20:31:52 +0300 Subject: [PATCH 16/19] docs: mark broker account sections DS migration tasks complete --- .../tasks.md | 121 +++++++++--------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/docs/features/broker-account-sections-ds-migration/tasks.md b/docs/features/broker-account-sections-ds-migration/tasks.md index 11aad48..7dcabb1 100644 --- a/docs/features/broker-account-sections-ds-migration/tasks.md +++ b/docs/features/broker-account-sections-ds-migration/tasks.md @@ -8,93 +8,94 @@ ## Task 1: BrokerAccountLayout -- [ ] Заменить `div.broker-account` на `` -- [ ] Перевести `header` + `h1` на `` + `` -- [ ] Перевести `nav` на `` + `` (оставить className callback) -- [ ] Перевести `div.broker-account__workspace` на grid `` -- [ ] Перевести `div.broker-account__content` на `` -- [ ] Удалить соответствующие CSS-selectors из `styles.css` +- [x] Заменить `div.broker-account` на `` +- [x] Перевести `header` + `h1` на `` + `` +- [x] Перевести `nav` на `` + `` (Box component={NavLink} с `aria-current="page"`) +- [x] Перевести `div.broker-account__workspace` на grid `` +- [x] Перевести `div.broker-account__content` на `` +- [x] Удалить соответствующие CSS-selectors из `styles.css` ## Task 2: BrokerAccountOverviewPage + BrokerSummary + BrokerAssetCards -- [ ] Перевести `BrokerSummary` на `` + `` -- [ ] Перевести eyebrow/label на `` -- [ ] Перевести `` значений на `` -- [ ] Перевести `
          ` cash на `` -- [ ] Перевести `BrokerAssetCards` на `` + `` + `` -- [ ] Перевести `BrokerAccountOverviewPage` container на `` -- [ ] Оставить hero-gradient без изменений (Out of scope) +- [x] Перевести `BrokerSummary` на `` + `` +- [x] Перевести eyebrow/label на `` +- [x] Перевести `` значений на `` +- [x] Перевести `
            ` cash на `` +- [x] Перевести `BrokerAssetCards` на `` + `` + `` +- [x] Перевести `BrokerAccountOverviewPage` container на `` +- [x] Перевести `

            ` на `` +- [x] Оставить hero-gradient без изменений (Out of scope) ## Task 3: BrokerOverviewSkeleton -- [ ] Заменить импорт `SkeletonBlock` на DS `` -- [ ] Перевести контейнеры на `` -- [ ] Сделать circular skeleton для портфельной карточки -- [ ] Сделать text skeleton для текстовых placeholder'ей +- [x] Заменить импорт `SkeletonBlock` на DS `` +- [x] Перевести контейнеры на `` +- [x] Сделать circular skeleton для портфельной карточки +- [x] Сделать text skeleton для текстовых placeholder'ей ## Task 4: BrokerAllocationChart (wrap only) -- [ ] Перевести `

            ` на `` -- [ ] Перевести `
            ` на `` -- [ ] Перевести `

            ` пустого состояния на `` -- [ ] Перевести `

              ` / `
            • ` легенды на `` + `` -- [ ] Перевести swatch на `` с inline-цветом в `sx` -- [ ] Перевести тексты легенды на `` / `` -- [ ] Перевести negative-список на `` -- [ ] Удалить CSS для `.broker-allocation*` (кроме inline SVG) в `styles.css` +- [x] Перевести `
              ` на `` +- [x] Перевести `
              ` на `` +- [x] Перевести `

              ` пустого состояния на `` +- [x] Перевести `

                ` / `
              • ` легенды на `` + `` +- [x] Перевести swatch на `` с inline-цветом в `sx` +- [x] Перевести тексты легенды на `` +- [x] Перевести negative-список на `` +- [x] Удалить CSS для `.broker-allocation*` в `styles.css` ## Task 5: BrokerPositionTable + PositionTicker -- [ ] Перевести вспомогательные стили на `` -- [ ] Перевести `

                ` на `` с кастомной `sx` -- [ ] Перевести alert-тексты на `` -- [ ] Перевести пагинационные `