Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View File

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