{ "index": 203, "slug": "editorial-2022-05-mechanism-design-system", "title": "Контракт кнопки: как связать токены, состояния и доступную семантику", "excerpt": "Разбор малого component contract: какие данные принадлежат токенам, состояниям, семантике и usage inventory, а какие нельзя подменять модельным visual payload.", "contentHtml": "

Кнопка начинает расходиться не потому, что в ней много CSS. Обычно один код владеет className, другой — disabled, третий — текстом, а четвёртый копирует цвет. Симптом проявляется после безопасной на вид правки: новая loading-версия смотрится правильно, но action уже доступен для повторного запуска; focus ring пропадает в одном варианте; иконка получает label только в profile. Цена — review видит фрагменты, а пользователь получает разный contract для одного знакомого действия.

\n

Минимальный component contract собирает эти фрагменты в данные: token names отвечают за значения, required states — за допустимые ветки, semantic fields — за смысл control, usage inventory — за известный радиус изменения. Это не универсальная система и не готовая React API. В учебном скрипте нет JSX, DOM, CSS cascade и assistive technology. Он лишь показывает, как проверить, что один договор не пропустил обязательную часть до того, как команда начнёт спорить о структуре библиотек.

\n

Четыре владельца вместо одного большого объекта

\n

У contract есть четыре слоя. Первый — token layer: он знает только именованные значения и не должен решать, в каком состоянии находится кнопка. Второй — state layer: default, hover, focus-visible, disabled и loading; он определяет, какие ветки продукт обязан обсудить. Третий — semantic layer: name, role, state и disabled. Он не выводится из цвета, потому что одинаковый серый может означать disabled, loading или ошибочно применённый style. Четвёртый — inventory: он хранит известные точки применения и не выдаёт себя за поиск по всему репозиторию.

\n
Границы малого component contract
СлойВладеетНе доказываетНужная внешняя проверка
Tokensимена и значения background, foreground, focus ring, radius, gapчто все pixels в браузере обновилисьсобранный CSS и visual diff в выбранной среде
Statesразрешённые default/hover/focus-visible/disabled/loadingчто browser реально получил hover или focusручной keyboard/mouse scenario или автоматизация
Semanticsdeclared name, role button, declared stateчто screen reader произнёс ожидаемую фразупроверка DOM/accessibility tree и выбранной технологии
Inventoryтри явно перечисленных usageчто больше usage не существуетпоиск в кодовой базе и review migration
Visual payloadviewports, states и token names для будущего снимкареальный screenshot, diff, score или regressionнастоящий visual-regression runner с сохранённым артефактом
\n

Почему CSS variable не является semantic token автоматически

\n

Спецификация CSS Custom Properties говорит о custom properties и подстановке var(). Она не назначает им смысл. Поэтому --button-primary-background может быть технически валиден, но неясен как договор, если никто не определил, для какой роли он существует, кто меняет его значение и какие variants от него зависят. Обратная ошибка тоже частая: назвать произвольное значение «semantic» и считать, что оно должно жить в глобальном файле. Семантика появляется не в пунктуации имени, а в повторяемом решении и известном owner.

\n

Для маленькой системы полезен направленный путь: button.primary.background → primary button → конкретные usage. Он позволяет отдельно решить, как component получает CSS variable. В одном коде это может быть custom property, в другом объект theme, в третьем stylesheet. Contract не требует выбрать транспорт заранее. Он требует не потерять связь между ролью значения и точками, на которые повлияет изменение. Такая граница оставляет миграцию обратимой.

\n

Name, role и state — не оформление

\n

WAI-ARIA 1.2 описывает роли, состояния и свойства для интерфейсных объектов. Для простой кнопки лучший путь обычно начинается с native button: браузер уже знает базовую keyboard-модель. Если вместо него нужен custom host, команда обязана явно воспроизвести поведение, а не только написать role=\"button\". Однако даже native host не решает вопрос имени: «Сохранить изменения» и иконка без text alternative не равны для человека, который не видит layout.

\n

В модели semantic contract намеренно мал: name, role, state, disabled и список permitted states. Поле state — declared data, не прочитанное browser tree. Такое различие нужно сохранить в тексте review: fixture может показать, что state loading предусмотрен в contract, но не может сказать, что конкретный browser запретил повторный click или что screen reader сообщил disabled. Для этого понадобятся платформенные тесты, которых здесь нет.

\n

Исполнимый пример: проверить contract без UI

\n

Функция checkContract сопоставляет required states с permitted states, проверяет, что payload не ссылается на отсутствующий token, и что каждое известное usage имеет name и роль button. Она не ищет реальные файлы и не запускает построение styles. Именно поэтому результат true означает «данные учебной модели согласованы», а не «кнопка доступна и визуально одинакова». Это узкое утверждение легко повторить и трудно неверно истолковать.

\n
const system = {\n  tokens: {\n    'button.primary.background': '#2457D6',\n    'button.primary.foreground': '#FFFFFF',\n    'button.primary.focus-ring': '#F0B429',\n    'button.primary.radius': '8px'\n  },\n  requiredStates: ['default', 'hover', 'focus-visible', 'disabled', 'loading'],\n  contract: {\n    name: 'Сохранить изменения',\n    role: 'button',\n    state: 'default',\n    permittedStates: ['default', 'hover', 'focus-visible', 'disabled', 'loading']\n  },\n  usage: [\n    { id: 'profile-save', name: 'Сохранить изменения', role: 'button' },\n    { id: 'billing-pay', name: 'Оплатить счёт', role: 'button' },\n    { id: 'dialog-cancel', name: 'Отмена', role: 'button' }\n  ],\n  visualPayload: {\n    tokenNames: [\n      'button.primary.background', 'button.primary.foreground',\n      'button.primary.focus-ring', 'button.primary.radius'\n    ],\n    states: ['default', 'hover', 'focus-visible', 'disabled', 'loading'],\n    viewports: [375, 1280]\n  }\n};\n\nfunction checkContract(value) {\n  const missingTokens = value.visualPayload.tokenNames\n    .filter((name) => !(name in value.tokens));\n  const missingStates = value.requiredStates\n    .filter((state) => !value.contract.permittedStates.includes(state));\n  const invalidUsage = value.usage\n    .filter((item) => !item.name || item.role !== 'button');\n  return {\n    ok: missingTokens.length === 0 && missingStates.length === 0 && invalidUsage.length === 0,\n    missingTokens, missingStates, invalidUsage\n  };\n}\n\nconsole.log(checkContract(system)); // { ok: true, ... }\nconst broken = {\n  ...system,\n  visualPayload: {\n    ...system.visualPayload,\n    tokenNames: [...system.visualPayload.tokenNames, 'button.primary.shadow']\n  }\n};\nconsole.log(checkContract(broken).ok); // false
\n
\"Схема
Компонент получает несколько независимых видов данных. Схема показывает границы ответственности, чтобы token, состояние и семантика не превращались в один неразличимый объект.
\n

Маршрут: симптом → причина → проверка → действие

\n
  1. Симптом. Запишите один разрыв: кнопка теряет focus, loading не блокирует повтор, label отличается в одинаковом действии или literal color появился в новом usage.
  2. Причина. Разложите изменение по четырём владельцам. Если token пытается хранить state, а CSS class несёт name, граница уже размыта.
  3. Проверка contract. Сверьте required states, token names, semantic fields и inventory. Запустите фрагмент; он должен вывести true для исправной модели и false для unknown token.
  4. Проверка платформы. Отдельно создайте реальный control и пройдите согласованный keyboard/mouse/a11y сценарий. Результат сохраните как новый артефакт, не внутри model.
  5. Действие. Сначала поменяйте один owner: вынесите literal в named token, добавьте state или верните native host. Не объединяйте это с переписыванием всей библиотеки.
  6. Откат. Применяйте token correction с сохранённым previous value. Если inventory показывает неожиданный effect, откатите малый change и сузьте variant.
\n

Против ложной универсальности

\n

Слово «Button» не делает все действия одним компонентом. Link-like navigation, destructive confirmation, toggle, split button и async submit имеют разные риски. У них могут совпадать radius и gap, но не обязательно name, behavior или state matrix. Универсальный API, который принимает двадцать optional props ради такого сходства, обычно скрывает больше решений, чем экономит. Малый contract ценнее, когда он допускает честный ответ: этот control пока не входит в primary button.

\n

Не стоит и использовать fixture как gate для чужого продукта. Её inventory полностью создан внутри примера, а values выбраны для объяснения. В настоящем проекте сначала нужно получить существующие usage и владельца правила. Затем выбрать, какие values public, какие variants поддерживаются, как маркируется deprecation и кто проводит visual review. Это следующий слой T-shape автора 2022 года: не говорить за процесс, которого не наблюдали, а назвать факт, которым можно проверить изменение.

\n

Ограничение и следующий проверяемый шаг

\n

Модель не меряет contrast, не запускает CSS, не сравнивает image pixels, не читает accessibility tree и не знает всех компонентов в архиве. WCAG 2.1 не разрешает заменить сочетание автоматической и ручной оценки полем role в JavaScript. Поэтому в материале нет claim о доступности или visual stability. Здесь есть только контракт данных, который уменьшает шанс забыть state или нечаянно изменить token без пути назад.

\n

Следующий проверяемый шаг — выбрать один production-like component в отдельном репозитории, составить его реальный inventory и записать маленький test plan: browser/version, viewport, states, keyboard route и ожидаемый результат. После первого наблюдения можно привязать к contract настоящий screenshot или accessibility-tree artifact. До этого visual payload должен оставаться честным списком входов, а не «зелёным» score.

\n

Историческая граница мая 2022

\n

Здесь использованы датированные версии, существовавшие до мая 2022 года: CSS Custom Properties CR Draft 11.11.2021 и WAI-ARIA 1.2 CR Draft 08.12.2021; WCAG 2.1 Recommendation опубликована в 2018-м. Фразы о native button и roles относятся к нормативным моделям документов. Слои contract, token names и fixture — решения этого учебного пакета, не цитата из существующей библиотеки.

\n

Проверяемые источники

\n" }