codex/design-system-foundation #33

Merged
ksv741 merged 12 commits from codex/design-system-foundation into main 2026-06-21 16:44:32 +03:00
12 changed files with 756 additions and 1 deletions
Showing only changes of commit e6cf852741 - Show all commits

View File

@ -0,0 +1,79 @@
# ADR-016: Дизайн-система — гибридный подход (theme + wrapper-компоненты)
**Статус:** Accepted
**Дата:** 2026-06-21
## Контекст
Фронтенд MoexVibe нуждался в единообразном UI. Ранее каждая страница использовала MUI напрямую, что приводило к:
- неконсистентным вариантам компонентов (разные цвета, размеры, отступы);
- отсутствию единой системы токенов;
- размножению стилей в CSS custom properties.
Рост количества страниц и компонентов потребовал системного подхода.
## Рассмотренные варианты
### 1. Theme-only
Оставить прямое использование MUI, но настроить единую тему (palette, typography, shape).
**Плюсы:** минимальные изменения, полная гибкость MUI.
**Минусы:** нет гарантии консистентности — разработчики могут использовать любые варианты MUI-компонентов.
### 2. Hybrid (выбран)
MUI theme обеспечивает фундамент (палитра, типографика, скругления); wrapper-компоненты поверх MUI гарантируют консистентность и ограничивают варианты.
**Плюсы:**
- единый source of truth в теме MUI;
- компоненты с ограниченным API (только нужные варианты);
- возможность миграции (можно заменить реализацию под капотом);
- Box/Stack/Grid остаются как allowlist для лэйаута.
**Минусы:**
- необходимо написать 20+ wrapper-компонентов;
- двойная прослойка для некоторых сценариев.
### 3. Full wrapper
Все UI через собственные компоненты, без прямого использования MUI.
**Плюсы:** полный контроль, независимость от MUI.
**Минусы:** огромный объём работы, дублирование функциональности MUI.
## Решение
Выбран гибридный подход (option 2):
- **Токены:** трёхуровневая система (primitives → semantic → component) с DTCG-резолвером.
- **Тема:** MUI 6.5 theme, построенная на семантических токенах. CSS-переменные `--mv-*` для доступа из CSS.
- **Компоненты:** 20+ wrapper-компонентов, каждый с ограниченным пропс-интерфейсом.
- **ESLint:** `no-restricted-imports` запрещает прямой импорт MUI-компонентов (кроме Box, Stack, Grid).
- **Storybook:** инженерный стенд с a11y-проверками.
- **Документация:** Docusaurus для опубликованных гайдлайнов (этот сайт).
## Последствия
### Положительные
- Консистентный UI во всех страницах.
- Единая система токенов, доступная из CSS (через `--mv-*`).
- Изолированная библиотека компонентов, которую можно тестировать и развивать независимо.
- Возможность замены реализации под капотом без изменения потребителей.
### Отрицательные
- 20+ компонентов нужно поддерживать.
- Оверхед на поддержку пропс-интерфейсов (каждый компонент ограничивает MUI API).
- Новые разработчики должны знать два слоя (MUI theme + wrapper-компоненты).
### Future
- DTCG-экспорт токенов для дизайн-инструментов.
- Android-адаптеры для нативных приложений.
- Расширение компонентной базы.

View File

@ -16,5 +16,7 @@
| [ADR-012](ADR-012-frontend-broker-account-aggregation) | Accepted | Агрегация сводки брокерских счетов на frontend |
| [ADR-013](ADR-013-frontend-fsd-broker-pilot) | Accepted | Пилотная FSD-миграция broker-домена |
| [ADR-014](ADR-014-frontend-fsd-market-pages) | Accepted | FSD-миграция market pages и market widgets |
| [ADR-015](ADR-015-frontend-libraries-modernization) | — | Модернизация инфраструктуры фронтенда |
| [ADR-016](ADR-016-design-system) | Accepted | Дизайн-система — гибридный подход |
Все опубликованные ADR находятся в `apps/docs/docs/adr/` и отображаются в этом Docusaurus-разделе.

View File

@ -0,0 +1,49 @@
# Доступность (Accessibility)
## Целевой уровень
WCAG 2.2 AA.
## Требования к компонентам
### IconButton
**Требует `label`** — свойство `aria-label` обязательно для всех иконок без текста.
```tsx
<IconButton label="Настройки">
<SettingsIcon />
</IconButton>
```
### Dialog
- **Требует `title`** — заголовок, используемый как `aria-labelledby`
- Содержит focus trap (фокус не покидает диалог)
- Закрывается по Escape
### Progress
- `indeterminate`-режим выставляет `aria-busy` на контейнере
### PriceChange
- Использует **текстовый индикатор направления** (`+`, ``, `—`), а не только цвет
- Скринридеры получают знак перед числом
### DataTable
- **Требует `caption`** — описание таблицы для скринридеров
- Семантические заголовки через `<th scope="col">`
### Skeleton
- `aria-hidden` по умолчанию — скелетоны не должны озвучиваться
### Alert
- Использует `role="alert"` для немедленного объявления скринридером
### LoadingState
- `aria-live="polite"` — скринридер объявит об изменении после завершения текущей речи

View File

@ -0,0 +1,259 @@
# Компоненты
## Матрица «задача → компонент»
| Задача | Рекомендуемый компонент | Не использовать |
|--------|------------------------|-----------------|
| Отображение текста | Text, Heading | MUI Typography напрямую |
| Ссылка | Link | MUI Link напрямую |
| Действие | Button, IconButton | MUI Button напрямую |
| Ввод текста | TextField | MUI TextField напрямую |
| Выбор из списка | Select | MUI Select напрямую |
| Выбор опции | Checkbox | MUI Checkbox напрямую |
| Контейнер/фон | Surface | MUI Paper напрямую |
| Карточка | Card | MUI Card напрямую |
| Ярлык/тег | Chip | MUI Chip напрямую |
| Бейдж | Badge | MUI Badge напрямую |
| Уведомление | Alert | MUI Alert напрямую |
| Модальное окно | Dialog | MUI Dialog напрямую |
| Загрузка скелета | Skeleton | MUI Skeleton напрямую |
| Индикатор прогресса | Progress | MUI CircularProgress / LinearProgress напрямую |
| Финансовое значение | Money, Metric, PriceChange | сырое число |
| Таблица | DataTable | MUI Table напрямую |
| Поле формы | FormField | ручная связка label + helperText |
| Панель фильтров | FilterBar | ручная вёрстка фильтров |
| Пустое состояние | EmptyState | ручная вёрстка |
| Ошибка | ErrorState | ручная вёрстка |
| Загрузка | LoadingState | ручная вёрстка |
## Каталог компонентов
### Text
Отображение текста с заданным семантическим размером.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `variant` | `'body1' \| 'body2' \| 'caption' \| 'overline'` | `'body1'` |
| `color` | `'primary' \| 'secondary' \| 'disabled' \| 'inverse'` | `'primary'` |
### Heading
Заголовок.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `variant` | `'h1' \| 'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6'` | `'h3'` |
### Link
Ссылка. Под капотом MUI Link с цветом `color.action.primary`.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `href` | `string` | — |
| `underline` | `'none' \| 'hover' \| 'always'` | `'hover'` |
### Button
Кнопка действия. Обёртка MUI Button.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `variant` | `'primary' \| 'secondary' \| 'danger'` | `'primary'` |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
| `loading` | `boolean` | `false` |
### IconButton
Кнопка-иконка. **Требует `label`** (aria-label).
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `label` | `string` | — |
| `size` | `'sm' \| 'md'` | `'md'` |
### TextField
Текстовое поле ввода.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `size` | `'sm' \| 'md'` | `'md'` |
| `fullWidth` | `boolean` | `true` |
### Select
Выпадающий список.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `size` | `'sm' \| 'md'` | `'md'` |
| `fullWidth` | `boolean` | `true` |
### Checkbox
Чекбокс.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `label` | `string` | — |
### Surface
Базовый контейнер с фоном и скруглением.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `variant` | `'default' \| 'subtle' \| 'raised'` | `'default'` |
### Card
Карточка. Поверхность с тенью.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `padding` | `'sm' \| 'md' \| 'lg'` | `'md'` |
### Chip
Компактный элемент для отображения тега или статуса.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `variant` | `'filled' \| 'outlined'` | `'filled'` |
| `color` | `'default' \| 'info' \| 'success' \| 'warning' \| 'error'` | `'default'` |
### Badge
Бейдж с числовым или текстовым значением.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `variant` | `'dot' \| 'standard'` | `'standard'` |
| `color` | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` |
### Alert
Уведомление. Использует `role="alert"`.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `severity` | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` |
| `title` | `string` | — |
### Dialog
Модальное окно. **Требует `title`**, содержит focus trap, закрывается по Escape.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `open` | `boolean` | — |
| `title` | `string` | — |
| `onClose` | `() => void` | — |
### Skeleton
Скелетон загрузки. `aria-hidden` по умолчанию.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `width` | `string \| number` | `'100%'` |
| `height` | `string \| number` | `20` |
### Progress
Индикатор прогресса. Поддерживает indeterminate-режим с `aria-busy`.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `variant` | `'determinate' \| 'indeterminate'` | `'indeterminate'` |
| `value` | `number` (0100) | `0` |
### DataTable
Таблица данных на основе TanStack Table.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `columns` | `ColumnDef<T>[]` | — |
| `data` | `T[]` | — |
| `caption` | `string` | — |
| `density` | `'balanced' \| 'compact'` | `'balanced'` |
### Money
Финансовое значение с форматированием через `Intl.NumberFormat`.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `value` | `number` | — |
| `currency` | `string` | `'RUB'` |
| `locale` | `string` | `'ru-RU'` |
### Metric
Метрика: лейбл + значение + опционально supporting/trend.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `label` | `string` | — |
| `value` | `ReactNode` | — |
| `supporting` | `ReactNode` | — |
| `trend` | `'up' \| 'down' \| 'neutral'` | — |
### PriceChange
Изменение цены. Использует **текстовое направление** (не только цвет).
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `value` | `number` | — |
| `percent` | `number` | — |
| `variant` | `'absolute' \| 'percent' \| 'both'` | `'both'` |
### FormField
Обёртка для поля формы: label + helperText + error.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `label` | `string` | — |
| `error` | `string` | — |
| `helperText` | `string` | — |
### FilterBar
Панель фильтров с responsive-обёрткой.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `children` | `ReactNode` | — |
### EmptyState
Пустое состояние. **Требует `title`**.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `title` | `string` | — |
| `description` | `string` | — |
| `action` | `ReactNode` | — |
### ErrorState
Состояние ошибки. **Требует `title`**.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `title` | `string` | — |
| `description` | `string` | — |
| `onRetry` | `() => void` | — |
### LoadingState
Состояние загрузки. `aria-live="polite"`.
| Свойство | Тип | По умолчанию |
|----------|-----|--------------|
| `title` | `string` | — |

View File

@ -0,0 +1,52 @@
# Фундаментальные токены
## Архитектура
Система токенов состоит из трёх уровней:
```text
Primitives ──► Semantic ──► Component
(сырые (алиасы (алиасы для
значения) назначения) компонентов)
```
### Primitives
Хранят абсолютные значения: конкретные цвета, размеры, шрифты. Не несут семантической нагрузки.
Пример: `color.neutral.900``#1a1a1a`, `space.4``32px`.
### Semantic
Алиасы, ссылающиеся на примитивы и описывающие назначение. Используются в приложении и компонентах.
Пример: `color.text.primary``{color.neutral.900}``#1a1a1a`.
### Component
Алиасы для конкретных компонентов. Позволяют переопределять токены на уровне компонента без изменения семантических токенов.
Пример: `button.bg``{color.action.primary}``{color.green.700}`.
## Резолвер
Используется DTCG-подобный резолвер (Design Tokens Community Group):
- Цепочная резолюция: `component → semantic → primitive`
- Проверка циклов
- Проверка типов (`$type` должен совпадать)
- Кэширование результатов
## Токены v1
В версии 1 все токены светлые (light-only). Ключевые группы:
- **color** — цвета (canvas, surface, text, border, action, feedback, finance)
- **typography** — шрифты (family, weight)
- **spacing** — отступы (inset, stack, inline)
- **shape** — скругления (в примитивах)
- **shadow** — тени (в примитивах)
- **duration** — длительности анимаций (в примитивах)
- **easing** — функции сглаживания (в примитивах)
Полный список семантических токенов — [Токены](tokens).

View File

@ -0,0 +1,38 @@
# Управление (Governance)
## Режим v1
- **Только светлая тема**. Тёмная тема отложена до v2.
- Изменения, затрагивающие цветовую палитру или контрастность, должны проверяться на WCAG 2.2 AA.
## Процесс добавления компонентов
Новый компонент проходит полный цикл:
1. **Spec** — спецификация API компонента
2. **Plan** — план реализации
3. **TDD** — тесты пишутся до реализации
4. **Storybook** — интерактивный каталог с a11y-аддоном
5. **Review** — код-ревью с проверкой accessibility
## Breaking changes
Любое изменение публичного API компонента или токена требует:
- Обновления мажорной версии
- Миграционной заметки в `CHANGELOG.md`
## Импорты MUI
Прямые импорты из MUI в коде приложения (`apps/frontend`) запрещены, за исключением allowlist:
| Компонент | Разрешён |
|-----------|----------|
| Box | ✅ |
| Stack | ✅ |
| Grid | ✅ |
| Typography | ❌ → использовать Text / Heading |
| Button | ❌ → использовать Button |
| Остальные MUI | ❌ |
Нарушение контролируется ESLint-правилом `no-restricted-imports` (уровень `warn`).

View File

@ -0,0 +1,42 @@
# Дизайн-система
## Что это
`@moex-vibe/design-system` — внутренний пакет дизайн-системы (v0.1.0). Построен на [MUI 6.5](https://mui.com/material-ui/). Поддерживает только светлую тему (light-only).
## Архитектура токенов
Трёхуровневая система токенов:
1. **Primitives** — сырые значения (цвета, размеры, шрифты)
2. **Semantic** — алиасы, описывающие назначение (`color.text.primary`)
3. **Component** — алиасы для конкретных компонентов (`button.bg`)
Подробнее — [Токены](foundations).
## Компоненты
Библиотека включает 25 компонентов:
| Категория | Компоненты |
|-----------|------------|
| Typography | Text, Heading, Link |
| Actions | Button, IconButton |
| Inputs | TextField, Select, Checkbox |
| Surfaces | Surface, Card |
| Feedback | Chip, Badge, Alert, Dialog, Skeleton, Progress |
| Financial | Money, Metric, PriceChange |
| Form | FormField, FilterBar |
| Page States | EmptyState, ErrorState, LoadingState |
| Data | DataTable |
Подробнее — [Компоненты](components).
## Инструменты
- **Storybook** — инженерный стенд для разработки и визуального ревью
- **Docusaurus** — опубликованная документация (этот сайт)
## ADR
Архитектурное решение описано в [ADR-016](../adr/ADR-016-design-system).

View File

@ -0,0 +1,94 @@
# Паттерны
## Финансовые данные
### Money
Форматирование через `Intl.NumberFormat`:
- Локаль: `ru-RU`
- Валюта по умолчанию: `RUB`
```tsx
<Money value={1234.5} /> // 1 234,50 ₽
<Money value={1234.5} currency="USD" /> // 1 234,50 $
```
### PriceChange
Отображает изменение цены. Обязательное правило: **никогда не использовать только цвет**. Всегда добавлять текстовый индикатор направления (`+`, ``, `—`).
| Направление | Индикатор | Цвет токена |
|-------------|-----------|-------------|
| Рост | `+` | `color.finance.positive` |
| Падение | `` | `color.finance.negative` |
| Нейтрально | `—` | `color.finance.neutral` |
### Metric
Композитный компонент: label + value + опционально supporting (доп. текст) и trend (направление).
```tsx
<Metric
label="Общая доходность"
value={<Money value={12345} />}
trend="up"
/>
```
## Формы
### FormField
Обёртка для поля, связывающая label, helperText и error через ассоциацию `htmlFor`/`id`.
```tsx
<FormField label="Название" error="Обязательное поле">
<TextField />
</FormField>
```
### FilterBar
Responsive-панель фильтров с автоматическим переносом (wrap).
```tsx
<FilterBar>
<Select label="Тип" />
<TextField label="Поиск" />
</FilterBar>
```
## Состояния страницы
### EmptyState, ErrorState, LoadingState
Три компонента для трёх состояний любой страницы или секции:
- **EmptyState** — данных нет, `title` обязателен
- **ErrorState** — ошибка загрузки, `title` обязателен, `onRetry` для повтора
- **LoadingState** — загрузка, `title` обязателен, `aria-live="polite"`
```tsx
{isLoading && <LoadingState title="Загрузка портфеля" />}
{error && <ErrorState title="Ошибка загрузки" onRetry={refetch} />}
{data && data.length === 0 && <EmptyState title="Нет операций" />}
```
## Таблицы
### DataTable
Обёртка над TanStack Table с обязательным `caption` для доступности. Два режима плотности:
- **balanced** (по умолчанию) — отступы md
- **compact** — отступы sm, для плотных таблиц
```tsx
<DataTable
columns={columns}
data={rows}
caption="Список операций"
density="compact"
/>
```

View File

@ -0,0 +1,106 @@
# Семантические токены
Компонентные токены документированы на страницах соответствующих компонентов.
## Color
### Canvas
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `color.canvas` | `{color.neutral.50}` | `#f5f5f5` |
### Surface
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `color.surface.default` | `{color.neutral.0}` | `#ffffff` |
| `color.surface.subtle` | `{color.neutral.50}` | `#f5f5f5` |
| `color.surface.raised` | `{color.neutral.0}` | `#ffffff` |
### Text
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `color.text.primary` | `{color.neutral.900}` | `#1a1a1a` |
| `color.text.secondary` | `{color.neutral.600}` | `#666666` |
| `color.text.disabled` | `{color.neutral.400}` | `#999999` |
| `color.text.inverse` | `{color.neutral.0}` | `#ffffff` |
### Border
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `color.border.subtle` | `{color.neutral.100}` | `#e0e0e0` |
| `color.border.default` | `{color.neutral.200}` | `#cccccc` |
| `color.border.strong` | `{color.neutral.400}` | `#999999` |
| `color.border.focus` | `{color.blue.600}` | `#1e88e5` |
### Action
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `color.action.primary` | `{color.green.700}` | `#388e3c` |
| `color.action.primaryHover` | `{color.green.800}` | `#2e7d32` |
| `color.action.secondary` | `{color.neutral.600}` | `#666666` |
| `color.action.danger` | `{color.red.600}` | `#e53935` |
### Feedback
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `color.feedback.info` | `{color.blue.600}` | `#1e88e5` |
| `color.feedback.success` | `{color.green.600}` | `#43a047` |
| `color.feedback.warning` | `{color.amber.600}` | `#ffb300` |
| `color.feedback.error` | `{color.red.600}` | `#e53935` |
### Finance
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `color.finance.positive` | `{color.green.700}` | `#388e3c` |
| `color.finance.negative` | `{color.red.700}` | `#d32f2f` |
| `color.finance.neutral` | `{color.neutral.600}` | `#666666` |
## Typography
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `font.family.body` | `{font.family.sans}` | `Inter, system-ui, sans-serif` |
| `font.weight.body` | `{font.weight.regular}` | `400` |
| `font.weight.heading` | `{font.weight.semibold}` | `600` |
| `font.weight.strong` | `{font.weight.bold}` | `700` |
## Spacing
### Inset (внутренние отступы)
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `space.inset.sm` | `{space.2}` | `8px` |
| `space.inset.md` | `{space.3}` | `12px` |
| `space.inset.lg` | `{space.4}` | `16px` |
### Stack (вертикальные отступы)
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `space.stack.sm` | `{space.1}` | `4px` |
| `space.stack.md` | `{space.2}` | `8px` |
| `space.stack.lg` | `{space.4}` | `16px` |
### Inline (горизонтальные отступы)
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `space.inline.sm` | `{space.1}` | `4px` |
| `space.inline.md` | `{space.2}` | `8px` |
| `space.inline.lg` | `{space.3}` | `12px` |
## Size / Focus
| Токен | Значение | Разрешение |
|-------|----------|------------|
| `focus.ring.width` | `{size.focus.ring}` | `2px` |
| `focus.ring.offset` | `{size.focus.offset}` | `2px` |
| `focus.ring.color` | `{color.blue.600}` | `#1e88e5` |

View File

@ -2,7 +2,17 @@
## Подход
CSS через единый `styles.css` с CSS custom properties. Без CSS-in-JS или Tailwind.
Управление стилями осуществляется через пакет [дизайн-системы](../design-system/overview) `@moex-vibe/design-system`, который предоставляет:
- Систему токенов (primitives → semantic → component)
- MUI 6.5 тему, построенную на токенах
- CSS-переменные `--mv-*` для доступа из обычного CSS
Все новые компоненты должны использовать токены дизайн-системы. Подробнее — [Дизайн-система](../design-system/overview).
## Legacy: CSS custom properties
Ранее стили определялись через единый `styles.css` с CSS custom properties. Этот подход считается устаревшим — новые страницы должны использовать токены дизайн-системы.
## CSS custom properties

View File

@ -39,6 +39,19 @@ const sidebars: SidebarsConfig = {
label: 'Инфраструктура',
items: ['infrastructure/docker', 'infrastructure/ci'],
},
{
type: 'category',
label: 'Дизайн-система',
items: [
'design-system/overview',
'design-system/foundations',
'design-system/tokens',
'design-system/components',
'design-system/patterns',
'design-system/accessibility',
'design-system/governance',
],
},
{
type: 'category',
label: 'Разработка',

View File

@ -0,0 +1,11 @@
# Changelog
## 0.1.0 (2026-06-21)
### Added
- Token system: primitives, semantic (light), and component tokens with DTCG-like resolver
- Theme adapter: MUI 6.5 theme with CSS variables (`--mv-*`)
- Components: 20+ production-ready wrappers (Typography, Actions, Inputs, Surfaces, Feedback, Financial Data, Form, Page States)
- Storybook: interactive catalog with a11y checks
- First stable public API