Files
progcode/editorial/agent-rewrites/204.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
17 KiB
JSON
Raw 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": 204,
"slug": "editorial-2022-05-practice-design-system",
"title": "Маленькая дизайн-система: как закрепить контракт кнопки",
"excerpt": "Кнопки расходятся по цвету, фокусу и поведению, когда их контракт разбросан по разным слоям. Разбираем малую схему с токенами, состояниями, проверками и обратимой правкой.",
"contentHtml": "<p>Проблема: на одном экране primary button получает цвет из локального CSS, на другом теряет видимый фокус, а на третьем остаётся доступной во время загрузки; пользователь видит непредсказуемое действие, а команда платит повторной правкой и риском сломать соседний сценарий.</p>\n<p>Причина обычно не в том, что в проекте мало компонентов. У одной кнопки нет общего проверяемого контракта. Цвет живёт в стилях, состояние — в обработчике, текст — в разметке, а список мест использования никто не держит. Поэтому безопасная на вид замена цвета может затронуть оплату, профиль и диалог одновременно.</p>\n<p>Начинайте с одной повторяющейся primary button. Зафиксируйте её роль, значения, состояния и известные места применения. Такой малый contract не заменяет всю дизайн-систему. Он ограничивает изменение так, чтобы его можно было проверить и откатить.</p>\n<h2>Тезис: сначала договор, потом каталог</h2>\n<p>Каталог компонентов полезен, когда команда уже несколько раз приняла одно и то же решение. До этого каталог часто только прячет расхождения за общим названием. Две кнопки могут называться <code>Button</code>, но иметь разные правила loading, разные accessible name и разные последствия для отправки формы.</p>\n<p>Малый контракт отвечает на четыре вопроса. Какое значение меняется? Какие состояния поддерживает control? Как пользователь понимает его смысл? Какие существующие места попадут под правку? Если на один вопрос нет ответа, компонент ещё нельзя считать единым.</p>\n<h2>Механизм малого контракта</h2>\n<p>Разделите данные на четыре слоя. Named tokens хранят значения и их роль. State matrix перечисляет допустимые состояния. Semantic fields описывают имя и поведение control. Usage inventory показывает известный радиус изменения. Каждый слой проверяется отдельно, но вместе они дают узкий договор.</p>\n<p>Для primary button достаточно начать с пяти token names: <code>button.primary.background</code>, <code>button.primary.foreground</code>, <code>button.primary.focus-ring</code>, <code>button.primary.radius</code> и <code>button.primary.gap</code>. Названия в примере — учебное соглашение. В настоящем проекте они должны соответствовать принятой системе именования.</p>\n<p>State matrix может содержать <code>default</code>, <code>hover</code>, <code>focus-visible</code>, <code>disabled</code> и <code>loading</code>. Не каждое действие обязано поддерживать каждую ветку. Но отсутствие состояния должно быть решением. Если операция синхронная и loading ей не нужен, это нужно записать. Пустая ветка, которую команда просто забыла, станет дефектом позже.</p>\n<p>Semantic fields не выводятся из цвета. Серый цвет может означать disabled, loading, неактивную вкладку или случайный override. Контракт хранит имя действия, роль, объявленное состояние и признак disabled отдельно. Для обычного действия выбирайте native <code>button</code>: браузер уже даёт базовую роль и клавиатурную модель.</p>\n<div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Один синий цвет задан разными hex-значениями</td><td>Значение не связано с ролью компонента</td><td>Найти literal values и сравнить их usage</td><td>Ввести один named token для primary button</td></tr><tr><td>Фокус или loading появляется только после жалобы</td><td>Состояния не объявлены заранее</td><td>Сверить state matrix с кодом и сценариями</td><td>Добавить состояние или явно исключить его</td></tr><tr><td>Иконка выглядит как кнопка, но действие неясно</td><td>Имя control выводят из картинки</td><td>Проверить текстовое имя и native semantics</td><td>Оставить текст или задать проверяемое accessible name</td></tr><tr><td>Правка профиля меняет оплату</td><td>Неизвестны точки применения</td><td>Составить inventory с контекстом и состоянием</td><td>Сузить diff и повторить проверку мест</td></tr></tbody></table></div>\n<h2>Токен задаёт роль, а не просто переменную</h2>\n<p>CSS Custom Properties дают именованные свойства и подстановку через <code>var()</code>. Браузер понимает их синтаксис, но не знает, что значение относится к primary button и кто отвечает за его изменение. Смысл появляется в соглашении команды и в повторяемом применении.</p>\n<p>Например, <code>--button-primary-background</code> можно передать компоненту как CSS custom property. Но это не означает, что любой синий фон должен использовать тот же token. Primary, destructive и secondary button имеют разные роли, даже если сегодня два значения совпадают. Роль помогает менять одно решение без неожиданного каскадного эффекта.</p>\n<p>При проверке ищите четыре связи: имя token, значение, владелец и потребители. Если рядом с компонентом появляется новый <code>#2457D6</code>, решите, что это: новый вариант, локальное исключение или обход существующего contract. Временное значение тоже получает срок и способ отката. Иначе временное исключение быстро станет вторым стандартом.</p>\n<h2>Конкретный пример</h2>\n<p>Разметка должна сохранять смысл действия независимо от цвета и иконки. Этот фрагмент учебный: он показывает границу контракта, а не готовую библиотеку.</p>\n<pre><code>&lt;button\n type=\"submit\"\n class=\"button button--primary\"\n data-state=\"default\"\n&gt;\n Сохранить изменения\n&lt;/button&gt;</code></pre>\n<p>В реальном интерфейсе нужно проверить переходы <code>default</code> → <code>loading</code> → <code>default</code> или <code>error</code>. Пока запрос выполняется, повторная отправка должна иметь явное правило. После ошибки пользователь должен понимать, что произошло и какое действие доступно дальше. Эти решения нельзя прятать только в opacity.</p>\n<p>Если вместо native element используется <code>div role=\"button\"</code>, одной ARIA-роли недостаточно. Понадобятся клавиатурное управление, focus behavior, disabled semantics и проверка имени. Если custom host не даёт нужного поведения, это отрицательный результат: вернитесь к native <code>button</code>, а не добавляйте ещё один слой стилизации.</p>\n<h2>Проверка данных до проверки страницы</h2>\n<p>Сначала можно проверить сам контракт: все ли обязательные token names объявлены, все ли состояния перечислены, есть ли у control имя и роль, заполнены ли известные usage. Такой тест быстро ловит пропущенное поле и не требует рендера.</p>\n<pre><code>const requiredStates = [\n 'default', 'hover', 'focus-visible', 'disabled', 'loading'\n];\n\nconst contract = {\n name: 'Сохранить изменения',\n role: 'button',\n state: 'default',\n permittedStates: requiredStates\n};\n\nconst missingStates = requiredStates.filter(\n (state) =&gt; !contract.permittedStates.includes(state)\n);\n\nconst ok = missingStates.length === 0\n &amp;&amp; contract.role === 'button'\n &amp;&amp; Boolean(contract.name);</code></pre>\n<p>Результат <code>true</code> здесь означает только согласованность объекта. Код не создаёт DOM, не собирает CSS, не измеряет контраст, не проверяет порядок фокуса и не говорит, что screen reader произнесёт ожидаемое имя. Не называйте такую проверку visual regression или доказательством соответствия WCAG. Для этих утверждений нужна наблюдаемая страница и отдельный артефакт.</p>\n<figure><img src=\"/assets/editorial/2022/design-system-token-flow-2022.svg\" alt=\"Схема малого контракта primary button: named tokens и состояния поступают в компонент, затем известные места использования проверяются отдельно от визуального результата\" loading=\"lazy\" /><figcaption>Схема разделяет значения, состояния, семантику и места применения. Объявленный contract не является снимком интерфейса и не заменяет проверку в браузере.</figcaption></figure>\n<h2>Порядок действий</h2>\n<ol><li><strong>Выберите один control.</strong> Найдите повторяющуюся primary button с расхождением цвета, focus или label. Не включайте сразу все кнопки.</li><li><strong>Опишите симптом.</strong> Запишите экран, состояние и действие пользователя. Отделите наблюдаемую разницу от гипотезы о причине.</li><li><strong>Составьте inventory.</strong> Для каждого найденного usage укажите контекст, имя, роль, состояние и локальные overrides. Поиск по коду остаётся отдельной проверкой полноты.</li><li><strong>Объявите contract.</strong> Назовите tokens и state matrix. Для каждой неприменимой ветки запишите решение.</li><li><strong>Проверьте данные.</strong> Убедитесь, что нет пропущенных token, состояния или semantic name. Зафиксируйте, чего этот тест не наблюдает.</li><li><strong>Проверьте страницу.</strong> Соберите CSS, пройдите клавиатурный сценарий, проверьте DOM и accessibility tree, затем выполните согласованную visual-проверку на нужных viewport.</li><li><strong>Сделайте правку обратимой.</strong> Сохраните прежнее значение token и список затронутых usage. При неожиданном эффекте откатите точечную правку, а не весь unrelated CSS.</li></ol>\n<h2>Отрицательный путь: похожий control не всегда тот же</h2>\n<p>Предположим, три формы используют разные обязательные поля, цвета ошибок и правила disabled. Общий token не исправит проблему. Эти формы могут выглядеть похоже, но иметь разные state matrix и разные требования к имени. В таком случае не расширяйте primary button новым набором optional props только ради повторного названия.</p>\n<p>Сначала зафиксируйте различие в поведении. Если оно устойчиво, создайте отдельную роль или variant с собственным contract. Если различие появилось из-за случайного override, удалите override и верните control к исходному contract. Это дешевле, чем превращать исключение в универсальный API.</p>\n<h2>Ограничения и критерий готовности</h2>\n<p>Малый contract не строит темизацию, не мигрирует legacy CSS, не выбирает типографику бренда и не доказывает, что найден каждый usage. Он также не заменяет ручную проверку доступности и visual comparison. Значения, имена и места из примеров учебные. Production-результат можно утверждать только после реального запуска и сохранённого наблюдения.</p>\n<p>Критерий готовности проверяемый: для выбранной primary button каждый обязательный state имеет решение; каждый token имеет имя и место потребления; каждый найденный usage имеет name, role и state; проверка данных проходит; реальная страница отдельно подтверждает keyboard, focus-visible, DOM semantics и visual diff; rollback возвращает прежнее значение. Если хотя бы один пункт неизвестен, contract не готов.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.w3.org/TR/css-variables-1/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: CSS Custom Properties for Cascading Variables Module Level 1</a> — описывает custom properties и функцию <code>var()</code>.</li><li><a href=\"https://www.w3.org/TR/wai-aria/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: WAI-ARIA 1.2</a> — описывает роли, состояния и свойства; ARIA не заменяет поведение native button.</li><li><a href=\"https://www.w3.org/TR/WCAG22/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Web Content Accessibility Guidelines 2.2</a> — содержит критерии доступности, но не превращает проверку объекта в доказательство соответствия.</li></ul>"
}