# Маппинг токенов в 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"` на `` — явный выбор пользователя (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()` (см. категории 3–4 в §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}` по умолчанию через обёртку `` в `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.12–4.15). При переносе в `frontend/`: - **Раскрытие тумблером («Закрытая по паролю», «Закрепить постоянную конференцию») и radio-группа повторений** в HTML-макете сделаны чистым CSS через `:has()` (без JS/зависимостей) — во frontend это обычный **controlled-компонент** (`useState` для тумблера, `useState` для 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]` на карточке).