refactor: enforce frontend view model boundaries
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)]),
|
||||
|
||||
Reference in New Issue
Block a user