@motion-proto/live-tokens 0.96.0 → 0.98.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +5 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
- package/CHANGELOG.md +309 -0
- package/README.md +2 -2
- package/bin/check-component.mjs +8 -0
- package/bin/check-page.mjs +6 -0
- package/bin/cli.mjs +36 -6
- package/bin/contractRunner.mjs +142 -17
- package/bin/lib/findings.mjs +20 -4
- package/bin/rules/a11y.mjs +53 -0
- package/bin/set-colors.mjs +1 -0
- package/bin/setup-claude.mjs +2 -2
- package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
- package/dist-plugin/index.cjs +148 -3
- package/dist-plugin/index.js +250 -105
- package/dist-plugin/setColors/index.cjs +7 -1
- package/dist-plugin/setColors/index.d.cts +9 -4
- package/dist-plugin/setColors/index.d.ts +9 -4
- package/dist-plugin/setColors/index.js +8 -2
- package/dist-plugin/setGeometry/index.d.cts +1 -1
- package/dist-plugin/setGeometry/index.d.ts +1 -1
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
- package/package.json +49 -2
- package/src/demo/LabeledSelect.svelte +1 -1
- package/src/editor/bootstrap.ts +3 -1
- package/src/editor/component-editor/BadgeEditor.svelte +13 -0
- package/src/editor/component-editor/ButtonEditor.svelte +33 -2
- package/src/editor/component-editor/CalloutEditor.svelte +13 -0
- package/src/editor/component-editor/CardEditor.svelte +12 -2
- package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
- package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
- package/src/editor/component-editor/DialogEditor.svelte +15 -0
- package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
- package/src/editor/component-editor/ImageEditor.svelte +20 -9
- package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
- package/src/editor/component-editor/InputEditor.svelte +23 -0
- package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
- package/src/editor/component-editor/NotificationEditor.svelte +15 -0
- package/src/editor/component-editor/PanelEditor.svelte +1 -1
- package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
- package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
- package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
- package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
- package/src/editor/component-editor/SliderEditor.svelte +33 -0
- package/src/editor/component-editor/TabBarEditor.svelte +23 -2
- package/src/editor/component-editor/TableEditor.svelte +14 -0
- package/src/editor/component-editor/ToggleEditor.svelte +17 -0
- package/src/editor/component-editor/TooltipEditor.svelte +11 -2
- package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
- package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
- package/src/editor/component-editor/registration.ts +2 -1
- package/src/editor/component-editor/registry.ts +69 -27
- package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -10
- package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
- package/src/editor/core/a11y/a11yReport.ts +101 -0
- package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
- package/src/editor/core/a11y/contrastFailures.ts +22 -0
- package/src/editor/core/components/contrastPairRegistry.ts +18 -0
- package/src/editor/core/palettes/contrast.ts +44 -0
- package/src/editor/core/palettes/contrastPairs.ts +79 -0
- package/src/editor/core/palettes/resolveColor.ts +77 -0
- package/src/editor/core/store/editorPersistence.ts +9 -1
- package/src/editor/core/store/editorRenderer.ts +2 -0
- package/src/editor/core/store/editorStore.ts +29 -0
- package/src/editor/core/store/editorTypes.ts +9 -0
- package/src/editor/core/store/editorViewStore.ts +2 -2
- package/src/editor/core/store/unsavedEdits.ts +10 -0
- package/src/editor/core/themes/buildColors.ts +16 -5
- package/src/editor/core/themes/liveStateStream.ts +7 -1
- package/src/editor/core/themes/parsers/gradient.ts +5 -2
- package/src/editor/core/themes/slices/customColors.ts +226 -0
- package/src/editor/core/themes/themeTypes.ts +10 -0
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/accessibility.md +362 -0
- package/src/editor/docs/content/editing-tokens.md +56 -1
- package/src/editor/docs/content.generated.ts +2 -1
- package/src/editor/pages/ComponentEditorPage.svelte +10 -1
- package/src/editor/pages/EditorShell.svelte +97 -10
- package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
- package/src/editor/skill-atlas/trees/create-component.ts +12 -12
- package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
- package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
- package/src/editor/ui/GradientEditor.svelte +9 -1
- package/src/editor/ui/ThemePanel.svelte +4 -4
- package/src/editor/ui/UIContrastReadout.svelte +186 -0
- package/src/editor/ui/UIInfoPopover.svelte +16 -2
- package/src/editor/ui/UIPaletteSelector.svelte +347 -15
- package/src/editor/ui/UITokenSelector.svelte +33 -14
- package/src/editor/ui/VariablesTab.svelte +3 -0
- package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
- package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
- package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
- package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
- package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
- package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
- package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
- package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
- package/src/editor/ui/a11y/a11yView.ts +218 -0
- package/src/editor/ui/anchorToTrigger.ts +63 -0
- package/src/editor/ui/contrastReadout.ts +45 -0
- package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
- package/src/live-tokens/data/themes/autumn.json +20 -20
- package/src/live-tokens/data/themes/halloween.json +20 -20
- package/src/live-tokens/data/themes/midnight-study.json +20 -20
- package/src/live-tokens/data/themes/ocean.json +20 -20
- package/src/live-tokens/data/themes/royal-velvet.json +20 -20
- package/src/live-tokens/data/themes/sketchy.json +20 -20
- package/src/live-tokens/data/themes/spring-meadow.json +20 -20
- package/src/live-tokens/data/themes/sunset.json +20 -20
- package/src/system/components/Badge.svelte +3 -3
- package/src/system/components/Button.svelte +9 -3
- package/src/system/components/CodeSnippet.svelte +8 -1
- package/src/system/components/CornerBadge.svelte +3 -3
- package/src/system/components/Dialog.svelte +43 -14
- package/src/system/components/IconButton.svelte +1 -1
- package/src/system/components/Image.svelte +7 -0
- package/src/system/components/ImageLightbox.svelte +26 -26
- package/src/system/components/Input.svelte +8 -1
- package/src/system/components/MenuSelect.svelte +30 -2
- package/src/system/components/Notification.svelte +1 -1
- package/src/system/components/ProgressBar.svelte +6 -0
- package/src/system/components/RadioButton.svelte +36 -3
- package/src/system/components/SegmentedControl.svelte +36 -2
- package/src/system/components/SlidePager.svelte +2 -2
- package/src/system/components/Slider.svelte +13 -9
- package/src/system/components/TabBar.svelte +33 -3
- package/src/system/components/Toggle.svelte +2 -2
- package/src/system/components/VideoFrame.svelte +1 -1
- package/src/system/components/VideoLightbox.svelte +2 -2
- package/src/system/internal/focusTrap.ts +35 -0
- package/src/testing-js/__aria__/badge.yml +40 -0
- package/src/testing-js/__aria__/button.yml +88 -0
- package/src/testing-js/__aria__/callout.yml +12 -0
- package/src/testing-js/__aria__/card.yml +8 -0
- package/src/testing-js/__aria__/codesnippet.yml +21 -0
- package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
- package/src/testing-js/__aria__/cornerbadge.yml +24 -0
- package/src/testing-js/__aria__/dialog.yml +42 -0
- package/src/testing-js/__aria__/iconbutton.yml +57 -0
- package/src/testing-js/__aria__/image.yml +10 -0
- package/src/testing-js/__aria__/imagelightbox.yml +50 -0
- package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
- package/src/testing-js/__aria__/input.yml +61 -0
- package/src/testing-js/__aria__/menuselect.yml +30 -0
- package/src/testing-js/__aria__/notification.yml +30 -0
- package/src/testing-js/__aria__/panel.yml +2 -0
- package/src/testing-js/__aria__/progressbar.yml +2 -0
- package/src/testing-js/__aria__/radiobutton.yml +20 -0
- package/src/testing-js/__aria__/sectiondivider.yml +9 -0
- package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
- package/src/testing-js/__aria__/sidenavigation.yml +304 -0
- package/src/testing-js/__aria__/slidepager.yml +72 -0
- package/src/testing-js/__aria__/slider.yml +28 -0
- package/src/testing-js/__aria__/tabbar.yml +35 -0
- package/src/testing-js/__aria__/table.yml +144 -0
- package/src/testing-js/__aria__/toggle.yml +12 -0
- package/src/testing-js/__aria__/tooltip.yml +2 -0
- package/src/testing-js/__aria__/videoframe.yml +6 -0
- package/src/testing-js/__aria__/videolightbox.yml +12 -0
- package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
- package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
- package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
- package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
- package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
- package/src/testing-js/chunk-EWSGE27V.js +168 -0
- package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
- package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
- package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
- package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
- package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
- package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
- package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
- package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
- package/src/testing-js/component-a11y.contract.js +283 -0
- package/src/testing-js/component-a11y.contract.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-behavior.contract.js +2 -2
- package/src/testing-js/component-editor.contract.js +6 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +10 -6
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +44 -16
- package/src/testing-js/index.js +9 -9
- package/src/testing-js/page-compliance.contract.js +163 -18
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/testing-js/vitest.js +3 -3
- package/template/package.json +9 -0
- package/src/editor/ui/keepInViewport.ts +0 -84
- package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
- package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
- package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
- package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
- /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
- /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
package/bin/contractRunner.mjs
CHANGED
|
@@ -41,6 +41,8 @@ const COMPONENT_ENV = 'LIVE_TOKENS_COMPONENT';
|
|
|
41
41
|
// as JSON. Duplicated rather than imported for the same reason as
|
|
42
42
|
// SESSION_FILES below.
|
|
43
43
|
const PAGES_ENV = 'LIVE_TOKENS_PAGES';
|
|
44
|
+
// Mirrors src/testing/config.ts's own UPDATE_ARIA_ENV.
|
|
45
|
+
const UPDATE_ARIA_ENV = 'LIVE_TOKENS_UPDATE_ARIA';
|
|
44
46
|
|
|
45
47
|
/** Milliseconds a spawned tool gets before this file sends it SIGINT, then
|
|
46
48
|
* SIGKILL five seconds later. A hung dev server or worker can no longer hold
|
|
@@ -58,6 +60,7 @@ function testsTimeoutMs() {
|
|
|
58
60
|
// that module compiles into src/testing-js, which does not exist until
|
|
59
61
|
// `build:testing` runs, and this one ships unbuilt.
|
|
60
62
|
const SESSION_FILES = new Set(['_working.json', '_active.json', '_production.json']);
|
|
63
|
+
const THEME_POINTER = join('themes', '_active.json');
|
|
61
64
|
|
|
62
65
|
const REQUIRED_TOOLS = [
|
|
63
66
|
{
|
|
@@ -84,6 +87,7 @@ const ALL_CONTRACT_RULES = [
|
|
|
84
87
|
'contract-theme',
|
|
85
88
|
'contract-sketch',
|
|
86
89
|
'contract-render',
|
|
90
|
+
'a11y-component',
|
|
87
91
|
];
|
|
88
92
|
|
|
89
93
|
/**
|
|
@@ -106,6 +110,24 @@ export const EDITOR_SUITE_POSITIONAL_RULES = [
|
|
|
106
110
|
'contract-sketch',
|
|
107
111
|
];
|
|
108
112
|
|
|
113
|
+
/**
|
|
114
|
+
* `--tests --a11y` (decision 3 of docs/plans/a11y-editor.md): the
|
|
115
|
+
* accessibility suite, and from the editor suite the one test that runs the
|
|
116
|
+
* keyboard cases. `--grep` selects that test by title, so the editor suite
|
|
117
|
+
* reaches the mapper with one test per component, and `editorRules` stands in
|
|
118
|
+
* for the positional table. `bin/check-component.test.ts` pins `grep` to both
|
|
119
|
+
* suites' titles.
|
|
120
|
+
*/
|
|
121
|
+
export const A11Y_SELECTION = {
|
|
122
|
+
files: ['component-a11y.contract', 'component-editor.contract'],
|
|
123
|
+
grep: 'records its accessibility findings in every variant and state|answers the pointer and the keyboard',
|
|
124
|
+
editorRules: ['contract-interaction'],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export function isA11yRunFinding(finding) {
|
|
128
|
+
return finding.rule.startsWith('a11y-') || finding.rule.startsWith('tests-');
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
// ─── tool + path resolution ─────────────────────────────────────────────────
|
|
110
132
|
|
|
111
133
|
/** Node's own node_modules resolution, walked by hand: a monorepo hoists a
|
|
@@ -191,7 +213,10 @@ function guessViteConfigPath(root, settingsPath) {
|
|
|
191
213
|
|
|
192
214
|
// ─── data isolation ─────────────────────────────────────────────────────────
|
|
193
215
|
|
|
194
|
-
|
|
216
|
+
/** `keepActive` (`--open-theme`) leaves the editor's theme pointer in the copy,
|
|
217
|
+
* so the run reads that theme's saved file. Every buffer still goes, and so
|
|
218
|
+
* does any legacy per-layer pointer, whose presence marks a tree unmigrated. */
|
|
219
|
+
export function copyIsolatedDataDir(sourceDataDir, { keepActive = false } = {}) {
|
|
195
220
|
if (!existsSync(sourceDataDir)) {
|
|
196
221
|
throw new Error(
|
|
197
222
|
`No data directory at ${sourceDataDir}. Set "dataDir" in live-tokens.config.json ` +
|
|
@@ -203,11 +228,19 @@ function copyIsolatedDataDir(sourceDataDir) {
|
|
|
203
228
|
const dataDir = realpathSync(mkdtempSync(join(tmpdir(), 'live-tokens-check-')));
|
|
204
229
|
cpSync(sourceDataDir, dataDir, { recursive: true });
|
|
205
230
|
for (const entry of readdirSync(dataDir, { recursive: true })) {
|
|
231
|
+
if (keepActive && entry === THEME_POINTER) continue;
|
|
206
232
|
if (SESSION_FILES.has(basename(entry))) rmSync(join(dataDir, entry), { force: true });
|
|
207
233
|
}
|
|
208
234
|
return dataDir;
|
|
209
235
|
}
|
|
210
236
|
|
|
237
|
+
/** The theme a run over `dataDir` loads: the slug its pointer names, or
|
|
238
|
+
* `default` when the copy carries none. */
|
|
239
|
+
export function testedTheme(dataDir) {
|
|
240
|
+
const pointer = join(dataDir, THEME_POINTER);
|
|
241
|
+
return existsSync(pointer) ? JSON.parse(readFileSync(pointer, 'utf8')).activeFile : 'default';
|
|
242
|
+
}
|
|
243
|
+
|
|
211
244
|
/** The one child process a `withCleanup` signal handler needs to stop before
|
|
212
245
|
* it is safe to remove the directories that process is still writing into.
|
|
213
246
|
* Module-scoped because exactly one `runContractTests` call is ever active
|
|
@@ -407,11 +440,12 @@ export function readReportOrSetupFinding(tool, reportPath, code, stdout, stderr)
|
|
|
407
440
|
}
|
|
408
441
|
}
|
|
409
442
|
|
|
410
|
-
export async function runPlaywrightSuite({ root, configDir, playwrightConfigPath, project, timeoutMs = DEFAULT_TESTS_TIMEOUT_MS }) {
|
|
443
|
+
export async function runPlaywrightSuite({ root, configDir, playwrightConfigPath, project, files = [], grep, timeoutMs = DEFAULT_TESTS_TIMEOUT_MS }) {
|
|
411
444
|
const reportPath = join(configDir, 'playwright-report.json');
|
|
412
445
|
const bin = peerBin(root, '@playwright/test', 'cli.js');
|
|
413
|
-
const args = [bin, 'test', '-c', playwrightConfigPath, '--reporter=json'];
|
|
446
|
+
const args = [bin, 'test', '-c', playwrightConfigPath, '--reporter=json', ...files];
|
|
414
447
|
if (project) args.push('--project', project);
|
|
448
|
+
if (grep) args.push('--grep', grep);
|
|
415
449
|
const { code, stdout, stderr, timedOut } = await runCli(process.execPath, args, {
|
|
416
450
|
cwd: root,
|
|
417
451
|
env: { ...process.env, PLAYWRIGHT_JSON_OUTPUT_FILE: reportPath },
|
|
@@ -569,13 +603,16 @@ const VIOLATION_RE = /^ContractViolation:\s*\[([a-z-]+)\]\s+([a-z0-9-]+):\s*([\s
|
|
|
569
603
|
* `EDITOR_SUITE_POSITIONAL_RULES`), the suite file for the other two. A
|
|
570
604
|
* `ContractViolation`'s own `[rule]` prefix, when present, is more
|
|
571
605
|
* authoritative than either and wins in the caller. */
|
|
572
|
-
function structuralRule(entry, componentId, positionCounters) {
|
|
606
|
+
function structuralRule(entry, componentId, positionCounters, positionalRules) {
|
|
607
|
+
const file = basename(entry.specFile ?? '');
|
|
608
|
+
// Ahead of the positional branch: an a11y test grouped under its component's
|
|
609
|
+
// id must not advance that component's editor-suite position.
|
|
610
|
+
if (file.startsWith('component-a11y.contract')) return 'a11y-component';
|
|
573
611
|
if (componentId && entry.describeTitles[entry.describeTitles.length - 1] === componentId) {
|
|
574
612
|
const index = positionCounters.get(componentId) ?? 0;
|
|
575
613
|
positionCounters.set(componentId, index + 1);
|
|
576
|
-
return
|
|
614
|
+
return positionalRules[index] ?? null;
|
|
577
615
|
}
|
|
578
|
-
const file = basename(entry.specFile ?? '');
|
|
579
616
|
if (file.startsWith('component-render.contract')) return 'contract-render';
|
|
580
617
|
if (file.startsWith('component-alias.contract')) return 'contract-alias';
|
|
581
618
|
return null;
|
|
@@ -589,7 +626,39 @@ function structuralRule(entry, componentId, positionCounters) {
|
|
|
589
626
|
* sibling that passed clean the first time. */
|
|
590
627
|
const COVERAGE_PRIORITY = { failed: 4, flaky: 3, passed: 2, inapplicable: 1 };
|
|
591
628
|
|
|
592
|
-
|
|
629
|
+
// Mirrors the annotation type src/testing/support/a11y.ts records, duplicated
|
|
630
|
+
// for the same reason as SESSION_FILES above.
|
|
631
|
+
const A11Y_ANNOTATION = 'a11y';
|
|
632
|
+
|
|
633
|
+
function a11yResults(annotations) {
|
|
634
|
+
return annotations.filter((a) => a.type === A11Y_ANNOTATION).flatMap((a) => JSON.parse(a.description));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** One finding per result a passing test recorded (decision 3). Coverage stays
|
|
638
|
+
* with the test's own pass: a warning must not read as a failed obligation,
|
|
639
|
+
* which would excuse every gap reconciliation looks for. */
|
|
640
|
+
function a11yFinding(result, { file, line, componentId, context }) {
|
|
641
|
+
return {
|
|
642
|
+
rule: result.rule,
|
|
643
|
+
file,
|
|
644
|
+
line,
|
|
645
|
+
message: `${componentId ? `${componentId}: ` : ''}${result.engineRule} at ${result.target}${result.view ? ` in ${result.view}` : ''}: ${result.help}. ${result.helpUrl}`,
|
|
646
|
+
...(componentId ? { component: componentId } : {}),
|
|
647
|
+
details: {
|
|
648
|
+
engineRule: result.engineRule,
|
|
649
|
+
impact: result.impact,
|
|
650
|
+
target: result.target,
|
|
651
|
+
...(result.view ? { view: result.view } : {}),
|
|
652
|
+
html: result.html,
|
|
653
|
+
help: result.help,
|
|
654
|
+
helpUrl: result.helpUrl,
|
|
655
|
+
...(result.variables?.length ? { variables: result.variables } : {}),
|
|
656
|
+
},
|
|
657
|
+
context,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds, positionalRules = EDITOR_SUITE_POSITIONAL_RULES }) {
|
|
593
662
|
const zeroCollected = (report.suites ?? []).length === 0;
|
|
594
663
|
if (zeroCollected || (report.errors ?? []).length > 0) {
|
|
595
664
|
const detail = (report.errors ?? []).map((e) => e.message).join('\n') || 'the run collected no tests';
|
|
@@ -629,8 +698,14 @@ export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds })
|
|
|
629
698
|
|
|
630
699
|
for (const t of tests) {
|
|
631
700
|
const componentId = identifyComponent(t.describeTitles, t.specTitle, knownIds);
|
|
632
|
-
const rule = structuralRule(t, componentId, positionCounters);
|
|
701
|
+
const rule = structuralRule(t, componentId, positionCounters, positionalRules);
|
|
633
702
|
const inapplicable = t.annotations.find((a) => a.type === 'inapplicable')?.description;
|
|
703
|
+
const results = a11yResults(t.annotations);
|
|
704
|
+
if (results.length > 0) {
|
|
705
|
+
const { file, line } = artifactForContractRule(root, sourceDataDir, 'a11y-component', componentId, null);
|
|
706
|
+
const context = { suite: 'playwright', title: [...t.describeTitles, t.specTitle].join(' > '), suiteFile: t.specFile, suiteLine: t.specLine };
|
|
707
|
+
findings.push(...results.map((result) => a11yFinding(result, { file, line, componentId, context })));
|
|
708
|
+
}
|
|
634
709
|
|
|
635
710
|
if (t.status === 'expected' || t.status === 'flaky') {
|
|
636
711
|
// Flaky reaches a passing final attempt, same as expected, but a retry
|
|
@@ -655,6 +730,7 @@ export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds })
|
|
|
655
730
|
file: 'package.json',
|
|
656
731
|
line: 1,
|
|
657
732
|
message: `${componentId ? `${componentId}: ` : ''}"${t.specTitle}" did not finish (${last.status})`,
|
|
733
|
+
...(componentId ? { component: componentId } : {}),
|
|
658
734
|
context: { suite: 'playwright', title: [...t.describeTitles, t.specTitle].join(' > '), suiteFile: t.specFile, suiteLine: t.specLine },
|
|
659
735
|
});
|
|
660
736
|
markCoverage(componentId, rule, 'failed');
|
|
@@ -677,7 +753,9 @@ export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds })
|
|
|
677
753
|
}
|
|
678
754
|
const block = messageBlock(error.message);
|
|
679
755
|
const violation = VIOLATION_RE.exec(block);
|
|
680
|
-
|
|
756
|
+
// An accessibility test records its findings and passes, so one that
|
|
757
|
+
// throws broke its scan (decision 3 of docs/plans/a11y-testing.md).
|
|
758
|
+
const finalRule = violation?.[1] ?? (rule === 'a11y-component' ? 'tests-setup' : rule) ?? 'tests-setup';
|
|
681
759
|
const id = violation?.[2] ?? componentId;
|
|
682
760
|
const message = violation?.[3] ?? block;
|
|
683
761
|
const token = extractToken(message);
|
|
@@ -687,6 +765,7 @@ export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds })
|
|
|
687
765
|
file,
|
|
688
766
|
line,
|
|
689
767
|
message: id ? `${id}: ${message}` : message,
|
|
768
|
+
...(id ? { component: id } : {}),
|
|
690
769
|
context: {
|
|
691
770
|
suite: 'playwright',
|
|
692
771
|
title: [...t.describeTitles, t.specTitle].join(' > '),
|
|
@@ -695,7 +774,7 @@ export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds })
|
|
|
695
774
|
attachments: (last?.attachments ?? []).map((a) => a.path).filter(Boolean),
|
|
696
775
|
},
|
|
697
776
|
});
|
|
698
|
-
markCoverage(id, finalRule, 'failed');
|
|
777
|
+
markCoverage(id, violation ? finalRule : rule ?? finalRule, 'failed');
|
|
699
778
|
}
|
|
700
779
|
}
|
|
701
780
|
return { findings, coverage };
|
|
@@ -706,13 +785,26 @@ export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds })
|
|
|
706
785
|
/** The plan's Runtime rule ids, fixed rather than read off a page's own
|
|
707
786
|
* results: `reconcileCoverage`'s expected set, same reasoning as
|
|
708
787
|
* `ALL_CONTRACT_RULES` above. */
|
|
709
|
-
const PAGE_RUNTIME_RULES = [
|
|
788
|
+
const PAGE_RUNTIME_RULES = [
|
|
789
|
+
'page-component-paint', 'page-text-style', 'page-contrast', 'page-grid', 'page-overflow',
|
|
790
|
+
'a11y-page', 'a11y-text-spacing', 'a11y-reduced-motion',
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
/** The accessibility tests' titles keep the `page-` prefix the title pattern
|
|
794
|
+
* reads; coverage keys them by the rule each records, so `--off` reaches
|
|
795
|
+
* them. A test of these that throws broke its scan, which reports as
|
|
796
|
+
* `tests-setup` (decision 3 of docs/plans/a11y-testing.md). */
|
|
797
|
+
const PAGE_A11Y_TESTS = {
|
|
798
|
+
'page-a11y': 'a11y-page',
|
|
799
|
+
'page-text-spacing': 'a11y-text-spacing',
|
|
800
|
+
'page-reduced-motion': 'a11y-reduced-motion',
|
|
801
|
+
};
|
|
710
802
|
|
|
711
803
|
/** `page-compliance.contract.ts` titles each test `${rule} | ${source} |
|
|
712
804
|
* ${width}x${height}`, so the rule, the page, and the viewport are read off
|
|
713
805
|
* the title rather than off describe-block position (there is none — every
|
|
714
806
|
* page test is a sibling at the suite's top level). */
|
|
715
|
-
const PAGE_TEST_TITLE_RE = /^(page-[a-
|
|
807
|
+
const PAGE_TEST_TITLE_RE = /^(page-[a-z0-9-]+) \| (.+) \| (\d+x\d+)$/;
|
|
716
808
|
|
|
717
809
|
/** A `PageViolation`'s own `[rule] source:line: message`, read back out of
|
|
718
810
|
* Playwright's `Error.prototype.toString()` (`${name}: ${message}`), the
|
|
@@ -767,8 +859,14 @@ export function mapPageResults(report) {
|
|
|
767
859
|
};
|
|
768
860
|
|
|
769
861
|
for (const t of tests) {
|
|
770
|
-
const [,
|
|
862
|
+
const [, titleName, titleSource, titleViewport] = PAGE_TEST_TITLE_RE.exec(t.specTitle) ?? [];
|
|
863
|
+
const titleRule = PAGE_A11Y_TESTS[titleName] ?? titleName;
|
|
771
864
|
const pageAtViewport = titleSource && titleViewport ? `${titleSource}@${titleViewport}` : null;
|
|
865
|
+
const context = { suite: 'playwright', title: t.specTitle, suiteFile: t.specFile, suiteLine: t.specLine };
|
|
866
|
+
// Each viewport records its own results, so the viewport tells a page's
|
|
867
|
+
// two findings for one element apart.
|
|
868
|
+
findings.push(...a11yResults(t.annotations).map((result) =>
|
|
869
|
+
a11yFinding({ ...result, view: result.view ?? titleViewport }, { file: titleSource ?? 'package.json', line: result.line ?? 1, context })));
|
|
772
870
|
|
|
773
871
|
if (t.status === 'expected' || t.status === 'flaky') {
|
|
774
872
|
markCoverage(pageAtViewport, titleRule, t.status === 'flaky' ? 'flaky' : 'passed');
|
|
@@ -813,7 +911,7 @@ export function mapPageResults(report) {
|
|
|
813
911
|
const block = messageBlock(error.message);
|
|
814
912
|
const violation = PAGE_VIOLATION_RE.exec(block);
|
|
815
913
|
findings.push({
|
|
816
|
-
rule: violation?.[1] ?? titleRule
|
|
914
|
+
rule: violation?.[1] ?? (PAGE_A11Y_TESTS[titleName] || !titleRule ? 'tests-setup' : titleRule),
|
|
817
915
|
file: violation?.[2] ?? titleSource ?? 'package.json',
|
|
818
916
|
line: violation ? Number(violation[3]) : 1,
|
|
819
917
|
message: violation?.[4] ?? block,
|
|
@@ -1009,7 +1107,10 @@ export function reconcileCoverage(coverage, expectedIds, { expectedRules = ALL_C
|
|
|
1009
1107
|
|
|
1010
1108
|
// ─── entry point ────────────────────────────────────────────────────────────
|
|
1011
1109
|
|
|
1012
|
-
export async function runContractTests(
|
|
1110
|
+
export async function runContractTests(
|
|
1111
|
+
id,
|
|
1112
|
+
{ root = process.cwd(), dataDir: explicitDataDir, updateAria = false, a11y = false, openTheme = false } = {},
|
|
1113
|
+
) {
|
|
1013
1114
|
const toolFindings = missingToolFindings(root);
|
|
1014
1115
|
if (toolFindings.length > 0) return { findings: toolFindings, coverage: {} };
|
|
1015
1116
|
|
|
@@ -1031,12 +1132,14 @@ export async function runContractTests(id, { root = process.cwd(), dataDir: expl
|
|
|
1031
1132
|
let sourceDataDir;
|
|
1032
1133
|
let dataDir;
|
|
1033
1134
|
let configDir;
|
|
1135
|
+
let theme;
|
|
1034
1136
|
try {
|
|
1035
1137
|
// `dataDir` bypasses resolution entirely: the seam a caller (a test, or a
|
|
1036
1138
|
// future batch/CI runner) uses to point an isolated copy at a source tree
|
|
1037
1139
|
// other than the project's own, without ever touching the real one.
|
|
1038
1140
|
sourceDataDir = explicitDataDir ?? resolveSourceDataDir(root, settingsFilePath(root));
|
|
1039
|
-
dataDir = copyIsolatedDataDir(sourceDataDir);
|
|
1141
|
+
dataDir = copyIsolatedDataDir(sourceDataDir, { keepActive: openTheme });
|
|
1142
|
+
theme = testedTheme(dataDir);
|
|
1040
1143
|
configDir = mkdtempSync(join(tmpdir(), 'live-tokens-check-cfg-'));
|
|
1041
1144
|
} catch (error) {
|
|
1042
1145
|
if (dataDir) rmSync(dataDir, { recursive: true, force: true });
|
|
@@ -1056,8 +1159,29 @@ export async function runContractTests(id, { root = process.cwd(), dataDir: expl
|
|
|
1056
1159
|
process.env[DATA_DIR_ENV] = dataDir;
|
|
1057
1160
|
if (id) process.env[COMPONENT_ENV] = id;
|
|
1058
1161
|
else delete process.env[COMPONENT_ENV];
|
|
1162
|
+
if (updateAria) process.env[UPDATE_ARIA_ENV] = '1';
|
|
1163
|
+
else delete process.env[UPDATE_ARIA_ENV];
|
|
1059
1164
|
|
|
1060
1165
|
const knownIds = new Set([...discoverComponents(root), ...discoverComponents(PKG_ROOT)]);
|
|
1166
|
+
|
|
1167
|
+
// Reconciliation expects every contract rule of every component, and this
|
|
1168
|
+
// run covers two, so its coverage stands as the suites reported it.
|
|
1169
|
+
if (a11y) {
|
|
1170
|
+
const outcome = await runPlaywrightSuite({
|
|
1171
|
+
root,
|
|
1172
|
+
configDir,
|
|
1173
|
+
playwrightConfigPath,
|
|
1174
|
+
project: 'contract',
|
|
1175
|
+
files: A11Y_SELECTION.files,
|
|
1176
|
+
grep: A11Y_SELECTION.grep,
|
|
1177
|
+
timeoutMs,
|
|
1178
|
+
});
|
|
1179
|
+
const mapped = outcome.setupFinding
|
|
1180
|
+
? { findings: [outcome.setupFinding], coverage: {} }
|
|
1181
|
+
: mapPlaywrightResults(outcome.report, { root, sourceDataDir, knownIds, positionalRules: A11Y_SELECTION.editorRules });
|
|
1182
|
+
return { findings: mapped.findings.filter(isA11yRunFinding), coverage: mapped.coverage, theme };
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1061
1185
|
const expectedIds = id ? [id] : [...knownIds];
|
|
1062
1186
|
|
|
1063
1187
|
// Sequential: both share the one isolated data directory, and the
|
|
@@ -1087,11 +1211,12 @@ export async function runContractTests(id, { root = process.cwd(), dataDir: expl
|
|
|
1087
1211
|
explainedGlobally,
|
|
1088
1212
|
});
|
|
1089
1213
|
|
|
1090
|
-
return { findings: [...mergedFindings, ...reconciled.findings], coverage: reconciled.coverage };
|
|
1214
|
+
return { findings: [...mergedFindings, ...reconciled.findings], coverage: reconciled.coverage, theme };
|
|
1091
1215
|
} catch (error) {
|
|
1092
1216
|
return {
|
|
1093
1217
|
findings: [{ rule: 'tests-setup', file: 'package.json', line: 1, message: `check-component --tests crashed: ${error.message}` }],
|
|
1094
1218
|
coverage: {},
|
|
1219
|
+
theme,
|
|
1095
1220
|
};
|
|
1096
1221
|
} finally {
|
|
1097
1222
|
cleanup();
|
package/bin/lib/findings.mjs
CHANGED
|
@@ -57,11 +57,15 @@ export function isExcluded(relPath, root) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Parse `--off=a,b --warn=c --error=d --strict --json
|
|
60
|
+
* Parse `--off=a,b --warn=c --error=d --strict --json --tests --no-fix
|
|
61
|
+
* --update-aria --a11y --open-theme` out of argv.
|
|
61
62
|
* Unrecognised flags are returned in `rest` for the caller to handle.
|
|
62
63
|
*/
|
|
63
64
|
export function parseCheckFlags(argv) {
|
|
64
|
-
const opts = {
|
|
65
|
+
const opts = {
|
|
66
|
+
off: [], warn: [], error: [], strict: false, json: false, tests: false, noFix: false, updateAria: false,
|
|
67
|
+
a11y: false, openTheme: false, rest: [],
|
|
68
|
+
};
|
|
65
69
|
for (const arg of argv) {
|
|
66
70
|
const m = arg.match(/^--(off|warn|error)=(.+)$/);
|
|
67
71
|
if (m) {
|
|
@@ -70,6 +74,9 @@ export function parseCheckFlags(argv) {
|
|
|
70
74
|
else if (arg === '--json') opts.json = true;
|
|
71
75
|
else if (arg === '--tests') opts.tests = true;
|
|
72
76
|
else if (arg === '--no-fix') opts.noFix = true;
|
|
77
|
+
else if (arg === '--update-aria') opts.updateAria = true;
|
|
78
|
+
else if (arg === '--a11y') opts.a11y = true;
|
|
79
|
+
else if (arg === '--open-theme') opts.openTheme = true;
|
|
73
80
|
else opts.rest.push(arg);
|
|
74
81
|
}
|
|
75
82
|
return opts;
|
|
@@ -191,10 +198,19 @@ export function formatFindings(findings, { label, checked = 0 } = {}) {
|
|
|
191
198
|
return lines.join('\n');
|
|
192
199
|
}
|
|
193
200
|
|
|
194
|
-
export function toJson(findings, { label, checked = 0, coverage, fix } = {}) {
|
|
201
|
+
export function toJson(findings, { label, checked = 0, coverage, fix, theme } = {}) {
|
|
195
202
|
const { errors, warnings } = countBySeverity(findings);
|
|
196
203
|
return JSON.stringify(
|
|
197
|
-
{
|
|
204
|
+
{
|
|
205
|
+
check: label,
|
|
206
|
+
checked,
|
|
207
|
+
errors,
|
|
208
|
+
warnings,
|
|
209
|
+
...(theme ? { theme } : {}),
|
|
210
|
+
...(fix ? { fix } : {}),
|
|
211
|
+
findings,
|
|
212
|
+
...(coverage ? { coverage } : {}),
|
|
213
|
+
},
|
|
198
214
|
null,
|
|
199
215
|
2,
|
|
200
216
|
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const HELP =
|
|
2
|
+
'The message names the engine rule, the element, and the URL of a help page that states the requirement and its repair.';
|
|
3
|
+
|
|
4
|
+
const review = {
|
|
5
|
+
'a11y-review': {
|
|
6
|
+
severity: 'warn',
|
|
7
|
+
repair: 'authored',
|
|
8
|
+
guidance: `An engine could not decide the check, typically for text, a focus ring, or a control's edge over a gradient, an image, or a backdrop. ${HELP} Judge the element by eye against the help page, and repair it when it fails.`,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const adaptation = {
|
|
13
|
+
'a11y-text-spacing': {
|
|
14
|
+
severity: 'warn',
|
|
15
|
+
repair: 'authored',
|
|
16
|
+
guidance: `Content overflows under the WCAG 2.2 SC 1.4.12 text spacing override: line height 1.5, paragraph spacing 2em, letter spacing 0.12em, and word spacing 0.16em. ${HELP} Let the element the finding names grow with its text: drop its fixed height or width, or its white-space: nowrap. In a component finding, details.variables in --json names the semantic properties that set the element's size.`,
|
|
17
|
+
},
|
|
18
|
+
'a11y-reduced-motion': {
|
|
19
|
+
severity: 'warn',
|
|
20
|
+
repair: 'authored',
|
|
21
|
+
guidance: `An animation or transition keeps running past 200 ms under prefers-reduced-motion: reduce. ${HELP} Stop the motion, or cut it to a fade, inside @media (prefers-reduced-motion: reduce).`,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const componentRules = {
|
|
26
|
+
'a11y-component': {
|
|
27
|
+
severity: 'warn',
|
|
28
|
+
repair: 'authored',
|
|
29
|
+
guidance: `axe found a WCAG 2 A or AA failure inside the component's preview, or a focus ring or control boundary measures below 3:1 against its backdrop under WCAG 2.2 SC 1.4.11. ${HELP} Repair the runtime markup, or point the semantic property that fails at a design token that passes, then rerun check-component --tests. details.variables in --json names the semantic properties the finding traces to. An aria-snapshot finding means the component's accessibility tree changed since it was recorded: confirm the change is deliberate, then rerun with --update-aria.`,
|
|
30
|
+
},
|
|
31
|
+
...review,
|
|
32
|
+
'a11y-focus-visible': {
|
|
33
|
+
severity: 'warn',
|
|
34
|
+
repair: 'authored',
|
|
35
|
+
guidance: `Siteimprove Alfa rule SIA-R65 found no visible change when the element takes focus. ${HELP} Give the element a :focus-visible style, such as an outline, that differs from its resting style. Alfa reads only the element, its descendants, and its ancestors, so confirm by eye a focus style painted on a sibling.`,
|
|
36
|
+
},
|
|
37
|
+
'a11y-keyboard': {
|
|
38
|
+
severity: 'warn',
|
|
39
|
+
repair: 'authored',
|
|
40
|
+
guidance: `A keyboard case of the component's interaction contract failed: a key that should activate, move, or close did nothing. ${HELP} Handle the key on the element that holds focus, as the WAI-ARIA pattern for its role describes.`,
|
|
41
|
+
},
|
|
42
|
+
...adaptation,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const pageRules = {
|
|
46
|
+
'a11y-page': {
|
|
47
|
+
severity: 'warn',
|
|
48
|
+
repair: 'authored',
|
|
49
|
+
guidance: `axe found a WCAG 2 A or AA failure on the page, outside the shipped components. ${HELP} Repair the page markup at the line the finding names. Text contrast reports under page-contrast.`,
|
|
50
|
+
},
|
|
51
|
+
...review,
|
|
52
|
+
...adaptation,
|
|
53
|
+
};
|
package/bin/set-colors.mjs
CHANGED
package/bin/setup-claude.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { join } from 'node:path';
|
|
|
19
19
|
|
|
20
20
|
// The package owns this prefix in a project's skills directory. A skill without
|
|
21
21
|
// it is the project's own and is never touched, whatever the flags say.
|
|
22
|
-
const SKILL_PREFIX = 'live-tokens-';
|
|
22
|
+
export const SKILL_PREFIX = 'live-tokens-';
|
|
23
23
|
|
|
24
24
|
const SAMPLE_PROMPTS = {
|
|
25
25
|
'live-tokens-create-page': 'build a pricing page using live-tokens components',
|
|
@@ -44,7 +44,7 @@ export function runSetupClaude({ pkgRoot, cwd, force = false }) {
|
|
|
44
44
|
throw new Error(`No bundled skills found at ${srcSkills}. Is the package installed correctly?`);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const shipped = directoriesIn(srcSkills);
|
|
47
|
+
const shipped = directoriesIn(srcSkills).filter((name) => name.startsWith(SKILL_PREFIX));
|
|
48
48
|
if (shipped.length === 0) {
|
|
49
49
|
throw new Error('No bundled skills to install.');
|
|
50
50
|
}
|
|
@@ -89,7 +89,7 @@ var STOP_RE = new RegExp(String.raw`^(.*?)\s+(${NUM})%$`);
|
|
|
89
89
|
var CIRCLE_RADIUS_RE = new RegExp(String.raw`^circle\s+(${NUM})px$`, "i");
|
|
90
90
|
var ELLIPSE_RE = new RegExp(String.raw`^ellipse\s+(${NUM})px\s+(${NUM})px$`, "i");
|
|
91
91
|
var VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
|
|
92
|
-
function splitTopLevel(s) {
|
|
92
|
+
function splitTopLevel(s, isSeparator = (c) => c === ",") {
|
|
93
93
|
const parts = [];
|
|
94
94
|
let depth = 0;
|
|
95
95
|
let start = 0;
|
|
@@ -97,7 +97,7 @@ function splitTopLevel(s) {
|
|
|
97
97
|
const c = s[i];
|
|
98
98
|
if (c === "(") depth++;
|
|
99
99
|
else if (c === ")") depth--;
|
|
100
|
-
else if (
|
|
100
|
+
else if (depth === 0 && isSeparator(c)) {
|
|
101
101
|
parts.push(s.slice(start, i));
|
|
102
102
|
start = i + 1;
|
|
103
103
|
}
|