Перейти к основному содержимому

Дизайн-система

Визуальный дизайн Elftia стремится к теплоте, дружелюбию и изысканности, избегая холодной, сугубо технической эстетики.


Философия дизайна

Принципы цвета

  • Тёплые нейтральные цвета: все цвета поверхностей имеют слегка тёплый тон (hue 24–38), а не чистые оттенки серого
  • Тёмный режим: тёплый тёмно-серый (warm charcoal) вместо чистого чёрного
  • Светлый режим: тёплый кремовый (warm cream) вместо чистого белого
  • Никаких холодных серых: не использовать чистый серый (hsl(0, 0%, ...)) для фонов или рамок

Принципы скругления углов

ЭлементРадиусКласс Tailwind
Карточки/контейнеры12pxrounded-xl
Поля ввода12pxrounded-xl
Кнопки/значки6pxrounded-md
По умолчанию8pxrounded-lg

Не использовать скругление меньше 4px (кроме линейных декоративных элементов).

Принципы типографики

НазначениеШрифтКласс Tailwind
Витрина / крупные заголовкиNoto Serif / Georgiafont-display
Текст / интерфейсInterfont-sans
КодJetBrains Monofont-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)' }}>

Справочник общих токенов

НазначениеКласс TailwindCSS-переменная
Фон страницыbg-backgroundvar(--background)
Фон L0bg-surface-0var(--surface-0)
Фон L1 (карточки/боковая панель)bg-surface-1var(--surface-1)
Фон L2 (поля ввода/вторичные контейнеры)bg-surface-2var(--surface-2)
Фон L3 (поповеры)bg-surface-3var(--surface-3)
Основной текстtext-foregroundvar(--foreground)
Вторичный текстtext-muted-foregroundvar(--muted-foreground)
Вспомогательный текстtext-text-subtlevar(--text-subtle)
Рамкаborder-bordervar(--border)
Тематический цветbg-primary / text-primaryvar(--primary)
Успехtext-successvar(--success)
Ошибкаtext-destructivevar(--destructive)
Предупреждениеtext-warningvar(--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-07%Основной фон страницы#121212
L1--surface-112%Боковая панель, карточки#1E1E1E
L2--surface-217%Вторичные контейнеры, поля ввода#2B2928
L3--surface-322%Поповер, Dropdown#383635

Разница в яркости между уровнями должна быть >= 4–5%, чтобы обеспечить визуальное различие.

Стандарты рамок

В тёмном режиме человеческое восприятие тёмных областей менее чувствительно, поэтому рамки должны быть более заметными:

СценарийСветлый режимТёмный режим
Рамка карточкиborder-border/30 ~ /40border-border/50 ~ /70
Разделительborder-border/20 ~ /30border-border/40 ~ /50
Поле вводаborder-border/40border-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-blur35% непрозрачности + blurГлавный контейнер (WorkspaceShell)
3.wallpaper-blur .wallpaper-blurПрозрачный + blur*0.67Вложенный контейнер (во избежание наложения)
4bg-surface-0/XX внутри .wallpaper-blurПрозрачныйПанели макета
5bg-surface-1/XX внутри .wallpaper-blur12% + blurКонтентные карточки (alpha-вариант)
6bg-surface-1, bg-popover15% + blurКнопки/карточки (исключая input)
7bg-surface-220% + blurВторичный контейнер (исключая input)
8.wallpaper-panel85% + blur*1.33Плавающий dropdown/меню
9.wallpaper-solidНепрозрачный surface-0Dialog/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 / DropdownMenuSubContent
  • ContextMenuContent / 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

  1. Добавить поле одновременно в ThemePreferences (settings-types.ts) и ThemePreferencesSchema (configSchema.ts)
  2. Добавить ветку setter в ThemeService.setWallpaperPreferences + значения по умолчанию в readPreferences/importProfile/resetTheme/mergePreferences
  3. Добавить поле в оба Zod-схемы в ThemeRouter (themeProfileSchema и встроенную схему в theme:setWallpaperPreferences)
  4. Добавить значение контекста в ThemeContext + параметр setWallpaperPreferences + запасной вариант commitState
  5. Добавить параметр в themeUtils.applyWallpaperToDocument + кэш _prev* + запись атрибутов body/CSS-переменных
  6. Добавить UI в WallpaperPanel (переключатель/выбор цвета/ползунок) + i18n для трёх языков
  7. Добавить селектор в index.css (следить за порядком иерархии и приоритетом !important)
  8. Цепочка передачи: AppearanceTabSettings.tsx / ThemeStudioPage.tsx (включая DraftState + effective + отправку Apply)
  9. Заглушки агента: desktop-api.ts (контракт preload), shared/agent/types/settings.ts (общий интерфейс), shared/agent/web/theme.ts (реализация HTTP)
  10. Обновить данную таблицу + краткий справочник полей в architecture-index SKILL.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%