Агентный слой
Строка очереди
Одна задача в очереди агентов. Строка плоская: у неё нет тела, потому что их на экране сотни, и рамка вокруг каждой превратила бы очередь в сетку.
Разметка
<div role="listbox" aria-label="Очередь агентов">
<div class="inst-task" role="option" data-state="done" aria-selected="false" tabindex="-1">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main">
<span class="inst-task-title inst-u-truncate">Разбор карты высот</span>
<span class="inst-task-sub">terrain/heightmap.ts · 4 файла</span>
</span>
<span class="inst-task-meta">2,1 с</span>
</div>
<div class="inst-task" role="option" data-state="running" aria-selected="true" tabindex="0">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main">
<span class="inst-task-title inst-u-truncate">Генерация биомов</span>
<span class="inst-task-sub">world/biomes.ts</span>
</span>
<span class="inst-task-meta">8,4 с</span>
</div>
</div>
Использование
<div role="listbox" aria-label="Очередь агентов">
<div class="inst-task" role="option" data-state="running" aria-selected="true" tabindex="0">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main">
<span class="inst-task-title inst-u-truncate">Генерация биомов</span>
<span class="inst-task-sub">world/biomes.ts</span>
</span>
<span class="inst-task-meta">8,4 с</span>
</div>
</div>
| Что | Обязательно | Почему |
|---|---|---|
role="listbox" снаружи, role="option" на строке |
да | Очередь — список с выбором. Без ролей это поток из тридцати div |
data-state на строке |
да | Фаза задачи. Точка в жёлобе наследует тон отсюда |
Бегущий tabindex |
да | 0 у выбранной, -1 у остальных: Tab входит в список один раз, дальше стрелки |
inst-u-truncate на заголовке |
да | Длинное название иначе распирает строку и выталкивает время за край |
Когда использовать
Оба показывают работу машины, и путать их нельзя.
| Строка очереди | Шаг | |
|---|---|---|
| Форма | Плоская, одна строка | Есть тело, раскрывается |
| Что показывает | Задачу в списке | Вызов инструмента с выводом |
| Сколько на экране | Сотни | Десятки |
| Раскрытие | нет | <details>, бесплатно от платформы |
| Выбор | да, aria-selected |
нет |
| Возьмите другое | Когда |
|---|---|
| Таблица | Задачи сравнивают по нескольким полям |
| Лента событий | Важен момент времени, а не состояние |
| Шаги мастера | Шаги известны заранее и конечны |
Устройство
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
Левая колонка фиксированной ширины (--size-gutter). Она держит точку
состояния на одной вертикали во всех строках — без неё заголовки разной длины
разъезжают маркеры, и очередь перестаёт читаться столбцом.
Состояния
data-state |
Что происходит |
|---|---|
queued |
Базовое. Оформления нет — задача ждёт |
running |
Идёт. Точка пульсирует, заголовок берёт акцент |
done |
Завершено. Заголовок отступает в --text-secondary |
warn |
С замечаниями |
failed |
Упало |
skipped |
Пропущено. Прозрачность 0.7 |
Завершённые отступают, а не гаснут. История остаётся читаемой, но
перестаёт спорить с тем, что идёт сейчас. Прозрачность применяется только к
skipped — и она мягкая: 0.55 роняло подпись под порог читаемости.
JS
Подключите модуль один раз на страницу — инициализировать компоненты по отдельности не нужно, кит работает делегированием и видит узлы, пришедшие позже.
<script type="module" src="src/kit.js"></script>
Что делает кит
Пример в шапке живой: войдите в очередь Tab и пройдите строки стрелками — выделение идёт за фокусом.
Очередь объявлена как role="listbox", и кит выполняет контракт этой роли:
один Tab на весь список, стрелки внутри, Home и End, перебор по кругу.
Выделение следует за фокусом и переносится щелчком. Строки, пришедшие во время
работы, обходятся наравне с остальными — элементы ищутся в момент нажатия.
События
inst:select всплывает с выбранной строки, detail — { value }.
queue.addEventListener('inst:select', (e) => {
details.show(e.detail.value);
});
Отмена оставляет aria-selected нетронутым: приложение ведёт выделение само.
Опции
| Атрибут | Что делает |
|---|---|
data-value |
Идентификатор задачи в detail вместо текста строки |
aria-disabled="true" |
Строка выпадает из обхода стрелками, но остаётся видимой |
Что остаётся приложению
Порядок задач, их состояния и отмена — данные. Кит рисует состояние, но не знает, когда оно меняется.
Сценарии
Одна строка очереди не показывает почти ничего: состояния читаются только рядом друг с другом.
Что видно только здесь:
- идущая пульсирует, а завершённая — нет. Пульсация и есть носитель «идёт», тон вторичен;
- завершённая отступает: заголовок уходит в
--text-secondary, но остаётся читаемым. История не спорит с тем, что происходит сейчас; - пропущенная — единственная, к кому применена прозрачность, и она
мягкая:
0.55роняло подпись под порог читаемости; - точки стоят на одной вертикали независимо от длины заголовков. За это отвечает жёлоб фиксированной ширины.
Разметка
<div class="inst-panel">
<div class="inst-panel-header">
<span class="inst-panel-title">Очередь агентов</span>
<span class="inst-panel-actions"><span class="inst-badge">7 из 12</span></span>
</div>
<div class="inst-panel-body inst-panel-body--list">
<div class="tree-none" role="listbox" aria-label="Очередь агентов" id="queue">
<div class="inst-task" role="option" data-state="done" aria-selected="false" tabindex="-1">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main"><span class="inst-task-title inst-u-truncate">Разбор карты высот</span>
<span class="inst-task-sub">terrain/heightmap.ts · 4 файла</span></span>
<span class="inst-task-meta">2,1 с</span>
</div>
<div class="inst-task" role="option" data-state="done" aria-selected="false" tabindex="-1">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main"><span class="inst-task-title inst-u-truncate">Генерация биомов</span>
<span class="inst-task-sub">world/biomes.ts · 11 файлов</span></span>
<span class="inst-task-meta">8,4 с</span>
</div>
<div class="inst-task" role="option" data-state="running" aria-selected="true" tabindex="0">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main"><span class="inst-task-title inst-u-truncate">Расстановка ресурсов</span>
<span class="inst-task-sub">идёт третий проход<span class="inst-caret"></span></span></span>
<span class="inst-task-meta">14,0 с</span>
</div>
<div class="inst-task" role="option" data-state="failed" aria-selected="false" tabindex="-1">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main"><span class="inst-task-title inst-u-truncate">Валидация путей</span>
<span class="inst-task-sub">3 узла недостижимы</span></span>
<span class="inst-task-meta">1,2 с</span>
</div>
<div class="inst-task" role="option" data-state="queued" aria-selected="false" tabindex="-1">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main"><span class="inst-task-title inst-u-truncate">Запекание навмеша</span>
<span class="inst-task-sub">ждёт валидацию</span></span>
<span class="inst-task-meta">—</span>
</div>
<div class="inst-task" role="option" data-state="skipped" aria-selected="false" tabindex="-1">
<span class="inst-task-gutter"><span class="inst-dot"></span></span>
<span class="inst-task-main"><span class="inst-task-title inst-u-truncate">Экспорт превью</span>
<span class="inst-task-sub">пропущено флагом</span></span>
<span class="inst-task-meta">—</span>
</div>
</div>
</div>
</div>
Правила
Так Очередь — настоящий listbox
role="listbox" снаружи, role="option" на строке, бегущий tabindex. Иначе
это поток из тридцати div.
Не так Строка очереди вместо шага
У задачи есть фаза и время, у шага — вывод, который раскрывают. Это разные компоненты.
Так Фаза точкой и словом
Точка в жёлобе наследует тон от data-state, а подпись называет фазу.
Не так Обрезка заголовка без truncate
Длинное название распирает строку и выталкивает время за край.
Доступность
| Роли обязательны | Контейнер role="listbox", строка role="option". Без них aria-selected невалиден, и строка не озвучится как выбираемая |
| Клавиатура | Бегущий tabindex: 0 у активной, -1 у остальных. Tab входит в список один раз, дальше стрелки, Home/End. Поведение — на приложении |
| Состояние словом | data-state красит строку, но не озвучивается. Состояние обязано быть в тексте — в подписи или в бейдже рядом |
| Обрезанный заголовок | inst-u-truncate режет визуально, полный текст остаётся в DOM и озвучивается. Добавьте title для мыши |
| Уменьшенное движение | Пульсация точки running замедляется, а не гаснет: очередь агентов без признака работы бесполезна |
| Выделение и наведение складываются | Подсветка выбранной полупрозрачна, поэтому наведение на неё остаётся видно |
API
| Имя | Значение | Что делает |
|---|---|---|
| класс | ||
inst-task | — | Строка |
inst-task-gutter | — | Левый жёлоб под точку |
inst-task-main | — | Заголовок и подпись |
inst-task-title | — | Название задачи |
inst-task-sub | — | Что именно делается |
inst-task-meta | — | Время или счётчик, прижат к концу |
| атрибут | ||
data-state | — | queued running done warn failed skipped |
aria-selected | — | true · false |
| токен | ||
--size-gutter | 14px | |
--size-dot | 6px | |
--row-pad-y | var(--space-3) | |
--pad-cell-x | var(--space-4) | |
--gap-inline | var(--space-4) | |
--space-1 | 2px | |
--text-sm | 0.8125rem | |
--text-xs | 0.75rem | |
--surface-selected | color-mix(in oklab, var(--a-4) 14%, transparent) | |
--surface-hover | light-dark(oklch(0 0 0 / 0.035), oklch(1 0 0 / 0.045)) | |
--tone-ink | var(--accent-text) | |
Длинный заголовок обрезается через inst-u-truncate — иначе он выталкивает
метаданные за край.
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument