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
+85 -77
View File
@@ -16,53 +16,74 @@ import { colorRoles, designTokens } from "@wealthledger/ui/theme";
const ColorsView: FC = () => {
const theme = useMantineTheme();
return (
<Stack>
<Title order={1}>Цвета и семантические роли</Title>
<Text>
Светлая схема WealthLedger. Источник цветов и ролей —
@wealthledger/tokens/tokens.json. Цвет не заменяет подпись состояния.
</Text>
<SimpleGrid cols={{ base: 1, lg: 3, sm: 2 }}>
{Object.entries(designTokens.color.roles).map(([role, token]) => (
<Paper key={role} p="md" withBorder>
<Stack gap="xs">
<Box aria-hidden="true" bg={`var(--wl-color-${role})`} h={48} />
<Text fw={700}>{role}</Text>
<Code>
--wl-color-
{role}
</Code>
<Text size="sm">
{/* eslint-disable-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key. */ colorRoles[role]}
{" "}
·
{" "}
{parseThemeColor({ color: /* eslint-disable-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key. */ colorRoles[role], theme }).value}
{
// eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key.
colorRoles[role]
}{" "}
·{" "}
{
parseThemeColor({
color:
// eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key.
colorRoles[role],
theme,
}).value
}
</Text>
<Text size="sm">{token.description}</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
{Object.keys(designTokens.color.palettes).map(palette => (
<Stack gap="xs" key={palette}>
<Title order={2}>{palette}</Title>
<SimpleGrid cols={{ base: 2, lg: 10, sm: 5 }}>
{/* eslint-disable-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key. */ theme.colors[palette].map((value, shade) => (
<Paper key={value} p="xs" withBorder>
<Box aria-hidden="true" bg={value} h={40} />
<Text size="sm">
{palette}
.
{shade}
</Text>
<Text size="xs">{value}</Text>
</Paper>
))}
{
// eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key.
theme.colors[palette].map((value, shade) => (
<Paper key={value} p="xs" withBorder>
<Box aria-hidden="true" bg={value} h={40} />
<Text size="sm">
{palette}.{shade}
</Text>
<Text size="xs">{value}</Text>
</Paper>
))
}
</SimpleGrid>
</Stack>
))}
<Text>
Blue — фирменная палитра. Gray/dark — нейтрали; green/red/yellow —
статусы; orange — дополнительный акцент. Grape — цвет графиков, его
@@ -75,23 +96,20 @@ const ColorsView: FC = () => {
const TypographyView: FC = () => {
const theme = useMantineTheme();
return (
<Stack style={{ overflowWrap: "anywhere" }}>
<Title order={1}>Типографика</Title>
<Text>
Основной шрифт:
{" "}
{theme.fontFamily}
</Text>
<Text ff="monospace">
Моноширинный:
{" "}
{theme.fontFamilyMonospace}
</Text>
<Text>Основной шрифт: {theme.fontFamily}</Text>
<Text ff="monospace">Моноширинный: {theme.fontFamilyMonospace}</Text>
<Text>
Title задаёт смысловую иерархию; выбирайте order по структуре страницы.
Text — основной текст, size sm/xs — подписи; mono — идентификаторы.
</Text>
{Object.entries(theme.headings.sizes).map(([name, value]) => (
<Paper key={name} p="md" withBorder>
<Text
@@ -99,44 +117,32 @@ const TypographyView: FC = () => {
fz={value.fontSize}
lh={value.lineHeight}
>
{name}
{" "}
· Заголовок WealthLedger
{name} · Заголовок WealthLedger
</Text>
<Text size="sm">
{value.fontSize}
{" "}
/
{" "}
{value.lineHeight}
{" "}
· weight
{" "}
{value.fontSize} / {value.lineHeight} · weight{" "}
{theme.headings.fontWeight}
</Text>
</Paper>
))}
{Object.entries(theme.fontSizes).map(([size, value]) => (
<Paper key={size} p="md" withBorder>
<Text size={size}>
Размер
{" "}
{size}
{" "}
· Пример основного текста
</Text>
<Text size={size}>Размер {size} · Пример основного текста</Text>
<Text size="sm">
{value}
{" "}
/
{" "}
{/* eslint-disable-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key. */ theme.lineHeights[size]}
{value} /{" "}
{
// eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key.
theme.lineHeights[size]
}
</Text>
</Paper>
))}
<Text>
Веса:
{" "}
Веса:{" "}
{Object.entries(theme.fontWeights)
.map(([name, value]) => `${name} ${value}`)
.join("; ")}
@@ -147,66 +153,63 @@ const TypographyView: FC = () => {
const LayoutView: FC = () => {
const theme = useMantineTheme();
return (
<Stack style={{ overflowWrap: "anywhere" }}>
<Title order={1}>Интервалы, форма и адаптивность</Title>
<Text>
Используйте Mantine props gap/p/radius/shadow или --mantine-* в CSS.
Шкалы принадлежат tokens.json; адаптер Mantine сохраняет
масштабирование. Default radius:
{theme.defaultRadius}
; scale:
{" "}
{theme.scale}
. Размеры конкретных карточек
{theme.defaultRadius}; scale: {theme.scale}. Размеры конкретных карточек
и их особые тени остаются локальными.
</Text>
<Title order={2}>Spacing</Title>
{Object.entries(theme.spacing).map(([name, value]) => (
<Paper key={name} p="sm" withBorder>
<Text>
{name}
{" "}
·
{" "}
{value}
{name} · {value}
</Text>
<Box aria-hidden="true" bg="blue" h={16} w={value} />
</Paper>
))}
<Title order={2}>Radius</Title>
<SimpleGrid cols={{ base: 1, sm: 3 }}>
{Object.entries(theme.radius).map(([name, value]) => (
<Paper key={name} p="md" radius={name} withBorder>
<Text>
{name}
{" "}
·
{" "}
{value}
{name} · {value}
</Text>
</Paper>
))}
</SimpleGrid>
<Title order={2}>Shadows</Title>
{Object.entries(theme.shadows).map(([name, value]) => (
<Paper key={name} p="md" shadow={name} withBorder>
<Text fw={700}>{name}</Text>
<Text size="sm">{value}</Text>
</Paper>
))}
<Title order={2}>Breakpoints</Title>
<Text>
Именованные пороги responsive props. Узкий экран — base; не фиксируйте
ширину контента по размеру desktop.
</Text>
{Object.entries(theme.breakpoints).map(([name, value]) => (
<Text key={name}>
{name}
{" "}
·
{" "}
{value}
{name} · {value}
</Text>
))}
</Stack>
@@ -228,7 +231,9 @@ const meta = {
} satisfies Meta;
export default meta;
type Story = StoryObj<typeof meta>;
export const Colors: Story = { name: "Цвета", render: () => <ColorsView /> };
export const SecondaryText: Story = {
name: "Вторичный текст",
@@ -238,17 +243,20 @@ export const SecondaryText: Story = {
"var(--wl-color-surface)",
"var(--wl-color-canvas)",
"var(--wl-color-subtle)",
...Object.keys(designTokens.color.palettes).flatMap(palette => palette === "dark"
? []
: [
`var(--mantine-color-${palette}-0)`,
`color-mix(in srgb, var(--mantine-color-${palette}-0) 45%, var(--wl-color-surface))`,
]),
...Object.keys(designTokens.color.palettes).flatMap(palette =>
palette === "dark"
? []
: [
`var(--mantine-color-${palette}-0)`,
`color-mix(in srgb, var(--mantine-color-${palette}-0) 45%, var(--wl-color-surface))`,
],
),
].map(background => (
<Paper key={background} p="md" style={{ background }} withBorder>
<Text c="dimmed" size="sm">
22.09.2026 · Синтетические данные
</Text>
<Text c="dimmed" size="xs">
Не удалось обновить данные. Сохранён предыдущий результат.
</Text>
+9 -1
View File
@@ -7,16 +7,23 @@ function luminance(color: string) {
.map(value =>
value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4,
);
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
}
describe("semantic text contrast", () => {
it.each(["positive", "negative", "muted"] as const)(
"keeps %s readable on canvas, white, subtle and tinted surfaces",
(tone) => {
const role = /* eslint-disable-line security/detect-object-injection -- Keys come from the repository’s trusted design-token definitions. */ tokens.color.roles[tone];
const role
// eslint-disable-next-line security/detect-object-injection -- Keys come from the repository’s trusted design-token definitions.
= tokens.color.roles[tone];
const palettes = tokens.color.palettes;
const foreground
= palettes[role.color as keyof typeof palettes][role.shade];
const surfaces = [
"#ffffff",
palettes.gray[0],
@@ -41,6 +48,7 @@ describe("semantic text contrast", () => {
.join("")}`,
),
];
for (const background of surfaces)
expect(
(luminance(background) + 0.05) / (luminance(foreground) + 0.05),