@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,246 @@
1
+ <script lang="ts">
2
+ import { issueTotals, type A11yComponentRow, type A11yIssueTotals } from './a11yView';
3
+
4
+ interface Props {
5
+ rows: A11yComponentRow[];
6
+ onjump: (component: string) => void;
7
+ /** Scrolls to the By eye list. */
8
+ onreview?: () => void;
9
+ }
10
+
11
+ let { rows, onjump, onreview }: Props = $props();
12
+
13
+ const KINDS: { key: keyof A11yIssueTotals; label: string; column: string }[] = [
14
+ { key: 'property', label: 'Fix with a property', column: 'Property' },
15
+ { key: 'code', label: "Fix in the component's code", column: 'Code' },
16
+ { key: 'review', label: 'Check by eye', column: 'By eye' },
17
+ ];
18
+
19
+ let tallies = $derived(
20
+ rows
21
+ .map((row) => ({ row, totals: issueTotals(row.issues) }))
22
+ .sort(
23
+ (a, b) =>
24
+ b.totals.property - a.totals.property ||
25
+ b.totals.code - a.totals.code ||
26
+ a.row.label.localeCompare(b.row.label),
27
+ ),
28
+ );
29
+ let sums = $derived(
30
+ tallies.reduce(
31
+ (acc, { totals }) => ({ property: acc.property + totals.property, code: acc.code + totals.code, review: acc.review + totals.review }),
32
+ { property: 0, code: 0, review: 0 },
33
+ ),
34
+ );
35
+ let total = $derived(sums.property + sums.code);
36
+ let withIssues = $derived(rows.filter((row) => row.count > 0).length);
37
+ let fixed = $derived(rows.reduce((sum, row) => sum + row.issues.fixed.length, 0));
38
+ </script>
39
+
40
+ <section class="editor-section-card a11y-overview" aria-label="Overview">
41
+ <div class="a11y-stats">
42
+ <div class="a11y-stat a11y-stat-total">
43
+ <span class="a11y-stat-value">{total}</span>
44
+ <span class="a11y-stat-label">
45
+ {total === 1 ? 'issue' : 'issues'} in {withIssues} {withIssues === 1 ? 'component' : 'components'}
46
+ </span>
47
+ </div>
48
+ {#each KINDS as kind (kind.key)}
49
+ {#if kind.key === 'review' && onreview}
50
+ <button type="button" class="a11y-stat a11y-stat-jump" onclick={onreview}>
51
+ <span class="a11y-stat-value" class:zero={sums[kind.key] === 0}>{sums[kind.key]}</span>
52
+ <span class="a11y-stat-label">{kind.label} <i class="fas fa-arrow-down" aria-hidden="true"></i></span>
53
+ </button>
54
+ {:else}
55
+ <div class="a11y-stat">
56
+ <span class="a11y-stat-value" class:zero={sums[kind.key] === 0}>{sums[kind.key]}</span>
57
+ <span class="a11y-stat-label">{kind.label}</span>
58
+ </div>
59
+ {/if}
60
+ {/each}
61
+ {#if fixed > 0}
62
+ <div class="a11y-stat a11y-stat-fixed">
63
+ <span class="a11y-stat-value">{fixed}</span>
64
+ <span class="a11y-stat-label">
65
+ <i class="fa-solid fa-check" aria-hidden="true"></i>Fixed since the run
66
+ </span>
67
+ </div>
68
+ {/if}
69
+ </div>
70
+
71
+ <table class="a11y-table">
72
+ <thead>
73
+ <tr>
74
+ <th scope="col">Component</th>
75
+ {#each KINDS as kind (kind.key)}
76
+ <th scope="col" class="num">{kind.column}</th>
77
+ {/each}
78
+ {#if fixed > 0}
79
+ <th scope="col" class="num">Fixed</th>
80
+ {/if}
81
+ </tr>
82
+ </thead>
83
+ <tbody>
84
+ {#each tallies as { row, totals } (row.component)}
85
+ <tr>
86
+ <th scope="row">
87
+ <button type="button" class="a11y-jump" onclick={() => onjump(row.component)}>
88
+ <i class={row.icon} aria-hidden="true"></i>{row.label}
89
+ </button>
90
+ </th>
91
+ {#each KINDS as kind (kind.key)}
92
+ <td class="num" class:zero={totals[kind.key] === 0}>{totals[kind.key]}</td>
93
+ {/each}
94
+ {#if fixed > 0}
95
+ <td class="num fixed" class:zero={row.issues.fixed.length === 0}>{row.issues.fixed.length}</td>
96
+ {/if}
97
+ </tr>
98
+ {/each}
99
+ </tbody>
100
+ </table>
101
+ </section>
102
+
103
+ <style>
104
+ .a11y-overview {
105
+ gap: var(--ui-space-20);
106
+ }
107
+
108
+ .a11y-stats {
109
+ display: grid;
110
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
111
+ gap: var(--ui-space-16);
112
+ }
113
+
114
+ .a11y-stat {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: var(--ui-space-4);
118
+ padding-left: var(--ui-space-16);
119
+ border-left: 1px solid var(--ui-border-low);
120
+ }
121
+
122
+ .a11y-stat-jump {
123
+ margin: 0;
124
+ background: none;
125
+ border-top: none;
126
+ border-right: none;
127
+ border-bottom: none;
128
+ font: inherit;
129
+ text-align: left;
130
+ cursor: pointer;
131
+ }
132
+
133
+ .a11y-stat-jump i {
134
+ font-size: var(--ui-font-size-xs);
135
+ opacity: 0;
136
+ transition: opacity var(--ui-transition-fast);
137
+ }
138
+
139
+ .a11y-stat-jump:hover .a11y-stat-label,
140
+ .a11y-stat-jump:focus-visible .a11y-stat-label {
141
+ color: var(--ui-text-primary);
142
+ }
143
+
144
+ .a11y-stat-jump:hover i,
145
+ .a11y-stat-jump:focus-visible i {
146
+ opacity: 1;
147
+ }
148
+
149
+ .a11y-stat-total {
150
+ padding-left: 0;
151
+ border-left: none;
152
+ }
153
+
154
+ .a11y-stat-value {
155
+ font-size: var(--ui-font-size-3xl);
156
+ font-weight: var(--ui-font-weight-semibold);
157
+ line-height: 1;
158
+ font-variant-numeric: tabular-nums;
159
+ color: var(--ui-text-primary);
160
+ }
161
+
162
+ .a11y-stat-total .a11y-stat-value {
163
+ font-size: var(--ui-font-size-4xl);
164
+ }
165
+
166
+ .a11y-stat-label {
167
+ display: flex;
168
+ align-items: baseline;
169
+ gap: var(--ui-space-6);
170
+ font-size: var(--ui-font-size-sm);
171
+ color: var(--ui-text-tertiary);
172
+ }
173
+
174
+ .zero {
175
+ color: var(--ui-text-muted);
176
+ }
177
+
178
+ .a11y-stat-fixed .a11y-stat-value,
179
+ .a11y-stat-fixed i,
180
+ .a11y-table td.fixed:not(.zero) {
181
+ color: var(--ui-fixed);
182
+ }
183
+
184
+ .a11y-table {
185
+ width: 100%;
186
+ border-collapse: collapse;
187
+ font-size: var(--ui-font-size-sm);
188
+ }
189
+
190
+ .a11y-table th,
191
+ .a11y-table td {
192
+ padding: var(--ui-space-6) var(--ui-space-8);
193
+ text-align: left;
194
+ font-weight: var(--ui-font-weight-normal);
195
+ white-space: nowrap;
196
+ }
197
+
198
+ .a11y-table th:first-child {
199
+ padding-left: 0;
200
+ }
201
+
202
+ .a11y-table thead th {
203
+ font-size: var(--ui-font-size-xs);
204
+ color: var(--ui-text-tertiary);
205
+ border-bottom: 1px solid var(--ui-border-low);
206
+ }
207
+
208
+ .a11y-table tbody tr + tr > * {
209
+ border-top: 1px solid var(--ui-border-low);
210
+ }
211
+
212
+ .a11y-table .num {
213
+ width: 1%;
214
+ text-align: right;
215
+ font-variant-numeric: tabular-nums;
216
+ color: var(--ui-text-primary);
217
+ }
218
+
219
+ .a11y-table td.zero {
220
+ color: var(--ui-text-muted);
221
+ }
222
+
223
+ .a11y-jump {
224
+ display: inline-flex;
225
+ align-items: center;
226
+ gap: var(--ui-space-8);
227
+ padding: 0;
228
+ background: none;
229
+ border: none;
230
+ font: inherit;
231
+ color: var(--ui-text-secondary);
232
+ cursor: pointer;
233
+ }
234
+
235
+ .a11y-jump i {
236
+ width: var(--ui-space-16);
237
+ color: var(--ui-text-tertiary);
238
+ text-align: center;
239
+ }
240
+
241
+ .a11y-jump:hover {
242
+ color: var(--ui-text-primary);
243
+ text-decoration: underline;
244
+ text-underline-offset: var(--ui-space-2);
245
+ }
246
+ </style>
@@ -0,0 +1,153 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import { a11yFinishedReport, a11yIssues } from '../../core/a11y/a11yReport';
4
+ import { FLASH_DURATION_MS } from '../../core/flashStatus';
5
+ import { getEditorContext, type RevealRequest } from '../../component-editor/scaffolding/editorContext';
6
+ import A11yComponentIssues from './A11yComponentIssues.svelte';
7
+ import {
8
+ accessibilityIssuesLabel,
9
+ fixedLabel,
10
+ formatRunTime,
11
+ issueCount,
12
+ pendingA11yReveal,
13
+ reviewLabel,
14
+ } from './a11yView';
15
+
16
+ let { component }: { component: string } = $props();
17
+ const titleId = $props.id();
18
+
19
+ const editorCtx = getEditorContext();
20
+
21
+ let panel: HTMLElement | undefined = $state();
22
+ let issues = $derived($a11yIssues[component]);
23
+ let count = $derived(issues ? issueCount(issues) : 0);
24
+ let fixedCount = $derived(issues?.fixed.length ?? 0);
25
+ let reviewCount = $derived(issues?.review.length ?? 0);
26
+
27
+ /** Rendered rows for `variable`. The linked block can repeat a variant's row, so the variant's own row comes first. */
28
+ function renderedRows(variable: string): HTMLElement[] {
29
+ const scope = panel?.closest('.demo-block') ?? document;
30
+ const selector = `[data-token-variable="${variable}"], [data-token-variables~="${variable}"]`;
31
+ const rows = [...scope.querySelectorAll<HTMLElement>(selector)]
32
+ .filter((row) => row.getClientRects().length > 0);
33
+ return [...rows.filter((row) => row.closest('.variant-group')), ...rows.filter((row) => !row.closest('.variant-group'))];
34
+ }
35
+
36
+ async function reveal(variable: string) {
37
+ let rows = renderedRows(variable);
38
+ if (!rows[0]?.closest('.variant-group') && editorCtx) {
39
+ const request: RevealRequest = { variable, searches: [] };
40
+ editorCtx.revealVariable.set(request);
41
+ await tick();
42
+ await Promise.all(request.searches);
43
+ await tick();
44
+ rows = renderedRows(variable);
45
+ }
46
+ const row = rows[0];
47
+ if (row) flashRow(row.closest<HTMLElement>('.token-entry') ?? row);
48
+ }
49
+
50
+ function scrollTo(target: HTMLElement, block: ScrollLogicalPosition) {
51
+ const still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
52
+ target.scrollIntoView({ block, behavior: still ? 'auto' : 'smooth' });
53
+ }
54
+
55
+ function flash(target: HTMLElement, block: ScrollLogicalPosition = 'center') {
56
+ scrollTo(target, block);
57
+ const highlight = getComputedStyle(target).getPropertyValue('--ui-highlight').trim();
58
+ target.animate(
59
+ [{ boxShadow: `0 0 0 2px ${highlight}` }, { boxShadow: '0 0 0 2px transparent' }],
60
+ { duration: FLASH_DURATION_MS, easing: 'ease-in' },
61
+ );
62
+ }
63
+
64
+ /** A row's content runs to its edges, so its ring sits out to the sides on a pseudo-element. */
65
+ function flashRow(row: HTMLElement) {
66
+ scrollTo(row, 'center');
67
+ row.classList.add('a11y-flash-row');
68
+ row
69
+ .animate([{ opacity: 1 }, { opacity: 0 }], { duration: FLASH_DURATION_MS, easing: 'ease-in', pseudoElement: '::after' })
70
+ .finished.finally(() => row.classList.remove('a11y-flash-row'));
71
+ }
72
+
73
+ $effect(() => {
74
+ const pending = $pendingA11yReveal;
75
+ if (!pending || pending.component !== component || !panel) return;
76
+ pendingA11yReveal.set(null);
77
+ if (pending.variable) reveal(pending.variable);
78
+ else flash(panel, 'start');
79
+ });
80
+ </script>
81
+
82
+ {#if issues && count + fixedCount + reviewCount > 0 && $a11yFinishedReport}
83
+ <section class="editor-section-card a11y-panel" aria-labelledby={titleId} bind:this={panel}>
84
+ <header class="a11y-panel-head">
85
+ {#if count > 0}
86
+ <i class="fa-solid fa-triangle-exclamation a11y-panel-mark" aria-hidden="true"></i>
87
+ <h3 id={titleId} class="a11y-panel-title">{accessibilityIssuesLabel(count)}</h3>
88
+ {:else if reviewCount > 0}
89
+ <i class="fa-solid fa-eye a11y-panel-mark review" aria-hidden="true"></i>
90
+ <h3 id={titleId} class="a11y-panel-title">{reviewLabel(reviewCount)}</h3>
91
+ {:else}
92
+ <i class="fa-solid fa-check a11y-panel-mark fixed" aria-hidden="true"></i>
93
+ <h3 id={titleId} class="a11y-panel-title">{fixedLabel(fixedCount)}</h3>
94
+ {/if}
95
+ <span class="a11y-panel-run">Last run {formatRunTime($a11yFinishedReport.finishedAt)}</span>
96
+ </header>
97
+ <A11yComponentIssues {component} {issues} withReview onreveal={reveal} />
98
+ </section>
99
+ {/if}
100
+
101
+ <style>
102
+ :global(.a11y-flash-row) {
103
+ position: relative;
104
+ }
105
+
106
+ /* Overhangs the row by the same space-6 as its linked-hover band. */
107
+ :global(.a11y-flash-row)::after {
108
+ content: '';
109
+ position: absolute;
110
+ inset: calc(-1 * var(--ui-space-2)) calc(-1 * (var(--ui-space-6) + var(--ui-space-2)));
111
+ border: 2px solid var(--ui-highlight);
112
+ border-radius: calc(var(--ui-radius-sm) + var(--ui-space-6));
113
+ opacity: 0;
114
+ pointer-events: none;
115
+ }
116
+
117
+ .a11y-panel {
118
+ gap: var(--ui-space-12);
119
+ font-size: var(--ui-font-size-md);
120
+ color: var(--ui-text-secondary);
121
+ }
122
+
123
+ .a11y-panel-head {
124
+ display: flex;
125
+ align-items: baseline;
126
+ gap: var(--ui-space-8);
127
+ }
128
+
129
+ .a11y-panel-mark {
130
+ color: var(--ui-highlight);
131
+ }
132
+
133
+ .a11y-panel-mark.fixed {
134
+ color: var(--ui-fixed);
135
+ }
136
+
137
+ .a11y-panel-mark.review {
138
+ color: var(--ui-text-tertiary);
139
+ }
140
+
141
+ .a11y-panel-title {
142
+ margin: 0;
143
+ font-size: var(--ui-font-size-lg);
144
+ font-weight: var(--ui-font-weight-semibold);
145
+ color: var(--ui-text-primary);
146
+ }
147
+
148
+ .a11y-panel-run {
149
+ margin-left: auto;
150
+ font-size: var(--ui-font-size-sm);
151
+ color: var(--ui-text-tertiary);
152
+ }
153
+ </style>
@@ -0,0 +1,84 @@
1
+ <script lang="ts">
2
+ import { a11yPropertyFindings } from '../../core/a11y/a11yReport';
3
+ import UIInfoPopover from '../UIInfoPopover.svelte';
4
+ import A11yFindingRow from './A11yFindingRow.svelte';
5
+ import { accessibilityIssuesLabel, findingRows, reviewLabel } from './a11yView';
6
+
7
+ interface Props {
8
+ variables: string[];
9
+ /** `gutter` hangs the mark left of its nearest positioned ancestor, in the gap between a row's label and its control. */
10
+ placement?: 'inline' | 'gutter';
11
+ }
12
+
13
+ let { variables, placement = 'inline' }: Props = $props();
14
+
15
+ let findings = $derived([...new Set(variables.flatMap((variable) => $a11yPropertyFindings.get(variable) ?? []))]);
16
+ let reviewCount = $derived(findings.filter((finding) => finding.rule === 'a11y-review').length);
17
+ let review = $derived(reviewCount === findings.length);
18
+ let label = $derived([
19
+ review ? '' : accessibilityIssuesLabel(findings.length - reviewCount),
20
+ reviewCount > 0 ? reviewLabel(reviewCount) : '',
21
+ ].filter(Boolean).join(', '));
22
+ let rows = $derived(findingRows([
23
+ ...findings.filter((finding) => finding.rule !== 'a11y-review'),
24
+ ...findings.filter((finding) => finding.rule === 'a11y-review'),
25
+ ]));
26
+ </script>
27
+
28
+ {#if findings.length > 0}
29
+ <span class="a11y-property-mark" class:gutter={placement === 'gutter'}>
30
+ <UIInfoPopover title={label} ariaLabel={label}>
31
+ {#snippet trigger()}
32
+ {#if review}
33
+ <i class="fa-solid fa-eye a11y-property-mark-glyph review" aria-hidden="true"></i>
34
+ {:else}
35
+ <i class="fa-solid fa-triangle-exclamation a11y-property-mark-glyph" aria-hidden="true"></i>
36
+ {/if}
37
+ {/snippet}
38
+ <ul class="a11y-property-findings">
39
+ {#each rows as row, i (i)}
40
+ <A11yFindingRow component={row.finding.component ?? ''} {row} review={row.finding.rule === 'a11y-review'} />
41
+ {/each}
42
+ </ul>
43
+ </UIInfoPopover>
44
+ </span>
45
+ {/if}
46
+
47
+ <style>
48
+ .a11y-property-mark {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ margin-left: var(--ui-space-4);
52
+ line-height: 1;
53
+ }
54
+
55
+ /* No transform: it would make this span the containing block of the
56
+ popover, which positions itself against the viewport. */
57
+ .a11y-property-mark.gutter {
58
+ position: absolute;
59
+ top: 0;
60
+ bottom: 0;
61
+ right: 100%;
62
+ margin: 0 var(--ui-space-2) 0 0;
63
+ }
64
+
65
+ .a11y-property-mark-glyph {
66
+ color: var(--ui-highlight);
67
+ font-size: var(--ui-font-size-xs);
68
+ }
69
+
70
+ .a11y-property-mark-glyph.review {
71
+ color: var(--ui-text-tertiary);
72
+ }
73
+
74
+ .a11y-property-findings {
75
+ margin: 0;
76
+ padding: 0;
77
+ list-style: none;
78
+ }
79
+
80
+ /* The popover spaces its own paragraphs, which would double the row's gaps. */
81
+ .a11y-property-findings :global(.a11y-issue p) {
82
+ margin: 0;
83
+ }
84
+ </style>
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /** `corner` overlaps the icon of a condensed rail, where the label and its inline mark are hidden. */
4
+ placement?: 'inline' | 'corner';
5
+ /** Draws the eye of a component whose only results are to check by eye. */
6
+ review?: boolean;
7
+ }
8
+
9
+ let { placement = 'inline', review = false }: Props = $props();
10
+ let label = $derived(review ? 'Results to check by eye' : 'Accessibility issues');
11
+ let glyph = $derived(review ? 'fa-eye' : 'fa-triangle-exclamation');
12
+ </script>
13
+
14
+ {#if placement === 'inline'}
15
+ <i class="fa-solid {glyph} a11y-rail-mark" class:review role="img" aria-label={label} title={label}></i>
16
+ {:else}
17
+ <i class="fa-solid {glyph} a11y-rail-mark corner" class:review aria-hidden="true"></i>
18
+ {/if}
19
+
20
+ <style>
21
+ .a11y-rail-mark {
22
+ margin-right: var(--ui-space-6);
23
+ color: var(--ui-highlight);
24
+ font-size: var(--ui-font-size-sm);
25
+ }
26
+
27
+ .review {
28
+ color: var(--ui-text-tertiary);
29
+ }
30
+
31
+ /* Bottom-right of the icon, clear of the unsaved dot at its top-right. */
32
+ .corner {
33
+ position: absolute;
34
+ top: 50%;
35
+ left: 30px;
36
+ margin: 0;
37
+ font-size: var(--ui-font-size-xs);
38
+ text-shadow: 0 0 2px black;
39
+ pointer-events: none;
40
+ }
41
+ </style>