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

58 KiB
Raw Permalink Blame History

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, которая ловит нарушение:

[...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:

<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, а не в конец. Проверено измерением.

Кружок с номером

Шаг мастера, позиция в списке, порядковый номер.

<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>
<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:

<svg class="kt-icon"><use href="#circleNumber3"></use></svg>
<KTIcon name="circleNumber3" size={15} />

Набор взят из Tabler Icons (MIT) и приведён к нашей сетке: радиус окружности 10 вместо 9, штрих 1.75 вместо 2 — чтобы в ряду с check-circle они были одного размера и веса.

Иконка или компонент. Иконка — фиксированные 0–9 и один цвет, зато встаёт в любой поток иконок. Компонент .kt-ai-num-badge — любое число, включая двузначные, состояния и заливка активного. Нужен номер шага в мастере — компонент; нужен номер рядом с иконкой в строке — иконка.

Орб агента

Монохромное точечное облако на canvas — индикатор того, что агент работает. Заменяет спиннер там, где ожидание содержательное, и стоит в кнопке помощника вместо подписи «AI».

<canvas class="kt-ai-orb" data-state="thinking" style="width:20px;height:20px"></canvas>
<script src="kt-ai-orb.js"></script>
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 (MIT). Сам пакет не подошёл: его сборка импортирует React на верхнем уровне, а HTML app-shell работает без React.

Плашка показателя

Четыре уровня: подпись, значение, уточнение, сноска.

{ "label": "Просрочено в июне", "value": "58", "hint": "из 171" }
<div class="kt-ai-kpi">
  <span class="value">58</span><span class="label">Просрочено в июне</span>
  <span class="hint">из 171</span>
</div>
<KTKpiCard label="Просрочено в июне" value="58" hint="из 171" />

Значение — само число, уточнение — знаменатель, доля или период. Не «58 из 171» одной строкой: полоса перестаёт читаться как ряд величин, глаз ищет число, а находит фразу. Уточнение до 40 символов.

Значение идёт сразу за подписью, а не прижимается к низу карточки. Иначе в соседних карточках — с уточнением и без — числа встают на разной высоте и кажутся разными по важности. Проверяется измерением: у всех плашек полосы value обязан быть на одной высоте.

Уровень поддержан во всех трёх местах: поле hint в product.schema.json, вывод в HTML-рантайме и в KTKpiCard. Компонент рисуется классом .kt-ai-kpi в обоих рантаймах — своих утилит у React-версии нет, иначе правка в ДС до неё не доезжала бы.

Сноска: чем число НЕ является

hint отвечает «из чего считано» и потому ограничен 40 знаками — снимать этот потолок нельзя, иначе полоса плиток перестанет быть полосой. Но у поля-источника бывает вторая правда: оно меряет не ровно то, чем показатель назван. «Ожидание в очереди» не отделяет автоприветствие бота от первого сообщения оператора — число верное, а работой человека не является. Это другой вопрос, и отвечает на него отдельное поле:

{ "label": "Ожидание в очереди", "value": "00:41", "hint": "из 33 352 диалогов",
  "note": "Поле выгрузки считает время до первого сообщения в чате, а им бывает автоприветствие бота. Число верное, но работой оператора оно не является." }
<span class="note">Поле выгрузки считает время до первого сообщения…</span>
<KTKpiCard label="Ожидание в очереди" value="00:41" hint="из 33 352 диалогов" note="Поле выгрузки считает…" />

Потолка у сноски нет — она не стоит в потоке полосы, её отбивает линия. И она не прячется под клик. Оговорка существует затем, чтобы число не прочитали неверно; спрятанное под клик читают не все — ровно как карточку строки, из-за чего оговорку и потребовалось ставить у плитки. По той же причине у неё цвет fg-muted, а не самый тихий fg-faint: 4,9:1 на кегле 11px — это «тихо» на грани нечитаемого, а сноску надо прочитать.

Куда сноска НЕ идёт: в баннер. Баннер стоит над числами, и оговорки, собранные в него, дают абзац текста до первого числа (замеряли: 716 знаков на одном экране). Баннер предупреждает, из-за чего число соврёт; сноска объясняет, что число значит; вывод раздела — что из чисел следует. Три разных места.

Вывод раздела

Последний блок экрана, после таблицы. Контракт: conclusion: { title?, text }, пустая строка в text делит абзацы.

<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 уходит в корень САЙТА и не находится: пропадают спрайт иконок, орб, подсказка диаграммы и режим отзыва — тихо, только на развёртывании.

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 «Чарты».

Счётчик в пункте меню

Одно значение — как раньше, приглушённым тоном:

{ label: "Проекты", icon: "lots", count: "12" }

Прогресс — пара «всего / сделано». «Сделано» рисуется тоном ok, и по пункту меню видно не только объём работы, но и продвижение:

{ label: "Спецификации", icon: "file", count: { value: 11, done: 5 } }
<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 кита, а не в контракте.

Поиск в шапке

Два размера и один переключатель подсказки сочетания.

<KTAIShell search={{ items, placeholder: "Договор, филиал…", compact: true }}>
"search": { "placeholder": "Договор, филиал…", "compact": true }
<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 собирает идущие подряд пункты под общим заголовком:

"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).

Один зазор на весь корневой столбец — самая частая ошибка компоновки. Тогда панель фильтров, её вкладки и карточки списка расходятся на одно и то же расстояние: групп на экране нет, есть столбец одинаково далёких полос. Вкладывай стек в стек — регионы снаружи, группа внутри:

<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 не рисует того, что не умеет. Кнопки поиска и профиля появляются, только когда продукт дал им содержание, — иначе их нет:

<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 — ровно то, что канон себе уже запретил.

// отвечает маршрут продукта
<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>, состояние которого хранит браузер:

<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 — курсор единственный признак, по которому видно, что строка раскрывается. Оба продукта написали это себе сами, каждый по-своему; теперь это канон.

Подвал действий боковой панели

<KTRightDrawer title="Позиция 42" actions={<><button className="kt-ai-btn" data-variant="primary">Принять</button><button className="kt-ai-btn">Отклонить</button></>} … />

.kt-ai-drawer-actions липнет ко дну панели: разбор длинный, а решение принимают внизу — без этого «Принять» и «Отклонить» уезжают за экран и специалист скроллит обратно на каждой карточке.

Подсказка значения на диаграмме

Столбец, точка или сегмент показывает своё число при наведении, с клавиатуры и по касанию:

<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>
{ 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 следит, чтобы доехал.

Ширина свободного экрана

{ "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-кнопка.