diff --git a/editorial/agent-rewrites/204.json b/editorial/agent-rewrites/204.json index 5d026a0..32763dd 100644 --- a/editorial/agent-rewrites/204.json +++ b/editorial/agent-rewrites/204.json @@ -1,7 +1,7 @@ { "index": 204, "slug": "editorial-2022-05-practice-design-system", - "title": "Маленькая дизайн-система: как закрепить контракт кнопки", - "excerpt": "Кнопки расходятся по цвету, фокусу и поведению, когда их контракт разбросан по разным слоям. Разбираем малую схему с токенами, состояниями, проверками и обратимой правкой.", - "contentHtml": "
Проблема: на одном экране primary button получает цвет из локального CSS, на другом теряет видимый фокус, а на третьем остаётся доступной во время загрузки; пользователь видит непредсказуемое действие, а команда платит повторной правкой и риском сломать соседний сценарий.
\nПричина обычно не в том, что в проекте мало компонентов. У одной кнопки нет общего проверяемого контракта. Цвет живёт в стилях, состояние — в обработчике, текст — в разметке, а список мест использования никто не держит. Поэтому безопасная на вид замена цвета может затронуть оплату, профиль и диалог одновременно.
\nНачинайте с одной повторяющейся primary button. Зафиксируйте её роль, значения, состояния и известные места применения. Такой малый contract не заменяет всю дизайн-систему. Он ограничивает изменение так, чтобы его можно было проверить и откатить.
\nКаталог компонентов полезен, когда команда уже несколько раз приняла одно и то же решение. До этого каталог часто только прячет расхождения за общим названием. Две кнопки могут называться Button, но иметь разные правила loading, разные accessible name и разные последствия для отправки формы.
Малый контракт отвечает на четыре вопроса. Какое значение меняется? Какие состояния поддерживает control? Как пользователь понимает его смысл? Какие существующие места попадут под правку? Если на один вопрос нет ответа, компонент ещё нельзя считать единым.
\nРазделите данные на четыре слоя. Named tokens хранят значения и их роль. State matrix перечисляет допустимые состояния. Semantic fields описывают имя и поведение control. Usage inventory показывает известный радиус изменения. Каждый слой проверяется отдельно, но вместе они дают узкий договор.
\nДля primary button достаточно начать с пяти token names: button.primary.background, button.primary.foreground, button.primary.focus-ring, button.primary.radius и button.primary.gap. Названия в примере — учебное соглашение. В настоящем проекте они должны соответствовать принятой системе именования.
State matrix может содержать default, hover, focus-visible, disabled и loading. Не каждое действие обязано поддерживать каждую ветку. Но отсутствие состояния должно быть решением. Если операция синхронная и loading ей не нужен, это нужно записать. Пустая ветка, которую команда просто забыла, станет дефектом позже.
Semantic fields не выводятся из цвета. Серый цвет может означать disabled, loading, неактивную вкладку или случайный override. Контракт хранит имя действия, роль, объявленное состояние и признак disabled отдельно. Для обычного действия выбирайте native button: браузер уже даёт базовую роль и клавиатурную модель.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Один синий цвет задан разными hex-значениями | Значение не связано с ролью компонента | Найти literal values и сравнить их usage | Ввести один named token для primary button |
| Фокус или loading появляется только после жалобы | Состояния не объявлены заранее | Сверить state matrix с кодом и сценариями | Добавить состояние или явно исключить его |
| Иконка выглядит как кнопка, но действие неясно | Имя control выводят из картинки | Проверить текстовое имя и native semantics | Оставить текст или задать проверяемое accessible name |
| Правка профиля меняет оплату | Неизвестны точки применения | Составить inventory с контекстом и состоянием | Сузить diff и повторить проверку мест |
CSS Custom Properties дают именованные свойства и подстановку через var(). Браузер понимает их синтаксис, но не знает, что значение относится к primary button и кто отвечает за его изменение. Смысл появляется в соглашении команды и в повторяемом применении.
Например, --button-primary-background можно передать компоненту как CSS custom property. Но это не означает, что любой синий фон должен использовать тот же token. Primary, destructive и secondary button имеют разные роли, даже если сегодня два значения совпадают. Роль помогает менять одно решение без неожиданного каскадного эффекта.
При проверке ищите четыре связи: имя token, значение, владелец и потребители. Если рядом с компонентом появляется новый #2457D6, решите, что это: новый вариант, локальное исключение или обход существующего contract. Временное значение тоже получает срок и способ отката. Иначе временное исключение быстро станет вторым стандартом.
Разметка должна сохранять смысл действия независимо от цвета и иконки. Этот фрагмент учебный: он показывает границу контракта, а не готовую библиотеку.
\n<button\n type=\"submit\"\n class=\"button button--primary\"\n data-state=\"default\"\n>\n Сохранить изменения\n</button>\nВ реальном интерфейсе нужно проверить переходы default → loading → default или error. Пока запрос выполняется, повторная отправка должна иметь явное правило. После ошибки пользователь должен понимать, что произошло и какое действие доступно дальше. Эти решения нельзя прятать только в opacity.
Если вместо native element используется div role=\"button\", одной ARIA-роли недостаточно. Понадобятся клавиатурное управление, focus behavior, disabled semantics и проверка имени. Если custom host не даёт нужного поведения, это отрицательный результат: вернитесь к native button, а не добавляйте ещё один слой стилизации.
Сначала можно проверить сам контракт: все ли обязательные token names объявлены, все ли состояния перечислены, есть ли у control имя и роль, заполнены ли известные usage. Такой тест быстро ловит пропущенное поле и не требует рендера.
\nconst 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) => !contract.permittedStates.includes(state)\n);\n\nconst ok = missingStates.length === 0\n && contract.role === 'button'\n && Boolean(contract.name);\nРезультат true здесь означает только согласованность объекта. Код не создаёт DOM, не собирает CSS, не измеряет контраст, не проверяет порядок фокуса и не говорит, что screen reader произнесёт ожидаемое имя. Не называйте такую проверку visual regression или доказательством соответствия WCAG. Для этих утверждений нужна наблюдаемая страница и отдельный артефакт.
Предположим, три формы используют разные обязательные поля, цвета ошибок и правила disabled. Общий token не исправит проблему. Эти формы могут выглядеть похоже, но иметь разные state matrix и разные требования к имени. В таком случае не расширяйте primary button новым набором optional props только ради повторного названия.
\nСначала зафиксируйте различие в поведении. Если оно устойчиво, создайте отдельную роль или variant с собственным contract. Если различие появилось из-за случайного override, удалите override и верните control к исходному contract. Это дешевле, чем превращать исключение в универсальный API.
\nМалый contract не строит темизацию, не мигрирует legacy CSS, не выбирает типографику бренда и не доказывает, что найден каждый usage. Он также не заменяет ручную проверку доступности и visual comparison. Значения, имена и места из примеров учебные. Production-результат можно утверждать только после реального запуска и сохранённого наблюдения.
\nКритерий готовности проверяемый: для выбранной primary button каждый обязательный state имеет решение; каждый token имеет имя и место потребления; каждый найденный usage имеет name, role и state; проверка данных проходит; реальная страница отдельно подтверждает keyboard, focus-visible, DOM semantics и visual diff; rollback возвращает прежнее значение. Если хотя бы один пункт неизвестен, contract не готов.
\nvar().Три одинаковые на вид кнопки редко ломаются одновременно. Одна берёт синий цвет из локального файла, вторая не показывает фокус, третья на disabled меняет только opacity, а четвёртая вместо понятного имени имеет иконку. Симптом кажется косметическим, пока пользователь не попадает в другой сценарий. Цена — каждый новый экран получает ещё один почти такой же control, а исправление цвета начинает менять поведение там, где его не ожидали.
\nВ мае 2022 года я бы не начинал с «универсальной дизайн-системы». Сначала нужен узкий контракт одной primary button: именованные токены, обязательные состояния, семантическое имя, роль и список реальных мест использования. Это продолжает предыдущую статью о доступном control: внешний вид и name/role/state нельзя держать в разных случайных ветках. Пример ниже — локальная fixture, не DOM, не CSS и не visual regression test; он проверяет только объявленные данные.
\nМинимальная система отвечает на вопрос «какой button contract должны разделять эти три места», а не «как описать любой интерфейс». В неё входят пять токенов: background, foreground, focus ring, radius и gap. В неё входят пять состояний: default, hover, focus-visible, disabled и loading. Не все состояния обязаны выглядеть одинаково в каждом продукте, но их отсутствие не должно быть случайностью. Если loading невозможен для действия без сети, это решение нужно записать в contract, а не скрыть в одном компоненте.
\n| Часть | Симптом без неё | Проверяемый факт | Действие |
|---|---|---|---|
| Named token | два экрана называют один синий разными hex-значениями | у каждого required value есть стабильное имя | вынести значение в button.primary.* и искать локальные дубли |
| State matrix | focus или loading появляется только после жалобы | default, hover, focus-visible, disabled, loading названы до реализации | для отсутствующего state принять явное product decision |
| Semantic contract | иконка выглядит как кнопка, но не имеет понятного действия | есть name, role button и declared state | оставить native host, если custom behavior не нужен |
| Usage inventory | правка profile-save ломает оплату | перечислены известные usage с контекстом и состоянием | менять один token малым diff и повторять проверку мест |
CSS Custom Properties допускает author-defined properties с префиксом -- и подстановку через var(). Это полезный механизм, но он не создаёт за команду словарь design tokens. Имя button.primary.background в этой статье — соглашение пакета: оно говорит, что значение относится к роли primary button, а не ко всем синим пикселям проекта. Поэтому не стоит сразу делать brand.blue.500 единственным входом для компонента: у роли должна быть собственная граница, даже если сегодня она ссылается на тот же цвет.
Проверка проста: у каждой величины есть имя, владелец и место потребления. Если в pull request появляется #2457D6 рядом с кнопкой, сначала спросите, это новый token или обход существующего. Если ответ «временно», зафиксируйте срок и конкретный rollback. Не нужно объявлять каждую тень и каждый margin глобальным token. Глобальность оправдана только повторяемым contract; одиночная геометрия остаётся локальной, пока не появится второй подтверждённый use case.
Fixture создаёт in-memory объект с пятью named tokens, матрицей required states, declared name/role/state и usage inventory из трёх кнопок. Ещё в ней есть visual payload: ширины 375 и 1280, набор states и список token names. Это вход для будущего snapshot-процесса, а не screenshot, diff или PASS реального инструмента. Граница записана в самом объекте: DOM, browser, CSS compilation, HTTP и visual regression не запускались.
\nnode web/scripts/upgrade-2022-05.mjs --verify-fixture\n// PASS fixture: 13/13 assertions\n\n// Проверяется локальный contract: tokens, states, semantic fields, inventory,\n// declared visual payload, invalid configuration и rollback. Это не UI test.\nТакой тест ловит дешёвую ошибку раньше рендера: кто-то добавил usage без имени, убрал loading из состояния или стал использовать token, которого contract не объявляет. Он не ловит контраст на реальном фоне, порядок клавиатуры, cascade в существующем CSS или изменение пикселей на устройстве. Это разные проверки. Их полезно добавлять следующими, но нельзя дорисовывать их результат к локальному объекту числом score или словом «доступно».
\nЭтот подход не выбирает типографику бренда, не строит темизацию, не мигрирует legacy CSS и не заменяет дизайн-ревью. Он также не доказывает WCAG-conformance: WCAG содержит проверяемые критерии, но локальная fixture не наблюдает страницу. Числа, hex-значения и названия из примера — учебные проектные решения. В другом продукте focus ring может иметь другое имя и значение; важнее, чтобы его существование и ответственность были явными.
\nСледующий проверяемый шаг — выбрать три настоящих usage одной primary button, составить inventory до изменения и договориться о минимальном payload для внешнего visual review. Если один usage требует другого состояния или семантики, не расширяйте contract по умолчанию. Сначала зафиксируйте причину: это variant той же кнопки или другой control. Такой вопрос экономит больше времени, чем ранний каталог из двадцати компонентов.
\nТекст опирается на Candidate Recommendation Draft CSS Custom Properties от 11 ноября 2021 года и Candidate Recommendation Draft WAI-ARIA 1.2 от 8 декабря 2021 года — оба снимка доступны до мая 2022-го. WCAG 2.1 здесь приведён как стабильная Recommendation 2018 года. Эти документы описывают CSS-механизм и accessibility semantics, но не утверждают, что названия tokens, inventory или payload из fixture существовали в конкретной команде.
\n