diff --git a/editorial/agent-rewrites/202.json b/editorial/agent-rewrites/202.json index 16394f1..f786742 100644 --- a/editorial/agent-rewrites/202.json +++ b/editorial/agent-rewrites/202.json @@ -3,5 +3,5 @@ "slug": "editorial-2022-05-field-design-system", "title": "Как менять токен дизайн-системы и не сломать соседний экран", "excerpt": "Практический разбор правки токена: как зафиксировать симптом, оценить радиус изменения, проверить состояния кнопки и оставить точный путь к откату.", - "contentHtml": "
Цвет primary-кнопки меняют в одной строке, а ошибка появляется на другом экране. На форме оплаты пропадает focus ring. В диалоге отмены кнопка получает цвет действия подтверждения. В третьем месте новый токен не применяется, потому что компонент ждёт другое имя. Визуально это похоже на одну проблему. Технически это разные сбои контракта.
\nЦена ошибки растёт вместе с радиусом общего токена. Локальная правка исправляет один usage. Глобальная правка меняет все usages, включая те, которые не попали в поиск. Если команда не знает список мест и состояний, она не может объяснить diff, выбрать безопасный откат или отличить новый вариант от случайного исключения.
\nТезис. Токен нельзя менять по одному скриншоту. Сначала свяжите симптом с ролью компонента, состоянием, именем токена и известными usages. Затем проверьте допустимость значения. Только после этого меняйте один слой и сохраняйте прежнее значение. Такой порядок делает изменение ограниченным, наблюдаемым и обратимым.
\nДизайн-токен — не просто цветовая константа. Он выражает роль: например, фон primary-кнопки в обычном состоянии. Роль связывает значение с компонентом и состоянием. Если код использует #2457D6 напрямую, он обходит эту связь. Если код ссылается на неизвестное имя, система получает второй, неописанный способ задать ту же роль.
У одного значения есть четыре границы. Первая — имя роли, например button.primary.background. Вторая — состояние: default, hover, focus-visible, disabled или loading. Третья — компонент, который действительно может использовать эту роль. Четвёртая — набор известных мест в коде. Пропуск любой границы превращает косметическую правку в догадку.
Состояния нельзя восстановить из одного цвета. Кнопка может сохранить фон и потерять outline при переходе на клавиатуру. Она может выглядеть одинаково в спокойном состоянии, но стать неразличимой при отключении. Поэтому inventory должен хранить не только имя токена, но и ожидаемые состояния. Для ссылки, переключателя и destructive-действия нужен отдельный контракт, а не необязательный флаг в универсальной кнопке.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| На одном экране другой синий | Literal обошёл именованный токен | Найти значение и сравнить с ролью в inventory | Заменить подтверждённый usage |
| После refactor исчез focus ring | Состояние не входит в матрицу | Пройти кнопку клавиатурой | Вернуть focus-visible в контракт |
| Новое имя не работает | Имя отсутствует в словаре роли | Проверить декларацию и чтение | Остановить правку или добавить роль |
| Review назван visual-проверкой без снимка | Список входов перепутали с результатом | Проверить браузер, viewport и diff | Не выдавать зелёный статус без артефакта |
| Платёжный экран изменился вместе с профилем | Общий токен исправляли без радиуса | Сопоставить usages и роли | Вернуть прежнее значение и отделить variant |
Начните с короткой таблицы известных usages. Для каждого места запишите компонент, роль, состояние, имя и способ задания значения. Например, profile-save использует button.primary.background в состояниях default, hover, focus-visible, disabled и loading. dialog-cancel может быть secondary-кнопкой. Похожая разметка не делает эти usages одной ролью.
Поиск по имени и hex-значению даёт кандидатов, но не доказывает полноту inventory. Стиль может прийти из темы, CSS-переменной, inline-значения или обёртки. Автоматический поиск не определяет, является ли ссылка кнопкой по смыслу. Поэтому результат поиска нужно проверить на уровне entry point и фактического состояния. Учебный пример ниже ограничен тремя usages и не изображает полный обход настоящего репозитория.
\nНеверная конфигурация должна остановиться до записи. В примере есть словарь разрешённых ролей и простая проверка формата: цвет передаётся как #RRGGBB. Это учебное правило, а не универсальный валидатор CSS. Его задача — не дать срочной правке молча создать новый ключ или принять случайную строку.
const tokens = {'button.primary.background':'#2457D6'};\nfunction correctToken(name, nextValue) {\n if (!(name in tokens)) return {ok:false, reason:'unknown-token'};\n if (!/^#[0-9A-F]{6}$/i.test(nextValue)) return {ok:false, reason:'invalid-color'};\n return {ok:true, name, previousValue:tokens[name], nextValue};\n}\nfunction rollbackToken(change) {\n return {ok:change.ok, name:change.name, nextValue:change.previousValue};\n}\nconst change = correctToken('button.primary.background','#1D4ED8');\nconst restored = rollbackToken(change);\nФункция не меняет объект сама. Она возвращает решение с именем, прежним и новым значением. Отдельный слой применяет решение после проверки inventory. Валидатор не знает, подходит ли новый синий для оплаты, профиля или тёмной темы. Он проверяет только форму и существование роли.
\nВ учебном коде change.nextValue равен #1D4ED8, а restored.nextValue равен #2457D6. Пример не запускает CSS, не открывает браузер и не измеряет контраст. Он показывает только две операции: неизвестное имя отклоняется, а принятая правка хранит точное прежнее значение. В настоящем проекте формат, запись и права должны соответствовать его контракту.
До реального запуска можно составить список входов: viewport 375 и 1280, состояния default, hover, focus-visible, disabled, loading, светлая и тёмная темы. Список полезен как граница проверки: он заставляет назвать объём работы и не забыть состояние.
Но список не видит пиксели, cascade, media query, шрифт или порядок фокуса. Заполненный JSON не доказывает, что браузер применил нужную переменную. Для visual-проверки нужны страница, браузер, viewport, baseline, новый снимок, правило допустимого diff и сохранённый результат. Для доступности нужны keyboard route и выбранная комбинация браузера и вспомогательной технологии. Если артефакта нет, проверку нельзя называть успешной.
\nЭтот подход не вычисляет контраст и не заменяет ручную проверку. Он не решает каскад тем, локализацию, media queries, пользовательские настройки, разные браузеры и конкурирующие изменения. Он также не определяет, является ли новый цвет хорошим дизайном. Он отвечает на более узкий вопрос: существует ли роль, что именно она затрагивает и можно ли вернуть прежнее значение.
\nОдин общий токен не всегда лучше двух. Если профиль и оплата имеют разные требования к смыслу, состояниям или риску ошибки, их нужно разделить после подтверждения usages. Нельзя объявлять variant только потому, что один экран случайно выглядит иначе. Сначала исключите literal, неправильное состояние и неверное имя.
\nRollback не безопасен во всех слоях. Возврат значения токена не отменяет опубликованный CSS, не откатывает сборку и не компенсирует действие пользователя. Для этих уровней нужны собственные процедуры и артефакты. Здесь rollback ограничен одной парой name → previousValue.
Правка готова, если команда может показать inventory затронутых usages, матрицу состояний и запись с прежним значением. Неизвестный token и неверный формат останавливаются без изменения. Допустимая правка меняет только выбранную роль. Отдельный запуск подтверждает нужные viewport и состояния реальным артефактом. Повторный просмотр возвращает прежнее значение по сохранённому previousValue, а не по памяти или догадке.
Если visual или accessibility запуск ещё не выполнен, критерий должен прямо это показывать. Нельзя превращать объявленный объём проверки в результат. Для учебного примера достаточно проверить решения функции и отрицательные входы. Для своего интерфейса добавьте браузерную проверку, историю baseline и ссылку на сохранённый diff.
\nКоманда меняет цвет primary-кнопки в одной строке, а ошибка проявляется на другом экране. На форме оплаты исчезает видимый focus ring. В диалоге отмены кнопка получает цвет подтверждающего действия. В третьем месте новый токен не работает: компонент ждёт другое имя. Похожий синий цвет маскирует несколько разных разрывов контракта.
\nЦена ошибки зависит от радиуса токена. Локальная правка затрагивает один usage. Глобальная правка проходит по всем местам, где значение пришло через тему, CSS-переменную, обёртку или запасной вариант. Если команда не знает эти места и состояния, она не может объяснить diff, безопасно откатить изменение или доказать, что соседний экран не изменился.
\nТезис. Токен нельзя менять по одному скриншоту. Сначала свяжите симптом с ролью компонента, состоянием, именем токена и подтверждёнными местами использования. Затем проверьте формат и границы значения. После этого меняйте один слой, сохраняйте прежнее значение и отдельно проверяйте браузерный результат. Так правка остаётся ограниченной и обратимой.
\nCSS custom property — пользовательское CSS-свойство с именем вроде --button-primary-background. Браузер не знает, что такое «главная кнопка» как продуктовая роль: он применяет к свойству обычные правила наследования и каскада. Значение можно объявить в теме, переопределить в media query или передать через var(). Семантическое имя, список допустимых компонентов и решение о владельце — уже договорённость проекта.
Поэтому в изменении есть два разных уровня. На уровне CSS нужно понять, где вычисляется значение и какой каскад побеждает. На уровне дизайн-системы нужно понять, какую роль выражает токен и какие состояния она покрывает. Объявленный токен может быть технически валиден, но использован для неподходящего действия. И наоборот: правильная роль может потерять focus outline, если матрица состояний учитывала только обычный фон.
\nУ каждой правки должны быть четыре границы: роль, состояние, компонент и реестр использований. Например, button.primary.background описывает фон основной кнопки в обычном состоянии, но не отвечает за hover, focus-visible, disabled и loading. Эти состояния нужно перечислить отдельно. Для ссылки, переключателя и destructive-действия похожий цвет не означает одинаковый контракт.
| Симптом | Вероятная причина | Проверка | Ограниченное действие |
|---|---|---|---|
| На одном экране другой синий | Literal обошёл именованный токен или победил каскад | Найти имя, значение и источник вычисленного стиля | Исправить подтверждённый usage, не весь каталог |
| После правки исчез focus ring | Состояние не вошло в матрицу токена | Пройти control клавиатурой и посмотреть computed style | Вернуть отдельный контракт focus-visible |
| Новое имя не работает | Имя не объявлено или не читается компонентом | Проверить декларацию, область видимости и var() | Остановить правку до согласования имени |
| Платёжный экран изменился вместе с профилем | Общую роль изменили без проверки реестра | Сопоставить usages, темы и состояния | Вернуть прежнее значение или разделить роли |
| Review объявил visual-проверку без артефакта | Список входов выдали за результат | Проверить viewport, baseline, новый снимок и diff | Оставить статус «не проверено», пока нет результата |
Начните с короткого реестра, а не с массовой замены. Для каждого места запишите компонент, роль, состояние, имя токена и способ задания значения. Например, profile-save может использовать button.primary.background в состояниях default, hover, focus-visible, disabled и loading. dialog-cancel может быть secondary-кнопкой, даже если разметка выглядит почти так же.
Поиск по имени и hex-значению даёт кандидатов, но не доказывает полноту. Значение может прийти из темы, inline-стиля, запасного аргумента var() или обёртки компонента. Поиск также не определяет смысл элемента: ссылка и кнопка могут выглядеть одинаково, но иметь разную клавиатурную и семантическую модель. Поэтому каждый кандидат нужно подтвердить в точке входа и в реальном состоянии.
В реестре полезно разделить факт и гипотезу: «найдено в CSS-модуле» — факт, «используется только профилем» — гипотеза до проверки маршрута, темы и тестовых состояний. Это небольшое разделение не даёт предположению превратиться в обещание полного охвата.
\nСначала проверяется предложение об изменении, затем отдельный слой решает, применять ли его к стилям. Учебный валидатор ниже проверяет только существование роли и формат #RRGGBB. Это проектное ограничение примера, а не полный синтаксис CSS и не проверка контраста.
const tokens = Object.freeze({\n 'button.primary.background': '#2457D6',\n});\n\nfunction proposeTokenChange(tokenMap, name, nextValue) {\n const known = Object.prototype.hasOwnProperty.call(tokenMap, name);\n if (!known) return { ok: false, reason: 'unknown-token' };\n if (!/^#[0-9a-f]{6}$/i.test(nextValue)) {\n return { ok: false, reason: 'invalid-color' };\n }\n return {\n ok: true,\n name,\n previousValue: tokenMap[name],\n nextValue,\n };\n}\n\nfunction rollback(change) {\n if (!change.ok) return { ok: false, reason: 'nothing-to-rollback' };\n return {\n ok: true,\n name: change.name,\n nextValue: change.previousValue,\n rollback: true,\n };\n}\n\nconst accepted = proposeTokenChange(\n tokens,\n 'button.primary.background',\n '#1D4ED8',\n);\nconsole.assert(accepted.ok && accepted.previousValue === '#2457D6');\nconsole.assert(\n proposeTokenChange(tokens, 'button.secondary.background', '#1D4ED8').reason\n === 'unknown-token',\n);\nconsole.assert(\n proposeTokenChange(tokens, 'button.primary.background', 'blue').reason\n === 'invalid-color',\n);\nconst restored = rollback(accepted);\nconsole.assert(restored.rollback && restored.nextValue === '#2457D6');\nВ этом фрагменте карта токенов не меняется сама: функция возвращает решение, прежнее значение и новое значение. Поэтому положительный путь можно проверить отдельно от записи. Первый отрицательный путь отклоняет неизвестную роль, второй — строку, не соответствующую выбранному формату. Откат возвращает ровно #2457D6, сохранённое до изменения.
Запустите пример как обычный JavaScript-файл: все четыре console.assert должны завершиться без сообщения. Это доказывает только контракт предложения и отката. Код не собирает CSS, не открывает браузер, не вычисляет контраст и не показывает, какой компонент победит в каскаде. Для этих утверждений нужны отдельные проверки.
Список входов помогает очертить работу: viewport 375 и 1280, светлая и тёмная темы, состояния default, hover, focus-visible, disabled и loading. Он также фиксирует, что именно команда собирается смотреть. Но список не видит computed style, шрифт, перекрытый outline, порядок фокуса и фактический каскад.
Для visual-проверки нужны страница, браузер, viewport, baseline, новый снимок, правило допустимого diff и сохранённый результат. Для keyboard-проверки нужна последовательность переходов фокуса. Для accessibility-проверки нужно сопоставить конкретный критерий с наблюдаемым результатом; одного зелёного статуса валидатора токенов недостаточно. Если запуск не выполнен, это следует назвать ограничением, а не превращать объявленный объём в факт.
\nНативная кнопка тоже требует отдельной проверки поведения. Атрибут type влияет на то, отправляет ли кнопка форму, сбрасывает её или не выполняет это действие. CSS-токен может изменить цвет disabled-состояния, но не должен незаметно решать, принимает ли control клик. Смысл, состояние и стиль связаны в интерфейсе, но принадлежат разным слоям контракта.
var().focus-visible, затем проверьте disabled и loading. Обычный фон не заменяет эти состояния.previousValue, измените выбранную роль и запишите причину изменения.Подход отвечает на узкий вопрос: существует ли роль, какие места и состояния она затрагивает и можно ли вернуть прежнее значение. Он не выбирает хороший дизайн, не вычисляет контраст и не заменяет проверку экранным диктором или браузером. Он также не решает автоматически локализацию, media queries, пользовательские настройки, конкурирующие изменения и различия между браузерами.
\nОдин общий токен не всегда лучше двух. Если профиль и оплата имеют разные требования к смыслу, состояниям или цене ошибки, разделите роли после проверки usages. Не объявляйте variant только потому, что один экран случайно выглядит иначе: сначала исключите literal, неверный каскад, неправильное состояние и ошибочное имя.
\nОткат предложения не равен откату выпуска. Возврат пары name → previousValue не отменяет уже опубликованный CSS, сборку, кеш или действие пользователя. Для этих уровней нужны отдельные процедуры и артефакты. В этом примере rollback ограничен подготовленным решением и не притворяется операцией деплоя.
Правка готова, если команда может показать реестр затронутых мест, матрицу состояний и запись с прежним значением. Неизвестная роль и неверный формат останавливаются без изменения. Допустимая правка меняет только выбранный контракт. Отдельный браузерный запуск подтверждает нужные viewport и состояния сохранённым артефактом. Повторная проверка возвращает прежнее значение по previousValue, а не по памяти или догадке.
Если visual- или accessibility-запуск ещё не выполнен, критерий должен это показывать. Для учебного примера достаточно запустить четыре утверждения JavaScript и проверить отрицательные входы. Для настоящего интерфейса добавьте DOM-проверку, keyboard route, критерии доступности и историю baseline. Только так можно отличить рабочую модель данных от доказанного поведения страницы.
\nvar(). Спецификация не назначает им продуктовую семантику.type, влияющие на её поведение в форме.