Files

8 lines
18 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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) =&gt; !(name in value.tokens));\n const missingStates = value.requiredStates\n .filter((state) =&gt; !value.contract.permittedStates.includes(state));\n const invalidUsage = value.usage\n .filter((item) =&gt; !item.name || item.role !== 'button');\n return {\n ok: missingTokens.length === 0 &amp;&amp; missingStates.length === 0 &amp;&amp; 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>"
}