feat(frontend): migrate BrokerAccountsPage and widgets to design system
This commit is contained in:
parent
62a3389bfb
commit
860bed159c
@ -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 (
|
||||
<div className="broker-accounts-page">
|
||||
<header className="broker-accounts-page__header">
|
||||
<div>
|
||||
<p className="broker-accounts-page__eyebrow">T-Bank broker overview</p>
|
||||
<h1 className="broker-accounts-page__title">Брокерские счета</h1>
|
||||
</div>
|
||||
</header>
|
||||
<Box sx={{ display: 'grid', gap: 3 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
<Text tone="secondary" variant="label">
|
||||
T-Bank broker overview
|
||||
</Text>
|
||||
<Heading level={1}>Брокерские счета</Heading>
|
||||
</Box>
|
||||
<BrokerAccountsSummary
|
||||
aggregate={{ portfolios: [], cash: [] }}
|
||||
availableCount={0}
|
||||
totalCount={0}
|
||||
isLoading
|
||||
/>
|
||||
<div className="broker-accounts-page__cards">
|
||||
<Box sx={{ display: 'grid', gap: 2 }}>
|
||||
{['1', '2', '3'].map((id) => (
|
||||
<BrokerAccountCard
|
||||
key={id}
|
||||
@ -38,8 +40,8 @@ function BrokerAccountsPageSkeleton() {
|
||||
onRetry={() => undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -53,25 +55,23 @@ export function BrokerAccountsPage() {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <p style={{ color: 'var(--color-negative)' }}>Не удалось загрузить счета</p>;
|
||||
return <Text tone="negative">Не удалось загрузить счета</Text>;
|
||||
}
|
||||
|
||||
if (safeAccounts.length === 0) {
|
||||
return (
|
||||
<div className="broker-accounts-page">
|
||||
<header className="broker-accounts-page__header">
|
||||
<div>
|
||||
<p className="broker-accounts-page__eyebrow">T-Bank broker overview</p>
|
||||
<h1 className="broker-accounts-page__title">Брокерские счета</h1>
|
||||
</div>
|
||||
</header>
|
||||
<section className="broker-accounts-empty">
|
||||
<h2>Пока нет подключённых счетов</h2>
|
||||
<p>
|
||||
После подключения T-Bank здесь появятся брокерские счета и ИИС со сводкой по капиталу.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<Box sx={{ display: 'grid', gap: 3 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
<Text tone="secondary" variant="label">
|
||||
T-Bank broker overview
|
||||
</Text>
|
||||
<Heading level={1}>Брокерские счета</Heading>
|
||||
</Box>
|
||||
<EmptyState
|
||||
title="Пока нет подключённых счетов"
|
||||
description="После подключения T-Bank здесь появятся брокерские счета и ИИС со сводкой по капиталу."
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -85,16 +85,16 @@ export function BrokerAccountsPage() {
|
||||
const aggregate = aggregateBrokerAccounts(successfulPortfolios);
|
||||
|
||||
return (
|
||||
<div className="broker-accounts-page">
|
||||
<header className="broker-accounts-page__header">
|
||||
<div>
|
||||
<p className="broker-accounts-page__eyebrow">T-Bank broker overview</p>
|
||||
<h1 className="broker-accounts-page__title">Брокерские счета</h1>
|
||||
</div>
|
||||
<p className="broker-accounts-page__caption">
|
||||
{safeAccounts.length} счетов под наблюдением
|
||||
</p>
|
||||
</header>
|
||||
<Box sx={{ display: 'grid', gap: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 2, alignItems: 'end' }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
<Text tone="secondary" variant="label">
|
||||
T-Bank broker overview
|
||||
</Text>
|
||||
<Heading level={1}>Брокерские счета</Heading>
|
||||
</Box>
|
||||
<Text tone="secondary">{safeAccounts.length} счетов под наблюдением</Text>
|
||||
</Box>
|
||||
|
||||
<BrokerAccountsSummary
|
||||
aggregate={aggregate}
|
||||
@ -103,7 +103,7 @@ export function BrokerAccountsPage() {
|
||||
isLoading={availableCount === 0 && loadingCount > 0}
|
||||
/>
|
||||
|
||||
<div className="broker-accounts-page__cards">
|
||||
<Box sx={{ display: 'grid', gap: 2 }}>
|
||||
{accountQueries.map(({ account, query }) => (
|
||||
<BrokerAccountCard
|
||||
key={account.id}
|
||||
@ -116,7 +116,7 @@ export function BrokerAccountsPage() {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 <p className="broker-allocation-bar__empty">Нет данных для распределения</p>;
|
||||
return <Text tone="muted">Нет данных для распределения</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="broker-allocation-bar">
|
||||
<div className="broker-allocation-bar__track" role="img" aria-label={title}>
|
||||
<Box sx={{ display: 'grid', gap: 1.5 }}>
|
||||
<Box
|
||||
role="img"
|
||||
aria-label={title}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
minHeight: 12,
|
||||
borderRadius: 999,
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'rgba(19,54,38,0.08)',
|
||||
}}
|
||||
>
|
||||
{positiveItems.map((item) => (
|
||||
<span
|
||||
<Box
|
||||
key={item.key}
|
||||
className="broker-allocation-bar__segment"
|
||||
style={{ width: `${item.percent}%`, background: item.color }}
|
||||
sx={{ minWidth: 8, width: `${item.percent}%`, background: item.color }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ul className="broker-allocation-bar__legend" aria-label={`${title}: легенда`}>
|
||||
</Box>
|
||||
<Box
|
||||
component="ul"
|
||||
aria-label={`${title}: легенда`}
|
||||
sx={{ listStyle: 'none', p: 0, m: 0, display: 'flex', flexWrap: 'wrap', gap: 1 }}
|
||||
>
|
||||
{positiveItems.map((item) => (
|
||||
<li className="broker-allocation-bar__legend-item" key={item.key}>
|
||||
<span
|
||||
className="broker-allocation-bar__swatch"
|
||||
style={{ background: item.color }}
|
||||
<Box
|
||||
component="li"
|
||||
key={item.key}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
borderRadius: 999,
|
||||
bgcolor: 'rgba(19,54,38,0.05)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ width: 9, height: 9, borderRadius: 999, background: item.color }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
<strong>{item.percent.toFixed(0)}%</strong>
|
||||
</li>
|
||||
<Text>{item.label}</Text>
|
||||
<Text>{item.percent.toFixed(0)}%</Text>
|
||||
</Box>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
<article className="broker-account-card broker-account-card--loading" aria-busy="true">
|
||||
<div className="broker-account-card__header">
|
||||
<div>
|
||||
<p className="broker-account-card__eyebrow">{typeLabel}</p>
|
||||
<h2 className="broker-account-card__title">{name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="broker-account-card__grid">
|
||||
<Box aria-busy="true" sx={cardSx}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1.5, alignItems: 'start' }}>
|
||||
<Box>
|
||||
<Text variant="label" tone="secondary">
|
||||
{typeLabel}
|
||||
</Text>
|
||||
<Heading level={2}>{name}</Heading>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 1.5, mt: 2 }}
|
||||
>
|
||||
{[1, 2, 3, 4].map((item) => (
|
||||
<div className="broker-account-card__stat" key={item}>
|
||||
<SkeletonBlock height={12} width="50%" />
|
||||
<SkeletonBlock height={24} width="75%" />
|
||||
</div>
|
||||
<Box key={item} sx={statSx}>
|
||||
<Skeleton height={12} width="50%" shape="text" />
|
||||
<Skeleton height={24} width="75%" shape="text" />
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
<SkeletonBlock height={16} width="100%" borderRadius={999} />
|
||||
<SkeletonBlock height={16} width="65%" />
|
||||
</article>
|
||||
</Box>
|
||||
<Skeleton height={16} width="100%" shape="rounded" />
|
||||
<Skeleton height={16} width="65%" shape="rounded" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -45,20 +69,26 @@ function BrokerAccountCardError({
|
||||
onRetry: () => void;
|
||||
}) {
|
||||
return (
|
||||
<article className="broker-account-card broker-account-card--error">
|
||||
<div className="broker-account-card__header">
|
||||
<div>
|
||||
<p className="broker-account-card__eyebrow">{brokerAccountTypeLabel(account.type)}</p>
|
||||
<h2 className="broker-account-card__title">{account.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="broker-account-card__alert" role="alert">
|
||||
<p>Не удалось загрузить данные счёта</p>
|
||||
<button className="broker-account-card__retry" type="button" onClick={onRetry}>
|
||||
Повторить
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<Box sx={cardSx}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1.5, alignItems: 'start' }}>
|
||||
<Box>
|
||||
<Text variant="label" tone="secondary">
|
||||
{brokerAccountTypeLabel(account.type)}
|
||||
</Text>
|
||||
<Heading level={2}>{account.name}</Heading>
|
||||
</Box>
|
||||
</Box>
|
||||
<Alert
|
||||
severity="error"
|
||||
action={
|
||||
<Button variant="primary" onClick={onRetry}>
|
||||
Повторить
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
Не удалось загрузить данные счёта
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -74,44 +104,77 @@ function BrokerAccountCardSuccess({
|
||||
const allocation = buildBrokerAllocation(portfolio);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="broker-account-card broker-account-card--link"
|
||||
<Box
|
||||
component={Link}
|
||||
to={`/broker/${encodeURIComponent(account.id)}`}
|
||||
sx={{
|
||||
...cardSx,
|
||||
display: 'grid',
|
||||
gap: 2.25,
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
transition: 'transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 26px 50px rgba(31,48,39,0.11)',
|
||||
borderColor: 'rgba(38,92,55,0.22)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="broker-account-card__header">
|
||||
<div>
|
||||
<p className="broker-account-card__eyebrow">{typeLabel}</p>
|
||||
<h2 className="broker-account-card__title">{account.name}</h2>
|
||||
</div>
|
||||
{openedAt ? <p className="broker-account-card__opened">Открыт {openedAt}</p> : null}
|
||||
</div>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1.5, alignItems: 'start' }}>
|
||||
<Box>
|
||||
<Text variant="label" tone="secondary">
|
||||
{typeLabel}
|
||||
</Text>
|
||||
<Heading level={2}>{account.name}</Heading>
|
||||
</Box>
|
||||
{openedAt ? (
|
||||
<Text tone="secondary" variant="caption">
|
||||
Открыт {openedAt}
|
||||
</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
<div className="broker-account-card__grid">
|
||||
<div className="broker-account-card__stat broker-account-card__stat--wide">
|
||||
<span>Стоимость</span>
|
||||
<strong>{formatBrokerMoney(portfolio.totals.portfolio)}</strong>
|
||||
</div>
|
||||
<div className="broker-account-card__stat">
|
||||
<span>За день</span>
|
||||
<strong>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ ...statSx, gridColumn: 'span 2' }}>
|
||||
<Text variant="label" tone="secondary">
|
||||
Стоимость
|
||||
</Text>
|
||||
<Text>{formatBrokerMoney(portfolio.totals.portfolio)}</Text>
|
||||
</Box>
|
||||
<Box sx={statSx}>
|
||||
<Text variant="label" tone="secondary">
|
||||
За день
|
||||
</Text>
|
||||
<Text>
|
||||
{formatBrokerSignedCurrencyValue(
|
||||
portfolio.totals.portfolio?.currency ?? 'RUB',
|
||||
portfolio.yields.daily?.value ?? null,
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="broker-account-card__stat">
|
||||
<span>Дневная динамика</span>
|
||||
<strong>{formatBrokerSignedPercent(portfolio.yields.dailyPercent)}</strong>
|
||||
</div>
|
||||
<div className="broker-account-card__stat">
|
||||
<span>Ожидаемая доходность</span>
|
||||
<strong>{formatBrokerSignedPercent(portfolio.yields.expectedPercent)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box sx={statSx}>
|
||||
<Text variant="label" tone="secondary">
|
||||
Дневная динамика
|
||||
</Text>
|
||||
<Text>{formatBrokerSignedPercent(portfolio.yields.dailyPercent)}</Text>
|
||||
</Box>
|
||||
<Box sx={statSx}>
|
||||
<Text variant="label" tone="secondary">
|
||||
Ожидаемая доходность
|
||||
</Text>
|
||||
<Text>{formatBrokerSignedPercent(portfolio.yields.expectedPercent)}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<BrokerAllocationBar title={`Структура счёта ${account.name}`} items={allocation.sectors} />
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
<section className="broker-accounts-summary broker-accounts-summary--loading">
|
||||
<div className="broker-accounts-summary__hero">
|
||||
<SkeletonBlock height={18} width="34%" />
|
||||
<SkeletonBlock height={52} width="48%" />
|
||||
<SkeletonBlock height={18} width="28%" />
|
||||
</div>
|
||||
<div className="broker-accounts-summary__metrics">
|
||||
<Box sx={{ display: 'grid', gap: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
px: 3.75,
|
||||
py: 3.5,
|
||||
borderRadius: 3,
|
||||
background: 'linear-gradient(135deg, rgba(19,54,38,0.97), rgba(26,71,47,0.95))',
|
||||
}}
|
||||
>
|
||||
<Skeleton height={18} width="34%" shape="text" />
|
||||
<Skeleton height={52} width="48%" shape="text" />
|
||||
<Skeleton height={18} width="28%" shape="text" />
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 2 }}>
|
||||
{[1, 2, 3].map((item) => (
|
||||
<div className="broker-accounts-summary__metric" key={item}>
|
||||
<SkeletonBlock height={14} width="45%" />
|
||||
<SkeletonBlock height={26} width="70%" />
|
||||
</div>
|
||||
<Box
|
||||
key={item}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gap: 1,
|
||||
p: 1.75,
|
||||
borderRadius: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'rgba(31,48,39,0.08)',
|
||||
}}
|
||||
>
|
||||
<Skeleton height={14} width="45%" shape="text" />
|
||||
<Skeleton height={26} width="70%" shape="text" />
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="broker-accounts-summary" aria-label="Общая сводка по счетам">
|
||||
<div className="broker-accounts-summary__hero">
|
||||
<div>
|
||||
<p className="broker-accounts-summary__eyebrow">Финансовый обзор</p>
|
||||
<h2 className="broker-accounts-summary__title">Счета в одном кадре</h2>
|
||||
</div>
|
||||
<div className="broker-accounts-summary__status">
|
||||
<span>{totalCount} счетов</span>
|
||||
{availableCount !== totalCount ? (
|
||||
<span>
|
||||
Доступно по {availableCount} из {totalCount} счетов
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Box component="section" sx={{ display: 'grid', gap: 2.5 }} aria-label="Общая сводка по счетам">
|
||||
<Box
|
||||
sx={{
|
||||
px: 3.75,
|
||||
py: 3.5,
|
||||
borderRadius: 3,
|
||||
background: 'linear-gradient(135deg, rgba(19,54,38,0.97), rgba(26,71,47,0.95))',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 2, alignItems: 'start' }}>
|
||||
<Box>
|
||||
<Box sx={{ fontSize: 12, lineHeight: 1.33, color: 'rgba(255,255,255,0.55)' }}>
|
||||
Финансовый обзор
|
||||
</Box>
|
||||
<Box sx={{ fontSize: 22, fontWeight: 700, lineHeight: 1.27, color: '#fff' }}>
|
||||
Счета в одном кадре
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gap: 0.25, textAlign: 'right' }}>
|
||||
<Box sx={{ fontSize: 13, lineHeight: 1.53, color: 'rgba(255,255,255,0.7)' }}>
|
||||
{totalCount} счетов
|
||||
</Box>
|
||||
{availableCount !== totalCount ? (
|
||||
<Box sx={{ fontSize: 12, lineHeight: 1.33, color: 'rgba(255,255,255,0.45)' }}>
|
||||
Доступно по {availableCount} из {totalCount} счетов
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<div className="broker-accounts-summary__currency-grid">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))',
|
||||
gap: 2.5,
|
||||
}}
|
||||
>
|
||||
{aggregate.portfolios.map((portfolioSummary) => {
|
||||
const allocation = buildBrokerAllocation({
|
||||
account: {
|
||||
@ -114,51 +153,68 @@ export function BrokerAccountsSummary({
|
||||
});
|
||||
|
||||
return (
|
||||
<article
|
||||
className="broker-accounts-summary__currency-card"
|
||||
<Box
|
||||
key={portfolioSummary.currency}
|
||||
sx={{
|
||||
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)',
|
||||
display: 'grid',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<div className="broker-accounts-summary__currency-header">
|
||||
<span className="broker-accounts-summary__currency">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box component="span" sx={{ fontSize: 16, fontWeight: 600, lineHeight: 1.25 }}>
|
||||
{portfolioSummary.currency}
|
||||
</span>
|
||||
<strong className="broker-accounts-summary__total">
|
||||
</Box>
|
||||
<Box component="span" sx={{ fontSize: 22, fontWeight: 700, lineHeight: 1.27 }}>
|
||||
{formatBrokerCurrencyValue(portfolioSummary.currency, portfolioSummary.total)}
|
||||
</strong>
|
||||
</div>
|
||||
<dl className="broker-accounts-summary__metrics">
|
||||
<div className="broker-accounts-summary__metric">
|
||||
<dt>За день</dt>
|
||||
<dd>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 1.5 }}
|
||||
>
|
||||
<Box sx={{ display: 'grid', gap: 0.25 }}>
|
||||
<Text variant="label" tone="secondary">
|
||||
За день
|
||||
</Text>
|
||||
<Text>
|
||||
{formatBrokerSignedCurrencyValue(
|
||||
portfolioSummary.currency,
|
||||
portfolioSummary.daily,
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="broker-accounts-summary__metric">
|
||||
<dt>Динамика</dt>
|
||||
<dd>{formatBrokerSignedPercent(portfolioSummary.dailyPercent)}</dd>
|
||||
</div>
|
||||
<div className="broker-accounts-summary__metric">
|
||||
<dt>Свободные деньги</dt>
|
||||
<dd>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gap: 0.25 }}>
|
||||
<Text variant="label" tone="secondary">
|
||||
Динамика
|
||||
</Text>
|
||||
<Text>{formatBrokerSignedPercent(portfolioSummary.dailyPercent)}</Text>
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gap: 0.25 }}>
|
||||
<Text variant="label" tone="secondary">
|
||||
Свободные деньги
|
||||
</Text>
|
||||
<Text>
|
||||
{formatBrokerCurrencyValue(
|
||||
portfolioSummary.currency,
|
||||
aggregate.cash.find((cash) => cash.currency === portfolioSummary.currency)
|
||||
?.value ?? 0,
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<BrokerAllocationBar
|
||||
title={`Распределение активов ${portfolioSummary.currency}`}
|
||||
items={allocation.sectors}
|
||||
/>
|
||||
</article>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
94
docs/features/broker-accounts-page-migration/plan.md
Normal file
94
docs/features/broker-accounts-page-migration/plan.md
Normal file
@ -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 `<Box>` + `<Text>`.
|
||||
|
||||
- Empty: `<p className="broker-allocation-bar__empty">` → `<Text tone="muted">`
|
||||
- Track: `<div className="broker-allocation-bar__track">` → `<Box>` with `sx`
|
||||
- Segment: `<span style={{ width, background }}>` → `<Box>` with `sx`
|
||||
- Legend: `<ul>` / `<li>` → `<Box>` with `sx` flex layout
|
||||
- Swatch: `<span>` with inline style → `<Box>` 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: `<SkeletonBlock>` → `<Skeleton>` from DS
|
||||
- Eyebrow: `<p className="broker-account-card__eyebrow">` → `<Text variant="label" tone="secondary">`
|
||||
- Title: `<h2 className="broker-account-card__title">` → `<Heading level={2}>`
|
||||
- Stat labels/values: `<span>` + `<strong>` → `<Text variant="label" tone="secondary">` + `<Text>`
|
||||
- Card wrapper: `<article className="broker-account-card broker-account-card--link">` → `<Surface>` with `component={Link}` or `<Box>` with `sx`
|
||||
- Error: `<article className="broker-account-card broker-account-card--error">` → `<Surface>` + `<Alert>` + `<Button>`
|
||||
- Card header layout: `<div className="broker-account-card__header">` → `<Box>` with flex `sx`
|
||||
- Card grid: `<div className="broker-account-card__grid">` → `<Box>` with grid `sx`
|
||||
- Card stat: `<div className="broker-account-card__stat">` → `<Box>` with grid `sx`
|
||||
|
||||
## Task 3: BrokerAccountsSummary
|
||||
|
||||
Replace skeleton, stats, typography with DS equivalents.
|
||||
|
||||
- Skeleton: `<SkeletonBlock>` → `<Skeleton>`
|
||||
- Eyebrow: `<p className="broker-accounts-summary__eyebrow">` → `<Text variant="label" tone="secondary">`
|
||||
- Title (serif): keep as `<h2>` with CSS class (DS Heading не использует serif, теряется brand-стиль)
|
||||
- Status counts: `<span>` → `<Text>`
|
||||
- Currency label: `<p className="broker-accounts-summary__currency">` → `<Text variant="label" tone="secondary">`
|
||||
- Currency totals: `<strong className="broker-accounts-summary__total">` — keep as `<strong>` with CSS (serif)
|
||||
- Metrics: `<dl>`/`<dt>`/`<dd>` → `<Metric>` for each stat
|
||||
- Metric layout grid: `<div className="broker-accounts-summary__metrics">` → `<Box>` with grid `sx`
|
||||
|
||||
The hero section (`broker-accounts-summary__hero`) keeps its CSS classes — custom gradient, not replaceable.
|
||||
|
||||
## Task 4: BrokerAccountsPage
|
||||
|
||||
Replace page-level layout, typography, empty/error states.
|
||||
|
||||
- Page grid: `<div className="broker-accounts-page">` → `<Box>` with grid `sx`
|
||||
- Header: `<header className="broker-accounts-page__header">` → `<Box>` with flex `sx`
|
||||
- Eyebrow: `<p className="broker-accounts-page__eyebrow">` → `<Text variant="label" tone="secondary">`
|
||||
- Title: `<h1 className="broker-accounts-page__title">` → `<Heading level={1}>` (note: serif lost)
|
||||
- Caption: `<p className="broker-accounts-page__caption">` → `<Text tone="secondary">`
|
||||
- Cards grid: `<div className="broker-accounts-page__cards">` → `<Box>` with grid `sx`
|
||||
- Empty: `<section className="broker-accounts-empty">` → `<EmptyState>`
|
||||
- Error: `<p style={{ color: ... }}>` → `<Text tone="negative">`
|
||||
|
||||
## Task 5: Clean up orphaned CSS
|
||||
|
||||
After all migrations, remove from `styles.css`:
|
||||
- `.broker-accounts-page`, `.broker-accounts-page__header`, `.broker-accounts-page__eyebrow`, `.broker-accounts-page__title`, `.broker-accounts-page__caption`, `.broker-accounts-page__cards`
|
||||
- `.broker-accounts-empty`
|
||||
- `.broker-account-card`, `.broker-account-card--link`, `.broker-account-card--loading`, `.broker-account-card--error`, `.broker-account-card__header`, `.broker-account-card__eyebrow`, `.broker-account-card__title`, `.broker-account-card__opened`, `.broker-account-card__grid`, `.broker-account-card__stat`, `.broker-account-card__stat--wide`, `.broker-account-card__alert`, `.broker-account-card__retry`
|
||||
- `.broker-allocation-bar`, `.broker-allocation-bar__track`, `.broker-allocation-bar__segment`, `.broker-allocation-bar__legend`, `.broker-allocation-bar__legend-item`, `.broker-allocation-bar__swatch`, `.broker-allocation-bar__empty`
|
||||
- `.broker-accounts-summary__eyebrow`, `.broker-accounts-summary__currency`, `.broker-accounts-summary__total`, `.broker-accounts-summary__metrics`, `.broker-accounts-summary__metric`
|
||||
- `.broker-accounts-summary__currency-card` and its children (header, currency, total, metrics, metric)
|
||||
- `.broker-accounts-summary__status` and children
|
||||
|
||||
Keep: `.broker-accounts-summary`, `.broker-accounts-summary--loading`, `.broker-accounts-summary__hero`, `.broker-accounts-summary__title` (serif), shared `.broker-accounts-summary__eyebrow` shared rule (hero-specific), `.broker-accounts-summary__currency-grid`
|
||||
54
docs/features/broker-accounts-page-migration/spec.md
Normal file
54
docs/features/broker-accounts-page-migration/spec.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Broker Accounts Page — Design System Migration
|
||||
|
||||
## Status
|
||||
|
||||
Draft
|
||||
|
||||
## Goal
|
||||
|
||||
Мигрировать BrokerAccountsPage, BrokerAccountsSummary, BrokerAccountCard и BrokerAllocationBar на компоненты `@moex-vibe/design-system`, заменив legacy CSS и inline-стили на DS-компоненты и MUI layout utilities.
|
||||
|
||||
## Scope
|
||||
|
||||
- `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` — удалить CSS-классы и переменные, оставшиеся без потребителей
|
||||
|
||||
### DS-компоненты для внедрения
|
||||
|
||||
| HTML / legacy | DS-компонент |
|
||||
|-------------|-------------|
|
||||
| `<h1>`, `<h2>` | `<Heading>` |
|
||||
| `<p>`, `<span>` label | `<Text>` |
|
||||
| `<span>` eyebrow | `<Text variant="label" tone="secondary">` |
|
||||
| `<dl>`/`<dt>`/`<dd>` stat | `<Metric>` |
|
||||
| `div.broker-accounts-empty` | `<EmptyState>` |
|
||||
| `div.broker-account-card__alert` + button | `<Alert severity="error">` + `<Button variant="primary">` |
|
||||
| `div.broker-accounts-summary__hero` title/text | `<Heading>` + `<Text>` |
|
||||
| `.broker-account-card` card | `<Surface>` or `<Card>` |
|
||||
| `<SkeletonBlock>` | `<Skeleton>` from DS |
|
||||
| `div.broker-accounts-page` layout | `<Box>` with `sx` |
|
||||
| `div.broker-accounts-page__header` | `<Box>` with flex |
|
||||
| `div.broker-accounts-page__cards` | `<Box>` with grid |
|
||||
| `p.broker-allocation-bar__empty` | `<Text tone="muted">` |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Hero gradient section visual identity (too custom, no DS equivalent)
|
||||
- Serif font family on titles (DS uses Inter)
|
||||
- Allocation bar colors from ALLOCATION_CONFIG (business config, not tokens)
|
||||
- Добавление новых тестов (entity-тесты существуют, page-тесты — новая фича)
|
||||
- Рекламные/декоративные элементы (градиенты, кастомные скругления)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] BrokerAccountsPage не содержит CSS-классов из styles.css; `<h1>` / `<p>` заменены на `<Heading>` / `<Text>`
|
||||
- [ ] BrokerAccountsPage empty state использует `<EmptyState>`, error — `<Text tone="negative">`
|
||||
- [ ] BrokerAccountsSummary skeleton использует DS `<Skeleton>`, stats используют `<Metric>`, eyebrow/title используют `<Text>`/`<Heading>`
|
||||
- [ ] BrokerAccountCard skeleton/error/success используют DS `<Skeleton>`, `<Alert>`, `<Button>`, `<Heading>`, `<Text>`, `<Metric>`, `<Surface>`
|
||||
- [ ] BrokerAllocationBar использует `<Box>` + `<Text>` вместо div/p с CSS-классами
|
||||
- [ ] ESLint allowlist не нарушен
|
||||
- [ ] `npm run test:frontend && npm run lint -w apps/frontend && npm run build:frontend` проходят
|
||||
- [ ] CSS-классы, оставшиеся без потребителей, удалены из styles.css
|
||||
42
docs/features/broker-accounts-page-migration/tasks.md
Normal file
42
docs/features/broker-accounts-page-migration/tasks.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Broker Accounts Page — Tasks
|
||||
|
||||
## Tasks Checklist
|
||||
|
||||
- [x] **Task 1: BrokerAllocationBar**
|
||||
- [x] Replace `p.broker-allocation-bar__empty` with `<Text tone="muted">`
|
||||
- [x] Replace `div.broker-allocation-bar` with `<Box>` container
|
||||
- [x] Replace track and segment divs/spans with `<Box>` components with MUI `sx` styling
|
||||
- [x] Replace legend and swatch elements with `<Box>` and `<Text>` components
|
||||
- [x] Verify BrokerAllocationBar compile state
|
||||
|
||||
- [x] **Task 2: BrokerAccountCard**
|
||||
- [x] Replace `<SkeletonBlock>` with `<Skeleton>` from `@moex-vibe/design-system`
|
||||
- [x] Replace eyebrow with `<Text variant="label" tone="secondary">`
|
||||
- [x] Replace title `h2` with `<Heading level={2}>`
|
||||
- [x] Replace error alert block with `<Alert severity="error">` and a `<Button>`
|
||||
- [x] Replace layout grids, stats, and card component with `<Box>` with `sx`
|
||||
- [x] Set component on `<Box>` to `Link` from `react-router-dom` for card wrapper
|
||||
- [x] Ensure stat elements use proper Text styles and spacing
|
||||
|
||||
- [x] **Task 3: BrokerAccountsSummary**
|
||||
- [x] Replace skeleton placeholders with `<Skeleton>` from DS
|
||||
- [x] Replace headers and subheaders with proper typography
|
||||
- [x] Convert layout container to `<Box>` component with `sx` positioning
|
||||
- [x] Replace metrics detail grid with Box layout using custom margins and spacing
|
||||
- [x] Keep custom hero layout styles and replace text styling inside the hero with `<Box>` and custom variables to ensure colors map correctly
|
||||
|
||||
- [x] **Task 4: BrokerAccountsPage**
|
||||
- [x] Replace layout containers and headers with `<Box>` with `sx` flex/grid
|
||||
- [x] Replace main title with `<Heading level={1}>` and subtitle with `<Text>`
|
||||
- [x] Replace empty state section with `<EmptyState>` component
|
||||
- [x] Replace error styling with `<Text tone="negative">`
|
||||
|
||||
- [x] **Task 5: Clean up orphaned CSS**
|
||||
- [x] Find and delete unused CSS selectors in `apps/frontend/src/styles.css`
|
||||
- [x] Verify reduced motion and max-width 720px responsive rules are cleanly updated without leaving dead rules
|
||||
- [x] Ensure we only leave rules for pages and components that have not yet been migrated
|
||||
|
||||
- [x] **Task 6: Verification**
|
||||
- [x] Run `npm run test:frontend` and ensure all 111 tests pass
|
||||
- [x] Run `npm run lint -w apps/frontend` to verify ESLint compatibility
|
||||
- [x] Run `npm run build:frontend` to compile production assets and verify type safety
|
||||
Loading…
x
Reference in New Issue
Block a user