This commit is contained in:
@@ -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,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Четыре слоя контракта компонента кнопки</title>
|
||||
<desc id="desc">Центральная primary button получает отдельные слои token names, required states, semantic name role state и usage inventory. Visual payload вынесен пунктиром, потому что не доказывает результат проверки.</desc>
|
||||
<rect width="1200" height="720" fill="#F7F9FC"/>
|
||||
<text x="65" y="78" fill="#102A43" font-family="Arial, sans-serif" font-size="34" font-weight="700">Контракт не смешивает четыре ответственности</text>
|
||||
<g font-family="Arial, sans-serif">
|
||||
<rect x="430" y="250" width="340" height="190" rx="24" fill="#2457D6"/>
|
||||
<text x="510" y="315" fill="#FFFFFF" font-size="30" font-weight="700">Primary button</text>
|
||||
<text x="493" y="358" fill="#D9E9FF" font-size="21">один control contract</text>
|
||||
<rect x="70" y="165" width="275" height="170" rx="18" fill="#E6F0FF" stroke="#2457D6" stroke-width="3"/>
|
||||
<text x="102" y="213" fill="#102A43" font-size="24" font-weight="700">1. Tokens</text>
|
||||
<text x="102" y="254" fill="#243B53" font-size="18">named values</text>
|
||||
<text x="102" y="286" fill="#243B53" font-size="18">не владеют state</text>
|
||||
<rect x="855" y="165" width="275" height="170" rx="18" fill="#E8FFF6" stroke="#1F9D73" stroke-width="3"/>
|
||||
<text x="887" y="213" fill="#102A43" font-size="24" font-weight="700">2. States</text>
|
||||
<text x="887" y="254" fill="#243B53" font-size="18">default · hover</text>
|
||||
<text x="887" y="286" fill="#243B53" font-size="18">focus · disabled · loading</text>
|
||||
<rect x="70" y="465" width="275" height="170" rx="18" fill="#F4E9FF" stroke="#805AD5" stroke-width="3"/>
|
||||
<text x="102" y="513" fill="#102A43" font-size="24" font-weight="700">3. Semantics</text>
|
||||
<text x="102" y="554" fill="#243B53" font-size="18">name · role · state</text>
|
||||
<text x="102" y="586" fill="#243B53" font-size="18">declared, not observed</text>
|
||||
<rect x="855" y="465" width="275" height="170" rx="18" fill="#FFF6DD" stroke="#D69E2E" stroke-width="3"/>
|
||||
<text x="887" y="513" fill="#102A43" font-size="24" font-weight="700">4. Inventory</text>
|
||||
<text x="887" y="554" fill="#243B53" font-size="18">known usage only</text>
|
||||
<text x="887" y="586" fill="#243B53" font-size="18">not repository search</text>
|
||||
<path d="M345 250H415" stroke="#2457D6" stroke-width="5"/><path d="M405 237l20 13-20 13" fill="#2457D6"/>
|
||||
<path d="M855 250H785" stroke="#1F9D73" stroke-width="5"/><path d="M795 237l-20 13 20 13" fill="#1F9D73"/>
|
||||
<path d="M345 550H415" stroke="#805AD5" stroke-width="5"/><path d="M405 537l20 13-20 13" fill="#805AD5"/>
|
||||
<path d="M855 550H785" stroke="#D69E2E" stroke-width="5"/><path d="M795 537l-20 13 20 13" fill="#D69E2E"/>
|
||||
<rect x="438" y="510" width="324" height="94" rx="14" fill="#FFFFFF" stroke="#486581" stroke-width="2" stroke-dasharray="8 7"/>
|
||||
<text x="472" y="550" fill="#243B53" font-size="20" font-weight="700">Visual payload</text>
|
||||
<text x="472" y="581" fill="#486581" font-size="16">input for later test; no result</text>
|
||||
</g>
|
||||
<text x="65" y="682" fill="#486581" font-family="Arial, sans-serif" font-size="18">Реальный DOM, browser tree и screenshot должны проверяться отдельным инструментом и отдельным артефактом.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Диагностика и обратимая правка токена кнопки</title>
|
||||
<desc id="desc">Инвентарь и контракт ведут к проверке входа. Недекларированный token и неверное значение останавливаются без изменения. Допустимая правка сохраняет предыдущее значение и может быть возвращена. Visual payload остаётся входом, а не результатом теста.</desc>
|
||||
<rect width="1200" height="720" fill="#F7F9FC"/>
|
||||
<text x="65" y="76" fill="#102A43" font-family="Arial, sans-serif" font-size="34" font-weight="700">Сначала остановить неверный вход, затем менять один token</text>
|
||||
<g font-family="Arial, sans-serif">
|
||||
<rect x="65" y="175" width="210" height="110" rx="16" fill="#E6F0FF" stroke="#2457D6" stroke-width="3"/>
|
||||
<text x="103" y="221" fill="#102A43" font-size="23" font-weight="700">Inventory</text>
|
||||
<text x="92" y="254" fill="#486581" font-size="17">usage · state · owner</text>
|
||||
<rect x="350" y="175" width="230" height="110" rx="16" fill="#FFFFFF" stroke="#486581" stroke-width="3"/>
|
||||
<text x="387" y="221" fill="#102A43" font-size="23" font-weight="700">Validate input</text>
|
||||
<text x="385" y="254" fill="#486581" font-size="17">known token + value</text>
|
||||
<rect x="665" y="120" width="390" height="115" rx="16" fill="#FFF0F0" stroke="#C53030" stroke-width="3"/>
|
||||
<text x="702" y="166" fill="#9B2C2C" font-size="24" font-weight="700">Invalid configuration</text>
|
||||
<text x="702" y="199" fill="#9B2C2C" font-size="17">stop · changed: false · rollback not needed</text>
|
||||
<rect x="665" y="330" width="390" height="115" rx="16" fill="#E8FFF6" stroke="#1F9D73" stroke-width="3"/>
|
||||
<text x="702" y="376" fill="#166534" font-size="24" font-weight="700">Token correction</text>
|
||||
<text x="702" y="409" fill="#166534" font-size="17">save previous value for rollback</text>
|
||||
<rect x="665" y="540" width="390" height="95" rx="16" fill="#F4E9FF" stroke="#805AD5" stroke-width="3"/>
|
||||
<text x="702" y="580" fill="#553C9A" font-size="24" font-weight="700">Rollback applied</text>
|
||||
<text x="702" y="611" fill="#553C9A" font-size="17">restore exact previous token value</text>
|
||||
<path d="M275 230H340" stroke="#2457D6" stroke-width="5"/><path d="M330 217l20 13-20 13" fill="#2457D6"/>
|
||||
<path d="M580 230H650V177H655" fill="none" stroke="#C53030" stroke-width="5"/><path d="M645 164l20 13-20 13" fill="#C53030"/>
|
||||
<path d="M580 230H650V387H655" fill="none" stroke="#1F9D73" stroke-width="5"/><path d="M645 374l20 13-20 13" fill="#1F9D73"/>
|
||||
<path d="M860 445V530" stroke="#805AD5" stroke-width="5"/><path d="M847 520l13 20 13-20" fill="#805AD5"/>
|
||||
<rect x="65" y="420" width="455" height="155" rx="16" fill="#FFF6DD" stroke="#D69E2E" stroke-width="3" stroke-dasharray="10 8"/>
|
||||
<text x="98" y="466" fill="#744210" font-size="24" font-weight="700">Visual payload</text>
|
||||
<text x="98" y="500" fill="#744210" font-size="18">viewports · states · token names</text>
|
||||
<text x="98" y="534" fill="#744210" font-size="17">declared input, not screenshot or regression PASS</text>
|
||||
</g>
|
||||
<text x="65" y="680" fill="#486581" font-family="Arial, sans-serif" font-size="18">Граница модели: нет DOM, CSS compilation, HTTP, browser, screenshot или настоящего visual-regression запуска.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,37 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Поток токенов малого контракта кнопки</title>
|
||||
<desc id="desc">Именованные токены переходят в контракт primary button, затем в известные точки использования. Visual payload справа объявляет параметры будущей проверки, но не является снимком или результатом теста.</desc>
|
||||
<rect width="1200" height="720" fill="#F7F9FC"/>
|
||||
<text x="70" y="78" fill="#102A43" font-family="Arial, sans-serif" font-size="34" font-weight="700">Один token → один понятный путь изменения</text>
|
||||
<text x="70" y="116" fill="#486581" font-family="Arial, sans-serif" font-size="20">Малый контракт кнопки, май 2022</text>
|
||||
<g font-family="Arial, sans-serif">
|
||||
<rect x="70" y="180" width="265" height="370" rx="18" fill="#E6F0FF" stroke="#2457D6" stroke-width="3"/>
|
||||
<text x="103" y="225" fill="#102A43" font-size="25" font-weight="700">Named tokens</text>
|
||||
<text x="103" y="270" fill="#2457D6" font-size="19">button.primary</text>
|
||||
<text x="103" y="304" fill="#243B53" font-size="18">background</text>
|
||||
<text x="103" y="338" fill="#243B53" font-size="18">foreground</text>
|
||||
<text x="103" y="372" fill="#243B53" font-size="18">focus-ring</text>
|
||||
<text x="103" y="406" fill="#243B53" font-size="18">radius · gap</text>
|
||||
<rect x="385" y="180" width="355" height="370" rx="18" fill="#FFFFFF" stroke="#2457D6" stroke-width="3"/>
|
||||
<text x="420" y="225" fill="#102A43" font-size="25" font-weight="700">Primary button contract</text>
|
||||
<rect x="420" y="256" width="280" height="55" rx="10" fill="#2457D6"/>
|
||||
<text x="490" y="291" fill="#FFFFFF" font-size="22" font-weight="700">Сохранить</text>
|
||||
<text x="420" y="354" fill="#243B53" font-size="19">states: default · hover</text>
|
||||
<text x="420" y="386" fill="#243B53" font-size="19">focus-visible · disabled</text>
|
||||
<text x="420" y="418" fill="#243B53" font-size="19">loading</text>
|
||||
<text x="420" y="470" fill="#486581" font-size="18">name · role: button · state</text>
|
||||
<rect x="790" y="180" width="340" height="165" rx="18" fill="#E8FFF6" stroke="#1F9D73" stroke-width="3"/>
|
||||
<text x="824" y="225" fill="#102A43" font-size="24" font-weight="700">Usage inventory</text>
|
||||
<text x="824" y="267" fill="#243B53" font-size="18">profile-save</text>
|
||||
<text x="824" y="299" fill="#243B53" font-size="18">billing-pay</text>
|
||||
<text x="824" y="331" fill="#243B53" font-size="18">dialog-cancel</text>
|
||||
<rect x="790" y="392" width="340" height="158" rx="18" fill="#FFF6DD" stroke="#D69E2E" stroke-width="3" stroke-dasharray="10 8"/>
|
||||
<text x="824" y="437" fill="#744210" font-size="24" font-weight="700">Visual payload</text>
|
||||
<text x="824" y="475" fill="#744210" font-size="18">375 · 1280 · states</text>
|
||||
<text x="824" y="507" fill="#744210" font-size="16">declared input, not screenshot</text>
|
||||
<path d="M335 365H375" stroke="#2457D6" stroke-width="5"/><path d="M365 352l20 13-20 13" fill="#2457D6"/>
|
||||
<path d="M740 278H780" stroke="#1F9D73" stroke-width="5"/><path d="M770 265l20 13-20 13" fill="#1F9D73"/>
|
||||
<path d="M740 466H780" stroke="#D69E2E" stroke-width="5"/><path d="M770 453l20 13-20 13" fill="#D69E2E"/>
|
||||
</g>
|
||||
<text x="70" y="640" fill="#486581" font-family="Arial, sans-serif" font-size="18">Граница модели: DOM, CSS compilation, browser и visual regression не запускались.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,315 @@
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function paragraph(text) { return '<p>' + text + '</p>'; }
|
||||
function heading(text) { return '<h2>' + text + '</h2>'; }
|
||||
function codeBlock(lines) { return '<pre><code>' + escapeHtml(Array.isArray(lines) ? lines.join('\n') : lines) + '</code></pre>'; }
|
||||
function orderedList(items) { return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>'; }
|
||||
function figure(src, alt, caption) {
|
||||
return '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||||
}
|
||||
function dataTable(caption, headers, rows) {
|
||||
const head = '<thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead>';
|
||||
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody>';
|
||||
return '<div class="table-scroll"><table><caption>' + caption + '</caption>' + head + body + '</table></div>';
|
||||
}
|
||||
function sourceList(items) {
|
||||
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
|
||||
}
|
||||
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>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
|
||||
}
|
||||
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 с префиксом <code>--</code> и подстановку через <code>var()</code>. Это полезный механизм, но он не создаёт за команду словарь design tokens. Имя <code>button.primary.background</code> в этой статье — соглашение пакета: оно говорит, что значение относится к роли primary button, а не ко всем синим пикселям проекта. Поэтому не стоит сразу делать <code>brand.blue.500</code> единственным входом для компонента: у роли должна быть собственная граница, даже если сегодня она ссылается на тот же цвет.'),
|
||||
paragraph('Проверка проста: у каждой величины есть имя, владелец и место потребления. Если в pull request появляется <code>#2457D6</code> рядом с кнопкой, сначала спросите, это новый 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([
|
||||
'<strong>Симптом.</strong> Найдите одну повторяющуюся кнопку, у которой расходятся color, focus или label. Не группируйте сразу все controls.',
|
||||
'<strong>Причина.</strong> Выпишите, где лежат literal values, состояния и semantic fields. Обычно они принадлежат разным локальным файлам без общего contract.',
|
||||
'<strong>Проверка.</strong> Соберите usage inventory: context, name, role, состояние, локальные overrides. Затем запустите fixture и убедитесь, что declared payload не называют результатом visual test.',
|
||||
'<strong>Действие.</strong> Внесите один named token и одну state matrix для primary button. Оставьте native button там, где не требуется другой host.',
|
||||
'<strong>Откат.</strong> Сохраните прежнее значение token до правки. Если один usage изменился неожиданно, верните только token и разберите его локальный override.',
|
||||
'<strong>Следующая проверка.</strong> После 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 и подстановке <code>var()</code>. Она не назначает им смысл. Поэтому <code>--button-primary-background</code> может быть технически валиден, но неясен как договор, если никто не определил, для какой роли он существует, кто меняет его значение и какие variants от него зависят. Обратная ошибка тоже частая: назвать произвольное значение «semantic» и считать, что оно должно жить в глобальном файле. Семантика появляется не в пунктуации имени, а в повторяемом решении и известном owner.'),
|
||||
paragraph('Для маленькой системы полезен направленный путь: <code>button.primary.background</code> → primary button → конкретные usage. Он позволяет отдельно решить, как component получает CSS variable. В одном коде это может быть custom property, в другом объект theme, в третьем stylesheet. Contract не требует выбрать транспорт заранее. Он требует не потерять связь между ролью значения и точками, на которые повлияет изменение. Такая граница оставляет миграцию обратимой.'),
|
||||
heading('Name, role и state — не оформление'),
|
||||
paragraph('WAI-ARIA 1.2 описывает роли, состояния и свойства для интерфейсных объектов. Для простой кнопки лучший путь обычно начинается с native <code>button</code>: браузер уже знает базовую keyboard-модель. Если вместо него нужен custom host, команда обязана явно воспроизвести поведение, а не только написать <code>role="button"</code>. Однако даже native host не решает вопрос имени: «Сохранить изменения» и иконка без text alternative не равны для человека, который не видит layout.'),
|
||||
paragraph('В модели semantic contract намеренно мал: <code>name</code>, <code>role</code>, <code>state</code>, <code>disabled</code> и список permitted states. Поле <code>state</code> — declared data, не прочитанное browser tree. Такое различие нужно сохранить в тексте review: fixture может показать, что state loading предусмотрен в contract, но не может сказать, что конкретный browser запретил повторный click или что screen reader сообщил disabled. Для этого понадобятся платформенные тесты, которых здесь нет.'),
|
||||
heading('Исполнимый пример: проверить contract без UI'),
|
||||
paragraph('Функция <code>checkContract</code> сопоставляет required states с permitted states, проверяет, что payload не ссылается на отсутствующий token, и что каждое известное usage имеет name и роль button. Она не ищет реальные файлы и не запускает построение styles. Именно поэтому результат <code>true</code> означает «данные учебной модели согласованы», а не «кнопка доступна и визуально одинакова». Это узкое утверждение легко повторить и трудно неверно истолковать.'),
|
||||
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([
|
||||
'<strong>Симптом.</strong> Запишите один разрыв: кнопка теряет focus, loading не блокирует повтор, label отличается в одинаковом действии или literal color появился в новом usage.',
|
||||
'<strong>Причина.</strong> Разложите изменение по четырём владельцам. Если token пытается хранить state, а CSS class несёт name, граница уже размыта.',
|
||||
'<strong>Проверка contract.</strong> Сверьте required states, token names, semantic fields и inventory. Запустите <code>--verify-fixture</code>; он должен отклонить undeclared token и неверное значение.',
|
||||
'<strong>Проверка платформы.</strong> Отдельно создайте реальный control и пройдите согласованный keyboard/mouse/a11y сценарий. Результат сохраните как новый артефакт, не внутри model.',
|
||||
'<strong>Действие.</strong> Сначала поменяйте один owner: вынесите literal в named token, добавьте state или верните native host. Не объединяйте это с переписыванием всей библиотеки.',
|
||||
'<strong>Откат.</strong> Применяйте 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 не разрешает заменить сочетание автоматической и ручной оценки полем <code>role</code> в 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 — не список «всех кнопок мира». Это честная таблица того, что известно перед правкой: <code>profile-save</code>, <code>billing-pay</code>, <code>dialog-cancel</code>; контекст, 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('У модели есть два плохих входа. Первый пытается поменять <code>button.primary.shadow</code>, хотя такого named token нет. Второй пытается записать в background строку <code>brand-blue</code>, хотя учебный validator принимает только <code>#RRGGBB</code> или целые <code>px</code>. Оба входа возвращают <code>invalid-configuration</code> и не меняют объект. Это не полный 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 становится <code>#1D4ED8</code>, после rollback — снова <code>#2457D6</code>. 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([
|
||||
'<strong>Симптом.</strong> Зафиксируйте один экран, state и значение: например, primary button в loading использует другой background или не имеет focus-visible.',
|
||||
'<strong>Причина.</strong> Проверьте, это literal, неизвестный token, отсутствующий state или другой component role. Не лечите все варианты одним global rename.',
|
||||
'<strong>Инвентарь.</strong> Выпишите известные usage с name, role и state. Отделите подтверждённые места от предположений из поиска.',
|
||||
'<strong>Проверка модели.</strong> Запустите <code>node web/scripts/upgrade-2022-05.mjs --verify-fixture</code>. Она должна отклонить invalid configuration и восстановить previous value после rollback.',
|
||||
'<strong>Действие.</strong> Внесите один допустимый token correction или заведите отдельный variant после review. Не добавляйте undeclared key как «быстрое исключение».',
|
||||
'<strong>Проверка платформы.</strong> В отдельном реальном запуске проверьте собранный 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));
|
||||
Reference in New Issue
Block a user