Дизайн-система
Визуальный дизайн Elftia стремится к теплоте, дружелюбию и изысканности, избегая холодной, сугубо технической эстетики.
Философия дизайна
Принципы цвета
- Тёплые нейтральные цвета: все цвета поверхностей имеют слегка тёплый тон (hue 24–38), а не чистые оттенки серого
- Тёмный режим: тёплый тёмно-серый (warm charcoal) вместо чистого чёрного
- Светлый режим: тёплый кремовый (warm cream) вместо чистого белого
- Никаких холодных серых: не использовать чистый серый (
hsl(0, 0%, ...)) для фонов или рамок
Принципы скругления углов
| Элемент | Радиус | Класс Tailwind |
|---|---|---|
| Карточки/контейнеры | 12px | rounded-xl |
| Поля ввода | 12px | rounded-xl |
| Кнопки/значки | 6px | rounded-md |
| По умолчанию | 8px | rounded-lg |
Не использовать скругление меньше 4px (кроме линейных декоративных элементов).
Принципы типографики
| Назначение | Шрифт | Класс Tailwind |
|---|---|---|
| Витрина / крупные заголовки | Noto Serif / Georgia | font-display |
| Текст / интерфейс | Inter | font-sans |
| Код | JetBrains Mono | font-mono |
Для заголовков использовать font-semibold (не font-bold) в сочетании с tracking-tight.
Цветовая система
Семантические токены
Все цвета задаются через CSS-переменные, которые отображаются на утилитарные классы в конфигурации Tailwind. Жёстко заданные цветовые значения использовать запрещено.
// Нельзя
<div className="bg-white text-black border-gray-200">
<div style={{ backgroundColor: '#ffffff' }}>
// Правильно
<div className="bg-surface-0 text-foreground border-border">
<div style={{ backgroundColor: 'var(--surface-0)' }}>
Справочник общих токенов
| Назначение | Класс Tailwind | CSS-переменная |
|---|---|---|
| Фон страницы | bg-background | var(--background) |
| Фон L0 | bg-surface-0 | var(--surface-0) |
| Фон L1 (карточки/боковая панель) | bg-surface-1 | var(--surface-1) |
| Фон L2 (поля ввода/вторичные контейнеры) | bg-surface-2 | var(--surface-2) |
| Фон L3 (поповеры) | bg-surface-3 | var(--surface-3) |
| Основной текст | text-foreground | var(--foreground) |
| Вторичный текст | text-muted-foreground | var(--muted-foreground) |
| Вспомогательный текст | text-text-subtle | var(--text-subtle) |
| Рамка | border-border | var(--border) |
| Тематический цвет | bg-primary / text-primary | var(--primary) |
| Успех | text-success | var(--success) |
| Ошибка | text-destructive | var(--destructive) |
| Предупреждение | text-warning | var(--warning) |
Тёмный/светлый режим
Механизм переключения
Используется стратегия класса Tailwind (darkMode: 'class'), централизованно управляемая через ThemeContext.
// Правильно: получать информацию о теме через useTheme
import { useTheme } from '@/shared/state/themeStore';
function MyComponent() {
const { mode, resolvedMode, userTheme } = useTheme();
}
// Нельзя: ручное определение
const isDark = localStorage.getItem('theme') === 'dark';
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
Иерархическая система
В тёмном режиме иерархия различается по яркости: элементы UI, ближайшие к пользователю, имеют более светлый фон.
| Уровень | Токен | Яркость | Назначение | Референсный цвет |
|---|---|---|---|---|
| L0 | --surface-0 | 7% | Основной фон страницы | #121212 |
| L1 | --surface-1 | 12% | Боковая панель, карточки | #1E1E1E |
| L2 | --surface-2 | 17% | Вторичные контейнеры, поля ввода | #2B2928 |
| L3 | --surface-3 | 22% | Поповер, Dropdown | #383635 |
Разница в яркости между уровнями должна быть >= 4–5%, чтобы обеспечить визуальное различие.
Стандарты рамок
В тёмном режиме человеческое восприятие тёмных областей менее чувствительно, поэтому рамки должны быть более заметными:
| Сценарий | Светлый режим | Тёмный режим |
|---|---|---|
| Рамка карточки | border-border/30 ~ /40 | border-border/50 ~ /70 |
| Разделитель | border-border/20 ~ /30 | border-border/40 ~ /50 |
| Поле ввода | border-border/40 | border-border/60 ~ border-border |
Доступность WCAG
Основано на стандартах WCAG 2.1.
Требования к контрасту
| Тип элемента | Минимальный контраст | Описание |
|---|---|---|
| Обычный текст (< 18pt) | 4.5:1 | Основной текст, описания, метки |
| Крупный текст (>= 18pt или 14pt жирный) | 3:1 | Заголовки |
| UI-элементы управления (иконки, рамки) | 3:1 | Рамки полей ввода, иконки, значки |
| Отключённое состояние | Исключение, рекомендуется 2.5:1 | Избегать полной невидимости |
Правила использования текстовых токенов
| Токен | Яркость | Допустимые фоны | Типичное применение |
|---|---|---|---|
text-foreground (93%) | Наивысшая | Все поверхности | Основные заголовки, основной текст |
text-muted-foreground (65%) | Средняя | surface-0, surface-1 | Вторичный текст, описания |
text-text-subtle (50%) | Низкая | Только surface-0 | Временные метки, метаданные |
// Хорошо: описательный текст использует text-muted
<p className="text-muted-foreground">5 models total</p>
// Плохо: использование text-subtle на карточке surface-1 (недостаточный контраст)
<div className="bg-surface-1">
<span className="text-text-subtle">Hard to read</span>
</div>
Цвет как носитель информации
Никогда не полагаться исключительно на цвет для передачи состояния — необходимо дополнять текстовой меткой или иконкой:
// Плохо: только цвет
<div className={status === 'error' ? 'border-red-500' : 'border-border'} />
// Хорошо: цвет + иконка + текст
<div className={status === 'error' ? 'border-destructive' : 'border-border'}>
{status === 'error' && <AlertCircle className="text-destructive" />}
<span>{errorMessage}</span>
</div>
Состояние фокуса
Использовать focus-visible для обеспечения рамки фокуса пользователям клавиатурной навигации:
// Хорошо
<button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none">
Button
</button>
// Нельзя: убирать стили фокуса
<button className="outline-none focus:outline-none">Button</button>
Предпочтения анимации
Соблюдать системную настройку prefers-reduced-motion:
<div className="motion-safe:animate-fadeIn motion-reduce:animate-none">
Content
</div>
Стандарты UI-компонентов
Не использовать нативные элементы управления
| Нативный элемент | Компонент проекта | Путь |
|---|---|---|
<select> | Select | @/components/ui/select |
<input type="text"> | Input | @/components/ui/input |
<input type="checkbox"> | Switch / Checkbox | @/components/ui/switch |
<button> | Button | @/components/ui/button |
window.confirm() | ConfirmDialog | @/components/ui/confirm-dialog |
window.alert() | Toast-компонент | - |
Компоненты выпадающего списка
Все компоненты выпадающего списка должны поддерживать:
- Позиционирование с учётом области просмотра (использовать хук
useDropdownPosition) - Закрытие при клике за пределами
- Закрытие по клавише Escape
- Тематические цвета и стили элементов
Система прозрачности обоев
Когда пользователь устанавливает обои, к body добавляется атрибут data-wallpaper-active="true", активирующий CSS-правила прозрачности.
Иерархия CSS-правил
| Приоритет | Селектор | Эффект | Назначение |
|---|---|---|---|
| 1 | .bg-background, .bg-surface-0 | Полностью прозрачный | Основной фон страницы |
| 2 | .wallpaper-blur | 35% непрозрачности + blur | Главный контейнер (WorkspaceShell) |
| 3 | .wallpaper-blur .wallpaper-blur | Прозрачный + blur*0.67 | Вложенный контейнер (во избежание наложения) |
| 4 | bg-surface-0/XX внутри .wallpaper-blur | Прозрачный | Панели макета |
| 5 | bg-surface-1/XX внутри .wallpaper-blur | 12% + blur | Контентные карточки (alpha-вариант) |
| 6 | bg-surface-1, bg-popover | 15% + blur | Кнопки/карточки (исключая input) |
| 7 | bg-surface-2 | 20% + blur | Вторичный контейнер (исключая input) |
| 8 | .wallpaper-panel | 85% + blur*1.33 | Плавающий dropdown/меню |
| 9 | .wallpaper-solid | Непрозрачный surface-0 | Dialog/Modal |
Модель иерархического наложения
WorkspaceShell (wallpaper-blur, 35%)
+-- Sidebar (bg-surface-0/75 -> transparent) = 35%
+-- Main content (bg-background -> transparent) = 35%
| +-- Content cards (bg-surface-1/80 -> 12%) ~ 43%
| +-- Buttons (bg-surface-1 -> 15%) ~ 45%
| +-- Input (input -> solid) = Opaque
| +-- Dropdown panel (wallpaper-panel -> 85%) = 85%
+-- Bottombar (wallpaper-blur -> transparent) = 35%
Руководство по использованию CSS-классов обоев
| Сценарий | Рекомендация |
|---|---|
| Главный контейнер страницы | bg-background или bg-surface-0 (автоматически полностью прозрачный) |
| Кнопка/карточка | bg-surface-1 или bg-surface-1/80 (автоматически полупрозрачный) |
| Вторичный контейнер | bg-surface-2 (автоматически 20% полупрозрачности) |
| Главный контейнер макета | Добавить класс wallpaper-blur |
| Плавающий dropdown/меню | Добавить класс wallpaper-panel |
| Dialog/Modal | Добавить класс wallpaper-solid |
| Поле ввода | <input> / <textarea> + bg-surface-1 (автоматически исключается, остаётся непрозрачным) |
Компоненты со встроенной поддержкой обоев
Полупрозрачное матовое стекло (wallpaper-panel):
- Панель выпадающего списка
Select DropdownMenuContent/DropdownMenuSubContentContextMenuContent/ContextMenuSubContent
Полностью непрозрачный (wallpaper-solid):
DialogContent
Запрещено
- Не использовать голый
bg-surface-0в качестве фона карточки (в режиме обоев он становится полностью прозрачным) - Не использовать жёстко заданные цвета
bg-white,bg-black,bg-gray-*,bg-neutral-* - Для непрозрачного эффекта одновременно добавлять класс
wallpaper-solid
Пользовательские настраиваемые слои цветового наложения (0.1.11+)
Поверх описанной выше «базовой системы прозрачности обоев» панель обоев предоставляет три категории пользовательских цветовых слоёв, каждый из которых управляется через data-атрибуты body + CSS-переменные; CSS-селекторы послойно переопределяют поведение surface по умолчанию. Все операции записи централизованно управляются через themeUtils.applyWallpaperToDocument — компоненты не должны напрямую вызывать body.style.setProperty.
1. Слой затемнения (псевдоэлемент body::before)
| Data-атрибут | Условие срабатывания | CSS-переменная |
|---|---|---|
data-wallpaper-active="true" | Любой источник обоев готов | --wp-dimming (0–1), --wp-dim-{h,s,l} |
data-wp-dim-gradient="true" | Установлено wallpaperDimmingGradient | --wp-dim-gradient (переопределяет HSL) |
Запасной вариант яркости: если --wp-dim-l не задана, светлый режим по умолчанию принимает 100%, тёмный — 0% (соответствует исходному поведению white/black).
2. Слой поверхностей элементов (боковая панель / карточки / вкладки / контекстное меню)
| Data-атрибут | Условие срабатывания | CSS-переменная |
|---|---|---|
data-wp-element-tint="true" | wallpaperElementTint является валидным hex | --wp-elem-{h,s,l} |
data-wp-element-gradient="true" | Установлено wallpaperElementGradient | --wp-elem-gradient-{15,20,35} (по alpha-вариантам уровней surface) |
Ключевой принцип дизайна: каждый уровень surface сохраняет независимое значение alpha (surface-1 = 15%, surface-2 = 20%, .wallpaper-card = 35%), поэтому даже при изменении в один тон визуальная иерархия остаётся различимой. Селекторы Input/Textarea, wallpaper-solid и wallpaper-panel исключаются через :not(...) — читаемость важнее цветовой согласованности.
3. Слой пузырей сообщений (пользователь / ассистент раздельно)
| Data-атрибут | Условие срабатывания | CSS-переменная |
|---|---|---|
data-wp-bubble-override="true" | wallpaperBubbleOverride === true | --wp-bubble-alpha (0–1) |
data-wp-bubble-tint-user="true" | Hex пузыря пользователя валиден | --wp-bubble-user-{h,s,l} |
data-wp-bubble-tint-assistant="true" | Hex пузыря ассистента валиден | --wp-bubble-asst-{h,s,l} |
data-wp-bubble-gradient-{user,assistant}="true" | Соответствующий градиент задан | --wp-bubble-{user,asst}-gradient |
CSS-селекторы каскадируются в два слоя:
/* Слой 1: при выключенном override пузыри наследуют тонирование элементов */
body[data-wp-element-tint="true"]:not([data-wp-bubble-override="true"]) .chat-bubble-user,
body[data-wp-element-tint="true"]:not([data-wp-bubble-override="true"]) .chat-bubble-assistant {
background: hsl(var(--wp-elem-h) var(--wp-elem-s) var(--wp-elem-l) / var(--wp-alpha-35, 0.35)) !important;
}
/* Слой 2: при включённом override применяется тонирование по сторонам + пользовательский alpha */
body[data-wp-bubble-override="true"][data-wp-bubble-tint-user="true"] .chat-bubble-user {
background: hsl(var(--wp-bubble-user-h) var(--wp-bubble-user-s) var(--wp-bubble-user-l) / var(--wp-bubble-alpha, var(--wp-alpha-35, 0.35))) !important;
}
Селектор слоя 2 имеет более высокую специфичность и расположен позже, поэтому при равенстве !important побеждает он.
При добавлении нового поля user-tint
- Добавить поле одновременно в
ThemePreferences(settings-types.ts) иThemePreferencesSchema(configSchema.ts) - Добавить ветку setter в
ThemeService.setWallpaperPreferences+ значения по умолчанию вreadPreferences/importProfile/resetTheme/mergePreferences - Добавить поле в оба Zod-схемы в
ThemeRouter(themeProfileSchemaи встроенную схему вtheme:setWallpaperPreferences) - Добавить значение контекста в
ThemeContext+ параметр setWallpaperPreferences + запасной вариант commitState - Добавить параметр в
themeUtils.applyWallpaperToDocument+ кэш_prev*+ запись атрибутов body/CSS-переменных - Добавить UI в
WallpaperPanel(переключатель/выбор цвета/ползунок) + i18n для трёх языков - Добавить селектор в
index.css(следить за порядком иерархии и приоритетом!important) - Цепочка передачи:
AppearanceTab→Settings.tsx/ThemeStudioPage.tsx(включая DraftState + effective + отправку Apply) - Заглушки агента:
desktop-api.ts(контракт preload),shared/agent/types/settings.ts(общий интерфейс),shared/agent/web/theme.ts(реализация HTTP) - Обновить данную таблицу + краткий справочник полей в
architecture-indexSKILL.md + таблицу полейtheme:setWallpaperPreferencesвipc-channels.md+ пользовательскую документациюappearance.md
Правила разработки с учётом темы
Использовать только семантические токены
Не писать напрямую #fff/rgb() или стандартные цвета Tailwind.
Единообразно использовать ThemeContext
Когда компоненту нужна информация о теме, читать её через useTheme().
Соблюдать настраиваемые шрифты
Для текстовых/кодовых областей использовать CSS-переменные:
<div style={{ fontFamily: 'var(--font-ui)' }}>Normal text</div>
<code style={{ fontFamily: 'var(--font-code)' }}>Code</code>
// Или использовать классы Tailwind
<div className="font-ui">Normal text</div>
<code className="font-code">Code</code>
Разрешать переопределение через customCss
Избегать !important и объёмных встроенных стилей; отдавать предпочтение className + CSS-переменным.
Чек-лист самопроверки
При создании новых UI-компонентов:
- Использованы только семантические токены (без жёстко заданных цветов)
- Информация о теме получена через
useTheme() - Проверено переключение тёмного/светлого режима
- Проверен эффект прозрачности обоев
- Контраст обычного текста >= 4.5:1
- Рамки в тёмном режиме используют
dark:border-border/50или выше -
text-subtleиспользуется только на фонеsurface-0 - Интерактивные элементы используют семантические теги или содержат
role+tabIndex - Стиль фокуса задан через
focus-visible:ring-2 - Текст и рамки различимы при яркости экрана 30%
- Макет не ломается при масштабировании окна до 200%