Перейти к содержимому
EN
Масштаб
Плотность
Тема
Акцент

О проекте

Принципы дизайна

Правила, из которых выводятся компоненты 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, а не в этих словарях. У кнопки нет фазы жизни, есть переходный флаг.


Слои и адаптивность

Порядок слоёв объявлен явно и потому не зависит от порядка импортов; стили приложения лежат вне слоёв и всегда выигрывают. Полный список и объяснение — в токенах.

Адаптивность

Три уровня, порядок между ними обязателен.

  1. Интринсик. Раскладка перестраивается сама, без единого запроса: auto-fit у сетки, flex-wrap с порогом у сплита. Работает всегда, в том числе там, где контейнера-предка нет. Пробуется первым.
  2. @container. Компонент отвечает на ширину своей области, а не окна: одна и та же панель стоит и в узкой колонке инспектора, и во всю ширину дашборда. Контейнерами объявлены .inst-panel, .inst-container и .inst-shell-main — области, ширина которых приходит снаружи. Объявлять контейнером то, что получает ширину от содержимого, нельзя: container-type отключает интринсик по инлайновой оси и схлопнет элемент.
  3. @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