# План: единая дизайн-система Статус: план к выполнению. Следующий этап после 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`. - Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда потребуется реальная задача.