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/broker-account/ui/BrokerAccountOverviewPage.tsx b/apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx index e7b4b79..10cf30a 100644 --- a/apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx +++ b/apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx @@ -1,4 +1,6 @@ import { Link } from 'react-router-dom'; +import { Box } from '@mui/material'; +import { Text } from '@moex-vibe/design-system'; import { useBrokerOperations } from '@/entities/broker-operation'; import { useBrokerAccountContext } from '@/widgets/broker-account-layout'; import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart'; @@ -11,16 +13,22 @@ export function BrokerAccountOverviewPage() { if (portfolio.isLoading) return ; if (portfolio.error || !portfolio.data) { - return

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

; + return ( + + Не удалось загрузить сводку счёта + + ); } return ( -
+ {operations.error ? ( -

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

+ + Не удалось загрузить последние операции + ) : ( )} -
+ ); } 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/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' ? 'Не удалось загрузить акции' : 'Не удалось загрузить облигации'} -

-
+ + ); } diff --git a/apps/frontend/src/pages/home/ui/HomePage.tsx b/apps/frontend/src/pages/home/ui/HomePage.tsx index a0f2c52..9f7f049 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'; +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/pages/login/LoginPage.test.tsx b/apps/frontend/src/pages/login/LoginPage.test.tsx index 85bd7ec..94b23b1 100644 --- a/apps/frontend/src/pages/login/LoginPage.test.tsx +++ b/apps/frontend/src/pages/login/LoginPage.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 { Routes, Route } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; @@ -72,6 +72,8 @@ describe('LoginPage', () => { await user.type(screen.getByPlaceholderText('••••••••'), 'password'); 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/login/ui/LoginPage.tsx b/apps/frontend/src/pages/login/ui/LoginPage.tsx index e189b32..98b4cb4 100644 --- a/apps/frontend/src/pages/login/ui/LoginPage.tsx +++ b/apps/frontend/src/pages/login/ui/LoginPage.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 LoginPage() { @@ -28,84 +30,44 @@ export function LoginPage() { } return ( -
-

Вход

-
- {error &&
{error}
} -
- - setEmail(e.target.value)} - required - style={inputStyle} - placeholder="email@example.com" - /> -
-
- - setPassword(e.target.value)} - required - style={inputStyle} - placeholder="••••••••" - /> -
- -

+ + + Вход + + + {error && {error}} + setEmail(e.target.value)} + required + placeholder="email@example.com" + /> + setPassword(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', -}; 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', -}; 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 ( -
- ); -} diff --git a/apps/frontend/src/shared/ui/TableSkeleton.tsx b/apps/frontend/src/shared/ui/TableSkeleton.tsx index 5190a2e..34e94bd 100644 --- a/apps/frontend/src/shared/ui/TableSkeleton.tsx +++ b/apps/frontend/src/shared/ui/TableSkeleton.tsx @@ -1,4 +1,4 @@ -import { SkeletonBlock } from '@/shared/ui/SkeletonBlock'; +import { Skeleton } from '@moex-vibe/design-system'; const tdStyle = { borderBottom: '1px solid #eeeeee', @@ -15,7 +15,7 @@ export function TableSkeleton({ rows = 5, columns }: { rows?: number; columns: C {columns.map((col, j) => ( - + ))} 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/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'; diff --git a/apps/frontend/src/styles.css b/apps/frontend/src/styles.css index 343b42d..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,425 +88,7 @@ a { z-index: 1; } -.broker-account__workspace { - display: grid; - grid-template-columns: minmax(150px, 190px) minmax(0, 1fr); - gap: 24px; -} - -.broker-account__navigation { - display: flex; - flex-direction: column; - gap: 4px; -} - -.broker-account__link { - padding: 10px 12px; - border-radius: var(--border-radius); - color: var(--color-text-secondary); -} - -.broker-account__link.is-active, -.broker-account__link[aria-current='page'] { - background: color-mix(in srgb, var(--color-primary) 10%, transparent); - color: var(--color-primary); - font-weight: 700; -} - -.broker-account__link:focus-visible { - outline: 3px solid var(--color-primary); - outline-offset: 2px; -} - -.broker-account__content { - min-width: 0; -} - -.broker-overview { - display: grid; - gap: 24px; -} - -.broker-overview__summary, -.broker-overview__assets { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.broker-overview__card, -.broker-allocation { - padding: 16px; - border: 1px solid #e0e0e0; - border-radius: var(--border-radius); - background: var(--color-surface); -} - -.broker-overview__card { - display: grid; - align-content: start; - gap: 8px; -} - -.broker-overview__label, -.broker-overview__card > span { - color: var(--color-text-secondary); -} - -.broker-overview__total { - font-size: 24px; -} - -.broker-overview__cash, -.broker-allocation ul { - list-style: none; - display: grid; - gap: 8px; -} - -.broker-overview__cash li { - display: flex; - justify-content: space-between; - gap: 12px; -} - -.broker-overview__asset-link { - color: var(--color-text); -} - -.broker-overview__asset-title { - color: var(--color-primary); - font-size: 18px; -} - -.broker-overview__asset-link:focus-visible { - outline: 3px solid color-mix(in srgb, var(--color-primary) 35%, transparent); - outline-offset: 2px; -} - -.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); -} - -.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,112 +96,4 @@ 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; - 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; - } -} 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-account-layout/ui/BrokerAccountLayout.tsx b/apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx index 5d34f26..4bcce93 100644 --- a/apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx +++ b/apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx @@ -1,4 +1,6 @@ import { NavLink, Outlet, useOutletContext, useParams } from 'react-router-dom'; +import { Box } from '@mui/material'; +import { Heading } from '@moex-vibe/design-system'; import { useBrokerPortfolio } from '@/entities/broker-account'; export type BrokerAccountContext = { @@ -10,40 +12,87 @@ export function useBrokerAccountContext() { return useOutletContext(); } +const links = [ + { to: '', label: 'Обзор', end: true }, + { to: '/shares', label: 'Акции' }, + { to: '/bonds', label: 'Облигации' }, + { to: '/operations', label: 'Операции' }, +]; + export function BrokerAccountLayout() { const { accountId = '' } = useParams(); const portfolio = useBrokerPortfolio(accountId); const basePath = `/broker/${encodeURIComponent(accountId)}`; const context: BrokerAccountContext = { accountId, portfolio }; - const linkClassName = ({ isActive }: { isActive: boolean }) => - `broker-account__link${isActive ? ' is-active' : ''}`; return ( -
-
-

{portfolio.data?.account.name || 'Брокерский счёт'}

-
+ + + {portfolio.data?.account.name || 'Брокерский счёт'} + -
- + + + {links.map((link) => ( + + {link.label} + + ))} + -
+ -
-
-
+ + + ); } 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/apps/frontend/src/widgets/broker-allocation-chart/ui/BrokerAllocationChart.tsx b/apps/frontend/src/widgets/broker-allocation-chart/ui/BrokerAllocationChart.tsx index 232825d..8efdef9 100644 --- a/apps/frontend/src/widgets/broker-allocation-chart/ui/BrokerAllocationChart.tsx +++ b/apps/frontend/src/widgets/broker-allocation-chart/ui/BrokerAllocationChart.tsx @@ -1,4 +1,6 @@ import type { BrokerPortfolio } from '@/shared/api/responses'; +import { Box } from '@mui/material'; +import { Text } from '@moex-vibe/design-system'; import { buildBrokerAllocation } from '@/entities/broker-position'; import { formatBrokerCurrencyValue } from '@/shared/lib/formatters'; @@ -26,7 +28,7 @@ export function BrokerAllocationChart({ portfolio }: { portfolio: BrokerPortfoli }); return ( -
+ Структура брокерского портфеля {arcs.map((sector) => ( @@ -44,40 +46,50 @@ export function BrokerAllocationChart({ portfolio }: { portfolio: BrokerPortfoli /> ))} -
+ {sectors.length === 0 ? ( -

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

+ Нет данных для распределения ) : ( -
    + {sectors.map((sector) => ( -
  • - +
  • +
    + ))} -
+
)} {negative.length > 0 && ( -
    {negative.map((item) => ( -
  • + {item.label}: отрицательное значение{' '} {formatBrokerCurrencyValue(currency, item.value)} -
  • + ))} -
+ )} -
-
+ + ); } 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 && ( -
- + + )} -
+ )}
); diff --git a/apps/frontend/src/widgets/broker-overview/ui/BrokerAssetCards.tsx b/apps/frontend/src/widgets/broker-overview/ui/BrokerAssetCards.tsx index 662dbe9..5f1dc51 100644 --- a/apps/frontend/src/widgets/broker-overview/ui/BrokerAssetCards.tsx +++ b/apps/frontend/src/widgets/broker-overview/ui/BrokerAssetCards.tsx @@ -1,4 +1,6 @@ import { Link } from 'react-router-dom'; +import { Box } from '@mui/material'; +import { Text } from '@moex-vibe/design-system'; import type { BrokerMoney, BrokerPortfolio } from '@/shared/api/responses'; import { formatBrokerMoney, pluralize } from '@/shared/lib/formatters'; @@ -11,6 +13,16 @@ function formatAllocationPercent(value: number | null) { return value === null ? '\u2014' : `${value.toFixed(1)}%`; } +const cardSx = { + p: 2, + border: '1px solid', + borderColor: 'divider', + borderRadius: 2, + bgcolor: 'surface.default', + display: 'grid', + gap: 1, +}; + export function BrokerAssetCards({ accountId, portfolio, @@ -37,23 +49,23 @@ export function BrokerAssetCards({ ]; return ( -
+ {cards.map((card) => ( - - {card.label} - - {card.count} {card.countLabel} - - {formatBrokerMoney(card.value)} - - {formatAllocationPercent(allocationPercent(card.value, portfolio.totals.portfolio))} - + + + + {card.label} + + + {card.count} {card.countLabel} + + {formatBrokerMoney(card.value)} + + {formatAllocationPercent(allocationPercent(card.value, portfolio.totals.portfolio))} + + ))} -
+ ); } diff --git a/apps/frontend/src/widgets/broker-overview/ui/BrokerOverviewSkeleton.tsx b/apps/frontend/src/widgets/broker-overview/ui/BrokerOverviewSkeleton.tsx index 984978a..b9a8964 100644 --- a/apps/frontend/src/widgets/broker-overview/ui/BrokerOverviewSkeleton.tsx +++ b/apps/frontend/src/widgets/broker-overview/ui/BrokerOverviewSkeleton.tsx @@ -1,30 +1,55 @@ -import { SkeletonBlock } from '@/shared/ui/SkeletonBlock'; +import { Box } from '@mui/material'; +import { Skeleton } from '@moex-vibe/design-system'; export function BrokerOverviewSkeleton() { return ( -
-
+ + {[1, 2].map((item) => ( -
- - - -
+ + + + + ))} -
-
- - -
-
+ + + + + + + + {[1, 2].map((item) => ( -
- - - -
+ + + + + ))} -
-
+ + ); } diff --git a/apps/frontend/src/widgets/broker-overview/ui/BrokerSummary.tsx b/apps/frontend/src/widgets/broker-overview/ui/BrokerSummary.tsx index fd7dde4..d87e458 100644 --- a/apps/frontend/src/widgets/broker-overview/ui/BrokerSummary.tsx +++ b/apps/frontend/src/widgets/broker-overview/ui/BrokerSummary.tsx @@ -1,3 +1,5 @@ +import { Box } from '@mui/material'; +import { Text } from '@moex-vibe/design-system'; import type { BrokerPortfolio } from '@/shared/api/responses'; import { formatBrokerMoney as formatMoney, @@ -6,31 +8,61 @@ import { export function BrokerSummary({ portfolio }: { portfolio: BrokerPortfolio }) { return ( -
-
- Стоимость портфеля - + + + + Стоимость портфеля + + {formatMoney(portfolio.totals.portfolio)} - - За день: {formatMoney(portfolio.yields.daily)} - Дневная доходность: {formatPercent(portfolio.yields.dailyPercent)} - Ожидаемая доходность: {formatPercent(portfolio.yields.expectedPercent)} -
-
- Денежный остаток + + За день: {formatMoney(portfolio.yields.daily)} + Дневная доходность: {formatPercent(portfolio.yields.dailyPercent)} + Ожидаемая доходность: {formatPercent(portfolio.yields.expectedPercent)} + + + + Денежный остаток + {portfolio.cash.length === 0 ? ( - Нет денежных остатков + Нет денежных остатков ) : ( -
    + {portfolio.cash.map((money, index) => ( -
  • - {money.currency} - {formatMoney(money)} -
  • + + {money.currency} + + {formatMoney(money)} + + ))} -
+
)} -
-
+ + ); } diff --git a/apps/frontend/src/widgets/broker-positions-table/ui/BrokerPositionTable.tsx b/apps/frontend/src/widgets/broker-positions-table/ui/BrokerPositionTable.tsx index 881c37f..5422d18 100644 --- a/apps/frontend/src/widgets/broker-positions-table/ui/BrokerPositionTable.tsx +++ b/apps/frontend/src/widgets/broker-positions-table/ui/BrokerPositionTable.tsx @@ -1,44 +1,36 @@ import type { BrokerPositionsPage as BrokerPositionsPageData } from '@/shared/api/responses'; -import { TableSkeleton } from '@/shared/ui/TableSkeleton'; +import { Box } from '@mui/material'; +import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system'; import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters'; import { PositionTicker } from './PositionTicker'; -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: 'right', +} 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 tdSxLeft = { + ...tdSx, + textAlign: 'left', +} as const; function formatQuantity(value: number | null | undefined) { return value == null ? '-' : value.toLocaleString('ru-RU'); @@ -69,127 +61,127 @@ export function BrokerPositionTable({ return (
-
-

+ {title} -

-
- - : '←'} + + {pageNumber} - - -
-
+ {isFetching ? : '→'} + + + {isLoading ? ( -
- - - - - - - - - - - -
+ + + + + Тикер - + + Название - + + Количество - + + Цена - + + Стоимость -
-
+ + + + + {[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} + ); } diff --git a/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx b/apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx index 240f04b..e1b75d4 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'; +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/broker-account-sections-ds-migration/plan.md b/docs/features/broker-account-sections-ds-migration/plan.md new file mode 100644 index 0000000..dd016b7 --- /dev/null +++ b/docs/features/broker-account-sections-ds-migration/plan.md @@ -0,0 +1,144 @@ +# Дизайн-системный рефакторинг брокерских страниц — 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:** Удалить legacy-стили из страниц и компонентов брокерского раздела: CSS-классы, +`inline styles`, `SkeletonBlock`, `TableSkeleton` → перевести на `@moex-vibe/design-system`. + +**Architecture:** Бизнес-логика, контракты и маршрутизация меняться не будут. +Градиенты героических секций, которые не имеют DS-эквивалента, сохраняются как `sx`-инлайн-стили. +SVG-диаграмма (`BrokerAllocationChart`) сохраняется как есть, меняются только обёртки. + +**Tech Stack:** React 18, MUI 6.5, `@moex-vibe/design-system` 0.1.0, CSS custom properties. + +**Связанные документы:** + +- Epic: `docs/epics/BrokerPortfolio.md` +- Функциональная базовая фича: `docs/features/broker-account-sections/spec.md` +- Research: `docs/research/2026-06-18-broker-account-sections.md` + +--- + +## Task 1: BrokerAccountLayout + +Заменить layout shell на DS. + +- `div.broker-account` → `` +- `header.broker-account__header` → `` flex +- `h1` → `` +- `nav.broker-account__navigation` → `` +- `NavLink` классы → при оставлении `className` callback (уникальный prefix для active state), но в первую очередь сменить на `Box` + `