@motion-proto/live-tokens 0.96.0 → 0.98.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +5 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
  3. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
  4. package/CHANGELOG.md +309 -0
  5. package/README.md +2 -2
  6. package/bin/check-component.mjs +8 -0
  7. package/bin/check-page.mjs +6 -0
  8. package/bin/cli.mjs +36 -6
  9. package/bin/contractRunner.mjs +142 -17
  10. package/bin/lib/findings.mjs +20 -4
  11. package/bin/rules/a11y.mjs +53 -0
  12. package/bin/set-colors.mjs +1 -0
  13. package/bin/setup-claude.mjs +2 -2
  14. package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
  15. package/dist-plugin/index.cjs +148 -3
  16. package/dist-plugin/index.js +250 -105
  17. package/dist-plugin/setColors/index.cjs +7 -1
  18. package/dist-plugin/setColors/index.d.cts +9 -4
  19. package/dist-plugin/setColors/index.d.ts +9 -4
  20. package/dist-plugin/setColors/index.js +8 -2
  21. package/dist-plugin/setGeometry/index.d.cts +1 -1
  22. package/dist-plugin/setGeometry/index.d.ts +1 -1
  23. package/dist-plugin/setType/index.d.cts +1 -1
  24. package/dist-plugin/setType/index.d.ts +1 -1
  25. package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
  26. package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
  27. package/package.json +49 -2
  28. package/src/demo/LabeledSelect.svelte +1 -1
  29. package/src/editor/bootstrap.ts +3 -1
  30. package/src/editor/component-editor/BadgeEditor.svelte +13 -0
  31. package/src/editor/component-editor/ButtonEditor.svelte +33 -2
  32. package/src/editor/component-editor/CalloutEditor.svelte +13 -0
  33. package/src/editor/component-editor/CardEditor.svelte +12 -2
  34. package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
  35. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
  36. package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
  37. package/src/editor/component-editor/DialogEditor.svelte +15 -0
  38. package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
  39. package/src/editor/component-editor/ImageEditor.svelte +20 -9
  40. package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
  41. package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
  42. package/src/editor/component-editor/InputEditor.svelte +23 -0
  43. package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
  44. package/src/editor/component-editor/NotificationEditor.svelte +15 -0
  45. package/src/editor/component-editor/PanelEditor.svelte +1 -1
  46. package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
  47. package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
  48. package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
  49. package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
  50. package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
  51. package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
  52. package/src/editor/component-editor/SliderEditor.svelte +33 -0
  53. package/src/editor/component-editor/TabBarEditor.svelte +23 -2
  54. package/src/editor/component-editor/TableEditor.svelte +14 -0
  55. package/src/editor/component-editor/ToggleEditor.svelte +17 -0
  56. package/src/editor/component-editor/TooltipEditor.svelte +11 -2
  57. package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
  58. package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
  59. package/src/editor/component-editor/registration.ts +2 -1
  60. package/src/editor/component-editor/registry.ts +69 -27
  61. package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
  62. package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
  63. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
  64. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -10
  65. package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
  66. package/src/editor/core/a11y/a11yReport.ts +101 -0
  67. package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
  68. package/src/editor/core/a11y/contrastFailures.ts +22 -0
  69. package/src/editor/core/components/contrastPairRegistry.ts +18 -0
  70. package/src/editor/core/palettes/contrast.ts +44 -0
  71. package/src/editor/core/palettes/contrastPairs.ts +79 -0
  72. package/src/editor/core/palettes/resolveColor.ts +77 -0
  73. package/src/editor/core/store/editorPersistence.ts +9 -1
  74. package/src/editor/core/store/editorRenderer.ts +2 -0
  75. package/src/editor/core/store/editorStore.ts +29 -0
  76. package/src/editor/core/store/editorTypes.ts +9 -0
  77. package/src/editor/core/store/editorViewStore.ts +2 -2
  78. package/src/editor/core/store/unsavedEdits.ts +10 -0
  79. package/src/editor/core/themes/buildColors.ts +16 -5
  80. package/src/editor/core/themes/liveStateStream.ts +7 -1
  81. package/src/editor/core/themes/parsers/gradient.ts +5 -2
  82. package/src/editor/core/themes/slices/customColors.ts +226 -0
  83. package/src/editor/core/themes/themeTypes.ts +10 -0
  84. package/src/editor/docs/chapters.ts +1 -0
  85. package/src/editor/docs/content/accessibility.md +362 -0
  86. package/src/editor/docs/content/editing-tokens.md +56 -1
  87. package/src/editor/docs/content.generated.ts +2 -1
  88. package/src/editor/pages/ComponentEditorPage.svelte +10 -1
  89. package/src/editor/pages/EditorShell.svelte +97 -10
  90. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  91. package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
  92. package/src/editor/skill-atlas/trees/create-component.ts +12 -12
  93. package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
  94. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  95. package/src/editor/ui/GradientEditor.svelte +9 -1
  96. package/src/editor/ui/ThemePanel.svelte +4 -4
  97. package/src/editor/ui/UIContrastReadout.svelte +186 -0
  98. package/src/editor/ui/UIInfoPopover.svelte +16 -2
  99. package/src/editor/ui/UIPaletteSelector.svelte +347 -15
  100. package/src/editor/ui/UITokenSelector.svelte +33 -14
  101. package/src/editor/ui/VariablesTab.svelte +3 -0
  102. package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
  103. package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
  104. package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
  105. package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
  106. package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
  107. package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
  108. package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
  109. package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
  110. package/src/editor/ui/a11y/a11yView.ts +218 -0
  111. package/src/editor/ui/anchorToTrigger.ts +63 -0
  112. package/src/editor/ui/contrastReadout.ts +45 -0
  113. package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
  114. package/src/live-tokens/data/themes/autumn.json +20 -20
  115. package/src/live-tokens/data/themes/halloween.json +20 -20
  116. package/src/live-tokens/data/themes/midnight-study.json +20 -20
  117. package/src/live-tokens/data/themes/ocean.json +20 -20
  118. package/src/live-tokens/data/themes/royal-velvet.json +20 -20
  119. package/src/live-tokens/data/themes/sketchy.json +20 -20
  120. package/src/live-tokens/data/themes/spring-meadow.json +20 -20
  121. package/src/live-tokens/data/themes/sunset.json +20 -20
  122. package/src/system/components/Badge.svelte +3 -3
  123. package/src/system/components/Button.svelte +9 -3
  124. package/src/system/components/CodeSnippet.svelte +8 -1
  125. package/src/system/components/CornerBadge.svelte +3 -3
  126. package/src/system/components/Dialog.svelte +43 -14
  127. package/src/system/components/IconButton.svelte +1 -1
  128. package/src/system/components/Image.svelte +7 -0
  129. package/src/system/components/ImageLightbox.svelte +26 -26
  130. package/src/system/components/Input.svelte +8 -1
  131. package/src/system/components/MenuSelect.svelte +30 -2
  132. package/src/system/components/Notification.svelte +1 -1
  133. package/src/system/components/ProgressBar.svelte +6 -0
  134. package/src/system/components/RadioButton.svelte +36 -3
  135. package/src/system/components/SegmentedControl.svelte +36 -2
  136. package/src/system/components/SlidePager.svelte +2 -2
  137. package/src/system/components/Slider.svelte +13 -9
  138. package/src/system/components/TabBar.svelte +33 -3
  139. package/src/system/components/Toggle.svelte +2 -2
  140. package/src/system/components/VideoFrame.svelte +1 -1
  141. package/src/system/components/VideoLightbox.svelte +2 -2
  142. package/src/system/internal/focusTrap.ts +35 -0
  143. package/src/testing-js/__aria__/badge.yml +40 -0
  144. package/src/testing-js/__aria__/button.yml +88 -0
  145. package/src/testing-js/__aria__/callout.yml +12 -0
  146. package/src/testing-js/__aria__/card.yml +8 -0
  147. package/src/testing-js/__aria__/codesnippet.yml +21 -0
  148. package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
  149. package/src/testing-js/__aria__/cornerbadge.yml +24 -0
  150. package/src/testing-js/__aria__/dialog.yml +42 -0
  151. package/src/testing-js/__aria__/iconbutton.yml +57 -0
  152. package/src/testing-js/__aria__/image.yml +10 -0
  153. package/src/testing-js/__aria__/imagelightbox.yml +50 -0
  154. package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
  155. package/src/testing-js/__aria__/input.yml +61 -0
  156. package/src/testing-js/__aria__/menuselect.yml +30 -0
  157. package/src/testing-js/__aria__/notification.yml +30 -0
  158. package/src/testing-js/__aria__/panel.yml +2 -0
  159. package/src/testing-js/__aria__/progressbar.yml +2 -0
  160. package/src/testing-js/__aria__/radiobutton.yml +20 -0
  161. package/src/testing-js/__aria__/sectiondivider.yml +9 -0
  162. package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
  163. package/src/testing-js/__aria__/sidenavigation.yml +304 -0
  164. package/src/testing-js/__aria__/slidepager.yml +72 -0
  165. package/src/testing-js/__aria__/slider.yml +28 -0
  166. package/src/testing-js/__aria__/tabbar.yml +35 -0
  167. package/src/testing-js/__aria__/table.yml +144 -0
  168. package/src/testing-js/__aria__/toggle.yml +12 -0
  169. package/src/testing-js/__aria__/tooltip.yml +2 -0
  170. package/src/testing-js/__aria__/videoframe.yml +6 -0
  171. package/src/testing-js/__aria__/videolightbox.yml +12 -0
  172. package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
  173. package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
  174. package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
  175. package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
  176. package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
  177. package/src/testing-js/chunk-EWSGE27V.js +168 -0
  178. package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
  179. package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
  180. package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
  181. package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
  182. package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
  183. package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
  184. package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
  185. package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
  186. package/src/testing-js/component-a11y.contract.js +283 -0
  187. package/src/testing-js/component-a11y.contract.js.map +1 -0
  188. package/src/testing-js/component-alias.contract.js +2 -2
  189. package/src/testing-js/component-behavior.contract.js +2 -2
  190. package/src/testing-js/component-editor.contract.js +6 -5
  191. package/src/testing-js/component-editor.contract.js.map +1 -1
  192. package/src/testing-js/component-render.contract.js +10 -6
  193. package/src/testing-js/component-render.contract.js.map +1 -1
  194. package/src/testing-js/index.d.ts +44 -16
  195. package/src/testing-js/index.js +9 -9
  196. package/src/testing-js/page-compliance.contract.js +163 -18
  197. package/src/testing-js/page-compliance.contract.js.map +1 -1
  198. package/src/testing-js/vitest.js +3 -3
  199. package/template/package.json +9 -0
  200. package/src/editor/ui/keepInViewport.ts +0 -84
  201. package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
  202. package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
  203. package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
  204. package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
  205. /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
  206. /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
@@ -3,7 +3,7 @@
3
3
  A tour of the editor. The page behind it repaints on every change; saving
4
4
  writes a theme file you can reload later.
5
5
 
6
- The editor has four views:
6
+ The editor has five views:
7
7
 
8
8
  - **Tokens**: the design-system primitives (colour, type, spacing, and so on).
9
9
  They apply everywhere your site uses them.
@@ -13,6 +13,8 @@ The editor has four views:
13
13
  without changing the underlying system.
14
14
  - **Sketchstyle**: an effect layer that redraws the page by hand. See
15
15
  [Sketch mode](sketch-mode.md).
16
+ - **Accessibility**: Run test checks every component in the open theme as
17
+ saved and lists each issue it finds by component.
16
18
 
17
19
  This page covers **Tokens**. For components, see
18
20
  [Creating components](creating-components.md).
@@ -37,6 +39,34 @@ Editing a palette base ripples through every colour that depends on it, in real
37
39
  time. Colours use OKLCH, so the ramp stays perceptually even across hues
38
40
  without muddy mid-tones.
39
41
 
42
+ ## Custom colours
43
+
44
+ A custom colour is one colour of your own, for a slot no palette step fits.
45
+ The open theme holds it as a token named by number: `--color-custom-1`,
46
+ `--color-custom-2`, and so on.
47
+
48
+ - **Adding.** Every colour picker has a **Custom** row after Danger. Open it
49
+ and click the plus swatch. The new colour starts from the slot's current colour and
50
+ opens in the OKLCH editor, and the slot previews it as you drag. The check
51
+ assigns it to the slot; the cross discards it and restores the slot.
52
+ - **Reusing.** Pick a custom colour from the same row as you pick a palette
53
+ step, opacity included. Every slot that names it follows its value.
54
+ - **Editing.** Click the pencil on a custom swatch in the picker, or
55
+ click **Edit** in the **Custom Colours** section of the Tokens view. The edit
56
+ repaints every slot that names the colour.
57
+ - **Removing.** The bin on a custom swatch in the picker, or **Remove** in
58
+ the Tokens view, deletes a colour nothing names. When a component property,
59
+ gradient stop, or wash names it, the editor first asks for a replacement and
60
+ preselects the nearest colour. **Replace and remove** points each of those
61
+ at the replacement, keeps each one's opacity, and deletes the custom colour.
62
+
63
+ Each add, edit, and remove is one undo step. A custom colour holds one value in
64
+ light and dark mode. Saving writes the theme's custom colours into its file,
65
+ and loading another theme brings in that theme's set. Reach a custom colour
66
+ through a component property: `live-tokens check-page` reports `unknown-token`
67
+ for page CSS that writes `var(--color-custom-1)`, since the name exists only in
68
+ the theme.
69
+
40
70
  ## Type
41
71
 
42
72
  - **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an
@@ -74,6 +104,31 @@ and the grid takes the tablet values at 767px and narrower and the phone values
74
104
  at 480px and narrower. The columns button in the editor bar draws the grid over
75
105
  the page, and a page on the grid reflows as you edit.
76
106
 
107
+ ## Contrast readout
108
+
109
+ In the **Components** view, every colour row that paints text, an icon, or
110
+ another foreground mark shows its WCAG contrast ratio beside the colour's name.
111
+ The readout measures the colour against the layers the component paints behind
112
+ it. Each edit and each theme load updates it, and the rows keep their places.
113
+
114
+ - **Value.** The ratio shows on every foreground row, in grey while it meets
115
+ its floor. It truncates to two decimals, so 4.499 reads `4.49:1`, below the
116
+ 4.5:1 floor. Text needs 4.5:1, or 3:1 at 24px or 18.66px bold. An icon or
117
+ another mark needs 3:1.
118
+ - **Amber mark.** A ratio below its floor turns amber and gains an exclamation
119
+ mark.
120
+ - **Popover.** Click the ratio to open it. For each pair the colour forms, it
121
+ names the WCAG rule and its floor, the measured ratio, and the layers behind
122
+ the colour, top first, with a link to the success criterion. A colour over
123
+ several backgrounds forms one pair per background, and the row shows the pair
124
+ closest to failing. Notification's body colour paints the dismiss icon on the
125
+ header and the body text on the page, so its popover lists two pairs.
126
+ - **Disabled states.** WCAG exempts disabled controls, so a disabled state's
127
+ ratio shows in a dimmer grey, whatever its value.
128
+ - **Page background.** Every stack ends on the page background, `--page-bg`.
129
+ A translucent layer blends into the layers beneath it, and the readout
130
+ measures the blend. A gradient layer counts its worst stop.
131
+
77
132
  ## Saving
78
133
 
79
134
  The editor saves to your browser continuously, so work survives a reload
@@ -3,8 +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
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",
7
- "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 four 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\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## 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## 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- **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
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",
9
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",
10
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",
@@ -8,7 +8,9 @@
8
8
  import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
9
9
  import { listComponents } from '../core/components/componentConfigService';
10
10
  import { selectedComponent } from '../core/store/editorViewStore';
11
- import { componentDirty, editorState, mutate } from '../core/store/editorStore';
11
+ import { a11yIssues, componentDirty, editorState, mutate } from '../core/store/editorStore';
12
+ import A11yRailMark from '../ui/a11y/A11yRailMark.svelte';
13
+ import { componentsWithIssues } from '../ui/a11y/a11yView';
12
14
  import { setSketch } from '../core/sketch';
13
15
  import type { LiveTokensEditorHandle } from './liveTokensEditorHandle';
14
16
  // Editor chrome + form controls + icon font must be JS imports (not @import
@@ -110,6 +112,7 @@
110
112
  const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
111
113
  const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
112
114
  const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
115
+ let a11yMarked = $derived(new Set(componentsWithIssues($a11yIssues).map((row) => row.component)));
113
116
  </script>
114
117
 
115
118
  <!--
@@ -167,6 +170,9 @@
167
170
  {#if $componentDirty[item.id]}
168
171
  <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
169
172
  {/if}
173
+ {#if a11yMarked.has(item.id)}
174
+ <A11yRailMark />
175
+ {/if}
170
176
  </button>
171
177
  {/each}
172
178
  {#if customNavItems.length > 0}
@@ -187,6 +193,9 @@
187
193
  {#if $componentDirty[item.id]}
188
194
  <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
189
195
  {/if}
196
+ {#if a11yMarked.has(item.id)}
197
+ <A11yRailMark />
198
+ {/if}
190
199
  </button>
191
200
  {/each}
192
201
  {/if}
@@ -1,17 +1,27 @@
1
1
  <script lang="ts">
2
2
  import { run } from 'svelte/legacy';
3
3
 
4
- import { onMount } from 'svelte';
4
+ import { onMount, tick } from 'svelte';
5
5
  import { MediaQuery } from 'svelte/reactivity';
6
6
  import VariablesTab from '../ui/VariablesTab.svelte';
7
7
  import ColorsTab from '../ui/colors/ColorsTab.svelte';
8
8
  import EditorViewSwitcher from '../ui/EditorViewSwitcher.svelte';
9
9
  import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
10
10
  import SketchTab from '../ui/sketch/SketchTab.svelte';
11
+ import AccessibilityTab from '../ui/a11y/AccessibilityTab.svelte';
12
+ import A11yRailMark from '../ui/a11y/A11yRailMark.svelte';
13
+ import {
14
+ A11Y_REVIEW_SECTION,
15
+ A11Y_RUN_SECTION,
16
+ a11ySectionId,
17
+ componentsWithIssues,
18
+ issuesLabel,
19
+ setA11yViewLink,
20
+ } from '../ui/a11y/a11yView';
11
21
  import ThemePanel from '../ui/ThemePanel.svelte';
12
22
  import { scrollSectionIntoView } from '../ui/scrollSection';
13
23
  import { editorView, sidebarCondensed, selectedComponent, type EditorView } from '../core/store/editorViewStore';
14
- import { componentDirty } from '../core/store/editorStore';
24
+ import { a11yIssues, componentDirty } from '../core/store/editorStore';
15
25
  import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
16
26
  import { listComponents } from '../core/components/componentConfigService';
17
27
 
@@ -24,6 +34,7 @@
24
34
  { id: 'icon-sizes', label: 'Icon Sizes', icon: 'fas fa-expand' },
25
35
  { id: 'shadows', label: 'Shadows', icon: 'fas fa-cloud' },
26
36
  { id: 'washes', label: 'Washes', icon: 'fas fa-layer-group' },
37
+ { id: 'custom-colors', label: 'Custom Colours', icon: 'fas fa-eye-dropper' },
27
38
  { id: 'gradients', label: 'Gradients', icon: 'fas fa-droplet' },
28
39
  { id: 'utility-tokens', label: 'Utility Tokens', icon: 'fas fa-sliders' }
29
40
  ];
@@ -42,6 +53,8 @@
42
53
  { id: 'sketch-preview', label: 'Preview', icon: 'fas fa-eye' }
43
54
  ];
44
55
 
56
+ type NavItem = { id: string; label: string; icon: string; count?: number };
57
+
45
58
  let { onclose }: { onclose?: () => void } = $props();
46
59
 
47
60
  const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
@@ -98,6 +111,13 @@
98
111
  sectionJump = { id: sectionId };
99
112
  }
100
113
 
114
+ setA11yViewLink(async (sectionId) => {
115
+ editorView.set('accessibility');
116
+ await tick();
117
+ document.querySelector<HTMLDetailsElement>(`#${sectionId} details`)?.setAttribute('open', '');
118
+ scrollToSection(sectionId);
119
+ });
120
+
101
121
  function selectComponent(id: string) {
102
122
  selectedComponent.set(id);
103
123
  hideHint();
@@ -114,11 +134,26 @@
114
134
  let menuOpen = $state(false);
115
135
  let screenBarHeight = $state(0);
116
136
 
117
- const sectionNav: Partial<Record<EditorView, { id: string }[]>> = {
137
+ let a11yComponentNavItems: NavItem[] = $derived(
138
+ componentsWithIssues($a11yIssues).map(({ component, label, icon, count }) => ({
139
+ id: a11ySectionId(component), label, icon, count,
140
+ })),
141
+ );
142
+ let a11yMarked = $derived(new Set(componentsWithIssues($a11yIssues).map((row) => row.component)));
143
+ let a11yHasReview = $derived(Object.values($a11yIssues).some((issues) => issues.review.length > 0));
144
+
145
+ // The narrow layout shows one section at a time, so the run section and the
146
+ // check-by-eye line join the component sections the rail lists.
147
+ let sectionNav: Partial<Record<EditorView, NavItem[]>> = $derived({
118
148
  tokens: tokenNavItems,
119
149
  sketch: sketchNavItems,
120
150
  colors: colorsNavItems,
121
- };
151
+ accessibility: [
152
+ { id: A11Y_RUN_SECTION, label: 'Test run', icon: 'fas fa-universal-access' },
153
+ ...a11yComponentNavItems,
154
+ ...(a11yHasReview ? [{ id: A11Y_REVIEW_SECTION, label: 'Check by eye', icon: 'fas fa-eye' }] : []),
155
+ ],
156
+ });
122
157
 
123
158
  function shownSection(view: EditorView): string | null {
124
159
  const items = sectionNav[view];
@@ -126,8 +161,8 @@
126
161
  return items.some((i) => i.id === selectedTokenSection) ? selectedTokenSection : items[0].id;
127
162
  }
128
163
 
129
- let menuItems = $derived(
130
- $editorView === 'components' ? allComponentNavItems : (sectionNav[$editorView] as typeof tokenNavItems),
164
+ let menuItems: NavItem[] = $derived(
165
+ $editorView === 'components' ? allComponentNavItems : sectionNav[$editorView]!,
131
166
  );
132
167
  let activeItemId = $derived(
133
168
  themeScreen ? null : $editorView === 'components' ? $selectedComponent : shownSection($editorView),
@@ -213,12 +248,17 @@
213
248
  <button
214
249
  type="button"
215
250
  class="nav-item"
251
+ class:counted={item.count !== undefined}
216
252
  class:active={activeItemId === item.id}
253
+ aria-label={item.count !== undefined ? `${item.label}, ${issuesLabel(item.count)}` : undefined}
217
254
  class:dirty={$editorView === 'components' && $componentDirty[item.id]}
218
255
  onclick={() => pickSection(item.id)}
219
256
  >
220
257
  <i class={item.icon}></i>
221
- <span class="nav-label">{item.label}</span>
258
+ <span class="nav-label">{#if $editorView === 'components' && a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
259
+ {#if item.count !== undefined}
260
+ <span class="nav-count">{item.count}</span>
261
+ {/if}
222
262
  {#if $editorView === 'components' && $componentDirty[item.id]}
223
263
  <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
224
264
  {/if}
@@ -308,6 +348,28 @@
308
348
  <ThemePanel />
309
349
  </div>
310
350
  {/if}
351
+ {:else if $editorView === 'accessibility'}
352
+ <div class="nav-items">
353
+ {#each a11yComponentNavItems as item (item.id)}
354
+ <button
355
+ class="nav-item counted"
356
+ class:active={selectedTokenSection === item.id}
357
+ aria-label={`${item.label}, ${issuesLabel(item.count ?? 0)}`}
358
+ onmouseenter={(e) => showHint(item.label, e.currentTarget)}
359
+ onmouseleave={hideHint}
360
+ onclick={() => scrollToSection(item.id)}
361
+ >
362
+ <i class={item.icon}></i>
363
+ <span class="nav-label">{item.label}</span>
364
+ <span class="nav-count">{item.count}</span>
365
+ </button>
366
+ {/each}
367
+ </div>
368
+ {#if !condensed}
369
+ <div class="sidebar-footer">
370
+ <ThemePanel />
371
+ </div>
372
+ {/if}
311
373
  {:else}
312
374
  <div class="nav-items">
313
375
  {#each systemNavItems as item (item.id)}
@@ -320,10 +382,13 @@
320
382
  onclick={() => selectComponent(item.id)}
321
383
  >
322
384
  <i class={item.icon}></i>
323
- <span class="nav-label">{item.label}</span>
385
+ <span class="nav-label">{#if a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
324
386
  {#if $componentDirty[item.id]}
325
387
  <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
326
388
  {/if}
389
+ {#if condensed && a11yMarked.has(item.id)}
390
+ <A11yRailMark placement="corner" />
391
+ {/if}
327
392
  </button>
328
393
  {/each}
329
394
  {#if customNavItems.length > 0}
@@ -340,10 +405,13 @@
340
405
  onclick={() => selectComponent(item.id)}
341
406
  >
342
407
  <i class={item.icon}></i>
343
- <span class="nav-label">{item.label}</span>
408
+ <span class="nav-label">{#if a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
344
409
  {#if $componentDirty[item.id]}
345
410
  <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
346
411
  {/if}
412
+ {#if condensed && a11yMarked.has(item.id)}
413
+ <A11yRailMark placement="corner" />
414
+ {/if}
347
415
  </button>
348
416
  {/each}
349
417
  {/if}
@@ -366,6 +434,8 @@
366
434
  <ColorsTab />
367
435
  {:else if $editorView === 'sketch'}
368
436
  <SketchTab {sectionJump} />
437
+ {:else if $editorView === 'accessibility'}
438
+ <AccessibilityTab />
369
439
  {:else}
370
440
  <ComponentsTab selectedComponent={$selectedComponent} />
371
441
  {/if}
@@ -498,6 +568,11 @@
498
568
  min-width: 0;
499
569
  }
500
570
 
571
+ /* A classic scrollbar would cover a third of the 48px rail. It still scrolls by wheel, trackpad and keyboard. */
572
+ .sidebar.condensed {
573
+ scrollbar-width: none;
574
+ }
575
+
501
576
  .rail-toggle-row {
502
577
  display: flex;
503
578
  justify-content: flex-end;
@@ -606,10 +681,22 @@
606
681
  transition: opacity 180ms ease;
607
682
  }
608
683
 
609
- .layout.condensed .nav-label {
684
+ .layout.condensed .nav-label,
685
+ .layout.condensed .nav-count {
610
686
  opacity: 0;
611
687
  }
612
688
 
689
+ .nav-item.counted {
690
+ grid-template-columns: 48px 1fr auto;
691
+ }
692
+
693
+ .nav-count {
694
+ padding-right: var(--ui-space-12);
695
+ font-size: var(--ui-font-size-sm);
696
+ color: var(--ui-text-tertiary);
697
+ transition: opacity 180ms ease;
698
+ }
699
+
613
700
  .nav-item:hover {
614
701
  color: var(--ui-text-secondary);
615
702
  background: var(--ui-hover);