refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
@@ -35,5 +35,7 @@ const meta = {
} satisfies Meta<typeof EntryCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LongContentAndFallback: Story = {};
@@ -31,11 +31,13 @@ describe("EntryCard", () => {
value: "—",
valueLabel: "Неизвестно",
};
const { rerender } = render(
<MantineProvider>
<EntryCard {...props} />
</MantineProvider>,
);
// Decorative image has no accessible name; trigger its actual failure event.
const image = screen.queryByAltText("");
assert(image);
+21 -17
View File
@@ -13,24 +13,25 @@ type EntryCardProps = {
initial: string;
metadata: string;
title: string;
tone: "negative" | "neutral" | "positive";
tone: "negative"
| "neutral"
| "positive";
value: string;
valueLabel: string;
};
const EntryCard: FC<EntryCardProps> = (props) => {
return (
<Paper
className={classes.row}
style={
(props.color)
? ({
background: `color-mix(in srgb, var(--mantine-color-${props.color}-0) 45%, var(--wl-color-surface))`,
})
: undefined
const surfaceStyle = props.color
? {
background: `color-mix(in srgb, var(--mantine-color-${props.color}-0) 45%, var(--wl-color-surface))`,
}
withBorder
>
: undefined;
const valueContainerClass = props.actions ? classes.valueFooter : undefined;
const valueLabelClass = `${classes[props.tone]} ${props.actions ? classes.valueLabel : ""}`;
return (
<Paper className={classes.row} style={surfaceStyle} withBorder>
<Avatar
alt=""
aria-hidden="true"
@@ -40,26 +41,29 @@ const EntryCard: FC<EntryCardProps> = (props) => {
>
{props.icon ?? props.initial}
</Avatar>
<Stack className={classes.identity} gap={2}>
<Text fw={700} lineClamp={1}>
{props.title}
</Text>
<Text c="dimmed" size="sm">
{props.metadata}
</Text>
{props.details}
</Stack>
<Stack className={classes.value} gap={2}>
<Text className={classes[props.tone]} fw={700}>
{props.value}
</Text>
<div className={(props.actions) ? (classes.valueFooter) : undefined}>
<Text
className={`${classes[props.tone]} ${props.actions ? classes.valueLabel : ""}`}
size="sm"
>
<div className={valueContainerClass}>
<Text className={valueLabelClass} size="sm">
{props.valueLabel}
</Text>
{Boolean(props.actions) && (
<div className={classes.actions}>{props.actions}</div>
)}
@@ -23,7 +23,9 @@ const meta = {
<Paper p="md" withBorder>
<LoadingSkeleton {...arguments_}>
<Skeleton h={24} w="60%" />
<Skeleton h={72} />
<Skeleton h={16} w="80%" />
</LoadingSkeleton>
</Paper>
@@ -33,5 +35,7 @@ const meta = {
} satisfies Meta<typeof LoadingSkeleton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Initial: Story = { name: "Загрузка области" };
@@ -41,16 +41,17 @@ const meta = {
},
render: (arguments_) => {
const { onAction, withAction, ...stateProps } = arguments_;
return (
<StateView
{...stateProps}
action={
(withAction)
? ((
withAction
? (
<Button onClick={onAction} variant="default">
{arguments_.error ? "Повторить локально" : "Выполнить пример"}
</Button>
))
)
: undefined
}
/>
@@ -63,6 +64,7 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Empty: Story = { name: "Пустое состояние" };
export const EmptyWithAction: Story = {
args: { withAction: true },
+2
View File
@@ -17,7 +17,9 @@ const StateView: FC<StateViewProps> = (props) => {
<Paper p="xl" radius="md" role={role} shadow="xs" withBorder>
<Stack align="flex-start" gap="sm">
<Title order={2}>{title}</Title>
<Text c="dimmed">{text}</Text>
{action}
</Stack>
</Paper>
+3 -1
View File
@@ -1,4 +1,3 @@
import "./styles.css";
import {
colorsTuple,
createTheme,
@@ -11,8 +10,11 @@ import {
} from "@mantine/core";
import designTokens from "@wealthledger/tokens/tokens.json";
import "./styles.css";
const { breakpoints, color, radius, shadows, spacing, typography }
= designTokens;
const mapValues = <T, U>(values: Record<string, T>, convert: (value: T) => U) =>
Object.fromEntries(
Object.entries(values).map(([name, value]) => [name, convert(value)]),