# AIVEXA — правила вёрстки для агента

Обязательный свод. Каждое правило проверяемо (grep, скриншот, консоль). Подробности и происхождение значений — [DESIGN.md](https://test-design.aivexa.pro/aivexa/DESIGN.md), вход — [llms.txt](https://test-design.aivexa.pro/aivexa/llms.txt).
Нарушить правило можно только по прямой просьбе владельца — и с комментарием в коде, почему.

## 1. Цвет — только токены

- В своём коде нет литералов цвета: ни `#hex`, ни `rgb()/rgba()/hsl()/oklch()/color-mix()`, ни палитры Tailwind (`bg-red-500`, `text-white`, `bg-black/50`).
- Цвет берётся ролью: Tailwind-классы темы (`bg-background`, `bg-card`, `text-foreground`, `text-muted-foreground`, `bg-primary`, `border-border`, `text-destructive`…) или `var(--роль)` в CSS.
- Нужного цвета нет — ищи роль в DESIGN.md, раздел 2. Новый hex не вводи.
- Почему: тёмная тема и будущие правки палитры работают только через переменные. Один hex — и экран ломается в dark.

## 2. Шрифт — только Golos Text

- Шрифт приходит из темы (`--font-sans`) или `fonts.css`. Свои `font-family`, Google Fonts, Inter, Geist, системные стеки — нельзя.
- Веса: 400 текст, 500 кнопки и заголовки экранов, 600 акцентный текст и бейджи, 700 только значение KPI. Letter-spacing всегда 0.
- Почему: Golos — часть узнаваемости бренда и единственный шрифт с правильной кириллицей во всех весах кита.

## 3. Компоненты — только `@aivexa` + lucide

- Кнопки, поля, таблицы, меню, диалоги, карточки, каркас — только из реестра `@aivexa` (`npx shadcn@latest add @aivexa/<name>`). Не `@shadcn/*`, не MUI/Ant/Chakra, не самописные аналоги.
- Иконки — только `lucide-react`, размер 20 (в кнопке/меню), 16 (`size="icon"`), 12 (бейдж).
- Компонент реестра не делает нужного — расширь его через props/`className` с токенами; копию с другими цветами не создавай.
- Аватар (фото или инициалы) — только `@aivexa/avatar`, не самописный круг; статус в тосте — только из списка раздела 5.
- Свои компоненты — в `src/screens/` или `src/features/`; `src/components/` — только то, что ставит `shadcn add` (на этом построен чек-лист).
- Почему: в компонентах уже сняты числа Figma и все состояния; самописный аналог почти всегда теряет focus, dark или disabled.

## 4. Шкалы (числа)

| Что | Допустимые значения |
|---|---|
| Отступы, px | 4 · 8 · 12 · 16 · 18 · 20 · 24 · 30 · 38 (шаг 4; 18/30/38 — из Figma). В Tailwind шаг 1 = 4px: `gap-1` 4 · `2` 8 · `3` 12 · `4` 16 · `4.5` 18 · `5` 20 · `6` 24 · `7.5` 30 · `9.5` 38; в CSS — `var(--space-N)` |
| Радиусы, px | 4 (чекбокс, кнопка 28) · 8 (поле с подписью, пункт списка, табы) · 12 (кнопка, карточка, поле — по умолчанию) · 16 (CTA лендинга) · pill (бейджи, поиск, свитч) |
| Кегль / line-height / вес | 42/50.4/600 display · 48/58/500 h1 лендинга · 24/28.8/500 title · 20/24/500 подзаголовок, заголовок экрана · 16/19.2/400 заголовок карточки · 14/16.8/400 основной текст · 14/16.8/500 кнопки · 12/14.4/400 подписи (KPI — UPPERCASE) · 10/12 micro |
| Контролы | кнопка 44 (xs 34, icon 28, icon-round 32), поле 40/44, пункт меню 28, строка таблицы 48, шапка таблицы 51 |
| Каркас | сайдбар 291 / свёрнутый 69, контент приложения до 1141, контейнер лендинга 1280 |

- Произвольные значения (`p-[13px]`, `rounded-[10px]`, `text-[15px]`) — нельзя, кроме чисел из таблицы.
- Тени: только `--shadow-card/float/menu/popover/dropdown/tooltip/glass/strong/glow`. Карточка отделяется рамкой `--border` + `--shadow-card`, не плотной тенью.

## 5. Состояния — обязательны

У каждого интерактивного элемента и каждого блока с данными:

| Состояние | Как |
|---|---|
| hover | у компонентов реестра уже есть; у своих — фон `--accent`/`--secondary` или рамка `--ring` |
| focus-visible | видимое кольцо `--ring` (2px). `outline: none` без замены — нельзя |
| disabled | `disabled` / `aria-disabled`, цвета `--disabled` / `--disabled-foreground` |
| loading | `Button loading`; заглушки размером с контент (`animate-pulse bg-muted rounded-md`), не «прыгающая» вёрстка; кнопка в загрузке неактивна |
| empty | осмысленный текст + действие («Сотрудников пока нет» + «Добавить»), не пустая таблица |
| error | поле — `aria-invalid` + `FieldError` (связать `aria-describedby`); блок — сообщение + «Повторить»; глобально — `toast({ title, description, status: "error" })` |

- `toast()` из `@aivexa/notification`: `status` — `info` · `success` (по умолчанию) · `warning` · `block` (доступ заблокирован, DLP) · `error` (операция не удалась); `error` и `block` озвучиваются как `role="alert"`. Других значений нет — `"danger"`, `"destructive"` не пройдут проверку типов.
- Почему: эталоны и приёмка проверяют все шесть; экран только со «счастливым путём» не принимается.

## 6. Светлая и тёмная — обе обязательны

- Тема — класс `.dark` (или `data-theme="dark"`) на `<html>`, переключатель — `theme-switcher` / `useTheme()`.
- Не пиши `dark:`-цвета руками: роли сами переключаются. `dark:` допустим только для не-цветовых вещей (картинка фона, логотип `-on-dark`).
- Логотип на тёмном — `aivexa-logo-on-dark` (белое слово), на акцентном `--primary` — `aivexa-logo-white`.
- `--primary` / `--sidebar-primary` как цвет текста на тёмном фоне не используй (`#4F1FE5` на `#0A1020` — 2,4:1) — бери `--accent-foreground`: в светлой это тот же `#4F1FE5` (7,4:1 на подложке меню `#F8F7FF`), в тёмной `#9AA3FF` (8,2:1 на `#0A1020`). Так красится активный пункт `sidebar-menu`/`app-sidebar`, `mobile-bottom-nav`, история чата; `--sidebar-primary` — только полоса 3.6×18 у активного пункта.
- Тема при загрузке не мигает только со скриптом в `<head>` до бандла — готовый `index.html` в llms.txt, шаг 4; `<html lang="ru">`.

## 7. Адаптив: 390 / 768 / 1280+

| Ширина | Приложение | Таблицы | Лендинг |
|---|---|---|---|
| ≥ 1280 (`desktop:`) | сайдбар 291, у чата 69 + панель истории 291 (`aside`) | все колонки | шапка со ссылками |
| 768–1279 (`tablet:`) | сайдбар 69 (иконки), `aside` уходит в `AppDrawer` | второстепенные колонки `hidden desktop:table-cell` | гамбургер |
| < 768 | сайдбара нет: `mobile-bottom-nav` + меню в `AppDrawer` слева; действия шапки — строкой под заголовком | только 2–3 главные колонки (`hidden tablet:table-cell`), остальное — в строке/по клику | одна колонка |

- Mobile-first: базовые классы — для 390, расширение — через `tablet:` / `desktop:` (брейкпоинты темы 768 / 1280; `md:`/`lg:` не используй).
- Горизонтального скролла страницы на 390 быть не должно (`scrollWidth <= innerWidth`). Широкая таблица — прячь колонки, а не скролль весь экран.
- Зона касания на мобиле ≥ 44px (кнопку 28 расширяй паддингом).
- Каркас приложения не собирай сам — `app-shell` уже делает всё из таблицы.

## 8. Контраст ≥ 4.5:1

- Обычный текст ≥ 4.5:1, крупный (≥ 18.66px bold / 24px) и иконки ≥ 3:1. Проверено: `--foreground` на фоне 14,4:1, `--text-secondary` 7,3:1, `--icon` 4,8:1, `--primary` на белом 7,9:1.
- Известные исключения из Figma (оставлены сознательно, новых не добавлять):
  - `--muted-foreground` (`#A5A2B8`) на белом — 2,5:1: только подписи KPI, плейсхолдеры, неактивные пункты. Важный текст им не набирай — бери `--text-secondary` или `--icon`.
  - Статус-бейджи (зелёный 1,6:1, жёлтый 1,4:1, красный 2,4:1 на своей подложке): статус всегда дублируется словом («Активна»), цвет — не единственный носитель смысла.
  - Hover основной кнопки (`--primary-hover` с белым) — 2,3:1: кратковременное состояние.
  - Disabled — 3,9:1: WCAG не требует контраста для неактивных элементов.
- Почему: интерфейс рабочий, им пользуются часами; бледный текст — первая жалоба.

## 9. Do / Don't

**Do**
```tsx
<Button>Создать отдел</Button>                                  // один акцент на экран
<Button variant="outline">Отмена</Button>
<Card className="gap-3"><StatusBadge active /></Card>          // gap 12px; статус = слово + цвет
<p className="text-muted-foreground">Показано 10 из 48</p>     // роль, не hex
<div className="grid gap-4 desktop:gap-6 tablet:grid-cols-2 desktop:grid-cols-4"> // mobile-first, брейкпоинты темы
<Input aria-invalid={!!error} /> <FieldError>{error}</FieldError>
```

**Don't**
```tsx
<button style={{ background: '#4F1FE5' }}>                      // hex → сломается в dark
<div className="bg-white text-black shadow-lg">                 // палитра Tailwind и плотная тень
<h1 style={{ fontFamily: 'Inter' }}>                            // чужой шрифт
<div className="bg-gradient-to-r from-violet-500 ...">         // градиент на плоскости (он только для KPI-бейджа)
<Button className="bg-green-500">Сохранить</Button>             // второй хроматический цвет для действия
<button className="outline-none">                               // фокус без замены
import { Button } from '@mui/material'                          // чужой кит
```

Ещё: не подкрашивай слово логотипа акцентом (фиолетовый только у знака); не делай острые углы (минимум 4); не бери чистый чёрный для текста.

## 10. Чек-лист самопроверки

Выполни перед сдачей из корня проекта. Каждый пункт — команда или скриншот, не «посмотрел глазами».
`src/components/` и `src/index.css` из grep исключены: это код реестра и переменные темы, их пишет `shadcn add` (в комментариях там числа Figma). На чистом проекте после шагов 2–4 llms.txt каждый grep даёт 0 строк.

```bash
# 1. Нет литералов цвета и палитры Tailwind в своём коде (пусто = хорошо)
grep -rnE '#[0-9a-fA-F]{3,8}\b|\b(rgba?|hsla?|oklch|color-mix)\(' src index.html --include='*.tsx' --include='*.ts' --include='*.jsx' --include='*.css' --include='*.html' --exclude-dir=components --exclude=index.css
grep -rnE '\b(bg|text|border|from|to|via|fill|stroke|ring)-(white|black|(slate|gray|zinc|neutral|red|green|blue|violet|purple|yellow|amber|indigo)-[0-9]{2,3})\b' src --exclude-dir=components
# 2. Нет чужих шрифтов и китов, остатков Geist и шаблонного ThemeProvider (пусто = хорошо; index.css проверяется, строка темы body { font-family: var(--font-sans) } — не нарушение)
grep -rniE 'font-family|fonts\.googleapis|geist|@mui/|antd|@chakra-ui|@shadcn/|react-icons|@heroicons|theme-provider|ThemeProvider' src index.html package.json --exclude-dir=components | grep -v 'font-family: var(--font-sans)'
# 3. Сборка без ошибок (exit 0)
npm run build
```

Скриншоты и консоль — на собранном (`npx vite preview --port 4173`) или запущенном экране. Playwright: `pip install playwright && playwright install chromium`; если браузер уже есть (Chrome/Chromium в системе или кеш playwright) — без скачивания: `CHROME=/путь/к/chrome python3 shots.py …`.
Снимок «во всю высоту» делается как у эталонов: вьюпорт растягивается на высоту документа, потом обычный снимок. `full_page=True` без этого рисует `fixed`-элементы (нижняя мобильная панель, тосты) посреди страницы, а колонки `h-svh` — обрезанными.

```python
# python3 shots.py http://localhost:4173/
import os, sys
from playwright.sync_api import sync_playwright
url = sys.argv[1]
with sync_playwright() as p:
    b = p.chromium.launch(executable_path=os.environ.get("CHROME") or None)
    for theme in ("light", "dark"):
        for w, h in ((1440, 900), (390, 844)):
            pg = b.new_page(viewport={"width": w, "height": h})
            errs = []
            pg.on("console", lambda m: m.type == "error" and errs.append(m.text))
            pg.on("pageerror", lambda e: errs.append(str(e)))
            pg.add_init_script(f"localStorage.setItem('aivexa-theme','{theme}')")  # скрипт в <head> применит тему
            pg.goto(url, wait_until="networkidle")
            pg.evaluate("document.fonts.ready")
            dark = pg.evaluate("document.documentElement.classList.contains('dark')")
            sw = pg.evaluate("document.documentElement.scrollWidth")
            for _ in range(3):  # растянуть вьюпорт на всю страницу (fixed-панели — внизу, как на телефоне)
                full = pg.evaluate("document.documentElement.scrollHeight")
                if full <= pg.viewport_size["height"]:
                    break
                pg.set_viewport_size({"width": w, "height": full})
                pg.wait_for_timeout(300)
            pg.screenshot(path=f"shot-{theme}-{w}.png")
            print(theme, w, "overflow" if sw > w else "ok", "theme", "ok" if dark == (theme == "dark") else "НЕ ПРИМЕНИЛАСЬ", "errors:", errs or 0)
            pg.close()
    b.close()
```

- [ ] grep 1–2 пусты; `npm run build` — exit 0
- [ ] 4 снимка (light/dark × 1440/390) открыты и сравнены с эталоном того же типа экрана; `theme ok` у всех четырёх
- [ ] 0 ошибок консоли, нет `overflow` на 390
- [ ] у каждого интерактивного элемента видны hover и focus-visible (Tab по экрану), есть disabled/loading/empty/error
- [ ] текст не бледнее `--icon`, кроме исключений раздела 8
- [ ] иконки — только lucide, шрифт — только Golos (DevTools → Computed → font-family)
