dashbord-roznichnyh-prodazh-3/design-system/COMPONENTS.md

594 lines
58 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# KT AI – нормативная анатомия компонентов
Канон значений — `tokens.json`, канон правил — `docs/DESIGN.md`, этот файл — анатомия сложных паттернов.
## App Shell
Фиксированный левый sidebar + рабочая канва (grid `sidebar-width minmax(0,1fr)`).
Структура sidebar сверху вниз:
1. Brand-ряд: логотип KT AI 18–22px + подпись 13–15px.
2. Основная навигация (Главная, Лента).
3. Группы разделов по 3–5 пунктов (закон Миллера), секции разделены label.
4. Утилиты внизу (настройки, тема, профиль).
**Рейл (< 1024px).** Sidebar сжимается до 52px, и в него помещается только иконный ряд. Правило структурное, а не список классов: элемент с иконкой (`svg` или `img`) внутри остаётся и становится иконной кнопкой, элемент без иконки скрывается — показывать его в 52px нечем.
Значит **любой текстовый блок, добавленный продуктом в sidebar, на ноутбуке с окном в половину экрана исчезнет**. Это осознанно: раньше такие блоки оставались и раскладывались в колонку букв поверх навигации.
Нужно оставить элемент в рейле — `data-rail="keep"`. Тогда за то, что он влезает в 52px, отвечает продукт.
**Ограничение правила.** Скрывается элемент, у которого иконки нет. Элемент, который иконку **содержит**, остаётся — вместе со всем своим текстом: `<a><svg/>Как это работает</a>` уцелеет целиком, потому что голый текстовый узел селектором не спрятать. Отсюда требование к разметке: **текст в сайдбаре всегда оборачивать в элемент** (`<span>`, `<strong>`), а не оставлять голым рядом с иконкой. Тогда обёртка попадёт под правило и скроется. Собственные части шелла это уже соблюдают.
Проверка на 900px, которая ловит нарушение:
```js
[...document.querySelectorAll('.kt-ai-sidebar *')]
.filter(e => e.textContent.trim()
&& e.getBoundingClientRect().width < 24
&& getComputedStyle(e).display !== 'none').length === 0
```
Собственные части app-shell (например блок тура `.sb-tour`) закрывают рейл своим правилом: `kt-ai-components.css` лежит в `@layer`, а стили шелла беcслойные, и беcслойное правило побеждает слоёное независимо от специфичности.
Три состояния отзывчивости – docs/DESIGN.md «Отзывчивость». Активный пункт: `bg-active` + 2px-акцент слева.
## Поиск и command palette
Топ-поиск – компактный command-вход: высота 28px, max-width 320px, placeholder короткий («Поиск…»), бейдж `⌘K` в `.kt-ai-kbd`. Открывает command palette поверх экрана (`.kt-ai-command-overlay`).
## Карточки
Карточки информационные, не декоративные. Использовать только когда элемент – повторяемый объект, стартовое действие, превью или поверхность modal/drawer.
- v2.3: фон/рамка/тень – только через `card-bg / card-border / card-shadow` токены (dark: hairline + aurora-свечение; light: белая поверхность + тень). Радиус — `radius-3xl`, паддинг 12–14px.
- Усиленное свечение hero/AI-поверхностей: `data-glow="strong"`.
- Карточку в карточку не вкладывать.
## Ряды
Ряды – доминирующий паттерн каталогов (вместо крупных карточек).
- Основная подпись 13px; метаданные 11–12px muted.
- Иконка 16px слева (14px в плотных рядах).
- Справа – счётчик, статус или скор; действия (`.actions`) видимы на hover/focus/selected.
- Hover `bg-hover`, выбран `bg-active`. Зона клика – весь ряд.
## Таблицы
Таблица – для любого workflow с 5+ записями со статусом, владельцем, сроком или суммой.
Принципы (research-backed):
- Таблице – максимум полезной ширины (`content-table`); не заворачивать в карточку.
- Колонки и дефолтная сортировка – от задачи пользователя, не от схемы БД.
- Заголовки 1–2 слова; видимы только task-critical колонки, остальное – в detail/настройки колонок.
- Один тип значения на колонку. Главная entity-колонка – максимум две строки: label + muted-метаданные.
- Текст и даты – влево; суммы, скоры, счёт – вправо (sans + tabular-nums); заголовок выравнен со своими данными; центру – нет.
- Лёгкие горизонтальные dividers; вертикальных колонок-сепараторов нет (ElevenLabs, тихо/воздушно); зебра – только в read-only числовых таблицах. Шапка на лёгком divider (не border-strong). Высоты рядов — `--kt-ai-table-row*`, значения в `tokens.json`. Чекбоксы первой колонки скрыты до hover ряда или выбора (не загромождают).
- Hover ряда включён всегда (помогает сканировать), даже если ряд некликабелен.
- Sticky header при вертикальном скролле; sticky первая колонка при горизонтальном (`.kt-ai-sticky-first`).
- Row actions – только на hover/focus/selected (на touch – всегда); bulk-bar появляется после выбора 1+ рядов; дефолт – ноль выбранных.
- Первый ряд не предвыбирать. Подсветка совпадений при активном поиске.
- Пагинация/итог в футере; дефолт 25 строк; loading – skeleton-ряды; пусто – `.kt-ai-empty`.
- Настройки пользователя (колонки, сортировка, фильтр) живут в течение сессии + «сбросить по умолчанию».
Геометрия (v2.3): таблица по умолчанию frameless full-bleed (`data-framed="true"` – контейнер с рамкой); header sticky, 12px sentence-case medium fg-muted, hairline border-strong снизу; ячейки 12px, entity 13px/500; статус – rounded-rect тег (radius-tag); вертикальное выравнивание middle до 3 строк.
Toolbar: высота ≥40px; слева – имя таблицы/saved view/счётчик, справа – фильтр, сортировка, колонки, экспорт (≤4 действий); iconбуттоны из спрайта; saved views скрыты, пока их меньше двух. Применённые фильтры – `.kt-ai-filterbar` под тулбаром.
Mobile: скролл внутри контейнера, не страницы; первая колонка sticky; прячутся сначала вторичные колонки (источник, заказчик, владелец), потом суммы/сроки; футер компактный («1–25 из N»).
Dashboard-паттерн: KPI-полоса над таблицей только если помогает пользователю принять следующее рабочее решение по рядам; одна таблица – главный объект; детали – в правом drawer.
Operational dashboard copy:
- Заголовок экрана – короткое имя рабочего объекта, 1-3 слова: `Очередь рисков`, `Реестр заявок`, `Проверка договоров`. Не использовать длинное описание процесса как H1/topbar title.
- Subtitle объясняет scope одной короткой фразой, до 90 символов. Детали процесса, источники данных и ограничения – в drawer/help, не в H1.
- KPI над таблицей показывают состояние работы пользователя: новые, требуют решения, риски, ждут документы, просрочено, закрыто сегодня, сумма риска или SLA. Не выводить экономию часов, FTE, ROI или тенге эффекта на пользовательский dashboard, если это не management/reporting view.
- Деньги допустимы в KPI только когда это рабочий объект пользователя: `сумма риска`, `расхождение`, `к оплате`, `заблокировано`. `Экономия`, `эффект`, `ROI`, `FTE` – метрики проекта, а не основной рабочей очереди.
- Фильтры над таблицей – только по workflow-status главного объекта, максимум 5 пунктов включая `Все`. Confidence, тип риска, источник данных и technical labels не попадают в primary filters по умолчанию.
- Workflow-status, confidence и risk type – разные поля. Нельзя смешивать их в один набор `statuses`, иначе экран перегружает фильтры и ломает decision focus.
- Labels короткие: фильтр до 2 слов, status-pill до 4 слов, table header до 2 слов. Длинная причина риска уходит в drawer или отдельную колонку `Расхождение` с коротким текстом.
- Для process-owner экран должен показывать next action: что требует решения сегодня, что заблокировано, где риск, какой документ нужно дослать, что закрыто. Просто список документов без статуса работы – неполный dashboard.
## Подтверждение опасных действий
Три ступени по цене ошибки, а не по «важности»:
| Что за действие | Чем подтверждается |
|---|---|
| Обратимое, но раздражающее | выполнить сразу + тост с «Отменить» (`.kt-ai-toast .action`) |
| Деструктивное | модалка; кнопка — глаголом действия («Удалить агента»), не «Да» и не «ОК» |
| Необратимое | ввод имени объекта |
**Удержание кнопки («hold to delete») в систему не берём.** Разбор, чтобы к этому не возвращались:
- **Недоступно с клавиатуры.** У удержания мыши нет клавиатурного эквивалента; накрутить удержание пробела можно, но это конфликтует со штатным «пробел нажимает кнопку» и остаётся неочевидным. Любая доступная реализация требует запасного пути — и тогда на одно действие приходится два механизма.
- **Ничего не сообщает.** Модалка говорит, ЧТО удаляется, сколько записей и что от них зависит. Кольцо прогресса не говорит ничего — а в наших продуктах (закупки, договоры, инциденты) именно последствие и нужно видеть.
- **Не открывается само.** Чтобы понять, что кнопку надо держать, надо сначала прочитать подпись.
- **Ломается на тач-устройствах**, где долгое нажатие занято системным контекстным меню и выделением текста.
Ниша, на которую он метит — «действие жалко закрывать модалкой» — у нас уже закрыта отменой в тосте, и закрыта лучше: ноль трения до действия и полное восстановление после.
## Композер чата
`.kt-ai-prompt-bar` — `textarea` + кнопка отправки. Над полем может стоять ряд пилюль навыков `.kt-ai-prompt-skills`:
```html
<div class="kt-ai-prompt-bar">
<div class="kt-ai-prompt-skills">
<span class="kt-ai-prompt-skill">Сверка документов<button aria-label="Убрать навык">×</button></span>
</div>
<textarea rows="1" placeholder="Спросите…"></textarea>
<button class="kt-ai-prompt-send">…</button>
</div>
```
Выбранный навык — **объект, а не текст**: его нельзя случайно надкусить бэкспейсом, он удаляется целиком. Пустой ряд схлопывается и не занимает высоту.
**Почему пилюли над полем, а не внутри строки.** Вставить невредактируемый элемент в середину текста можно только в `contenteditable`. Переписывать композер на `contenteditable` ради этого нельзя: ломаются отмена ввода, набор через IME, вставка из буфера, доступность и мобильные клавиатуры. Пилюли над полем дают то же свойство без единого из этих рисков.
**Slash-команды.** Скрипт `kt-ai-composer.js` включается атрибутом `data-composer="true"`, список навыков — в `data-skills` (JSON: `id`, `label`, `hint`):
- «/» в начале слова открывает меню, дальнейший набор фильтрует по названию и подсказке;
- ↑ ↓ ходят по списку, Enter или Tab выбирают, Esc закрывает;
- при выборе «/запрос» **убирается из текста** — навык теперь объект, а не строка;
- крестик на пилюле снимает навык, Backspace в пустом поле снимает последний;
- Enter без Shift отправляет: событие `composer:submit` с `{ text, skills }`.
Меню открывается **над** полем: композер обычно стоит внизу экрана, и список, растущий вниз, уехал бы за край.
**Каретка при вставке из меню.** Специального механизма не нужно: `textarea.selectionStart` переживает потерю фокуса, поэтому вставлять надо в `selectionStart`, а не в конец. Проверено измерением.
## Кружок с номером
Шаг мастера, позиция в списке, порядковый номер.
```html
<span class="kt-ai-num-badge">1</span>
<span class="kt-ai-num-badge" data-state="current">3</span>
<span class="kt-ai-num-badge" data-size="lg" data-tone="risk">8</span>
```
```tsx
<KTNumBadge value={3} state="current" label="шаг 3 из 5" />
```
Состояния: по умолчанию (будущий), `done` (пройден — тише текущего, но не тусклее будущих), `current` (заливка: он один на экране и обязан находиться мгновенно). Тона `ok / warn / risk` — когда номер сам несёт оценку. Размеры `sm` 18px, обычный 22px, `lg` 28px.
**Почему это не иконка.** Наши иконки штриховые (1.75 в сетке 24) и используются на 14–15px. Цифра штрихом внутри штриховой окружности на таком размере выходит 6–7px высотой при штрихе около 1px — просветы у 6, 8 и 9 схлопываются. Плюс набор иконок был бы конечным (1–9), а номера бывают двузначные. Номер рисуется текстом: любое число, чёткость на любом размере, цвет и состояния от токенов.
Цифры моноширинные — в колонке номеров ряды не пляшут.
Степпер (`.kt-ai-step-item .n`) — тот же кружок; отдельного определения у него нет, иначе они разъедутся.
### Номер иконкой
Когда номер стоит в ряду с другими иконками — `circleNumber0` … `circleNumber9`:
```html
<svg class="kt-icon"><use href="#circleNumber3"></use></svg>
```
```tsx
<KTIcon name="circleNumber3" size={15} />
```
Набор взят из [Tabler Icons](https://tabler.io/icons) (MIT) и приведён к нашей сетке: радиус окружности 10 вместо 9, штрих 1.75 вместо 2 — чтобы в ряду с `check-circle` они были одного размера и веса.
**Иконка или компонент.** Иконка — фиксированные 0–9 и один цвет, зато встаёт в любой поток иконок. Компонент `.kt-ai-num-badge` — любое число, включая двузначные, состояния и заливка активного. Нужен номер шага в мастере — компонент; нужен номер рядом с иконкой в строке — иконка.
## Орб агента
Монохромное точечное облако на canvas — индикатор того, что агент работает. Заменяет спиннер там, где ожидание содержательное, и стоит в кнопке помощника вместо подписи «AI».
```html
<canvas class="kt-ai-orb" data-state="thinking" style="width:20px;height:20px"></canvas>
<script src="kt-ai-orb.js"></script>
```
```tsx
import { KTAIOrb } from "kt-ai-design-system/kit";
<KTAIOrb state="thinking" size={20} />
```
Состояния: `idle` (медленное дыхание), `thinking` (вращение), `listening` (пульс). Размеры — 20px в кнопке и строке, 64px как крупный индикатор.
**Цвет не задаётся.** Точки рисуются `currentColor`, поэтому орб темизуется теми же токенами, что и текст рядом, и работает в обеих темах без единого правила про тему. Хотите тише — поставьте `color: var(--kt-ai-fg-faint)`.
Рисование живёт в одном файле `kt-ai-orb.js` на оба рантайма: React-компонент его подключает, а не дублирует. Иначе HTML-превью и React-сборка разошлись бы в анимации — это запрещает гейт паритета G7.
Из этого следует поставка: `kt-ai-orb.js` входит в бандл кита как `public/kt-ai-orb.js` — компонент запрашивает его у сайта по абсолютному пути. Без файла кнопка помощника рисует пустой кружок, и ни сборка, ни типы об этом не скажут: проверяет гейт 15 в `doctor.py`.
Первый кадр рисуется синхронно, до `requestAnimationFrame`: в фоновой вкладке браузер rAF не вызывает, и орб оставался бы пустым прямоугольником. По той же причине при `prefers-reduced-motion` остаётся статичный кадр, а не пустота.
Идея заимствована у [thinking-orbs](https://orbs.jakubantalik.com) (MIT). Сам пакет не подошёл: его сборка импортирует React на верхнем уровне, а HTML app-shell работает без React.
## Плашка показателя
Четыре уровня: подпись, значение, уточнение, сноска.
```json
{ "label": "Просрочено в июне", "value": "58", "hint": "из 171" }
```
```html
<div class="kt-ai-kpi">
<span class="value">58</span><span class="label">Просрочено в июне</span>
<span class="hint">из 171</span>
</div>
```
```tsx
<KTKpiCard label="Просрочено в июне" value="58" hint="из 171" />
```
**Значение — само число, уточнение — знаменатель, доля или период.** Не «58 из 171» одной строкой: полоса перестаёт читаться как ряд величин, глаз ищет число, а находит фразу. Уточнение до 40 символов.
**Значение идёт сразу за подписью, а не прижимается к низу карточки.** Иначе в соседних карточках — с уточнением и без — числа встают на разной высоте и кажутся разными по важности. Проверяется измерением: у всех плашек полосы `value` обязан быть на одной высоте.
Уровень поддержан во всех трёх местах: поле `hint` в `product.schema.json`, вывод в HTML-рантайме и в `KTKpiCard`. Компонент рисуется классом `.kt-ai-kpi` в обоих рантаймах — своих утилит у React-версии нет, иначе правка в ДС до неё не доезжала бы.
### Сноска: чем число НЕ является
`hint` отвечает «из чего считано» и потому ограничен 40 знаками — снимать этот потолок нельзя, иначе полоса плиток перестанет быть полосой. Но у поля-источника бывает вторая правда: оно меряет не ровно то, чем показатель назван. «Ожидание в очереди» не отделяет автоприветствие бота от первого сообщения оператора — число верное, а работой человека не является. Это другой вопрос, и отвечает на него отдельное поле:
```json
{ "label": "Ожидание в очереди", "value": "00:41", "hint": "из 33 352 диалогов",
"note": "Поле выгрузки считает время до первого сообщения в чате, а им бывает автоприветствие бота. Число верное, но работой оператора оно не является." }
```
```html
<span class="note">Поле выгрузки считает время до первого сообщения…</span>
```
```tsx
<KTKpiCard label="Ожидание в очереди" value="00:41" hint="из 33 352 диалогов" note="Поле выгрузки считает…" />
```
**Потолка у сноски нет** — она не стоит в потоке полосы, её отбивает линия. **И она не прячется под клик.** Оговорка существует затем, чтобы число не прочитали неверно; спрятанное под клик читают не все — ровно как карточку строки, из-за чего оговорку и потребовалось ставить у плитки. По той же причине у неё цвет `fg-muted`, а не самый тихий `fg-faint`: 4,9:1 на кегле 11px — это «тихо» на грани нечитаемого, а сноску надо прочитать.
Куда сноска НЕ идёт: в баннер. Баннер стоит над числами, и оговорки, собранные в него, дают абзац текста до первого числа (замеряли: 716 знаков на одном экране). Баннер предупреждает, из-за чего число соврёт; сноска объясняет, что число значит; вывод раздела — что из чисел следует. Три разных места.
## Вывод раздела
Последний блок экрана, после таблицы. Контракт: `conclusion: { title?, text }`, пустая строка в `text` делит абзацы.
```html
<section class="kt-ai-conclusion">
<span class="title">Что показывают эти числа</span>
<span class="text">Средняя считается только по ответившим…</span>
<span class="text">Смотреть стоит на долю дольше норматива…</span>
</section>
```
**Это противовес баннера, а не его вариант.** Баннер стоит НАД числами — значит, читается до данных, и потому окрашен тоном: он предупреждает. Вывод стоит ПОСЛЕ данных и говорит, что из них следует: разбор смещения выборки, оговорка о методике, ответ на вопрос раздела. Цвета статуса у него нет — сигналить ему нечем; есть линия сверху («данные кончились, начинается их чтение») и мера строки 78ch, потому что вывод — сплошной текст, а не пары «подпись — значение».
Рисуется одинаково во всех архетипах, кроме `conversational`: там контент владеет всей областью сам.
## Статика под префиксом развёртывания
Приложение живёт не только в корне домена. Под `/cons` файл `/kt-ai-orb.js`
уходит в корень САЙТА и не находится: пропадают спрайт иконок, орб, подсказка
диаграммы и режим отзыва — тихо, только на развёртывании.
```tsx
import { ktAsset, ktSetBasePath } from "kt-ai-design-system/kit";
ktSetBasePath("/cons"); // явно, до первого рендера
<img src={ktAsset("/img/shema.png")} alt="" />
fetch(ktAsset("/api/proposals")); // свои маршруты ломаются так же
```
Префикс — свойство развёртывания, а не аргумент вызова, поэтому задаётся один
раз: `ktSetBasePath()` → `process.env.NEXT_PUBLIC_BASE_PATH` → атрибут
`<html data-kt-ai-base>` → пусто. Порядок и почему он такой — `docs/SETUP.md`.
В HTML-рантайме то же самое даёт `window.ktAiAsset("kt-ai-orb.js")`, а префикс
приходит атрибутом на `<html>`. Теги статики там пишет загрузчик, а не
разметка: адрес уже разобранного `<link>` изменить нельзя — браузер начинает
грузить его сразу.
Гейт 18 (`doctor.py`) считает путь от корня сайта мимо `ktAsset()` ошибкой
сборки ДС — в обоих рантаймах.
## Чарты
Высота 190–220px; bar 20–28px, радиус 2px; подписи значений 11px mono muted. Остальные правила – docs/DESIGN.md «Чарты».
## Счётчик в пункте меню
Одно значение — как раньше, приглушённым тоном:
```tsx
{ label: "Проекты", icon: "lots", count: "12" }
```
Прогресс — пара «всего / сделано». «Сделано» рисуется тоном `ok`, и по пункту меню видно не только объём работы, но и продвижение:
```tsx
{ label: "Спецификации", icon: "file", count: { value: 11, done: 5 } }
```
```html
<span class="kt-ai-count-split">11<span class="sep">/</span><span class="done">5</span></span>
```
Одним цветом «11/5» читается как одно число с косой чертой — именно поэтому продукты подставляли вместо счётчика свою строку. Класс `.kt-ai-count-split` не привязан к меню: пара «всего/сделано» уместна и во вкладке, и в заголовке секции.
Поле `nav[].count` в продуктовом контракте — устаревшее и одночисловое; пара живёт в API кита, а не в контракте.
## Поиск в шапке
Два размера и один переключатель подсказки сочетания.
```tsx
<KTAIShell search={{ items, placeholder: "Договор, филиал…", compact: true }}>
```
```json
"search": { "placeholder": "Договор, филиал…", "compact": true }
```
```html
<div class="kt-ai-search-field" data-shortcut="true">
<input class="kt-ai-search" placeholder="Поиск…" title="Поиск (⌘K)">
<kbd class="kt-ai-kbd">⌘K</kbd>
</div>
```
`compact` — 176px вместо 320px: шапка отдаёт место остальному. Бейдж в
компактном поле не показывается — это ширина запроса, а не запроса с подписью;
сочетание при этом работает и названо в `title`.
**Сочетание и его подпись включаются ОДНИМ полем** (`shortcut`). Подпись без
обработчика — обещание (правило 9d); обработчик без подписи — незаявленная
горячая клавиша, которую пользователь находит случайно. Порознь их выключать
нечем и незачем.
**Подпись называет клавишу этой машины:** `⌘K` на Apple, `Ctrl K` на остальных.
Обработчик слушает и `meta`, и `ctrl` — значит подпись «⌘K» на Windows была не
сокращением, а неверным именем работающей клавиши. В React подпись считается
после монтирования: на сервере `navigator` нет, и вычисленная там подпись
разошлась бы с клиентской (гидрация с предупреждением).
**Подсказка сочетания — элемент, а не текст в placeholder.** Пока «⌘K» стояло
строкой внутри «Поиск… ⌘K», её нельзя было ни убрать, ни перевести, ни
отличить от собственно подсказки поля. Гейт 19 держит это правило.
Голый `.kt-ai-search` без обёртки продолжает работать — им набраны фильтры над
таблицами, у которых никакого сочетания нет.
## Группа разделов в меню
Тринадцать пунктов подряд читаются как список ссылок, а не как устройство продукта. Необязательное поле `group` собирает идущие подряд пункты под общим заголовком:
```json
"nav": [
{ "label": "Обзор", "group": "Поток" },
{ "label": "Нагрузка", "group": "Поток" },
{ "label": "Сверки", "group": "Достоверность" },
{ "label": "Контроль Alem","group": "Достоверность" }
]
```
**Заголовок — не пункт.** Он не кликается, не получает фокус и в нумерации разделов не участвует: переход идёт по индексу пункта в `nav`, поэтому `banner.actionNav` и `S.nav(i)` считают ровно то же, что считали без групп. Пункты без `group` идут списком, как раньше.
Заголовок собирает пункты, идущие ПОДРЯД. Разнесённые по списку пункты с одним значением дадут два одинаковых заголовка — валидатор об этом предупреждает, а рантайм не переставляет разделы: их порядок принадлежит продукту.
В полосе значков (832–1023px) слов показать негде, и заголовок становится тем, чем является по сути, — тонким разделителем между группами. Ниже 832px sidebar превращается в выдвижную панель во всю ширину, и заголовки снова показываются словами.
Сворачивания групп нет намеренно: свёрнутая группа прячет разделы, которые продукт обязан показывать, — в том числе пустые.
## Стек, который группирует
`.kt-ai-v` — вертикальный стек вместо `marginTop` у каждого потомка. Кроме сырых ступеней (`xs/sm/lg/xl` → `space-*`) у него есть **ступени ритма**, и на экране пользоваться нужно именно ими:
| `data-gap` | | Что этим сказано |
|---|---|---|
| `tight` | 12 | это одна вещь: фильтры и их вкладки, карточка ↔ карточка в списке |
| `block` | 16 | панель управления и то, чем она управляет; блоки одного региона |
| `group` | 24 | регионы экрана: шапка ↔ баннер ↔ содержимое |
| `region` | 32 | поля рабочей области, отрыв от топбара |
Ступени — у `.kt-ai-h-stack` тоже (`tight`, `block`).
**Один зазор на весь корневой столбец — самая частая ошибка компоновки.** Тогда панель фильтров, её вкладки и карточки списка расходятся на одно и то же расстояние: групп на экране нет, есть столбец одинаково далёких полос. Вкладывай стек в стек — регионы снаружи, группа внутри:
```html
<div class="kt-ai-v" data-gap="group"> <!-- регионы: 24 -->
<div class="kt-ai-kpi-strip" data-cards="true">…</div>
<div class="kt-ai-v" data-gap="block"> <!-- панель + её содержимое: 16 -->
<div class="kt-ai-toolbar"> <!-- фильтры и вкладки — одна вещь: 12 -->
<div class="kt-ai-filterbar">…</div>
<div class="kt-ai-filterbar">…</div>
</div>
<div class="kt-ai-v" data-gap="tight">…карточки списка…</div>
</div>
</div>
```
`.kt-ai-toolbar` — та же ступень `tight`, названная по роли: панель шага. Отдельный класс, потому что панель повторяется на каждом экране-очереди, и без имени её собирали своей обёрткой в каждом продукте.
**Вертикальный `padding` `.kt-ai-filterbar` внутри стека снимается** (`.kt-ai-v > .kt-ai-filterbar`, `.kt-ai-toolbar > .kt-ai-filterbar`): иначе собственные 6px бара складывались бы со ступенью и `tight` давал бы на экране 24 вместо 12 — ступень называла бы одно, а показывала другое. Вне стека padding остаётся: там он отделяет бар от соседнего содержимого.
Свой `app-stack-*` или своя обёртка панели в продукте — сигнал, что группировку не выразили, а обошли.
## Ряд, которому запрещено переноситься
`.kt-ai-h-stack` по умолчанию переносится. Запрет переноса — два разных значения, и разница не косметическая:
| | |
|---|---|
| `data-nowrap="true"` | ряд не переносится. Что делать с переполнением — забота автора. В узкой колонке такой ряд выходит за родителя и толкает горизонтальную прокрутку **всей страницы** |
| `data-nowrap="scroll"` | ряд не переносится и прокручивает себя сам |
Почему `true` не сделали прокручиваемым молча: `overflow-x: auto` по спецификации вынуждает `overflow-y` стать `auto` тоже — ряд начинает обрезать всё, что вылезает вверх и вниз: меню, поповер, подсказку, кольцо фокуса. Ряд с кебаб-кнопкой сломался бы, и виновника было бы не найти. Поэтому выбор явный и по имени.
Прокрутка широкой **таблицы** — не этот случай: ей занимается `.kt-ai-table-wrap`.
## Сенсорная цель
На ширине ≤768px (или при `pointer: coarse`) ни один интерактивный элемент не ниже **44px** — минимальной цели, в которую попадают пальцем. Порог хранится в одном месте, токене `--kt-ai-control-h-touch`, и раздаётся правилом в конце `kt-ai-components.css`.
Своими руками высоты поднимать не нужно — нужно попасть под правило:
- компоненты ДС — через классы `.kt-ai-*` (высота приходит из `--kt-ai-control-h-*`, которые на сенсорной ширине сами становятся 44px);
- разметка React-кита — через `[data-kt-ai-kit]`, метку поддерева, которую ставит `KTAIShell`: кит размечен утилитами Tailwind, и порог на каждой из ~70 кнопок означал бы 70 мест, где его можно забыть.
Контрол, который обязан быть меньше, помечается `data-kt-touch="off"` — явным отказом. Молча меньше быть нельзя: цена промаха по «Принять»/«Отклонить» — чужое решение.
Ширина при этом остаётся по содержимому: растягивать пилюли фильтра по горизонтали значит ломать плотность. Квадратную цель (иконка, чекбокс) правило задаёт отдельно.
## Рама приложения: контрол появляется вместе со смыслом
`KTAIShell` не рисует того, что не умеет. Кнопки поиска и профиля появляются, только когда продукт дал им содержание, — иначе их нет:
```tsx
<KTAIShell
productName="Консолидация закупок"
search={{ items: команды, placeholder: "Найти проект", onQueryChange: искать }}
user={{ name: "Ержан Асанов", email: "e@kt.kz" }}
userMenu={[{ label: "Настройки", onSelect: открытьНастройки }]}
onSignOut={выйти}
notifications={[{ text: "Запуск завершён", time: "10:24", unread: true, onSelect: открыть }]}
showTourCard={false}
/>
```
Почему так, а не «покажем, потом подключим»: кит вшивал три собственных пункта палитры и меню профиля из четырёх строк — ни у одной не было обработчика. Продукт починить раму не может, он может только заклеить её своим `display: none`, и оба продукта так и сделали (заявка DS-005). Правило компоновки 9d говорит прямо: сломанный или декоративный контрол показывать нельзя. Проверяет гейт 17.
Аватар берёт инициалы из имени (`ktInitials`), уведомление становится кнопкой только при `onSelect`, «перейти» в «Активности» — только при `href`/`onSelect`.
**Помощник — по тому же правилу, с 6.8.0.** Он был единственным исключением:
кнопка рисовалась всегда и била в `/api/ai/chat` — маршрут-заглушку из бандла
ДС, отвечавшую текстом для разработчика. Специалист закупок читал «Mock AI
response… Подключите реальный LLM provider», а продукт прятал кнопку своим CSS
— ровно то, что канон себе уже запретил.
```tsx
// отвечает маршрут продукта
<KTAIShell assistant={{ apiPath: ktAsset("/api/ai/chat"), suggestions: ["Что изменилось?"] }} … />
// отвечает контракт — как в HTML-рантайме, сети не нужно
<KTAIShell assistant={{ answers: { "Почему флаг?": "Тариф ниже SAP." }, fallback: "Ответа пока нет." }} … />
```
Без пропа `assistant` кнопки помощника нет. Пропы `aiContext` и `aiSuggestions`
сняты: ни один из них не говорил, что помощник умеет отвечать. `KTScreen`
собирает `assistant` из блока `ai` контракта сам — экран из контракта получает
работающего помощника, а не заглушку.
Клиент принимает и `reply`, и `answer`: маршрут в бандле отдавал `answer`, а
кит читал только `reply` — помощник не показал бы ответ собственной заглушки ни
разу.
## Раскрытие без JS
`.kt-ai-disclosure` — на div-ах и требует скрипта. Серверной странице нужен нативный `<details>`, состояние которого хранит браузер:
```html
<details class="kt-ai-details">
<summary>Подробности обработки<svg class="kt-icon kt-icon-sm kt-ai-details-chevron">…</svg></summary>
<div>Прочитано 1 284 строки, пригодных 1 191.</div>
</details>
```
Маркер ОС погашен в обеих записях (`::marker` и `::-webkit-details-marker`), шеврон крутится трансформом. Одиночный `<summary>` внутри оболочки тоже получает `cursor: pointer` — курсор единственный признак, по которому видно, что строка раскрывается. Оба продукта написали это себе сами, каждый по-своему; теперь это канон.
## Подвал действий боковой панели
```tsx
<KTRightDrawer title="Позиция 42" actions={<><button className="kt-ai-btn" data-variant="primary">Принять</button><button className="kt-ai-btn">Отклонить</button></>} … />
```
`.kt-ai-drawer-actions` липнет ко дну панели: разбор длинный, а решение принимают внизу — без этого «Принять» и «Отклонить» уезжают за экран и специалист скроллит обратно на каждой карточке.
## Подсказка значения на диаграмме
Столбец, точка или сегмент показывает своё число при наведении, с клавиатуры и по касанию:
```html
<div class="cc-bar" tabindex="0" data-kt-tip='{"title":"14 мая 2026, четверг","rows":[{"label":"Диалогов","value":"1 284"},{"label":"Доля периода","value":"3,9%"}]}'></div>
<div class="cc-bar" tabindex="0" data-kt-tip="15.05.2026: 948"></div>
```
```tsx
{ label: "14.05.2026", value: 1284, hint: { title: "14 мая 2026, четверг", rows: [{ label: "Диалогов", value: "1 284" }] } }
```
**Числа даёт продукт.** `hint` уходит в разметку как есть: рантайм ничего не форматирует и не пересчитывает. Сервер и браузер округляют по-разному, и разошедшийся формат — это разошедшееся число. Без `hint` показывается `label: value` — то же, что показывал нативный `title`.
Почему не `title`: он появляется через секунду, рисуется средствами ОС (чужой шрифт, ни одного токена, в тёмной теме — вставка из чужого продукта), недоступен с клавиатуры, не читается диктором, на планшете не показывается вовсе и умеет одну строку. Почему не `.kt-ai-tooltip`: та однострочная и у края карточки обрезается.
| | |
|---|---|
| марка | любой элемент с `data-kt-tip` — столбец, точка SVG, сегмент donut, клетка будущей тепловой карты |
| появление | сразу, без паузы; привязано к марке, а не к курсору |
| край | подсказка разворачивается внутрь и не обрезается |
| клавиатура | `tabindex="0"`, показ по фокусу, `Esc` убирает НЕ снимая фокус |
| диктор | `role="tooltip"` + `aria-describedby` на марке |
| тач | касание показывает, касание вне — убирает |
| строк | заголовок + до трёх пар; значения справа, `tabular-nums` |
| перерисовка | переживает: слушатели делегированы на `document`, не на марки |
Рисование — `kt-ai-chart-tip.js`, один файл на оба рантайма (как орб). В бандле кита лежит как `public/kt-ai-chart-tip.js`; гейт 15 следит, чтобы доехал.
## Ширина свободного экрана
```json
{ "width": "full" } // общая ширина продукта
{ "width": "reading" } // мера чтения сплошного текста, ~720px
```
Без поля ширину определяют типы секций: экран только из текстовых — `reading`, любой другой — `full`. **Секция `fields` считается НЕ текстом**: пары «подпись — значение» и таблица критериев на 720px читаются хуже, чем на общей ширине, а заголовок экрана всё равно идёт во всю ширину — экран выглядел съехавшим к центру.
Поле нужно, чтобы продукт называл ширину сам, а не подбирал типы секций ради неё.
## Чипы и segmented
Чип (v2.3, Attio): rounded-rect `radius-tag` (5px), высота 20–24px, паддинг 8–10px, шрифт 11–12px, фон chip-токены. Segmented: сегмент 28px, активный – `bg-active` + `fg`.
## Drawer
Правый drawer для деталей с сохранением контекста списка.
- Ширина 420–560px (720px для сравнения evidence); < md – full-screen.
- Header 48px: заголовок + крестик справа сверху; закрытие – Esc и клик мимо.
## v3.6
| Класс | Что это | Референс |
|---|---|---|
| `.kt-ai-slider` (+`.kt-ai-slider-row` с `.ends`) | слайдер настройки на `input[type=range]` (Speed/Stability) | ElevenLabs Settings-панель |
| `.kt-ai-cols` | адаптивный двухколонник: складывается в одну колонку <720px (wizard/inbox/copilot). Прямым детям сам ставит `min-width: 0` — без этого `1fr` (= `minmax(auto,1fr)`) растягивается под содержимое и на телефоне даёт горизонтальную прокрутку страницы | — |
| `.kt-ai-row-hover` / `.kt-ai-hover-lift` | hover-обратная связь рядов и кликабельных карточек | ElevenLabs rows |
---
## Компоновка экрана
Держать прототипы визуально консистентными и полезными разработчикам без тяжёлого процесса.
### Правила
1. Один главный рабочий объект на экран. В табличных продуктах главный объект – таблица.
2. Не вкладывать карточки в карточки. Вторичные детали – ряды, dividers или drawer.
3. Шапка приложения компактная. Никаких лендинг-hero внутри операционных инструментов.
4. KPI – тихая метрик-полоса; для кабинета/дашборда с 2-4 главными метриками допустима сетка стат-карточек (`data-cards="true"`).
5. Фильтры – в одном тулбаре непосредственно над таблицей; применённые – filter-chips под ним.
6. Таблица идёт раньше вспомогательных панелей (статусы, фоновые задачи).
7. Действия ряда тихие: появляются на hover или при выборе (на touch – видимы всегда).
8. Детали открываются в правом drawer, не на новой странице.
9. Точка входа AI не конкурирует с таблицей: кнопка-орб Төре в топбаре рядом с поиском (не плавающий FAB).
9a. Главное действие создания – primary-кнопка вверху sidebar под брендом («Новый …» от product.entity); мастер создания (этапы процесса + загрузка документов) – в modal. Этапы/таймлайн НЕ выносить на главный список – их место в мастере и в деталях записи.
9b. (ElevenLabs) Топбар слева: панель-toggle (свернуть sidebar) + breadcrumb с именем текущего раздела (мелко, muted). Контент: hero-заголовок раздела (text-2xl semibold — 24px; text-3xl/bold это красный флаг DoD) + подпись-назначение слева, главное действие справа (как «New order» в ElevenLabs); лого только в sidebar. Воздух между топбаром и заголовком — `stack-region` (32px), дальше блоки экрана идут через `stack-group` (24px). Имя продукта живёт в sidebar, имя раздела — в breadcrumb и hero (это паттерн ElevenLabs, не дубль двух больших заголовков).
9c. Boilerplate обязателен для shell: sidebar, topbar, theme toggle и AI-кнопка Төре. Эти элементы не перерисовывать и не заменять. Главная рабочая область проектируется под задачу процесса.
9c1. Для рабочих очередей, реестров и консолидации закупок default – classic app-shell: таблица возможностей, детали в правом drawer, короткие статусы и одно primary-действие. Custom workspace используется только если таблица + drawer не решают основную работу пользователя.
9d. Если в прототипе показаны theme toggle, поиск, AI-кнопка или основные action-кнопки, они должны быть интерактивными в рамках прототипа. Нельзя показывать сломанные или декоративные controls.
9e. Поле поиска по умолчанию пустое. Не подставлять случайный пример вроде названия товара, если пользователь прямо не выбрал этот поиск.
9f. В операционном экране консолидации допустимы метрики, которые помогают принять решение: сколько ТС обработано, сколько имеют потенциал консолидации, какой предварительный потенциал экономии в тенге. Это не project ROI, а рабочие числа для приоритизации.
9g. В UI запрещены служебные формулировки генерации: `модель вернула`, `fallback`, `не проходит дизайн-систему`, `shell`, `JSON`, `prompt`. Пользователь видит продуктовый язык, а не внутренний процесс сборки.
10. Горизонтальный скролл на мобильных – только внутри контейнера таблицы.
### Порядок страницы по умолчанию
1. Shell: sidebar и топбар (glass, `.kt-ai-topbar`).
2. Компактный заголовок страницы.
3. Рабочая область под конкретный процесс: таблица, матрица, карточка возможности, сравнение источников или другой подходящий формат.
4. Метрики и фильтры – только если помогают выполнить текущую работу пользователя.
5. Вспомогательные статус-ряды.
6. Второстепенные панели.
7. Вывод раздела (`.kt-ai-conclusion`) — последним блоком, после таблицы.
8. Drawer, toast, AI-кнопка.