Files
progcode/editorial/agent-rewrites/202.json
T
2026-09-03 19:43:13 +03:00

8 lines
21 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": 202,
"slug": "editorial-2022-05-field-design-system",
"title": "Как менять токен дизайн-системы и не сломать соседний экран",
"excerpt": "Практический разбор правки токена: как зафиксировать симптом, оценить радиус изменения, проверить состояния кнопки и оставить точный путь к откату.",
"contentHtml": "<p>Команда меняет цвет primary-кнопки в одной строке, а ошибка проявляется на другом экране. На форме оплаты исчезает видимый focus ring. В диалоге отмены кнопка получает цвет подтверждающего действия. В третьем месте новый токен не работает: компонент ждёт другое имя. Похожий синий цвет маскирует несколько разных разрывов контракта.</p>\n<p>Цена ошибки зависит от радиуса токена. Локальная правка затрагивает один usage. Глобальная правка проходит по всем местам, где значение пришло через тему, CSS-переменную, обёртку или запасной вариант. Если команда не знает эти места и состояния, она не может объяснить diff, безопасно откатить изменение или доказать, что соседний экран не изменился.</p>\n<p><strong>Тезис.</strong> Токен нельзя менять по одному скриншоту. Сначала свяжите симптом с ролью компонента, состоянием, именем токена и подтверждёнными местами использования. Затем проверьте формат и границы значения. После этого меняйте один слой, сохраняйте прежнее значение и отдельно проверяйте браузерный результат. Так правка остаётся ограниченной и обратимой.</p>\n<h2>Что на самом деле меняется</h2>\n<p>CSS custom property — пользовательское CSS-свойство с именем вроде <code>--button-primary-background</code>. Браузер не знает, что такое «главная кнопка» как продуктовая роль: он применяет к свойству обычные правила наследования и каскада. Значение можно объявить в теме, переопределить в media query или передать через <code>var()</code>. Семантическое имя, список допустимых компонентов и решение о владельце — уже договорённость проекта.</p>\n<p>Поэтому в изменении есть два разных уровня. На уровне CSS нужно понять, где вычисляется значение и какой каскад побеждает. На уровне дизайн-системы нужно понять, какую роль выражает токен и какие состояния она покрывает. Объявленный токен может быть технически валиден, но использован для неподходящего действия. И наоборот: правильная роль может потерять focus outline, если матрица состояний учитывала только обычный фон.</p>\n<p>У каждой правки должны быть четыре границы: роль, состояние, компонент и реестр использований. Например, <code>button.primary.background</code> описывает фон основной кнопки в обычном состоянии, но не отвечает за <code>hover</code>, <code>focus-visible</code>, <code>disabled</code> и <code>loading</code>. Эти состояния нужно перечислить отдельно. Для ссылки, переключателя и destructive-действия похожий цвет не означает одинаковый контракт.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Минимальная диагностика разрыва контракта</caption><thead><tr><th>Симптом</th><th>Вероятная причина</th><th>Проверка</th><th>Ограниченное действие</th></tr></thead><tbody><tr><td>На одном экране другой синий</td><td>Literal обошёл именованный токен или победил каскад</td><td>Найти имя, значение и источник вычисленного стиля</td><td>Исправить подтверждённый usage, не весь каталог</td></tr><tr><td>После правки исчез focus ring</td><td>Состояние не вошло в матрицу токена</td><td>Пройти control клавиатурой и посмотреть computed style</td><td>Вернуть отдельный контракт focus-visible</td></tr><tr><td>Новое имя не работает</td><td>Имя не объявлено или не читается компонентом</td><td>Проверить декларацию, область видимости и <code>var()</code></td><td>Остановить правку до согласования имени</td></tr><tr><td>Платёжный экран изменился вместе с профилем</td><td>Общую роль изменили без проверки реестра</td><td>Сопоставить usages, темы и состояния</td><td>Вернуть прежнее значение или разделить роли</td></tr><tr><td>Review объявил visual-проверку без артефакта</td><td>Список входов выдали за результат</td><td>Проверить viewport, baseline, новый снимок и diff</td><td>Оставить статус «не проверено», пока нет результата</td></tr></tbody></table>\n<h2>Реестр до изменения</h2>\n<p>Начните с короткого реестра, а не с массовой замены. Для каждого места запишите компонент, роль, состояние, имя токена и способ задания значения. Например, <code>profile-save</code> может использовать <code>button.primary.background</code> в состояниях <code>default</code>, <code>hover</code>, <code>focus-visible</code>, <code>disabled</code> и <code>loading</code>. <code>dialog-cancel</code> может быть secondary-кнопкой, даже если разметка выглядит почти так же.</p>\n<p>Поиск по имени и hex-значению даёт кандидатов, но не доказывает полноту. Значение может прийти из темы, inline-стиля, запасного аргумента <code>var()</code> или обёртки компонента. Поиск также не определяет смысл элемента: ссылка и кнопка могут выглядеть одинаково, но иметь разную клавиатурную и семантическую модель. Поэтому каждый кандидат нужно подтвердить в точке входа и в реальном состоянии.</p>\n<p>В реестре полезно разделить факт и гипотезу: «найдено в CSS-модуле» — факт, «используется только профилем» — гипотеза до проверки маршрута, темы и тестовых состояний. Это небольшое разделение не даёт предположению превратиться в обещание полного охвата.</p>\n<h2>Проверяемый пример с откатом</h2>\n<p>Сначала проверяется предложение об изменении, затем отдельный слой решает, применять ли его к стилям. Учебный валидатор ниже проверяет только существование роли и формат <code>#RRGGBB</code>. Это проектное ограничение примера, а не полный синтаксис CSS и не проверка контраста.</p>\n<pre><code>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');</code></pre>\n<p>В этом фрагменте карта токенов не меняется сама: функция возвращает решение, прежнее значение и новое значение. Поэтому положительный путь можно проверить отдельно от записи. Первый отрицательный путь отклоняет неизвестную роль, второй — строку, не соответствующую выбранному формату. Откат возвращает ровно <code>#2457D6</code>, сохранённое до изменения.</p>\n<p>Запустите пример как обычный JavaScript-файл: все четыре <code>console.assert</code> должны завершиться без сообщения. Это доказывает только контракт предложения и отката. Код не собирает CSS, не открывает браузер, не вычисляет контраст и не показывает, какой компонент победит в каскаде. Для этих утверждений нужны отдельные проверки.</p>\n<figure><img src='/assets/editorial/2022/design-system-diagnosis-rollback-2022.svg' alt='Схема проверки токена: реестр использований ведёт к проверке роли и состояния, неверное предложение останавливается, допустимое сохраняет прежнее значение для отката' /><figcaption>Диагностический маршрут правки. Схема показывает порядок проверки и возврата значения; она не является результатом visual-запуска.</figcaption></figure>\n<h2>Почему валидный токен ещё не даёт валидный интерфейс</h2>\n<p>Список входов помогает очертить работу: viewport 375 и 1280, светлая и тёмная темы, состояния <code>default</code>, <code>hover</code>, <code>focus-visible</code>, <code>disabled</code> и <code>loading</code>. Он также фиксирует, что именно команда собирается смотреть. Но список не видит computed style, шрифт, перекрытый outline, порядок фокуса и фактический каскад.</p>\n<p>Для visual-проверки нужны страница, браузер, viewport, baseline, новый снимок, правило допустимого diff и сохранённый результат. Для keyboard-проверки нужна последовательность переходов фокуса. Для accessibility-проверки нужно сопоставить конкретный критерий с наблюдаемым результатом; одного зелёного статуса валидатора токенов недостаточно. Если запуск не выполнен, это следует назвать ограничением, а не превращать объявленный объём в факт.</p>\n<p>Нативная кнопка тоже требует отдельной проверки поведения. Атрибут <code>type</code> влияет на то, отправляет ли кнопка форму, сбрасывает её или не выполняет это действие. CSS-токен может изменить цвет disabled-состояния, но не должен незаметно решать, принимает ли control клик. Смысл, состояние и стиль связаны в интерфейсе, но принадлежат разным слоям контракта.</p>\n<h2>Порядок безопасной правки</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Запишите экран, компонент, состояние, ожидаемое и фактическое значение. Для focus ring укажите маршрут клавиатуры.</li><li><strong>Определите роль.</strong> Проверьте, что control действительно primary-кнопка. Для другого смысла нужен другой контракт.</li><li><strong>Соберите реестр.</strong> Найдите usages по имени, роли и literal-значению. Отделите подтверждённые места от предположений.</li><li><strong>Проверьте каскад.</strong> Найдите декларацию, область видимости, переопределения темы и запасной путь <code>var()</code>.</li><li><strong>Проверьте состояния.</strong> Пройдите клавиатурой <code>focus-visible</code>, затем проверьте <code>disabled</code> и <code>loading</code>. Обычный фон не заменяет эти состояния.</li><li><strong>Проверьте вход.</strong> Отклоните неизвестное имя и неверный формат до записи. Не добавляйте новый ключ как временное исключение без владельца.</li><li><strong>Сделайте одну правку.</strong> Сохраните <code>previousValue</code>, измените выбранную роль и запишите причину изменения.</li><li><strong>Проверьте отрицательные пути.</strong> Передайте неизвестное имя, неверное значение и отмену принятой правки. Ни один путь не должен менять baseline молча.</li><li><strong>Проверьте браузер.</strong> Соберите CSS, откройте нужные viewport, пройдите клавиатурой и сохраните снимок или иной воспроизводимый артефакт.</li></ol>\n<h2>Ограничения</h2>\n<p>Подход отвечает на узкий вопрос: существует ли роль, какие места и состояния она затрагивает и можно ли вернуть прежнее значение. Он не выбирает хороший дизайн, не вычисляет контраст и не заменяет проверку экранным диктором или браузером. Он также не решает автоматически локализацию, media queries, пользовательские настройки, конкурирующие изменения и различия между браузерами.</p>\n<p>Один общий токен не всегда лучше двух. Если профиль и оплата имеют разные требования к смыслу, состояниям или цене ошибки, разделите роли после проверки usages. Не объявляйте variant только потому, что один экран случайно выглядит иначе: сначала исключите literal, неверный каскад, неправильное состояние и ошибочное имя.</p>\n<p>Откат предложения не равен откату выпуска. Возврат пары <code>name → previousValue</code> не отменяет уже опубликованный CSS, сборку, кеш или действие пользователя. Для этих уровней нужны отдельные процедуры и артефакты. В этом примере rollback ограничен подготовленным решением и не притворяется операцией деплоя.</p>\n<h2>Критерий готовности</h2>\n<p>Правка готова, если команда может показать реестр затронутых мест, матрицу состояний и запись с прежним значением. Неизвестная роль и неверный формат останавливаются без изменения. Допустимая правка меняет только выбранный контракт. Отдельный браузерный запуск подтверждает нужные viewport и состояния сохранённым артефактом. Повторная проверка возвращает прежнее значение по <code>previousValue</code>, а не по памяти или догадке.</p>\n<p>Если visual- или accessibility-запуск ещё не выполнен, критерий должен это показывать. Для учебного примера достаточно запустить четыре утверждения JavaScript и проверить отрицательные входы. Для настоящего интерфейса добавьте DOM-проверку, keyboard route, критерии доступности и историю baseline. Только так можно отличить рабочую модель данных от доказанного поведения страницы.</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> — пользовательские CSS-свойства, наследование, каскад и подстановка через <code>var()</code>. Спецификация не назначает им продуктовую семантику.</li><li><a href='https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element' target='_blank' rel='noopener noreferrer'>WHATWG HTML Standard: The button element</a> — семантика кнопки и значения <code>type</code>, влияющие на её поведение в форме.</li><li><a href='https://www.w3.org/TR/WCAG22/' target='_blank' rel='noopener noreferrer'>W3C: Web Content Accessibility Guidelines (WCAG) 2.2</a> — проверяемые критерии доступности, включая видимый фокус и контраст. Пример выше не заявляет соответствие WCAG без отдельного запуска.</li></ul>"
}