diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..c7287d2 --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,109 @@ +# План: единая дизайн-система + +Статус: план к выполнению. Следующий этап после MVP, до UX-работ из backlog (реактивные параметры, слайдеры/пипетка, раскладка). + +## 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`; слайдеры/пипетка из backlog потребуют копирования. +- **Блок «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()`, показ значения рядом | ничего — база для backlog п.2 (яркость, качество, порог) | +| `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`, слот действия | «Обработка…», будущие пустые панели исходника/результата (backlog п.3) | + +`Field` — чисто раскладочная обёртка (`
`), ничего не знает о типе контрола. На ней построены `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`. +- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда потребуется реальная задача.