@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,658 @@
1
+ // src/testing/support/traceProperties.ts
2
+ var BOX_SIZE = ["width", "height", "min-*", "inline-size", "block-size", "padding*"];
3
+ var FOCUS = ["focus", "focus-visible"];
4
+ var BORDER_PAINT = ["border-block*", "border-inline*", ...["", "-top", "-right", "-bottom", "-left"].flatMap((side) => [`border${side}`, `border${side}-color`, `border${side}-width`, `border${side}-style`])];
5
+ var MOTION = [
6
+ "animation",
7
+ "animation-name",
8
+ "animation-duration",
9
+ "animation-delay",
10
+ "animation-iteration-count",
11
+ "transition",
12
+ "transition-property",
13
+ "transition-duration",
14
+ "transition-delay"
15
+ ];
16
+ var TRACE_AXES = {
17
+ "color-contrast": {
18
+ target: ["color", "background*", "opacity", "text-shadow"],
19
+ pseudoElements: ["*"],
20
+ ancestors: ["background*", "opacity"]
21
+ },
22
+ "SIA-R65": {
23
+ target: ["outline*", "box-shadow", "border*", "background*", "color", "text-decoration*"],
24
+ pseudoElements: [],
25
+ ancestors: [],
26
+ force: FOCUS
27
+ },
28
+ "target-size": { target: BOX_SIZE, pseudoElements: BOX_SIZE, ancestors: [] },
29
+ "focus-indicator-contrast": {
30
+ target: ["outline*", "box-shadow", ...BORDER_PAINT, "background*", "color"],
31
+ pseudoElements: [],
32
+ ancestors: ["background*"],
33
+ force: FOCUS
34
+ },
35
+ "control-boundary-contrast": {
36
+ target: [...BORDER_PAINT, "outline", "outline-color", "outline-width", "outline-style", "background*"],
37
+ pseudoElements: [],
38
+ ancestors: ["background*"]
39
+ },
40
+ // Every element's line height is 1.5 under the override, so the theme's own cannot cause the overflow.
41
+ "text-spacing": {
42
+ target: ["height", "max-height", "width", "max-width", "padding*", "overflow*", "white-space"],
43
+ pseudoElements: [],
44
+ ancestors: []
45
+ },
46
+ // A pseudo-element that does not display does not move, so its display gates the motion, as Button's shimmer does.
47
+ "prefers-reduced-motion": { target: MOTION, pseudoElements: [...MOTION, "display"], ancestors: [] }
48
+ };
49
+ var sessions = /* @__PURE__ */ new WeakMap();
50
+ function cdpSession(page) {
51
+ let session = sessions.get(page);
52
+ if (!session) {
53
+ session = (async () => {
54
+ const cdp = await page.context().newCDPSession(page);
55
+ await cdp.send("DOM.enable");
56
+ await cdp.send("CSS.enable");
57
+ return cdp;
58
+ })();
59
+ sessions.set(page, session);
60
+ }
61
+ return session;
62
+ }
63
+ function onAxis(property, axis) {
64
+ return axis.some((name) => name.endsWith("*") ? property.startsWith(name.slice(0, -1)) : property === name);
65
+ }
66
+ function authored(style) {
67
+ return (style?.cssProperties ?? []).filter((declaration) => declaration.text);
68
+ }
69
+ function regularRules(styles) {
70
+ return (styles.matchedCSSRules ?? []).filter(({ rule }) => rule.origin === "regular").map(({ rule }) => rule);
71
+ }
72
+ function declarations(styles) {
73
+ return [authored(styles.inlineStyle), ...regularRules(styles).map((rule) => authored(rule.style))].flat();
74
+ }
75
+ function cascadeOrder(styles) {
76
+ return [...regularRules(styles).flatMap((rule) => authored(rule.style)), ...authored(styles.inlineStyle)];
77
+ }
78
+ function winning(ordered, name) {
79
+ const all = ordered.filter((d) => d.name === name);
80
+ return all.filter((d) => d.important || /!\s*important\s*$/i.test(d.value)).at(-1) ?? all.at(-1);
81
+ }
82
+ function ruleKey(rule) {
83
+ return JSON.stringify([rule.styleSheetId, rule.selectorList.text, rule.style.range]);
84
+ }
85
+ function longhandsIn(page) {
86
+ return async (names) => new Map(await page.evaluate((names2) => names2.map((name) => {
87
+ if (name.startsWith("--")) return [name, [name]];
88
+ const style = document.createElement("div").style;
89
+ style.setProperty(name, "initial");
90
+ return [name, style.length ? Array.from({ length: style.length }, (_, i) => style.item(i)) : [name]];
91
+ }), names));
92
+ }
93
+ async function forcedDeclarations(plain, forced, longhands) {
94
+ const before = new Set(regularRules(plain).map(ruleKey));
95
+ const cascade = [
96
+ ...regularRules(forced).map((rule) => ({ forcedOnly: !before.has(ruleKey(rule)), declarations: authored(rule.style) })),
97
+ { forcedOnly: false, declarations: authored(forced.inlineStyle) }
98
+ ];
99
+ const expanded = await longhands([...new Set(cascade.flatMap((entry) => entry.declarations.map((d) => d.name)))]);
100
+ const changed = /* @__PURE__ */ new Set();
101
+ const kept = [];
102
+ for (const { forcedOnly, declarations: declarations2 } of cascade) {
103
+ for (const d of declarations2) {
104
+ const sets = expanded.get(d.name) ?? [d.name];
105
+ if (forcedOnly) sets.forEach((name) => changed.add(name));
106
+ if (forcedOnly || sets.some((name) => changed.has(name))) kept.push(d);
107
+ }
108
+ }
109
+ return kept;
110
+ }
111
+ function references(value) {
112
+ return [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map((match) => match[1]);
113
+ }
114
+ function scopeDepths(page, selector, scope) {
115
+ return page.evaluate(({ selector: selector2, scope: scope2 }) => [...document.querySelectorAll(selector2)].map((element) => {
116
+ let depth = 0;
117
+ for (let node = element; node; node = node.parentElement, depth++) {
118
+ if (node.matches(scope2)) return depth;
119
+ }
120
+ return -1;
121
+ }), { selector, scope });
122
+ }
123
+ async function matchedStyles(cdp, nodeId, force = [], forceNodeId = nodeId) {
124
+ const forcePseudoState = (forcedPseudoClasses) => cdp.send("CSS.forcePseudoState", { nodeId: forceNodeId, forcedPseudoClasses });
125
+ if (force.length === 0) return cdp.send("CSS.getMatchedStylesForNode", { nodeId });
126
+ await forcePseudoState(force);
127
+ return cdp.send("CSS.getMatchedStylesForNode", { nodeId }).finally(() => forcePseudoState([]));
128
+ }
129
+ async function traceNode(cdp, node, axes, known, longhands) {
130
+ const matched = await matchedStyles(cdp, node.nodeId, axes.force, node.forceNodeId);
131
+ const own = axes.force ? await forcedDeclarations(await matchedStyles(cdp, node.nodeId), matched, longhands) : declarations(matched);
132
+ const pseudo = (matched.pseudoElements ?? []).flatMap(({ matches }) => declarations({ matchedCSSRules: matches }));
133
+ const chain = (matched.inherited ?? []).slice(0, node.depth);
134
+ const PSEUDO = -1;
135
+ const levels = [cascadeOrder(matched), ...chain.map(cascadeOrder)];
136
+ const pseudoCustom = pseudo.filter((d) => d.name.startsWith("--"));
137
+ const from = Math.max(node.ancestorsFrom - 1, 0);
138
+ const pending = [
139
+ ...own.filter((d) => onAxis(d.name, axes.target)).map((d) => ({ d, level: 0 })),
140
+ ...pseudo.filter((d) => onAxis(d.name, axes.pseudoElements)).map((d) => ({ d, level: PSEUDO })),
141
+ ...chain.slice(from).flatMap((entry, i) => declarations(entry).filter((d) => onAxis(d.name, axes.ancestors)).map((d) => ({ d, level: from + i + 1 })))
142
+ ].flatMap(({ d, level }) => references(d.value).map((name) => ({ name, level })));
143
+ const resolve = (name, level) => {
144
+ if (level === PSEUDO) {
145
+ const own2 = pseudoCustom.filter((d) => d.name === name);
146
+ if (own2.length > 0) return { declarations: own2, level: PSEUDO };
147
+ }
148
+ for (let at = Math.max(level, 0); at < levels.length; at++) {
149
+ const won = winning(levels[at], name);
150
+ if (won) return { declarations: [won], level: at };
151
+ }
152
+ return { declarations: [], level };
153
+ };
154
+ const found = /* @__PURE__ */ new Set();
155
+ const followed = /* @__PURE__ */ new Set();
156
+ for (let next = pending.pop(); next !== void 0; next = pending.pop()) {
157
+ const { name, level } = next;
158
+ const key = `${name}@${level}`;
159
+ if (known.has(name)) found.add(name);
160
+ else if (!followed.has(key)) {
161
+ followed.add(key);
162
+ const resolved = resolve(name, level);
163
+ for (const d of resolved.declarations) pending.push(...references(d.value).map((ref) => ({ name: ref, level: resolved.level })));
164
+ }
165
+ }
166
+ return [...found];
167
+ }
168
+ async function traceProperties(page, target, axes, known) {
169
+ let depths;
170
+ let forceDepths = [];
171
+ try {
172
+ depths = await scopeDepths(page, target.selector, target.scope);
173
+ if (target.forceOn) forceDepths = await scopeDepths(page, target.forceOn, target.scope);
174
+ } catch {
175
+ return [];
176
+ }
177
+ const cdp = await cdpSession(page);
178
+ const { root } = await cdp.send("DOM.getDocument", { depth: 0 });
179
+ const query = async (selector) => (await cdp.send("DOM.querySelectorAll", { nodeId: root.nodeId, selector })).nodeIds;
180
+ const nodeIds = await query(target.selector);
181
+ const forceNodeId = target.forceOn ? (await query(target.forceOn)).find((_, index) => forceDepths[index] >= 0) : void 0;
182
+ if (target.forceOn && forceNodeId === void 0) return [];
183
+ const longhands = longhandsIn(page);
184
+ const found = /* @__PURE__ */ new Set();
185
+ for (const [index, nodeId] of nodeIds.entries()) {
186
+ if (depths[index] < 0) continue;
187
+ const node = { nodeId, depth: depths[index], forceNodeId, ancestorsFrom: target.ancestorsFrom ?? 1 };
188
+ for (const name of await traceNode(cdp, node, axes, known, longhands)) found.add(name);
189
+ }
190
+ return [...found].sort();
191
+ }
192
+
193
+ // src/testing/support/nonTextContrast.ts
194
+ var FOCUS_INDICATOR_RULE = "focus-indicator-contrast";
195
+ var BOUNDARY_RULE = "control-boundary-contrast";
196
+ var NON_TEXT_URL = "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html";
197
+ var FOCUS_ATTR = "data-lt-focus-probe";
198
+ var NO_TRANSITION_ID = "lt-focus-probe-no-transition";
199
+ var FORCE = ["focus", "focus-visible"];
200
+ function inPage(request) {
201
+ const scopes = [...document.querySelectorAll("scope" in request && request.scope || ":not(*)")];
202
+ const excluded = (el) => "exclude" in request && request.exclude.some((selector) => el.closest(selector));
203
+ const inScope = (el) => scopes.some((scope) => scope.contains(el)) && !excluded(el);
204
+ const canvas = document.createElement("canvas");
205
+ canvas.width = canvas.height = 1;
206
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
207
+ const paint = (layers) => {
208
+ ctx.clearRect(0, 0, 1, 1);
209
+ for (const layer of ["#fff", ...layers]) {
210
+ ctx.fillStyle = "transparent";
211
+ ctx.fillStyle = layer;
212
+ ctx.fillRect(0, 0, 1, 1);
213
+ }
214
+ return [...ctx.getImageData(0, 0, 1, 1).data.slice(0, 3)];
215
+ };
216
+ const alpha = (color) => {
217
+ ctx.clearRect(0, 0, 1, 1);
218
+ ctx.fillStyle = "transparent";
219
+ ctx.fillStyle = color;
220
+ ctx.fillRect(0, 0, 1, 1);
221
+ return ctx.getImageData(0, 0, 1, 1).data[3] / 255;
222
+ };
223
+ const rgb = (channels) => `rgb(${channels.join(" ")})`;
224
+ const luminance = (channels) => {
225
+ const [r, g, b] = channels.map((c) => {
226
+ const s = c / 255;
227
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
228
+ });
229
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
230
+ };
231
+ const contrast = (a, b) => {
232
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
233
+ return (hi + 0.05) / (lo + 0.05);
234
+ };
235
+ const selectorFor = (el) => {
236
+ const count = (selector) => scopes.reduce((n, scope) => n + (scope.matches(selector) ? 1 : 0) + scope.querySelectorAll(selector).length, 0);
237
+ const own = (e) => e.tagName.toLowerCase() + [...e.classList].filter((name) => !name.startsWith("svelte-") && /^[\w-]+$/.test(name)).map((name) => `.${name}`).join("");
238
+ const path = [];
239
+ for (let e = el; e; e = e.parentElement) {
240
+ const step = own(e);
241
+ path.unshift(step);
242
+ if (count(path.join(" > ")) === 1) break;
243
+ const same = [...e.parentElement?.children ?? []].filter((c) => c.tagName === e.tagName);
244
+ if (same.length > 1) path[0] = `${step}:nth-of-type(${same.indexOf(e) + 1})`;
245
+ if (count(path.join(" > ")) === 1 || scopes.includes(e)) break;
246
+ }
247
+ return path.join(" > ");
248
+ };
249
+ const backdrop = (from, ring) => {
250
+ const layers = [];
251
+ let level = 0;
252
+ let first = -1;
253
+ for (let e = from; e; e = e.parentElement, level++) {
254
+ const box = e.getBoundingClientRect();
255
+ const contains = box.left <= ring.left + 0.5 && box.top <= ring.top + 0.5 && box.right >= ring.right - 0.5 && box.bottom >= ring.bottom - 0.5;
256
+ if (!contains) continue;
257
+ if (first < 0) first = level;
258
+ const style = getComputedStyle(e);
259
+ if (style.backgroundImage !== "none") return { color: null, first };
260
+ const a = alpha(style.backgroundColor);
261
+ if (a > 0) layers.unshift(style.backgroundColor);
262
+ if (a >= 1) break;
263
+ }
264
+ return { color: paint(layers), first: Math.max(first, 0) };
265
+ };
266
+ const inflate = (box, by) => new DOMRect(box.x - by, box.y - by, box.width + 2 * by, box.height + 2 * by);
267
+ const px = (value) => parseFloat(value) || 0;
268
+ const html = (el) => el.outerHTML.replace(/\s+/g, " ").slice(0, 300);
269
+ const SIDES = ["Top", "Right", "Bottom", "Left"];
270
+ const visibleSide = (style, side) => {
271
+ const s = style;
272
+ return !["none", "hidden"].includes(s[`border${side}Style`]) && px(s[`border${side}Width`]) >= 0.99 ? s[`border${side}Color`] : null;
273
+ };
274
+ const candidates = (target) => {
275
+ const list = [target, ...target.querySelectorAll("*")];
276
+ for (const sibling of target.parentElement?.children ?? []) {
277
+ if (sibling !== target) list.push(sibling, ...sibling.querySelectorAll("*"));
278
+ }
279
+ return list.filter(inScope);
280
+ };
281
+ const signature = (el) => {
282
+ const s = getComputedStyle(el);
283
+ return JSON.stringify([
284
+ [s.outlineStyle, s.outlineWidth, s.outlineColor, s.outlineOffset],
285
+ s.boxShadow,
286
+ SIDES.map((side) => visibleSide(s, side))
287
+ ]);
288
+ };
289
+ const indicatorOf = (el, before) => {
290
+ const [outlineBefore, shadowBefore, bordersBefore] = JSON.parse(before);
291
+ const s = getComputedStyle(el);
292
+ const outline = [s.outlineStyle, s.outlineWidth, s.outlineColor, s.outlineOffset];
293
+ if (JSON.stringify(outline) !== JSON.stringify(outlineBefore) && s.outlineStyle !== "none" && px(s.outlineWidth) > 0) {
294
+ if (s.outlineStyle === "auto") return { channel: "browser", color: "", extent: 0 };
295
+ if (alpha(s.outlineColor) > 0) return { channel: "outline", color: s.outlineColor, extent: px(s.outlineOffset) + px(s.outlineWidth) };
296
+ }
297
+ if (s.boxShadow !== shadowBefore && s.boxShadow !== "none") {
298
+ const shadows = s.boxShadow.split(/,(?![^(]*\))/).map((part) => part.trim());
299
+ for (const shadow of shadows) {
300
+ if (/\binset\b/.test(shadow)) continue;
301
+ const color = /^(?:[a-z-]+\([^)]*\)|#[\da-f]+|[a-z]+)/i.exec(shadow)?.[0] ?? "";
302
+ if (!color || alpha(color) === 0) continue;
303
+ const [x = 0, y = 0, blur = 0, spread = 0] = (shadow.slice(color.length).match(/-?[\d.]+px/g) ?? []).map(px);
304
+ return { channel: "shadow", color, extent: Math.max(Math.abs(x), Math.abs(y)) + blur + spread };
305
+ }
306
+ }
307
+ const borders = SIDES.map((side) => visibleSide(s, side));
308
+ for (const [i, color] of borders.entries()) {
309
+ if (color && color !== bordersBefore[i] && alpha(color) > 0) return { channel: "border", color, extent: 0 };
310
+ }
311
+ return null;
312
+ };
313
+ switch (request.kind) {
314
+ case "focusables": {
315
+ const style = document.createElement("style");
316
+ style.id = request.styleId;
317
+ style.textContent = "*, *::before, *::after { transition: none !important; }";
318
+ document.head.append(style);
319
+ const focusable = 'a[href], area[href], button, input:not([type="hidden"]), select, textarea, summary, iframe, [tabindex], [contenteditable]:not([contenteditable="false"])';
320
+ const found = scopes.flatMap((scope) => [scope, ...scope.querySelectorAll(focusable)]).filter((el, i, all) => all.indexOf(el) === i && el.matches(focusable) && inScope(el)).filter((el) => el.tabIndex >= 0 && !el.matches(":disabled") && el.checkVisibility({ visibilityProperty: true }));
321
+ found.forEach((el, i) => el.setAttribute(request.attr, String(i)));
322
+ return found.length;
323
+ }
324
+ case "snapshot": {
325
+ const target = document.querySelector(`[${request.attr}="${request.index}"]`);
326
+ return candidates(target).map(signature);
327
+ }
328
+ case "measure": {
329
+ const target = document.querySelector(`[${request.attr}="${request.index}"]`);
330
+ const list = candidates(target);
331
+ for (const [i, el] of list.entries()) {
332
+ if (request.before[i] === void 0 || request.before[i] === signature(el)) continue;
333
+ const found = indicatorOf(el, request.before[i]);
334
+ if (!found) continue;
335
+ if (found.channel === "browser") return { kind: "browser" };
336
+ const ring = inflate(el.getBoundingClientRect(), Math.max(found.extent, 0));
337
+ const behind = backdrop(found.channel === "border" ? el.parentElement : el, found.extent < 0 ? el.getBoundingClientRect() : ring);
338
+ const offset = found.channel === "border" ? 1 : 0;
339
+ const common = {
340
+ channel: found.channel,
341
+ target: selectorFor(target),
342
+ indicator: selectorFor(el),
343
+ html: html(target),
344
+ ancestorsFrom: behind.first + offset
345
+ };
346
+ if (!behind.color) return { kind: "review", ...common };
347
+ const ratio = contrast(paint([rgb(behind.color), found.color]), behind.color);
348
+ return ratio < 3 ? { kind: "fail", ratio, ...common } : { kind: "pass" };
349
+ }
350
+ return { kind: "none" };
351
+ }
352
+ case "cleanup": {
353
+ document.getElementById(request.styleId)?.remove();
354
+ for (const el of document.querySelectorAll(`[${request.attr}]`)) el.removeAttribute(request.attr);
355
+ return null;
356
+ }
357
+ case "boundaries": {
358
+ const outcomes = [];
359
+ const control = "button, input, select, textarea, [aria-disabled]";
360
+ const inactive = (el) => {
361
+ const root = el.closest(request.root) ?? el;
362
+ const controls = [root, ...root.querySelectorAll(control)].filter((c) => c.matches(control));
363
+ return controls.length > 0 && controls.every((c) => c.matches(":disabled") || c.getAttribute("aria-disabled") === "true");
364
+ };
365
+ for (const part of request.parts) {
366
+ const elements = scopes.flatMap((scope) => [scope, ...scope.querySelectorAll(part.selector)]).filter((el, i, all) => all.indexOf(el) === i && el.matches(part.selector) && inScope(el)).filter((el) => !part.when || el.matches(part.when)).filter((el) => el.checkVisibility({ visibilityProperty: true }) && el.getBoundingClientRect().width > 0 && !inactive(el));
367
+ for (const el of elements) {
368
+ const style = getComputedStyle(el);
369
+ const behind = backdrop(el.parentElement, el.getBoundingClientRect());
370
+ const common = { label: part.label, target: selectorFor(el), html: html(el), ancestorsFrom: behind.first + 1 };
371
+ if (!behind.color) {
372
+ outcomes.push({ kind: "review", edge: null, surface: null, ...common });
373
+ continue;
374
+ }
375
+ const surfaceColor = paint([rgb(behind.color), style.backgroundColor]);
376
+ const surface = style.backgroundImage === "none" ? contrast(surfaceColor, behind.color) : null;
377
+ const edges = [];
378
+ const borderUnder = style.backgroundClip === "border-box" ? surfaceColor : behind.color;
379
+ for (const side of SIDES) {
380
+ const color = visibleSide(style, side);
381
+ if (color) edges.push({ channel: "border", color, under: borderUnder });
382
+ }
383
+ if (!["none", "auto"].includes(style.outlineStyle) && px(style.outlineWidth) >= 0.99) {
384
+ edges.push({ channel: "outline", color: style.outlineColor, under: px(style.outlineOffset) < 0 ? surfaceColor : behind.color });
385
+ }
386
+ const edge = edges.map(({ channel, color, under }) => ({ channel, ratio: contrast(paint([rgb(under), color]), behind.color) })).reduce((best, next) => best && best.ratio >= next.ratio ? best : next, null);
387
+ if ((edge?.ratio ?? 0) >= 3 || (surface ?? 0) >= 3) continue;
388
+ outcomes.push({ kind: surface === null ? "review" : "fail", edge, surface, ...common });
389
+ }
390
+ }
391
+ return outcomes;
392
+ }
393
+ case "describe": {
394
+ return request.values.map((value) => {
395
+ const el = document.querySelector(`[${request.attr}="${value}"]`);
396
+ return { target: selectorFor(el), html: html(el) };
397
+ });
398
+ }
399
+ }
400
+ }
401
+ function run(page, request) {
402
+ return page.evaluate(inPage, request);
403
+ }
404
+ function describeTagged(page, scope, exclude, attr, values) {
405
+ return run(page, { kind: "describe", scope, exclude, attr, values });
406
+ }
407
+ function ratioText(ratio) {
408
+ return `${Math.floor(ratio * 100) / 100}:1`;
409
+ }
410
+ async function scanFocusIndicators(page, scope, exclude) {
411
+ const cdp = await cdpSession(page);
412
+ const count = await run(page, { kind: "focusables", scope, exclude, attr: FOCUS_ATTR, styleId: NO_TRANSITION_ID });
413
+ const checked = [];
414
+ try {
415
+ const { root } = await cdp.send("DOM.getDocument", { depth: 0 });
416
+ for (let index = 0; index < count; index++) {
417
+ const { nodeId } = await cdp.send("DOM.querySelector", { nodeId: root.nodeId, selector: `[${FOCUS_ATTR}="${index}"]` });
418
+ const before = await run(page, { kind: "snapshot", attr: FOCUS_ATTR, index, scope, exclude });
419
+ await cdp.send("CSS.forcePseudoState", { nodeId, forcedPseudoClasses: FORCE });
420
+ let outcome;
421
+ try {
422
+ outcome = await run(page, { kind: "measure", attr: FOCUS_ATTR, index, scope, exclude, before });
423
+ } finally {
424
+ await cdp.send("CSS.forcePseudoState", { nodeId, forcedPseudoClasses: [] });
425
+ }
426
+ if (outcome.kind !== "fail" && outcome.kind !== "review") continue;
427
+ const where = outcome.indicator === outcome.target ? "" : ` on ${outcome.indicator}`;
428
+ checked.push({
429
+ result: {
430
+ rule: outcome.kind === "fail" ? "a11y-component" : "a11y-review",
431
+ engineRule: FOCUS_INDICATOR_RULE,
432
+ impact: null,
433
+ target: outcome.target,
434
+ html: outcome.html,
435
+ help: outcome.kind === "fail" ? `Focus ${outcome.channel}${where} ${ratioText(outcome.ratio)} against the backdrop, needs 3:1` : `Focus ${outcome.channel}${where} sits over a gradient or image. Check by eye that it contrasts 3:1 with the backdrop`,
436
+ helpUrl: NON_TEXT_URL
437
+ },
438
+ trace: { selector: outcome.indicator, forceOn: outcome.target, ancestorsFrom: outcome.ancestorsFrom }
439
+ });
440
+ }
441
+ } finally {
442
+ await run(page, { kind: "cleanup", attr: FOCUS_ATTR, styleId: NO_TRANSITION_ID });
443
+ }
444
+ return checked;
445
+ }
446
+ async function scanBoundaries(page, scope, exclude, parts, root) {
447
+ if (parts.length === 0) return [];
448
+ const outcomes = await run(page, { kind: "boundaries", scope, exclude, parts, root });
449
+ return outcomes.map((outcome) => {
450
+ const edge = outcome.edge === null ? "no border" : `${outcome.edge.channel} ${ratioText(outcome.edge.ratio)}`;
451
+ return {
452
+ result: {
453
+ rule: outcome.kind === "fail" ? "a11y-component" : "a11y-review",
454
+ engineRule: BOUNDARY_RULE,
455
+ impact: null,
456
+ target: outcome.target,
457
+ html: outcome.html,
458
+ help: outcome.kind === "fail" ? `${outcome.label} ${edge} and surface ${ratioText(outcome.surface)} against the backdrop, needs 3:1` : `${outcome.label} sits on or carries a gradient or image. Check by eye that its edge contrasts 3:1 with the backdrop`,
459
+ helpUrl: NON_TEXT_URL
460
+ },
461
+ trace: { selector: outcome.target, ancestorsFrom: outcome.ancestorsFrom }
462
+ };
463
+ });
464
+ }
465
+
466
+ // src/testing/support/textSpacingAndMotion.ts
467
+ var TEXT_SPACING_URL = "https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html";
468
+ var MOTION_MS = 200;
469
+ var MOTION_URL = "https://www.w3.org/WAI/WCAG22/Techniques/css/C39";
470
+ var TEXT_SPACING_RULE = "text-spacing";
471
+ var REDUCED_MOTION_RULE = "prefers-reduced-motion";
472
+ var SPACING_ATTR = "data-lt-spacing-probe";
473
+ var MOTION_ATTR = "data-lt-motion-probe";
474
+ var STILL_ID = "lt-spacing-probe-still";
475
+ var TOLERANCE = 1;
476
+ function textSpacingCss(scope) {
477
+ const every = scope ? `${scope}, ${scope} *` : "*";
478
+ const paragraphs = scope ? `${scope} p` : "p";
479
+ return `
480
+ ${every} { line-height: 1.5 !important; letter-spacing: 0.12em !important; word-spacing: 0.16em !important; }
481
+ ${paragraphs} { margin-bottom: 2em !important; }
482
+ `;
483
+ }
484
+ async function newUnderTextSpacing(page, observe, key, scope) {
485
+ const before = await observe();
486
+ const style = await page.addStyleTag({ content: textSpacingCss(scope) });
487
+ let after;
488
+ try {
489
+ after = await observe();
490
+ } finally {
491
+ await style.evaluate((node) => node.remove());
492
+ }
493
+ const known = /* @__PURE__ */ new Map();
494
+ for (const failure of before) known.set(key(failure), (known.get(key(failure)) ?? 0) + 1);
495
+ return after.filter((failure) => {
496
+ const left = known.get(key(failure)) ?? 0;
497
+ if (left > 0) known.set(key(failure), left - 1);
498
+ return left === 0;
499
+ });
500
+ }
501
+ function tagInScope(page, scope, exclude, own, attr) {
502
+ return page.evaluate(({ scope: scope2, exclude: exclude2, own: own2, attr: attr2 }) => {
503
+ const found = [...document.querySelectorAll(scope2)].flatMap((root) => [root, ...root.querySelectorAll("*")]).filter((el, i, all) => all.indexOf(el) === i && el.closest(own2) && !exclude2.some((selector) => el.closest(selector)));
504
+ found.forEach((el, i) => el.setAttribute(attr2, String(i)));
505
+ return found.length;
506
+ }, { scope, exclude, own: own.join(", "), attr });
507
+ }
508
+ function untag(page, attr, styleId) {
509
+ return page.evaluate(({ attr: attr2, styleId: styleId2 }) => {
510
+ if (styleId2) document.getElementById(styleId2)?.remove();
511
+ for (const el of document.querySelectorAll(`[${attr2}]`)) el.removeAttribute(attr2);
512
+ }, { attr, styleId });
513
+ }
514
+ function observeOverflow(page) {
515
+ return page.evaluate(({ attr, tolerance }) => [...document.querySelectorAll(`[${attr}]`)].flatMap((el) => {
516
+ const style = getComputedStyle(el);
517
+ if (style.display === "none" || style.display === "contents") return [];
518
+ if (el.clientWidth === 0 && el.clientHeight === 0) return [];
519
+ if (!el.textContent?.trim()) return [];
520
+ const index = el.getAttribute(attr);
521
+ const tag = el.tagName.toLowerCase();
522
+ const found = [];
523
+ const axes = [
524
+ ["x", el.scrollWidth, el.clientWidth, style.overflowX],
525
+ ["y", el.scrollHeight, el.clientHeight, style.overflowY]
526
+ ];
527
+ for (const [axis, content, box, overflow] of axes) {
528
+ if (overflow === "auto" || overflow === "scroll" || content <= box + tolerance) continue;
529
+ found.push({ index, axis, tag, content, box, overflow });
530
+ }
531
+ return found;
532
+ }), { attr: SPACING_ATTR, tolerance: TOLERANCE });
533
+ }
534
+ function groupBy(items, key) {
535
+ const groups = /* @__PURE__ */ new Map();
536
+ for (const item of items) groups.set(key(item), [...groups.get(key(item)) ?? [], item]);
537
+ return groups;
538
+ }
539
+ async function scanTextSpacing(page, scope, exclude, own) {
540
+ await page.evaluate(({ id, scope: scope2 }) => {
541
+ const style = document.createElement("style");
542
+ style.id = id;
543
+ style.textContent = `${scope2} *, ${scope2} *::before, ${scope2} *::after { transition: none !important; }`;
544
+ document.head.append(style);
545
+ }, { id: STILL_ID, scope });
546
+ try {
547
+ if (await tagInScope(page, scope, exclude, own, SPACING_ATTR) === 0) return [];
548
+ const found = await newUnderTextSpacing(page, () => observeOverflow(page), (o) => `${o.index}|${o.axis}`, scope);
549
+ if (found.length === 0) return [];
550
+ const byElement = groupBy(found, (o) => o.index);
551
+ const described = await describeTagged(page, scope, exclude, SPACING_ATTR, [...byElement.keys()]);
552
+ return [...byElement.values()].map((overflows, i) => ({
553
+ rule: "a11y-text-spacing",
554
+ engineRule: TEXT_SPACING_RULE,
555
+ impact: null,
556
+ target: described[i].target,
557
+ html: described[i].html,
558
+ help: `Under the text spacing override, ${overflows.map((o) => `<${o.tag}> holds ${o.content}px of content in ${o.box}px of ${o.axis === "x" ? "width" : "height"}, and its overflow-${o.axis} is ${o.overflow}`).join("; ")}`,
559
+ helpUrl: TEXT_SPACING_URL
560
+ }));
561
+ } finally {
562
+ await untag(page, SPACING_ATTR, STILL_ID);
563
+ }
564
+ }
565
+ function observeMotion(page) {
566
+ return page.evaluate(({ attr, limit }) => {
567
+ const ms = (time) => parseFloat(time) * (time.trim().endsWith("ms") ? 1 : 1e3) || 0;
568
+ const list = (value) => value.split(/,(?![^(]*\))/).map((part) => part.trim());
569
+ const at = (values, i) => values[i % values.length];
570
+ const found = [];
571
+ const tagged = [...document.querySelectorAll(`[${attr}]`)].filter((el) => el.checkVisibility());
572
+ for (const el of tagged) {
573
+ const index = el.getAttribute(attr);
574
+ for (const pseudo of ["", "::before", "::after"]) {
575
+ const style = getComputedStyle(el, pseudo || null);
576
+ if (pseudo && (style.content === "none" || style.content === "normal" || style.display === "none")) continue;
577
+ const durations = list(style.animationDuration);
578
+ const counts = list(style.animationIterationCount);
579
+ const delays = list(style.animationDelay);
580
+ list(style.animationName).forEach((name, i) => {
581
+ if (name === "none") return;
582
+ const count = at(counts, i);
583
+ const end = ms(at(delays, i)) + (count === "infinite" ? Infinity : ms(at(durations, i)) * parseFloat(count));
584
+ if (end > limit) found.push({ index, pseudo, kind: "animation", name: name.replace(/^svelte-[a-z0-9]+-/, ""), end });
585
+ });
586
+ const transitionDurations = list(style.transitionDuration);
587
+ const transitionDelays = list(style.transitionDelay);
588
+ list(style.transitionProperty).forEach((name, i) => {
589
+ const end = ms(at(transitionDurations, i)) + ms(at(transitionDelays, i));
590
+ if (name !== "none" && end > limit) found.push({ index, pseudo, kind: "transition", name, end });
591
+ });
592
+ }
593
+ }
594
+ for (const animation of document.getAnimations()) {
595
+ if (animation instanceof CSSAnimation || animation instanceof CSSTransition || animation.playState !== "running") continue;
596
+ const effect = animation.effect;
597
+ if (!(effect instanceof KeyframeEffect) || !effect.target?.hasAttribute(attr)) continue;
598
+ const end = Number(effect.getComputedTiming().endTime ?? 0);
599
+ if (end > limit) {
600
+ found.push({ index: effect.target.getAttribute(attr), pseudo: effect.pseudoElement ?? "", kind: "script", name: animation.id, end });
601
+ }
602
+ }
603
+ return found;
604
+ }, { attr: MOTION_ATTR, limit: MOTION_MS });
605
+ }
606
+ var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
607
+ function describeMotion({ kind, name, end }) {
608
+ const what = kind === "animation" ? `the animation ${name}` : kind === "transition" ? `a transition of ${name}` : `a script animation${name ? ` ${name}` : ""}`;
609
+ return `${what} ${Number.isFinite(end) ? `lasts ${Math.round(end)} ms` : "runs without end"}`;
610
+ }
611
+ async function scanReducedMotion(page, scope, exclude, own) {
612
+ try {
613
+ if (await tagInScope(page, scope, exclude, own, MOTION_ATTR) === 0) return [];
614
+ if ((await observeMotion(page)).length === 0) return [];
615
+ let reduced;
616
+ await page.emulateMedia({ reducedMotion: "reduce" });
617
+ try {
618
+ await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
619
+ reduced = await observeMotion(page);
620
+ } finally {
621
+ await page.emulateMedia({ reducedMotion: null });
622
+ }
623
+ if (reduced.length === 0) return [];
624
+ const byTarget = groupBy(reduced, (m) => `${m.index}
625
+ ${m.pseudo}`);
626
+ const groups = [...byTarget.values()];
627
+ const described = await describeTagged(page, scope, exclude, MOTION_ATTR, groups.map((motions) => motions[0].index));
628
+ return groups.map((motions, i) => ({
629
+ result: {
630
+ rule: "a11y-reduced-motion",
631
+ engineRule: REDUCED_MOTION_RULE,
632
+ impact: null,
633
+ target: `${described[i].target}${motions[0].pseudo}`,
634
+ html: described[i].html,
635
+ help: capitalize(`${motions.map(describeMotion).join("; ")} under prefers-reduced-motion: reduce`),
636
+ helpUrl: MOTION_URL
637
+ },
638
+ trace: { selector: described[i].target }
639
+ }));
640
+ } finally {
641
+ await untag(page, MOTION_ATTR);
642
+ }
643
+ }
644
+
645
+ export {
646
+ TRACE_AXES,
647
+ traceProperties,
648
+ scanFocusIndicators,
649
+ scanBoundaries,
650
+ TEXT_SPACING_URL,
651
+ MOTION_MS,
652
+ MOTION_URL,
653
+ TEXT_SPACING_RULE,
654
+ newUnderTextSpacing,
655
+ scanTextSpacing,
656
+ scanReducedMotion
657
+ };
658
+ //# sourceMappingURL=chunk-SHLPDNAQ.js.map