@motion-proto/live-tokens 0.97.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 (184) 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 +272 -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/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
  13. package/dist-plugin/index.cjs +148 -3
  14. package/dist-plugin/index.js +250 -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 +3 -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 +41 -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 +101 -0
  56. package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
  57. package/src/editor/core/a11y/contrastFailures.ts +22 -0
  58. package/src/editor/core/components/contrastPairRegistry.ts +18 -0
  59. package/src/editor/core/palettes/contrast.ts +44 -0
  60. package/src/editor/core/palettes/contrastPairs.ts +79 -0
  61. package/src/editor/core/palettes/resolveColor.ts +77 -0
  62. package/src/editor/core/store/editorStore.ts +4 -0
  63. package/src/editor/core/store/editorViewStore.ts +2 -2
  64. package/src/editor/core/store/unsavedEdits.ts +10 -0
  65. package/src/editor/core/themes/liveStateStream.ts +7 -1
  66. package/src/editor/core/themes/parsers/gradient.ts +5 -2
  67. package/src/editor/docs/chapters.ts +1 -0
  68. package/src/editor/docs/content/accessibility.md +362 -0
  69. package/src/editor/docs/content/editing-tokens.md +28 -1
  70. package/src/editor/docs/content.generated.ts +2 -1
  71. package/src/editor/pages/ComponentEditorPage.svelte +10 -1
  72. package/src/editor/pages/EditorShell.svelte +96 -10
  73. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  74. package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
  75. package/src/editor/skill-atlas/trees/create-component.ts +12 -12
  76. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  77. package/src/editor/ui/GradientEditor.svelte +9 -1
  78. package/src/editor/ui/ThemePanel.svelte +4 -4
  79. package/src/editor/ui/UIContrastReadout.svelte +186 -0
  80. package/src/editor/ui/UIInfoPopover.svelte +16 -2
  81. package/src/editor/ui/UIPaletteSelector.svelte +9 -0
  82. package/src/editor/ui/UITokenSelector.svelte +26 -2
  83. package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
  84. package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
  85. package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
  86. package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
  87. package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
  88. package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
  89. package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
  90. package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
  91. package/src/editor/ui/a11y/a11yView.ts +218 -0
  92. package/src/editor/ui/contrastReadout.ts +45 -0
  93. package/src/live-tokens/data/themes/autumn.json +20 -20
  94. package/src/live-tokens/data/themes/halloween.json +20 -20
  95. package/src/live-tokens/data/themes/midnight-study.json +20 -20
  96. package/src/live-tokens/data/themes/ocean.json +20 -20
  97. package/src/live-tokens/data/themes/royal-velvet.json +20 -20
  98. package/src/live-tokens/data/themes/sketchy.json +20 -20
  99. package/src/live-tokens/data/themes/spring-meadow.json +20 -20
  100. package/src/live-tokens/data/themes/sunset.json +20 -20
  101. package/src/system/components/Badge.svelte +3 -3
  102. package/src/system/components/Button.svelte +9 -3
  103. package/src/system/components/CodeSnippet.svelte +8 -1
  104. package/src/system/components/CornerBadge.svelte +3 -3
  105. package/src/system/components/Dialog.svelte +43 -14
  106. package/src/system/components/IconButton.svelte +1 -1
  107. package/src/system/components/Image.svelte +7 -0
  108. package/src/system/components/ImageLightbox.svelte +18 -25
  109. package/src/system/components/Input.svelte +8 -1
  110. package/src/system/components/MenuSelect.svelte +30 -2
  111. package/src/system/components/Notification.svelte +1 -1
  112. package/src/system/components/ProgressBar.svelte +6 -0
  113. package/src/system/components/RadioButton.svelte +36 -3
  114. package/src/system/components/SegmentedControl.svelte +36 -2
  115. package/src/system/components/SlidePager.svelte +2 -2
  116. package/src/system/components/Slider.svelte +13 -9
  117. package/src/system/components/TabBar.svelte +33 -3
  118. package/src/system/components/Toggle.svelte +2 -2
  119. package/src/system/components/VideoFrame.svelte +1 -1
  120. package/src/system/components/VideoLightbox.svelte +2 -2
  121. package/src/system/internal/focusTrap.ts +35 -0
  122. package/src/testing-js/__aria__/badge.yml +40 -0
  123. package/src/testing-js/__aria__/button.yml +88 -0
  124. package/src/testing-js/__aria__/callout.yml +12 -0
  125. package/src/testing-js/__aria__/card.yml +8 -0
  126. package/src/testing-js/__aria__/codesnippet.yml +21 -0
  127. package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
  128. package/src/testing-js/__aria__/cornerbadge.yml +24 -0
  129. package/src/testing-js/__aria__/dialog.yml +42 -0
  130. package/src/testing-js/__aria__/iconbutton.yml +57 -0
  131. package/src/testing-js/__aria__/image.yml +10 -0
  132. package/src/testing-js/__aria__/imagelightbox.yml +50 -0
  133. package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
  134. package/src/testing-js/__aria__/input.yml +61 -0
  135. package/src/testing-js/__aria__/menuselect.yml +30 -0
  136. package/src/testing-js/__aria__/notification.yml +30 -0
  137. package/src/testing-js/__aria__/panel.yml +2 -0
  138. package/src/testing-js/__aria__/progressbar.yml +2 -0
  139. package/src/testing-js/__aria__/radiobutton.yml +20 -0
  140. package/src/testing-js/__aria__/sectiondivider.yml +9 -0
  141. package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
  142. package/src/testing-js/__aria__/sidenavigation.yml +304 -0
  143. package/src/testing-js/__aria__/slidepager.yml +72 -0
  144. package/src/testing-js/__aria__/slider.yml +28 -0
  145. package/src/testing-js/__aria__/tabbar.yml +35 -0
  146. package/src/testing-js/__aria__/table.yml +144 -0
  147. package/src/testing-js/__aria__/toggle.yml +12 -0
  148. package/src/testing-js/__aria__/tooltip.yml +2 -0
  149. package/src/testing-js/__aria__/videoframe.yml +6 -0
  150. package/src/testing-js/__aria__/videolightbox.yml +12 -0
  151. package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
  152. package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
  153. package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
  154. package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
  155. package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
  156. package/src/testing-js/chunk-EWSGE27V.js +168 -0
  157. package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
  158. package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
  159. package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
  160. package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
  161. package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
  162. package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
  163. package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
  164. package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
  165. package/src/testing-js/component-a11y.contract.js +283 -0
  166. package/src/testing-js/component-a11y.contract.js.map +1 -0
  167. package/src/testing-js/component-alias.contract.js +2 -2
  168. package/src/testing-js/component-behavior.contract.js +2 -2
  169. package/src/testing-js/component-editor.contract.js +6 -5
  170. package/src/testing-js/component-editor.contract.js.map +1 -1
  171. package/src/testing-js/component-render.contract.js +10 -6
  172. package/src/testing-js/component-render.contract.js.map +1 -1
  173. package/src/testing-js/index.d.ts +44 -16
  174. package/src/testing-js/index.js +9 -9
  175. package/src/testing-js/page-compliance.contract.js +132 -13
  176. package/src/testing-js/page-compliance.contract.js.map +1 -1
  177. package/src/testing-js/vitest.js +3 -3
  178. package/template/package.json +9 -0
  179. package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
  180. package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
  181. package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
  182. package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
  183. /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
  184. /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
@@ -0,0 +1,242 @@
1
+ <script lang="ts">
2
+ import { issueTotals, issuesLabel, type A11yComponentRow, type A11yIssueTotals } from './a11yView';
3
+
4
+ interface Props {
5
+ rows: A11yComponentRow[];
6
+ onjump: (component: string) => void;
7
+ }
8
+
9
+ let { rows, onjump }: Props = $props();
10
+
11
+ const KINDS: { key: keyof A11yIssueTotals; label: string; column: string }[] = [
12
+ { key: 'property', label: 'Fix with a property', column: 'Property' },
13
+ { key: 'code', label: "Fix in the component's code", column: 'Code' },
14
+ { key: 'review', label: 'Check by eye', column: 'By eye' },
15
+ ];
16
+
17
+ let tallies = $derived(rows.map((row) => ({ row, totals: issueTotals(row.issues) })));
18
+ let sums = $derived(
19
+ tallies.reduce(
20
+ (acc, { totals }) => ({ property: acc.property + totals.property, code: acc.code + totals.code, review: acc.review + totals.review }),
21
+ { property: 0, code: 0, review: 0 },
22
+ ),
23
+ );
24
+ let total = $derived(sums.property + sums.code + sums.review);
25
+ let widest = $derived(Math.max(1, ...rows.map((row) => row.count)));
26
+ </script>
27
+
28
+ <section class="editor-section-card a11y-overview" aria-label="Overview">
29
+ <div class="a11y-stats">
30
+ <div class="a11y-stat a11y-stat-total">
31
+ <span class="a11y-stat-value">{total}</span>
32
+ <span class="a11y-stat-label">
33
+ {total === 1 ? 'issue' : 'issues'} in {rows.length} {rows.length === 1 ? 'component' : 'components'}
34
+ </span>
35
+ </div>
36
+ {#each KINDS as kind (kind.key)}
37
+ <div class="a11y-stat">
38
+ <span class="a11y-stat-value" class:zero={sums[kind.key] === 0}>{sums[kind.key]}</span>
39
+ <span class="a11y-stat-label">
40
+ <span class="a11y-swatch {kind.key}" aria-hidden="true"></span>{kind.label}
41
+ </span>
42
+ </div>
43
+ {/each}
44
+ </div>
45
+
46
+ <table class="a11y-table">
47
+ <thead>
48
+ <tr>
49
+ <th scope="col">Component</th>
50
+ {#each KINDS as kind (kind.key)}
51
+ <th scope="col" class="num">{kind.column}</th>
52
+ {/each}
53
+ <th scope="col" class="bar-col"><span class="a11y-hidden">Issues</span></th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ {#each tallies as { row, totals } (row.component)}
58
+ <tr>
59
+ <th scope="row">
60
+ <button type="button" class="a11y-jump" onclick={() => onjump(row.component)}>
61
+ <i class={row.icon} aria-hidden="true"></i>{row.label}
62
+ </button>
63
+ </th>
64
+ {#each KINDS as kind (kind.key)}
65
+ <td class="num" class:zero={totals[kind.key] === 0}>{totals[kind.key]}</td>
66
+ {/each}
67
+ <td class="bar-col">
68
+ <span class="a11y-bar" style:width={`${(row.count / widest) * 100}%`} title={issuesLabel(row.count)}>
69
+ {#each KINDS as kind (kind.key)}
70
+ {#if totals[kind.key] > 0}
71
+ <span class="a11y-bar-part {kind.key}" style:flex-grow={totals[kind.key]}></span>
72
+ {/if}
73
+ {/each}
74
+ </span>
75
+ </td>
76
+ </tr>
77
+ {/each}
78
+ </tbody>
79
+ </table>
80
+ </section>
81
+
82
+ <style>
83
+ .a11y-overview {
84
+ gap: var(--ui-space-20);
85
+ }
86
+
87
+ .a11y-stats {
88
+ display: grid;
89
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
90
+ gap: var(--ui-space-16);
91
+ }
92
+
93
+ .a11y-stat {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: var(--ui-space-4);
97
+ padding-left: var(--ui-space-16);
98
+ border-left: 1px solid var(--ui-border-low);
99
+ }
100
+
101
+ .a11y-stat-total {
102
+ padding-left: 0;
103
+ border-left: none;
104
+ }
105
+
106
+ .a11y-stat-value {
107
+ font-size: var(--ui-font-size-3xl);
108
+ font-weight: var(--ui-font-weight-semibold);
109
+ line-height: 1;
110
+ font-variant-numeric: tabular-nums;
111
+ color: var(--ui-text-primary);
112
+ }
113
+
114
+ .a11y-stat-total .a11y-stat-value {
115
+ font-size: var(--ui-font-size-4xl);
116
+ }
117
+
118
+ .a11y-stat-label {
119
+ display: flex;
120
+ align-items: baseline;
121
+ gap: var(--ui-space-6);
122
+ font-size: var(--ui-font-size-sm);
123
+ color: var(--ui-text-tertiary);
124
+ }
125
+
126
+ .zero {
127
+ color: var(--ui-text-muted);
128
+ }
129
+
130
+ .a11y-swatch {
131
+ flex: none;
132
+ width: var(--ui-space-8);
133
+ height: var(--ui-space-8);
134
+ border-radius: var(--ui-radius-full);
135
+ }
136
+
137
+ .property {
138
+ background: var(--ui-text-primary);
139
+ }
140
+
141
+ .code {
142
+ background: var(--ui-border-high);
143
+ }
144
+
145
+ .review {
146
+ border: 1px solid var(--ui-border-high);
147
+ }
148
+
149
+ .a11y-table {
150
+ width: 100%;
151
+ border-collapse: collapse;
152
+ font-size: var(--ui-font-size-sm);
153
+ }
154
+
155
+ .a11y-table th,
156
+ .a11y-table td {
157
+ padding: var(--ui-space-6) var(--ui-space-8);
158
+ text-align: left;
159
+ font-weight: var(--ui-font-weight-normal);
160
+ white-space: nowrap;
161
+ }
162
+
163
+ .a11y-table th:first-child {
164
+ padding-left: 0;
165
+ }
166
+
167
+ .a11y-table thead th {
168
+ font-size: var(--ui-font-size-xs);
169
+ color: var(--ui-text-tertiary);
170
+ border-bottom: 1px solid var(--ui-border-low);
171
+ }
172
+
173
+ .a11y-table tbody tr + tr > * {
174
+ border-top: 1px solid var(--ui-border-low);
175
+ }
176
+
177
+ .a11y-table .num {
178
+ width: 1%;
179
+ text-align: right;
180
+ font-variant-numeric: tabular-nums;
181
+ color: var(--ui-text-primary);
182
+ }
183
+
184
+ .a11y-table td.zero {
185
+ color: var(--ui-text-muted);
186
+ }
187
+
188
+ .bar-col {
189
+ width: 40%;
190
+ }
191
+
192
+ .a11y-jump {
193
+ display: inline-flex;
194
+ align-items: center;
195
+ gap: var(--ui-space-8);
196
+ padding: 0;
197
+ background: none;
198
+ border: none;
199
+ font: inherit;
200
+ color: var(--ui-text-secondary);
201
+ cursor: pointer;
202
+ }
203
+
204
+ .a11y-jump i {
205
+ width: var(--ui-space-16);
206
+ color: var(--ui-text-tertiary);
207
+ text-align: center;
208
+ }
209
+
210
+ .a11y-jump:hover {
211
+ color: var(--ui-text-primary);
212
+ text-decoration: underline;
213
+ text-underline-offset: var(--ui-space-2);
214
+ }
215
+
216
+ .a11y-bar {
217
+ display: flex;
218
+ gap: var(--ui-space-2);
219
+ height: var(--ui-space-8);
220
+ min-width: var(--ui-space-8);
221
+ }
222
+
223
+ .a11y-bar-part {
224
+ flex-basis: 0;
225
+ border-radius: var(--ui-radius-full);
226
+ }
227
+
228
+ .a11y-hidden {
229
+ position: absolute;
230
+ width: 1px;
231
+ height: 1px;
232
+ overflow: hidden;
233
+ clip-path: inset(50%);
234
+ white-space: nowrap;
235
+ }
236
+
237
+ @media (max-width: 600px) {
238
+ .bar-col {
239
+ display: none;
240
+ }
241
+ }
242
+ </style>
@@ -0,0 +1,128 @@
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 { accessibilityIssuesLabel, formatRunTime, issueCount, pendingA11yReveal } from './a11yView';
8
+
9
+ let { component }: { component: string } = $props();
10
+ const titleId = $props.id();
11
+
12
+ const editorCtx = getEditorContext();
13
+
14
+ let panel: HTMLElement | undefined = $state();
15
+ let issues = $derived($a11yIssues[component]);
16
+ let count = $derived(issues ? issueCount(issues) : 0);
17
+
18
+ /** Rendered rows for `variable`. The linked block can repeat a variant's row, so the variant's own row comes first. */
19
+ function renderedRows(variable: string): HTMLElement[] {
20
+ const scope = panel?.closest('.demo-block') ?? document;
21
+ const selector = `[data-token-variable="${variable}"], [data-token-variables~="${variable}"]`;
22
+ const rows = [...scope.querySelectorAll<HTMLElement>(selector)]
23
+ .filter((row) => row.getClientRects().length > 0);
24
+ return [...rows.filter((row) => row.closest('.variant-group')), ...rows.filter((row) => !row.closest('.variant-group'))];
25
+ }
26
+
27
+ async function reveal(variable: string) {
28
+ let rows = renderedRows(variable);
29
+ if (!rows[0]?.closest('.variant-group') && editorCtx) {
30
+ const request: RevealRequest = { variable, searches: [] };
31
+ editorCtx.revealVariable.set(request);
32
+ await tick();
33
+ await Promise.all(request.searches);
34
+ await tick();
35
+ rows = renderedRows(variable);
36
+ }
37
+ const row = rows[0];
38
+ if (row) flashRow(row.closest<HTMLElement>('.token-entry') ?? row);
39
+ }
40
+
41
+ function scrollTo(target: HTMLElement, block: ScrollLogicalPosition) {
42
+ const still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
43
+ target.scrollIntoView({ block, behavior: still ? 'auto' : 'smooth' });
44
+ }
45
+
46
+ function flash(target: HTMLElement, block: ScrollLogicalPosition = 'center') {
47
+ scrollTo(target, block);
48
+ const highlight = getComputedStyle(target).getPropertyValue('--ui-highlight').trim();
49
+ target.animate(
50
+ [{ boxShadow: `0 0 0 2px ${highlight}` }, { boxShadow: '0 0 0 2px transparent' }],
51
+ { duration: FLASH_DURATION_MS, easing: 'ease-in' },
52
+ );
53
+ }
54
+
55
+ /** A row's content runs to its edges, so its ring sits out to the sides on a pseudo-element. */
56
+ function flashRow(row: HTMLElement) {
57
+ scrollTo(row, 'center');
58
+ row.classList.add('a11y-flash-row');
59
+ row
60
+ .animate([{ opacity: 1 }, { opacity: 0 }], { duration: FLASH_DURATION_MS, easing: 'ease-in', pseudoElement: '::after' })
61
+ .finished.finally(() => row.classList.remove('a11y-flash-row'));
62
+ }
63
+
64
+ $effect(() => {
65
+ const pending = $pendingA11yReveal;
66
+ if (!pending || pending.component !== component || !panel) return;
67
+ pendingA11yReveal.set(null);
68
+ if (pending.variable) reveal(pending.variable);
69
+ else flash(panel, 'start');
70
+ });
71
+ </script>
72
+
73
+ {#if issues && count > 0 && $a11yFinishedReport}
74
+ <section class="editor-section-card a11y-panel" aria-labelledby={titleId} bind:this={panel}>
75
+ <header class="a11y-panel-head">
76
+ <i class="fa-solid fa-triangle-exclamation a11y-panel-mark" aria-hidden="true"></i>
77
+ <h3 id={titleId} class="a11y-panel-title">{accessibilityIssuesLabel(count)}</h3>
78
+ <span class="a11y-panel-run">Last run {formatRunTime($a11yFinishedReport.finishedAt)}</span>
79
+ </header>
80
+ <A11yComponentIssues {component} {issues} withReview onreveal={reveal} />
81
+ </section>
82
+ {/if}
83
+
84
+ <style>
85
+ :global(.a11y-flash-row) {
86
+ position: relative;
87
+ }
88
+
89
+ /* Overhangs the row by the same space-6 as its linked-hover band. */
90
+ :global(.a11y-flash-row)::after {
91
+ content: '';
92
+ position: absolute;
93
+ inset: calc(-1 * var(--ui-space-2)) calc(-1 * (var(--ui-space-6) + var(--ui-space-2)));
94
+ border: 2px solid var(--ui-highlight);
95
+ border-radius: calc(var(--ui-radius-sm) + var(--ui-space-6));
96
+ opacity: 0;
97
+ pointer-events: none;
98
+ }
99
+
100
+ .a11y-panel {
101
+ gap: var(--ui-space-12);
102
+ font-size: var(--ui-font-size-md);
103
+ color: var(--ui-text-secondary);
104
+ }
105
+
106
+ .a11y-panel-head {
107
+ display: flex;
108
+ align-items: baseline;
109
+ gap: var(--ui-space-8);
110
+ }
111
+
112
+ .a11y-panel-mark {
113
+ color: var(--ui-highlight);
114
+ }
115
+
116
+ .a11y-panel-title {
117
+ margin: 0;
118
+ font-size: var(--ui-font-size-lg);
119
+ font-weight: var(--ui-font-weight-semibold);
120
+ color: var(--ui-text-primary);
121
+ }
122
+
123
+ .a11y-panel-run {
124
+ margin-left: auto;
125
+ font-size: var(--ui-font-size-sm);
126
+ color: var(--ui-text-tertiary);
127
+ }
128
+ </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 review = $derived(findings.every((finding) => finding.rule === 'a11y-review'));
17
+ let label = $derived(review ? reviewLabel(findings.length) : accessibilityIssuesLabel(findings.length));
18
+ let rows = $derived(findingRows([
19
+ ...findings.filter((finding) => finding.rule !== 'a11y-review'),
20
+ ...findings.filter((finding) => finding.rule === 'a11y-review'),
21
+ ]));
22
+ </script>
23
+
24
+ {#if findings.length > 0}
25
+ <span class="a11y-property-mark" class:gutter={placement === 'gutter'}>
26
+ <UIInfoPopover title={label} ariaLabel={label}>
27
+ {#snippet trigger()}
28
+ {#if review}
29
+ <span class="a11y-property-mark-glyph review" aria-hidden="true"></span>
30
+ {:else}
31
+ <i class="fa-solid fa-triangle-exclamation a11y-property-mark-glyph" aria-hidden="true"></i>
32
+ {/if}
33
+ {/snippet}
34
+ <ul class="a11y-property-findings">
35
+ {#each rows as row, i (i)}
36
+ <A11yFindingRow component={row.finding.component ?? ''} {row} review={row.finding.rule === 'a11y-review'} />
37
+ {/each}
38
+ </ul>
39
+ </UIInfoPopover>
40
+ </span>
41
+ {/if}
42
+
43
+ <style>
44
+ .a11y-property-mark {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ margin-left: var(--ui-space-4);
48
+ line-height: 1;
49
+ }
50
+
51
+ /* No transform: it would make this span the containing block of the
52
+ popover, which positions itself against the viewport. */
53
+ .a11y-property-mark.gutter {
54
+ position: absolute;
55
+ top: 0;
56
+ bottom: 0;
57
+ right: 100%;
58
+ margin: 0 var(--ui-space-2) 0 0;
59
+ }
60
+
61
+ .a11y-property-mark-glyph {
62
+ color: var(--ui-highlight);
63
+ font-size: var(--ui-font-size-xs);
64
+ }
65
+
66
+ /* The "?" is generated content because the contract harness matches rows by their text. */
67
+ .a11y-property-mark-glyph.review::after {
68
+ content: '?';
69
+ color: var(--ui-text-tertiary);
70
+ font-size: var(--ui-font-size-sm);
71
+ font-weight: var(--ui-font-weight-bold);
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,34 @@
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
+ }
6
+
7
+ let { placement = 'inline' }: Props = $props();
8
+ const label = 'Accessibility issues';
9
+ </script>
10
+
11
+ {#if placement === 'inline'}
12
+ <i class="fa-solid fa-triangle-exclamation a11y-rail-mark" role="img" aria-label={label} title={label}></i>
13
+ {:else}
14
+ <i class="fa-solid fa-triangle-exclamation a11y-rail-mark corner" aria-hidden="true"></i>
15
+ {/if}
16
+
17
+ <style>
18
+ .a11y-rail-mark {
19
+ margin-right: var(--ui-space-6);
20
+ color: var(--ui-highlight);
21
+ font-size: var(--ui-font-size-sm);
22
+ }
23
+
24
+ /* Bottom-right of the icon, clear of the unsaved dot at its top-right. */
25
+ .corner {
26
+ position: absolute;
27
+ top: 50%;
28
+ left: 30px;
29
+ margin: 0;
30
+ font-size: var(--ui-font-size-xs);
31
+ text-shadow: 0 0 2px black;
32
+ pointer-events: none;
33
+ }
34
+ </style>