@motion-proto/live-tokens 0.96.0 → 0.98.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +5 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
  3. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
  4. package/CHANGELOG.md +309 -0
  5. package/README.md +2 -2
  6. package/bin/check-component.mjs +8 -0
  7. package/bin/check-page.mjs +6 -0
  8. package/bin/cli.mjs +36 -6
  9. package/bin/contractRunner.mjs +142 -17
  10. package/bin/lib/findings.mjs +20 -4
  11. package/bin/rules/a11y.mjs +53 -0
  12. package/bin/set-colors.mjs +1 -0
  13. package/bin/setup-claude.mjs +2 -2
  14. package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
  15. package/dist-plugin/index.cjs +148 -3
  16. package/dist-plugin/index.js +250 -105
  17. package/dist-plugin/setColors/index.cjs +7 -1
  18. package/dist-plugin/setColors/index.d.cts +9 -4
  19. package/dist-plugin/setColors/index.d.ts +9 -4
  20. package/dist-plugin/setColors/index.js +8 -2
  21. package/dist-plugin/setGeometry/index.d.cts +1 -1
  22. package/dist-plugin/setGeometry/index.d.ts +1 -1
  23. package/dist-plugin/setType/index.d.cts +1 -1
  24. package/dist-plugin/setType/index.d.ts +1 -1
  25. package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
  26. package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
  27. package/package.json +49 -2
  28. package/src/demo/LabeledSelect.svelte +1 -1
  29. package/src/editor/bootstrap.ts +3 -1
  30. package/src/editor/component-editor/BadgeEditor.svelte +13 -0
  31. package/src/editor/component-editor/ButtonEditor.svelte +33 -2
  32. package/src/editor/component-editor/CalloutEditor.svelte +13 -0
  33. package/src/editor/component-editor/CardEditor.svelte +12 -2
  34. package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
  35. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
  36. package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
  37. package/src/editor/component-editor/DialogEditor.svelte +15 -0
  38. package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
  39. package/src/editor/component-editor/ImageEditor.svelte +20 -9
  40. package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
  41. package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
  42. package/src/editor/component-editor/InputEditor.svelte +23 -0
  43. package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
  44. package/src/editor/component-editor/NotificationEditor.svelte +15 -0
  45. package/src/editor/component-editor/PanelEditor.svelte +1 -1
  46. package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
  47. package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
  48. package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
  49. package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
  50. package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
  51. package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
  52. package/src/editor/component-editor/SliderEditor.svelte +33 -0
  53. package/src/editor/component-editor/TabBarEditor.svelte +23 -2
  54. package/src/editor/component-editor/TableEditor.svelte +14 -0
  55. package/src/editor/component-editor/ToggleEditor.svelte +17 -0
  56. package/src/editor/component-editor/TooltipEditor.svelte +11 -2
  57. package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
  58. package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
  59. package/src/editor/component-editor/registration.ts +2 -1
  60. package/src/editor/component-editor/registry.ts +69 -27
  61. package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
  62. package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
  63. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
  64. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -10
  65. package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
  66. package/src/editor/core/a11y/a11yReport.ts +101 -0
  67. package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
  68. package/src/editor/core/a11y/contrastFailures.ts +22 -0
  69. package/src/editor/core/components/contrastPairRegistry.ts +18 -0
  70. package/src/editor/core/palettes/contrast.ts +44 -0
  71. package/src/editor/core/palettes/contrastPairs.ts +79 -0
  72. package/src/editor/core/palettes/resolveColor.ts +77 -0
  73. package/src/editor/core/store/editorPersistence.ts +9 -1
  74. package/src/editor/core/store/editorRenderer.ts +2 -0
  75. package/src/editor/core/store/editorStore.ts +29 -0
  76. package/src/editor/core/store/editorTypes.ts +9 -0
  77. package/src/editor/core/store/editorViewStore.ts +2 -2
  78. package/src/editor/core/store/unsavedEdits.ts +10 -0
  79. package/src/editor/core/themes/buildColors.ts +16 -5
  80. package/src/editor/core/themes/liveStateStream.ts +7 -1
  81. package/src/editor/core/themes/parsers/gradient.ts +5 -2
  82. package/src/editor/core/themes/slices/customColors.ts +226 -0
  83. package/src/editor/core/themes/themeTypes.ts +10 -0
  84. package/src/editor/docs/chapters.ts +1 -0
  85. package/src/editor/docs/content/accessibility.md +362 -0
  86. package/src/editor/docs/content/editing-tokens.md +56 -1
  87. package/src/editor/docs/content.generated.ts +2 -1
  88. package/src/editor/pages/ComponentEditorPage.svelte +10 -1
  89. package/src/editor/pages/EditorShell.svelte +97 -10
  90. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  91. package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
  92. package/src/editor/skill-atlas/trees/create-component.ts +12 -12
  93. package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
  94. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  95. package/src/editor/ui/GradientEditor.svelte +9 -1
  96. package/src/editor/ui/ThemePanel.svelte +4 -4
  97. package/src/editor/ui/UIContrastReadout.svelte +186 -0
  98. package/src/editor/ui/UIInfoPopover.svelte +16 -2
  99. package/src/editor/ui/UIPaletteSelector.svelte +347 -15
  100. package/src/editor/ui/UITokenSelector.svelte +33 -14
  101. package/src/editor/ui/VariablesTab.svelte +3 -0
  102. package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
  103. package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
  104. package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
  105. package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
  106. package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
  107. package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
  108. package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
  109. package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
  110. package/src/editor/ui/a11y/a11yView.ts +218 -0
  111. package/src/editor/ui/anchorToTrigger.ts +63 -0
  112. package/src/editor/ui/contrastReadout.ts +45 -0
  113. package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
  114. package/src/live-tokens/data/themes/autumn.json +20 -20
  115. package/src/live-tokens/data/themes/halloween.json +20 -20
  116. package/src/live-tokens/data/themes/midnight-study.json +20 -20
  117. package/src/live-tokens/data/themes/ocean.json +20 -20
  118. package/src/live-tokens/data/themes/royal-velvet.json +20 -20
  119. package/src/live-tokens/data/themes/sketchy.json +20 -20
  120. package/src/live-tokens/data/themes/spring-meadow.json +20 -20
  121. package/src/live-tokens/data/themes/sunset.json +20 -20
  122. package/src/system/components/Badge.svelte +3 -3
  123. package/src/system/components/Button.svelte +9 -3
  124. package/src/system/components/CodeSnippet.svelte +8 -1
  125. package/src/system/components/CornerBadge.svelte +3 -3
  126. package/src/system/components/Dialog.svelte +43 -14
  127. package/src/system/components/IconButton.svelte +1 -1
  128. package/src/system/components/Image.svelte +7 -0
  129. package/src/system/components/ImageLightbox.svelte +26 -26
  130. package/src/system/components/Input.svelte +8 -1
  131. package/src/system/components/MenuSelect.svelte +30 -2
  132. package/src/system/components/Notification.svelte +1 -1
  133. package/src/system/components/ProgressBar.svelte +6 -0
  134. package/src/system/components/RadioButton.svelte +36 -3
  135. package/src/system/components/SegmentedControl.svelte +36 -2
  136. package/src/system/components/SlidePager.svelte +2 -2
  137. package/src/system/components/Slider.svelte +13 -9
  138. package/src/system/components/TabBar.svelte +33 -3
  139. package/src/system/components/Toggle.svelte +2 -2
  140. package/src/system/components/VideoFrame.svelte +1 -1
  141. package/src/system/components/VideoLightbox.svelte +2 -2
  142. package/src/system/internal/focusTrap.ts +35 -0
  143. package/src/testing-js/__aria__/badge.yml +40 -0
  144. package/src/testing-js/__aria__/button.yml +88 -0
  145. package/src/testing-js/__aria__/callout.yml +12 -0
  146. package/src/testing-js/__aria__/card.yml +8 -0
  147. package/src/testing-js/__aria__/codesnippet.yml +21 -0
  148. package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
  149. package/src/testing-js/__aria__/cornerbadge.yml +24 -0
  150. package/src/testing-js/__aria__/dialog.yml +42 -0
  151. package/src/testing-js/__aria__/iconbutton.yml +57 -0
  152. package/src/testing-js/__aria__/image.yml +10 -0
  153. package/src/testing-js/__aria__/imagelightbox.yml +50 -0
  154. package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
  155. package/src/testing-js/__aria__/input.yml +61 -0
  156. package/src/testing-js/__aria__/menuselect.yml +30 -0
  157. package/src/testing-js/__aria__/notification.yml +30 -0
  158. package/src/testing-js/__aria__/panel.yml +2 -0
  159. package/src/testing-js/__aria__/progressbar.yml +2 -0
  160. package/src/testing-js/__aria__/radiobutton.yml +20 -0
  161. package/src/testing-js/__aria__/sectiondivider.yml +9 -0
  162. package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
  163. package/src/testing-js/__aria__/sidenavigation.yml +304 -0
  164. package/src/testing-js/__aria__/slidepager.yml +72 -0
  165. package/src/testing-js/__aria__/slider.yml +28 -0
  166. package/src/testing-js/__aria__/tabbar.yml +35 -0
  167. package/src/testing-js/__aria__/table.yml +144 -0
  168. package/src/testing-js/__aria__/toggle.yml +12 -0
  169. package/src/testing-js/__aria__/tooltip.yml +2 -0
  170. package/src/testing-js/__aria__/videoframe.yml +6 -0
  171. package/src/testing-js/__aria__/videolightbox.yml +12 -0
  172. package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
  173. package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
  174. package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
  175. package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
  176. package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
  177. package/src/testing-js/chunk-EWSGE27V.js +168 -0
  178. package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
  179. package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
  180. package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
  181. package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
  182. package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
  183. package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
  184. package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
  185. package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
  186. package/src/testing-js/component-a11y.contract.js +283 -0
  187. package/src/testing-js/component-a11y.contract.js.map +1 -0
  188. package/src/testing-js/component-alias.contract.js +2 -2
  189. package/src/testing-js/component-behavior.contract.js +2 -2
  190. package/src/testing-js/component-editor.contract.js +6 -5
  191. package/src/testing-js/component-editor.contract.js.map +1 -1
  192. package/src/testing-js/component-render.contract.js +10 -6
  193. package/src/testing-js/component-render.contract.js.map +1 -1
  194. package/src/testing-js/index.d.ts +44 -16
  195. package/src/testing-js/index.js +9 -9
  196. package/src/testing-js/page-compliance.contract.js +163 -18
  197. package/src/testing-js/page-compliance.contract.js.map +1 -1
  198. package/src/testing-js/vitest.js +3 -3
  199. package/template/package.json +9 -0
  200. package/src/editor/ui/keepInViewport.ts +0 -84
  201. package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
  202. package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
  203. package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
  204. package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
  205. /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
  206. /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
@@ -0,0 +1,103 @@
1
+ <script lang="ts">
2
+ import type { ComponentA11yIssues } from '../../core/a11y/a11yReport';
3
+ import { CONTRAST_RULES } from '../../core/palettes/contrast';
4
+ import { formatFloor, formatRatio } from '../contrastReadout';
5
+ import A11yFindingRow from './A11yFindingRow.svelte';
6
+ import A11yIssueRow from './A11yIssueRow.svelte';
7
+ import { findingRows, propertyLabel } from './a11yView';
8
+
9
+ interface Props {
10
+ component: string;
11
+ issues: ComponentA11yIssues;
12
+ /** Lists the results to check by eye as a third group. The Accessibility view lists them apart. */
13
+ withReview?: boolean;
14
+ onreveal?: (variable: string) => void;
15
+ /** Opens the component, for a row that traced no property. Absent on the component's own page. */
16
+ onopen?: () => void;
17
+ }
18
+
19
+ let { component, issues, withReview = false, onreveal, onopen }: Props = $props();
20
+
21
+ let propertyRows = $derived(findingRows(issues.property));
22
+ let codeRows = $derived(findingRows(issues.code));
23
+ let reviewRows = $derived(withReview ? findingRows(issues.review) : []);
24
+ </script>
25
+
26
+ {#if issues.contrast.length > 0 || propertyRows.length > 0}
27
+ <div class="a11y-group">
28
+ <h4 class="a11y-group-title">Fix with a property</h4>
29
+ <ul class="a11y-issues">
30
+ {#each issues.contrast as failure, i (i)}
31
+ {@const label = propertyLabel(component, failure.variable) ?? failure.variable}
32
+ {@const rule = CONTRAST_RULES[failure.role]}
33
+ <A11yIssueRow href={rule.url} linkLabel={`Success criterion ${rule.criterion}`}>
34
+ {label} {formatRatio(failure.ratio)}, needs {formatFloor(failure.floor)}
35
+ {#snippet detail()}
36
+ <code>{failure.variable}</code>{#if failure.context}, {failure.context}{/if}
37
+ {/snippet}
38
+ {#snippet links()}
39
+ {#if onreveal}
40
+ <button type="button" class="a11y-link" aria-label={`Go to ${label}`} title={failure.variable} onclick={() => onreveal(failure.variable)}>
41
+ {label} <i class="fas fa-arrow-right" aria-hidden="true"></i>
42
+ </button>
43
+ {/if}
44
+ {/snippet}
45
+ </A11yIssueRow>
46
+ {/each}
47
+ {#each propertyRows as row, i (i)}
48
+ <A11yFindingRow {component} {row} {onreveal} {onopen} />
49
+ {/each}
50
+ </ul>
51
+ </div>
52
+ {/if}
53
+
54
+ {#if codeRows.length > 0}
55
+ <div class="a11y-group">
56
+ <h4 class="a11y-group-title">Fix in the component's code</h4>
57
+ <ul class="a11y-issues">
58
+ {#each codeRows as row, i (i)}
59
+ <A11yFindingRow {component} {row} {onreveal} {onopen} />
60
+ {/each}
61
+ </ul>
62
+ </div>
63
+ {/if}
64
+
65
+ {#if reviewRows.length > 0}
66
+ <div class="a11y-group">
67
+ <h4 class="a11y-group-title">Check by eye</h4>
68
+ <ul class="a11y-issues">
69
+ {#each reviewRows as row, i (i)}
70
+ <A11yFindingRow {component} {row} review {onreveal} {onopen} />
71
+ {/each}
72
+ </ul>
73
+ </div>
74
+ {/if}
75
+
76
+ <style>
77
+ .a11y-group {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: var(--ui-space-4);
81
+ }
82
+
83
+ .a11y-group + .a11y-group {
84
+ margin-top: var(--ui-space-12);
85
+ }
86
+
87
+ .a11y-group-title {
88
+ margin: 0;
89
+ font-size: var(--ui-font-size-xs);
90
+ font-weight: var(--ui-font-weight-semibold);
91
+ color: var(--ui-text-secondary);
92
+ }
93
+
94
+ .a11y-group code {
95
+ font-family: var(--ui-font-mono);
96
+ }
97
+
98
+ .a11y-issues {
99
+ margin: 0;
100
+ padding: 0;
101
+ list-style: none;
102
+ }
103
+ </style>
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import A11yIssueRow from './A11yIssueRow.svelte';
3
+ import {
4
+ componentLabel,
5
+ elementsLabel,
6
+ findingText,
7
+ guidanceLabel,
8
+ propertyLabel,
9
+ type A11yFindingRow,
10
+ } from './a11yView';
11
+
12
+ interface Props {
13
+ component: string;
14
+ row: A11yFindingRow;
15
+ review?: boolean;
16
+ /** Leads the text, as the component's name does in a list that spans components. */
17
+ prefix?: string;
18
+ onreveal?: (variable: string) => void;
19
+ /** Opens the component, for a row that traced no property. Absent on the component's own page. */
20
+ onopen?: () => void;
21
+ }
22
+
23
+ let { component, row, review = false, prefix, onreveal, onopen }: Props = $props();
24
+
25
+ let properties = $derived(row.variables.map((variable) => ({ variable, label: propertyLabel(component, variable) ?? variable })));
26
+ let where = $derived([row.targets.length > 1 ? elementsLabel(row.targets.length) : '', row.views].filter(Boolean).join(', '));
27
+ let helpUrl = $derived(row.finding.details?.helpUrl);
28
+ let hasLinks = $derived((properties.length > 0 && onreveal !== undefined) || (properties.length === 0 && onopen !== undefined));
29
+ </script>
30
+
31
+ {#snippet location()}
32
+ {#if row.targets.length === 1}<code>{row.targets[0]}</code>{#if row.views}{`, ${row.views}`}{/if}{:else}{where}{/if}
33
+ {/snippet}
34
+
35
+ {#snippet links()}
36
+ {#if properties.length > 0 && onreveal}
37
+ {#each properties as { variable, label } (variable)}
38
+ <button type="button" class="a11y-link" aria-label={`Go to ${label}`} title={variable} onclick={() => onreveal(variable)}>
39
+ {label} <i class="fas fa-arrow-right" aria-hidden="true"></i>
40
+ </button>
41
+ {/each}
42
+ {:else if onopen}
43
+ <button type="button" class="a11y-link" onclick={onopen}>
44
+ Open {componentLabel(component)} <i class="fas fa-arrow-right" aria-hidden="true"></i>
45
+ </button>
46
+ {/if}
47
+ {/snippet}
48
+
49
+ <A11yIssueRow
50
+ {review}
51
+ href={helpUrl}
52
+ linkLabel={helpUrl ? guidanceLabel(helpUrl) : undefined}
53
+ detail={row.targets.length > 0 || row.views ? location : undefined}
54
+ links={hasLinks ? links : undefined}
55
+ >
56
+ {#if prefix}{prefix}: {/if}{findingText(row.finding)}
57
+ </A11yIssueRow>
@@ -0,0 +1,121 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ /** A result to check by eye: its text in grey, where an issue's is amber. */
6
+ review?: boolean;
7
+ href?: string;
8
+ /** Names what `href` points at, such as "Rule" or "Success criterion 1.4.11". */
9
+ linkLabel?: string;
10
+ children: Snippet;
11
+ detail?: Snippet;
12
+ /** The in-editor links that lead the link group, such as the traced properties. */
13
+ links?: Snippet;
14
+ }
15
+
16
+ let { review = false, href, linkLabel = 'Guidance', children, detail, links }: Props = $props();
17
+ </script>
18
+
19
+ <li class="a11y-issue" class:review>
20
+ <div class="a11y-issue-head">
21
+ <p class="a11y-issue-text">{@render children()}</p>
22
+ {#if links || href}
23
+ <div class="a11y-issue-links">
24
+ {@render links?.()}
25
+ {#if href}
26
+ <a
27
+ class="a11y-link a11y-issue-guidance"
28
+ {href}
29
+ target="_blank"
30
+ rel="noopener"
31
+ title={href}
32
+ aria-label={`${linkLabel} (opens in a new tab)`}
33
+ >
34
+ {linkLabel}
35
+ <i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i>
36
+ </a>
37
+ {/if}
38
+ </div>
39
+ {/if}
40
+ </div>
41
+ {#if detail}
42
+ <p class="a11y-issue-detail">{@render detail()}</p>
43
+ {/if}
44
+ </li>
45
+
46
+ <style>
47
+ .a11y-issue {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: var(--ui-space-4);
51
+ padding: var(--ui-space-12) 0;
52
+ }
53
+
54
+ .a11y-issue + :global(.a11y-issue) {
55
+ border-top: 1px solid var(--ui-border-low);
56
+ }
57
+
58
+ .a11y-issue-head {
59
+ display: flex;
60
+ flex-wrap: wrap;
61
+ align-items: center;
62
+ gap: var(--ui-space-8) var(--ui-space-16);
63
+ }
64
+
65
+ .a11y-issue-text,
66
+ .a11y-issue-detail {
67
+ margin: 0;
68
+ overflow-wrap: anywhere;
69
+ }
70
+
71
+ .a11y-issue-text {
72
+ flex: 1 1 16rem;
73
+ min-width: 0;
74
+ font-size: var(--ui-font-size-md);
75
+ color: var(--ui-highlight);
76
+ }
77
+
78
+ .review .a11y-issue-text {
79
+ color: var(--ui-text-secondary);
80
+ }
81
+
82
+ .a11y-issue-detail {
83
+ font-size: var(--ui-font-size-sm);
84
+ color: var(--ui-text-tertiary);
85
+ }
86
+
87
+ .a11y-issue-links {
88
+ display: flex;
89
+ flex-wrap: wrap;
90
+ align-items: center;
91
+ gap: var(--ui-space-6);
92
+ margin-left: auto;
93
+ }
94
+
95
+ .a11y-issue-links :global(.a11y-link) {
96
+ display: inline-flex;
97
+ align-items: center;
98
+ gap: var(--ui-space-6);
99
+ padding: var(--ui-space-2) var(--ui-space-10);
100
+ background: none;
101
+ border: 1px solid var(--ui-border);
102
+ border-radius: var(--ui-radius-full);
103
+ font: inherit;
104
+ font-size: var(--ui-font-size-sm);
105
+ color: var(--ui-text-secondary);
106
+ text-decoration: none;
107
+ white-space: nowrap;
108
+ cursor: pointer;
109
+ transition: background var(--ui-transition-fast), border-color var(--ui-transition-fast), color var(--ui-transition-fast);
110
+ }
111
+
112
+ .a11y-issue-links :global(.a11y-link:hover) {
113
+ background: var(--ui-surface-higher);
114
+ border-color: var(--ui-border-high);
115
+ color: var(--ui-text-primary);
116
+ }
117
+
118
+ .a11y-issue-links :global(.a11y-link i) {
119
+ font-size: var(--ui-font-size-xs);
120
+ }
121
+ </style>
@@ -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>