594 lines
58 KiB
Markdown
594 lines
58 KiB
Markdown
# 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-кнопка.
|