Агентный слой
Поле ввода
Поле, в котором пишут реплику: одна рамка держит текст и контролы, которые его отправляют.
Разметка
<div class="inst-stack inst-stack--tight">
<div class="inst-composer">
<textarea class="inst-textarea" rows="1" placeholder="Опишите задачу или задайте вопрос" aria-label="Опишите задачу или задайте вопрос"></textarea>
<div class="inst-composer-bar inst-composer-bar--edge">
<button class="inst-btn inst-btn--sm inst-btn--icon inst-btn--ghost" type="button" aria-label="Отправить"><svg class="inst-icon" aria-hidden="true"><use href="#i-enter"/></svg></button>
</div>
</div>
<div class="inst-composer-bar">
<button class="inst-btn inst-btn--xs inst-btn--ghost" type="button" aria-haspopup="menu">Авто</button>
<button class="inst-btn inst-btn--xs inst-btn--icon inst-btn--ghost" type="button" aria-label="Добавить контекст"><svg class="inst-icon" aria-hidden="true"><use href="#i-plus"/></svg></button>
<span class="inst-cluster-spacer"></span>
<button class="inst-btn inst-btn--xs inst-btn--ghost" type="button" aria-haspopup="menu">Opus 5</button>
<span class="inst-badge" data-tone="warn">контекст 90%</span>
</div>
</div>
Контракт
| Что | Обязательно | Почему |
|---|---|---|
aria-label или <label> у поля |
да | Плейсхолдер — не имя: он исчезает на первом же символе |
| Контролы сообщения внутри рамки | да | Вложение и отправка принадлежат полю. Снаружи они второй объект, и кольцо фокуса подсвечивает половину контрола. То, что переживает сообщение, — режим, модель, контекст — стоит под рамкой отдельной полосой, и быть вторым объектом там как раз и требуется |
<button type="button"> для отправки |
да | Внутри формы кнопка без типа её отправляет |
Отправка по Enter |
нет | Кит оформляет; отправляет ли Enter или переносит строку — решение приложения, и обоснованы оба |
Граница живёт на рамке, а поле отдаёт свою наружу — тот же приём, что у группы поля. Текстовое поле рядом с панелью инструментов даёт две границы с зазором; группа поля растягивает кнопку отправки на всю высоту; панель вокруг обоих рисует рамку внутри рамки.
Доступность
| Имя | На поле, а не на рамке: рамка это <div> и фокуса не берёт |
| Фокус | Кольцо охватывает рамку целиком, поэтому подсвечен тот объект, в котором находится клавиатура |
| Контраст | Граница — --border-control, 3.46:1: линия несущая, потому что здесь граница и есть контрол |
Варианты
Строка может занимать всю ширину внутри рамки, а не хвостовой слот. Это форма для композера, у которого контролов больше, чем отправка: вложение, модель, счётчик остатка. Цена — пустое поле теряет свою одну строку, и это размен.
Разметка
<div class="inst-composer">
<textarea class="inst-textarea" rows="3" placeholder="Опишите задачу" aria-label="Опишите задачу"></textarea>
<div class="inst-composer-bar">
<button class="inst-btn inst-btn--xs inst-btn--ghost" type="button">Вложить</button>
<span class="inst-badge">opus</span>
<span class="inst-cluster-spacer"></span>
12 480 / 200 000
<button class="inst-btn inst-btn--sm inst-btn--primary" type="button">Отправить</button>
</div>
</div>
Размеры
Пустой композер — высотой в одну строку. Было две с половиной: столько textarea имеет по определению, и для формы это верно — поле говорит, какого объёма ответ ожидается. Композер говорит другое: три пустые строки обещают абзац, которого никто не набирал, и форма объекта начинает врать о его содержимом.
Поэтому нижняя граница снята, а высоту задаёт содержимое — через
field-sizing: content. Потолок — восемь контролов, дальше поле прокручивается,
а не съедает ленту над собой. Там, где field-sizing не реализован, поле
принимает высоту своего rows и прокручивается: поле поменьше, а не сломанное,
и альтернатива — скрипт, которого у кита нет.
inst-composer-bar--edge ставит строку в хвостовой слот, а не на
отдельную строку, — именно это и делает одну строку возможной: отправка это
один контрол, и своей строки он не заслуживает. Слот прижат к низу поля:
на одной строке это читается как по центру, на десяти — остаётся у последней.
Разметка
<div class="inst-composer">
<textarea class="inst-textarea" rows="1" aria-label="Выросшее поле">Поле растёт вместе с тем, что в него набирают, а отправка остаётся у последней строки, а не висит посреди пустой рамки.</textarea>
<div class="inst-composer-bar inst-composer-bar--edge">
<button class="inst-btn inst-btn--sm inst-btn--icon inst-btn--ghost" type="button" aria-label="Отправить"><svg class="inst-icon" aria-hidden="true"><use href="#i-enter"/></svg></button>
</div>
</div>
API
| Имя | Что делает |
|---|---|
| класс | |
inst-composer | Рамка. Несёт границу, фон и кольцо фокуса |
inst-composer-bar | Контролы. Внутри рамки они принадлежат сообщению, под ней — сессии. Всё, что не контрол, читается метаданными, а глифы берут мелкую иконку |
| модификатор | |
inst-composer-bar--edge | Строка в хвостовом слоте рядом с полем, а не на отдельной строке. Прижата к низу, поэтому пустой композер высотой в одну строку |
| Имя | Значение | Откуда |
|---|---|---|
| токен | ||
--control-h-mdПотолок роста — восемь таких | 32px | база +14: плотность · масштаб |
--border-control | light-dark(oklch(0 0 0 / 0.46), oklch(1 0 0 / 0.38)) | одно на все ячейки |
--accent-border | light-dark(var(--a-4), var(--a-3)) | одно на все ячейки |
--surface-field | var(--surface-raised) | одно на все ячейки |
--focus-ring | light-dark(var(--a-4), var(--a-3)) | одно на все ячейки |
--radius-lg | 12px | база +4: масштаб |
--control-pad-sm | calc(var(--control-h-sm) * var(--control-ratio-sm)) | одно на все ячейки |
--space-3 | 6px | одно на все ячейки |
--text-xs | 0.78125rem | база +4: масштаб |
--text-muted | light-dark(var(--n-8), var(--n-6)) | одно на все ячейки |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument