feat(portfolio): add expanded layout for bond position cards

Add variant="bond" to PositionCard with bond-specific CSS classes
(.bondCard, .bondMain, etc.) matching the stock expanded layout.
PositionSection now passes variant based on section kind.
Bond cards render 3-column grid (logo | identity | value) with
meta row, identical to stock cards.
This commit is contained in:
2026-08-26 13:21:18 +03:00
parent 273778c4cf
commit bcf93691aa
5 changed files with 95 additions and 15 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": 24695,
"src/pages/account/index.ts": 25407,
"src/pages/accounts/index.ts": 6145,
"src/pages/login/index.ts": 662,
"src/pages/not-found/index.ts": 877,
+26 -1
View File
@@ -56,12 +56,37 @@
white-space: nowrap;
}
.stockMeta {
.stockMeta,
.bondMeta {
padding-top: var(--mantine-spacing-xs);
margin-top: var(--mantine-spacing-sm);
border-top: 1px solid var(--mantine-color-gray-2);
}
.bondCard {
padding: var(--mantine-spacing-md);
}
.bondMain {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: var(--mantine-spacing-sm);
align-items: center;
}
.bondIdentity,
.bondValue {
min-width: 0;
}
.bondIdentifiers {
overflow-wrap: anywhere;
}
.bondValue {
white-space: nowrap;
}
.mono {
font-family: var(--mantine-font-family-monospace);
}
+34 -3
View File
@@ -165,9 +165,12 @@ describe("AccountPage states", () => {
expect(screen.getByText("10 ₽")).toBeInTheDocument();
expect(screen.getByText("-1 ₽")).toBeInTheDocument();
expect(screen.queryByText(/Доход:/)).not.toBeInTheDocument();
expect(screen.getByText("Средняя цена: 11 ₽")).toBeInTheDocument();
expect(screen.getByText("Доля: 100 %")).toBeInTheDocument();
expect(screen.getByText("RU000A0JX0J2")).toBeInTheDocument();
expect(
screen.getByText("Средняя цена: 11 ₽ · Доля: 100 %"),
).toBeInTheDocument();
expect(
screen.getByText("Количество: 1 · RU000A0JX0J2"),
).toBeInTheDocument();
expect(screen.queryByText("—")).not.toBeInTheDocument();
expect(screen.getByRole("img", { name: "Вторая" })).toHaveAttribute(
"src",
@@ -289,6 +292,34 @@ describe("AccountPage states", () => {
expect(screen.queryByText(/Доход:/)).not.toBeInTheDocument();
});
it("renders bond cards in expanded layout with bond-specific classes", () => {
renderPage(
<AccountPageLoaded
{...loadedProps({
...portfolio,
positions: [
{
figi: "FIGI-BOND",
displayName: "Облигация 2030",
type: "BOND",
quantity: "10",
currentValue: { currency: "RUB", value: "1000" },
averagePurchasePrice: { currency: "RUB", value: "950" },
absoluteReturn: { currency: "RUB", value: "50" },
isin: "RU000B0ND123",
logoName: null,
},
],
})}
/>,
);
expect(screen.getByText("О")).toBeInTheDocument();
expect(screen.getByText("+50 ₽")).toBeInTheDocument();
expect(
screen.getByText("Средняя цена: 950 ₽ · Доля: 100 %"),
).toBeInTheDocument();
});
it("renders the bonds section as a single-column grid on any screen width", () => {
renderPage(
<AccountPageLoaded
@@ -9,13 +9,31 @@ import { PositionLogo } from "./PositionLogo.view";
type PositionCardProps = {
position: AccountPagePosition;
wide?: boolean;
variant?: "stock";
variant?: "stock" | "bond";
};
const PositionCard: FC<PositionCardProps> = (props) => {
const { position, wide = false, variant } = props;
const { t } = useTranslation();
const isStock = variant === "stock";
const isExpanded = variant === "stock" || variant === "bond";
const ec =
variant === "bond"
? {
card: classes.bondCard,
main: classes.bondMain,
identity: classes.bondIdentity,
identifiers: classes.bondIdentifiers,
value: classes.bondValue,
meta: classes.bondMeta,
}
: {
card: classes.stockCard,
main: classes.stockMain,
identity: classes.stockIdentity,
identifiers: classes.stockIdentifiers,
value: classes.stockValue,
meta: classes.stockMeta,
};
const returnClass = position.returnTone
? classes[position.returnTone]
: undefined;
@@ -46,24 +64,24 @@ const PositionCard: FC<PositionCardProps> = (props) => {
.filter(Boolean)
.join(" · ");
if (isStock)
if (isExpanded)
return (
<Card className={`${classes.tile} ${classes.stockCard}`} withBorder>
<div className={classes.stockMain}>
<Card className={`${classes.tile} ${ec.card}`} withBorder>
<div className={ec.main}>
<PositionLogo
key={position.logoSrc ?? "no-logo"}
src={position.logoSrc}
label={position.displayName}
/>
<Stack gap={2} miw={0} className={classes.stockIdentity}>
<Stack gap={2} miw={0} className={ec.identity}>
<Text fw={600} lineClamp={1}>
{position.displayName}
</Text>
<Text c="dimmed" size="xs" className={classes.stockIdentifiers}>
<Text c="dimmed" size="xs" className={ec.identifiers}>
{identifierText}
</Text>
</Stack>
<Stack gap={2} align="flex-end" className={classes.stockValue}>
<Stack gap={2} align="flex-end" className={ec.value}>
{position.currentValue ? (
<Text fw={700} size="lg">
{formatMoney(position.currentValue)}
@@ -77,7 +95,7 @@ const PositionCard: FC<PositionCardProps> = (props) => {
</Stack>
</div>
{secondaryText ? (
<Text c="dimmed" size="xs" className={classes.stockMeta}>
<Text c="dimmed" size="xs" className={ec.meta}>
{secondaryText}
</Text>
) : null}
@@ -20,7 +20,13 @@ const PositionSection: FC<PositionSectionProps> = (props) => {
key={`${position.figi}-${position.displayName}-${position.quantity}`}
position={position}
wide={section.wide}
variant={section.kind === "stocks" ? "stock" : undefined}
variant={
section.kind === "stocks"
? "stock"
: section.kind === "bonds"
? "bond"
: undefined
}
/>
))}
</SimpleGrid>