Files
vidconf/docs/architecture/frontend-themes.md
Max Ronzhin 8757bec8ac
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
first commit
2026-07-23 02:38:05 +03:00

11 KiB
Raw Blame History

Архитектура тем оболочки VidConf

Ссылки: design/DESIGN_SYSTEM.md §0§0.1, design/mockups/dark/README.md, frontend/README.md раздел «Темы оболочки»

Суть

Оболочка VidConf (auth, лобби, календарь, join, админка) поддерживает две темы:

  • Светлая — дефолт, индиго-на-белом
  • Тёмная — графит-мята-роза-янтарь

Переключатель (☀️/🌙) в UI; автодетект системной темы (prefers-color-scheme); выбор сохраняется в localStorage. Комната конференции — всегда своя тёмная тема, не затрагивается оболочкой.

Механизм переключения

Атрибут data-theme на <html>

:root                         → светлая (индиго-на-белом)
:root[data-theme="dark"]      → тёмная (мята-на-графите)
:root[data-theme="light"]     → светлая (явный выбор)
[data-theme="room"]           → комната (отдельный набор токенов, не переключается)

Приоритет:

  1. Явный выбор (data-theme="light" или data-theme="dark"), если есть
  2. Системная тема (@media (prefers-color-scheme: dark)), если явного выбора нет
  3. Светлая по дефолту, если ОС не поддерживает prefers-color-scheme

Токены CSS (design/tokens.css)

/* Светлая — базовая */
:root {
  --color-bg: #F1F1F1;
  --color-ink-900: #2E3454;
  --color-ink-700: #3B4D95;           /* индиго-бренд */
  --color-accent: #D4F2E3;             /* пастель-мята CTA */
  /* … ещё 20+ токенов */
}

/* Тёмная — явный выбор пользователя */
:root[data-theme="dark"] {
  --color-bg: #1E1E1E;
  --color-ink-900: #E8E8E8;
  --color-ink-700: #7FDDA8;           /* мята вместо индиго */
  --color-accent: #7FDDA8;            /* мята CTA */
  /* … новые значения, тот же набор переменных */
}

/* Тёмная — автодетект ОС (если выбора нет) */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    /* тот же набор, что и [data-theme="dark"] */
  }
}

/* Комната — независимый третий скоуп */
[data-theme="room"] {
  --color-room-bg: #1E1E1E;
  --color-room-mic-on: #7FDDA8;       /* пастель-мята */
  --color-room-mic-off: #EB93A1;      /* пастель-роза */
  /* … отдельный неймспейс, не трогается переключателем */
}

Ключевой момент: весь CSS оболочки ссылается на переменные через var(), поэтому его не нужно менять под разные темы. Переиспользование var() обеспечивает переключение автоматически.

Сохранение выбора (localStorage)

Ключ: vidconf-theme (строго совпадает между frontend/index.html и src/hooks/useTheme.ts).

Значения:

  • 'light'data-theme="light" на <html>
  • 'dark'data-theme="dark" на <html>
  • null / отсутствует → data-theme не проставляется, действует системная тема

Реализация (React)

Hook: src/hooks/useTheme.ts

export function useTheme() {
  // Читает явный выбор из localStorage при монтировании
  const [explicit, setExplicit] = useState<'light' | 'dark' | null>(() => readStoredTheme())
  
  // Живое отслеживание системной темы (matchMedia listener)
  const [systemPrefersDark, setSystemPrefersDark] = useState(...)
  
  // Вычисляет текущую активную тему: явный выбор ИЛИ системная
  const resolved: 'light' | 'dark' = explicit ?? (systemPrefersDark ? 'dark' : 'light')
  
  // Проставляет data-theme на <html> и сохраняет в localStorage
  const setTheme = (theme: 'light' | 'dark') => { /* … */ }
  
  return { resolved, setTheme }
}

Особенность: читает из localStorage ДО первого рендера (через инициализатор useState), чтобы синхронизировать с инлайн-скриптом в index.html.

Компонент: src/components/ui/ThemeToggle.tsx

export function ThemeToggle({ className = '' }: { className?: string }) {
  const { resolved, setTheme } = useTheme()

  return (
    <div className="theme-toggle" role="group" aria-label="Переключить тему">
      <button
        className={resolved === 'light' ? 'is-active' : ''}
        onClick={() => setTheme('light')}
        aria-label="Светлая тема"
      >
        <Sun className="icon" />
      </button>
      <button
        className={resolved === 'dark' ? 'is-active' : ''}
        onClick={() => setTheme('dark')}
        aria-label="Тёмная тема"
      >
        <Moon className="icon" />
      </button>
    </div>
  )
}

Монтирование:

  • ShellTopbar.tsx (лобби, календарь, админка, мои конференции)
  • AuthLayout.tsx (login, register, verify-email)
  • JoinPage.tsx (вход по номеру/ссылке)

Анти-FOUC: инлайн-скрипт (frontend/index.html)

<script>
  try {
    var vidconfTheme = localStorage.getItem('vidconf-theme')
    if (vidconfTheme === 'light' || vidconfTheme === 'dark') {
      document.documentElement.setAttribute('data-theme', vidconfTheme)
    }
  } catch (e) {
    // localStorage недоступен (приватный режим) — работает системная тема
  }
</script>

Выполняется до бандла React, до <div id="root">. Исключает вспышку светлой темы при загрузке тёмного интерфейса.

Палитра тёмной оболочки

Не новые цвета, а переиспользование уже утверждённых токенов комнаты:

Роль Светлая Тёмная Hex
Основной текст --color-ink-900 (индиго) --color-room-text-primary #E8E8E8
Бренд/заголовки --color-ink-700 (индиго) --color-room-mic-on (мята) #7FDDA8
CTA заливка --color-accent (пастель-мята) --color-room-mic-on (мята) #7FDDA8
Статус ошибки --color-danger (красный) --color-room-mic-off (роза) #EB93A1
Danger-кнопка (заливка) --color-danger --color-danger-solid #B85468
Warning/focus --color-warning (янтарь) --color-room-speaker-ring (янтарь) #D6A83D

Новые токены для оболочки (не в комнате, но получены смешиванием утверждённых цветов):

  • --color-accent-text (#10331F) — текст на мятных кнопках
  • --color-accent-hover / --color-accent-active — состояния CTA
  • --color-ink-400 — подписи в скобках
  • --color-success-bg / --color-danger-bg / --color-warning-bg — подложки бейджей

Подробный расчёт (WCAG 2.1, контраст ≥ 4.5:1) — см. design/mockups/dark/README.md.

Комната конференции (инвариант)

[data-theme="room"] — скоуп контейнера экрана конференции (обычно на <div class="room-container">):

  • Использует отдельный набор токенов --color-room-* (§1.2a DESIGN_SYSTEM.md)
  • Никогда не переключается на светлую тему
  • Не слушает prefers-color-scheme и data-theme на <html>
  • Всегда тёмная, независимо от выбора пользователя в лобби

Пример в RoomPage.tsx:

return <div data-theme="room" className="room-container">
  {/* весь контент комнаты — видео, участники, чат, тулбар */}
</div>

CSS страниц оболочки

Каждая страница оболочки (auth, lobby, calendar, join, my-conferences, admin) имеет свой файл стилей:

  • frontend/src/styles/auth.css
  • frontend/src/styles/lobby.css
  • frontend/src/styles/calendar.css
  • frontend/src/styles/join.css
  • frontend/src/styles/my-conferences.css
  • frontend/src/styles/admin.css

Они содержат точечные правки сверх токенов (декоративные градиенты, тени, layout-контроль), но не задают цвета — цвета задаются через var() из tokens.css. При переключении темы CSS-переменные меняются автоматически. Отдельные патчи под тёмную тему нужны там, где значение задано литеральным hex, а не через var() — см. комментарии в файлах.

Тестирование

# Проверить, что localStorage-ключ совпадает
grep -n 'vidconf-theme' frontend/index.html
grep -n 'STORAGE_KEY' frontend/src/hooks/useTheme.ts

# Запустить dev сервер
cd frontend && npm run dev

# Выключить в DevTools: localStorage → удалить vidconf-theme
# Проверить, что тема следует prefers-color-scheme ОС

# Кликнуть на переключатель (☀️/🌙)
# Проверить, что localStorage получил видconf-theme='dark' или 'light'
# Перезагрузить (F5) — тема должна восстановиться без вспышки

# Открыть RoomPage — комната должна остаться тёмной, несмотря на светлую оболочку

Ссылки

  • design/DESIGN_SYSTEM.md — полная дизайн-система (§0§1.2b)
  • design/tokens.css — авторитетный набор переменных (синхронизировано с frontend/src/styles/tokens.css)
  • design/mockups/dark/ — макеты тёмной оболочки (утверждены)
  • frontend/README.md — раздел «Темы оболочки»
  • design/tools/contrast.py — скрипт проверки контраста WCAG 2.1 (покрывает и тёмные пары)