fix(frontend): align badges, pagination, chart labels with reference

- Replace MUI Chip in events card with custom Badge matching reference
  colors (blue coupon, green dividend, amber maturity, red tax/fee)
- Add working pagination (useState, 7 per page, ←/→ handlers)
- Increase events API limit to 100 for multi-page support
- Fix chart month labels: left-align first, right-align last
  to prevent overflow beyond container edges
This commit is contained in:
Sergey Krylov 2026-06-27 20:16:40 +03:00
parent d21e2411b8
commit 8b24d8b82b
6 changed files with 401 additions and 128 deletions

View File

@ -6,7 +6,7 @@ import { createContext, type ReactNode } from 'react'
import { useBrokerPortfolio } from '@/entities/broker-account' import { useBrokerPortfolio } from '@/entities/broker-account'
import type { BrokerPortfolio } from '@/shared/api' import type { BrokerPortfolio } from '@/shared/api'
import { formatBrokerMoney, formatBrokerPercent } from '@/shared/lib/formatters' import { formatBrokerMoney, formatBrokerPercent } from '@/shared/lib/formatters'
import { moneyTone, moneyToneToColor } from '@/widgets/broker-dashboard/lib/dashboardVisual' import { moneyTone } from '@/widgets/broker-dashboard/lib/dashboardVisual'
const baseLinkStyle: React.CSSProperties = { const baseLinkStyle: React.CSSProperties = {
padding: '10px 14px', padding: '10px 14px',
@ -45,27 +45,67 @@ export function BrokerAccountLayout({ children }: { children: ReactNode }) {
return ( return (
<BrokerAccountContext.Provider value={{ accountId, portfolio }}> <BrokerAccountContext.Provider value={{ accountId, portfolio }}>
<Box sx={{ display: 'grid', gap: 3 }}> <Box sx={{ display: 'grid', gap: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> <Box
<Heading level={1}>{portfolio.data?.account.name || 'Брокерский счёт'}</Heading> sx={{
{portfolio.data ? ( display: 'flex',
<Box sx={{ textAlign: 'right', flexShrink: 0 }}> alignItems: 'flex-end',
<Box justifyContent: 'space-between',
sx={{ gap: 3,
fontWeight: 700, minHeight: 72,
fontSize: 16, }}
lineHeight: 1.25, >
color: moneyToneToColor( <Box
moneyTone(portfolio.data.yields.expectedPercent as number | null), sx={{
), display: 'flex',
}} alignItems: 'flex-end',
> gap: 2.5,
{formatBrokerPercent(portfolio.data.yields.expectedPercent as number | null)} minWidth: 0,
flex: '1 1 auto',
}}
>
<Heading
level={1}
style={{ fontSize: 'clamp(28px, 4vw, 42px)', fontWeight: 700, lineHeight: 1.05 }}
>
{portfolio.data?.account.name || 'Брокерский счёт'}
</Heading>
{portfolio.data ? (
<Box sx={{ flexShrink: 0, display: 'grid', gap: 0.25, pb: 0.25 }}>
<Box sx={{ fontSize: 11, fontWeight: 700, color: 'text.secondary', lineHeight: 1 }}>
Доходность
</Box>
<Box
sx={{
fontWeight: 700,
fontSize: 22,
lineHeight: 1,
color:
moneyTone(portfolio.data.yields.expectedPercent as number | null) ===
'positive'
? 'success.main'
: 'error.main',
}}
>
{formatBrokerPercent(portfolio.data.yields.expectedPercent as number | null)}
</Box>
<Box
sx={{
fontSize: 12,
lineHeight: 1.25,
color:
moneyTone(portfolio.data.yields.daily as number | null) === 'positive'
? 'success.main'
: 'text.secondary',
}}
>
За день:{' '}
<Box component="span" sx={{ fontWeight: 700 }}>
{formatBrokerMoney(portfolio.data.yields.daily)}
</Box>
</Box>
</Box> </Box>
<Box sx={{ fontSize: 12, lineHeight: 1.5, color: 'text.secondary' }}> ) : null}
За день: {formatBrokerMoney(portfolio.data.yields.daily)} </Box>
</Box>
</Box>
) : null}
</Box> </Box>
<Box <Box

View File

@ -20,7 +20,7 @@ export function BrokerDashboard({
const eventsOps = useBrokerOperations( const eventsOps = useBrokerOperations(
accountId, accountId,
{ categories: 'income,tax,fee', limit: 7 }, { categories: 'income,tax,fee', limit: 100 },
{ enabled: true }, { enabled: true },
) )
@ -34,6 +34,7 @@ export function BrokerDashboard({
/> />
<BrokerDashboardAnalyticsCard <BrokerDashboardAnalyticsCard
data={analytics.data} data={analytics.data}
portfolioValue={portfolio.totals.portfolio ?? undefined}
isLoading={analytics.isLoading} isLoading={analytics.isLoading}
isError={analytics.isError} isError={analytics.isError}
/> />

View File

@ -1,12 +1,7 @@
import { Metric, Text } from '@moex-vibe/design-system' import { Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import type { BrokerAnalytics } from '@/shared/api' import type { BrokerAnalytics, BrokerMoney } from '@/shared/api'
import { import { formatDashboardCurrency, type MoneyTone, moneyTone } from '../lib/dashboardVisual'
formatDashboardCurrency,
type MoneyTone,
moneyTone,
moneyToneToColor,
} from '../lib/dashboardVisual'
import { BrokerDashboardCard } from './BrokerDashboardCard' import { BrokerDashboardCard } from './BrokerDashboardCard'
type AnalyticsField = type AnalyticsField =
@ -54,10 +49,12 @@ function analyticsDisplay(field: AnalyticsField, value: number, currency: string
export function BrokerDashboardAnalyticsCard({ export function BrokerDashboardAnalyticsCard({
data, data,
portfolioValue,
isLoading, isLoading,
isError, isError,
}: { }: {
data: BrokerAnalytics | undefined data: BrokerAnalytics | undefined
portfolioValue?: BrokerMoney
isLoading: boolean isLoading: boolean
isError: boolean isError: boolean
}) { }) {
@ -70,48 +67,137 @@ export function BrokerDashboardAnalyticsCard({
) : !data ? ( ) : !data ? (
<Text tone="muted">Нет данных для аналитики</Text> <Text tone="muted">Нет данных для аналитики</Text>
) : ( ) : (
<Box <Box sx={{ display: 'grid', gap: 2 }}>
sx={{ <Box
display: 'grid', sx={{
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }, display: 'grid',
gap: 2, gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)' },
}} gap: 2,
> }}
{ANALYTICS_METRICS.map(({ field, label, testId }) => { >
const value = data[field] <Box
const tone = analyticsTone(field, value) sx={{
return ( borderRadius: 2,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'rgba(255,255,255,0.5)',
p: 1.5,
minHeight: 72,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<Text variant="label" tone="secondary">
Стоимость портфеля
</Text>
<Box sx={{ mt: 0.5, fontWeight: 700, fontSize: 18, whiteSpace: 'nowrap' }}>
{formatDashboardCurrency({
currency: portfolioValue?.currency ?? data.currency,
value: portfolioValue?.value ?? 0,
})}
</Box>
</Box>
<Box
sx={{
borderRadius: 2,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'rgba(255,255,255,0.5)',
p: 1.5,
minHeight: 72,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<Text variant="label" tone="secondary">
Всего доходов
</Text>
<Box <Box
key={field}
sx={{ sx={{
borderRadius: 2, mt: 0.5,
border: '1px solid', fontWeight: 700,
borderColor: tone === 'negative' ? 'error.light' : 'divider', fontSize: 18,
bgcolor: whiteSpace: 'nowrap',
tone === 'positive' color: 'success.main',
? 'rgba(46, 125, 50, 0.06)'
: tone === 'negative'
? 'rgba(211, 47, 47, 0.06)'
: 'grey.50',
p: 1.5,
}} }}
> >
<Metric {formatDashboardCurrency({
label={label} currency: data.currency,
value={ value: data.totalDividends + data.totalCoupons,
<Box })}
component="span"
data-testid={testId}
data-tone={tone}
sx={{ color: moneyToneToColor(tone), fontWeight: 700 }}
>
{analyticsDisplay(field, value, data.currency)}
</Box>
}
/>
</Box> </Box>
) </Box>
})} </Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' },
gap: 2,
}}
>
{ANALYTICS_METRICS.map(({ field, label, testId }) => {
const value = data[field]
const tone = analyticsTone(field, value)
return (
<Box
key={field}
sx={{
borderRadius: 2,
border: '1px solid',
borderColor:
tone === 'positive'
? 'rgba(46, 125, 50, 0.3)'
: tone === 'negative'
? 'rgba(211, 47, 47, 0.3)'
: 'divider',
background:
tone === 'positive'
? 'linear-gradient(180deg, #f3faf5, #edf7f0)'
: tone === 'negative'
? 'linear-gradient(180deg, #fff7f5, #fff1ef)'
: undefined,
bgcolor: tone === 'positive' || tone === 'negative' ? undefined : 'grey.50',
p: 1.5,
minHeight: 72,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<Box
sx={{
color: 'text.secondary',
fontSize: 12,
fontWeight: 700,
lineHeight: 1.25,
}}
>
{label}
</Box>
<Box
data-testid={testId}
data-tone={tone}
sx={{
mt: 0.5,
fontWeight: 700,
fontSize: 15,
lineHeight: 1.25,
color:
tone === 'positive'
? 'success.main'
: tone === 'negative'
? 'error.main'
: 'text.disabled',
}}
>
{analyticsDisplay(field, value, data.currency)}
</Box>
</Box>
)
})}
</Box>
</Box> </Box>
)} )}
</BrokerDashboardCard> </BrokerDashboardCard>

View File

@ -47,7 +47,11 @@ export function BrokerDashboardCard({
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Heading level={2} size="section"> <Heading
level={2}
size="section"
style={{ fontSize: 22, fontWeight: 700, lineHeight: 1.18 }}
>
{title} {title}
</Heading> </Heading>
{badge ? ( {badge ? (

View File

@ -1,6 +1,7 @@
import { Chip, Text } from '@moex-vibe/design-system' import { Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { useState } from 'react'
import type { BrokerOperation } from '@/shared/api' import type { BrokerOperation } from '@/shared/api'
import { formatBrokerDate } from '@/shared/lib/formatters' import { formatBrokerDate } from '@/shared/lib/formatters'
import { formatDashboardCurrency, moneyToneToColor } from '../lib/dashboardVisual' import { formatDashboardCurrency, moneyToneToColor } from '../lib/dashboardVisual'
@ -38,6 +39,18 @@ const TD_SX_INSTRUMENT = {
minWidth: 180, minWidth: 180,
} }
const ITEMS_PER_PAGE = 7
const BADGE_STYLES: Record<string, { bg: string; borderColor: string; color: string }> = {
coupon: { bg: '#e8f0ff', borderColor: '#cddcff', color: '#2563eb' },
dividend: { bg: '#e5f2ea', borderColor: '#bcdcc9', color: '#176747' },
maturity: { bg: '#fff5dc', borderColor: '#f0d898', color: '#8d6400' },
tax: { bg: '#fef0ef', borderColor: '#f6c6c2', color: '#b42318' },
fee: { bg: '#fef0ef', borderColor: '#f6c6c2', color: '#b42318' },
}
type BadgeInfo = { label: string; badgeKey: string }
type BrokerDashboardEventsCardProps = { type BrokerDashboardEventsCardProps = {
accountId: string accountId: string
data: BrokerOperation[] | undefined data: BrokerOperation[] | undefined
@ -45,29 +58,94 @@ type BrokerDashboardEventsCardProps = {
isError: boolean isError: boolean
} }
function getOperationBadge(operation: BrokerOperation): { label: string } { function getOperationBadge(operation: BrokerOperation): BadgeInfo {
if (operation.category === 'tax') return { label: 'Налог' } if (operation.category === 'tax') return { label: 'Налог', badgeKey: 'tax' }
if (operation.category === 'fee') return { label: 'Комиссия' } if (operation.category === 'fee') return { label: 'Комиссия', badgeKey: 'fee' }
if (operation.category === 'income') { if (operation.category === 'income') {
if ( if (
operation.type === 'OPERATION_TYPE_DIVIDEND' || operation.type === 'OPERATION_TYPE_DIVIDEND' ||
operation.type === 'OPERATION_TYPE_DIV_EXT' operation.type === 'OPERATION_TYPE_DIV_EXT'
) { ) {
return { label: 'Дивиденд' } return { label: 'Дивиденд', badgeKey: 'dividend' }
} }
if (operation.type === 'OPERATION_TYPE_COUPON') { if (operation.type === 'OPERATION_TYPE_COUPON') {
return { label: 'Купон' } return { label: 'Купон', badgeKey: 'coupon' }
} }
if ( if (
operation.type === 'OPERATION_TYPE_BOND_REPAYMENT' || operation.type === 'OPERATION_TYPE_BOND_REPAYMENT' ||
operation.type === 'OPERATION_TYPE_BOND_REPAYMENT_FULL' || operation.type === 'OPERATION_TYPE_BOND_REPAYMENT_FULL' ||
operation.type === 'OPERATION_TYPE_MATURITY' operation.type === 'OPERATION_TYPE_MATURITY'
) { ) {
return { label: 'Погашение' } return { label: 'Погашение', badgeKey: 'maturity' }
} }
return { label: 'Доход' } return { label: 'Доход', badgeKey: '' }
} }
return { label: 'Прочее' } return { label: 'Прочее', badgeKey: '' }
}
function Badge({ label, badgeKey }: { label: string; badgeKey: string }) {
const style = BADGE_STYLES[badgeKey]
return (
<Box
component="span"
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 24,
px: 1.25,
borderRadius: '999px',
border: 1,
borderColor: style?.borderColor ?? 'divider',
bgcolor: style?.bg ?? 'transparent',
color: style?.color ?? 'text.secondary',
fontSize: 12,
fontWeight: 700,
whiteSpace: 'nowrap',
}}
>
{label}
</Box>
)
}
function PagerButton({
disabled,
label,
onClick,
}: {
disabled?: boolean
label: string
onClick?: () => void
}) {
return (
<Box
component="button"
type="button"
disabled={disabled}
onClick={onClick}
sx={{
minWidth: 34,
height: 32,
border: 1,
borderColor: disabled ? 'divider' : 'grey.300',
borderRadius: 1,
bgcolor: 'background.paper',
color: disabled ? 'text.disabled' : 'primary.main',
fontSize: 13,
fontWeight: 700,
cursor: disabled ? 'default' : 'pointer',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 1,
px: 1,
'&:hover:not(:disabled)': { bgcolor: 'grey.100' },
}}
>
{label}
</Box>
)
} }
export function BrokerDashboardEventsCard({ export function BrokerDashboardEventsCard({
@ -76,7 +154,13 @@ export function BrokerDashboardEventsCard({
isLoading, isLoading,
isError, isError,
}: BrokerDashboardEventsCardProps) { }: BrokerDashboardEventsCardProps) {
const [page, setPage] = useState(0)
const operations = data ?? [] const operations = data ?? []
const totalPages = Math.max(1, Math.ceil(operations.length / ITEMS_PER_PAGE))
const safePage = Math.min(page, totalPages - 1)
const pageStart = safePage * ITEMS_PER_PAGE
const pageEnd = pageStart + ITEMS_PER_PAGE
const pageItems = operations.slice(pageStart, pageEnd)
return ( return (
<BrokerDashboardCard <BrokerDashboardCard
@ -113,10 +197,10 @@ export function BrokerDashboardEventsCard({
</Box> </Box>
</Box> </Box>
<Box component="tbody"> <Box component="tbody">
{operations.slice(0, 7).map((op) => { {pageItems.map((op) => {
const boldText = op.name ?? op.description ?? op.ticker ?? '—' const boldText = op.name ?? op.description ?? op.ticker ?? '—'
const grayText = op.name ? (op.ticker ?? null) : null const grayText = op.name ? (op.ticker ?? null) : null
const { label } = getOperationBadge(op) const { label, badgeKey } = getOperationBadge(op)
const amountValue = op.payment?.value ?? 0 const amountValue = op.payment?.value ?? 0
const amountTone = amountValue >= 0 ? 'positive' : 'negative' const amountTone = amountValue >= 0 ? 'positive' : 'negative'
const formattedAmount = `${amountValue >= 0 ? '+' : '\u2212'}${formatDashboardCurrency( const formattedAmount = `${amountValue >= 0 ? '+' : '\u2212'}${formatDashboardCurrency(
@ -152,7 +236,7 @@ export function BrokerDashboardEventsCard({
</Box> </Box>
</Box> </Box>
<Box component="td" sx={TD_SX_LEFT}> <Box component="td" sx={TD_SX_LEFT}>
<Chip label={label} tone="neutral" selected={false} /> <Badge label={label} badgeKey={badgeKey} />
</Box> </Box>
<Box <Box
component="td" component="td"
@ -171,6 +255,32 @@ export function BrokerDashboardEventsCard({
})} })}
</Box> </Box>
</Box> </Box>
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
gap: 1.5,
mt: 1.5,
minHeight: 34,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PagerButton
disabled={safePage === 0}
label="←"
onClick={() => setPage((p) => Math.max(0, p - 1))}
/>
<Box component="span" sx={{ fontSize: 13, color: 'text.secondary' }}>
{safePage + 1}
</Box>
<PagerButton
disabled={safePage >= totalPages - 1}
label="→"
onClick={() => setPage((p) => p + 1)}
/>
</Box>
</Box>
</Box> </Box>
)} )}
</BrokerDashboardCard> </BrokerDashboardCard>

View File

@ -19,11 +19,11 @@ function generateChartPath(points: { value: BrokerMoney }[]): { line: string; ar
const padding = range * 0.1 const padding = range * 0.1
const viewWidth = 300 const viewWidth = 300
const viewHeight = 120 const viewHeight = 90
const padLeft = 10 const padLeft = 10
const padRight = 10 const padRight = 10
const padTop = 5 const padTop = 5
const padBottom = 20 const padBottom = 5
const plotWidth = viewWidth - padLeft - padRight const plotWidth = viewWidth - padLeft - padRight
const plotHeight = viewHeight - padTop - padBottom const plotHeight = viewHeight - padTop - padBottom
const yMin = min - padding const yMin = min - padding
@ -48,7 +48,7 @@ function generateChartPath(points: { value: BrokerMoney }[]): { line: string; ar
lineCmd += ` C ${cx1},${y0} ${cx2},${y1} ${x1},${y1}` lineCmd += ` C ${cx1},${y0} ${cx2},${y1} ${x1},${y1}`
} }
const bottom = viewHeight - padBottom const bottom = viewHeight
const areaCmd = `${lineCmd} L ${x(points.length - 1)},${bottom} L ${x(0)},${bottom} Z` const areaCmd = `${lineCmd} L ${x(points.length - 1)},${bottom} L ${x(0)},${bottom} Z`
return { line: lineCmd, area: areaCmd } return { line: lineCmd, area: areaCmd }
@ -65,7 +65,7 @@ export function BrokerPortfolioHistoryCard({
title="Стоимость портфеля за 6 месяцев" title="Стоимость портфеля за 6 месяцев"
sx={{ sx={{
borderColor: 'success.light', borderColor: 'success.light',
bgcolor: 'rgba(46, 125, 50, 0.06)', background: 'linear-gradient(135deg, rgba(229, 242, 234, 0.96), rgba(255, 255, 255, 0.86))',
}} }}
> >
{(() => { {(() => {
@ -74,7 +74,7 @@ export function BrokerPortfolioHistoryCard({
} }
if (isLoading || !data) { if (isLoading || !data) {
return <Skeleton height={200} shape="rounded" /> return <Skeleton height={180} shape="rounded" />
} }
const points = data.points const points = data.points
@ -86,50 +86,82 @@ export function BrokerPortfolioHistoryCard({
return ( return (
<Box sx={{ display: 'grid', gap: 1 }}> <Box sx={{ display: 'grid', gap: 1 }}>
<Text variant="label" tone="secondary"> <Box
Текущая стоимость:{' '} sx={{
<Box component="span" sx={{ fontWeight: 700, color: 'text.primary' }}> display: 'flex',
{portfolioValue ? formatDashboardCurrency(portfolioValue) : '—'} alignItems: 'flex-start',
</Box> justifyContent: 'space-between',
</Text> gap: 2,
<svg }}
viewBox="0 0 300 120"
style={{ width: '100%', height: 'auto', display: 'block' }}
aria-label="График изменения стоимости портфеля"
> >
<defs> <Box sx={{ display: 'grid', gap: 0.25 }}>
<linearGradient id="areaGradient" x1="0" x2="0" y1="0" y2="1"> <Text variant="label" tone="secondary">
<stop offset="0%" stopColor="#2e7d32" stopOpacity={0.2} /> Текущая стоимость
<stop offset="100%" stopColor="#2e7d32" stopOpacity={0.02} /> </Text>
</linearGradient> <Box sx={{ fontWeight: 700, fontSize: 18, lineHeight: 1.25 }}>
</defs> {portfolioValue ? formatDashboardCurrency(portfolioValue) : '—'}
<path d={area} fill="url(#areaGradient)" /> </Box>
<path </Box>
d={line} </Box>
fill="none" <Box sx={{ mx: -2.25, width: 'calc(100% + 36px)' }}>
stroke="#2e7d32" <svg
strokeWidth={2} viewBox="0 0 300 90"
strokeLinecap="round" style={{ width: '100%', height: 'auto', display: 'block' }}
strokeLinejoin="round" aria-label="График изменения стоимости портфеля"
/> >
{points.map((point, i) => { <defs>
const plotWidth = 300 - 10 - 10 <linearGradient id="areaGradient" x1="0" x2="0" y1="0" y2="1">
const px = 10 + (i / (points.length - 1)) * plotWidth <stop offset="0%" stopColor="#2e7d32" stopOpacity={0.2} />
return ( <stop offset="100%" stopColor="#2e7d32" stopOpacity={0.02} />
<text </linearGradient>
key={point.month} </defs>
x={px} <path d={area} fill="url(#areaGradient)" />
y={115} <path
textAnchor="middle" d={line}
fill="text.secondary" fill="none"
fontSize={9} stroke="#2e7d32"
style={{ fill: 'var(--color-text-secondary, #64748b)' }} strokeWidth={2}
> strokeLinecap="round"
{point.label} strokeLinejoin="round"
</text> />
) </svg>
})} <Box
</svg> sx={{
position: 'relative',
mx: -2.25,
width: 'calc(100% + 36px)',
height: 20,
color: 'text.disabled',
fontSize: 11,
fontWeight: 700,
}}
>
{points.map((point, i) => {
const plotWidth = 300 - 10 - 10
const leftPct = ((10 + (i / (points.length - 1)) * plotWidth) / 300) * 100
const isFirst = i === 0
const isLast = i === points.length - 1
return (
<Box
key={point.month}
component="span"
sx={{
position: 'absolute',
bottom: 0,
left: isLast ? `${leftPct}%` : `${leftPct}%`,
transform: isFirst
? 'none'
: isLast
? 'translateX(-100%)'
: 'translateX(-50%)',
}}
>
{point.label}
</Box>
)
})}
</Box>
</Box>
</Box> </Box>
) )
})()} })()}