feat(account): wrap portfolio sections in Mantine Accordion

All portfolio sections (stocks, bonds, other, cash, recent operations,
recent events) are now collapsible via Mantine Accordion. Sections
expand by default. Accordion control label uses h2 size to match
previous Section headings. Panel horizontal padding removed.
Smooth hover transition added. Removed unused Section component.
Updated lazy-route bundle baseline for account chunk.
This commit is contained in:
2026-08-26 13:55:44 +03:00
parent bcf93691aa
commit c1a8674f16
9 changed files with 88 additions and 66 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"src/app/routes/ProtectedLayout.tsx": 3057,
"src/pages/account-events/index.ts": 4364,
"src/pages/account-operations/index.ts": 13854,
"src/pages/account/index.ts": 25407,
"src/pages/account/index.ts": 32466,
"src/pages/accounts/index.ts": 6145,
"src/pages/login/index.ts": 662,
"src/pages/not-found/index.ts": 877,
@@ -90,3 +90,20 @@
.mono {
font-family: var(--mantine-font-family-monospace);
}
.accordionControl {
transition:
background-color 150ms ease,
color 150ms ease;
}
.accordionLabel {
font-size: var(--mantine-h2-font-size);
font-weight: var(--mantine-h2-font-weight);
line-height: var(--mantine-h2-line-height);
}
.accordionPanel {
padding-left: 0;
padding-right: 0;
}
+2 -2
View File
@@ -154,9 +154,9 @@ describe("AccountPage states", () => {
"T-Bank временно недоступен. Показан последний успешный снимок.",
),
).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Акции" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Акции" })).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Облигации" }),
screen.getByRole("button", { name: "Облигации" }),
).toBeInTheDocument();
const positions = screen
.getAllByText(/Вторая|Первая/)
+45 -14
View File
@@ -1,4 +1,4 @@
import { Alert, Button, Stack, Text } from "@mantine/core";
import { Accordion, Alert, Button, Stack, Text } from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
@@ -20,7 +20,6 @@ import {
PositionSection,
RecentEvents,
RecentOperations,
Section,
} from "./components";
type AccountPageErrorProps = {
@@ -167,18 +166,50 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
})}
</Text>
<PortfolioTotals totals={portfolio.totalsByCurrency} />
{positionSections.map((section) => (
<PositionSection
key={section.kind}
title={t(positionSectionTitleKey(section.kind))}
section={section}
/>
))}
<Section title={t("portfolio.cashBalances")}>
<CashBalances balances={portfolio.cashBalances} />
</Section>
<RecentOperations operations={recentOperations} />
<RecentEvents events={recentEvents} />
<Accordion
multiple
defaultValue={[
...positionSections.map((s) => s.kind),
"cash",
"recentOperations",
"recentEvents",
]}
classNames={{
control: classes.accordionControl,
label: classes.accordionLabel,
chevron: classes.accordionChevron,
panel: classes.accordionPanel,
}}
>
{positionSections.map((section) => (
<Accordion.Item key={section.kind} value={section.kind}>
<Accordion.Control>
{t(positionSectionTitleKey(section.kind))}
</Accordion.Control>
<Accordion.Panel>
<PositionSection section={section} />
</Accordion.Panel>
</Accordion.Item>
))}
<Accordion.Item value="cash">
<Accordion.Control>{t("portfolio.cashBalances")}</Accordion.Control>
<Accordion.Panel>
<CashBalances balances={portfolio.cashBalances} />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="recentOperations">
<Accordion.Control>{t("ledger.recentTitle")}</Accordion.Control>
<Accordion.Panel>
<RecentOperations operations={recentOperations} />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="recentEvents">
<Accordion.Control>{t("events.recentTitle")}</Accordion.Control>
<Accordion.Panel>
<RecentEvents events={recentEvents} />
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
);
};
@@ -2,35 +2,31 @@ import { SimpleGrid } from "@mantine/core";
import type { FC } from "react";
import type { AccountPagePositionSection } from "../AccountPage.vm";
import { PositionCard } from "./PositionCard.view";
import { Section } from "./Section.view";
type PositionSectionProps = {
title: string;
section: AccountPagePositionSection;
};
const PositionSection: FC<PositionSectionProps> = (props) => {
const { title, section } = props;
const { section } = props;
return (
<Section title={title}>
<SimpleGrid cols={section.wide ? 1 : { base: 1, sm: 2, md: 3 }}>
{section.positions.map((position) => (
<PositionCard
key={`${position.figi}-${position.displayName}-${position.quantity}`}
position={position}
wide={section.wide}
variant={
section.kind === "stocks"
? "stock"
: section.kind === "bonds"
? "bond"
: undefined
}
/>
))}
</SimpleGrid>
</Section>
<SimpleGrid cols={section.wide ? 1 : { base: 1, sm: 2, md: 3 }}>
{section.positions.map((position) => (
<PositionCard
key={`${position.figi}-${position.displayName}-${position.quantity}`}
position={position}
wide={section.wide}
variant={
section.kind === "stocks"
? "stock"
: section.kind === "bonds"
? "bond"
: undefined
}
/>
))}
</SimpleGrid>
);
};
@@ -1,4 +1,4 @@
import { Alert, Button, Paper, Text } from "@mantine/core";
import { Alert, Button, Paper, Stack, Text } from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
@@ -6,7 +6,6 @@ import type { BrokerageEvent } from "@/entities/brokerage-event";
import { formatMoney } from "@/entities/portfolio";
import classes from "../AccountPage.module.css";
import type { AccountPageRecentEvents } from "../AccountPage.vm";
import { Section } from "./Section.view";
type RecentEventsProps = {
events: AccountPageRecentEvents;
@@ -21,7 +20,7 @@ const RecentEvents: FC<RecentEventsProps> = (props) => {
const { t } = useTranslation();
return (
<Section title={t("events.recentTitle")}>
<Stack gap="sm">
{events.state === "loading" ? (
<Text c="dimmed">{t("events.recentLoading")}</Text>
) : null}
@@ -42,7 +41,7 @@ const RecentEvents: FC<RecentEventsProps> = (props) => {
<Button component={Link} to={events.to} variant="subtle">
{t("events.open")}
</Button>
</Section>
</Stack>
);
};
@@ -1,11 +1,10 @@
import { Alert, Button, Paper, Text } from "@mantine/core";
import { Alert, Button, Paper, Stack, Text } from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { formatMoney } from "@/entities/portfolio";
import classes from "../AccountPage.module.css";
import type { AccountPageRecentOperations } from "../AccountPage.vm";
import { Section } from "./Section.view";
type RecentOperationsProps = {
operations: AccountPageRecentOperations;
@@ -16,7 +15,7 @@ const RecentOperations: FC<RecentOperationsProps> = (props) => {
const { t } = useTranslation();
return (
<Section title={t("ledger.recentTitle")}>
<Stack gap="sm">
{operations.state === "loading" ? (
<Text c="dimmed">{t("ledger.recentLoading")}</Text>
) : null}
@@ -41,7 +40,7 @@ const RecentOperations: FC<RecentOperationsProps> = (props) => {
<Button component={Link} to={operations.to} variant="subtle">
{t("ledger.open")}
</Button>
</Section>
</Stack>
);
};
@@ -1,19 +0,0 @@
import { Stack, Title } from "@mantine/core";
import type { FC, PropsWithChildren } from "react";
type SectionProps = PropsWithChildren<{
title: string;
}>;
const Section: FC<SectionProps> = (props) => {
const { title, children } = props;
return (
<Stack gap="sm">
<Title order={2}>{title}</Title>
{children}
</Stack>
);
};
export { Section };
-1
View File
@@ -5,4 +5,3 @@ export { PortfolioTotals } from "./PortfolioTotals.view";
export { PositionSection } from "./PositionSection.view";
export { RecentEvents } from "./RecentEvents.view";
export { RecentOperations } from "./RecentOperations.view";
export { Section } from "./Section.view";