О проекте
Принципы дизайна
Правила, из которых выводятся компоненты instrument. Это не список компонентов: при расхождении правила и компонента приоритет у правила — исправляется компонент.
Дисциплина — утилитарный минимализм: швейцарская сетка плюс Tufte, отрендеренные плоско. Область применения — интерфейсы, которые показывают человеку работу машины: очереди задач, прогоны, логи, инспекторы, состояния и неопределённость.
Три закона
1. Цвет — это смысл. Если цвет не кодирует состояние, категорию или полярность, он удаляется. Четыре разных цвета на четырёх полосках, меряющих одну величину, — ложное сообщение.
2. Компонент не знает о темах. Он обращается только к семантике и ролям
(--text-primary, --pad-panel). --n-3 или #333 в компоненте — это
захардкоженная светлая тема, 14px — сломанная плотность. Тёмная тема не
инверсия: она объявлена в тех же строках, что светлая, через light-dark().
3. Иерархия важнее украшения. Главное на экране — данные. Подписи, рамки, шапки и иконки обслуживают их и должны быть тише. Заголовок капсом с трекингом звучит громче числа, которое подписывает.
Правило о правилах
Правило, выведенное из замера, обязано назвать объект, на котором замер сделан. Обобщение за пределы замеренного — не правило, а гипотеза.
Дефект, ради которого это записано, встретился трижды за один разбор, и каждый раз замер был честным — неверна была область применения.
| Замерили | Записали | Что сломалось |
|---|---|---|
| плитку метрики внутри панели | закон «о метрике» | полоса под шапкой, которой в замере не было, оказалась запрещена |
| панель площадью 36 000 px² | закон «о панели» | на реальных 201 000 px² вывод переворачивается |
| — | следствие первого в tokens.css: --text-2xl только для h1 |
запись пережила причину и вводила в заблуждение |
Такое не ловится ни гейтом, ни глазом: правило внутренне непротиворечиво и подтверждено числом. Его ловит только тот, кто приходит с другим объектом. Назвать объект в самом правиле — единственная страховка, которая ничего не стоит.
Словарь
Четыре яруса. Каждый параметр существует в единственном экземпляре: дубликат означает будущее расхождение.
| Ярус | Где | Правило |
|---|---|---|
| 1. Рампы | tokens.css |
15 нейтральных шагов + акцент + 3 статуса. Не меняются между темами. Рампа непрерывна: шаг, не занятый семантикой, — резерв |
| 2. Семантика | tokens.css |
Для чего цвет нужен. Одно объявление на токен через light-dark() |
| 3. Роли | tokens.css |
Отступы, высоты, размеры глифов и жёлобов по назначению — и пропорции, когда одно должно ехать за другим. Единственный ярус, который перенастраивают плотность и масштаб |
| 4. Компонент | рядом с компонентом | --btn-bg и подобные. Одна-две у компонента без вариантов; по одной на ось у компонента с вариантной матрицей |
Компонент имеет право видеть ярусы 2 и 3. Ярус 1 — никогда.
Счёт переменных яруса 4 — не норма, а мера. Медиана по библиотеке — одна на компонент, и это правильное число для компонента, у которого вариантов нет. Больше их ровно там, где есть матрица: у кнопки четыре веса × тон × четыре состояния, и семь переменных — самая дешёвая запись такой матрицы, потому что вариант остаётся двумя строками вместо переписывания правила. Признак нарушения не количество, а то, что переменная не отвечает ни за какую ось: заведённая «на всякий случай» и читаемая в одном месте — это не ярус, а имя для константы.
Правила ярусов
-
У смыслового тона ровно два токена:
--*-text(метка: текст, точка, заливка меры, полоса сноски) и--*-bg(тонированный фон под ней). Исключение одно — акцент, у которого есть заливка под белой подписью (--accent-solid). -
Несущая рамка и декоративная — разные токены. Если граница и есть контрол (чекбокс, поле, дорожка свитча, слайдер), она держит 3:1 и берёт
--border-control. Если рядом есть перепад поверхностей (панель, карточка) — берёт--border.Список несущих закрыт, и кнопки в нём нет: у кнопки внутри подпись, и опознаёт её подпись, а не контур. Кнопка и клавиша (
kbd— изображение клавиши, а не контрол) берут--border-strong, то есть 1.69:1 против 3.4:1 у несущей. Всё, у чего внутри пусто, остаётся на несущей. -
Границу находят перепадом И площадью, на которой он лежит. Контраст пары цветов — свойство пары; находимость края — свойство пары и того, какую долю экрана она занимает. Гейт, меряющий первое и выносящий приговор второму, ошибётся.
Замерено 2026-09-10, страница против панели по пяти темам: 1.068 · 1.071 · 1.071 · 1.071 · 1.121 — и один и тот же перепад прочитался по-разному на двух размерах. На панели 36 000 px² безрамочная панель теряется на
dark; на панели реального экрана 201 000 px² — в 5.6 раза больше площади при тех же токенах — держится во всех пяти темах.Поэтому вопрос «нужна ли панели рамка» рампой не решается, и любая проба его обязана идти на том размере, какой у панели на самом деле.
-
Ссылка интерфейса и ссылка прозы — разные вещи. В интерфейсе
<a>наследует цвет и подчёркивается только под курсором: её опознают по месту и форме, как кнопку. Акцент с постоянным подчёркиванием живёт в.inst-prose, где ссылка стоит среди текста и опознать её иначе нечем. -
Кольцо фокуса отличается от того, что обводит. Ему мало держать 3:1 к поверхностям: под ним бывает сплошная заливка.
--focus-ringберёт шаг акцента, который никогда не бывает заливкой. -
Поверхность аннотации выпадает из стопки. У тултипа инверсная поверхность (
--surface-inverse/--text-on-inverse) — такого цвета на экране больше нет ни у чего, и подсказка опознаётся раньше, чем прочитана. Инверсия к странице, а не фиксированный чёрный: чёрная плашка на тёмной странице исчезает. -
--text-faintне применяется к тексту, который читают. Это порог декорации (3:1), а не чтения (4.5:1). Таймстемп лога — данные, ему положен--text-muted. -
Текст и метка — разные передние планы. У тона есть
--tone-ink(текст, 4.5:1) и--tone-mark(точка, каретка, бегунок, заливка меры — 3:1, но ещё и против дорожки). У статусов они совпадают на шаге 5. У акцента расходятся:--accent-textберёт шаг 5, иначе не возьмёт 4.5:1 на белом, а--accent-mark— шаг 4, тот же, что заливка кнопки. Метка и заливка обязаны совпадать, иначе на одном экране соседствуют два разных синих. В тёмной теме они расходятся: заливке нельзя вверх из-за белой подписи, метке нельзя вниз из-за дорожки. -
overscroll-behavior: containставится только там, где прокрутка гарантирована. Контейнер сoverflow: auto, которому нечего прокручивать, приcontainсъедает колесо мыши: страница под ним не двигается. То есть вместе с виртуализацией, а не заранее. -
Псевдоэлемент в гриде размещается явно.
::beforeучаствует в автопотоке наравне с элементами и идёт первым; одна явнаяgrid-columnу него ломает авторазмещение остальных ячеек. Либо все ячейки явные, либо ни одной. -
overflow-wrap: anywhereживёт только на той ячейке, где нужен. Он уменьшает минимальную внутреннюю ширину до одного знака и на контейнере схлопывает колонки грида. -
Дефолты объявляются через
:where(). Дефолт с той же специфичностью, что и переопределение, — это не дефолт. -
Зазор ставится между соседями, а не вокруг каждого.
margin-blockна каждом элементе складывается с внутренним отступом контейнера сверху и снизу, но не по бокам. Правильная форма —X + X { margin-block-start }. -
Порядок состояний записывается селектором, а не порядком строк: отключено > ошибка > фокус > наведение. У
:hover:not(:disabled)специфичность (0,3,0), у:user-invalid— (0,2,0), поэтому наведение уступает явно, через:not(:user-invalid). -
Валидацию показывает платформа:
:user-invalid, а не:invalid.:invalidгорит красным до ввода, и форма встречает пользователя ошибками, которых он не совершал. -
readonlyиdisabled— разные вещи. Одно нельзя изменить, но можно прочитать и скопировать (врез, полный цвет текста); другое недоступно целиком (прозрачность 0.5). -
Хайрлайн включается по плотности пикселей. Браузер не рисует рамку тоньше физического пикселя: на 1x-дисплее
0.5pxи1px— одно и то же.
Запрещено
-
Вес 700.
base.cssзакрывает единственную дверь, через которую его приносит платформа:strongиbсброшены на--weight-medium.--weight-mediumравен 600. У Segoe UI настоящего 500 нет, и системный стек снапает его к Semibold, тогда как SF Pro на macOS даёт настоящий Medium — при значении 500 рисунок отличался бы между платформами в самой нагруженной оси иерархии. Своего шрифта нет: он стоил бы обещания «без сборки и без зависимостей». -
ЗАГОЛОВКИ КАПСОМ и Title Case. Только обычное предложение.
-
Градиенты, свечения, размытия. Скелетон пульсирует прозрачностью, а не бликом. Граница проходит по РАЗМЫТИЮ: запрещено то, у чего есть радиус. Жёсткая волосяная линия внутрь — кромка сплошной кнопки — не свечение, а край: у неё нет ни расплыва, ни ореола, и работает она тем же, чем рамка.
-
Тень на карточке или панели. Карточка получает поверхность, а не тень.
-
Скругление на односторонней рамке.
border-left+border-radius— всегда баг. -
Второй акцентный тон одновременно. Сменить сам акцент целиком можно:
data-accentна четыре положения (петроль · графит · индиго · глина), и все четыре проходят гейт контраста наравне с пятью темами. -
Размер шрифта меньше 11px.
-
Цвет как единственный носитель состояния. Статус ходит с точкой и словом, сноска несёт иконку, строка дифа несёт знак.
-
Двойная ось Y на графике. Две шкалы — это два графика.
-
Число в компоненте. Новое значение заводится токеном яруса ролей.
-
Цвет внутри data-URI. Форма рисуется маской, цвет приходит токеном.
-
!important. Одно исключение, названное поимённо:[hidden]вbase.css— это корректность, а не оформление.
Категориальная палитра
Оговорённое исключение из первого закона: ряд графика — это категория, поэтому исключение закон уточняет, а не нарушает. Правила у палитры свои:
-
Порядок — часть API. Ряд №1 всегда
--chart-1. Иначе один и тот же показатель меняет цвет от экрана к экрану. -
Один ряд — не категория. Единственный ряд берёт
--accent-mark, а не--chart-1: категориальный цвет существует, чтобы различать ряды. -
Тона отстоят минимум на 25° от каждого статусного (25, 85, 150), чтобы категориальный цвет не прочитался как «упало».
Про акцент такой гарантии нет: он ручка на четыре положения (215 · 250 · 275 · 45). Вместо дистанции — разделение областей: акцент не заходит внутрь графика, категориальный цвет не выходит наружу. Встречаются они только в легенде многорядного графика, где их различает светлота.
-
Светлота внутри палитры разная, чтобы ряды оставались различимыми при дальтонизме и на чёрно-белой печати.
-
Категориальный цвет кодирует категорию, и только её. Он допустим там, где множество значений перечислимо и закрыто: ряд графика, тип синтаксического токена, тип файла. Запрещён там, где значение означает состояние, полярность или интеракцию: статус, заливка кнопки, подсветка строки.
Два условия при этом не обсуждаются. Цвет никогда не единственный носитель: у токена есть форма, у файла — расширение, у ряда — легенда. И порог берётся по работе: палитра рассчитана на метку (3:1), на тексте нужен текстовый (4.5:1).
-
Шесть — потолок. Седьмой ряд означает, что это не график, а таблица.
-
Легенда обязательна при двух и более рядах.
Информационного тона нет
Синий занят акцентом — интеракцией и состоянием «идёт». Третья роль сделала бы
его нечитаемым, а второй синий запрещён. Информационное сообщение берёт тон
neutral: приглушённая заливка и значок ⓘ.
Одно совмещение
Акцент несёт и интеракцию, и состояние «идёт». Формально это нарушение первого закона, и оно допущено потому, что носителем «идёт» является пульсация, а не тон: ничто другое в библиотеке не пульсирует. Если запульсирует что-то ещё, совмещение придётся разводить.
Правила оформления
Табличные цифры по умолчанию. В инструментальном интерфейсе почти любое
число либо стоит в колонке, либо обновляется на месте, и пропорциональные цифры
дёргают оба случая. Проза отключает это через .inst-prose.
База документа — интерфейс, а не проза: 14/1.4. Иначе каждый компонент обязан переобъявлять размер.
Вложенные радиусы уменьшаются. Внутренний элемент с тем же радиусом, что у контейнера, выглядит выпирающим.
Радиусы чётные. Радиус умножается на плотность пикселей, и на плотности 1.5 нечётный даёт половину устройственного пикселя (7 × 1.5 = 10.5): дуга упирается в прямую грань мимо сетки, и сглаживание ложится по-своему на каждом углу. Чётные 4 · 6 · 8 · 12 ложатся целыми на 1.0, 1.5 и 2.0.
Отступ контрола выводится из его высоты. Высота и отступ, набранные как две
независимые лестницы, расходятся, и кнопка меняет форму вместе с плотностью.
Пропорция объявлена один раз (--control-ratio-*), отступ считается из высоты.
Вертикаль строки и вертикаль блока — разные роли. --row-pad-y для строк,
--pad-block-y для шапок, тулбаров, баннеров и текстовых блоков. По вертикали
теснее, чем по горизонтали: часть вертикального воздуха приносит интерлиньяж.
Нажатие — scale(0.985).
Фокус с отступом 1px и без собственного радиуса. Кольцо впритык к контролу читается как изменение рамки. Обводка и так следует радиусу элемента, а заданный радиус меняет форму самого элемента.
Заливка сплошной кнопки при наведении уходит от цвета подписи. Подпись белая — значит, в обеих темах вниз: зеркальное «в тёмной светлее» роняет контраст подписи ниже порога.
Поле стоит во врезе, а не на цвете панели. Иначе его единственной границей остаётся рамка. Врез лежит на том же шаге, что приглушённая поверхность, во всех пяти темах; поле от углубления отличает несущая рамка, а не светлота.
Одна раскладка подписи на группу, а не на поле. Подпись сбоку уместна там,
где значения однотипны и их сканируют не читая, но применяется это к форме или
fieldset: иначе в одной форме появляется два разных левых края. Раскладку
объявляет контейнер (inst-form--side, inst-fieldset--side), поля её
наследуют.
Подсказка и ошибка в одном слоте. Ошибка заменяет подсказку, а не сдвигает форму.
Дорожка слайдера заполнена до бегунка. Положение точки на ровной полосе
оценивают глазомером, долю читают формой. Долю несёт --fill — канал данных,
как inline-size у меры.
У тени два смысла, и они не смешиваются. «Плавает сверху и сейчас исчезнет» — большой расплыв и заметное смещение, положено поповеру и модалке. «Это физическая поверхность» — расплыв в один пиксель, смещения почти нет, положено приподнятой кнопке. Проверка одна: тень отрывает слой или утолщает его? Отрывает — оверлей, и таких ровно два. Утолщает — контрол. Карточка и панель не получают ни того, ни другого.
У кнопки четыре веса, и каждый занят одним ответом на вопрос, насколько громко кнопка просит нажать себя:
| вес | вид | где стоит |
|---|---|---|
| сплошная | заливка акцентом | одно действие на экран |
| приподнятая (умолчание) | заливка, волосяная рамка, тень в пиксель | действие, стоящее само по себе |
| мягкая | приглушённая заливка, рамки нет | контрол внутри группы: тулбар, ряд значков, чип |
| призрачная | ничего до наведения | действие, которое ищут глазами |
Обведённого веса нет: в ряду из пяти контуры складываются в решётку, и группа читается как таблица. Заливка без контура собирает её в один объект, а выбранный элемент внутри отличается плотностью заливки.
Опасность — тон, а не пятый вес. --danger ставится поверх любого из
четырёх и красит подпись, не добавляя рамки. Сплошного красного нет: заливка
под белой подписью принадлежит акценту, а второй сплошной цвет означал бы
второе главное действие на экране.
Значок в кнопке тише подписи. Исключений два: на сплошной заливке гасить нечего (подпись белая), а у иконочной кнопки значок и есть подпись.
Иконочная кнопка квадратная.
Единица измерения мельче и тише числа. «42 с» одним кеглем читается как одно слово; при разделении размера и цвета глаз хватает число, а «с» уходит в фон.
Направление и полярность — два разных атрибута. Стрелка вниз у времени прогона — это хорошо, стрелка вверх у предупреждений — плохо. Класс, названный стрелкой и означающий оценку, приводит к ошибкам применения.
Ряд метрик без рамок. Четыре числа — это одна группа, и контуры вокруг каждого складываются в решётку. Заливка разрешена: у неё нет линии, соседние плитки разделяет воздух. Общая заливка на весь ряд, наоборот, делает его бесшовной полосой, в которой числа перестают читаться отдельными величинами.
Завершённые задачи отступают, а не гаснут. История остаётся читаемой. То же делает отвеченный запрос подтверждения.
Неопределённый прогресс — отдельный вид полосы. У неопределённой нет
aria-valuenow, и это само по себе сообщение.
Свёрнутый вывод называет своё число словами.
Ширины колонок лога заданы в ch. Сетка объявлена на строке, значит, у
каждой строки она своя, и колонка уровня иначе меняет ширину от слова к слову.
Отключённое состояние — прозрачность, а не серый. Видно, какое именно действие недоступно.
Reduced-motion сжимает переходы и замедляет индикаторы активности, а не гасит их. Анимация, сжатая до 0.01ms, — это остановка, а остановленный индикатор сообщает, что работа встала.
Разметочный контракт
Стили завязаны на ARIA: состояние живёт в одном месте. Компонент без своей разметки считается неприменённым.
| Компонент | Обязательная разметка |
|---|---|
.inst-segmented |
role="radiogroup" + role="radio" + aria-checked + бегущий tabindex |
.inst-tabs |
role="tablist" + role="tab" + aria-selected + бегущий tabindex; у вкладки с панелью — aria-controls |
.inst-task |
контейнер role="listbox", строка role="option" + aria-selected + бегущий tabindex |
.inst-tree-item |
контейнер role="tree", узел role="treeitem" + aria-level + aria-expanded + бегущий tabindex |
.inst-meter |
role="progressbar" + aria-valuenow/min/max; у неопределённой aria-valuenow нет |
.inst-step |
<details> / <summary> — раскрытие и клавиатура достаются от платформы |
.inst-select |
обёртка .inst-select-wrap (в ней живёт шеврон) |
.inst-num-field |
aria-label с осью на input; .inst-num-axis не является подписью |
.inst-prop-label |
title с полным текстом, потому что подпись обрезается |
.inst-failure |
role="alert" и хотя бы одно действие: без выхода это не блок отказа |
.inst-popover |
popover на блоке + popovertarget на кнопке. Кнопка становится неявным якорем — имена якорей заводить не нужно |
.inst-menu |
role="menu" + role="menuitem". Стрелки и бегущий tabindex выполняет instrument.js |
.inst-tooltip-text |
role="tooltip" + id, триггер несёт aria-describedby. Внутри панели с overflow: hidden обрежется — там нужен .inst-popover |
.inst-dialog |
нативный <dialog>; открытие showModal(), закрытие — кнопки внутри <form method="dialog"> |
.inst-accordion-item |
<details>; общий name делает группу взаимоисключающей без JS |
| любая кнопка | type="button", иначе внутри формы она её отправит |
| занятая кнопка | aria-busy="true". Подпись остаётся в разметке: она держит ширину и озвучивается. disabled не ставится — он выбрасывает кнопку из порядка обхода. Мышь снимается через pointer-events, защиту от повторного нажатия ставит обработчик |
Стрелки, Home и End выполняет
instrument.js. Роль обещает клавиатуру, значит
клавиатура должна работать; отдельный файл — чтобы CSS оставался пригодным без
скрипта.
Начальный бегущий tabindex пишет автор — везде, где группа видна сразу:
сегментированный контрол, очередь, дерево, вкладки. Скрипт его потом
переставляет, но до его приезда группа без tabindex — это не «пока без
клавиатуры», а обход, проходящий по каждому пункту подряд. Исключение одно и
названо в таблице: у меню пункты лежат в закрытом поповере и в обход не
попадают, пока его не открыли, — там начальный tabindex ставит скрипт.
Кто чем владеет, записано в components.json полем
roving, и сверяет это cmd/registry по живым примерам страниц.
Состояние и вариант
- Вариант — то, что выбирает автор разметки: модификатор
--(.inst-btn--primary,.inst-panel-body--flush). - Состояние — то, что меняется в течение жизни: атрибут.
Атрибутных словарей четыре, и все они закрыты. Класс вида .is-* для состояния
запрещён. Новое значение заводится вместе со строкой в таблице ниже, иначе оно
молча не сработает.
Проверка, отличающая ось от дубликата: если два атрибута дают один и тот же цвет, это один атрибут с двумя именами.
1. data-tone — смысловой регистр. Один словарь на всю библиотеку.
neutral · running · ok · warn · error
Значит одно и то же везде: на бейдже, сноске, баннере, точке, строке лога,
дельте метрики. Ставит --tone-ink, --tone-mark, --tone-bg; компонент их
читает и больше ничего о тоне не знает.
2. data-state — фаза жизни. Словарь свой у каждого компонента.
У строки очереди и у шага мастера разные фазы. Каждый набор перечислен здесь целиком, включая базовое значение — то, у которого нет своего оформления.
| Компонент | Словарь | База |
|---|---|---|
.inst-task |
queued running done warn failed skipped |
queued |
.inst-step |
running ok failed |
атрибута нет |
.inst-approval |
pending approved denied |
pending — единственная, где есть действия |
.inst-failure |
pending resolved given-up |
pending — единственная, где есть действия |
.inst-stepper-item |
todo current done |
todo |
.inst-meter |
indeterminate |
атрибута нет = определённая |
Базовое значение пишется в разметке, хотя правил под него нет:
data-state="queued" читается, а его отсутствие — нет, и опечатка вроде
quued выглядит как база.
3. data-kind — категория, а не состояние. Диф: add · del. Добавленная
строка не находится в состоянии «ok» — она относится к виду «добавление». Один и
тот же data-kind несут и строки дифа, и числа в его шапке.
4. data-when — кто управляет показом.
.inst-field-error[data-when="invalid"] показывается платформой по
:user-invalid; тот же блок без атрибута показывает приложение, например по
ответу сервера.
5. data-rank — сколько внимания просит это место. Один словарь на всю
библиотеку.
lead · default · support
Ставится на ОБЛАСТЬ и отвечает на вопрос об ЭКРАНЕ, а не о компоненте: одна и та же панель — суть одного экрана и сноска на другом. Поэтому это атрибут, а не модификатор: модификатор сказал бы «бывает особый вид панели», а его не бывает.
У кита эта ось была у кнопки и больше ни у чего. У кнопки четыре веса, и каждый — ответ на «насколько громко это просит нажатия»; у панели был один вид и ни одного вопроса, так что две панели совершенно разной важности выходили с одинаковыми вычисленными стилями, и никакая разметка не могла их различить.
| что меняет | |
|---|---|
lead |
имя области поднимается на ступень шкалы кегля. Больше ничего |
default |
ровно сегодняшний вид. Отсутствие атрибута разрешается в него |
support |
ступень вниз, тон текста вниз, одна плёнка утопления и никакой рамки |
Четыре закона — они и не дают оси превратиться в общий регулятор яркости.
- Направление задаёт вложенность. Область внутри области утопает всегда и не спрашивая ранга. См. высоту.
- Ранг задаёт присутствие и может сдвинуть фон не более чем на одну ступень
утопления и только вниз. Ранг никогда не поднимает:
--surface-raised— потолок шкалы во всех светлых темах, так что поднимающийсяleadработал бы в двух темах из пяти и молча ничего не делал в трёх. - Ранг трогает оформление области, но не её данные. Заголовок, рамку, фон. Не текст тела, не числа, не строки.
- Ранг не наследуется. Область внутри ведущей области, если у неё нет
собственного ранга, —
default. Ведущая область ведёт экран, а не своё содержимое.
Где он допустим. .inst-panel, .inst-card, .inst-section — и больше
нигде. В остальных местах он не запрещён, а бездействует, и это обычный режим
отказа любого закрытого словаря здесь, поэтому список сторожится с обеих сторон:
cmd/registry сверяет его с полем region в
components.json.
Примитив раскладки ранга не берёт: примитив не рисует, а рисующий нельзя было бы вложить во что-то, что рисует иначе. Ранг берёт панель внутри половины сплита, а не сама половина.
Кнопка, бейдж, тег и поле ранга тоже не берут. У них есть акцент — это другой вопрос со своим ответом, а две оси на один вопрос суть одна ось с двумя именами.
Ранг — не тон, и они складываются. Ведущая область — главная, а не хорошая:
ранг говорит, куда смотреть, тон — что это значит, и ни один не подразумевает
другого. data-rank="lead" data-tone="error" — область, которой принадлежит
экран и которая при этом в ошибке. Тон области идёт на ФОН, но не на рамку, как
у баннера, и на фоне тон старше ранга: тихая область в ошибке — это ошибка,
нарисованная тихо. См. ранг.
На экране не больше одной ведущей области. Две ведущих — ни одной. Это
проверяется на отрисованном дереве через kitAudit.composition(), а не текстовым
гейтом: это факт о странице, а не о токене.
Занятость — на aria-busy, а не в этих словарях. У кнопки нет фазы жизни,
есть переходный флаг.
Слои и адаптивность
Порядок слоёв объявлен явно и потому не зависит от порядка импортов; стили приложения лежат вне слоёв и всегда выигрывают. Полный список и объяснение — в токенах.
Адаптивность
Три уровня, порядок между ними обязателен.
- Интринсик. Раскладка перестраивается сама, без единого запроса:
auto-fitу сетки,flex-wrapс порогом у сплита. Работает всегда, в том числе там, где контейнера-предка нет. Пробуется первым. @container. Компонент отвечает на ширину своей области, а не окна: одна и та же панель стоит и в узкой колонке инспектора, и во всю ширину дашборда. Контейнерами объявлены.inst-panel,.inst-containerи.inst-shell-main— области, ширина которых приходит снаружи. Объявлять контейнером то, что получает ширину от содержимого, нельзя:container-typeотключает интринсик по инлайновой оси и схлопнет элемент.@media. Один порог на всю библиотеку — 60rem у оболочки приложения.
Отступы задаются примитивами
Утилит вида mt-3, p-2, pt-0d25 нет. Шкала разрежена сверху, чтобы «чуть
побольше» не было доступным решением; набор утилит отступов вернул бы его
первым классом и перенёс решение о ритме в разметку каждого экрана.
Вместо них — три примитива потока (.inst-stack, .inst-cluster,
.inst-grid) с тремя шагами зазора, названными намерением, а не числом:
обычный, --tight, --loose. Плотность перенастраивает все три разом.
Темы, масштаб и плотность
<html data-theme="dark"> <!-- принудительно -->
<html> <!-- по системной настройке -->
<html data-accent="clay"> <!-- акцент: ортогонален теме -->
<html data-scale="16"> <!-- масштаб: базовый кегль интерфейса -->
<section data-density="compact"> <!-- плотность как атрибут контейнера -->
Четыре ручки, и они не пересекаются: 5 тем × 4 акцента × 5 масштабов × 3
плотности — 300 сочетаний. Умолчание нейтрали — --tint: 0, чистый серый;
уклон есть у именованных светлых тем light и light-cool, где множитель
поднят до 2.
Переключение темы меняет color-scheme, а light-dark() в семантике начинает
отдавать вторую ветку. Второго блока токенов не существует.
Масштаб и плотность — разные оси, и различает их кегль. Плотность двигает
геометрию и текст не трогает: во всех трёх режимах он остаётся 14px. Масштаб
двигает кегль вместе с геометрией, сохраняя рисунок. Проверка та же, что у
атрибутных словарей: 18 + compact даёт высоту контрола 34px при кегле 18, а
умолчание + comfortable — 36px при кегле 14. Высоты почти совпали, экраны
разные.
Масштаб первичен. При одиночном совпадении токен забирает он: блоки
[data-scale] стоят после блоков плотности при равной специфичности.
Плотность возвращает своё в двумерной ячейке.
Ступень названа своим базовым кеглем, и ниже 14 лестница не идёт. Это арифметика двух правил, перемноженных друг на друга: пол кегля 11px, минимальный шаг лестницы 1.12, под базой две ступени — значит минимальная база равна 11 × 1.12² = 13.8px. «Мельче» остаётся работой плотности.
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument