Первоначальная версия 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

236
design/tailwind.md Normal file
View File

@@ -0,0 +1,236 @@
# Маппинг токенов в Tailwind (для frontend-dev)
Источник значений — `design/tokens.css` (единственный источник истины).
Ниже — как подключить их в `tailwind.config.ts` проекта `frontend/` через
`tailwind.config` + CSS-переменные, без дублирования значений в JS.
## 1. Подключение
1. Скопировать (или импортировать) `design/tokens.css` в `frontend/src/styles/tokens.css`,
подключить в корневом `index.css` до Tailwind-директив:
```css
@import './tokens.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
```
2. В `tailwind.config.ts` **не хардкодить hex** — ссылаться на CSS-переменные,
чтобы тема комнаты (`[data-theme="room"]`) и тёмная тема оболочки
(`[data-theme="dark"]` / `prefers-color-scheme: dark`, см. ниже §1a)
переопределяли значения без пересборки Tailwind:
```ts
import type { Config } from 'tailwindcss'
export default {
darkMode: ['selector', '[data-theme="room"]'], // room-тема — не системная OS dark, а локальный скоуп
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
bg: 'var(--color-bg)',
'bg-alt': 'var(--color-bg-alt)',
surface: 'var(--color-surface)',
border: {
DEFAULT: 'var(--color-border)',
strong: 'var(--color-border-strong)',
pill: 'var(--color-border-pill)',
},
ink: {
900: 'var(--color-ink-900)',
700: 'var(--color-ink-700)',
500: 'var(--color-ink-500)',
400: 'var(--color-ink-400)',
300: 'var(--color-ink-300)',
},
// Акцент не обязан быть зелёным — см. DESIGN_SYSTEM.md §1.1.
accent: {
DEFAULT: 'var(--color-accent)',
hover: 'var(--color-accent-hover)',
active: 'var(--color-accent-active)',
text: 'var(--color-accent-text)',
},
success: { DEFAULT: 'var(--color-success)', bg: 'var(--color-success-bg)', dot: 'var(--color-success-dot)' },
danger: { DEFAULT: 'var(--color-danger)', bg: 'var(--color-danger-bg)', dot: 'var(--color-danger-dot)' },
warning: { DEFAULT: 'var(--color-warning)', bg: 'var(--color-warning-bg)', dot: 'var(--color-warning-dot)' },
focus: 'var(--color-focus-ring)',
// Тема комнаты — доступна везде как room-* (реально применяется только
// внутри [data-theme="room"], т.к. сами переменные там переопределены)
room: {
bg: 'var(--color-room-bg)',
surface: 'var(--color-room-surface)',
'surface-raised': 'var(--color-room-surface-raised)',
tile: 'var(--color-room-tile)',
'tile-border': 'var(--color-room-tile-border)',
'tile-hover': 'var(--color-room-tile-hover)',
'text-primary': 'var(--color-room-text-primary)',
'text-secondary': 'var(--color-room-text-secondary)',
'text-tertiary': 'var(--color-room-text-tertiary)',
'mic-on': 'var(--color-room-mic-on)',
'mic-off': 'var(--color-room-mic-off)',
'speaker-ring': 'var(--color-room-speaker-ring)',
danger: 'var(--color-room-danger)',
'danger-bg': 'var(--color-room-danger-bg)',
'danger-bg-hover': 'var(--color-room-danger-bg-hover)',
focus: 'var(--color-room-focus-ring)',
},
},
fontFamily: {
display: ['Unbounded', 'Arial Rounded MT Bold', '-apple-system', 'Segoe UI', 'sans-serif'],
body: ['Inter', '-apple-system', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
mono: ['JetBrains Mono', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
},
fontSize: {
'display-2xl': ['64px', { lineHeight: '1.05', fontWeight: '700' }],
'display-xl': ['48px', { lineHeight: '1.1', fontWeight: '700' }],
'display-lg': ['34px', { lineHeight: '1.2', fontWeight: '600' }],
h1: ['28px', { lineHeight: '1.25', fontWeight: '700' }],
h2: ['22px', { lineHeight: '1.3', fontWeight: '600' }],
h3: ['18px', { lineHeight: '1.35', fontWeight: '600' }],
'body-lg': ['16px', { lineHeight: '1.5' }],
body: ['14px', { lineHeight: '1.5' }],
caption: ['12px', { lineHeight: '1.4', letterSpacing: '.04em', fontWeight: '600' }],
'mono-sm': ['13px', { lineHeight: '1.4', fontWeight: '500' }],
},
spacing: {
1: 'var(--space-1)', 2: 'var(--space-2)', 3: 'var(--space-3)',
4: 'var(--space-4)', 5: 'var(--space-5)', 6: 'var(--space-6)',
8: 'var(--space-8)', 10: 'var(--space-10)', 12: 'var(--space-12)',
16: 'var(--space-16)', 20: 'var(--space-20)', 24: 'var(--space-24)',
},
borderRadius: {
sm: 'var(--radius-sm)', md: 'var(--radius-md)', lg: 'var(--radius-lg)',
xl: 'var(--radius-xl)', '2xl': 'var(--radius-2xl)', '3xl': 'var(--radius-3xl)',
full: 'var(--radius-full)',
},
boxShadow: {
sm: 'var(--shadow-sm)', md: 'var(--shadow-md)', lg: 'var(--shadow-lg)',
glass: 'var(--shadow-glass)',
'speaker-glow': 'var(--shadow-speaker-glow)',
'room-panel': 'var(--shadow-room-panel)',
},
screens: {
// desktop-first: минимальная поддерживаемая ширина — 1280px
min: { raw: '(min-width: 1280px)' },
},
transitionDuration: {
fast: '120ms',
base: '200ms',
},
},
},
plugins: [],
} satisfies Config
```
## 1a. Тёмная тема оболочки — контракт для frontend
Механизм (детали и обоснование — `design/DESIGN_SYSTEM.md` §0.1/§1.2b,
реализация значений — `design/tokens.css`):
- Атрибут `data-theme="dark"` / `data-theme="light"` на `<html>` — явный
выбор пользователя (persist в localStorage/профиле); без атрибута — дефолт
по `@media (prefers-color-scheme: dark)`.
- Оба триггера переопределяют **те же самые** CSS-переменные, что и светлый
`:root` (`--color-bg`, `--color-ink-900`, `--color-accent`, …). Так как
весь маппинг Tailwind-цветов в §1 уже идёт через `var(--color-*)`, а не
хардкод, **подавляющему большинству компонентов не нужна отдельная
`dark:`-логика** — те же классы (`bg-bg`, `text-ink-900`, `bg-accent`)
автоматически перекрашиваются вместе со сменой `data-theme`.
- Существующий Tailwind `dark:`-вариант (`darkMode: ['selector', '[data-theme="room"]']`)
остаётся зарезервирован **только за комнатой** (см. комментарий в конфиге
§1) — не путать с тёмной темой оболочки, у них разный контракт (комната не
переключается, оболочка — переключается и по умолчанию следует ОС).
- Для редких мест, где нужен именно Tailwind-вариант под тёмную оболочку
(например, компонентные исключения §1.2b DESIGN_SYSTEM.md — glass-обводка,
focus/error-glow, если их проще выразить `class`, а не `var()`) — завести
отдельный кастомный вариант `shell-dark:` через `addVariant`, с тем же
двойным условием, что и в `tokens.css`:
```ts
import plugin from 'tailwindcss/plugin'
export default {
// ...
plugins: [
plugin(({ addVariant }) => {
addVariant('shell-dark', [
'&[data-theme="dark"]',
'@media (prefers-color-scheme: dark) { &:not([data-theme]) }',
])
}),
],
} satisfies Config
```
Использование: `shell-dark:border-white/10` и т.п. — только для случаев,
которые принципиально нельзя выразить через `var()` (см. категории 34 в
§1.2b DESIGN_SYSTEM.md); для всех обычных цветов компонентов — `var()`-токены,
`shell-dark:` не нужен.
## 2. shadcn/ui
`components.json` продолжает использовать CSS-переменные (`cssVariables: true`),
но вместо стандартной shadcn-палитры (`--primary`, `--secondary`, …) — алиасить
их на токены проекта в `globals.css`:
```css
:root {
--primary: var(--color-ink-700);
--primary-foreground: var(--color-surface);
--secondary: var(--color-bg-alt);
--destructive: var(--color-danger);
--destructive-foreground: #FFFFFF;
--ring: var(--color-focus-ring);
--radius: 24px; /* соответствует --radius-2xl, база для shadcn radius-cascade */
}
[data-theme='room'] {
--primary: var(--color-room-mic-on);
--destructive: var(--color-room-danger-bg);
--ring: var(--color-room-focus-ring);
}
```
Тёмную тему оболочки (`[data-theme='dark']`) здесь отдельно объявлять не
нужно: блок `:root` выше уже целиком построен на `var(--color-*)`, а эти
переменные сами переопределяются в `tokens.css` под `[data-theme='dark']`/
`prefers-color-scheme` — shadcn-компоненты перекрашиваются вместе с оболочкой
автоматически, без дублирования правил.
`Button` variant `default` переопределить на pill (`rounded-full`) + `bg-accent
text-accent-text hover:bg-accent-hover active:bg-accent-active`
вместо стандартного `bg-primary`. Variant `outline` — `border-border-pill
text-ink-700 rounded-full`.
## 3. Иконки
`lucide-react` устанавливается как обычная npm-зависимость (не CDN),
`stroke-width={2}` по умолчанию через обёртку `<Icon />` в `frontend/src/components/ui/icon.tsx`,
цвет — через `currentColor` (наследуется от текстового токена родителя).
Список иконок по экранам — раздел 6 `DESIGN_SYSTEM.md`.
## 4. Паттерны динамических конференций — для frontend-dev
Макеты `design/mockups/lobby.html`, `calendar.html`, `my-conferences.html`,
`join.html` вводят компоненты, которых нет в статичном списке комнат (см.
`DESIGN_SYSTEM.md` §4.124.15). При переносе в `frontend/`:
- **Раскрытие тумблером («Закрытая по паролю», «Закрепить постоянную
конференцию») и radio-группа повторений** в HTML-макете сделаны чистым
CSS через `:has()` (без JS/зависимостей) — во frontend это обычный
**controlled-компонент** (`useState<boolean>` для тумблера,
`useState<RecurrenceType>` для radio-группы), `:has()` не переносится.
- **Пилюли «ссылка/номер» с копированием** (§4.15) — обёртка над
`navigator.clipboard.writeText`, состояние «скопировано» — таймер
`setTimeout` ~1.5s (как в статическом макете `my-conferences.html`), плюс
`aria-live="polite"` для тултипа «Скопировано» (в макете — визуальный
`opacity`-переход, без `aria-live`, добавить во frontend).
- **Бейдж статуса конференции** (§4.6) заменяет прежний `Badge` для комнат:
варианты `scheduled` / `pinned` / `live` / `ended` / `cancelled`; закрытость
паролем — отдельная иконка `Lock` рядом с заголовком, не вариант `Badge`.
- **Карточки-действия хаба** (§4.12) — фиксированная высота (не `min-height`)
обязательна для симметрии сетки 2×2; в CSS-grid Tailwind — `grid-rows-2`
с явной высотой строки (`grid-template-rows: repeat(2, 340px)` через
`[grid-template-rows:repeat(2,340px)]` или отдельный `h-[340px]` на карточке).