feat(ui): establish shared design system foundations

This commit is contained in:
2026-09-08 07:09:05 +03:00
parent 36698322b7
commit d762ba5034
18 changed files with 401 additions and 40 deletions
+202
View File
@@ -0,0 +1,202 @@
import {
Box,
Code,
Paper,
parseThemeColor,
SimpleGrid,
Stack,
Text,
Title,
useMantineTheme,
} from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { FC } from "react";
import { colorRoles } from "./index";
const roleUsage: Record<keyof typeof colorRoles, string> = {
canvas: "Фон приложения",
surface: "Поверхность Paper / формы",
subtle: "Приглушённая поверхность сводки",
text: "Основной текст страницы",
muted: "Вторичный текст; проверяйте контраст при выборе фона",
border: "Граница поверхности",
primary: "Основное действие и активная навигация",
info: "Информационное сообщение",
positive: "Положительный результат; дополните знаком и текстом",
negative: "Ошибка / отрицательный результат; дополните текстом",
warning: "Предупреждение; не мелкий текст на белом фоне",
};
const ColorsView: FC = () => {
const theme = useMantineTheme();
return (
<Stack>
<Title order={1}>Цвета и семантические роли</Title>
<Text>
Светлая схема WealthLedger. Роли ссылаются на палитры Mantine, а не
хранят вторую копию цветов. Цвет не заменяет подпись состояния.
</Text>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>
{Object.entries(colorRoles).map(([role, color]) => (
<Paper key={role} p="md" withBorder>
<Stack gap="xs">
<Box h={48} bg={`var(--wl-color-${role})`} aria-hidden="true" />
<Text fw={700}>{role}</Text>
<Code>--wl-color-{role}</Code>
<Text size="sm">
{color} · {parseThemeColor({ color, theme }).value}
</Text>
<Text size="sm">
{roleUsage[role as keyof typeof colorRoles]}
</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
{["blue", "gray", "dark", "green", "red", "yellow", "grape"].map(
(palette) => (
<Stack key={palette} gap="xs">
<Title order={2}>{palette}</Title>
<SimpleGrid cols={{ base: 2, sm: 5, lg: 10 }}>
{theme.colors[palette].map((value, shade) => (
<Paper key={value} p="xs" withBorder>
<Box h={40} bg={value} aria-hidden="true" />
<Text size="sm">
{palette}.{shade}
</Text>
<Text size="xs">{value}</Text>
</Paper>
))}
</SimpleGrid>
</Stack>
),
)}
<Text>
Blue — фирменная палитра. Gray/dark — нейтрали; green/red/yellow —
статусы. Grape — дополнительный цвет графиков, его предметное значение
задаёт Product. Палитры не гарантируют контраст любых сочетаний.
</Text>
</Stack>
);
};
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>
Title задаёт смысловую иерархию; выбирайте order по структуре страницы.
Text — основной текст, size sm/xs — подписи; mono — идентификаторы.
</Text>
{Object.entries(theme.headings.sizes).map(([name, value]) => (
<Paper key={name} withBorder p="md">
<Text
fz={value.fontSize}
lh={value.lineHeight}
fw={theme.headings.fontWeight}
>
{name} · Заголовок WealthLedger
</Text>
<Text size="sm">
{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="sm">
{value} / {theme.lineHeights[size]}
</Text>
</Paper>
))}
<Text>
Веса:{" "}
{Object.entries(theme.fontWeights)
.map(([name, value]) => `${name} ${value}`)
.join("; ")}
</Text>
</Stack>
);
};
const LayoutView: FC = () => {
const theme = useMantineTheme();
return (
<Stack style={{ overflowWrap: "anywhere" }}>
<Title order={1}>Интервалы, форма и адаптивность</Title>
<Text>
Используйте Mantine props gap/p/radius/shadow или --mantine-* в CSS.
Шкалы приняты из установленной Mantine без копий чисел. Default radius:
{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}
</Text>
<Box w={value} h={16} bg="blue" aria-hidden="true" />
</Paper>
))}
<Title order={2}>Radius</Title>
<SimpleGrid cols={{ base: 1, sm: 3 }}>
{Object.entries(theme.radius).map(([name, value]) => (
<Paper key={name} radius={name} p="md" withBorder>
<Text>
{name} · {value}
</Text>
</Paper>
))}
</SimpleGrid>
<Title order={2}>Shadows</Title>
{Object.entries(theme.shadows).map(([name, value]) => (
<Paper key={name} shadow={name} p="md" 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}
</Text>
))}
</Stack>
);
};
const meta = {
title: "Design System/Foundations",
tags: ["autodocs"],
parameters: {
controls: { disable: true },
docs: {
description: {
component:
"Единый shared/theme приложения и каталога. Все образцы читают effective Mantine theme. Показаны используемые шкалы и роли, а не весь Mantine. Светлая схема; контраст, смысловые заголовки и отсутствие передачи статуса только цветом проверяются в конкретной композиции.",
},
},
},
} satisfies Meta;
export default meta;
type Story = StoryObj<typeof meta>;
export const Colors: Story = { name: "Цвета", render: () => <ColorsView /> };
export const Typography: Story = {
name: "Типографика",
render: () => <TypographyView />,
};
export const Layout: Story = {
name: "Интервалы и форма",
render: () => <LayoutView />,
};
+3
View File
@@ -0,0 +1,3 @@
import "./styles.css";
export { colorRoles, cssVariablesResolver, theme } from "./theme";
+12
View File
@@ -0,0 +1,12 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--wl-color-canvas);
color: var(--wl-color-text);
}
a {
color: inherit;
text-decoration: none;
}
+69
View File
@@ -0,0 +1,69 @@
import {
type CSSVariablesResolver,
createTheme,
DEFAULT_THEME,
getThemeColor,
} from "@mantine/core";
// Native Mantine color references, not a second palette. Light scheme only.
const colorRoles = {
canvas: "gray.2",
surface: "white",
subtle: "gray.0",
text: "dark.7",
muted: "gray.6",
border: "gray.3",
primary: "blue",
info: "blue.6",
positive: "green.6",
negative: "red.6",
warning: "yellow.6",
} as const;
const fontFamily =
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
const theme = createTheme({
primaryColor: "blue",
defaultRadius: "md",
fontFamily,
// Explicitly adopt the installed Mantine scales without duplicating values.
fontFamilyMonospace: DEFAULT_THEME.fontFamilyMonospace,
fontSizes: DEFAULT_THEME.fontSizes,
fontWeights: DEFAULT_THEME.fontWeights,
lineHeights: DEFAULT_THEME.lineHeights,
headings: { ...DEFAULT_THEME.headings, fontFamily },
spacing: DEFAULT_THEME.spacing,
radius: DEFAULT_THEME.radius,
shadows: DEFAULT_THEME.shadows,
breakpoints: DEFAULT_THEME.breakpoints,
primaryShade: DEFAULT_THEME.primaryShade,
focusRing: DEFAULT_THEME.focusRing,
colors: {
blue: [
"#eff6ff",
"#dbeafe",
"#bfdbfe",
"#93c5fd",
"#60a5fa",
"#3b82f6",
"#2563eb",
"#1d4ed8",
"#1e40af",
"#1e3a8a",
],
},
});
const cssVariablesResolver: CSSVariablesResolver = (resolvedTheme) => ({
variables: Object.fromEntries(
Object.entries(colorRoles).map(([role, color]) => [
`--wl-color-${role}`,
getThemeColor(color, resolvedTheme),
]),
),
light: {},
dark: {},
});
export { colorRoles, cssVariablesResolver, theme };