Files
vidconf/docs/architecture/frontend-themes.md

225 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура тем оболочки 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)
```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`
```typescript
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`
```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)
```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`:
```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()` — см. комментарии в файлах.
## Тестирование
```bash
# Проверить, что 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 (покрывает и тёмные пары)