@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.
Files changed (206) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +5 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
  3. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
  4. package/CHANGELOG.md +309 -0
  5. package/README.md +2 -2
  6. package/bin/check-component.mjs +8 -0
  7. package/bin/check-page.mjs +6 -0
  8. package/bin/cli.mjs +36 -6
  9. package/bin/contractRunner.mjs +142 -17
  10. package/bin/lib/findings.mjs +20 -4
  11. package/bin/rules/a11y.mjs +53 -0
  12. package/bin/set-colors.mjs +1 -0
  13. package/bin/setup-claude.mjs +2 -2
  14. package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
  15. package/dist-plugin/index.cjs +148 -3
  16. package/dist-plugin/index.js +250 -105
  17. package/dist-plugin/setColors/index.cjs +7 -1
  18. package/dist-plugin/setColors/index.d.cts +9 -4
  19. package/dist-plugin/setColors/index.d.ts +9 -4
  20. package/dist-plugin/setColors/index.js +8 -2
  21. package/dist-plugin/setGeometry/index.d.cts +1 -1
  22. package/dist-plugin/setGeometry/index.d.ts +1 -1
  23. package/dist-plugin/setType/index.d.cts +1 -1
  24. package/dist-plugin/setType/index.d.ts +1 -1
  25. package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
  26. package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
  27. package/package.json +49 -2
  28. package/src/demo/LabeledSelect.svelte +1 -1
  29. package/src/editor/bootstrap.ts +3 -1
  30. package/src/editor/component-editor/BadgeEditor.svelte +13 -0
  31. package/src/editor/component-editor/ButtonEditor.svelte +33 -2
  32. package/src/editor/component-editor/CalloutEditor.svelte +13 -0
  33. package/src/editor/component-editor/CardEditor.svelte +12 -2
  34. package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
  35. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
  36. package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
  37. package/src/editor/component-editor/DialogEditor.svelte +15 -0
  38. package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
  39. package/src/editor/component-editor/ImageEditor.svelte +20 -9
  40. package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
  41. package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
  42. package/src/editor/component-editor/InputEditor.svelte +23 -0
  43. package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
  44. package/src/editor/component-editor/NotificationEditor.svelte +15 -0
  45. package/src/editor/component-editor/PanelEditor.svelte +1 -1
  46. package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
  47. package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
  48. package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
  49. package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
  50. package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
  51. package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
  52. package/src/editor/component-editor/SliderEditor.svelte +33 -0
  53. package/src/editor/component-editor/TabBarEditor.svelte +23 -2
  54. package/src/editor/component-editor/TableEditor.svelte +14 -0
  55. package/src/editor/component-editor/ToggleEditor.svelte +17 -0
  56. package/src/editor/component-editor/TooltipEditor.svelte +11 -2
  57. package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
  58. package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
  59. package/src/editor/component-editor/registration.ts +2 -1
  60. package/src/editor/component-editor/registry.ts +69 -27
  61. package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
  62. package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
  63. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
  64. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -10
  65. package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
  66. package/src/editor/core/a11y/a11yReport.ts +101 -0
  67. package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
  68. package/src/editor/core/a11y/contrastFailures.ts +22 -0
  69. package/src/editor/core/components/contrastPairRegistry.ts +18 -0
  70. package/src/editor/core/palettes/contrast.ts +44 -0
  71. package/src/editor/core/palettes/contrastPairs.ts +79 -0
  72. package/src/editor/core/palettes/resolveColor.ts +77 -0
  73. package/src/editor/core/store/editorPersistence.ts +9 -1
  74. package/src/editor/core/store/editorRenderer.ts +2 -0
  75. package/src/editor/core/store/editorStore.ts +29 -0
  76. package/src/editor/core/store/editorTypes.ts +9 -0
  77. package/src/editor/core/store/editorViewStore.ts +2 -2
  78. package/src/editor/core/store/unsavedEdits.ts +10 -0
  79. package/src/editor/core/themes/buildColors.ts +16 -5
  80. package/src/editor/core/themes/liveStateStream.ts +7 -1
  81. package/src/editor/core/themes/parsers/gradient.ts +5 -2
  82. package/src/editor/core/themes/slices/customColors.ts +226 -0
  83. package/src/editor/core/themes/themeTypes.ts +10 -0
  84. package/src/editor/docs/chapters.ts +1 -0
  85. package/src/editor/docs/content/accessibility.md +362 -0
  86. package/src/editor/docs/content/editing-tokens.md +56 -1
  87. package/src/editor/docs/content.generated.ts +2 -1
  88. package/src/editor/pages/ComponentEditorPage.svelte +10 -1
  89. package/src/editor/pages/EditorShell.svelte +97 -10
  90. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  91. package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
  92. package/src/editor/skill-atlas/trees/create-component.ts +12 -12
  93. package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
  94. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  95. package/src/editor/ui/GradientEditor.svelte +9 -1
  96. package/src/editor/ui/ThemePanel.svelte +4 -4
  97. package/src/editor/ui/UIContrastReadout.svelte +186 -0
  98. package/src/editor/ui/UIInfoPopover.svelte +16 -2
  99. package/src/editor/ui/UIPaletteSelector.svelte +347 -15
  100. package/src/editor/ui/UITokenSelector.svelte +33 -14
  101. package/src/editor/ui/VariablesTab.svelte +3 -0
  102. package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
  103. package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
  104. package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
  105. package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
  106. package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
  107. package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
  108. package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
  109. package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
  110. package/src/editor/ui/a11y/a11yView.ts +218 -0
  111. package/src/editor/ui/anchorToTrigger.ts +63 -0
  112. package/src/editor/ui/contrastReadout.ts +45 -0
  113. package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
  114. package/src/live-tokens/data/themes/autumn.json +20 -20
  115. package/src/live-tokens/data/themes/halloween.json +20 -20
  116. package/src/live-tokens/data/themes/midnight-study.json +20 -20
  117. package/src/live-tokens/data/themes/ocean.json +20 -20
  118. package/src/live-tokens/data/themes/royal-velvet.json +20 -20
  119. package/src/live-tokens/data/themes/sketchy.json +20 -20
  120. package/src/live-tokens/data/themes/spring-meadow.json +20 -20
  121. package/src/live-tokens/data/themes/sunset.json +20 -20
  122. package/src/system/components/Badge.svelte +3 -3
  123. package/src/system/components/Button.svelte +9 -3
  124. package/src/system/components/CodeSnippet.svelte +8 -1
  125. package/src/system/components/CornerBadge.svelte +3 -3
  126. package/src/system/components/Dialog.svelte +43 -14
  127. package/src/system/components/IconButton.svelte +1 -1
  128. package/src/system/components/Image.svelte +7 -0
  129. package/src/system/components/ImageLightbox.svelte +26 -26
  130. package/src/system/components/Input.svelte +8 -1
  131. package/src/system/components/MenuSelect.svelte +30 -2
  132. package/src/system/components/Notification.svelte +1 -1
  133. package/src/system/components/ProgressBar.svelte +6 -0
  134. package/src/system/components/RadioButton.svelte +36 -3
  135. package/src/system/components/SegmentedControl.svelte +36 -2
  136. package/src/system/components/SlidePager.svelte +2 -2
  137. package/src/system/components/Slider.svelte +13 -9
  138. package/src/system/components/TabBar.svelte +33 -3
  139. package/src/system/components/Toggle.svelte +2 -2
  140. package/src/system/components/VideoFrame.svelte +1 -1
  141. package/src/system/components/VideoLightbox.svelte +2 -2
  142. package/src/system/internal/focusTrap.ts +35 -0
  143. package/src/testing-js/__aria__/badge.yml +40 -0
  144. package/src/testing-js/__aria__/button.yml +88 -0
  145. package/src/testing-js/__aria__/callout.yml +12 -0
  146. package/src/testing-js/__aria__/card.yml +8 -0
  147. package/src/testing-js/__aria__/codesnippet.yml +21 -0
  148. package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
  149. package/src/testing-js/__aria__/cornerbadge.yml +24 -0
  150. package/src/testing-js/__aria__/dialog.yml +42 -0
  151. package/src/testing-js/__aria__/iconbutton.yml +57 -0
  152. package/src/testing-js/__aria__/image.yml +10 -0
  153. package/src/testing-js/__aria__/imagelightbox.yml +50 -0
  154. package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
  155. package/src/testing-js/__aria__/input.yml +61 -0
  156. package/src/testing-js/__aria__/menuselect.yml +30 -0
  157. package/src/testing-js/__aria__/notification.yml +30 -0
  158. package/src/testing-js/__aria__/panel.yml +2 -0
  159. package/src/testing-js/__aria__/progressbar.yml +2 -0
  160. package/src/testing-js/__aria__/radiobutton.yml +20 -0
  161. package/src/testing-js/__aria__/sectiondivider.yml +9 -0
  162. package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
  163. package/src/testing-js/__aria__/sidenavigation.yml +304 -0
  164. package/src/testing-js/__aria__/slidepager.yml +72 -0
  165. package/src/testing-js/__aria__/slider.yml +28 -0
  166. package/src/testing-js/__aria__/tabbar.yml +35 -0
  167. package/src/testing-js/__aria__/table.yml +144 -0
  168. package/src/testing-js/__aria__/toggle.yml +12 -0
  169. package/src/testing-js/__aria__/tooltip.yml +2 -0
  170. package/src/testing-js/__aria__/videoframe.yml +6 -0
  171. package/src/testing-js/__aria__/videolightbox.yml +12 -0
  172. package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
  173. package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
  174. package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
  175. package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
  176. package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
  177. package/src/testing-js/chunk-EWSGE27V.js +168 -0
  178. package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
  179. package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
  180. package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
  181. package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
  182. package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
  183. package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
  184. package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
  185. package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
  186. package/src/testing-js/component-a11y.contract.js +283 -0
  187. package/src/testing-js/component-a11y.contract.js.map +1 -0
  188. package/src/testing-js/component-alias.contract.js +2 -2
  189. package/src/testing-js/component-behavior.contract.js +2 -2
  190. package/src/testing-js/component-editor.contract.js +6 -5
  191. package/src/testing-js/component-editor.contract.js.map +1 -1
  192. package/src/testing-js/component-render.contract.js +10 -6
  193. package/src/testing-js/component-render.contract.js.map +1 -1
  194. package/src/testing-js/index.d.ts +44 -16
  195. package/src/testing-js/index.js +9 -9
  196. package/src/testing-js/page-compliance.contract.js +163 -18
  197. package/src/testing-js/page-compliance.contract.js.map +1 -1
  198. package/src/testing-js/vitest.js +3 -3
  199. package/template/package.json +9 -0
  200. package/src/editor/ui/keepInViewport.ts +0 -84
  201. package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
  202. package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
  203. package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
  204. package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
  205. /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
  206. /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
@@ -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
- function copyIsolatedDataDir(sourceDataDir) {
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 EDITOR_SUITE_POSITIONAL_RULES[index] ?? null;
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
- export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds }) {
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
- const finalRule = violation?.[1] ?? rule ?? 'tests-setup';
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 = ['page-component-paint', 'page-text-style', 'page-contrast', 'page-grid', 'page-overflow'];
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-z-]+) \| (.+) \| (\d+x\d+)$/;
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 [, titleRule, titleSource, titleViewport] = PAGE_TEST_TITLE_RE.exec(t.specTitle) ?? [];
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 ?? 'tests-setup',
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(id, { root = process.cwd(), dataDir: explicitDataDir } = {}) {
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();
@@ -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` out of argv.
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 = { off: [], warn: [], error: [], strict: false, json: false, tests: false, noFix: false, rest: [] };
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
- { check: label, checked, errors, warnings, ...(fix ? { fix } : {}), findings, ...(coverage ? { coverage } : {}) },
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
+ };
@@ -82,6 +82,7 @@ export async function runSetColors({
82
82
  const { colors, report } = buildColors(input, {
83
83
  cssVariables: live.cssVariables,
84
84
  gradients: live.gradients,
85
+ customColors: live.customColors,
85
86
  });
86
87
 
87
88
  const next = {
@@ -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 (c === "," && depth === 0) {
100
+ else if (depth === 0 && isSeparator(c)) {
101
101
  parts.push(s.slice(start, i));
102
102
  start = i + 1;
103
103
  }