8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 203,
|
||
"slug": "editorial-2022-05-mechanism-design-system",
|
||
"title": "Контракт кнопки: как связать токены, состояния и доступную семантику",
|
||
"excerpt": "Разбор малого component contract: какие данные принадлежат токенам, состояниям, семантике и usage inventory, а какие нельзя подменять модельным visual payload.",
|
||
"contentHtml": "<p>Кнопка начинает расходиться не потому, что в ней много CSS. Обычно один код владеет className, другой — disabled, третий — текстом, а четвёртый копирует цвет. Симптом проявляется после безопасной на вид правки: новая loading-версия смотрится правильно, но action уже доступен для повторного запуска; focus ring пропадает в одном варианте; иконка получает label только в profile. Цена — review видит фрагменты, а пользователь получает разный contract для одного знакомого действия.</p>\n<p>Минимальный component contract собирает эти фрагменты в данные: token names отвечают за значения, required states — за допустимые ветки, semantic fields — за смысл control, usage inventory — за известный радиус изменения. Это не универсальная система и не готовая React API. В учебном скрипте нет JSX, DOM, CSS cascade и assistive technology. Он лишь показывает, как проверить, что один договор не пропустил обязательную часть до того, как команда начнёт спорить о структуре библиотек.</p>\n<h2>Четыре владельца вместо одного большого объекта</h2>\n<p>У contract есть четыре слоя. Первый — token layer: он знает только именованные значения и не должен решать, в каком состоянии находится кнопка. Второй — state layer: default, hover, focus-visible, disabled и loading; он определяет, какие ветки продукт обязан обсудить. Третий — semantic layer: name, role, state и disabled. Он не выводится из цвета, потому что одинаковый серый может означать disabled, loading или ошибочно применённый style. Четвёртый — inventory: он хранит известные точки применения и не выдаёт себя за поиск по всему репозиторию.</p>\n<div class=\"table-scroll\"><table><caption>Границы малого component contract</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Владеет</th><th scope=\"col\">Не доказывает</th><th scope=\"col\">Нужная внешняя проверка</th></tr></thead><tbody><tr><td>Tokens</td><td>имена и значения background, foreground, focus ring, radius, gap</td><td>что все pixels в браузере обновились</td><td>собранный CSS и visual diff в выбранной среде</td></tr><tr><td>States</td><td>разрешённые default/hover/focus-visible/disabled/loading</td><td>что browser реально получил hover или focus</td><td>ручной keyboard/mouse scenario или автоматизация</td></tr><tr><td>Semantics</td><td>declared name, role button, declared state</td><td>что screen reader произнёс ожидаемую фразу</td><td>проверка DOM/accessibility tree и выбранной технологии</td></tr><tr><td>Inventory</td><td>три явно перечисленных usage</td><td>что больше usage не существует</td><td>поиск в кодовой базе и review migration</td></tr><tr><td>Visual payload</td><td>viewports, states и token names для будущего снимка</td><td>реальный screenshot, diff, score или regression</td><td>настоящий visual-regression runner с сохранённым артефактом</td></tr></tbody></table></div>\n<h2>Почему CSS variable не является semantic token автоматически</h2>\n<p>Спецификация CSS Custom Properties говорит о custom properties и подстановке <code>var()</code>. Она не назначает им смысл. Поэтому <code>--button-primary-background</code> может быть технически валиден, но неясен как договор, если никто не определил, для какой роли он существует, кто меняет его значение и какие variants от него зависят. Обратная ошибка тоже частая: назвать произвольное значение «semantic» и считать, что оно должно жить в глобальном файле. Семантика появляется не в пунктуации имени, а в повторяемом решении и известном owner.</p>\n<p>Для маленькой системы полезен направленный путь: <code>button.primary.background</code> → primary button → конкретные usage. Он позволяет отдельно решить, как component получает CSS variable. В одном коде это может быть custom property, в другом объект theme, в третьем stylesheet. Contract не требует выбрать транспорт заранее. Он требует не потерять связь между ролью значения и точками, на которые повлияет изменение. Такая граница оставляет миграцию обратимой.</p>\n<h2>Name, role и state — не оформление</h2>\n<p>WAI-ARIA 1.2 описывает роли, состояния и свойства для интерфейсных объектов. Для простой кнопки лучший путь обычно начинается с native <code>button</code>: браузер уже знает базовую keyboard-модель. Если вместо него нужен custom host, команда обязана явно воспроизвести поведение, а не только написать <code>role=\"button\"</code>. Однако даже native host не решает вопрос имени: «Сохранить изменения» и иконка без text alternative не равны для человека, который не видит layout.</p>\n<p>В модели semantic contract намеренно мал: <code>name</code>, <code>role</code>, <code>state</code>, <code>disabled</code> и список permitted states. Поле <code>state</code> — declared data, не прочитанное browser tree. Такое различие нужно сохранить в тексте review: fixture может показать, что state loading предусмотрен в contract, но не может сказать, что конкретный browser запретил повторный click или что screen reader сообщил disabled. Для этого понадобятся платформенные тесты, которых здесь нет.</p>\n<h2>Исполнимый пример: проверить contract без UI</h2>\n<p>Функция <code>checkContract</code> сопоставляет required states с permitted states, проверяет, что payload не ссылается на отсутствующий token, и что каждое известное usage имеет name и роль button. Она не ищет реальные файлы и не запускает построение styles. Именно поэтому результат <code>true</code> означает «данные учебной модели согласованы», а не «кнопка доступна и визуально одинакова». Это узкое утверждение легко повторить и трудно неверно истолковать.</p>\n<pre><code>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</code></pre>\n<figure><img src=\"/assets/editorial/2022/design-system-component-contract-2022.svg\" alt=\"Схема component contract primary button: слева перечислены named tokens, сверху обязательные states, справа semantic fields name/role/state, снизу usage inventory. Пунктирная рамка visual payload показывает viewports и snapshots как объявленные входы, а не проверенный результат.\" loading=\"lazy\" /><figcaption>Компонент получает несколько независимых видов данных. Схема показывает границы ответственности, чтобы token, состояние и семантика не превращались в один неразличимый объект.</figcaption></figure>\n<h2>Маршрут: симптом → причина → проверка → действие</h2>\n<ol><li><strong>Симптом.</strong> Запишите один разрыв: кнопка теряет focus, loading не блокирует повтор, label отличается в одинаковом действии или literal color появился в новом usage.</li><li><strong>Причина.</strong> Разложите изменение по четырём владельцам. Если token пытается хранить state, а CSS class несёт name, граница уже размыта.</li><li><strong>Проверка contract.</strong> Сверьте required states, token names, semantic fields и inventory. Запустите фрагмент; он должен вывести <code>true</code> для исправной модели и <code>false</code> для unknown token.</li><li><strong>Проверка платформы.</strong> Отдельно создайте реальный control и пройдите согласованный keyboard/mouse/a11y сценарий. Результат сохраните как новый артефакт, не внутри model.</li><li><strong>Действие.</strong> Сначала поменяйте один owner: вынесите literal в named token, добавьте state или верните native host. Не объединяйте это с переписыванием всей библиотеки.</li><li><strong>Откат.</strong> Применяйте token correction с сохранённым previous value. Если inventory показывает неожиданный effect, откатите малый change и сузьте variant.</li></ol>\n<h2>Против ложной универсальности</h2>\n<p>Слово «Button» не делает все действия одним компонентом. Link-like navigation, destructive confirmation, toggle, split button и async submit имеют разные риски. У них могут совпадать radius и gap, но не обязательно name, behavior или state matrix. Универсальный API, который принимает двадцать optional props ради такого сходства, обычно скрывает больше решений, чем экономит. Малый contract ценнее, когда он допускает честный ответ: этот control пока не входит в primary button.</p>\n<p>Не стоит и использовать fixture как gate для чужого продукта. Её inventory полностью создан внутри примера, а values выбраны для объяснения. В настоящем проекте сначала нужно получить существующие usage и владельца правила. Затем выбрать, какие values public, какие variants поддерживаются, как маркируется deprecation и кто проводит visual review. Это следующий слой T-shape автора 2022 года: не говорить за процесс, которого не наблюдали, а назвать факт, которым можно проверить изменение.</p>\n<h2>Ограничение и следующий проверяемый шаг</h2>\n<p>Модель не меряет contrast, не запускает CSS, не сравнивает image pixels, не читает accessibility tree и не знает всех компонентов в архиве. WCAG 2.1 не разрешает заменить сочетание автоматической и ручной оценки полем <code>role</code> в JavaScript. Поэтому в материале нет claim о доступности или visual stability. Здесь есть только контракт данных, который уменьшает шанс забыть state или нечаянно изменить token без пути назад.</p>\n<p>Следующий проверяемый шаг — выбрать один production-like component в отдельном репозитории, составить его реальный inventory и записать маленький test plan: browser/version, viewport, states, keyboard route и ожидаемый результат. После первого наблюдения можно привязать к contract настоящий screenshot или accessibility-tree artifact. До этого visual payload должен оставаться честным списком входов, а не «зелёным» score.</p>\n<h2>Историческая граница мая 2022</h2>\n<p>Здесь использованы датированные версии, существовавшие до мая 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 — решения этого учебного пакета, не цитата из существующей библиотеки.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.w3.org/TR/2021/CRD-css-variables-1-20211111/\" target=\"_blank\" rel=\"noopener noreferrer\">CSS Custom Properties for Cascading Variables Module Level 1, Candidate Recommendation Draft от 11 ноября 2021 года</a> — датированный нормативный снимок: custom properties имеют имена --* и подставляются через var(). Он не определяет taxonomy дизайн-токенов и не подтверждает результат visual regression.</li><li><a href=\"https://www.w3.org/TR/2021/CRD-wai-aria-1.2-20211208/\" target=\"_blank\" rel=\"noopener noreferrer\">WAI-ARIA 1.2, Candidate Recommendation Draft от 8 декабря 2021 года</a> — датированный нормативный снимок, доступный в мае 2022 года. Он описывает роли и состояния, но не выбирает за продукт токены, текст кнопки или набор variants.</li><li><a href=\"https://www.w3.org/TR/2018/REC-WCAG21-20180605/\" target=\"_blank\" rel=\"noopener noreferrer\">Web Content Accessibility Guidelines 2.1, Recommendation от 5 июня 2018 года</a> — неизменяемая W3C Recommendation с проверяемыми критериями, включая Keyboard, Focus Visible, Name/Role/Value и Non-text Contrast. Fixture пакета их не измеряет.</li><li><a href='https://www.w3.org/TR/2017/REC-html52-20171214/' target='_blank' rel='noopener noreferrer'>HTML 5.2, W3C Recommendation от 14 декабря 2017 года</a> — исторический нормативный источник по семантике HTML и native button. Он не описывает token taxonomy или правила конкретного component contract.</li></ul>"
|
||
}
|