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