@motion-proto/live-tokens 0.97.0 → 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 (191) 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 +339 -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 +144 -19
  10. package/bin/lib/findings.mjs +20 -4
  11. package/bin/rules/a11y.mjs +53 -0
  12. package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
  13. package/dist-plugin/index.cjs +204 -3
  14. package/dist-plugin/index.js +306 -105
  15. package/dist-plugin/setColors/index.js +1 -1
  16. package/package.json +47 -2
  17. package/src/demo/LabeledSelect.svelte +1 -1
  18. package/src/editor/bootstrap.ts +5 -1
  19. package/src/editor/component-editor/BadgeEditor.svelte +13 -0
  20. package/src/editor/component-editor/ButtonEditor.svelte +33 -2
  21. package/src/editor/component-editor/CalloutEditor.svelte +13 -0
  22. package/src/editor/component-editor/CardEditor.svelte +12 -2
  23. package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
  24. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
  25. package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
  26. package/src/editor/component-editor/DialogEditor.svelte +15 -0
  27. package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
  28. package/src/editor/component-editor/ImageEditor.svelte +20 -9
  29. package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
  30. package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
  31. package/src/editor/component-editor/InputEditor.svelte +23 -0
  32. package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
  33. package/src/editor/component-editor/NotificationEditor.svelte +15 -0
  34. package/src/editor/component-editor/PanelEditor.svelte +1 -1
  35. package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
  36. package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
  37. package/src/editor/component-editor/SectionDividerEditor.svelte +40 -13
  38. package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
  39. package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
  40. package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
  41. package/src/editor/component-editor/SliderEditor.svelte +33 -0
  42. package/src/editor/component-editor/TabBarEditor.svelte +23 -2
  43. package/src/editor/component-editor/TableEditor.svelte +14 -0
  44. package/src/editor/component-editor/ToggleEditor.svelte +17 -0
  45. package/src/editor/component-editor/TooltipEditor.svelte +11 -2
  46. package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
  47. package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
  48. package/src/editor/component-editor/registration.ts +2 -1
  49. package/src/editor/component-editor/registry.ts +69 -27
  50. package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
  51. package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
  52. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
  53. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -3
  54. package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
  55. package/src/editor/core/a11y/a11yReport.ts +164 -0
  56. package/src/editor/core/a11y/a11yReportTypes.ts +77 -0
  57. package/src/editor/core/a11y/contrastFailures.ts +22 -0
  58. package/src/editor/core/a11y/contrastFixes.ts +41 -0
  59. package/src/editor/core/components/contrastPairRegistry.ts +18 -0
  60. package/src/editor/core/palettes/contrast.ts +44 -0
  61. package/src/editor/core/palettes/contrastPairs.ts +82 -0
  62. package/src/editor/core/palettes/resolveColor.ts +77 -0
  63. package/src/editor/core/store/editorStore.ts +4 -0
  64. package/src/editor/core/store/editorViewStore.ts +2 -2
  65. package/src/editor/core/store/unsavedEdits.ts +10 -0
  66. package/src/editor/core/themes/liveStateStream.ts +7 -1
  67. package/src/editor/core/themes/parsers/gradient.ts +5 -2
  68. package/src/editor/docs/chapters.ts +1 -0
  69. package/src/editor/docs/content/accessibility.md +411 -0
  70. package/src/editor/docs/content/editing-tokens.md +32 -1
  71. package/src/editor/docs/content.generated.ts +2 -1
  72. package/src/editor/pages/ComponentEditorPage.svelte +11 -1
  73. package/src/editor/pages/EditorShell.svelte +98 -10
  74. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  75. package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
  76. package/src/editor/skill-atlas/trees/create-component.ts +12 -12
  77. package/src/editor/styles/ui-editor.css +2 -0
  78. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  79. package/src/editor/ui/GradientEditor.svelte +9 -1
  80. package/src/editor/ui/ThemePanel.svelte +4 -4
  81. package/src/editor/ui/UIContrastReadout.svelte +186 -0
  82. package/src/editor/ui/UIInfoPopover.svelte +16 -2
  83. package/src/editor/ui/UIPaletteSelector.svelte +9 -0
  84. package/src/editor/ui/UITokenSelector.svelte +26 -2
  85. package/src/editor/ui/a11y/A11yByEye.svelte +264 -0
  86. package/src/editor/ui/a11y/A11yComponentIssues.svelte +121 -0
  87. package/src/editor/ui/a11y/A11yFindingRow.svelte +119 -0
  88. package/src/editor/ui/a11y/A11yIssueRow.svelte +152 -0
  89. package/src/editor/ui/a11y/A11yOverview.svelte +246 -0
  90. package/src/editor/ui/a11y/A11yPanel.svelte +153 -0
  91. package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
  92. package/src/editor/ui/a11y/A11yRailMark.svelte +41 -0
  93. package/src/editor/ui/a11y/AccessibilityTab.svelte +321 -0
  94. package/src/editor/ui/a11y/a11yView.ts +257 -0
  95. package/src/editor/ui/contrastReadout.ts +45 -0
  96. package/src/live-tokens/data/colors-and-type/halloween.json +1 -1
  97. package/src/live-tokens/data/themes/autumn.json +35 -35
  98. package/src/live-tokens/data/themes/halloween.json +36 -36
  99. package/src/live-tokens/data/themes/midnight-study.json +35 -35
  100. package/src/live-tokens/data/themes/ocean.json +35 -35
  101. package/src/live-tokens/data/themes/royal-velvet.json +35 -35
  102. package/src/live-tokens/data/themes/sketchy.json +35 -35
  103. package/src/live-tokens/data/themes/spring-meadow.json +35 -35
  104. package/src/live-tokens/data/themes/sunset.json +35 -35
  105. package/src/system/components/Badge.svelte +3 -3
  106. package/src/system/components/Button.svelte +12 -6
  107. package/src/system/components/CodeSnippet.svelte +8 -1
  108. package/src/system/components/CornerBadge.svelte +3 -3
  109. package/src/system/components/Dialog.svelte +43 -14
  110. package/src/system/components/IconButton.svelte +1 -1
  111. package/src/system/components/Image.svelte +7 -0
  112. package/src/system/components/ImageLightbox.svelte +18 -25
  113. package/src/system/components/InlineEditActions.svelte +1 -1
  114. package/src/system/components/Input.svelte +9 -2
  115. package/src/system/components/MenuSelect.svelte +31 -3
  116. package/src/system/components/Notification.svelte +1 -1
  117. package/src/system/components/ProgressBar.svelte +7 -1
  118. package/src/system/components/RadioButton.svelte +36 -3
  119. package/src/system/components/SegmentedControl.svelte +36 -2
  120. package/src/system/components/SlidePager.svelte +3 -3
  121. package/src/system/components/Slider.svelte +19 -15
  122. package/src/system/components/TabBar.svelte +33 -3
  123. package/src/system/components/Toggle.svelte +3 -3
  124. package/src/system/components/VideoFrame.svelte +1 -1
  125. package/src/system/components/VideoLightbox.svelte +2 -2
  126. package/src/system/internal/focusTrap.ts +35 -0
  127. package/src/testing-js/__aria__/badge.yml +40 -0
  128. package/src/testing-js/__aria__/button.yml +88 -0
  129. package/src/testing-js/__aria__/callout.yml +12 -0
  130. package/src/testing-js/__aria__/card.yml +8 -0
  131. package/src/testing-js/__aria__/codesnippet.yml +21 -0
  132. package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
  133. package/src/testing-js/__aria__/cornerbadge.yml +24 -0
  134. package/src/testing-js/__aria__/dialog.yml +42 -0
  135. package/src/testing-js/__aria__/iconbutton.yml +57 -0
  136. package/src/testing-js/__aria__/image.yml +10 -0
  137. package/src/testing-js/__aria__/imagelightbox.yml +50 -0
  138. package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
  139. package/src/testing-js/__aria__/input.yml +61 -0
  140. package/src/testing-js/__aria__/menuselect.yml +30 -0
  141. package/src/testing-js/__aria__/notification.yml +30 -0
  142. package/src/testing-js/__aria__/panel.yml +2 -0
  143. package/src/testing-js/__aria__/progressbar.yml +2 -0
  144. package/src/testing-js/__aria__/radiobutton.yml +20 -0
  145. package/src/testing-js/__aria__/sectiondivider.yml +9 -0
  146. package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
  147. package/src/testing-js/__aria__/sidenavigation.yml +304 -0
  148. package/src/testing-js/__aria__/slidepager.yml +72 -0
  149. package/src/testing-js/__aria__/slider.yml +28 -0
  150. package/src/testing-js/__aria__/tabbar.yml +35 -0
  151. package/src/testing-js/__aria__/table.yml +144 -0
  152. package/src/testing-js/__aria__/toggle.yml +12 -0
  153. package/src/testing-js/__aria__/tooltip.yml +2 -0
  154. package/src/testing-js/__aria__/videoframe.yml +6 -0
  155. package/src/testing-js/__aria__/videolightbox.yml +12 -0
  156. package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
  157. package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
  158. package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
  159. package/src/testing-js/{chunk-X6KHEVIG.js → chunk-7QXUJRSO.js} +97 -29
  160. package/src/testing-js/chunk-7QXUJRSO.js.map +1 -0
  161. package/src/testing-js/chunk-GZKUYLDW.js +193 -0
  162. package/src/testing-js/chunk-GZKUYLDW.js.map +1 -0
  163. package/src/testing-js/chunk-RTZQLW3F.js +649 -0
  164. package/src/testing-js/chunk-RTZQLW3F.js.map +1 -0
  165. package/src/testing-js/{chunk-SHBYZUPD.js → chunk-RWOT2XDB.js} +212 -15
  166. package/src/testing-js/chunk-RWOT2XDB.js.map +1 -0
  167. package/src/testing-js/{chunk-GKO27NGN.js → chunk-THWGMSRH.js} +2 -2
  168. package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
  169. package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
  170. package/src/testing-js/component-a11y.contract.js +313 -0
  171. package/src/testing-js/component-a11y.contract.js.map +1 -0
  172. package/src/testing-js/component-alias.contract.js +2 -2
  173. package/src/testing-js/component-behavior.contract.js +2 -2
  174. package/src/testing-js/component-editor.contract.js +6 -5
  175. package/src/testing-js/component-editor.contract.js.map +1 -1
  176. package/src/testing-js/component-render.contract.js +10 -6
  177. package/src/testing-js/component-render.contract.js.map +1 -1
  178. package/src/testing-js/index.d.ts +44 -16
  179. package/src/testing-js/index.js +11 -11
  180. package/src/testing-js/index.js.map +1 -1
  181. package/src/testing-js/page-compliance.contract.js +132 -13
  182. package/src/testing-js/page-compliance.contract.js.map +1 -1
  183. package/src/testing-js/vitest.js +3 -3
  184. package/template/_gitignore +3 -0
  185. package/template/package.json +9 -0
  186. package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
  187. package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
  188. package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
  189. package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
  190. /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
  191. /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-THWGMSRH.js.map} +0 -0
@@ -0,0 +1,321 @@
1
+ <script lang="ts">
2
+ import { a11yFinishedReport, a11yIssues, a11yReport, startA11yRun } from '../../core/a11y/a11yReport';
3
+ import { contrastFailures } from '../../core/a11y/contrastFailures';
4
+ import { allContrastPairs } from '../../core/components/contrastPairRegistry';
5
+ import { createDomResolver } from '../../core/palettes/resolveColor';
6
+ import { unsavedEdits } from '../../core/store/unsavedEdits';
7
+ import { listThemes } from '../../core/themes/themeService';
8
+ import UIPillButton from '../UIPillButton.svelte';
9
+ import A11yByEye from './A11yByEye.svelte';
10
+ import A11yComponentIssues from './A11yComponentIssues.svelte';
11
+ import A11yOverview from './A11yOverview.svelte';
12
+ import {
13
+ A11Y_REVIEW_SECTION,
14
+ A11Y_RUN_SECTION,
15
+ a11ySectionId,
16
+ codeSpans,
17
+ componentSections,
18
+ componentsInReport,
19
+ fixedLabel,
20
+ formatElapsed,
21
+ formatRunTime,
22
+ getA11yViewLink,
23
+ issuesLabel,
24
+ openComponentAt,
25
+ reviewLabel,
26
+ reviewResults,
27
+ } from './a11yView';
28
+
29
+ const openView = getA11yViewLink();
30
+
31
+ let tableRows = $derived(componentsInReport($a11yIssues));
32
+ let sectionRows = $derived(componentSections($a11yIssues));
33
+ let review = $derived(reviewResults($a11yIssues));
34
+ let running = $derived($a11yReport?.state === 'running' ? $a11yReport : null);
35
+ let failed = $derived($a11yReport?.state === 'failed' ? $a11yReport : null);
36
+ let finished = $derived($a11yFinishedReport);
37
+ let runProblems = $derived(finished?.findings.filter((f) => f.rule.startsWith('tests-')) ?? []);
38
+ let notInstalled = $derived(runProblems.some((f) => f.rule === 'tests-not-installed'));
39
+
40
+ /** Set by a press while edits were pending; the alert shows only while they still are. */
41
+ let blockedByEdits = $state(false);
42
+ let startError: string | null = $state(null);
43
+ let starting = $state(false);
44
+
45
+ let now = $state(Date.now());
46
+ $effect(() => {
47
+ if (!running) return;
48
+ now = Date.now();
49
+ const timer = setInterval(() => (now = Date.now()), 1000);
50
+ return () => clearInterval(timer);
51
+ });
52
+
53
+ let themeNames: Record<string, string> = $state({});
54
+ let themeName = $derived(finished?.theme ? (themeNames[finished.theme] ?? finished.theme) : null);
55
+ $effect(() => {
56
+ if (!finished?.theme) return;
57
+ listThemes()
58
+ .then((themes) => (themeNames = Object.fromEntries(themes.map((t) => [t.fileName, t.name]))))
59
+ .catch(() => {});
60
+ });
61
+
62
+ async function runTest() {
63
+ startError = null;
64
+ if ($unsavedEdits) {
65
+ blockedByEdits = true;
66
+ return;
67
+ }
68
+ blockedByEdits = false;
69
+ starting = true;
70
+ try {
71
+ await startA11yRun(contrastFailures(allContrastPairs(), createDomResolver()));
72
+ } catch (err) {
73
+ const { code, message } = err as Error & { code?: string };
74
+ startError = code === 'A11Y_RUN_IN_PROGRESS' ? 'A test is already running.' : message;
75
+ } finally {
76
+ starting = false;
77
+ }
78
+ }
79
+ </script>
80
+
81
+ {#snippet message(text: string)}
82
+ {#each codeSpans(text) as part, i (i)}{#if part.code}<code>{part.text}</code>{:else}{part.text}{/if}{/each}
83
+ {/snippet}
84
+
85
+ <div class="a11y-tab">
86
+ <section id={A11Y_RUN_SECTION} class="a11y-run">
87
+ <header class="a11y-head">
88
+ <h2 class="a11y-title">Accessibility</h2>
89
+ <UIPillButton
90
+ variant="primary"
91
+ icon="fa-play"
92
+ disabled={notInstalled || running !== null || starting}
93
+ onclick={runTest}
94
+ >Run test</UIPillButton>
95
+ </header>
96
+
97
+ {#if blockedByEdits && $unsavedEdits}
98
+ <p class="a11y-alert" role="alert">Save the changes you want to have checked.</p>
99
+ {/if}
100
+ {#if startError}
101
+ <p class="a11y-alert" role="alert">{startError}</p>
102
+ {/if}
103
+
104
+ {#if running}
105
+ <p class="a11y-status" role="status">Test running, {formatElapsed(now - Date.parse(running.startedAt))}</p>
106
+ {/if}
107
+ {#if failed}
108
+ <div class="a11y-failed">
109
+ <p class="a11y-status">The run of {formatRunTime(failed.startedAt)} failed.</p>
110
+ <details class="a11y-disclosure">
111
+ <summary>Error</summary>
112
+ <pre class="a11y-error">{failed.error}</pre>
113
+ </details>
114
+ </div>
115
+ {/if}
116
+
117
+ {#if finished}
118
+ <p class="a11y-status">
119
+ Last run {formatRunTime(finished.finishedAt)}{#if themeName}, theme {themeName}{/if}
120
+ </p>
121
+ {#if runProblems.length > 0}
122
+ <ul class="a11y-problems">
123
+ {#each runProblems as problem, i (i)}
124
+ <li>{@render message(problem.message)}</li>
125
+ {/each}
126
+ </ul>
127
+ {#if notInstalled}
128
+ <div class="a11y-recheck">
129
+ <UIPillButton size="compact" variant="outline" icon="fa-rotate-right" onclick={runTest}>Check again</UIPillButton>
130
+ </div>
131
+ {/if}
132
+ {/if}
133
+ {#if !notInstalled && sectionRows.length === 0}
134
+ <p class="a11y-summary">Every component passes the automated checks.</p>
135
+ {/if}
136
+ {:else if !running && !failed}
137
+ <div class="a11y-empty">
138
+ <p>
139
+ Run test checks every component in the open theme as saved. It scans each one with axe and Alfa,
140
+ presses the keys each one answers, and measures the contrast of every color pair.
141
+ </p>
142
+ <p>A run takes about three minutes. Each theme keeps its report until its next run.</p>
143
+ </div>
144
+ {/if}
145
+ </section>
146
+
147
+ {#if tableRows.length > 0}
148
+ <A11yOverview
149
+ rows={tableRows}
150
+ onjump={(component) => openView?.(a11ySectionId(component))}
151
+ onreview={() => openView?.(A11Y_REVIEW_SECTION)}
152
+ />
153
+ {/if}
154
+
155
+ {#each sectionRows as row (row.component)}
156
+ <section id={a11ySectionId(row.component)} class="editor-section-card a11y-component">
157
+ <header class="a11y-component-head">
158
+ <i class={row.icon} aria-hidden="true"></i>
159
+ <h3 class="editor-subsection-title">{row.label}</h3>
160
+ <span class="a11y-count">{[
161
+ row.count > 0 ? issuesLabel(row.count) : '',
162
+ row.issues.fixed.length > 0 ? fixedLabel(row.issues.fixed.length) : '',
163
+ ].filter(Boolean).join(', ')}</span>
164
+ <UIPillButton
165
+ size="compact"
166
+ variant="outline"
167
+ icon="fa-arrow-right"
168
+ title={`Open ${row.label} in the Components view`}
169
+ onclick={() => openComponentAt(row.component)}
170
+ >Open component</UIPillButton>
171
+ </header>
172
+ <A11yComponentIssues
173
+ component={row.component}
174
+ issues={row.issues}
175
+ onreveal={(variable) => openComponentAt(row.component, variable)}
176
+ onopen={() => openComponentAt(row.component)}
177
+ />
178
+ {#if row.review > 0}
179
+ <button type="button" class="a11y-review-link" onclick={() => openView?.(A11Y_REVIEW_SECTION)}>
180
+ {reviewLabel(row.review)}
181
+ </button>
182
+ {/if}
183
+ </section>
184
+ {/each}
185
+
186
+ {#if review.length > 0}
187
+ <A11yByEye />
188
+ {/if}
189
+ </div>
190
+
191
+ <style>
192
+ .a11y-tab {
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: var(--ui-space-20);
196
+ font-size: var(--ui-font-size-md);
197
+ color: var(--ui-text-secondary);
198
+ }
199
+
200
+ .a11y-run {
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: var(--ui-space-8);
204
+ }
205
+
206
+ .a11y-head {
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: space-between;
210
+ gap: var(--ui-space-12);
211
+ /* Clears an overlay scrollbar and lines the button up with the cards' own buttons. */
212
+ padding-right: var(--ui-space-20);
213
+ }
214
+
215
+ .a11y-title {
216
+ margin: 0;
217
+ font-size: var(--ui-font-size-2xl);
218
+ font-weight: var(--ui-font-weight-semibold);
219
+ color: var(--ui-text-primary);
220
+ }
221
+
222
+ .a11y-alert,
223
+ .a11y-status,
224
+ .a11y-summary,
225
+ .a11y-empty p {
226
+ margin: 0;
227
+ }
228
+
229
+ .a11y-alert {
230
+ color: var(--ui-highlight);
231
+ }
232
+
233
+ .a11y-summary {
234
+ color: var(--ui-text-primary);
235
+ font-weight: var(--ui-font-weight-semibold);
236
+ }
237
+
238
+ .a11y-empty {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: var(--ui-space-8);
242
+ }
243
+
244
+ .a11y-problems {
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: var(--ui-space-4);
248
+ margin: 0;
249
+ padding-left: var(--ui-space-20);
250
+ color: var(--ui-text-primary);
251
+ }
252
+
253
+ .a11y-recheck {
254
+ display: flex;
255
+ }
256
+
257
+ .a11y-failed {
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: var(--ui-space-4);
261
+ }
262
+
263
+ .a11y-disclosure summary {
264
+ cursor: pointer;
265
+ color: var(--ui-text-primary);
266
+ }
267
+
268
+ .a11y-error {
269
+ margin: var(--ui-space-8) 0 0;
270
+ padding: var(--ui-space-12);
271
+ overflow-x: auto;
272
+ background: var(--ui-surface-low);
273
+ border: 1px solid var(--ui-border-low);
274
+ border-radius: var(--ui-radius-md);
275
+ font-family: var(--ui-font-mono);
276
+ font-size: var(--ui-font-size-xs);
277
+ white-space: pre-wrap;
278
+ }
279
+
280
+ .a11y-component {
281
+ gap: var(--ui-space-12);
282
+ }
283
+
284
+ .a11y-component-head {
285
+ display: flex;
286
+ flex-wrap: wrap;
287
+ align-items: center;
288
+ gap: var(--ui-space-8) var(--ui-space-12);
289
+ color: var(--ui-text-primary);
290
+ }
291
+
292
+ .a11y-count {
293
+ font-size: var(--ui-font-size-sm);
294
+ color: var(--ui-text-tertiary);
295
+ }
296
+
297
+ .a11y-component-head :global(.ui-pill) {
298
+ margin-left: auto;
299
+ }
300
+
301
+ .a11y-review-link {
302
+ align-self: flex-start;
303
+ padding: 0;
304
+ background: none;
305
+ border: none;
306
+ font-size: var(--ui-font-size-sm);
307
+ color: var(--ui-text-tertiary);
308
+ text-decoration: underline;
309
+ text-underline-offset: 2px;
310
+ cursor: pointer;
311
+ }
312
+
313
+ .a11y-review-link:hover {
314
+ color: var(--ui-text-primary);
315
+ }
316
+
317
+ .a11y-tab code {
318
+ font-family: var(--ui-font-mono);
319
+ font-size: var(--ui-font-size-sm);
320
+ }
321
+ </style>
@@ -0,0 +1,257 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { writable } from 'svelte/store';
3
+ import { getComponentRegistry, getComponentRegistryEntries } from '../../component-editor/registry';
4
+ import { selectedComponent, setEditorView } from '../../core/store/editorViewStore';
5
+ import type { ComponentA11yIssues } from '../../core/a11y/a11yReport';
6
+ import type { A11yFinding } from '../../core/a11y/a11yReportTypes';
7
+
8
+ export const A11Y_RUN_SECTION = 'a11y-run';
9
+ export const A11Y_REVIEW_SECTION = 'a11y-review';
10
+
11
+ export function a11ySectionId(component: string): string {
12
+ return `a11y-component-${component}`;
13
+ }
14
+
15
+ const VIEW_LINK = Symbol('a11y-view-link');
16
+
17
+ /** Set by the editor shell, which hosts the Accessibility view. The standalone component page hosts none. */
18
+ export function setA11yViewLink(open: (sectionId: string) => void): void {
19
+ setContext(VIEW_LINK, open);
20
+ }
21
+
22
+ export function getA11yViewLink(): ((sectionId: string) => void) | undefined {
23
+ return getContext(VIEW_LINK);
24
+ }
25
+
26
+ export interface A11yRevealTarget {
27
+ component: string;
28
+ /** The property row to scroll to. Null scrolls to the component's issues panel. */
29
+ variable: string | null;
30
+ }
31
+
32
+ /** One-shot: where the component page scrolls once it opens. The issues panel of that component clears it. */
33
+ export const pendingA11yReveal = writable<A11yRevealTarget | null>(null);
34
+
35
+ export function openComponentAt(component: string, variable: string | null = null): void {
36
+ pendingA11yReveal.set({ component, variable });
37
+ selectedComponent.set(component);
38
+ setEditorView('components');
39
+ }
40
+
41
+ export function componentLabel(component: string): string {
42
+ return getComponentRegistry()[component]?.label ?? component;
43
+ }
44
+
45
+ /** The success criteria the suite's own checks link to by their Understanding page. */
46
+ const UNDERSTANDING_CRITERIA: Record<string, string> = {
47
+ 'non-text-contrast': '1.4.11',
48
+ 'text-spacing': '1.4.12',
49
+ };
50
+
51
+ /** Names the kind of page a finding's help URL opens, so the link says where it goes.
52
+ * axe and Alfa link to their own rule pages; the suite's own checks link to WCAG and the APG. */
53
+ export function guidanceLabel(url: string): string {
54
+ const { hostname, pathname } = new URL(url);
55
+ if (hostname === 'www.w3.org') {
56
+ const understanding = pathname.match(/\/Understanding\/([a-z-]+)\.html$/);
57
+ if (understanding) {
58
+ const criterion = UNDERSTANDING_CRITERIA[understanding[1]];
59
+ return criterion ? `Success criterion ${criterion}` : 'Success criterion';
60
+ }
61
+ if (pathname.includes('/Techniques/')) return 'Technique';
62
+ if (pathname.includes('/ARIA/apg/')) return 'Pattern';
63
+ }
64
+ if (hostname === 'playwright.dev') return 'Reference';
65
+ return 'Rule';
66
+ }
67
+
68
+ export interface A11yIssueTotals {
69
+ property: number;
70
+ code: number;
71
+ review: number;
72
+ }
73
+
74
+ /** A component's findings counted by the fix they need, as the view groups them. */
75
+ export function issueTotals(issues: ComponentA11yIssues): A11yIssueTotals {
76
+ return {
77
+ property: issues.contrast.length + issues.property.length,
78
+ code: issues.code.length,
79
+ review: issues.review.length,
80
+ };
81
+ }
82
+
83
+ export function accessibilityIssuesLabel(count: number): string {
84
+ return `${count} accessibility ${count === 1 ? 'issue' : 'issues'}`;
85
+ }
86
+
87
+ export function fixedLabel(count: number): string {
88
+ return `${count} ${count === 1 ? 'issue' : 'issues'} fixed`;
89
+ }
90
+
91
+ export function reviewLabel(count: number): string {
92
+ return `${count} ${count === 1 ? 'result' : 'results'} to check by eye`;
93
+ }
94
+
95
+ /** The findings a fix clears. A result to check by eye counts apart, since only a person can settle it. */
96
+ export function issueCount(issues: ComponentA11yIssues): number {
97
+ return issues.contrast.length + issues.property.length + issues.code.length;
98
+ }
99
+
100
+ /** One panel row: the findings that share an engine rule, a help text and the properties they trace to. */
101
+ export interface A11yFindingRow {
102
+ finding: A11yFinding;
103
+ /** Every finding the row merges, `finding` first. */
104
+ findings: A11yFinding[];
105
+ targets: string[];
106
+ views: string;
107
+ variables: string[];
108
+ }
109
+
110
+ const VIEW_LIMIT = 4;
111
+ const MORE_VIEWS = /^and \d+ more$/;
112
+
113
+ /** The views of several findings, once each, in the order they first
114
+ * appear. A finding lists four views and counts the rest, so the merged list
115
+ * cannot count what it drops and says "and more". */
116
+ function mergeViews(views: (string | undefined)[]): string {
117
+ const named: string[] = [];
118
+ let hidden = false;
119
+ for (const view of views) {
120
+ for (const part of view ? view.split('; ') : []) {
121
+ if (MORE_VIEWS.test(part)) hidden = true;
122
+ else if (!named.includes(part)) named.push(part);
123
+ }
124
+ }
125
+ const shown = named.slice(0, VIEW_LIMIT);
126
+ return hidden || named.length > shown.length ? [...shown, 'and more'].join('; ') : shown.join('; ');
127
+ }
128
+
129
+ /** Findings that share an engine rule, a help text and their traced properties merge into one row (decision 6). */
130
+ export function findingRows(findings: A11yFinding[]): A11yFindingRow[] {
131
+ const rows = new Map<string, A11yFinding[]>();
132
+ for (const finding of findings) {
133
+ const key = finding.details
134
+ ? [finding.rule, finding.details.engineRule, finding.details.help, ...(finding.details.variables ?? [])].join('\n')
135
+ : finding.message;
136
+ rows.set(key, [...(rows.get(key) ?? []), finding]);
137
+ }
138
+ return [...rows.values()].map((group) => ({
139
+ finding: group[0],
140
+ findings: group,
141
+ targets: [...new Set(group.flatMap((f) => (f.details ? [f.details.target] : [])))],
142
+ views: group.length === 1 ? (group[0].details?.view ?? '') : mergeViews(group.map((f) => f.details?.view)),
143
+ variables: group[0].details?.variables ?? [],
144
+ }));
145
+ }
146
+
147
+ /** A finding's own words. A keyboard case's engine rule is the case's own name, which the help alone leaves out. */
148
+ export function findingText(finding: A11yFinding): string {
149
+ if (!finding.details) return finding.message;
150
+ return finding.rule === 'a11y-keyboard' ? `${finding.details.engineRule}: ${finding.details.help}` : finding.details.help;
151
+ }
152
+
153
+ export function elementsLabel(count: number): string {
154
+ return `${count} ${count === 1 ? 'element' : 'elements'}`;
155
+ }
156
+
157
+ export function issuesLabel(count: number): string {
158
+ return `${count} ${count === 1 ? 'issue' : 'issues'}`;
159
+ }
160
+
161
+ export interface A11yComponentRow {
162
+ component: string;
163
+ label: string;
164
+ icon: string;
165
+ /** `issueCount`: the findings a fix clears. */
166
+ count: number;
167
+ /** The results to check by eye no one has marked. */
168
+ review: number;
169
+ issues: ComponentA11yIssues;
170
+ }
171
+
172
+ function componentRows(
173
+ all: Record<string, ComponentA11yIssues>,
174
+ listed: (row: A11yComponentRow) => boolean,
175
+ ): A11yComponentRow[] {
176
+ return getComponentRegistryEntries().flatMap(({ id, label, icon }) => {
177
+ const issues = all[id];
178
+ if (!issues) return [];
179
+ const row = { component: id, label, icon, count: issueCount(issues), review: issues.review.length, issues };
180
+ return listed(row) ? [row] : [];
181
+ });
182
+ }
183
+
184
+ /** Every registered component with an issue or an unmarked result to check by eye, in the component rail's order. */
185
+ export function componentsWithIssues(all: Record<string, ComponentA11yIssues>): A11yComponentRow[] {
186
+ return componentRows(all, (row) => row.count > 0 || row.review > 0);
187
+ }
188
+
189
+ /** Every registered component the overview table lists: an issue, a fix, or an unmarked result to check by eye.
190
+ * A result marked as looking right is acknowledged, so it alone lists nothing. */
191
+ export function componentsInReport(all: Record<string, ComponentA11yIssues>): A11yComponentRow[] {
192
+ return componentRows(all, (row) => row.count > 0 || row.review > 0 || row.issues.fixed.length > 0);
193
+ }
194
+
195
+ /** Every component with its own section in the Accessibility view: an issue or a fix. The By eye list holds the rest. */
196
+ export function componentSections(all: Record<string, ComponentA11yIssues>): A11yComponentRow[] {
197
+ return componentRows(all, (row) => row.count > 0 || row.issues.fixed.length > 0);
198
+ }
199
+
200
+ export interface A11yReviewResult {
201
+ component: string;
202
+ label: string;
203
+ row: A11yFindingRow;
204
+ confirmed: boolean;
205
+ }
206
+
207
+ /** Every result to check by eye, merged into rows per component in the rail's order, the unmarked ones first. */
208
+ export function reviewResults(all: Record<string, ComponentA11yIssues>): A11yReviewResult[] {
209
+ const entries = getComponentRegistryEntries();
210
+ const results = (confirmed: boolean) =>
211
+ entries.flatMap(({ id, label }) =>
212
+ findingRows((confirmed ? all[id]?.confirmed : all[id]?.review) ?? []).map((row) => ({ component: id, label, row, confirmed })),
213
+ );
214
+ return [...results(false), ...results(true)];
215
+ }
216
+
217
+ /** The words of `variable` after the component's prefix that `label` lacks,
218
+ * such as "focused" for `--input-focused-border` and "border color". Rows in
219
+ * different variants and states share a label, so these words tell them apart. */
220
+ function qualifier(component: string, variable: string, label: string): string {
221
+ const prefix = `--${component}-`;
222
+ if (!variable.startsWith(prefix)) return '';
223
+ const own = new Set(label.split(/[\s-]+/));
224
+ return variable.slice(prefix.length).split('-').filter((word) => !own.has(word)).join(' ');
225
+ }
226
+
227
+ /** The label of the property row that sets `variable`, led by its variant and
228
+ * state words, with its first letter raised. An intrinsic has no schema row,
229
+ * so its key names it. */
230
+ export function propertyLabel(component: string, variable: string): string | null {
231
+ const entry = getComponentRegistry()[component];
232
+ const token = entry?.schema.find((t) => t.variable === variable);
233
+ const label = token
234
+ ? [qualifier(component, variable, token.label), token.label].filter(Boolean).join(' ')
235
+ : entry?.intrinsics?.find((spec) => spec.variants.some((v) => spec.variable(v) === variable))?.key.replaceAll('-', ' ');
236
+ return label ? label.charAt(0).toUpperCase() + label.slice(1) : null;
237
+ }
238
+
239
+ /** A message split on its backticks, so a command or a variable renders as code. */
240
+ export function codeSpans(message: string): { text: string; code: boolean }[] {
241
+ return message.split('`').map((text, i) => ({ text, code: i % 2 === 1 })).filter((part) => part.text);
242
+ }
243
+
244
+ const pad = (n: number) => String(n).padStart(2, '0');
245
+
246
+ /** "14:02" for a time today, "22 Oct 14:02" for any other day. */
247
+ export function formatRunTime(iso: string, now: Date = new Date()): string {
248
+ const at = new Date(iso);
249
+ const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}`;
250
+ if (at.toDateString() === now.toDateString()) return clock;
251
+ return `${at.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })} ${clock}`;
252
+ }
253
+
254
+ export function formatElapsed(ms: number): string {
255
+ const seconds = Math.max(0, Math.floor(ms / 1000));
256
+ return `${Math.floor(seconds / 60)}:${pad(seconds % 60)}`;
257
+ }
@@ -0,0 +1,45 @@
1
+ import { CONTRAST_RULES, LARGE_BOLD_PX, LARGE_TEXT_PX } from '../core/palettes/contrast';
2
+ import { closestToFailing, type PairMeasure } from '../core/palettes/contrastPairs';
3
+
4
+ export type ReadoutState = 'pass' | 'fail' | 'inactive';
5
+
6
+ export interface Readout {
7
+ label: string;
8
+ state: ReadoutState;
9
+ ariaLabel: string;
10
+ measure: PairMeasure;
11
+ }
12
+
13
+ /** Truncation keeps a failing value from displaying as its floor. */
14
+ export function formatRatio(ratio: number): string {
15
+ return `${(Math.floor(ratio * 100 + 1e-9) / 100).toFixed(2)}:1`;
16
+ }
17
+
18
+ export const formatFloor = (floor: number) => `${floor}:1`;
19
+
20
+ export function measureState(measure: PairMeasure): ReadoutState {
21
+ if (measure.pair.inactive) return 'inactive';
22
+ return measure.passes ? 'pass' : 'fail';
23
+ }
24
+
25
+ export function readoutState(measures: PairMeasure[]): Readout {
26
+ const measure = closestToFailing(measures);
27
+ const state = measureState(measure);
28
+ const label = formatRatio(measure.ratio);
29
+ const verdict = state === 'inactive'
30
+ ? 'exempt as a disabled control'
31
+ : `${state === 'pass' ? 'meets' : 'below'} ${formatFloor(measure.floor)}`;
32
+ return { label, state, ariaLabel: `Contrast ${label}, ${verdict}`, measure };
33
+ }
34
+
35
+ export function ruleSentence(measure: PairMeasure): string {
36
+ const rule = CONTRAST_RULES[measure.pair.role];
37
+ let sentence = `WCAG 2.2 success criterion ${rule.criterion}, ${rule.name} (${rule.level}): `
38
+ + `${formatFloor(rule.floor)} against the colors behind it`;
39
+ if (rule.largeFloor !== undefined) {
40
+ sentence += `, or ${formatFloor(rule.largeFloor)} for large text (${LARGE_TEXT_PX}px, or ${LARGE_BOLD_PX}px bold)`;
41
+ }
42
+ sentence += '.';
43
+ if (measure.pair.inactive) sentence += ' Disabled controls are exempt.';
44
+ return sentence;
45
+ }
@@ -897,7 +897,7 @@
897
897
  },
898
898
  {
899
899
  "position": 100,
900
- "paletteLabel": "700"
900
+ "paletteLabel": "800"
901
901
  }
902
902
  ],
903
903
  "gradientSize": "window"