From 98a64abeb7bf220c31f3a041da834821dec5d865 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 31 Jul 2026 13:46:13 +0300 Subject: [PATCH] revise May 2022 design system articles --- editorial/production/README.md | 2 +- editorial/reviews/2022-05-draft.md | 69 ++++ web/data/editorial-revisions.mjs | 2 + .../design-system-component-contract-2022.svg | 35 ++ .../design-system-diagnosis-rollback-2022.svg | 32 ++ .../2022/design-system-token-flow-2022.svg | 37 ++ web/scripts/upgrade-2022-05.mjs | 315 ++++++++++++++++++ 7 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 editorial/reviews/2022-05-draft.md create mode 100644 web/public/assets/editorial/2022/design-system-component-contract-2022.svg create mode 100644 web/public/assets/editorial/2022/design-system-diagnosis-rollback-2022.svg create mode 100644 web/public/assets/editorial/2022/design-system-token-flow-2022.svg create mode 100644 web/scripts/upgrade-2022-05.mjs diff --git a/editorial/production/README.md b/editorial/production/README.md index 6ab4488..d1ea87c 100644 --- a/editorial/production/README.md +++ b/editorial/production/README.md @@ -1,6 +1,6 @@ # Производство редакционных партий -На 31 июля 2026 года строгий аудит проходит 154 из 358 созданных материалов. Остальные 204 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. +На 31 июля 2026 года строгий аудит проходит 157 из 358 созданных материалов. Остальные 201 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. ## Одна партия diff --git a/editorial/reviews/2022-05-draft.md b/editorial/reviews/2022-05-draft.md new file mode 100644 index 0000000..9f37f36 --- /dev/null +++ b/editorial/reviews/2022-05-draft.md @@ -0,0 +1,69 @@ +# П51 · 2022-05 · Маленькая дизайн-система — три прохода саморевью + +## Рамка пакета + +- Slug: `editorial-2022-05-practice-design-system`, `editorial-2022-05-mechanism-design-system`, `editorial-2022-05-field-design-system`. +- Голос: М5, системный практик 2022 года. Тема продолжает доступный control: внешняя форма, состояния и семантика должны иметь явный контракт, но не раздуваются до выдуманной универсальной платформы. +- Граница компетентности: в текстах нет существующей библиотеки, команды, production-дефекта, пользовательского исследования, скриншота, visual-regression запуска или результата accessibility-проверки. +- Fixture: детерминированные локальные объекты и массивы. `visualPayload` — объявленные входы будущей проверки, не screenshot, score или тестовый результат. DOM, браузер, Playwright, screen reader, CSS compilation, HTTP и реальные visual tests не запускаются. + +## Проход 1 — факты и техника + +- Историческая рамка сверена по датированным W3C-снимкам: [CSS Custom Properties Level 1, Candidate Recommendation Draft, 11.11.2021](https://www.w3.org/TR/2021/CRD-css-variables-1-20211111/), [WAI-ARIA 1.2, Candidate Recommendation Draft, 08.12.2021](https://www.w3.org/TR/2021/CRD-wai-aria-1.2-20211208/) и неизменяемой [WCAG 2.1 Recommendation, 05.06.2018](https://www.w3.org/TR/2018/REC-WCAG21-20180605/). Ссылки существуют до мая 2022 года; современные mutable docs не используются как доказательство прошлого состояния. +- Уточнена граница CSS Custom Properties: спецификация задаёт `--*` и `var()`, но не taxonomy дизайн-токенов. WAI-ARIA/WCAG не выданы за правила конкретного token API, инвентаря или visual pipeline. +- Fixture проверяет named tokens, пять required states, name/role/state, три именованных usage, declared payload с каждым required state, два invalid configuration и безопасную пару correction/rollback. В ней нет симуляции CSS, пикселей, accessibility tree или browser behavior. +- После финальной правки успешно выполнены `node --check web/scripts/upgrade-2022-05.mjs`, `node web/scripts/upgrade-2022-05.mjs --verify-fixture` (13/13) и `npm run audit:draft -- scripts/upgrade-2022-05.mjs`. + +## Проход 2 — редактура и голос + +- В первых двух абзацах каждой статьи есть конкретный симптом и цена: рассинхрон кнопок, разрыв владельцев contract или опасная общая правка token. +- Речь построена как «симптом → причина → проверка → действие». М5 проявляется в разделении слоёв, явном радиусе изменения, тестовой границе и обратимой правке, без роли автора как владельца большой дизайн-платформы. +- Во всех трёх статьях есть таблица, рисунок с содержательным `alt` и caption, исполнимый JS-пример, нумерованный маршрут, ограничение и следующий проверяемый шаг. +- Основной текст без источников проверен редакционным аудитом: 6 862 / 8 576 / 8 495 знаков — внутри диапазона 5 000–15 000. + +## Проход 3 — визуал и выпуск + +- SVG разделены по задаче: token flow, четыре слоя component contract и diagnosis с обратимой правкой. Каждый открыт после Sharp-рендера на ширине 375 px: заголовки, ключевые ветки, подписи и границы модели читаемы. +- XML-проверка прошла для всех трёх SVG. Safety scan подтвердил отсутствие `script`, `foreignObject`, внешних URL и `data:image`; изображения содержат только встроенную vector-разметку. +- Sidecar содержит ровно пять новых файлов. Registry, README, `articles.json`, QUALITY_STANDARD, очередь, `docs/`, Git и чужие sidecar-файлы не менялись. Интеграция и публикация намеренно не выполнялись. + +## Независимый редакторский приём + +### Проход 1 — фактчек и модель + +Проверка исторических ссылок обнаружила неверный URL WAI-ARIA: вариант с +суффиксом `20211209` возвращал `300`, хотя текст правильно называл документ +от 8 декабря. Ссылка заменена на +`CRD-wai-aria-1.2-20211208`; она, как и датированные CSS Custom Properties +CR Draft и WCAG 2.1 Recommendation, отвечает `200`. Статус WAI-ARIA не +повышен до будущей Recommendation. + +В code review обнаружен второй разрыв: `requiredStates` содержал пять +значений, а declared visual payload не включал `hover`. Payload дополнен +пятым state, fixture получила отдельный assertion о покрытии каждого +required state. Это по-прежнему только вход будущего visual test, а не +screenshot, diff или результат browser run. Fixture завершилась с **13/13 +assertions**. + +### Проход 2 — текст и полнота + +Повторно проверены problem/cost, таблицы, исполнимые примеры, ordered route, +rollback и ограничения трёх статей. Тексты не выдают CSS custom properties за +taxonomy токенов, а visual payload — за факт visual regression. Draft audit +подтверждает 6 862 / 8 576 / 8 495 знаков в требуемом диапазоне; import-safe +проверка вернула три revision-объекта без `date` и `author`. + +### Проход 3 — визуал и выпуск + +`xmllint` прошёл; safety scan не нашёл script, `foreignObject`, внешних URL +или `data:image`. Sharp-рендеры на 375 px просмотрены: token flow, четыре +слоя contract и correction/rollback отвечают на разные вопросы и явно +отделяют declared payload от реального результата. После подключения мая +registry содержит 148 ревизий. Строгий slug-audit прошёл с одной figure, +таблицей и code example на статью; production build успешно сгенерировал 374 +страницы. Материалы июня и позже не затрагиваются. + +## Финальный вердикт + +П51 принята и интегрирована после трёх независимых проходов. В коммит войдут +только пять файлов мая и два точечных файла интеграции. diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs index cd98bc9..6c629fa 100644 --- a/web/data/editorial-revisions.mjs +++ b/web/data/editorial-revisions.mjs @@ -47,6 +47,7 @@ import { revisions as january2022Revisions } from '../scripts/upgrade-2022-01.mj import { revisions as february2022Revisions } from '../scripts/upgrade-2022-02.mjs'; import { revisions as march2022Revisions } from '../scripts/upgrade-2022-03.mjs'; import { revisions as april2022Revisions } from '../scripts/upgrade-2022-04.mjs'; +import { revisions as may2022Revisions } from '../scripts/upgrade-2022-05.mjs'; // This layer replaces archived source entries without losing their stable slug and date. export const editorialRevisions = [ @@ -99,4 +100,5 @@ export const editorialRevisions = [ ...february2022Revisions, ...march2022Revisions, ...april2022Revisions, + ...may2022Revisions, ]; diff --git a/web/public/assets/editorial/2022/design-system-component-contract-2022.svg b/web/public/assets/editorial/2022/design-system-component-contract-2022.svg new file mode 100644 index 0000000..1272b9f --- /dev/null +++ b/web/public/assets/editorial/2022/design-system-component-contract-2022.svg @@ -0,0 +1,35 @@ + + Четыре слоя контракта компонента кнопки + Центральная primary button получает отдельные слои token names, required states, semantic name role state и usage inventory. Visual payload вынесен пунктиром, потому что не доказывает результат проверки. + + Контракт не смешивает четыре ответственности + + + Primary button + один control contract + + 1. Tokens + named values + не владеют state + + 2. States + default · hover + focus · disabled · loading + + 3. Semantics + name · role · state + declared, not observed + + 4. Inventory + known usage only + not repository search + + + + + + Visual payload + input for later test; no result + + Реальный DOM, browser tree и screenshot должны проверяться отдельным инструментом и отдельным артефактом. + diff --git a/web/public/assets/editorial/2022/design-system-diagnosis-rollback-2022.svg b/web/public/assets/editorial/2022/design-system-diagnosis-rollback-2022.svg new file mode 100644 index 0000000..f0b673b --- /dev/null +++ b/web/public/assets/editorial/2022/design-system-diagnosis-rollback-2022.svg @@ -0,0 +1,32 @@ + + Диагностика и обратимая правка токена кнопки + Инвентарь и контракт ведут к проверке входа. Недекларированный token и неверное значение останавливаются без изменения. Допустимая правка сохраняет предыдущее значение и может быть возвращена. Visual payload остаётся входом, а не результатом теста. + + Сначала остановить неверный вход, затем менять один token + + + Inventory + usage · state · owner + + Validate input + known token + value + + Invalid configuration + stop · changed: false · rollback not needed + + Token correction + save previous value for rollback + + Rollback applied + restore exact previous token value + + + + + + Visual payload + viewports · states · token names + declared input, not screenshot or regression PASS + + Граница модели: нет DOM, CSS compilation, HTTP, browser, screenshot или настоящего visual-regression запуска. + diff --git a/web/public/assets/editorial/2022/design-system-token-flow-2022.svg b/web/public/assets/editorial/2022/design-system-token-flow-2022.svg new file mode 100644 index 0000000..1c1ce59 --- /dev/null +++ b/web/public/assets/editorial/2022/design-system-token-flow-2022.svg @@ -0,0 +1,37 @@ + + Поток токенов малого контракта кнопки + Именованные токены переходят в контракт primary button, затем в известные точки использования. Visual payload справа объявляет параметры будущей проверки, но не является снимком или результатом теста. + + Один token → один понятный путь изменения + Малый контракт кнопки, май 2022 + + + Named tokens + button.primary + background + foreground + focus-ring + radius · gap + + Primary button contract + + Сохранить + states: default · hover + focus-visible · disabled + loading + name · role: button · state + + Usage inventory + profile-save + billing-pay + dialog-cancel + + Visual payload + 375 · 1280 · states + declared input, not screenshot + + + + + Граница модели: DOM, CSS compilation, browser и visual regression не запускались. + diff --git a/web/scripts/upgrade-2022-05.mjs b/web/scripts/upgrade-2022-05.mjs new file mode 100644 index 0000000..5d6aa5b --- /dev/null +++ b/web/scripts/upgrade-2022-05.mjs @@ -0,0 +1,315 @@ +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function paragraph(text) { return '

' + text + '

'; } +function heading(text) { return '

' + text + '

'; } +function codeBlock(lines) { return '
' + escapeHtml(Array.isArray(lines) ? lines.join('\n') : lines) + '
'; } +function orderedList(items) { return '
    ' + items.map((item) => '
  1. ' + item + '
  2. ').join('') + '
'; } +function figure(src, alt, caption) { + return '
' + alt + '
' + caption + '
'; +} +function dataTable(caption, headers, rows) { + const head = '' + headers.map((item) => '' + item + '').join('') + ''; + const body = '' + rows.map((row) => '' + row.map((item) => '' + item + '').join('') + '').join('') + ''; + return '
' + head + body + '
' + caption + '
'; +} +function sourceList(items) { + return ''; +} +function plainText(content) { + return content.replace(/<[^>]+>/g, ' ').replaceAll(' ', ' ').replaceAll('"', '"').replaceAll(''', "'").replaceAll('<', '<').replaceAll('>', '>').replaceAll('&', '&').replace(/\s+/g, ' ').trim(); +} +function bodyText(content) { + return plainText(content.replace(/

Проверяемые источники<\/h2>[\s\S]*?(?=

|$)/, '')); +} +function createRevision(meta, parts, sources) { + const contentHtml = parts.join('\n') + '\n' + heading('Проверяемые источники') + '\n' + sourceList(sources); + const proseLength = bodyText(contentHtml).length; + if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': основной текст вне диапазона: ' + proseLength); + return { ...meta, contentHtml, proseLength }; +} + +const cssVariables2021 = { + title: 'CSS Custom Properties for Cascading Variables Module Level 1, Candidate Recommendation Draft от 11 ноября 2021 года', + url: 'https://www.w3.org/TR/2021/CRD-css-variables-1-20211111/', + note: 'датированный нормативный снимок: custom properties имеют имена --* и подставляются через var(). Он не определяет taxonomy дизайн-токенов и не подтверждает результат visual regression.', +}; +const waiAria2021 = { + title: 'WAI-ARIA 1.2, Candidate Recommendation Draft от 8 декабря 2021 года', + url: 'https://www.w3.org/TR/2021/CRD-wai-aria-1.2-20211208/', + note: 'датированный нормативный снимок, доступный в мае 2022 года. Он описывает роли и состояния, но не выбирает за продукт токены, текст кнопки или набор variants.', +}; +const wcag21 = { + title: 'Web Content Accessibility Guidelines 2.1, Recommendation от 5 июня 2018 года', + url: 'https://www.w3.org/TR/2018/REC-WCAG21-20180605/', + note: 'неизменяемая W3C Recommendation с проверяемыми критериями, включая Keyboard, Focus Visible, Name/Role/Value и Non-text Contrast. Fixture пакета их не измеряет.', +}; +const commonSources = [cssVariables2021, waiAria2021, wcag21]; + +/** + * Детерминированная in-memory модель малого button contract. + * Она не создаёт DOM или CSS, не запускает браузер, Playwright, screen reader, + * CSS compilation, HTTP либо настоящий visual-regression test. visualPayload — + * только объявленные входные данные для будущей внешней проверки, не её результат. + */ +function createButtonSystem() { + return { + boundary: Object.freeze({ + dom: 'not-created', browser: 'not-run', playwright: 'not-run', screenReader: 'not-observed', + cssCompilation: 'not-run', http: 'not-run', visualRegression: 'not-run', + }), + tokens: { + 'button.primary.background': '#2457D6', + 'button.primary.foreground': '#FFFFFF', + 'button.primary.focus-ring': '#F0B429', + 'button.primary.radius': '8px', + 'button.primary.gap': '8px', + }, + requiredStates: Object.freeze(['default', 'hover', 'focus-visible', 'disabled', 'loading']), + contract: Object.freeze({ + name: 'Сохранить изменения', role: 'button', state: 'default', disabled: false, + permittedStates: Object.freeze(['default', 'hover', 'focus-visible', 'disabled', 'loading']), + }), + usageInventory: Object.freeze([ + Object.freeze({ id: 'profile-save', context: 'профиль', name: 'Сохранить изменения', role: 'button', state: 'default' }), + Object.freeze({ id: 'billing-pay', context: 'оплата', name: 'Оплатить счёт', role: 'button', state: 'loading' }), + Object.freeze({ id: 'dialog-cancel', context: 'диалог', name: 'Отмена', role: 'button', state: 'default' }), + ]), + visualPayload: Object.freeze({ + kind: 'declared-visual-regression-payload-v1', + observation: 'not-a-screenshot-not-a-test-result', + viewports: Object.freeze([375, 1280]), + states: Object.freeze(['default', 'hover', 'focus-visible', 'disabled', 'loading']), + tokenNames: Object.freeze(['button.primary.background', 'button.primary.foreground', 'button.primary.focus-ring', 'button.primary.radius', 'button.primary.gap']), + }), + }; +} + +function checkContract(system) { + const missingTokens = system.visualPayload.tokenNames.filter((name) => !(name in system.tokens)); + const missingStates = system.requiredStates.filter((state) => !system.contract.permittedStates.includes(state)); + const unnamedUsage = system.usageInventory.filter((item) => !item.name || item.role !== 'button'); + return Object.freeze({ ok: missingTokens.length === 0 && missingStates.length === 0 && unnamedUsage.length === 0, missingTokens, missingStates, unnamedUsage }); +} + +function applyTokenCorrection(system, change) { + if (!change || typeof change.name !== 'string' || typeof change.value !== 'string') { + return Object.freeze({ ok: false, kind: 'invalid-configuration', reason: 'token-name-and-string-value-required', changed: false }); + } + if (!(change.name in system.tokens)) { + return Object.freeze({ ok: false, kind: 'invalid-configuration', reason: 'undeclared-token', changed: false, rollback: 'not-needed' }); + } + if (!/^#[0-9A-F]{6}$/i.test(change.value) && !/^\d+px$/.test(change.value)) { + return Object.freeze({ ok: false, kind: 'invalid-configuration', reason: 'unsupported-teaching-token-value', changed: false, rollback: 'not-needed' }); + } + const before = system.tokens[change.name]; + system.tokens[change.name] = change.value; + return Object.freeze({ ok: true, kind: 'token-corrected', changed: before !== change.value, rollback: Object.freeze({ name: change.name, value: before }) }); +} + +function rollbackTokenCorrection(system, rollback) { + if (!rollback || !(rollback.name in system.tokens) || typeof rollback.value !== 'string') { + return Object.freeze({ ok: false, kind: 'invalid-rollback', changed: false }); + } + const before = system.tokens[rollback.name]; + system.tokens[rollback.name] = rollback.value; + return Object.freeze({ ok: true, kind: 'rollback-applied', changed: before !== rollback.value, restored: rollback.value }); +} + +function runDesignSystemFixture() { + const system = createButtonSystem(); + const initial = checkContract(system); + const invalid = applyTokenCorrection(system, { name: 'button.primary.shadow', value: '#111111' }); + const wrongValue = applyTokenCorrection(system, { name: 'button.primary.background', value: 'brand-blue' }); + const correction = applyTokenCorrection(system, { name: 'button.primary.background', value: '#1D4ED8' }); + const correctedValue = system.tokens['button.primary.background']; + const rollback = rollbackTokenCorrection(system, correction.rollback); + const assertions = Object.freeze({ + modelDoesNotCreateDom: system.boundary.dom === 'not-created', + modelDoesNotRunBrowserOrPlaywright: system.boundary.browser === 'not-run' && system.boundary.playwright === 'not-run', + modelDoesNotClaimAccessibilityObservation: system.boundary.screenReader === 'not-observed', + visualPayloadIsNotTestResult: system.visualPayload.observation === 'not-a-screenshot-not-a-test-result' && system.boundary.visualRegression === 'not-run', + visualPayloadDeclaresEveryRequiredState: system.requiredStates.every((state) => system.visualPayload.states.includes(state)), + namedTokensExist: initial.missingTokens.length === 0 && Object.keys(system.tokens).length === 5, + requiredStatesAreDeclared: JSON.stringify(system.requiredStates) === JSON.stringify(['default', 'hover', 'focus-visible', 'disabled', 'loading']), + semanticContractHasNameRoleAndState: system.contract.name === 'Сохранить изменения' && system.contract.role === 'button' && system.contract.state === 'default', + usageInventoryHasThreeNamedButtons: system.usageInventory.length === 3 && initial.unnamedUsage.length === 0, + invalidUndeclaredTokenDoesNotChangeSystem: invalid.ok === false && invalid.reason === 'undeclared-token' && !('button.primary.shadow' in system.tokens), + invalidValueDoesNotChangeToken: wrongValue.ok === false && system.tokens['button.primary.background'] === '#2457D6', + validCorrectionIsExplicit: correction.ok === true && correction.kind === 'token-corrected' && correctedValue === '#1D4ED8', + rollbackRestoresPriorToken: rollback.ok === true && system.tokens['button.primary.background'] === '#2457D6', + }); + return Object.freeze({ system, initial, invalid, wrongValue, correction, rollback, assertions }); +} + +const fixtureCommand = ['node web/scripts/upgrade-2022-05.mjs --verify-fixture', '// PASS fixture: 13/13 assertions', '', '// Проверяется локальный contract: tokens, states, semantic fields, inventory,', '// declared visual payload, invalid configuration и rollback. Это не UI test.'].join('\n'); +const contractExample = ['const system = createButtonSystem();', 'const review = checkContract(system);', '', 'console.log(review.ok); // true', 'console.log(system.contract); // { name, role, state, disabled, permittedStates }', '', '// Никакой button в DOM не создан; поля описывают проектный contract.'].join('\n'); +const rollbackExample = ['const system = createButtonSystem();', 'const result = applyTokenCorrection(system, {', ' name: "button.primary.background",', ' value: "#1D4ED8",', '});', 'rollbackTokenCorrection(system, result.rollback);', '', '// background снова "#2457D6". Это rollback локального объекта,', '// а не отмена CSS build, deploy или результата visual-regression.'].join('\n'); + +const practiceArticle = createRevision( + { + slug: 'editorial-2022-05-practice-design-system', + title: 'Маленькая дизайн-система: начать с контракта кнопки, а не с каталога компонентов', + categories: ['Frontend', 'Качество'], + cover: '/assets/editorial/2022/design-system-token-flow-2022.svg', + excerpt: 'Практический маршрут для одинаковых кнопок, которые разошлись по цвету, состояниям и семантике: named tokens, один минимальный contract, usage inventory и обратимая правка.', + readingMinutes: 12, + }, + [ + paragraph('Три одинаковые на вид кнопки редко ломаются одновременно. Одна берёт синий цвет из локального файла, вторая не показывает фокус, третья на disabled меняет только opacity, а четвёртая вместо понятного имени имеет иконку. Симптом кажется косметическим, пока пользователь не попадает в другой сценарий. Цена — каждый новый экран получает ещё один почти такой же control, а исправление цвета начинает менять поведение там, где его не ожидали.'), + paragraph('В мае 2022 года я бы не начинал с «универсальной дизайн-системы». Сначала нужен узкий контракт одной primary button: именованные токены, обязательные состояния, семантическое имя, роль и список реальных мест использования. Это продолжает предыдущую статью о доступном control: внешний вид и name/role/state нельзя держать в разных случайных ветках. Пример ниже — локальная fixture, не DOM, не CSS и не visual regression test; он проверяет только объявленные данные.'), + heading('Выбрать границу: одна кнопка, а не вся библиотека'), + paragraph('Минимальная система отвечает на вопрос «какой button contract должны разделять эти три места», а не «как описать любой интерфейс». В неё входят пять токенов: background, foreground, focus ring, radius и gap. В неё входят пять состояний: default, hover, focus-visible, disabled и loading. Не все состояния обязаны выглядеть одинаково в каждом продукте, но их отсутствие не должно быть случайностью. Если loading невозможен для действия без сети, это решение нужно записать в contract, а не скрыть в одном компоненте.'), + dataTable('Минимальный контракт primary button', ['Часть', 'Симптом без неё', 'Проверяемый факт', 'Действие'], [ + ['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 и повторять проверку мест'], + ]), + heading('Токен — имя решения, а не переменная ради переменной'), + paragraph('CSS Custom Properties допускает author-defined properties с префиксом -- и подстановку через var(). Это полезный механизм, но он не создаёт за команду словарь design tokens. Имя button.primary.background в этой статье — соглашение пакета: оно говорит, что значение относится к роли primary button, а не ко всем синим пикселям проекта. Поэтому не стоит сразу делать brand.blue.500 единственным входом для компонента: у роли должна быть собственная граница, даже если сегодня она ссылается на тот же цвет.'), + paragraph('Проверка проста: у каждой величины есть имя, владелец и место потребления. Если в pull request появляется #2457D6 рядом с кнопкой, сначала спросите, это новый token или обход существующего. Если ответ «временно», зафиксируйте срок и конкретный rollback. Не нужно объявлять каждую тень и каждый margin глобальным token. Глобальность оправдана только повторяемым contract; одиночная геометрия остаётся локальной, пока не появится второй подтверждённый use case.'), + heading('Учебная fixture: проверить данные до сборки CSS'), + paragraph('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 не запускались.'), + codeBlock(fixtureCommand), + paragraph('Такой тест ловит дешёвую ошибку раньше рендера: кто-то добавил usage без имени, убрал loading из состояния или стал использовать token, которого contract не объявляет. Он не ловит контраст на реальном фоне, порядок клавиатуры, cascade в существующем CSS или изменение пикселей на устройстве. Это разные проверки. Их полезно добавлять следующими, но нельзя дорисовывать их результат к локальному объекту числом score или словом «доступно».'), + figure('/assets/editorial/2022/design-system-token-flow-2022.svg', 'Поток малого button contract: пять named tokens поступают в компонент primary button с пятью обязательными состояниями; затем usage inventory перечисляет профиль, оплату и диалог; справа visual payload объявляет viewports и states, но помечен как не являющийся screenshot или test result.', 'Схема отделяет источник значения, contract компонента и будущий вход visual-проверки. Между ними нет выдуманного production-результата.'), + heading('Маршрут: симптом → причина → проверка → действие'), + orderedList([ + 'Симптом. Найдите одну повторяющуюся кнопку, у которой расходятся color, focus или label. Не группируйте сразу все controls.', + 'Причина. Выпишите, где лежат literal values, состояния и semantic fields. Обычно они принадлежат разным локальным файлам без общего contract.', + 'Проверка. Соберите usage inventory: context, name, role, состояние, локальные overrides. Затем запустите fixture и убедитесь, что declared payload не называют результатом visual test.', + 'Действие. Внесите один named token и одну state matrix для primary button. Оставьте native button там, где не требуется другой host.', + 'Откат. Сохраните прежнее значение token до правки. Если один usage изменился неожиданно, верните только token и разберите его локальный override.', + 'Следующая проверка. После contract запустите отдельную реальную visual и a11y-проверку в согласованной среде. Её артефакт должен содержать версии и наблюдения.', + ]), + heading('Где маленький contract заканчивается'), + paragraph('Этот подход не выбирает типографику бренда, не строит темизацию, не мигрирует legacy CSS и не заменяет дизайн-ревью. Он также не доказывает WCAG-conformance: WCAG содержит проверяемые критерии, но локальная fixture не наблюдает страницу. Числа, hex-значения и названия из примера — учебные проектные решения. В другом продукте focus ring может иметь другое имя и значение; важнее, чтобы его существование и ответственность были явными.'), + paragraph('Следующий проверяемый шаг — выбрать три настоящих usage одной primary button, составить inventory до изменения и договориться о минимальном payload для внешнего visual review. Если один usage требует другого состояния или семантики, не расширяйте contract по умолчанию. Сначала зафиксируйте причину: это variant той же кнопки или другой control. Такой вопрос экономит больше времени, чем ранний каталог из двадцати компонентов.'), + heading('Историческая граница мая 2022'), + paragraph('Текст опирается на 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 существовали в конкретной команде.'), + ], commonSources, +); + +const mechanismArticle = createRevision( + { + slug: 'editorial-2022-05-mechanism-design-system', + title: 'Контракт кнопки: как связать токены, состояния и доступную семантику', + categories: ['Frontend', 'Архитектура'], + cover: '/assets/editorial/2022/design-system-component-contract-2022.svg', + excerpt: 'Разбор малого component contract: какие данные принадлежат токенам, состояниям, семантике и usage inventory, а какие нельзя подменять модельным visual payload.', + readingMinutes: 13, + }, + [ + paragraph('Кнопка начинает расходиться не потому, что в ней много CSS. Обычно один код владеет className, другой — disabled, третий — текстом, а четвёртый копирует цвет. Симптом проявляется после безопасной на вид правки: новая loading-версия смотрится правильно, но action уже доступен для повторного запуска; focus ring пропадает в одном варианте; иконка получает label только в profile. Цена — review видит фрагменты, а пользователь получает разный contract для одного знакомого действия.'), + paragraph('Минимальный component contract собирает эти фрагменты в данные: token names отвечают за значения, required states — за допустимые ветки, semantic fields — за смысл control, usage inventory — за известный радиус изменения. Это не универсальная система и не готовая React API. В учебном скрипте нет JSX, DOM, CSS cascade и assistive technology. Он лишь показывает, как проверить, что один договор не пропустил обязательную часть до того, как команда начнёт спорить о структуре библиотек.'), + heading('Четыре владельца вместо одного большого объекта'), + paragraph('У contract есть четыре слоя. Первый — token layer: он знает только именованные значения и не должен решать, в каком состоянии находится кнопка. Второй — state layer: default, hover, focus-visible, disabled и loading; он определяет, какие ветки продукт обязан обсудить. Третий — semantic layer: name, role, state и disabled. Он не выводится из цвета, потому что одинаковый серый может означать disabled, loading или ошибочно применённый style. Четвёртый — inventory: он хранит известные точки применения и не выдаёт себя за поиск по всему репозиторию.'), + dataTable('Границы малого component contract', ['Слой', 'Владеет', 'Не доказывает', 'Нужная внешняя проверка'], [ + ['Tokens', 'имена и значения background, foreground, focus ring, radius, gap', 'что все pixels в браузере обновились', 'собранный CSS и visual diff в выбранной среде'], + ['States', 'разрешённые default/hover/focus-visible/disabled/loading', 'что browser реально получил hover или focus', 'ручной keyboard/mouse scenario или автоматизация'], + ['Semantics', 'declared name, role button, declared state', 'что screen reader произнёс ожидаемую фразу', 'проверка DOM/accessibility tree и выбранной технологии'], + ['Inventory', 'три явно перечисленных usage', 'что больше usage не существует', 'поиск в кодовой базе и review migration'], + ['Visual payload', 'viewports, states и token names для будущего снимка', 'реальный screenshot, diff, score или regression', 'настоящий visual-regression runner с сохранённым артефактом'], + ]), + heading('Почему CSS variable не является semantic token автоматически'), + paragraph('Спецификация CSS Custom Properties говорит о custom properties и подстановке var(). Она не назначает им смысл. Поэтому --button-primary-background может быть технически валиден, но неясен как договор, если никто не определил, для какой роли он существует, кто меняет его значение и какие variants от него зависят. Обратная ошибка тоже частая: назвать произвольное значение «semantic» и считать, что оно должно жить в глобальном файле. Семантика появляется не в пунктуации имени, а в повторяемом решении и известном owner.'), + paragraph('Для маленькой системы полезен направленный путь: button.primary.background → primary button → конкретные usage. Он позволяет отдельно решить, как component получает CSS variable. В одном коде это может быть custom property, в другом объект theme, в третьем stylesheet. Contract не требует выбрать транспорт заранее. Он требует не потерять связь между ролью значения и точками, на которые повлияет изменение. Такая граница оставляет миграцию обратимой.'), + heading('Name, role и state — не оформление'), + paragraph('WAI-ARIA 1.2 описывает роли, состояния и свойства для интерфейсных объектов. Для простой кнопки лучший путь обычно начинается с native button: браузер уже знает базовую keyboard-модель. Если вместо него нужен custom host, команда обязана явно воспроизвести поведение, а не только написать role="button". Однако даже native host не решает вопрос имени: «Сохранить изменения» и иконка без text alternative не равны для человека, который не видит layout.'), + paragraph('В модели semantic contract намеренно мал: name, role, state, disabled и список permitted states. Поле state — declared data, не прочитанное browser tree. Такое различие нужно сохранить в тексте review: fixture может показать, что state loading предусмотрен в contract, но не может сказать, что конкретный browser запретил повторный click или что screen reader сообщил disabled. Для этого понадобятся платформенные тесты, которых здесь нет.'), + heading('Исполнимый пример: проверить contract без UI'), + paragraph('Функция checkContract сопоставляет required states с permitted states, проверяет, что payload не ссылается на отсутствующий token, и что каждое известное usage имеет name и роль button. Она не ищет реальные файлы и не запускает построение styles. Именно поэтому результат true означает «данные учебной модели согласованы», а не «кнопка доступна и визуально одинакова». Это узкое утверждение легко повторить и трудно неверно истолковать.'), + codeBlock(contractExample), + figure('/assets/editorial/2022/design-system-component-contract-2022.svg', 'Схема component contract primary button: слева перечислены named tokens, сверху обязательные states, справа semantic fields name/role/state, снизу usage inventory. Пунктирная рамка visual payload показывает viewports и snapshots как объявленные входы, а не проверенный результат.', 'Компонент получает несколько независимых видов данных. Схема показывает границы ответственности, чтобы token, состояние и семантика не превращались в один неразличимый объект.'), + heading('Маршрут: симптом → причина → проверка → действие'), + orderedList([ + 'Симптом. Запишите один разрыв: кнопка теряет focus, loading не блокирует повтор, label отличается в одинаковом действии или literal color появился в новом usage.', + 'Причина. Разложите изменение по четырём владельцам. Если token пытается хранить state, а CSS class несёт name, граница уже размыта.', + 'Проверка contract. Сверьте required states, token names, semantic fields и inventory. Запустите --verify-fixture; он должен отклонить undeclared token и неверное значение.', + 'Проверка платформы. Отдельно создайте реальный control и пройдите согласованный keyboard/mouse/a11y сценарий. Результат сохраните как новый артефакт, не внутри model.', + 'Действие. Сначала поменяйте один owner: вынесите literal в named token, добавьте state или верните native host. Не объединяйте это с переписыванием всей библиотеки.', + 'Откат. Применяйте token correction с сохранённым previous value. Если inventory показывает неожиданный effect, откатите малый change и сузьте variant.', + ]), + heading('Против ложной универсальности'), + paragraph('Слово «Button» не делает все действия одним компонентом. Link-like navigation, destructive confirmation, toggle, split button и async submit имеют разные риски. У них могут совпадать radius и gap, но не обязательно name, behavior или state matrix. Универсальный API, который принимает двадцать optional props ради такого сходства, обычно скрывает больше решений, чем экономит. Малый contract ценнее, когда он допускает честный ответ: этот control пока не входит в primary button.'), + paragraph('Не стоит и использовать fixture как gate для чужого продукта. Её inventory полностью создан внутри примера, а values выбраны для объяснения. В настоящем проекте сначала нужно получить существующие usage и владельца правила. Затем выбрать, какие values public, какие variants поддерживаются, как маркируется deprecation и кто проводит visual review. Это следующий слой T-shape автора 2022 года: не говорить за процесс, которого не наблюдали, а назвать факт, которым можно проверить изменение.'), + heading('Ограничение и следующий проверяемый шаг'), + paragraph('Модель не меряет contrast, не запускает CSS, не сравнивает image pixels, не читает accessibility tree и не знает всех компонентов в архиве. WCAG 2.1 не разрешает заменить сочетание автоматической и ручной оценки полем role в JavaScript. Поэтому в материале нет claim о доступности или visual stability. Здесь есть только контракт данных, который уменьшает шанс забыть state или нечаянно изменить token без пути назад.'), + paragraph('Следующий проверяемый шаг — выбрать один production-like component в отдельном репозитории, составить его реальный inventory и записать маленький test plan: browser/version, viewport, states, keyboard route и ожидаемый результат. После первого наблюдения можно привязать к contract настоящий screenshot или accessibility-tree artifact. До этого visual payload должен оставаться честным списком входов, а не «зелёным» score.'), + heading('Историческая граница мая 2022'), + paragraph('Здесь использованы датированные версии, существовавшие до мая 2022 года: CSS Custom Properties CR Draft 11.11.2021 и WAI-ARIA 1.2 CR Draft 08.12.2021; WCAG 2.1 Recommendation опубликована в 2018-м. Фразы о native button и roles относятся к нормативным моделям документов. Слои contract, token names и fixture — решения этого учебного пакета, не цитата из существующей библиотеки.'), + ], commonSources, +); + +const fieldArticle = createRevision( + { + slug: 'editorial-2022-05-field-design-system', + title: 'Правка токена без сюрпризов: инвентарь кнопок, диагностика и обратимый шаг', + categories: ['Frontend', 'Тестирование'], + cover: '/assets/editorial/2022/design-system-diagnosis-rollback-2022.svg', + excerpt: 'Диагностический маршрут для изменения малого design-system contract: отличить неверную конфигурацию от допустимой правки, не назвать payload результатом visual test и оставить безопасный rollback.', + readingMinutes: 13, + }, + [ + paragraph('Самая дорогая правка design token часто выглядит как одна строка. Например, цвет primary button меняют, чтобы исправить один экран, а после merge другой сценарий теряет ожидаемый contrast или focus ring. Другой симптом: в новом usage написали удобное имя token, которого система не знает, и оно тихо живёт рядом с исходным. Цена не в самом hex-значении. Команда перестаёт понимать, какой change общий, какой локальный и как вернуть предыдущее состояние без отката чужих исправлений.'), + paragraph('Диагностика начинается с факта, а не с косметического решения. Нужно назвать usage, state и значение, которые расходятся; затем проверить, существует ли token в контракте, относится ли он к роли button и какие известные места затронет правка. Эта статья не показывает production regression и не делает скриншоты. Она использует локальную модель с deliberate invalid configuration, declared visual payload и обратимым correction. Поэтому выводы ограничены данными модели, а не реальными устройствами или пользовательскими исследованиями.'), + heading('Пять причин одинакового визуального симптома'), + dataTable('Диагностика разрыва малого design-system contract', ['Наблюдение', 'Вероятная причина', 'Минимальный факт', 'Обратимое действие'], [ + ['В одном экране другой синий', 'literal value обошёл named token', 'значение не ссылается на button.primary.background', 'вынести только это usage на существующий token и проверить inventory'], + ['Focus исчез после refactor', 'focus-visible отсутствует в required states', 'state matrix не содержит focus-visible или не проходит к payload', 'добавить state в contract до CSS-правки'], + ['Новая кнопка неясна без иконки', 'name и role добавили после visual слоя', 'usage inventory содержит пустой name либо role не button', 'задать semantic fields и проверить native host'], + ['Visual review назван успешным без артефакта', 'payload спутали с фактическим запуском', 'есть viewports, но boundary говорит visualRegression not-run', 'создать отдельный реальный job и хранить его output отдельно'], + ['Правка задела платёжный экран', 'изменили общий token без inventory', 'profile-save и billing-pay используют одну роль', 'вернуть previous value, затем выделить variant только по подтверждённой причине'], + ]), + heading('Сначала построить маленький радиус изменения'), + paragraph('Usage inventory — не список «всех кнопок мира». Это честная таблица того, что известно перед правкой: profile-save, billing-pay, dialog-cancel; контекст, name, role, state. Она делает две вещи. Во-первых, reviewer видит возможный blast radius токена. Во-вторых, команда замечает, когда похожий control на самом деле отличается: cancel может не быть primary button, а payment в loading нуждается в дополнительном поведенческом contract. В этом случае не надо включать его ради красивого числа usage.'), + paragraph('Инвентарь полезен и при поиске. Сначала ищут известные component entry points и literal values, затем вручную классифицируют найденное. Автоматический поиск не понимает, что text link стилизован под button или что label появляется после локализации. Поэтому результат поиска — вход в review, не доказательство полноты. В fixture inventory задан вручную и прямо помечен как учебный; он не создаёт ложного claim, что репозиторий просканирован.'), + heading('Invalid configuration должна останавливаться до изменения'), + paragraph('У модели есть два плохих входа. Первый пытается поменять button.primary.shadow, хотя такого named token нет. Второй пытается записать в background строку brand-blue, хотя учебный validator принимает только #RRGGBB или целые px. Оба входа возвращают invalid-configuration и не меняют объект. Это не полный CSS parser и не политика production token format. Это маленькая защита против тихого расширения contract в процессе срочной правки.'), + paragraph('Когда допустимая правка всё же нужна, функция сохраняет прежнее значение рядом с результатом. Тогда rollback не «угадывает» цвет из истории, а применяет конкретную пару name/value. Это полезный минимальный инвариант: неожиданный effect можно убрать небольшим обратным действием. Он не равен откату релиза, git revert, CSS build или компенсации серверного платежа. В статье о кнопке достаточно не потерять собственное предыдущее token value; более широкий rollback требует отдельного процесса и артефактов.'), + heading('Исполнимый пример: correction и возврат'), + codeBlock(rollbackExample), + paragraph('После correction значение background становится #1D4ED8, после rollback — снова #2457D6. Fixture дополнительно проверяет, что недекларированный token не появился в объекте и неверная строка не изменила baseline. Это позволяет отделить две причины. Если правка отвергнута — сначала договоритесь о расширении contract. Если она принята, но usage ведёт себя иначе — проблема в радиусе применения или variant, а не в том, что validator обязан был сам выбрать дизайн.'), + figure('/assets/editorial/2022/design-system-diagnosis-rollback-2022.svg', 'Диагностическая схема: inventory ведёт к проверке named token и required state. Недекларированный token или неверное значение останавливаются без изменения; допустимая смена background сохраняет previous value и может быть возвращена. Отдельный блок visual payload помечен как объявление входов без запуска visual regression.', 'Диаграмма показывает обратимый путь: сначала остановить неверную конфигурацию, затем менять один известный token и хранить точное значение для возврата.'), + heading('Маршрут: симптом → причина → проверка → действие'), + orderedList([ + 'Симптом. Зафиксируйте один экран, state и значение: например, primary button в loading использует другой background или не имеет focus-visible.', + 'Причина. Проверьте, это literal, неизвестный token, отсутствующий state или другой component role. Не лечите все варианты одним global rename.', + 'Инвентарь. Выпишите известные usage с name, role и state. Отделите подтверждённые места от предположений из поиска.', + 'Проверка модели. Запустите node web/scripts/upgrade-2022-05.mjs --verify-fixture. Она должна отклонить invalid configuration и восстановить previous value после rollback.', + 'Действие. Внесите один допустимый token correction или заведите отдельный variant после review. Не добавляйте undeclared key как «быстрое исключение».', + 'Проверка платформы. В отдельном реальном запуске проверьте собранный CSS, нужные viewports, states и accessibility scenario. Сохраните screenshot/diff/версии там, где это действительно выполнялось.', + ]), + heading('Visual payload — это очередь работы, не доказательство'), + paragraph('Payload перечисляет 375 и 1280, состояния default/hover/focus-visible/disabled/loading и пять token names. Такой формат полезен: он заставляет заранее назвать, что именно должен покрыть будущий visual check. Но пустой payload не видит pixels, а заполненный payload не знает, как конкретный browser применил cascade. Не стоит добавлять к нему synthetic score, ticks или «green» статус. Эти числа создают видимость измерения и затем мешают найти реальный артефакт, когда change нужно объяснить.'), + paragraph('Для настоящей visual-regression проверки понадобится другой слой: stable fixture page, поддерживаемый browser/version, viewport, screenshot baseline, правило допустимого diff и путь к output. Для доступности нужны ещё keyboard route и выбранная комбинация технологий. В 2022 году это уже нормальная инженерная дисциплина, но она начинается с честной границы. Нельзя заявить, что control проверен, только потому что его token names красиво лежат в JSON-like объекте.'), + heading('Ограничение и следующий проверяемый шаг'), + paragraph('Учебная модель намеренно не знает CSS inheritance, media queries, dark theme, locale, permissions, сетевой submit и всех usage в кодовой базе. Она не вычисляет contrast и не определяет, будет ли кнопка удобна. WAI-ARIA и WCAG помогают сформулировать нормы для семантики и доступности, но не превращают token correction в универсальное решение. Если один продукт требует destructive action или progress indicator, ему нужен отдельный contract, а не новый optional flag в primary button без обсуждения.'), + paragraph('Следующий проверяемый шаг — выбрать одну фактическую правку и оформить короткий change record: исходный token, причина, inventory до правки, expected states, previous value для rollback и ссылка на настоящий visual/a11y result после запуска. Если этой ссылки пока нет, record должен так и говорить. Такой скромный документ удерживает границу между планом и наблюдением, а затем позволяет расширять малую систему только по повторяющимся доказанным случаям.'), + heading('Историческая граница мая 2022'), + paragraph('Нормативные ссылки зафиксированы датами до мая 2022 года: CSS Custom Properties Candidate Recommendation Draft 11 ноября 2021 года, WAI-ARIA 1.2 Candidate Recommendation Draft 8 декабря 2021 года и WCAG 2.1 Recommendation 2018 года. Они не описывают данный inventory, token format или rollback API. Это учебные решения пакета; настоящие visual и accessibility результаты здесь сознательно не заявляются.'), + ], commonSources, +); + +export const revisions = [practiceArticle, mechanismArticle, fieldArticle].map(({ proseLength, ...revision }) => revision); + +function verifyFixture() { + const fixture = runDesignSystemFixture(); + const failed = Object.entries(fixture.assertions).filter(([, passed]) => passed !== true).map(([name]) => name); + if (failed.length > 0) { + console.error('FAIL fixture: ' + failed.join(', ')); + process.exitCode = 1; + return; + } + console.log('PASS fixture: ' + Object.keys(fixture.assertions).length + '/' + Object.keys(fixture.assertions).length + ' assertions'); +} + +if (process.argv.includes('--verify-fixture')) verifyFixture(); +if (process.argv.includes('--print-revisions')) console.log(JSON.stringify(revisions));