13 KiB
Маппинг токенов в Tailwind (для frontend-dev)
Источник значений — design/tokens.css (единственный источник истины).
Ниже — как подключить их в tailwind.config.ts проекта frontend/ через
tailwind.config + CSS-переменные, без дублирования значений в JS.
1. Подключение
- Скопировать (или импортировать)
design/tokens.cssвfrontend/src/styles/tokens.css, подключить в корневомindex.cssдо Tailwind-директив:@import './tokens.css'; @tailwind base; @tailwind components; @tailwind utilities; - В
tailwind.config.tsне хардкодить hex — ссылаться на CSS-переменные, чтобы тема комнаты ([data-theme="room"]) и тёмная тема оболочки ([data-theme="dark"]/prefers-color-scheme: dark, см. ниже §1a) переопределяли значения без пересборки Tailwind:
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:
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:
: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.12–4.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]на карточке).