10 KiB
План: единая дизайн-система
Статус: план к выполнению. Следующий этап после MVP, до UX-работ из backlog (реактивные параметры, слайдеры/пипетка, раскладка).
1. Цели и принципы
- Один источник правды — все визуальные константы живут в токенах
web/src/app.css; ни один компонент не пишет «магические» цвета/размеры напрямую. - Никакого дублирования — повторяющийся визуальный рецепт оформляется один раз: либо токен, либо глобальный утилитарный класс, либо компонент в
ui/. - Простые компоненты важнее умных — если проще сделать отдельные
SliderFieldиTextField, делаем два маленьких компонента, а не один конфигурируемый монстр. - 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)
Существующие остаются; добавляются недостающие группы:
: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 — чисто раскладочная обёртка (<div class="field"><label for>…</label><slot /></div>), ничего не знает о типе контрола. На ней построены TextField, SliderField, SelectField, ColorField. Исключение — CheckboxField: у чекбокса label оборачивает контрол (кликабельная строка), поэтому у него своя раскладка.
6. Миграция существующего
ParamForm— переписывается наField+*Field-компоненты; собственная стилизация контролов и label удаляется.DownloadButtonи кнопкиToolPage— наButton.DropZone, карточки главной, строкиInfoPanel— на.panel+ свои раскладочные стили.ToolPage, главная, лейаут — приглушённые тексты на.text-*, заголовки секций на.heading-section, баннер на.error-banner.- Из
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. - Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда потребуется реальная задача.