@motion-proto/live-tokens 0.98.2 → 0.99.0

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 (69) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/bin/contractRunner.mjs +2 -2
  3. package/bin/rules/a11y.mjs +1 -1
  4. package/dist-plugin/index.cjs +71 -15
  5. package/dist-plugin/index.js +71 -15
  6. package/package.json +1 -1
  7. package/src/editor/bootstrap.ts +3 -1
  8. package/src/editor/component-editor/SectionDividerEditor.svelte +9 -10
  9. package/src/editor/core/a11y/a11yReport.ts +85 -22
  10. package/src/editor/core/a11y/a11yReportTypes.ts +11 -3
  11. package/src/editor/core/a11y/contrastFixes.ts +41 -0
  12. package/src/editor/core/palettes/contrastPairs.ts +4 -1
  13. package/src/editor/docs/content/accessibility.md +67 -18
  14. package/src/editor/docs/content/editing-tokens.md +4 -0
  15. package/src/editor/docs/content.generated.ts +2 -2
  16. package/src/editor/pages/ComponentEditorPage.svelte +4 -3
  17. package/src/editor/pages/EditorShell.svelte +10 -8
  18. package/src/editor/styles/ui-editor.css +2 -0
  19. package/src/editor/ui/a11y/A11yByEye.svelte +264 -0
  20. package/src/editor/ui/a11y/A11yComponentIssues.svelte +34 -16
  21. package/src/editor/ui/a11y/A11yFindingRow.svelte +66 -4
  22. package/src/editor/ui/a11y/A11yIssueRow.svelte +55 -24
  23. package/src/editor/ui/a11y/A11yOverview.svelte +72 -68
  24. package/src/editor/ui/a11y/A11yPanel.svelte +29 -4
  25. package/src/editor/ui/a11y/A11yPropertyMark.svelte +8 -8
  26. package/src/editor/ui/a11y/A11yRailMark.svelte +11 -4
  27. package/src/editor/ui/a11y/AccessibilityTab.svelte +22 -35
  28. package/src/editor/ui/a11y/a11yView.ts +50 -11
  29. package/src/live-tokens/data/colors-and-type/halloween.json +1 -1
  30. package/src/live-tokens/data/themes/autumn.json +16 -16
  31. package/src/live-tokens/data/themes/halloween.json +17 -17
  32. package/src/live-tokens/data/themes/midnight-study.json +16 -16
  33. package/src/live-tokens/data/themes/ocean.json +16 -16
  34. package/src/live-tokens/data/themes/royal-velvet.json +16 -16
  35. package/src/live-tokens/data/themes/sketchy.json +16 -16
  36. package/src/live-tokens/data/themes/spring-meadow.json +16 -16
  37. package/src/live-tokens/data/themes/sunset.json +16 -16
  38. package/src/system/components/Button.svelte +3 -3
  39. package/src/system/components/InlineEditActions.svelte +1 -1
  40. package/src/system/components/Input.svelte +1 -1
  41. package/src/system/components/MenuSelect.svelte +1 -1
  42. package/src/system/components/ProgressBar.svelte +1 -1
  43. package/src/system/components/SlidePager.svelte +1 -1
  44. package/src/system/components/Slider.svelte +6 -6
  45. package/src/system/components/TabBar.svelte +1 -1
  46. package/src/system/components/Toggle.svelte +1 -1
  47. package/src/testing-js/{chunk-RWH46IU5.js → chunk-7QXUJRSO.js} +2 -2
  48. package/src/testing-js/chunk-GZKUYLDW.js +193 -0
  49. package/src/testing-js/chunk-GZKUYLDW.js.map +1 -0
  50. package/src/testing-js/{chunk-SHLPDNAQ.js → chunk-RTZQLW3F.js} +172 -181
  51. package/src/testing-js/chunk-RTZQLW3F.js.map +1 -0
  52. package/src/testing-js/{chunk-5HNZHIHG.js → chunk-RWOT2XDB.js} +2 -2
  53. package/src/testing-js/{chunk-5HNZHIHG.js.map → chunk-RWOT2XDB.js.map} +1 -1
  54. package/src/testing-js/{chunk-R54I7YL6.js → chunk-THWGMSRH.js} +2 -2
  55. package/src/testing-js/component-a11y.contract.js +53 -23
  56. package/src/testing-js/component-a11y.contract.js.map +1 -1
  57. package/src/testing-js/component-behavior.contract.js +1 -1
  58. package/src/testing-js/component-editor.contract.js +3 -3
  59. package/src/testing-js/component-render.contract.js +3 -3
  60. package/src/testing-js/index.js +4 -4
  61. package/src/testing-js/index.js.map +1 -1
  62. package/src/testing-js/page-compliance.contract.js +3 -3
  63. package/src/testing-js/vitest.js +2 -2
  64. package/template/_gitignore +3 -0
  65. package/src/testing-js/chunk-EWSGE27V.js +0 -168
  66. package/src/testing-js/chunk-EWSGE27V.js.map +0 -1
  67. package/src/testing-js/chunk-SHLPDNAQ.js.map +0 -1
  68. /package/src/testing-js/{chunk-RWH46IU5.js.map → chunk-7QXUJRSO.js.map} +0 -0
  69. /package/src/testing-js/{chunk-R54I7YL6.js.map → chunk-THWGMSRH.js.map} +0 -0
@@ -1,6 +1,15 @@
1
- import { derived, writable, type Readable } from 'svelte/store';
1
+ import { derived, get, writable, type Readable } from 'svelte/store';
2
2
  import { API_BASE } from '../storage/apiBase';
3
- import type { A11yContrastFailure, A11yFinding, A11yFinishedReport, A11yReport } from './a11yReportTypes';
3
+ import { openThemeSlug } from '../store/editorConfigStore';
4
+ import type {
5
+ A11yContrastFailure,
6
+ A11yFailedReport,
7
+ A11yFinding,
8
+ A11yFinishedReport,
9
+ A11yReport,
10
+ A11yReportState,
11
+ A11yRunningReport,
12
+ } from './a11yReportTypes';
4
13
 
5
14
  /** One component's share of the last finished run, sorted by its fix (decision 6). */
6
15
  export interface ComponentA11yIssues {
@@ -10,44 +19,73 @@ export interface ComponentA11yIssues {
10
19
  property: A11yFinding[];
11
20
  /** Fix in the component's code: every other `a11y-*` finding but `a11y-review`. */
12
21
  code: A11yFinding[];
13
- /** Check by eye. */
22
+ /** Check by eye, unmarked. */
14
23
  review: A11yFinding[];
24
+ /** Check by eye, marked as looking right. They count as no issue. */
25
+ confirmed: A11yFinding[];
26
+ /** Contrast failures of the run that pass as the editor now measures them. They count as no issue. */
27
+ fixed: A11yFixedContrast[];
28
+ }
29
+
30
+ export interface A11yFixedContrast {
31
+ failure: A11yContrastFailure;
32
+ /** The ratio the editor measures now. */
33
+ ratio: number;
15
34
  }
16
35
 
17
36
  /** Engine rules whose failure a property's value fixes. */
18
37
  export const PROPERTY_FIX_RULES: ReadonlySet<string> = new Set(['focus-indicator-contrast', 'control-boundary-contrast']);
19
38
 
20
- const latest = writable<A11yReport | null>(null);
21
- const lastFinished = writable<A11yFinishedReport | null>(null);
39
+ const running = writable<A11yRunningReport | null>(null);
40
+ const failed = writable<A11yFailedReport | null>(null);
41
+ const reports = writable<Record<string, A11yFinishedReport>>({});
42
+ const fixedRatios = writable<ReadonlyMap<A11yContrastFailure, number>>(new Map());
22
43
  let heard = false;
23
44
 
24
- /** The dev server's latest word: a run going, finished or failed. */
25
- export const a11yReport: Readable<A11yReport | null> = { subscribe: latest.subscribe };
45
+ /** The dev server's latest word for the open theme: a run going (for any theme, since one runs at a time), a failed run, or the theme's last finished report. */
46
+ export const a11yReport: Readable<A11yReport | null> = derived(
47
+ [running, failed, reports, openThemeSlug],
48
+ ([$running, $failed, $reports, $open]) => $running ?? ($failed?.theme === $open ? $failed : null) ?? $reports[$open] ?? null,
49
+ );
26
50
 
27
- /** The last run that finished, which the markers follow while a new run goes (decision 7). */
28
- export const a11yFinishedReport: Readable<A11yFinishedReport | null> = { subscribe: lastFinished.subscribe };
51
+ /** The open theme's last finished run, which the markers follow while a new run goes (decision 7). */
52
+ export const a11yFinishedReport: Readable<A11yFinishedReport | null> = derived(
53
+ [reports, openThemeSlug],
54
+ ([$reports, $open]) => $reports[$open] ?? null,
55
+ );
56
+
57
+ /** Sets the run's contrast failures that pass now, with the ratio each measures. `contrastFixes` measures them. */
58
+ export function setFixedContrast(ratios: ReadonlyMap<A11yContrastFailure, number>): void {
59
+ fixedRatios.set(ratios);
60
+ }
29
61
 
30
- export const a11yIssues: Readable<Record<string, ComponentA11yIssues>> = derived(lastFinished, ($report) => {
62
+ export const a11yIssues: Readable<Record<string, ComponentA11yIssues>> = derived([a11yFinishedReport, fixedRatios], ([$report, $fixed]) => {
31
63
  const out: Record<string, ComponentA11yIssues> = {};
32
64
  if (!$report) return out;
33
- const entry = (id: string) => (out[id] ??= { contrast: [], property: [], code: [], review: [] });
34
- for (const failure of $report.contrast) entry(failure.component).contrast.push(failure);
65
+ const confirmed = new Set($report.confirmed);
66
+ const entry = (id: string) => (out[id] ??= { contrast: [], property: [], code: [], review: [], confirmed: [], fixed: [] });
67
+ for (const failure of $report.contrast) {
68
+ const ratio = $fixed.get(failure);
69
+ if (ratio === undefined) entry(failure.component).contrast.push(failure);
70
+ else entry(failure.component).fixed.push({ failure, ratio });
71
+ }
35
72
  for (const finding of $report.findings) {
36
73
  if (!finding.component || !finding.rule.startsWith('a11y-')) continue;
37
74
  const issues = entry(finding.component);
38
75
  const group = finding.rule === 'a11y-review'
39
- ? issues.review
76
+ ? (confirmed.has(finding.message) ? issues.confirmed : issues.review)
40
77
  : PROPERTY_FIX_RULES.has(finding.details?.engineRule ?? '') ? issues.property : issues.code;
41
78
  group.push(finding);
42
79
  }
43
80
  return out;
44
81
  });
45
82
 
46
- /** The run's findings under each component property they trace to (decision 7). The editor's contrast failures stay out. */
47
- export const a11yPropertyFindings: Readable<Map<string, A11yFinding[]>> = derived(lastFinished, ($report) => {
83
+ /** The run's findings under each component property they trace to (decision 7). The editor's contrast failures and the results marked as looking right stay out. */
84
+ export const a11yPropertyFindings: Readable<Map<string, A11yFinding[]>> = derived(a11yFinishedReport, ($report) => {
48
85
  const out = new Map<string, A11yFinding[]>();
86
+ const confirmed = new Set($report?.confirmed);
49
87
  for (const finding of $report?.findings ?? []) {
50
- if (!finding.component || !finding.rule.startsWith('a11y-')) continue;
88
+ if (!finding.component || !finding.rule.startsWith('a11y-') || confirmed.has(finding.message)) continue;
51
89
  for (const variable of finding.details?.variables ?? []) {
52
90
  const list = out.get(variable);
53
91
  if (list) list.push(finding);
@@ -59,16 +97,23 @@ export const a11yPropertyFindings: Readable<Map<string, A11yFinding[]>> = derive
59
97
 
60
98
  export function receiveA11yReport(report: A11yReport): void {
61
99
  heard = true;
62
- latest.set(report);
63
- if (report.state === 'finished') lastFinished.set(report);
100
+ if (report.state === 'running') {
101
+ running.set(report);
102
+ failed.set(null);
103
+ return;
104
+ }
105
+ running.set(null);
106
+ if (report.state === 'failed') failed.set(report);
107
+ else reports.update(($reports) => ({ ...$reports, [report.theme]: report }));
64
108
  }
65
109
 
66
110
  export async function init(): Promise<void> {
67
111
  const res = await fetch(`${API_BASE}/a11y/report`);
68
112
  if (!res.ok) return;
69
- const { report } = (await res.json()) as { report: A11yReport | null };
113
+ const state = (await res.json()) as A11yReportState;
70
114
  // An event that landed while the read was in flight is the newer word.
71
- if (report && !heard) receiveA11yReport(report);
115
+ reports.update(($reports) => ({ ...state.reports, ...$reports }));
116
+ if (!heard) running.set(state.running);
72
117
  }
73
118
 
74
119
  /**
@@ -94,8 +139,26 @@ export async function startA11yRun(contrast: A11yContrastFailure[]): Promise<A11
94
139
  return report;
95
140
  }
96
141
 
142
+ /** Marks results to check by eye in the open theme's last report as looking right, or clears the mark. */
143
+ export async function confirmReview(findings: A11yFinding[], confirmed: boolean): Promise<void> {
144
+ const theme = get(a11yFinishedReport)?.theme;
145
+ if (!theme) return;
146
+ const res = await fetch(`${API_BASE}/a11y/confirmed`, {
147
+ method: 'PUT',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify({ theme, messages: findings.map((f) => f.message), confirmed }),
150
+ });
151
+ if (!res.ok) {
152
+ const body = (await res.json().catch(() => ({}))) as { error?: string };
153
+ throw new Error(body.error || 'The mark did not save');
154
+ }
155
+ receiveA11yReport((await res.json()) as A11yReport);
156
+ }
157
+
97
158
  export function __resetForTests(): void {
98
159
  heard = false;
99
- latest.set(null);
100
- lastFinished.set(null);
160
+ running.set(null);
161
+ failed.set(null);
162
+ reports.set({});
163
+ fixedRatios.set(new Map());
101
164
  }
@@ -43,6 +43,8 @@ export interface A11yFinding {
43
43
 
44
44
  interface A11yReportBase {
45
45
  startedAt: string;
46
+ /** The theme the run tests, the one open when it started. */
47
+ theme: string;
46
48
  contrast: A11yContrastFailure[];
47
49
  }
48
50
 
@@ -54,12 +56,12 @@ export interface A11yRunningReport extends A11yReportBase {
54
56
  export interface A11yFinishedReport extends A11yReportBase {
55
57
  state: 'finished';
56
58
  finishedAt: string;
57
- /** Absent when the run stopped before it copied the data tree, as a missing tool stops it. */
58
- theme?: string;
59
59
  findings: A11yFinding[];
60
+ /** The messages of the results to check by eye that someone marked as looking right. A new run keeps each one it finds again. */
61
+ confirmed?: string[];
60
62
  }
61
63
 
62
- /** The CLI crashed or printed no report. */
64
+ /** The CLI crashed or printed no report. The theme's last finished report stands. */
63
65
  export interface A11yFailedReport extends A11yReportBase {
64
66
  state: 'failed';
65
67
  finishedAt: string;
@@ -67,3 +69,9 @@ export interface A11yFailedReport extends A11yReportBase {
67
69
  }
68
70
 
69
71
  export type A11yReport = A11yRunningReport | A11yFinishedReport | A11yFailedReport;
72
+
73
+ /** `GET /a11y/report`: the run going, if any, and each theme's last finished report by theme name. */
74
+ export interface A11yReportState {
75
+ running: A11yRunningReport | null;
76
+ reports: Record<string, A11yFinishedReport>;
77
+ }
@@ -0,0 +1,41 @@
1
+ import { get } from 'svelte/store';
2
+ import { getContrastPairs } from '../components/contrastPairRegistry';
3
+ import { measurePair, type ContrastPair, type Resolver } from '../palettes/contrastPairs';
4
+ import { createDomResolver } from '../palettes/resolveColor';
5
+ import { editorState } from '../store/editorStore';
6
+ import { THEME_APPLIED_EVENT } from '../themes/themeDocumentSync';
7
+ import { a11yFinishedReport, setFixedContrast } from './a11yReport';
8
+ import type { A11yContrastFailure } from './a11yReportTypes';
9
+
10
+ function samePair(pair: ContrastPair, failure: A11yContrastFailure): boolean {
11
+ return pair.role === failure.role
12
+ && pair.context === failure.context
13
+ && pair.backdrop.join('\n') === failure.backdrop.join('\n');
14
+ }
15
+
16
+ /** Each failure whose pair passes now, with the ratio it measures. */
17
+ export function measureFixes(failures: A11yContrastFailure[], resolver: Resolver): Map<A11yContrastFailure, number> {
18
+ const out = new Map<A11yContrastFailure, number>();
19
+ for (const failure of failures) {
20
+ const pair = getContrastPairs(failure.component, failure.variable).find((p) => samePair(p, failure));
21
+ if (!pair) continue;
22
+ const measure = measurePair(pair, resolver);
23
+ if (measure.passes) out.set(failure, measure.ratio);
24
+ }
25
+ return out;
26
+ }
27
+
28
+ /** Re-measures the last run's contrast failures whenever the editor's state or the open theme changes. */
29
+ export function init(): void {
30
+ let frame = 0;
31
+ // Measuring reads the DOM, so it waits a frame for the renderer's CSS variables.
32
+ const schedule = () => {
33
+ cancelAnimationFrame(frame);
34
+ frame = requestAnimationFrame(() => {
35
+ setFixedContrast(measureFixes(get(a11yFinishedReport)?.contrast ?? [], createDomResolver()));
36
+ });
37
+ };
38
+ a11yFinishedReport.subscribe(schedule);
39
+ editorState.subscribe(schedule);
40
+ document.addEventListener(THEME_APPLIED_EVENT, schedule);
41
+ }
@@ -68,7 +68,10 @@ export function measurePair(pair: ContrastPair, resolver: Resolver): PairMeasure
68
68
  pair.fontWeight ? resolver.fontWeight(pair.fontWeight) : NORMAL_WEIGHT,
69
69
  );
70
70
  const floor = floorFor(pair.role, large);
71
- return { pair, ratio, floor, large, passes: pair.inactive === true || ratio >= floor };
71
+ // A non-text part painted fully transparent, such as an unselected tab's
72
+ // underline, draws nothing for SC 1.4.11 to measure.
73
+ const absent = pair.role === 'non-text' && fronts.every((front) => front[3] === 0);
74
+ return { pair, ratio, floor, large, passes: pair.inactive === true || absent || ratio >= floor };
72
75
  }
73
76
 
74
77
  /** An inactive pair applies no floor, so it ranks only when every pair is inactive. */
@@ -83,12 +83,19 @@ sibling, such as a slider thumb's cap, counts. The indicator is the outline
83
83
  when focus changes one, else the first outer box shadow, else a border side.
84
84
 
85
85
  The backdrop is every box behind the ring's outer edge, composited in order
86
- over white. A ring below 3:1 against it reports under `a11y-component` with
86
+ over white. The walk stops at the preview's edge and ends on `--page-bg`, so a
87
+ part that fills its preview edge to edge is measured against the page. A ring below 3:1 against it reports under `a11y-component` with
87
88
  engine rule `focus-indicator-contrast`, such as "Focus outline on span.cap
88
89
  2.14:1 against the backdrop, needs 3:1". Ratios floor to two places, because
89
90
  WCAG does not round. Chromium's own `outline-style: auto` ring passes: it pairs
90
91
  a dark band with a light one. An element with no indicator at all is
91
- SIA-R65's finding and adds none here.
92
+ SIA-R65's finding and adds none here. Alfa reads only the element, its
93
+ descendants, and its ancestors, so a ring this check finds on a sibling settles
94
+ SIA-R65's "can't tell" for that element.
95
+
96
+ A state view that forces the focused look with a class, such as Input's
97
+ focused state, would leave focus nothing to change. Both focus checks lift
98
+ that class while they measure, then put it back.
92
99
 
93
100
  ## Control boundary contrast
94
101
 
@@ -236,10 +243,19 @@ three and a half minutes. With edits pending, it shows "Save the changes you
236
243
  want to have checked." and starts nothing. Save the theme first, so the run
237
244
  and the editor read the same values.
238
245
 
239
- Every finding counts, results to check by eye included. An overview opens
240
- the report. It shows the total, the count in each repair group below, and a
241
- table of every component with a finding: its count per group and a bar of
242
- its share. A component's name in the table scrolls to its section.
246
+ Each theme keeps its own report. Opening another theme shows that theme's
247
+ last run, or none if it has never run. The dev server keeps each report in
248
+ `_a11y-reports/` in the data directory, and deleting a theme deletes its
249
+ report. A failed run leaves the theme's last report in place.
250
+
251
+ The total counts the findings a fix clears. A result to check by eye counts
252
+ apart, because only a person can settle it. An overview opens the report. It
253
+ shows the total, the count in each repair group below, and a table of every
254
+ component with a finding and its count per group. The "Check by eye" figure
255
+ scrolls to the By eye list. The table
256
+ lists first the components with the most findings a property fixes, then those
257
+ with the most a code change fixes. A component's name in the table scrolls to
258
+ its section.
243
259
 
244
260
  Each component's section heads its count with **Open component**, which opens
245
261
  the component's page at its accessibility panel. The section sorts its rows
@@ -251,7 +267,8 @@ by repair:
251
267
  - **Fix in the component's code** holds every other finding: axe failures,
252
268
  focus styles that SIA-R65 finds unchanged, failed keyboard cases, text that
253
269
  clips under the spacing override, and motion that ignores reduced motion.
254
- - **Check by eye** holds the `a11y-review` results.
270
+ - **Check by eye** holds the `a11y-review` results, each led by an eye. The
271
+ By eye list gathers them for the whole report.
255
272
 
256
273
  Findings that share an engine rule, a help text, and their traced properties
257
274
  merge into one row. The row names its element, or its element count, and the
@@ -265,15 +282,33 @@ check by eye is grey. The row's links sit beside its text:
265
282
  "Rule" for an axe or Alfa rule, "Success criterion" with its number for a
266
283
  WCAG criterion, "Technique" for a WCAG technique, and "Pattern" for a
267
284
  WAI-ARIA pattern.
285
+ - A result to check by eye has **Looks right** at the row's right edge.
286
+ Press it once you have judged the element and found it passes. The row's
287
+ eye turns to a check, its text dims, and it stops counting and stops
288
+ marking the editor. Press it again to clear the mark. The mark belongs to
289
+ the open theme's report, and each later run of that theme keeps it while
290
+ the run finds the same result.
268
291
 
269
292
  A property's label leads with the words from its variable name that the
270
293
  editor's label leaves out, so `--input-focused-border` reads "Focused border
271
294
  color" where the editor's row reads "Border color". The contrast rows read
272
295
  the same labels.
273
296
 
274
- The overview and the per-component counts include the results to check by
275
- eye. A component's section ends in a "N results to check by eye" button that
276
- opens one closed list of those results, merged per component. axe's contrast
297
+ The overview's "Check by eye" column counts the results no one has marked.
298
+ A component whose results all look right leaves the table, the sections, and
299
+ the view's sidebar. A component section lists only issues and fixes, so the
300
+ view's sidebar lists the components with one of those.
301
+
302
+ The **By eye** list closes the view and holds every result to check by eye,
303
+ merged per component. The results to check come first, under a tally such
304
+ as "2 to check, 18 look right" and a bar that fills as you mark them. Pressing
305
+ **Looks right** moves focus to the result that takes the row's place, so
306
+ Enter or Space walks the list. **Mark all as looking right** marks every
307
+ result still to check in one step. The marked results follow in **Looks
308
+ right**, a group that starts closed and shows their count. Open it to
309
+ revisit them, each with its button pressed, or to clear a mark. A component's
310
+ section with results still to check ends in a link such as "2 results to
311
+ check by eye" that scrolls to the list. axe's contrast
277
312
  violations stay out of the report, because the contrast checker measures the
278
313
  same pairs.
279
314
 
@@ -289,18 +324,25 @@ the `default` theme. Both work from the command line too.
289
324
 
290
325
  ## Marks in the editor
291
326
 
292
- The last finished run marks the rest of the editor:
327
+ The last finished run marks the rest of the editor. A result marked as
328
+ looking right is acknowledged and marks nothing: no sidebar mark, no panel,
329
+ and no property mark. The By eye list keeps it to revisit.
293
330
 
294
- - The component list leads the name of each component with any finding
295
- with a warning triangle. A condensed list shows the triangle on the
331
+ - The component list leads the name of each component with a finding with a
332
+ warning triangle, or with a grey eye when its only results are unmarked
333
+ results to check by eye. A condensed list shows the mark on the
296
334
  component's icon.
297
335
  - That component's page opens with a panel under its title that lists its
298
- findings in all three groups, check by eye included.
336
+ findings in all three groups, check by eye included, and leaves out the
337
+ results marked as looking right. The panel heads with
338
+ the triangle and the issue count, or with the eye and the count to check by
339
+ eye when no issue remains.
299
340
  - Each "→" in the panel opens the tab holding the property and highlights
300
341
  its row.
301
342
  - Each property row a finding traces to carries a mark beside its label: a
302
- warning triangle in the highlight colour, or a grey "?" when every finding on the row is a
303
- result to check by eye. On a colour or size row the mark sits between the
343
+ warning triangle in the highlight colour, or a grey eye when every finding
344
+ on the row is a result to check by eye. A result marked as looking right
345
+ adds no mark. On a colour or size row the mark sits between the
304
346
  label and the value. On a toggle row it follows the label.
305
347
 
306
348
  Click a row's mark to open its findings: the count, then each merged row
@@ -308,10 +350,17 @@ with its element, its views, its help page, and the properties it traces to. The
308
350
  editor's own contrast failures add no row mark, because the contrast readout
309
351
  on the colour row already shows them.
310
352
 
311
- The marks and the panel keep the run's time and change when the next run
312
- finishes. The contrast readout on each colour row stays live and measures
353
+ The marks and the panel keep the run's time and change when the open
354
+ theme's next run finishes. The contrast readout on each colour row stays live and measures
313
355
  every edit as you make it.
314
356
 
357
+ The editor also re-measures each contrast failure of the run as you edit.
358
+ Once one passes, its row in the panel and in the Accessibility view turns
359
+ green and shows the run's ratio beside the ratio now, such as
360
+ "Fill color 2.66:1 → 5.31:1". A fixed row counts as no issue, so a component
361
+ whose only issues are fixed loses its triangle in the component list. The
362
+ run's other findings change only when the next run finishes.
363
+
315
364
  ## Installing the engines
316
365
 
317
366
  The engines ship as optional peer dependencies. A project made with `create`
@@ -125,6 +125,10 @@ it. Each edit and each theme load updates it, and the rows keep their places.
125
125
  header and the body text on the page, so its popover lists two pairs.
126
126
  - **Disabled states.** WCAG exempts disabled controls, so a disabled state's
127
127
  ratio shows in a dimmer grey, whatever its value.
128
+ - **Transparent marks.** A mark painted fully transparent, such as an
129
+ unselected tab's underline, draws nothing, so it meets its floor at any
130
+ ratio. Transparent text still needs its floor. A decorative line, such as
131
+ a section divider's hairline, shows no readout.
128
132
  - **Page background.** Every stack ends on the page background, `--page-bg`.
129
133
  A translucent layer blends into the layers beneath it, and the readout
130
134
  measures the blend. A gradient layer counts its worst stop.
@@ -3,9 +3,9 @@
3
3
 
4
4
  export const docContent: Record<string, string> = {
5
5
  "01-overview": "# Overview\n\nLiveTokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button,\n IconButton, Card, Dialog, Table, and more). You style components by changing\n the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Sketch mode](sketch-mode.md)**: redraw the page by hand.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
6
- "accessibility": "# Accessibility\n\n`check-component --tests` and `check-page --tests` test the shipped\ncomponents, your own components, and your pages against WCAG 2 A and AA. Every\naccessibility finding reports at `warn`: the run prints it and leaves the exit\nstatus to the other rules. `--strict` turns each one into an error that fails\nthe run, as it does every other warning.\n\n```bash\nnpx live-tokens check-component statcard --tests\nnpx live-tokens check-page src/pages/Home.svelte --tests --strict\n```\n\n## Suites and checks\n\n**Components.** The component suite opens every variant and state the\ncomponent's contract names and scans the preview:\n\n- **axe-core** checks names, roles, ARIA, contrast, target size, and\n structure.\n- **Siteimprove Alfa**, rule SIA-R65, checks that each focusable element\n looks different when it takes focus.\n- **Focus indicator contrast** measures each focus ring against the backdrop\n around it, at 3:1 under WCAG 2.2 SC 1.4.11.\n- **Control boundary contrast** measures each part the contract lists in\n `boundaries` against the backdrop behind it, at 3:1 under WCAG 2.2\n SC 1.4.11.\n- **Text spacing** applies the WCAG 2.2 SC 1.4.12 override inside the\n component and reports text that clips under it.\n- **Reduced motion** reports motion longer than 200 ms that still runs under\n `prefers-reduced-motion: reduce`.\n- **Playwright** reads each view's accessibility tree and compares it with\n the tree recorded for that view.\n- **Keyboard cases** in the contract press Enter, Space, the arrow keys, and\n Escape, and read what each key did.\n\nA project's run scans the open theme. The library's own test run also repeats\naxe's contrast rule, and both contrast checks, under each of the eight preset\nthemes.\n\n**Pages.** `check-page --tests` runs three tests on each page at each\nviewport:\n\n- `page-a11y` runs axe over the page, leaving out the router's chrome and\n every component the component suite scans. axe's contrast rule stays off here, because\n `page-contrast` already reports text contrast at `error`.\n- `page-text-spacing` applies the WCAG 2.2 SC 1.4.12 text spacing override\n (line height 1.5, paragraph spacing 2em, letter spacing 0.12em, word\n spacing 0.16em) and reports content that overflows under it.\n- `page-reduced-motion` loads the page under `prefers-reduced-motion: reduce`\n and reports animations that keep running.\n\n## Rules\n\n| Rule | Checker | Reports |\n|------|---------|---------|\n| `a11y-component` | `check-component` | An axe failure inside a component's preview, a focus ring or control boundary below 3:1, or a changed accessibility tree. |\n| `a11y-focus-visible` | `check-component` | An element that looks the same focused as at rest (SIA-R65). |\n| `a11y-keyboard` | `check-component` | A keyboard case whose key failed to activate, move, or close. |\n| `a11y-page` | `check-page` | An axe failure in the page's own markup. |\n| `a11y-text-spacing` | `check-component`, `check-page` | Content that overflows under the text spacing override. |\n| `a11y-reduced-motion` | `check-component`, `check-page` | Motion that still runs under reduced motion. |\n| `a11y-review` | both | A check the engine could not decide, for you to judge by eye. |\n\nEach finding's message names the engine's own rule, the element, the view it\nfailed in, and a help page that states the requirement and its repair. The\nrules take the same severity settings as every other rule. `--off=a11y-review`\nsilences one for a single run, `--error=a11y-page` raises one, and\n`live-tokens.config.json` records either for good:\n\n```json\n{ \"checks\": { \"rules\": { \"a11y-review\": \"off\", \"a11y-page\": \"error\" } } }\n```\n\nA scan that crashes reports `tests-setup`.\n\n## Focus indicator contrast\n\nThe suite forces `:focus` and `:focus-visible` on each focusable element in\neach view and looks for the style focus changes. It searches the element, its\ndescendants, and its siblings with their descendants, so a ring painted on a\nsibling, such as a slider thumb's cap, counts. The indicator is the outline\nwhen focus changes one, else the first outer box shadow, else a border side.\n\nThe backdrop is every box behind the ring's outer edge, composited in order\nover white. A ring below 3:1 against it reports under `a11y-component` with\nengine rule `focus-indicator-contrast`, such as \"Focus outline on span.cap\n2.14:1 against the backdrop, needs 3:1\". Ratios floor to two places, because\nWCAG does not round. Chromium's own `outline-style: auto` ring passes: it pairs\na dark band with a light one. An element with no indicator at all is\nSIA-R65's finding and adds none here.\n\n## Control boundary contrast\n\nA control whose edge shows where to click or what it holds needs that edge\nat 3:1 against the backdrop behind it. So does a selected or current item\nwhose surface or edge alone shows that state. The contract lists these parts\nin `boundaries`. Each part passes when its border, on any side, or its\noutline, 1px or wider, or its surface reaches 3:1. A failure names both ratios, such as \"Track border\n1.45:1 and surface 1.3:1 against the backdrop, needs 3:1\", and reports under\n`a11y-component` with engine rule `control-boundary-contrast`. A part whose\nform controls are all disabled is skipped, because SC 1.4.11 exempts inactive\ncontrols.\n\nThe shipped controls that draw their own edge declare it: Input's field,\nRadioButton's circle, Slider's and Toggle's track and thumb, and\nSegmentedControl's bar. The selected states that show by surface and edge\nalone declare their part too: TabBar's selected tab, SegmentedControl's\nselected option, and SideNavigation's selected title, section, item and\nfooter. Each measures against the bar or panel behind it, the surface the\nunselected items show. MenuSelect's selected option carries a check mark, so\nit declares none.\n\n## Text spacing and motion per component\n\nThe text spacing check measures every element of the component that holds\ntext: its root part, its portaled parts, and their descendants. It applies\nthe override inside the preview alone and reports each element the override\npushes more than 1px past its box, on an axis it does not scroll. An element\nalready cut at rest adds nothing. The help names the measure: \"Under the text\nspacing override, `<span>` holds 24px of content in 18px of height, and its\noverflow-y is hidden\".\n\nThe reduced motion check reads the declared animations and transitions of\nthe same elements and their `::before` and `::after`, along with every\nscript animation running on them. Motion that lasts past 200 ms, its delay\nincluded, is recorded. The check then emulates\n`prefers-reduced-motion: reduce` in the same view and reports each motion\nstill that long, such as \"A transition of left lasts 500 ms under\nprefers-reduced-motion: reduce\". A view with no long motion costs no second\nread. The help page is WCAG technique C39, which serves WCAG 2.2 SC 2.3.3\nAnimation from Interactions.\n\n## Property tracing\n\nA finding that a property can fix names that property. The suite reads the\nCSS rules that match the failing element through the Chrome DevTools\nprotocol, keeps the declarations the finding depends on, such as the outline\nand border of a focus ring or the height and padding of a clipped label, and\nfollows each `var()` to the component's semantic properties and intrinsics.\nA component's private custom properties lead through to the property behind\nthem. Theme and editor variables drop out.\n\n`--json` output carries the result in each finding's `details.variables`,\nsorted, and leaves the field out when the trace finds nothing. An excerpt:\n\n```json\n{\n \"rule\": \"a11y-component\",\n \"component\": \"slider\",\n \"details\": {\n \"engineRule\": \"focus-indicator-contrast\",\n \"target\": \"span.cap\",\n \"variables\": [\"--slider-range-thumb-border\", \"--slider-range-thumb-border-width\"]\n }\n}\n```\n\naxe's contrast and target size rules, SIA-R65, both contrast checks, text\nspacing, and reduced motion trace. The ARIA rules, the keyboard cases, and\nthe accessibility tree mark the component alone.\n\n## Review findings\n\n`a11y-review` collects the checks an engine left undecided. axe reads contrast\nfrom the colour behind the text, so it returns no verdict for text over a\ngradient, an image, or a backdrop, or under a pseudo-element that covers it.\nThe focus indicator and control boundary checks do the same for a ring or an\nedge over a gradient or an image. Judge each element by eye against the help\npage the finding names.\n\nTwo sources account for most of them. Button's shimmer pseudo-element and its\ntinted hover surfaces leave every Button view undecided, so a Button run lists\nmany `a11y-review` warnings. A theme that paints gradient surfaces does the\nsame for every component that sits on one.\n\n## Accessibility trees\n\nThe package records each shipped component's accessibility tree, one block\nper view, in `src/testing/contracts/__aria__/<id>.yml`. A run that reads a\ndifferent tree reports `a11y-component` with engine rule `aria-snapshot` and\nthe lines that changed.\n\nInside the live-tokens repository, after a deliberate markup change,\n`--update-aria` records the tree anew, and `LIVE_TOKENS_UPDATE_ARIA=1 npm run\ntest:e2e:contract` rewrites every file:\n\n```bash\nnode bin/cli.mjs check-component button --tests --update-aria\n```\n\nIn a project the trees sit under `node_modules`, where the suite reads them\nand never writes. A project's own components therefore have no recorded tree\nyet.\n\n## Keyboard cases\n\nA component contract declares its keyboard cases among its interaction\ncases, each marked `rule: 'a11y-keyboard'`: Enter or Space activates, an\narrow key moves where the component's WAI-ARIA pattern moves, and Escape\ncloses an overlay. A failed keyboard case reports `a11y-keyboard` at `warn`.\nA case without the mark fails as `contract-interaction`, an error. The\n`live-tokens-create-component` skill writes these cases for a new component,\nand its `references/contract-tests.md` describes their shape.\n\nAn editor that adds placeholder content to its preview, such as a demo label\nor a hover target, marks it `data-preview-demo`, and the scans skip it.\n\n## Boundaries in the contract\n\n`boundaries` is an optional contract field,\n`{ part: string; label: string; when?: string }[]`, for the parts that draw a\ncontrol's visible edge or show its selected state. `part` is a key of the\ncontract's `parts`, and `label` names the part in a finding. `when` is a\nselector the part must also match to be measured, for a part that carries its\nselected state as a class in some views only.\n\n```ts\nboundaries: [\n { part: 'track', label: 'Track' },\n { part: 'thumb', label: 'Thumb' },\n],\n```\n\nList a part whose border or surface is the only thing that shows the control\nis there, a thumb whose position shows the value, or a selected item whose\nsurface or edge is the only thing that shows it is selected. Leave out a part whose\ntext or icon already shows it. A contract without the field runs no boundary\ncheck.\n\n## Accessibility view\n\nThe editor's Accessibility view runs the component checks from the browser.\n**Run test** checks every component in the open theme as saved, in about\nthree and a half minutes. With edits pending, it shows \"Save the changes you\nwant to have checked.\" and starts nothing. Save the theme first, so the run\nand the editor read the same values.\n\nEvery finding counts, results to check by eye included. An overview opens\nthe report. It shows the total, the count in each repair group below, and a\ntable of every component with a finding: its count per group and a bar of\nits share. A component's name in the table scrolls to its section.\n\nEach component's section heads its count with **Open component**, which opens\nthe component's page at its accessibility panel. The section sorts its rows\nby repair:\n\n- **Fix with a property** holds each colour pair below its WCAG floor, as the\n editor's contrast checker measures it, such as \"Brand text color 3.87:1,\n needs 4.5:1\", and every focus ring and control boundary below 3:1.\n- **Fix in the component's code** holds every other finding: axe failures,\n focus styles that SIA-R65 finds unchanged, failed keyboard cases, text that\n clips under the spacing override, and motion that ignores reduced motion.\n- **Check by eye** holds the `a11y-review` results.\n\nFindings that share an engine rule, a help text, and their traced properties\nmerge into one row. The row names its element, or its element count, and the\nviews it failed in, up to four. An issue's text is amber, and a result to\ncheck by eye is grey. The row's links sit beside its text:\n\n- Each property the row traces to links with a \"→\" to its row on the\n component's page, which scrolls to the row and highlights it. A row that\n traces to no property links to the component's page instead.\n- The link on the right opens the page the finding cites and names its kind:\n \"Rule\" for an axe or Alfa rule, \"Success criterion\" with its number for a\n WCAG criterion, \"Technique\" for a WCAG technique, and \"Pattern\" for a\n WAI-ARIA pattern.\n\nA property's label leads with the words from its variable name that the\neditor's label leaves out, so `--input-focused-border` reads \"Focused border\ncolor\" where the editor's row reads \"Border color\". The contrast rows read\nthe same labels.\n\nThe overview and the per-component counts include the results to check by\neye. A component's section ends in a \"N results to check by eye\" button that\nopens one closed list of those results, merged per component. axe's contrast\nviolations stay out of the report, because the contrast checker measures the\nsame pairs.\n\nWhen Playwright, axe, or Alfa is missing, the report names its install\ncommand and Run test stays disabled. Install the package and press **Check\nagain**. The view runs its tests through the dev server, so it exists under\nthe dev server alone, with the rest of the editor.\n\nThe view runs `check-component --tests --a11y --no-fix --json --open-theme`.\n`--a11y` runs the accessibility suite and the keyboard cases alone, and\n`--open-theme` tests the theme the editor has open. A run without it tests\nthe `default` theme. Both work from the command line too.\n\n## Marks in the editor\n\nThe last finished run marks the rest of the editor:\n\n- The component list leads the name of each component with any finding\n with a warning triangle. A condensed list shows the triangle on the\n component's icon.\n- That component's page opens with a panel under its title that lists its\n findings in all three groups, check by eye included.\n- Each \"→\" in the panel opens the tab holding the property and highlights\n its row.\n- Each property row a finding traces to carries a mark beside its label: a\n warning triangle in the highlight colour, or a grey \"?\" when every finding on the row is a\n result to check by eye. On a colour or size row the mark sits between the\n label and the value. On a toggle row it follows the label.\n\nClick a row's mark to open its findings: the count, then each merged row\nwith its element, its views, its help page, and the properties it traces to. The\neditor's own contrast failures add no row mark, because the contrast readout\non the colour row already shows them.\n\nThe marks and the panel keep the run's time and change when the next run\nfinishes. The contrast readout on each colour row stays live and measures\nevery edit as you make it.\n\n## Installing the engines\n\nThe engines ship as optional peer dependencies. A project made with `create`\nhas them. Any other project installs them beside `@playwright/test`:\n\n```bash\nnpm install -D axe-core @axe-core/playwright \\\n @siteimprove/alfa-act@^0.119.0 @siteimprove/alfa-device@^0.119.0 \\\n @siteimprove/alfa-dom@^0.119.0 @siteimprove/alfa-http@^0.119.0 \\\n @siteimprove/alfa-rules@^0.119.0 @siteimprove/alfa-url@^0.119.0 \\\n @siteimprove/alfa-web@^0.119.0\n```\n\nThe Alfa packages share one minor version. axe-core needs 4.11.1 or later to\nread OKLCH colours, which every shipped theme uses. A test whose engine is\nmissing reports as inapplicable, and its coverage names the package. A\nmissing engine leaves the exit status to the other rules, under `--strict`\ntoo.\n\n## Limits\n\n- The page's text spacing test measures widths, so it reports horizontal\n overflow alone. The component check measures both axes.\n- The page's reduced motion test reports animations still running 200 ms\n after the page loads. A short entrance animation finishes first and goes\n unseen.\n- The component's reduced motion check reads the preference within each\n view. A script that reads it only when the component mounts goes unseen.\n- The page scan leaves contrast to `page-contrast` and focus visibility to\n the component suite.\n- SIA-R65 reads the focused element, its descendants, and its ancestors, so\n it reports a focus ring painted on a sibling as missing. The focus\n indicator check measures that ring; confirm it by eye.\n- A view that forces the focused look at rest, such as Input's focused\n state, leaves real focus nothing to change, so SIA-R65 reports it there.\n The same element passes in the views that leave it at rest.\n- A finding traces to the component's own semantic properties and\n intrinsics. A ring or colour that a theme token paints directly, such as\n SegmentedControl's `--border-neutral-strong` focus ring, traces to\n nothing. A finding on a component nested\n inside another, such as a Button inside a Dialog, marks the host and names\n none of the nested component's properties.\n- A literal value in the component's code, such as a transition's duration,\n has no property to name. The finding traces to whatever property governs\n the same declaration, or to none.\n- A project's own components get no recorded accessibility tree.\n- Automated checks find part of what WCAG asks. Reading order, the meaning of\n labels and alt text, and screen reader behaviour still need a person.\n",
6
+ "accessibility": "# Accessibility\n\n`check-component --tests` and `check-page --tests` test the shipped\ncomponents, your own components, and your pages against WCAG 2 A and AA. Every\naccessibility finding reports at `warn`: the run prints it and leaves the exit\nstatus to the other rules. `--strict` turns each one into an error that fails\nthe run, as it does every other warning.\n\n```bash\nnpx live-tokens check-component statcard --tests\nnpx live-tokens check-page src/pages/Home.svelte --tests --strict\n```\n\n## Suites and checks\n\n**Components.** The component suite opens every variant and state the\ncomponent's contract names and scans the preview:\n\n- **axe-core** checks names, roles, ARIA, contrast, target size, and\n structure.\n- **Siteimprove Alfa**, rule SIA-R65, checks that each focusable element\n looks different when it takes focus.\n- **Focus indicator contrast** measures each focus ring against the backdrop\n around it, at 3:1 under WCAG 2.2 SC 1.4.11.\n- **Control boundary contrast** measures each part the contract lists in\n `boundaries` against the backdrop behind it, at 3:1 under WCAG 2.2\n SC 1.4.11.\n- **Text spacing** applies the WCAG 2.2 SC 1.4.12 override inside the\n component and reports text that clips under it.\n- **Reduced motion** reports motion longer than 200 ms that still runs under\n `prefers-reduced-motion: reduce`.\n- **Playwright** reads each view's accessibility tree and compares it with\n the tree recorded for that view.\n- **Keyboard cases** in the contract press Enter, Space, the arrow keys, and\n Escape, and read what each key did.\n\nA project's run scans the open theme. The library's own test run also repeats\naxe's contrast rule, and both contrast checks, under each of the eight preset\nthemes.\n\n**Pages.** `check-page --tests` runs three tests on each page at each\nviewport:\n\n- `page-a11y` runs axe over the page, leaving out the router's chrome and\n every component the component suite scans. axe's contrast rule stays off here, because\n `page-contrast` already reports text contrast at `error`.\n- `page-text-spacing` applies the WCAG 2.2 SC 1.4.12 text spacing override\n (line height 1.5, paragraph spacing 2em, letter spacing 0.12em, word\n spacing 0.16em) and reports content that overflows under it.\n- `page-reduced-motion` loads the page under `prefers-reduced-motion: reduce`\n and reports animations that keep running.\n\n## Rules\n\n| Rule | Checker | Reports |\n|------|---------|---------|\n| `a11y-component` | `check-component` | An axe failure inside a component's preview, a focus ring or control boundary below 3:1, or a changed accessibility tree. |\n| `a11y-focus-visible` | `check-component` | An element that looks the same focused as at rest (SIA-R65). |\n| `a11y-keyboard` | `check-component` | A keyboard case whose key failed to activate, move, or close. |\n| `a11y-page` | `check-page` | An axe failure in the page's own markup. |\n| `a11y-text-spacing` | `check-component`, `check-page` | Content that overflows under the text spacing override. |\n| `a11y-reduced-motion` | `check-component`, `check-page` | Motion that still runs under reduced motion. |\n| `a11y-review` | both | A check the engine could not decide, for you to judge by eye. |\n\nEach finding's message names the engine's own rule, the element, the view it\nfailed in, and a help page that states the requirement and its repair. The\nrules take the same severity settings as every other rule. `--off=a11y-review`\nsilences one for a single run, `--error=a11y-page` raises one, and\n`live-tokens.config.json` records either for good:\n\n```json\n{ \"checks\": { \"rules\": { \"a11y-review\": \"off\", \"a11y-page\": \"error\" } } }\n```\n\nA scan that crashes reports `tests-setup`.\n\n## Focus indicator contrast\n\nThe suite forces `:focus` and `:focus-visible` on each focusable element in\neach view and looks for the style focus changes. It searches the element, its\ndescendants, and its siblings with their descendants, so a ring painted on a\nsibling, such as a slider thumb's cap, counts. The indicator is the outline\nwhen focus changes one, else the first outer box shadow, else a border side.\n\nThe backdrop is every box behind the ring's outer edge, composited in order\nover white. The walk stops at the preview's edge and ends on `--page-bg`, so a\npart that fills its preview edge to edge is measured against the page. A ring below 3:1 against it reports under `a11y-component` with\nengine rule `focus-indicator-contrast`, such as \"Focus outline on span.cap\n2.14:1 against the backdrop, needs 3:1\". Ratios floor to two places, because\nWCAG does not round. Chromium's own `outline-style: auto` ring passes: it pairs\na dark band with a light one. An element with no indicator at all is\nSIA-R65's finding and adds none here. Alfa reads only the element, its\ndescendants, and its ancestors, so a ring this check finds on a sibling settles\nSIA-R65's \"can't tell\" for that element.\n\nA state view that forces the focused look with a class, such as Input's\nfocused state, would leave focus nothing to change. Both focus checks lift\nthat class while they measure, then put it back.\n\n## Control boundary contrast\n\nA control whose edge shows where to click or what it holds needs that edge\nat 3:1 against the backdrop behind it. So does a selected or current item\nwhose surface or edge alone shows that state. The contract lists these parts\nin `boundaries`. Each part passes when its border, on any side, or its\noutline, 1px or wider, or its surface reaches 3:1. A failure names both ratios, such as \"Track border\n1.45:1 and surface 1.3:1 against the backdrop, needs 3:1\", and reports under\n`a11y-component` with engine rule `control-boundary-contrast`. A part whose\nform controls are all disabled is skipped, because SC 1.4.11 exempts inactive\ncontrols.\n\nThe shipped controls that draw their own edge declare it: Input's field,\nRadioButton's circle, Slider's and Toggle's track and thumb, and\nSegmentedControl's bar. The selected states that show by surface and edge\nalone declare their part too: TabBar's selected tab, SegmentedControl's\nselected option, and SideNavigation's selected title, section, item and\nfooter. Each measures against the bar or panel behind it, the surface the\nunselected items show. MenuSelect's selected option carries a check mark, so\nit declares none.\n\n## Text spacing and motion per component\n\nThe text spacing check measures every element of the component that holds\ntext: its root part, its portaled parts, and their descendants. It applies\nthe override inside the preview alone and reports each element the override\npushes more than 1px past its box, on an axis it does not scroll. An element\nalready cut at rest adds nothing. The help names the measure: \"Under the text\nspacing override, `<span>` holds 24px of content in 18px of height, and its\noverflow-y is hidden\".\n\nThe reduced motion check reads the declared animations and transitions of\nthe same elements and their `::before` and `::after`, along with every\nscript animation running on them. Motion that lasts past 200 ms, its delay\nincluded, is recorded. The check then emulates\n`prefers-reduced-motion: reduce` in the same view and reports each motion\nstill that long, such as \"A transition of left lasts 500 ms under\nprefers-reduced-motion: reduce\". A view with no long motion costs no second\nread. The help page is WCAG technique C39, which serves WCAG 2.2 SC 2.3.3\nAnimation from Interactions.\n\n## Property tracing\n\nA finding that a property can fix names that property. The suite reads the\nCSS rules that match the failing element through the Chrome DevTools\nprotocol, keeps the declarations the finding depends on, such as the outline\nand border of a focus ring or the height and padding of a clipped label, and\nfollows each `var()` to the component's semantic properties and intrinsics.\nA component's private custom properties lead through to the property behind\nthem. Theme and editor variables drop out.\n\n`--json` output carries the result in each finding's `details.variables`,\nsorted, and leaves the field out when the trace finds nothing. An excerpt:\n\n```json\n{\n \"rule\": \"a11y-component\",\n \"component\": \"slider\",\n \"details\": {\n \"engineRule\": \"focus-indicator-contrast\",\n \"target\": \"span.cap\",\n \"variables\": [\"--slider-range-thumb-border\", \"--slider-range-thumb-border-width\"]\n }\n}\n```\n\naxe's contrast and target size rules, SIA-R65, both contrast checks, text\nspacing, and reduced motion trace. The ARIA rules, the keyboard cases, and\nthe accessibility tree mark the component alone.\n\n## Review findings\n\n`a11y-review` collects the checks an engine left undecided. axe reads contrast\nfrom the colour behind the text, so it returns no verdict for text over a\ngradient, an image, or a backdrop, or under a pseudo-element that covers it.\nThe focus indicator and control boundary checks do the same for a ring or an\nedge over a gradient or an image. Judge each element by eye against the help\npage the finding names.\n\nTwo sources account for most of them. Button's shimmer pseudo-element and its\ntinted hover surfaces leave every Button view undecided, so a Button run lists\nmany `a11y-review` warnings. A theme that paints gradient surfaces does the\nsame for every component that sits on one.\n\n## Accessibility trees\n\nThe package records each shipped component's accessibility tree, one block\nper view, in `src/testing/contracts/__aria__/<id>.yml`. A run that reads a\ndifferent tree reports `a11y-component` with engine rule `aria-snapshot` and\nthe lines that changed.\n\nInside the live-tokens repository, after a deliberate markup change,\n`--update-aria` records the tree anew, and `LIVE_TOKENS_UPDATE_ARIA=1 npm run\ntest:e2e:contract` rewrites every file:\n\n```bash\nnode bin/cli.mjs check-component button --tests --update-aria\n```\n\nIn a project the trees sit under `node_modules`, where the suite reads them\nand never writes. A project's own components therefore have no recorded tree\nyet.\n\n## Keyboard cases\n\nA component contract declares its keyboard cases among its interaction\ncases, each marked `rule: 'a11y-keyboard'`: Enter or Space activates, an\narrow key moves where the component's WAI-ARIA pattern moves, and Escape\ncloses an overlay. A failed keyboard case reports `a11y-keyboard` at `warn`.\nA case without the mark fails as `contract-interaction`, an error. The\n`live-tokens-create-component` skill writes these cases for a new component,\nand its `references/contract-tests.md` describes their shape.\n\nAn editor that adds placeholder content to its preview, such as a demo label\nor a hover target, marks it `data-preview-demo`, and the scans skip it.\n\n## Boundaries in the contract\n\n`boundaries` is an optional contract field,\n`{ part: string; label: string; when?: string }[]`, for the parts that draw a\ncontrol's visible edge or show its selected state. `part` is a key of the\ncontract's `parts`, and `label` names the part in a finding. `when` is a\nselector the part must also match to be measured, for a part that carries its\nselected state as a class in some views only.\n\n```ts\nboundaries: [\n { part: 'track', label: 'Track' },\n { part: 'thumb', label: 'Thumb' },\n],\n```\n\nList a part whose border or surface is the only thing that shows the control\nis there, a thumb whose position shows the value, or a selected item whose\nsurface or edge is the only thing that shows it is selected. Leave out a part whose\ntext or icon already shows it. A contract without the field runs no boundary\ncheck.\n\n## Accessibility view\n\nThe editor's Accessibility view runs the component checks from the browser.\n**Run test** checks every component in the open theme as saved, in about\nthree and a half minutes. With edits pending, it shows \"Save the changes you\nwant to have checked.\" and starts nothing. Save the theme first, so the run\nand the editor read the same values.\n\nEach theme keeps its own report. Opening another theme shows that theme's\nlast run, or none if it has never run. The dev server keeps each report in\n`_a11y-reports/` in the data directory, and deleting a theme deletes its\nreport. A failed run leaves the theme's last report in place.\n\nThe total counts the findings a fix clears. A result to check by eye counts\napart, because only a person can settle it. An overview opens the report. It\nshows the total, the count in each repair group below, and a table of every\ncomponent with a finding and its count per group. The \"Check by eye\" figure\nscrolls to the By eye list. The table\nlists first the components with the most findings a property fixes, then those\nwith the most a code change fixes. A component's name in the table scrolls to\nits section.\n\nEach component's section heads its count with **Open component**, which opens\nthe component's page at its accessibility panel. The section sorts its rows\nby repair:\n\n- **Fix with a property** holds each colour pair below its WCAG floor, as the\n editor's contrast checker measures it, such as \"Brand text color 3.87:1,\n needs 4.5:1\", and every focus ring and control boundary below 3:1.\n- **Fix in the component's code** holds every other finding: axe failures,\n focus styles that SIA-R65 finds unchanged, failed keyboard cases, text that\n clips under the spacing override, and motion that ignores reduced motion.\n- **Check by eye** holds the `a11y-review` results, each led by an eye. The\n By eye list gathers them for the whole report.\n\nFindings that share an engine rule, a help text, and their traced properties\nmerge into one row. The row names its element, or its element count, and the\nviews it failed in, up to four. An issue's text is amber, and a result to\ncheck by eye is grey. The row's links sit beside its text:\n\n- Each property the row traces to links with a \"→\" to its row on the\n component's page, which scrolls to the row and highlights it. A row that\n traces to no property links to the component's page instead.\n- The link on the right opens the page the finding cites and names its kind:\n \"Rule\" for an axe or Alfa rule, \"Success criterion\" with its number for a\n WCAG criterion, \"Technique\" for a WCAG technique, and \"Pattern\" for a\n WAI-ARIA pattern.\n- A result to check by eye has **Looks right** at the row's right edge.\n Press it once you have judged the element and found it passes. The row's\n eye turns to a check, its text dims, and it stops counting and stops\n marking the editor. Press it again to clear the mark. The mark belongs to\n the open theme's report, and each later run of that theme keeps it while\n the run finds the same result.\n\nA property's label leads with the words from its variable name that the\neditor's label leaves out, so `--input-focused-border` reads \"Focused border\ncolor\" where the editor's row reads \"Border color\". The contrast rows read\nthe same labels.\n\nThe overview's \"Check by eye\" column counts the results no one has marked.\nA component whose results all look right leaves the table, the sections, and\nthe view's sidebar. A component section lists only issues and fixes, so the\nview's sidebar lists the components with one of those.\n\nThe **By eye** list closes the view and holds every result to check by eye,\nmerged per component. The results to check come first, under a tally such\nas \"2 to check, 18 look right\" and a bar that fills as you mark them. Pressing\n**Looks right** moves focus to the result that takes the row's place, so\nEnter or Space walks the list. **Mark all as looking right** marks every\nresult still to check in one step. The marked results follow in **Looks\nright**, a group that starts closed and shows their count. Open it to\nrevisit them, each with its button pressed, or to clear a mark. A component's\nsection with results still to check ends in a link such as \"2 results to\ncheck by eye\" that scrolls to the list. axe's contrast\nviolations stay out of the report, because the contrast checker measures the\nsame pairs.\n\nWhen Playwright, axe, or Alfa is missing, the report names its install\ncommand and Run test stays disabled. Install the package and press **Check\nagain**. The view runs its tests through the dev server, so it exists under\nthe dev server alone, with the rest of the editor.\n\nThe view runs `check-component --tests --a11y --no-fix --json --open-theme`.\n`--a11y` runs the accessibility suite and the keyboard cases alone, and\n`--open-theme` tests the theme the editor has open. A run without it tests\nthe `default` theme. Both work from the command line too.\n\n## Marks in the editor\n\nThe last finished run marks the rest of the editor. A result marked as\nlooking right is acknowledged and marks nothing: no sidebar mark, no panel,\nand no property mark. The By eye list keeps it to revisit.\n\n- The component list leads the name of each component with a finding with a\n warning triangle, or with a grey eye when its only results are unmarked\n results to check by eye. A condensed list shows the mark on the\n component's icon.\n- That component's page opens with a panel under its title that lists its\n findings in all three groups, check by eye included, and leaves out the\n results marked as looking right. The panel heads with\n the triangle and the issue count, or with the eye and the count to check by\n eye when no issue remains.\n- Each \"→\" in the panel opens the tab holding the property and highlights\n its row.\n- Each property row a finding traces to carries a mark beside its label: a\n warning triangle in the highlight colour, or a grey eye when every finding\n on the row is a result to check by eye. A result marked as looking right\n adds no mark. On a colour or size row the mark sits between the\n label and the value. On a toggle row it follows the label.\n\nClick a row's mark to open its findings: the count, then each merged row\nwith its element, its views, its help page, and the properties it traces to. The\neditor's own contrast failures add no row mark, because the contrast readout\non the colour row already shows them.\n\nThe marks and the panel keep the run's time and change when the open\ntheme's next run finishes. The contrast readout on each colour row stays live and measures\nevery edit as you make it.\n\nThe editor also re-measures each contrast failure of the run as you edit.\nOnce one passes, its row in the panel and in the Accessibility view turns\ngreen and shows the run's ratio beside the ratio now, such as\n\"Fill color 2.66:1 → 5.31:1\". A fixed row counts as no issue, so a component\nwhose only issues are fixed loses its triangle in the component list. The\nrun's other findings change only when the next run finishes.\n\n## Installing the engines\n\nThe engines ship as optional peer dependencies. A project made with `create`\nhas them. Any other project installs them beside `@playwright/test`:\n\n```bash\nnpm install -D axe-core @axe-core/playwright \\\n @siteimprove/alfa-act@^0.119.0 @siteimprove/alfa-device@^0.119.0 \\\n @siteimprove/alfa-dom@^0.119.0 @siteimprove/alfa-http@^0.119.0 \\\n @siteimprove/alfa-rules@^0.119.0 @siteimprove/alfa-url@^0.119.0 \\\n @siteimprove/alfa-web@^0.119.0\n```\n\nThe Alfa packages share one minor version. axe-core needs 4.11.1 or later to\nread OKLCH colours, which every shipped theme uses. A test whose engine is\nmissing reports as inapplicable, and its coverage names the package. A\nmissing engine leaves the exit status to the other rules, under `--strict`\ntoo.\n\n## Limits\n\n- The page's text spacing test measures widths, so it reports horizontal\n overflow alone. The component check measures both axes.\n- The page's reduced motion test reports animations still running 200 ms\n after the page loads. A short entrance animation finishes first and goes\n unseen.\n- The component's reduced motion check reads the preference within each\n view. A script that reads it only when the component mounts goes unseen.\n- The page scan leaves contrast to `page-contrast` and focus visibility to\n the component suite.\n- SIA-R65 reads the focused element, its descendants, and its ancestors, so\n it reports a focus ring painted on a sibling as missing. The focus\n indicator check measures that ring; confirm it by eye.\n- A view that forces the focused look at rest, such as Input's focused\n state, leaves real focus nothing to change, so SIA-R65 reports it there.\n The same element passes in the views that leave it at rest.\n- A finding traces to the component's own semantic properties and\n intrinsics. A ring or colour that a theme token paints directly, such as\n SegmentedControl's `--border-neutral-strong` focus ring, traces to\n nothing. A finding on a component nested\n inside another, such as a Button inside a Dialog, marks the host and names\n none of the nested component's properties.\n- A literal value in the component's code, such as a transition's duration,\n has no property to name. The finding traces to whatever property governs\n the same declaration, or to none.\n- A project's own components get no recorded accessibility tree.\n- Automated checks find part of what WCAG asks. Reading order, the meaning of\n labels and alt text, and screen reader behaviour still need a person.\n",
7
7
  "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Browse the skill atlas\n\nThe package also ships a Skill Atlas component: a diagram of the reasoning\nbehind each bundled skill, with its `SKILL.md` and reference files alongside\nso you can see exactly which lines drive which step. Import it from\n`@motion-proto/live-tokens/skill-atlas` and mount it on a route of your own:\n\n```js\n'/skills': {\n lazy: () => import('@motion-proto/live-tokens/skill-atlas'),\n},\n```\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- A catalogue entry in the runtime file that says what the component is for and\n what to use instead. `npx live-tokens components` prints it, and the skills\n read it when they choose a component for a page.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
8
- "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has five views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours\n tell across a page.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n- **Sketchstyle**: an effect layer that redraws the page by hand. See\n [Sketch mode](sketch-mode.md).\n- **Accessibility**: Run test checks every component in the open theme as\n saved and lists each issue it finds by component.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Custom colours\n\nA custom colour is one colour of your own, for a slot no palette step fits.\nThe open theme holds it as a token named by number: `--color-custom-1`,\n`--color-custom-2`, and so on.\n\n- **Adding.** Every colour picker has a **Custom** row after Danger. Open it\n and click the plus swatch. The new colour starts from the slot's current colour and\n opens in the OKLCH editor, and the slot previews it as you drag. The check\n assigns it to the slot; the cross discards it and restores the slot.\n- **Reusing.** Pick a custom colour from the same row as you pick a palette\n step, opacity included. Every slot that names it follows its value.\n- **Editing.** Click the pencil on a custom swatch in the picker, or\n click **Edit** in the **Custom Colours** section of the Tokens view. The edit\n repaints every slot that names the colour.\n- **Removing.** The bin on a custom swatch in the picker, or **Remove** in\n the Tokens view, deletes a colour nothing names. When a component property,\n gradient stop, or wash names it, the editor first asks for a replacement and\n preselects the nearest colour. **Replace and remove** points each of those\n at the replacement, keeps each one's opacity, and deletes the custom colour.\n\nEach add, edit, and remove is one undo step. A custom colour holds one value in\nlight and dark mode. Saving writes the theme's custom colours into its file,\nand loading another theme brings in that theme's set. Reach a custom colour\nthrough a component property: `live-tokens check-page` reports `unknown-token`\nfor page CSS that writes `var(--color-custom-1)`, since the name exists only in\nthe theme.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Washes and gradients\n\n- **Scrims** are translucent layers that dim what sits behind them, like the\n one a dialog draws over the page. Set a colour and opacity per stop.\n- **Tints** are the opposite operation: they shade the surface they sit on\n rather than dimming what is behind it, which is what a hover needs.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nPages lay out on the column grid. The column count and the max width hold at\nevery width. Desktop, Tablet, and Phone each set a gutter and an outer margin,\nand the grid takes the tablet values at 767px and narrower and the phone values\nat 480px and narrower. The columns button in the editor bar draws the grid over\nthe page, and a page on the grid reflows as you edit.\n\n## Contrast readout\n\nIn the **Components** view, every colour row that paints text, an icon, or\nanother foreground mark shows its WCAG contrast ratio beside the colour's name.\nThe readout measures the colour against the layers the component paints behind\nit. Each edit and each theme load updates it, and the rows keep their places.\n\n- **Value.** The ratio shows on every foreground row, in grey while it meets\n its floor. It truncates to two decimals, so 4.499 reads `4.49:1`, below the\n 4.5:1 floor. Text needs 4.5:1, or 3:1 at 24px or 18.66px bold. An icon or\n another mark needs 3:1.\n- **Amber mark.** A ratio below its floor turns amber and gains an exclamation\n mark.\n- **Popover.** Click the ratio to open it. For each pair the colour forms, it\n names the WCAG rule and its floor, the measured ratio, and the layers behind\n the colour, top first, with a link to the success criterion. A colour over\n several backgrounds forms one pair per background, and the row shows the pair\n closest to failing. Notification's body colour paints the dismiss icon on the\n header and the body text on the page, so its popover lists two pairs.\n- **Disabled states.** WCAG exempts disabled controls, so a disabled state's\n ratio shows in a dimmer grey, whatever its value.\n- **Page background.** Every stack ends on the page background, `--page-bg`.\n A translucent layer blends into the layers beneath it, and the readout\n measures the blend. A gradient layer counts its worst stop.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
8
+ "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has five views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours\n tell across a page.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n- **Sketchstyle**: an effect layer that redraws the page by hand. See\n [Sketch mode](sketch-mode.md).\n- **Accessibility**: Run test checks every component in the open theme as\n saved and lists each issue it finds by component.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Custom colours\n\nA custom colour is one colour of your own, for a slot no palette step fits.\nThe open theme holds it as a token named by number: `--color-custom-1`,\n`--color-custom-2`, and so on.\n\n- **Adding.** Every colour picker has a **Custom** row after Danger. Open it\n and click the plus swatch. The new colour starts from the slot's current colour and\n opens in the OKLCH editor, and the slot previews it as you drag. The check\n assigns it to the slot; the cross discards it and restores the slot.\n- **Reusing.** Pick a custom colour from the same row as you pick a palette\n step, opacity included. Every slot that names it follows its value.\n- **Editing.** Click the pencil on a custom swatch in the picker, or\n click **Edit** in the **Custom Colours** section of the Tokens view. The edit\n repaints every slot that names the colour.\n- **Removing.** The bin on a custom swatch in the picker, or **Remove** in\n the Tokens view, deletes a colour nothing names. When a component property,\n gradient stop, or wash names it, the editor first asks for a replacement and\n preselects the nearest colour. **Replace and remove** points each of those\n at the replacement, keeps each one's opacity, and deletes the custom colour.\n\nEach add, edit, and remove is one undo step. A custom colour holds one value in\nlight and dark mode. Saving writes the theme's custom colours into its file,\nand loading another theme brings in that theme's set. Reach a custom colour\nthrough a component property: `live-tokens check-page` reports `unknown-token`\nfor page CSS that writes `var(--color-custom-1)`, since the name exists only in\nthe theme.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Washes and gradients\n\n- **Scrims** are translucent layers that dim what sits behind them, like the\n one a dialog draws over the page. Set a colour and opacity per stop.\n- **Tints** are the opposite operation: they shade the surface they sit on\n rather than dimming what is behind it, which is what a hover needs.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nPages lay out on the column grid. The column count and the max width hold at\nevery width. Desktop, Tablet, and Phone each set a gutter and an outer margin,\nand the grid takes the tablet values at 767px and narrower and the phone values\nat 480px and narrower. The columns button in the editor bar draws the grid over\nthe page, and a page on the grid reflows as you edit.\n\n## Contrast readout\n\nIn the **Components** view, every colour row that paints text, an icon, or\nanother foreground mark shows its WCAG contrast ratio beside the colour's name.\nThe readout measures the colour against the layers the component paints behind\nit. Each edit and each theme load updates it, and the rows keep their places.\n\n- **Value.** The ratio shows on every foreground row, in grey while it meets\n its floor. It truncates to two decimals, so 4.499 reads `4.49:1`, below the\n 4.5:1 floor. Text needs 4.5:1, or 3:1 at 24px or 18.66px bold. An icon or\n another mark needs 3:1.\n- **Amber mark.** A ratio below its floor turns amber and gains an exclamation\n mark.\n- **Popover.** Click the ratio to open it. For each pair the colour forms, it\n names the WCAG rule and its floor, the measured ratio, and the layers behind\n the colour, top first, with a link to the success criterion. A colour over\n several backgrounds forms one pair per background, and the row shows the pair\n closest to failing. Notification's body colour paints the dismiss icon on the\n header and the body text on the page, so its popover lists two pairs.\n- **Disabled states.** WCAG exempts disabled controls, so a disabled state's\n ratio shows in a dimmer grey, whatever its value.\n- **Transparent marks.** A mark painted fully transparent, such as an\n unselected tab's underline, draws nothing, so it meets its floor at any\n ratio. Transparent text still needs its floor. A decorative line, such as\n a section divider's hairline, shows no readout.\n- **Page background.** Every stack ends on the page background, `--page-bg`.\n A translucent layer blends into the layers beneath it, and the readout\n measures the blend. A gradient layer counts its worst stop.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
9
9
  "getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. In the **Theme** panel at the foot of the sidebar, choose **Save As**. Your\n theme appears as JSON under `src/live-tokens/data/themes/`.\n5. Reload. The editor reopens on your theme, so the page returns as you left\n it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, click **Adopt** in the Theme panel. That saves the open theme and bakes\nit into `src/live-tokens/data/tokens.generated.css`, which your build bundles\nalongside `tokens.css`. Adopt is the only action that changes what your site\nships, so try any theme you like first. The editor itself never reaches\nproduction.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
10
10
  "light-and-dark": "# Light and dark\n\nSome things on a page cannot be written as a token. A wordmark drawn in white\ndisappears on a pale theme. Ink that multiplies onto paper vanishes on a dark\none. A photograph behind a headline is dark no matter what the palette says.\n\nEach of those needs the same fact first: which way does the surface behind this\nthing lean? One attribute carries it.\n\n## The attribute\n\n`data-backdrop` is either `light` or `dark`, and it does two things at once: it\nselects, so a rule can key on it, and it sets `color-scheme`, so every\n`light-dark()` under it resolves the half that reads.\n\n```css\n.title {\n color: light-dark(var(--color-black), var(--color-white));\n}\n```\n\nThat line is right on both sides of the theme, and it is right inside a dark\nsection on a pale page, because the nearest `color-scheme` wins.\n\n## Stating it\n\nPut it in the markup when the surface knows its own tone — a hero over a\nphotograph, a plate that stays pale in every theme:\n\n```svelte\n<div class=\"hero-panel\" data-backdrop=\"dark\">\n```\n\nA stated tone beats any measurement, and it inherits, so everything inside the\npanel resolves against it.\n\n## Measuring it\n\nWhere the tone is a property of the theme rather than of the markup, let it be\nmeasured:\n\n```svelte\n<script>\n import { backdrop } from '@motion-proto/live-tokens/backdrop';\n</script>\n\n<section use:backdrop>\n```\n\nThe action reads whatever actually paints behind the element — the nearest\nancestor with an opaque fill, averaged across its gradient stops, falling back\nto the theme's `--page-bg` — and stamps the answer. It re-reads when the theme\nchanges, which the editor does by rewriting custom properties with no reload,\nso the stamp follows a live edit.\n\nThe page itself is stamped for you: the build bakes the production theme's\npolarity into `tokens.generated.css`, so the first paint is already right, and\n`syncDocumentBackdrop()` keeps `<html>` current as themes switch.\n\n```ts\nimport { syncDocumentBackdrop } from '@motion-proto/live-tokens/backdrop';\n\nsyncDocumentBackdrop();\n```\n\n## Reading it from JavaScript\n\nAnything that paints outside CSS — a canvas, a WebGL uniform, an `<img>` that\ncomes in two versions — asks the same question through the same module:\n\n```ts\nimport { isLightBackdrop, watchBackdrop, cssColorToHex } from '@motion-proto/live-tokens/backdrop';\n\nconst stop = watchBackdrop(logoEl, {\n stamp: false,\n onChange: (polarity) => (src = polarity === 'light' ? darkMark : lightMark),\n});\n```\n\n`isLightBackdrop(el)` answers once. `watchBackdrop` keeps answering and returns\na stop function. `cssColorToHex` resolves any CSS colour — including the\n`oklch()` a token holds — to a hex a non-CSS consumer can take.\n\n## What it does not do\n\nPolarity is a property of a surface, not of a component, so nothing is stamped\nfor you below `<html>`: a section that needs an answer either states one or asks\nfor one. And a measurement reads the paint at the moment it runs — an element\nthat scrolls from a pale section onto a dark one keeps the answer it was given.\nState the tone on each section instead.\n",
11
11
  "production-css": "# Production CSS\n\nIn dev, every design token sits on `:root` as a CSS variable, because the\neditor can rewrite any of them while the page runs. A production build runs no\neditor, so most of those values never change. `vite build` rewrites the CSS it\nemits to match: a token stays a variable only where its value can change at\nruntime, and every other `var()` gives way to the value it reads.\n\nThe dev server keeps every variable. Your source files keep every token too:\nthe build rewrites only the files it emits, so `tokens.css`,\n`tokens.generated.css` and your components still name each token, and the\ntoken names stay the API you write against.\n\n## The build step\n\n`themeFileApi` runs the step at the end of every `vite build`, after Vite has\nbundled and minified the CSS. It reads every stylesheet and script in the\nbundle, including lazy chunks, and then:\n\n- **removes** each custom property that no rule reads, directly or through\n another property;\n- **substitutes** each `var()` that reads a static property with the text of\n that property's value, so `calc()` and `color-mix()` still resolve on the\n element that uses them;\n- **declares** each remaining property once on `:root`, with the value that\n wins the cascade, so DevTools lists it once.\n\nThe build prints one line with the result:\n\n```\nlive-tokens: the production CSS declares 76 of 848 custom properties and is 61.3 kB smaller.\n```\n\nA file's hashed name follows its content. Each stylesheet the build rewrites\ntakes a new hash, and so does each script that loads it, so a cache that keeps\nhashed files forever serves each release its own files. A file name pattern\nwithout `[hash]` keeps every name.\n\n## The dynamic rule\n\nA property stays a variable when its value can change while the page runs, or\nwhen the build cannot prove it never does. A property that changes and loses\nits variable breaks a page, and an extra variable costs a few bytes, so a case\nthat could fall either way stays a variable. A property is **dynamic** when any\nof these hold:\n\n- A rule declares it under a selector besides a root selector such as `:root`\n or `:root:root`, or inside `@media`, `@supports` or `@container`. The size\n tokens and the column gutter and margin in the three responsive `@media`\n blocks of `tokens.css` stay variables this way.\n- Its name appears in a script or HTML page the build emits. That covers\n `style:--name` directives, `setProperty`, `getPropertyValue` and inline\n `style` attributes. Card sets `--card-hover-border-enabled` through a\n `style:` directive, so the token stays a variable.\n- Its name appears in an at-rule's prelude, such as a `style()` container\n query.\n- An `@property` rule registers it.\n- The `keep` option lists it.\n- Its value reads a dynamic property.\n- Its winning value is a CSS-wide keyword such as `initial`, or it reads\n itself through a loop of `var()` references.\n- Two stylesheets give it different `:root` values.\n- Its value reads a name that no stylesheet declares and no script or `keep`\n entry names.\n- Only a lazily loaded stylesheet declares it, and another stylesheet reads\n it.\n\nEvery other property is **static**: one value on `:root` for the life of the\npage.\n\n## Computed names\n\nThe build finds a name in a script only when the script spells it out. A\nscript that assembles a name while the page runs, such as\n`` `--color-${family}-500` ``, hides it. The build then substitutes or removes\nthat token, so a read returns an empty string and a write changes nothing. List\neach such name under `keep`:\n\n```ts\nthemeFileApi({\n tokensCssPath: 'src/system/styles/tokens.css',\n productionCss: { keep: ['--color-brand-500', '--color-accent-500'] },\n}),\n```\n\n`keep` takes exact names. The same goes for a stylesheet outside the bundle,\nsuch as one in `public/`: the build never reads it, so list each token it\nreads under `keep`.\n\n## Options\n\n`productionCss` on `themeFileApi` sets the step:\n\n- **Default.** The build removes, substitutes and declares, as above.\n- **`productionCss: { keep: [...] }`.** Each listed name stays a variable.\n- **`productionCss: { substitute: false }`.** The build removes properties no\n rule reads and keeps every other `var()`, so DevTools shows token names while\n you debug. It still declares each property once on `:root`.\n- **`productionCss: false`.** The build keeps every variable, as dev does.\n\n`keep` and `substitute` combine in one object. When a page renders differently\nin production, build it with `productionCss: false`. A difference that goes\naway points at a token the rule missed, and `keep` fixes it.\n\n## Limits\n\n- **Values a property rejects.** In dev, `width: var(--color-brand-500)` passes\n the parser, and when the color reaches `width` the browser treats the\n declaration as `unset`. After substitution, `width` holds a color, the parser\n drops the declaration, and an earlier `width` rule applies. Keep each token's\n value valid for every property that reads it.\n- **Fixed stylesheet names.** The build renames each stylesheet it rewrites\n through the hash in the build's `assetFileNames` pattern. A plugin that emits\n a stylesheet under a fixed `fileName` breaks that pattern, and the build\n stops with `live-tokens: <file> does not follow the output pattern\n \"<pattern>\"`. Set `productionCss: false` for that build.\n",
@@ -112,7 +112,8 @@
112
112
  const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
113
113
  const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
114
114
  const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
115
- let a11yMarked = $derived(new Set(componentsWithIssues($a11yIssues).map((row) => row.component)));
115
+ /** Each marked component, true when its only results are to check by eye. */
116
+ let a11yMarked = $derived(new Map(componentsWithIssues($a11yIssues).map((row) => [row.component, row.count === 0])));
116
117
  </script>
117
118
 
118
119
  <!--
@@ -171,7 +172,7 @@
171
172
  <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
172
173
  {/if}
173
174
  {#if a11yMarked.has(item.id)}
174
- <A11yRailMark />
175
+ <A11yRailMark review={a11yMarked.get(item.id)} />
175
176
  {/if}
176
177
  </button>
177
178
  {/each}
@@ -194,7 +195,7 @@
194
195
  <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
195
196
  {/if}
196
197
  {#if a11yMarked.has(item.id)}
197
- <A11yRailMark />
198
+ <A11yRailMark review={a11yMarked.get(item.id)} />
198
199
  {/if}
199
200
  </button>
200
201
  {/each}