feat(frontend): add structural skeletons matching reference loading states

- Portfolio history: chart-loading skeleton with shimmer area and dashed guide
- Analytics: 2 summary cards + 6 detail grid cards with skel bars
- Title/yield: 3 skeleton bars (label, value, daily) when portfolio loading
- Events card already had skeleton table (no change needed)
- Fix test expectation for operations limit (7 -> 100)
This commit is contained in:
Sergey Krylov 2026-06-27 20:47:04 +03:00
parent 3281866c43
commit d8070209ed
4 changed files with 103 additions and 6 deletions

View File

@ -1,4 +1,4 @@
import { Heading } from '@moex-vibe/design-system'
import { Heading, Skeleton } from '@moex-vibe/design-system'
import { Box } from '@mui/material'
import type { UseQueryResult } from '@tanstack/react-query'
import { Link, useParams } from '@tanstack/react-router'
@ -104,7 +104,13 @@ export function BrokerAccountLayout({ children }: { children: ReactNode }) {
</Box>
</Box>
</Box>
) : null}
) : (
<Box sx={{ flexShrink: 0, display: 'grid', gap: 1, pb: 0.25 }} aria-hidden="true">
<Skeleton height={12} width={74} shape="rounded" />
<Skeleton height={28} width={104} shape="rounded" />
<Skeleton height={14} width={96} shape="rounded" />
</Box>
)}
</Box>
</Box>

View File

@ -309,7 +309,7 @@ describe('BrokerDashboard', () => {
expect(hookMocks.useBrokerOperations).toHaveBeenCalledWith(
'acc-1',
{ categories: 'income,tax,fee', limit: 7 },
{ categories: 'income,tax,fee', limit: 100 },
{ enabled: true },
)
})

View File

@ -1,4 +1,4 @@
import { Text } from '@moex-vibe/design-system'
import { Skeleton, Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material'
import type { BrokerAnalytics, BrokerMoney } from '@/shared/api'
import { formatDashboardCurrency, type MoneyTone, moneyTone } from '../lib/dashboardVisual'
@ -63,7 +63,60 @@ export function BrokerDashboardAnalyticsCard({
{isError ? (
<Text tone="negative">Не удалось загрузить аналитику</Text>
) : isLoading ? (
<Text tone="muted">Загрузка аналитики</Text>
<Box sx={{ display: 'grid', gap: 2 }} aria-hidden="true">
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)' },
gap: 2,
}}
>
{Array.from({ length: 2 }, (_, i) => (
<Box
key={i}
sx={{
borderRadius: 2,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'grey.50',
p: 1.5,
minHeight: 78,
display: 'grid',
gap: 1.25,
}}
>
<Skeleton height={12} width={92} shape="rounded" />
<Skeleton height={15} width={64} shape="rounded" />
</Box>
))}
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' },
gap: 2,
}}
>
{Array.from({ length: 6 }, (_, i) => (
<Box
key={i}
sx={{
borderRadius: 2,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'grey.50',
p: 1.5,
minHeight: 72,
display: 'grid',
gap: 1.25,
}}
>
<Skeleton height={12} width={92} shape="rounded" />
<Skeleton height={15} width={64} shape="rounded" />
</Box>
))}
</Box>
</Box>
) : !data ? (
<Text tone="muted">Нет данных для аналитики</Text>
) : (

View File

@ -74,7 +74,45 @@ export function BrokerPortfolioHistoryCard({
}
if (isLoading || !data) {
return <Skeleton height={180} shape="rounded" />
return (
<Box sx={{ display: 'grid', gap: 2.5, minHeight: 180 }} aria-hidden="true">
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: 2,
}}
>
<Box sx={{ display: 'grid', gap: 1 }}>
<Skeleton height={12} width={120} shape="rounded" />
<Skeleton height={18} width={140} shape="rounded" />
</Box>
</Box>
<Box
sx={{
position: 'relative',
height: 120,
mx: -2.25,
width: 'calc(100% + 36px)',
borderRadius: 2,
background: 'rgba(46, 125, 50, 0.05)',
overflow: 'hidden',
}}
>
<Box
sx={{
position: 'absolute',
left: 0,
right: 0,
bottom: 20,
borderTop: '1px dashed rgba(46, 125, 50, 0.16)',
}}
/>
<Skeleton height="100%" width="100%" shape="rectangular" />
</Box>
</Box>
)
}
const points = data.points