225 lines
11 KiB
Markdown
225 lines
11 KiB
Markdown
# Архитектура тем оболочки 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 (покрывает и тёмные пары)
|