feat(ui): establish shared design system foundations
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
import "@mantine/dates/styles.css";
|
import "@mantine/dates/styles.css";
|
||||||
import "dayjs/locale/ru";
|
import "dayjs/locale/ru";
|
||||||
import "../src/app/styles.css";
|
|
||||||
import "../src/shared/i18n";
|
import "../src/shared/i18n";
|
||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import type { Preview } from "@storybook/react-vite";
|
import type { Preview } from "@storybook/react-vite";
|
||||||
import { theme } from "../src/app/theme";
|
import { cssVariablesResolver, theme } from "@/shared/theme";
|
||||||
|
|
||||||
const colorSchemeManager = {
|
const colorSchemeManager = {
|
||||||
get: () => "light" as const,
|
get: () => "light" as const,
|
||||||
@@ -18,11 +17,17 @@ const colorSchemeManager = {
|
|||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: "padded",
|
layout: "padded",
|
||||||
|
options: {
|
||||||
|
storySort: {
|
||||||
|
order: ["Design System", ["Foundations", "Components"], "Product"],
|
||||||
|
},
|
||||||
|
},
|
||||||
controls: { expanded: true },
|
controls: { expanded: true },
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story) => (
|
(Story) => (
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
|
cssVariablesResolver={cssVariablesResolver}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
forceColorScheme="light"
|
forceColorScheme="light"
|
||||||
colorSchemeManager={colorSchemeManager}
|
colorSchemeManager={colorSchemeManager}
|
||||||
|
|||||||
+8
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Проект использует FSD-направление импортов: `app -> pages -> features -> entities -> shared`.
|
Проект использует FSD-направление импортов: `app -> pages -> features -> entities -> shared`.
|
||||||
|
|
||||||
- `app` - providers, routes и глобальные стили.
|
- `app` - providers и routes; общий визуальный фундамент принадлежит shared/theme.
|
||||||
- `pages` - страницы маршрутов.
|
- `pages` - страницы маршрутов.
|
||||||
- `features` - пользовательские сценарии, включая authentication.
|
- `features` - пользовательские сценарии, включая authentication.
|
||||||
- `entities` - доменные модели и их UI/API.
|
- `entities` - доменные модели и их UI/API.
|
||||||
@@ -11,3 +11,10 @@
|
|||||||
Каждый слайс предоставляет наружу только public API через `index.ts`; внутренние файлы соседний слой не импортирует напрямую.
|
Каждый слайс предоставляет наружу только public API через `index.ts`; внутренние файлы соседний слой не импортирует напрямую.
|
||||||
|
|
||||||
Для cross-slice imports используйте alias `@/` и public API, например `@/entities/portfolio`. Vite и TypeScript используют одинаковый alias.
|
Для cross-slice imports используйте alias `@/` и public API, например `@/entities/portfolio`. Vite и TypeScript используют одинаковый alias.
|
||||||
|
|
||||||
|
`shared/theme/index.ts` экспортирует единую Mantine theme, семантические color
|
||||||
|
roles и CSSVariablesResolver и подключает базовые styles. App и Storybook
|
||||||
|
подключают свои MantineProvider к этому public API. Scales принимаются из Mantine,
|
||||||
|
локальные domain styles остаются у владельцев. `shared/ui` предоставляет
|
||||||
|
StateView и LoadingSkeleton; их каталог не включает route-aware AppShell.
|
||||||
|
Подробнее: [дизайн-система](../../docs/development/design-system.md).
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { MemoryRouter } from "react-router";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { AuthProvider } from "@/features/auth";
|
import { AuthProvider } from "@/features/auth";
|
||||||
|
import { cssVariablesResolver, theme } from "@/shared/theme";
|
||||||
import { createTestQueryClient } from "@/test/query";
|
import { createTestQueryClient } from "@/test/query";
|
||||||
import { server } from "@/test/server";
|
import { server } from "@/test/server";
|
||||||
import { theme } from "../theme";
|
|
||||||
import { AppRoutes } from "./AppRoutes";
|
import { AppRoutes } from "./AppRoutes";
|
||||||
|
|
||||||
describe("AppRoutes", () => {
|
describe("AppRoutes", () => {
|
||||||
@@ -16,7 +16,10 @@ describe("AppRoutes", () => {
|
|||||||
return render(
|
return render(
|
||||||
<QueryClientProvider client={createTestQueryClient()}>
|
<QueryClientProvider client={createTestQueryClient()}>
|
||||||
<MemoryRouter initialEntries={[path]}>
|
<MemoryRouter initialEntries={[path]}>
|
||||||
<MantineProvider theme={theme}>
|
<MantineProvider
|
||||||
|
cssVariablesResolver={cssVariablesResolver}
|
||||||
|
theme={theme}
|
||||||
|
>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { createTheme } from "@mantine/core";
|
|
||||||
|
|
||||||
const theme = createTheme({
|
|
||||||
primaryColor: "blue",
|
|
||||||
defaultRadius: "md",
|
|
||||||
fontFamily:
|
|
||||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
||||||
colors: {
|
|
||||||
blue: [
|
|
||||||
"#eff6ff",
|
|
||||||
"#dbeafe",
|
|
||||||
"#bfdbfe",
|
|
||||||
"#93c5fd",
|
|
||||||
"#60a5fa",
|
|
||||||
"#3b82f6",
|
|
||||||
"#2563eb",
|
|
||||||
"#1d4ed8",
|
|
||||||
"#1e40af",
|
|
||||||
"#1e3a8a",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export { theme };
|
|
||||||
+6
-2
@@ -4,8 +4,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { BrowserRouter } from "react-router";
|
import { BrowserRouter } from "react-router";
|
||||||
import { AppRoutes, RouteErrorBoundary } from "@/app/routes";
|
import { AppRoutes, RouteErrorBoundary } from "@/app/routes";
|
||||||
import { theme } from "@/app/theme";
|
|
||||||
import { AuthProvider } from "@/features/auth";
|
import { AuthProvider } from "@/features/auth";
|
||||||
|
import { cssVariablesResolver, theme } from "@/shared/theme";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -15,7 +15,11 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
return (
|
return (
|
||||||
<MantineProvider theme={theme} forceColorScheme="light">
|
<MantineProvider
|
||||||
|
cssVariablesResolver={cssVariablesResolver}
|
||||||
|
theme={theme}
|
||||||
|
forceColorScheme="light"
|
||||||
|
>
|
||||||
<DatesProvider settings={{ locale: "ru" }}>
|
<DatesProvider settings={{ locale: "ru" }}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const syntheticAccount = {
|
|||||||
} satisfies BrokerageAccount;
|
} satisfies BrokerageAccount;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "WealthLedger/BrokerageAccountList",
|
title: "Product/BrokerageAccountList",
|
||||||
component: BrokerageAccountList,
|
component: BrokerageAccountList,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { createRoot } from "react-dom/client";
|
|||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
import "@mantine/dates/styles.css";
|
import "@mantine/dates/styles.css";
|
||||||
import "dayjs/locale/ru";
|
import "dayjs/locale/ru";
|
||||||
import "./app/styles.css";
|
|
||||||
import "./shared/i18n";
|
import "./shared/i18n";
|
||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|||||||
import { AccountPageLoading } from "./AccountPageLoading.view";
|
import { AccountPageLoading } from "./AccountPageLoading.view";
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "WealthLedger/AccountPageLoading",
|
title: "Product/AccountPageLoading",
|
||||||
component: AccountPageLoading,
|
component: AccountPageLoading,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PortfolioRefreshState } from "./PortfolioRefreshState.view";
|
|||||||
import { PortfolioTotals } from "./PortfolioTotals.view";
|
import { PortfolioTotals } from "./PortfolioTotals.view";
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "WealthLedger/PortfolioRefreshState",
|
title: "Product/PortfolioRefreshState",
|
||||||
component: PortfolioRefreshState,
|
component: PortfolioRefreshState,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
args: { refreshing: false, refreshError: false, retry: fn() },
|
args: { refreshing: false, refreshError: false, retry: fn() },
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const syntheticPosition = {
|
|||||||
} satisfies AccountPagePosition;
|
} satisfies AccountPagePosition;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "WealthLedger/PositionCard",
|
title: "Product/PositionCard",
|
||||||
component: PositionCard,
|
component: PositionCard,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const syntheticEvent = {
|
|||||||
const eventsPath = "/accounts/storybook-account/events";
|
const eventsPath = "/accounts/storybook-account/events";
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "WealthLedger/RecentEvents",
|
title: "Product/RecentEvents",
|
||||||
component: RecentEvents,
|
component: RecentEvents,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--mantine-color-gray-2);
|
background: var(--wl-color-canvas);
|
||||||
color: var(--mantine-color-dark-7);
|
color: var(--wl-color-text);
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Button, type ButtonProps } from "@mantine/core";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { expect, fn, userEvent, within } from "storybook/test";
|
||||||
|
|
||||||
|
type ButtonStoryArgs = ButtonProps & { onClick: () => void };
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Design System/Components/Button",
|
||||||
|
component: Button,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: {
|
||||||
|
children: "Выполнить пример",
|
||||||
|
onClick: fn(),
|
||||||
|
loading: false,
|
||||||
|
disabled: false,
|
||||||
|
variant: "filled",
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
children: { control: "text" },
|
||||||
|
variant: { control: "select", options: ["filled", "default", "subtle"] },
|
||||||
|
loading: { control: "boolean" },
|
||||||
|
disabled: { control: "boolean" },
|
||||||
|
onClick: { control: false },
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Прямой Mantine Button с общей темой WealthLedger, без wrapper. Filled — основное действие; default — вторичное действие/retry; subtle — малозаметное вспомогательное действие. Radius md, primary blue и keyboard focus приходят из темы. Дайте кнопке доступное имя; loading означает mutation, disabled — недоступное действие. Loading/disabled не исполняют callback. Actions и interaction проверяют локальный callback, не API recovery.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<ButtonStoryArgs>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<ButtonStoryArgs>;
|
||||||
|
export const Primary: Story = {
|
||||||
|
name: "Основное действие",
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const button = within(canvasElement).getByRole("button", {
|
||||||
|
name: String(args.children),
|
||||||
|
});
|
||||||
|
button.focus();
|
||||||
|
await expect(button).toHaveFocus();
|
||||||
|
await userEvent.keyboard("{Enter}");
|
||||||
|
await expect(args.onClick).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const Secondary: Story = {
|
||||||
|
name: "Вторичное действие",
|
||||||
|
args: { variant: "default" },
|
||||||
|
};
|
||||||
|
export const Subtle: Story = {
|
||||||
|
name: "Вспомогательное действие",
|
||||||
|
args: { variant: "subtle" },
|
||||||
|
};
|
||||||
|
export const Loading: Story = { name: "Выполнение", args: { loading: true } };
|
||||||
|
export const Disabled: Story = { name: "Недоступно", args: { disabled: true } };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Paper, Skeleton } from "@mantine/core";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { LoadingSkeleton } from "./LoadingSkeleton";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Design System/Components/LoadingSkeleton",
|
||||||
|
component: LoadingSkeleton,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: { label: "Загрузка содержимого" },
|
||||||
|
argTypes: {
|
||||||
|
label: { control: "text" },
|
||||||
|
children: { table: { disable: true } },
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Общая status/busy область для initial или partial loading. Передайте доступную метку и только декоративные Skeleton shapes без интерактивных элементов. Геометрию определяет владелец section/page (см. Product/AccountPageLoading). При refetch сохраняйте уже видимые данные; для действия используйте Button loading. Этот пример не запускает запрос и не демонстрирует query lifecycle.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<Paper p="md" withBorder>
|
||||||
|
<LoadingSkeleton {...args}>
|
||||||
|
<Skeleton h={24} w="60%" />
|
||||||
|
<Skeleton h={72} />
|
||||||
|
<Skeleton h={16} w="80%" />
|
||||||
|
</LoadingSkeleton>
|
||||||
|
</Paper>
|
||||||
|
),
|
||||||
|
} satisfies Meta<typeof LoadingSkeleton>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
export const Initial: Story = { name: "Загрузка области" };
|
||||||
@@ -10,7 +10,7 @@ type StateStoryArgs = ComponentProps<typeof StateView> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "WealthLedger/StateView",
|
title: "Design System/Components/StateView",
|
||||||
component: StateView,
|
component: StateView,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
Reference in New Issue
Block a user