8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"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>"
|
||
}
|