11 KiB
Архитектура тем оболочки 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"] → комната (отдельный набор токенов, не переключается)
Приоритет:
- Явный выбор (
data-theme="light"илиdata-theme="dark"), если есть - Системная тема (
@media (prefers-color-scheme: dark)), если явного выбора нет - Светлая по дефолту, если ОС не поддерживает
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.2aDESIGN_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.cssfrontend/src/styles/lobby.cssfrontend/src/styles/calendar.cssfrontend/src/styles/join.cssfrontend/src/styles/my-conferences.cssfrontend/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 (покрывает и тёмные пары)