From 860bed159c2bd908919d379cb6cc0f2fdad89b6a Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 21 Jun 2026 17:42:05 +0300 Subject: [PATCH] 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) => ( -
- -
+ + ); } 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: `
    ` → `` + `` + `