feat(ui): establish shared design system foundations
This commit is contained in:
@@ -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 />,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import "./styles.css";
|
||||
|
||||
export { colorRoles, cssVariablesResolver, theme } from "./theme";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user