141 lines
10 KiB
Markdown
141 lines
10 KiB
Markdown
# План: единая дизайн-система
|
||
|
||
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
|
||
|
||
## 1. Цели и принципы
|
||
|
||
1. **Один источник правды** — все визуальные константы живут в токенах
|
||
`web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры
|
||
напрямую.
|
||
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один
|
||
раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
|
||
3. **Простые компоненты важнее умных** — если проще сделать отдельные
|
||
`SliderField` и `TextField`, делаем два маленьких компонента, а не один
|
||
конфигурируемый монстр.
|
||
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка,
|
||
отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
|
||
|
||
## 2. Аудит: что дублируется прямо сейчас
|
||
|
||
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в
|
||
семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc),
|
||
лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label),
|
||
`InfoPanel` (dt).
|
||
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная
|
||
копипаста в `ToolPage` и на главной.
|
||
- **Базовые инпуты** (padding, border, radius) — живут только внутри
|
||
`ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы
|
||
копировать вручную.
|
||
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается
|
||
обёрткой `Field.svelte`.
|
||
- **«Панель»** (surface + border + radius) — три ручные реализации одного
|
||
паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
|
||
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента;
|
||
состояния «занят/недоступен» каждый описывает заново.
|
||
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в
|
||
workspace.
|
||
|
||
## 3. Токены (расширение `app.css`)
|
||
|
||
Существующие остаются; добавляются недостающие группы:
|
||
|
||
```css
|
||
:root {
|
||
/* типографика */
|
||
--text-xs: 0.8rem;
|
||
--text-s: 0.85rem;
|
||
--text-m: 0.95rem;
|
||
--text-l: 1.1rem;
|
||
--text-xl: 1.25rem;
|
||
--text-xxl: 1.75rem;
|
||
--leading-tight: 1.25;
|
||
|
||
/* тени */
|
||
--shadow-card: 0 4px 14px rgb(37 99 235 / 12%);
|
||
|
||
/* фокус */
|
||
--focus-ring: 2px solid var(--accent);
|
||
|
||
/* контрольные размеры контролов */
|
||
--control-height: 2.4rem;
|
||
--control-max-width: 16rem;
|
||
}
|
||
```
|
||
|
||
Правило именования: семантика, не значение (`--text-muted`, а не `--gray-500`).
|
||
|
||
## 4. Глобальные утилиты (`app.css`)
|
||
|
||
Только то, что не требует JS и не является интерактивным:
|
||
|
||
- `.text-muted` — приглушённый текст (размер наследуется);
|
||
- `.text-caption` — мелкий текст `--text-s`;
|
||
- `.heading-section` — заголовок секции капсом (вместо двух копипаст);
|
||
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки,
|
||
InfoPanel-строки);
|
||
- `.error-banner` — баннер ошибки;
|
||
- `.visually-hidden` — скрытый, но читаемый скринридерами.
|
||
|
||
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать —
|
||
внешний вид контролов переезжает в компоненты `ui/`.
|
||
|
||
## 5. Состав UI-компонентов (`web/src/lib/components/ui/`)
|
||
|
||
Все — тонкие обёртки над нативными элементами, без состояний кроме своих:
|
||
|
||
| Компонент | Пропы | Заменяет |
|
||
| ---------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||
| `Field.svelte` | `id`, `label`, `hint?`, слот контрола | label + раскладка поля; единая точка для hint'ов и ошибок валидации |
|
||
| `Button.svelte` | `variant: 'primary' \| 'secondary'`, `disabled`, `busy`, слот текста | глобальные `.primary`/`.secondary`, локальный busy в `DownloadButton` |
|
||
| `TextField.svelte` | `id`, `label`, `type: 'number' \| 'text'`, `min/max/step`, `value = $bindable()` | number/text ветки `ParamForm` |
|
||
| `SliderField.svelte` | `id`, `label`, `min/max/step`, `value = $bindable()`, показ значения рядом | ничего — готовая база для будущих слайдеров (яркость, качество, порог) |
|
||
| `SelectField.svelte` | `id`, `label`, `options`, `value = $bindable()` | select-ветка `ParamForm` |
|
||
| `CheckboxField.svelte` | `id`, `label`, `checked = $bindable()` | чекбокс `ParamForm` |
|
||
| `ColorField.svelte` | `id`, `label`, `value = $bindable()` | color-ветка `ParamForm`; точка будущего расширения пипеткой |
|
||
| `EmptyState.svelte` | `title`, `hint`, слот действия | «Обработка…», будущие пустые панели исходника/результата |
|
||
|
||
`Field` — чисто раскладочная обёртка
|
||
(`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о
|
||
типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`,
|
||
`ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол
|
||
(кликабельная строка), поэтому у него своя раскладка.
|
||
|
||
## 6. Миграция существующего
|
||
|
||
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная
|
||
стилизация контролов и label удаляется.
|
||
2. `DownloadButton` и кнопки `ToolPage` — на `Button`.
|
||
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои
|
||
раскладочные стили.
|
||
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки
|
||
секций на `.heading-section`, баннер на `.error-banner`.
|
||
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после
|
||
перехода.
|
||
|
||
## 7. Порядок работ
|
||
|
||
| Этап | Содержимое | Проверка |
|
||
| ------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
|
||
| A. Фундамент | Токены + утилиты в `app.css` | `pnpm check` |
|
||
| B. Примитивы | Восемь компонентов в `components/ui/` | `pnpm check`; каждый используется хотя бы раз после этапа C |
|
||
| C. Миграция | Переделка пяти файлов из §6 | Визуально: страницы выглядят как раньше; `test/check/build` зелёные |
|
||
| D. Чистка | Удаление устаревших классов и дублей из scoped-стилей | grep: в `components/` нет hex-цветов и `var(--space…)` вне раскладки |
|
||
|
||
## 8. Критерии готовности
|
||
|
||
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов
|
||
системы.
|
||
- Все интерактивные контролы на страницах — экземпляры `ui/*`.
|
||
- `grep -E '#[0-9a-f]{3,6}' web/src/lib/components` не находит ничего, кроме
|
||
токенов в `app.css`.
|
||
- Визуальной регрессии нет (ручная сверка всех страниц до/после), тесты и билд
|
||
зелёные.
|
||
|
||
## 9. Что сознательно не делаем
|
||
|
||
- Никаких CSS-фреймворков и препроцессоров — обычный CSS, как решено на старте.
|
||
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема
|
||
добавилась заменой значений `:root`.
|
||
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда
|
||
потребуется реальная задача.
|