@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.
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +5 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
- package/CHANGELOG.md +309 -0
- package/README.md +2 -2
- package/bin/check-component.mjs +8 -0
- package/bin/check-page.mjs +6 -0
- package/bin/cli.mjs +36 -6
- package/bin/contractRunner.mjs +142 -17
- package/bin/lib/findings.mjs +20 -4
- package/bin/rules/a11y.mjs +53 -0
- package/bin/set-colors.mjs +1 -0
- package/bin/setup-claude.mjs +2 -2
- package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
- package/dist-plugin/index.cjs +148 -3
- package/dist-plugin/index.js +250 -105
- package/dist-plugin/setColors/index.cjs +7 -1
- package/dist-plugin/setColors/index.d.cts +9 -4
- package/dist-plugin/setColors/index.d.ts +9 -4
- package/dist-plugin/setColors/index.js +8 -2
- package/dist-plugin/setGeometry/index.d.cts +1 -1
- package/dist-plugin/setGeometry/index.d.ts +1 -1
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
- package/package.json +49 -2
- package/src/demo/LabeledSelect.svelte +1 -1
- package/src/editor/bootstrap.ts +3 -1
- package/src/editor/component-editor/BadgeEditor.svelte +13 -0
- package/src/editor/component-editor/ButtonEditor.svelte +33 -2
- package/src/editor/component-editor/CalloutEditor.svelte +13 -0
- package/src/editor/component-editor/CardEditor.svelte +12 -2
- package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
- package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
- package/src/editor/component-editor/DialogEditor.svelte +15 -0
- package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
- package/src/editor/component-editor/ImageEditor.svelte +20 -9
- package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
- package/src/editor/component-editor/InputEditor.svelte +23 -0
- package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
- package/src/editor/component-editor/NotificationEditor.svelte +15 -0
- package/src/editor/component-editor/PanelEditor.svelte +1 -1
- package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
- package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
- package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
- package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
- package/src/editor/component-editor/SliderEditor.svelte +33 -0
- package/src/editor/component-editor/TabBarEditor.svelte +23 -2
- package/src/editor/component-editor/TableEditor.svelte +14 -0
- package/src/editor/component-editor/ToggleEditor.svelte +17 -0
- package/src/editor/component-editor/TooltipEditor.svelte +11 -2
- package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
- package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
- package/src/editor/component-editor/registration.ts +2 -1
- package/src/editor/component-editor/registry.ts +69 -27
- package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -10
- package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
- package/src/editor/core/a11y/a11yReport.ts +101 -0
- package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
- package/src/editor/core/a11y/contrastFailures.ts +22 -0
- package/src/editor/core/components/contrastPairRegistry.ts +18 -0
- package/src/editor/core/palettes/contrast.ts +44 -0
- package/src/editor/core/palettes/contrastPairs.ts +79 -0
- package/src/editor/core/palettes/resolveColor.ts +77 -0
- package/src/editor/core/store/editorPersistence.ts +9 -1
- package/src/editor/core/store/editorRenderer.ts +2 -0
- package/src/editor/core/store/editorStore.ts +29 -0
- package/src/editor/core/store/editorTypes.ts +9 -0
- package/src/editor/core/store/editorViewStore.ts +2 -2
- package/src/editor/core/store/unsavedEdits.ts +10 -0
- package/src/editor/core/themes/buildColors.ts +16 -5
- package/src/editor/core/themes/liveStateStream.ts +7 -1
- package/src/editor/core/themes/parsers/gradient.ts +5 -2
- package/src/editor/core/themes/slices/customColors.ts +226 -0
- package/src/editor/core/themes/themeTypes.ts +10 -0
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/accessibility.md +362 -0
- package/src/editor/docs/content/editing-tokens.md +56 -1
- package/src/editor/docs/content.generated.ts +2 -1
- package/src/editor/pages/ComponentEditorPage.svelte +10 -1
- package/src/editor/pages/EditorShell.svelte +97 -10
- package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
- package/src/editor/skill-atlas/trees/create-component.ts +12 -12
- package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
- package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
- package/src/editor/ui/GradientEditor.svelte +9 -1
- package/src/editor/ui/ThemePanel.svelte +4 -4
- package/src/editor/ui/UIContrastReadout.svelte +186 -0
- package/src/editor/ui/UIInfoPopover.svelte +16 -2
- package/src/editor/ui/UIPaletteSelector.svelte +347 -15
- package/src/editor/ui/UITokenSelector.svelte +33 -14
- package/src/editor/ui/VariablesTab.svelte +3 -0
- package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
- package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
- package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
- package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
- package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
- package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
- package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
- package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
- package/src/editor/ui/a11y/a11yView.ts +218 -0
- package/src/editor/ui/anchorToTrigger.ts +63 -0
- package/src/editor/ui/contrastReadout.ts +45 -0
- package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
- package/src/live-tokens/data/themes/autumn.json +20 -20
- package/src/live-tokens/data/themes/halloween.json +20 -20
- package/src/live-tokens/data/themes/midnight-study.json +20 -20
- package/src/live-tokens/data/themes/ocean.json +20 -20
- package/src/live-tokens/data/themes/royal-velvet.json +20 -20
- package/src/live-tokens/data/themes/sketchy.json +20 -20
- package/src/live-tokens/data/themes/spring-meadow.json +20 -20
- package/src/live-tokens/data/themes/sunset.json +20 -20
- package/src/system/components/Badge.svelte +3 -3
- package/src/system/components/Button.svelte +9 -3
- package/src/system/components/CodeSnippet.svelte +8 -1
- package/src/system/components/CornerBadge.svelte +3 -3
- package/src/system/components/Dialog.svelte +43 -14
- package/src/system/components/IconButton.svelte +1 -1
- package/src/system/components/Image.svelte +7 -0
- package/src/system/components/ImageLightbox.svelte +26 -26
- package/src/system/components/Input.svelte +8 -1
- package/src/system/components/MenuSelect.svelte +30 -2
- package/src/system/components/Notification.svelte +1 -1
- package/src/system/components/ProgressBar.svelte +6 -0
- package/src/system/components/RadioButton.svelte +36 -3
- package/src/system/components/SegmentedControl.svelte +36 -2
- package/src/system/components/SlidePager.svelte +2 -2
- package/src/system/components/Slider.svelte +13 -9
- package/src/system/components/TabBar.svelte +33 -3
- package/src/system/components/Toggle.svelte +2 -2
- package/src/system/components/VideoFrame.svelte +1 -1
- package/src/system/components/VideoLightbox.svelte +2 -2
- package/src/system/internal/focusTrap.ts +35 -0
- package/src/testing-js/__aria__/badge.yml +40 -0
- package/src/testing-js/__aria__/button.yml +88 -0
- package/src/testing-js/__aria__/callout.yml +12 -0
- package/src/testing-js/__aria__/card.yml +8 -0
- package/src/testing-js/__aria__/codesnippet.yml +21 -0
- package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
- package/src/testing-js/__aria__/cornerbadge.yml +24 -0
- package/src/testing-js/__aria__/dialog.yml +42 -0
- package/src/testing-js/__aria__/iconbutton.yml +57 -0
- package/src/testing-js/__aria__/image.yml +10 -0
- package/src/testing-js/__aria__/imagelightbox.yml +50 -0
- package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
- package/src/testing-js/__aria__/input.yml +61 -0
- package/src/testing-js/__aria__/menuselect.yml +30 -0
- package/src/testing-js/__aria__/notification.yml +30 -0
- package/src/testing-js/__aria__/panel.yml +2 -0
- package/src/testing-js/__aria__/progressbar.yml +2 -0
- package/src/testing-js/__aria__/radiobutton.yml +20 -0
- package/src/testing-js/__aria__/sectiondivider.yml +9 -0
- package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
- package/src/testing-js/__aria__/sidenavigation.yml +304 -0
- package/src/testing-js/__aria__/slidepager.yml +72 -0
- package/src/testing-js/__aria__/slider.yml +28 -0
- package/src/testing-js/__aria__/tabbar.yml +35 -0
- package/src/testing-js/__aria__/table.yml +144 -0
- package/src/testing-js/__aria__/toggle.yml +12 -0
- package/src/testing-js/__aria__/tooltip.yml +2 -0
- package/src/testing-js/__aria__/videoframe.yml +6 -0
- package/src/testing-js/__aria__/videolightbox.yml +12 -0
- package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
- package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
- package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
- package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
- package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
- package/src/testing-js/chunk-EWSGE27V.js +168 -0
- package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
- package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
- package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
- package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
- package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
- package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
- package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
- package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
- package/src/testing-js/component-a11y.contract.js +283 -0
- package/src/testing-js/component-a11y.contract.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-behavior.contract.js +2 -2
- package/src/testing-js/component-editor.contract.js +6 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +10 -6
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +44 -16
- package/src/testing-js/index.js +9 -9
- package/src/testing-js/page-compliance.contract.js +163 -18
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/testing-js/vitest.js +3 -3
- package/template/package.json +9 -0
- package/src/editor/ui/keepInViewport.ts +0 -84
- package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
- package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
- package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
- package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
- /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
- /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
|
@@ -2,6 +2,8 @@ import { API_BASE } from '../storage/apiBase';
|
|
|
2
2
|
import { refreshProductionTheme } from '../productionPulse';
|
|
3
3
|
import { hydrateAppliedTheme } from './themeDocumentSync';
|
|
4
4
|
import type { LiveState } from './themeService';
|
|
5
|
+
import { receiveA11yReport } from '../a11y/a11yReport';
|
|
6
|
+
import { A11Y_EVENT, type A11yReport } from '../a11y/a11yReportTypes';
|
|
5
7
|
|
|
6
8
|
export const LIVE_STATE_EVENT = 'live-state';
|
|
7
9
|
|
|
@@ -12,7 +14,8 @@ let source: EventSource | null = null;
|
|
|
12
14
|
* from outside the page (a CLI verb, a branch switch) arrives as one frame,
|
|
13
15
|
* and this document hydrates from it the way it does from an Apply. An outside
|
|
14
16
|
* writer can also save the production theme past its bake (`save-theme` into
|
|
15
|
-
* the open, published theme), so production is re-read too.
|
|
17
|
+
* the open, published theme), so production is re-read too. The same stream
|
|
18
|
+
* carries each accessibility run's report on its start and on its end.
|
|
16
19
|
*/
|
|
17
20
|
export function init(): void {
|
|
18
21
|
if (source || typeof EventSource === 'undefined') return;
|
|
@@ -22,6 +25,9 @@ export function init(): void {
|
|
|
22
25
|
hydrateAppliedTheme(state.fileName, state);
|
|
23
26
|
void refreshProductionTheme();
|
|
24
27
|
});
|
|
28
|
+
source.addEventListener(A11Y_EVENT, (event) => {
|
|
29
|
+
receiveA11yReport(JSON.parse((event as MessageEvent<string>).data) as A11yReport);
|
|
30
|
+
});
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export function __resetForTests(): void {
|
|
@@ -181,7 +181,10 @@ const VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
|
|
|
181
181
|
|
|
182
182
|
/** Split on top-level separators only. Stop colours nest commas inside
|
|
183
183
|
* `color-mix(…)`, so a plain `split(',')` shreds them. */
|
|
184
|
-
function splitTopLevel(
|
|
184
|
+
export function splitTopLevel(
|
|
185
|
+
s: string,
|
|
186
|
+
isSeparator: (c: string) => boolean = (c) => c === ',',
|
|
187
|
+
): string[] {
|
|
185
188
|
const parts: string[] = [];
|
|
186
189
|
let depth = 0;
|
|
187
190
|
let start = 0;
|
|
@@ -189,7 +192,7 @@ function splitTopLevel(s: string): string[] {
|
|
|
189
192
|
const c = s[i];
|
|
190
193
|
if (c === '(') depth++;
|
|
191
194
|
else if (c === ')') depth--;
|
|
192
|
-
else if (
|
|
195
|
+
else if (depth === 0 && isSeparator(c)) {
|
|
193
196
|
parts.push(s.slice(start, i));
|
|
194
197
|
start = i + 1;
|
|
195
198
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { CssVarRef, CustomColorToken, EditorState } from '../../store/editorTypes';
|
|
2
|
+
import type { CustomColorDiskToken } from '../themeTypes';
|
|
3
|
+
import type { GradientStop } from '../parsers/gradient';
|
|
4
|
+
import { get } from 'svelte/store';
|
|
5
|
+
import { mutate, store } from '../../store/editorCore';
|
|
6
|
+
import { oklchToCss, type Oklch } from '../../palettes/oklch';
|
|
7
|
+
import { PALETTE_SPECS, PALETTE_STEPS, palettesToValues } from '../../palettes/paletteDerivation';
|
|
8
|
+
import { WASH_FAMILIES, type WashFamily } from './washes';
|
|
9
|
+
|
|
10
|
+
const CUSTOM_COLOR_SLOT_RE = /^--color-custom-\d+$/;
|
|
11
|
+
|
|
12
|
+
export function isCustomColorSlot(variable: string): boolean {
|
|
13
|
+
return CUSTOM_COLOR_SLOT_RE.test(variable);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isCustomColorToken(t: unknown): t is CustomColorToken {
|
|
17
|
+
if (!t || typeof t !== 'object') return false;
|
|
18
|
+
const { variable, color } = t as Partial<CustomColorToken>;
|
|
19
|
+
return typeof variable === 'string' && isCustomColorSlot(variable)
|
|
20
|
+
&& !!color && typeof color === 'object'
|
|
21
|
+
&& [color.l, color.c, color.h].every((n) => typeof n === 'number' && Number.isFinite(n));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isCustomColorList(tokens: unknown): tokens is CustomColorToken[] {
|
|
25
|
+
return Array.isArray(tokens) && tokens.every(isCustomColorToken);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The lowest free slot, so a removed number is reused before the list grows. */
|
|
29
|
+
export function nextCustomColorSlot(tokens: readonly CustomColorToken[]): string {
|
|
30
|
+
const used = new Set(tokens.map((t) => t.variable));
|
|
31
|
+
for (let n = 1; ; n++) {
|
|
32
|
+
const name = `--color-custom-${n}`;
|
|
33
|
+
if (!used.has(name)) return name;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function customColorsToVars(c: EditorState['customColors']): Record<string, string> {
|
|
38
|
+
const out: Record<string, string> = {};
|
|
39
|
+
for (const t of c.tokens) out[t.variable] = oklchToCss(t.color.l, t.color.c, t.color.h);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The rendered `--color-custom-N` strings in `cssVariables` are a projection
|
|
44
|
+
* for production CSS; the structured field is the basis, so the loader sheds
|
|
45
|
+
* them from the bag. A file without the field keeps its bag as it is. */
|
|
46
|
+
export function loadCustomColorsFromFile(
|
|
47
|
+
next: EditorState,
|
|
48
|
+
customColors: CustomColorDiskToken[] | undefined,
|
|
49
|
+
rawVars: Record<string, string>,
|
|
50
|
+
): void {
|
|
51
|
+
for (const t of customColors ?? []) delete rawVars[t.variable];
|
|
52
|
+
if (isCustomColorList(customColors)) {
|
|
53
|
+
next.customColors.tokens = structuredClone(customColors);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findCustomColor(s: EditorState, variable: string): CustomColorToken | undefined {
|
|
58
|
+
return s.customColors.tokens.find((t) => t.variable === variable);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function addCustomColor(color: Oklch): string {
|
|
62
|
+
let variable = '';
|
|
63
|
+
mutate('add custom colour', (s) => {
|
|
64
|
+
variable = nextCustomColorSlot(s.customColors.tokens);
|
|
65
|
+
s.customColors.tokens.push({ variable, color: { ...color } });
|
|
66
|
+
});
|
|
67
|
+
return variable;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function setCustomColor(variable: string, color: Oklch): void {
|
|
71
|
+
mutate(`set custom colour ${variable}`, (s) => {
|
|
72
|
+
const t = findCustomColor(s, variable);
|
|
73
|
+
if (t) t.color = { ...color };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One slot that names a custom colour. An alias counts once however many of
|
|
78
|
+
* its inline gradient stops name the colour; so does a library gradient. */
|
|
79
|
+
export type CustomColorUsage =
|
|
80
|
+
| { kind: 'alias'; component: string; property: string }
|
|
81
|
+
| { kind: 'gradient'; gradient: string }
|
|
82
|
+
| { kind: 'wash'; family: WashFamily };
|
|
83
|
+
|
|
84
|
+
function escapeRegExp(s: string): string {
|
|
85
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `var(--color-custom-1)` inside a literal, never `var(--color-custom-10)`. */
|
|
89
|
+
function varRefPattern(variable: string): RegExp {
|
|
90
|
+
return new RegExp(`var\\(${escapeRegExp(variable)}\\)`, 'g');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stopsName(stops: readonly GradientStop[], variable: string): boolean {
|
|
94
|
+
return stops.some((st) => st.color === variable);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A literal names a custom colour when the picker materialized a library
|
|
98
|
+
* gradient with a per-slot angle: the stop list is inlined as `var()` refs. */
|
|
99
|
+
function refNames(ref: CssVarRef, variable: string): boolean {
|
|
100
|
+
switch (ref.kind) {
|
|
101
|
+
case 'token':
|
|
102
|
+
return ref.name === variable;
|
|
103
|
+
case 'literal':
|
|
104
|
+
return varRefPattern(variable).test(ref.value);
|
|
105
|
+
case 'gradient':
|
|
106
|
+
return stopsName(ref.value.stops, variable);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function customColorUsages(state: EditorState, variable: string): CustomColorUsage[] {
|
|
111
|
+
const out: CustomColorUsage[] = [];
|
|
112
|
+
for (const [component, slice] of Object.entries(state.components)) {
|
|
113
|
+
for (const [property, ref] of Object.entries(slice.aliases)) {
|
|
114
|
+
if (refNames(ref, variable)) out.push({ kind: 'alias', component, property });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const g of state.gradients.tokens) {
|
|
118
|
+
if (stopsName(g.stops, variable)) out.push({ kind: 'gradient', gradient: g.variable });
|
|
119
|
+
}
|
|
120
|
+
for (const family of WASH_FAMILIES) {
|
|
121
|
+
if (state.washes[family].color === variable) out.push({ kind: 'wash', family });
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function joinAnd(items: readonly string[]): string {
|
|
127
|
+
if (items.length <= 1) return items[0] ?? '';
|
|
128
|
+
return `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The replace step's usage line: "Used by 4 properties in Button and Card." */
|
|
132
|
+
export function describeCustomColorUsages(
|
|
133
|
+
usages: readonly CustomColorUsage[],
|
|
134
|
+
componentLabel: (component: string) => string,
|
|
135
|
+
): string {
|
|
136
|
+
const aliases = usages.filter((u) => u.kind === 'alias');
|
|
137
|
+
const parts: string[] = [];
|
|
138
|
+
if (aliases.length > 0) {
|
|
139
|
+
const components = [...new Set(aliases.map((u) => u.component))].map(componentLabel);
|
|
140
|
+
const noun = aliases.length === 1 ? 'property' : 'properties';
|
|
141
|
+
parts.push(`${aliases.length} ${noun} in ${joinAnd(components)}`);
|
|
142
|
+
}
|
|
143
|
+
for (const u of usages) {
|
|
144
|
+
if (u.kind === 'gradient') parts.push(`Gradient ${u.gradient.replace(/^--gradient-/, '')}`);
|
|
145
|
+
else if (u.kind === 'wash') parts.push(`the ${u.family} colour`);
|
|
146
|
+
}
|
|
147
|
+
return `Used by ${joinAnd(parts)}.`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const WHITE: Oklch = { l: 1, c: 0, h: 0 };
|
|
151
|
+
const BLACK: Oklch = { l: 0, c: 0, h: 0 };
|
|
152
|
+
|
|
153
|
+
function oklabDistance(a: Oklch, b: Oklch): number {
|
|
154
|
+
const ar = (a.h * Math.PI) / 180;
|
|
155
|
+
const br = (b.h * Math.PI) / 180;
|
|
156
|
+
const dl = a.l - b.l;
|
|
157
|
+
const da = a.c * Math.cos(ar) - b.c * Math.cos(br);
|
|
158
|
+
const db = a.c * Math.sin(ar) - b.c * Math.sin(br);
|
|
159
|
+
return Math.hypot(dl, da, db);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The token a replace step preselects: nearest by OKLab distance among the
|
|
163
|
+
* palette steps, White, Black and the custom colours other than `exclude`. */
|
|
164
|
+
export function nearestColorToken(color: Oklch, state: EditorState, exclude?: string): string {
|
|
165
|
+
const candidates: [string, Oklch][] = [];
|
|
166
|
+
const values = palettesToValues(state.palettes);
|
|
167
|
+
for (const spec of PALETTE_SPECS) {
|
|
168
|
+
for (const step of PALETTE_STEPS) {
|
|
169
|
+
const name = `--color-${spec.cssNamespace}-${step.label}`;
|
|
170
|
+
const v = values[name];
|
|
171
|
+
if (v?.kind === 'color') candidates.push([name, v]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
candidates.push(['--color-white', WHITE], ['--color-black', BLACK]);
|
|
175
|
+
for (const t of state.customColors.tokens) {
|
|
176
|
+
if (t.variable !== exclude) candidates.push([t.variable, t.color]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let best = candidates[0][0];
|
|
180
|
+
let bestDistance = Infinity;
|
|
181
|
+
for (const [name, value] of candidates) {
|
|
182
|
+
const d = oklabDistance(color, value);
|
|
183
|
+
if (d < bestDistance) {
|
|
184
|
+
best = name;
|
|
185
|
+
bestDistance = d;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return best;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Removes a custom colour that no slot names. Returns false, changing
|
|
192
|
+
* nothing, while any slot still names it; `replaceAndRemoveCustomColor`
|
|
193
|
+
* serves that case. */
|
|
194
|
+
export function removeCustomColor(variable: string): boolean {
|
|
195
|
+
const current = get(store);
|
|
196
|
+
if (!findCustomColor(current, variable) || customColorUsages(current, variable).length > 0) return false;
|
|
197
|
+
mutate(`remove custom colour ${variable}`, (s) => {
|
|
198
|
+
s.customColors.tokens = s.customColors.tokens.filter((t) => t.variable !== variable);
|
|
199
|
+
});
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function replaceInStops(stops: GradientStop[], variable: string, replacement: string): void {
|
|
204
|
+
for (const st of stops) if (st.color === variable) st.color = replacement;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Repoints every slot that names `variable` at `replacement`, keeping each
|
|
208
|
+
* slot's opacity, then removes the custom colour. One undo step. */
|
|
209
|
+
export function replaceAndRemoveCustomColor(variable: string, replacement: string): void {
|
|
210
|
+
if (replacement === variable || !findCustomColor(get(store), variable)) return;
|
|
211
|
+
const pattern = varRefPattern(variable);
|
|
212
|
+
mutate(`replace and remove custom colour ${variable}`, (s) => {
|
|
213
|
+
for (const slice of Object.values(s.components)) {
|
|
214
|
+
for (const ref of Object.values(slice.aliases)) {
|
|
215
|
+
if (ref.kind === 'token' && ref.name === variable) ref.name = replacement;
|
|
216
|
+
else if (ref.kind === 'literal') ref.value = ref.value.replace(pattern, `var(${replacement})`);
|
|
217
|
+
else if (ref.kind === 'gradient') replaceInStops(ref.value.stops, variable, replacement);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const g of s.gradients.tokens) replaceInStops(g.stops, variable, replacement);
|
|
221
|
+
for (const family of WASH_FAMILIES) {
|
|
222
|
+
if (s.washes[family].color === variable) s.washes[family].color = replacement;
|
|
223
|
+
}
|
|
224
|
+
s.customColors.tokens = s.customColors.tokens.filter((t) => t.variable !== variable);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -148,6 +148,13 @@ export interface GradientDiskToken {
|
|
|
148
148
|
stops: { position: number; color: string; opacity?: number }[];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/** On-disk shape of one `--color-custom-N` token: a theme-owned colour that
|
|
152
|
+
* holds its own OKLCH value, one value for light and dark alike. */
|
|
153
|
+
export interface CustomColorDiskToken {
|
|
154
|
+
variable: string;
|
|
155
|
+
color: Oklch;
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
/**
|
|
152
159
|
* Where a live component config resolved from: the unsaved `_working`
|
|
153
160
|
* buffer, or the open theme's embedded copy. Every theme carries every
|
|
@@ -177,6 +184,9 @@ export interface ColorsAndType {
|
|
|
177
184
|
/** Absent on files saved before gradients round-tripped; the loader keeps
|
|
178
185
|
* the stock defaults then, which match what those files rendered. */
|
|
179
186
|
gradients?: GradientDiskToken[];
|
|
187
|
+
/** Absent when the theme holds no custom colour; the loader reads that as
|
|
188
|
+
* an empty list. */
|
|
189
|
+
customColors?: CustomColorDiskToken[];
|
|
180
190
|
/** Four fixed harmony axes, each owning a hue with an optional bound family. */
|
|
181
191
|
harmonyAxes?: HarmonyAxis[];
|
|
182
192
|
/**
|
|
@@ -18,6 +18,7 @@ export const chapters: Chapter[] = [
|
|
|
18
18
|
{ id: 'where-themes-live', title: 'Where themes live' },
|
|
19
19
|
{ id: 'light-and-dark', title: 'Light and dark' },
|
|
20
20
|
{ id: 'creating-components', title: 'Creating components' },
|
|
21
|
+
{ id: 'accessibility', title: 'Accessibility' },
|
|
21
22
|
{ id: 'production-css', title: 'Production CSS' },
|
|
22
23
|
];
|
|
23
24
|
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
`check-component --tests` and `check-page --tests` test the shipped
|
|
4
|
+
components, your own components, and your pages against WCAG 2 A and AA. Every
|
|
5
|
+
accessibility finding reports at `warn`: the run prints it and leaves the exit
|
|
6
|
+
status to the other rules. `--strict` turns each one into an error that fails
|
|
7
|
+
the run, as it does every other warning.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx live-tokens check-component statcard --tests
|
|
11
|
+
npx live-tokens check-page src/pages/Home.svelte --tests --strict
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Suites and checks
|
|
15
|
+
|
|
16
|
+
**Components.** The component suite opens every variant and state the
|
|
17
|
+
component's contract names and scans the preview:
|
|
18
|
+
|
|
19
|
+
- **axe-core** checks names, roles, ARIA, contrast, target size, and
|
|
20
|
+
structure.
|
|
21
|
+
- **Siteimprove Alfa**, rule SIA-R65, checks that each focusable element
|
|
22
|
+
looks different when it takes focus.
|
|
23
|
+
- **Focus indicator contrast** measures each focus ring against the backdrop
|
|
24
|
+
around it, at 3:1 under WCAG 2.2 SC 1.4.11.
|
|
25
|
+
- **Control boundary contrast** measures each part the contract lists in
|
|
26
|
+
`boundaries` against the backdrop behind it, at 3:1 under WCAG 2.2
|
|
27
|
+
SC 1.4.11.
|
|
28
|
+
- **Text spacing** applies the WCAG 2.2 SC 1.4.12 override inside the
|
|
29
|
+
component and reports text that clips under it.
|
|
30
|
+
- **Reduced motion** reports motion longer than 200 ms that still runs under
|
|
31
|
+
`prefers-reduced-motion: reduce`.
|
|
32
|
+
- **Playwright** reads each view's accessibility tree and compares it with
|
|
33
|
+
the tree recorded for that view.
|
|
34
|
+
- **Keyboard cases** in the contract press Enter, Space, the arrow keys, and
|
|
35
|
+
Escape, and read what each key did.
|
|
36
|
+
|
|
37
|
+
A project's run scans the open theme. The library's own test run also repeats
|
|
38
|
+
axe's contrast rule, and both contrast checks, under each of the eight preset
|
|
39
|
+
themes.
|
|
40
|
+
|
|
41
|
+
**Pages.** `check-page --tests` runs three tests on each page at each
|
|
42
|
+
viewport:
|
|
43
|
+
|
|
44
|
+
- `page-a11y` runs axe over the page, leaving out the router's chrome and
|
|
45
|
+
every component the component suite scans. axe's contrast rule stays off here, because
|
|
46
|
+
`page-contrast` already reports text contrast at `error`.
|
|
47
|
+
- `page-text-spacing` applies the WCAG 2.2 SC 1.4.12 text spacing override
|
|
48
|
+
(line height 1.5, paragraph spacing 2em, letter spacing 0.12em, word
|
|
49
|
+
spacing 0.16em) and reports content that overflows under it.
|
|
50
|
+
- `page-reduced-motion` loads the page under `prefers-reduced-motion: reduce`
|
|
51
|
+
and reports animations that keep running.
|
|
52
|
+
|
|
53
|
+
## Rules
|
|
54
|
+
|
|
55
|
+
| Rule | Checker | Reports |
|
|
56
|
+
|------|---------|---------|
|
|
57
|
+
| `a11y-component` | `check-component` | An axe failure inside a component's preview, a focus ring or control boundary below 3:1, or a changed accessibility tree. |
|
|
58
|
+
| `a11y-focus-visible` | `check-component` | An element that looks the same focused as at rest (SIA-R65). |
|
|
59
|
+
| `a11y-keyboard` | `check-component` | A keyboard case whose key failed to activate, move, or close. |
|
|
60
|
+
| `a11y-page` | `check-page` | An axe failure in the page's own markup. |
|
|
61
|
+
| `a11y-text-spacing` | `check-component`, `check-page` | Content that overflows under the text spacing override. |
|
|
62
|
+
| `a11y-reduced-motion` | `check-component`, `check-page` | Motion that still runs under reduced motion. |
|
|
63
|
+
| `a11y-review` | both | A check the engine could not decide, for you to judge by eye. |
|
|
64
|
+
|
|
65
|
+
Each finding's message names the engine's own rule, the element, the view it
|
|
66
|
+
failed in, and a help page that states the requirement and its repair. The
|
|
67
|
+
rules take the same severity settings as every other rule. `--off=a11y-review`
|
|
68
|
+
silences one for a single run, `--error=a11y-page` raises one, and
|
|
69
|
+
`live-tokens.config.json` records either for good:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{ "checks": { "rules": { "a11y-review": "off", "a11y-page": "error" } } }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A scan that crashes reports `tests-setup`.
|
|
76
|
+
|
|
77
|
+
## Focus indicator contrast
|
|
78
|
+
|
|
79
|
+
The suite forces `:focus` and `:focus-visible` on each focusable element in
|
|
80
|
+
each view and looks for the style focus changes. It searches the element, its
|
|
81
|
+
descendants, and its siblings with their descendants, so a ring painted on a
|
|
82
|
+
sibling, such as a slider thumb's cap, counts. The indicator is the outline
|
|
83
|
+
when focus changes one, else the first outer box shadow, else a border side.
|
|
84
|
+
|
|
85
|
+
The backdrop is every box behind the ring's outer edge, composited in order
|
|
86
|
+
over white. A ring below 3:1 against it reports under `a11y-component` with
|
|
87
|
+
engine rule `focus-indicator-contrast`, such as "Focus outline on span.cap
|
|
88
|
+
2.14:1 against the backdrop, needs 3:1". Ratios floor to two places, because
|
|
89
|
+
WCAG does not round. Chromium's own `outline-style: auto` ring passes: it pairs
|
|
90
|
+
a dark band with a light one. An element with no indicator at all is
|
|
91
|
+
SIA-R65's finding and adds none here.
|
|
92
|
+
|
|
93
|
+
## Control boundary contrast
|
|
94
|
+
|
|
95
|
+
A control whose edge shows where to click or what it holds needs that edge
|
|
96
|
+
at 3:1 against the backdrop behind it. So does a selected or current item
|
|
97
|
+
whose surface or edge alone shows that state. The contract lists these parts
|
|
98
|
+
in `boundaries`. Each part passes when its border, on any side, or its
|
|
99
|
+
outline, 1px or wider, or its surface reaches 3:1. A failure names both ratios, such as "Track border
|
|
100
|
+
1.45:1 and surface 1.3:1 against the backdrop, needs 3:1", and reports under
|
|
101
|
+
`a11y-component` with engine rule `control-boundary-contrast`. A part whose
|
|
102
|
+
form controls are all disabled is skipped, because SC 1.4.11 exempts inactive
|
|
103
|
+
controls.
|
|
104
|
+
|
|
105
|
+
The shipped controls that draw their own edge declare it: Input's field,
|
|
106
|
+
RadioButton's circle, Slider's and Toggle's track and thumb, and
|
|
107
|
+
SegmentedControl's bar. The selected states that show by surface and edge
|
|
108
|
+
alone declare their part too: TabBar's selected tab, SegmentedControl's
|
|
109
|
+
selected option, and SideNavigation's selected title, section, item and
|
|
110
|
+
footer. Each measures against the bar or panel behind it, the surface the
|
|
111
|
+
unselected items show. MenuSelect's selected option carries a check mark, so
|
|
112
|
+
it declares none.
|
|
113
|
+
|
|
114
|
+
## Text spacing and motion per component
|
|
115
|
+
|
|
116
|
+
The text spacing check measures every element of the component that holds
|
|
117
|
+
text: its root part, its portaled parts, and their descendants. It applies
|
|
118
|
+
the override inside the preview alone and reports each element the override
|
|
119
|
+
pushes more than 1px past its box, on an axis it does not scroll. An element
|
|
120
|
+
already cut at rest adds nothing. The help names the measure: "Under the text
|
|
121
|
+
spacing override, `<span>` holds 24px of content in 18px of height, and its
|
|
122
|
+
overflow-y is hidden".
|
|
123
|
+
|
|
124
|
+
The reduced motion check reads the declared animations and transitions of
|
|
125
|
+
the same elements and their `::before` and `::after`, along with every
|
|
126
|
+
script animation running on them. Motion that lasts past 200 ms, its delay
|
|
127
|
+
included, is recorded. The check then emulates
|
|
128
|
+
`prefers-reduced-motion: reduce` in the same view and reports each motion
|
|
129
|
+
still that long, such as "A transition of left lasts 500 ms under
|
|
130
|
+
prefers-reduced-motion: reduce". A view with no long motion costs no second
|
|
131
|
+
read. The help page is WCAG technique C39, which serves WCAG 2.2 SC 2.3.3
|
|
132
|
+
Animation from Interactions.
|
|
133
|
+
|
|
134
|
+
## Property tracing
|
|
135
|
+
|
|
136
|
+
A finding that a property can fix names that property. The suite reads the
|
|
137
|
+
CSS rules that match the failing element through the Chrome DevTools
|
|
138
|
+
protocol, keeps the declarations the finding depends on, such as the outline
|
|
139
|
+
and border of a focus ring or the height and padding of a clipped label, and
|
|
140
|
+
follows each `var()` to the component's semantic properties and intrinsics.
|
|
141
|
+
A component's private custom properties lead through to the property behind
|
|
142
|
+
them. Theme and editor variables drop out.
|
|
143
|
+
|
|
144
|
+
`--json` output carries the result in each finding's `details.variables`,
|
|
145
|
+
sorted, and leaves the field out when the trace finds nothing. An excerpt:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"rule": "a11y-component",
|
|
150
|
+
"component": "slider",
|
|
151
|
+
"details": {
|
|
152
|
+
"engineRule": "focus-indicator-contrast",
|
|
153
|
+
"target": "span.cap",
|
|
154
|
+
"variables": ["--slider-range-thumb-border", "--slider-range-thumb-border-width"]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
axe's contrast and target size rules, SIA-R65, both contrast checks, text
|
|
160
|
+
spacing, and reduced motion trace. The ARIA rules, the keyboard cases, and
|
|
161
|
+
the accessibility tree mark the component alone.
|
|
162
|
+
|
|
163
|
+
## Review findings
|
|
164
|
+
|
|
165
|
+
`a11y-review` collects the checks an engine left undecided. axe reads contrast
|
|
166
|
+
from the colour behind the text, so it returns no verdict for text over a
|
|
167
|
+
gradient, an image, or a backdrop, or under a pseudo-element that covers it.
|
|
168
|
+
The focus indicator and control boundary checks do the same for a ring or an
|
|
169
|
+
edge over a gradient or an image. Judge each element by eye against the help
|
|
170
|
+
page the finding names.
|
|
171
|
+
|
|
172
|
+
Two sources account for most of them. Button's shimmer pseudo-element and its
|
|
173
|
+
tinted hover surfaces leave every Button view undecided, so a Button run lists
|
|
174
|
+
many `a11y-review` warnings. A theme that paints gradient surfaces does the
|
|
175
|
+
same for every component that sits on one.
|
|
176
|
+
|
|
177
|
+
## Accessibility trees
|
|
178
|
+
|
|
179
|
+
The package records each shipped component's accessibility tree, one block
|
|
180
|
+
per view, in `src/testing/contracts/__aria__/<id>.yml`. A run that reads a
|
|
181
|
+
different tree reports `a11y-component` with engine rule `aria-snapshot` and
|
|
182
|
+
the lines that changed.
|
|
183
|
+
|
|
184
|
+
Inside the live-tokens repository, after a deliberate markup change,
|
|
185
|
+
`--update-aria` records the tree anew, and `LIVE_TOKENS_UPDATE_ARIA=1 npm run
|
|
186
|
+
test:e2e:contract` rewrites every file:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
node bin/cli.mjs check-component button --tests --update-aria
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
In a project the trees sit under `node_modules`, where the suite reads them
|
|
193
|
+
and never writes. A project's own components therefore have no recorded tree
|
|
194
|
+
yet.
|
|
195
|
+
|
|
196
|
+
## Keyboard cases
|
|
197
|
+
|
|
198
|
+
A component contract declares its keyboard cases among its interaction
|
|
199
|
+
cases, each marked `rule: 'a11y-keyboard'`: Enter or Space activates, an
|
|
200
|
+
arrow key moves where the component's WAI-ARIA pattern moves, and Escape
|
|
201
|
+
closes an overlay. A failed keyboard case reports `a11y-keyboard` at `warn`.
|
|
202
|
+
A case without the mark fails as `contract-interaction`, an error. The
|
|
203
|
+
`live-tokens-create-component` skill writes these cases for a new component,
|
|
204
|
+
and its `references/contract-tests.md` describes their shape.
|
|
205
|
+
|
|
206
|
+
An editor that adds placeholder content to its preview, such as a demo label
|
|
207
|
+
or a hover target, marks it `data-preview-demo`, and the scans skip it.
|
|
208
|
+
|
|
209
|
+
## Boundaries in the contract
|
|
210
|
+
|
|
211
|
+
`boundaries` is an optional contract field,
|
|
212
|
+
`{ part: string; label: string; when?: string }[]`, for the parts that draw a
|
|
213
|
+
control's visible edge or show its selected state. `part` is a key of the
|
|
214
|
+
contract's `parts`, and `label` names the part in a finding. `when` is a
|
|
215
|
+
selector the part must also match to be measured, for a part that carries its
|
|
216
|
+
selected state as a class in some views only.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
boundaries: [
|
|
220
|
+
{ part: 'track', label: 'Track' },
|
|
221
|
+
{ part: 'thumb', label: 'Thumb' },
|
|
222
|
+
],
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
List a part whose border or surface is the only thing that shows the control
|
|
226
|
+
is there, a thumb whose position shows the value, or a selected item whose
|
|
227
|
+
surface or edge is the only thing that shows it is selected. Leave out a part whose
|
|
228
|
+
text or icon already shows it. A contract without the field runs no boundary
|
|
229
|
+
check.
|
|
230
|
+
|
|
231
|
+
## Accessibility view
|
|
232
|
+
|
|
233
|
+
The editor's Accessibility view runs the component checks from the browser.
|
|
234
|
+
**Run test** checks every component in the open theme as saved, in about
|
|
235
|
+
three and a half minutes. With edits pending, it shows "Save the changes you
|
|
236
|
+
want to have checked." and starts nothing. Save the theme first, so the run
|
|
237
|
+
and the editor read the same values.
|
|
238
|
+
|
|
239
|
+
Every finding counts, results to check by eye included. An overview opens
|
|
240
|
+
the report. It shows the total, the count in each repair group below, and a
|
|
241
|
+
table of every component with a finding: its count per group and a bar of
|
|
242
|
+
its share. A component's name in the table scrolls to its section.
|
|
243
|
+
|
|
244
|
+
Each component's section heads its count with **Open component**, which opens
|
|
245
|
+
the component's page at its accessibility panel. The section sorts its rows
|
|
246
|
+
by repair:
|
|
247
|
+
|
|
248
|
+
- **Fix with a property** holds each colour pair below its WCAG floor, as the
|
|
249
|
+
editor's contrast checker measures it, such as "Brand text color 3.87:1,
|
|
250
|
+
needs 4.5:1", and every focus ring and control boundary below 3:1.
|
|
251
|
+
- **Fix in the component's code** holds every other finding: axe failures,
|
|
252
|
+
focus styles that SIA-R65 finds unchanged, failed keyboard cases, text that
|
|
253
|
+
clips under the spacing override, and motion that ignores reduced motion.
|
|
254
|
+
- **Check by eye** holds the `a11y-review` results.
|
|
255
|
+
|
|
256
|
+
Findings that share an engine rule, a help text, and their traced properties
|
|
257
|
+
merge into one row. The row names its element, or its element count, and the
|
|
258
|
+
views it failed in, up to four. An issue's text is amber, and a result to
|
|
259
|
+
check by eye is grey. The row's links sit beside its text:
|
|
260
|
+
|
|
261
|
+
- Each property the row traces to links with a "→" to its row on the
|
|
262
|
+
component's page, which scrolls to the row and highlights it. A row that
|
|
263
|
+
traces to no property links to the component's page instead.
|
|
264
|
+
- The link on the right opens the page the finding cites and names its kind:
|
|
265
|
+
"Rule" for an axe or Alfa rule, "Success criterion" with its number for a
|
|
266
|
+
WCAG criterion, "Technique" for a WCAG technique, and "Pattern" for a
|
|
267
|
+
WAI-ARIA pattern.
|
|
268
|
+
|
|
269
|
+
A property's label leads with the words from its variable name that the
|
|
270
|
+
editor's label leaves out, so `--input-focused-border` reads "Focused border
|
|
271
|
+
color" where the editor's row reads "Border color". The contrast rows read
|
|
272
|
+
the same labels.
|
|
273
|
+
|
|
274
|
+
The overview and the per-component counts include the results to check by
|
|
275
|
+
eye. A component's section ends in a "N results to check by eye" button that
|
|
276
|
+
opens one closed list of those results, merged per component. axe's contrast
|
|
277
|
+
violations stay out of the report, because the contrast checker measures the
|
|
278
|
+
same pairs.
|
|
279
|
+
|
|
280
|
+
When Playwright, axe, or Alfa is missing, the report names its install
|
|
281
|
+
command and Run test stays disabled. Install the package and press **Check
|
|
282
|
+
again**. The view runs its tests through the dev server, so it exists under
|
|
283
|
+
the dev server alone, with the rest of the editor.
|
|
284
|
+
|
|
285
|
+
The view runs `check-component --tests --a11y --no-fix --json --open-theme`.
|
|
286
|
+
`--a11y` runs the accessibility suite and the keyboard cases alone, and
|
|
287
|
+
`--open-theme` tests the theme the editor has open. A run without it tests
|
|
288
|
+
the `default` theme. Both work from the command line too.
|
|
289
|
+
|
|
290
|
+
## Marks in the editor
|
|
291
|
+
|
|
292
|
+
The last finished run marks the rest of the editor:
|
|
293
|
+
|
|
294
|
+
- The component list leads the name of each component with any finding
|
|
295
|
+
with a warning triangle. A condensed list shows the triangle on the
|
|
296
|
+
component's icon.
|
|
297
|
+
- That component's page opens with a panel under its title that lists its
|
|
298
|
+
findings in all three groups, check by eye included.
|
|
299
|
+
- Each "→" in the panel opens the tab holding the property and highlights
|
|
300
|
+
its row.
|
|
301
|
+
- Each property row a finding traces to carries a mark beside its label: a
|
|
302
|
+
warning triangle in the highlight colour, or a grey "?" when every finding on the row is a
|
|
303
|
+
result to check by eye. On a colour or size row the mark sits between the
|
|
304
|
+
label and the value. On a toggle row it follows the label.
|
|
305
|
+
|
|
306
|
+
Click a row's mark to open its findings: the count, then each merged row
|
|
307
|
+
with its element, its views, its help page, and the properties it traces to. The
|
|
308
|
+
editor's own contrast failures add no row mark, because the contrast readout
|
|
309
|
+
on the colour row already shows them.
|
|
310
|
+
|
|
311
|
+
The marks and the panel keep the run's time and change when the next run
|
|
312
|
+
finishes. The contrast readout on each colour row stays live and measures
|
|
313
|
+
every edit as you make it.
|
|
314
|
+
|
|
315
|
+
## Installing the engines
|
|
316
|
+
|
|
317
|
+
The engines ship as optional peer dependencies. A project made with `create`
|
|
318
|
+
has them. Any other project installs them beside `@playwright/test`:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
npm install -D axe-core @axe-core/playwright \
|
|
322
|
+
@siteimprove/alfa-act@^0.119.0 @siteimprove/alfa-device@^0.119.0 \
|
|
323
|
+
@siteimprove/alfa-dom@^0.119.0 @siteimprove/alfa-http@^0.119.0 \
|
|
324
|
+
@siteimprove/alfa-rules@^0.119.0 @siteimprove/alfa-url@^0.119.0 \
|
|
325
|
+
@siteimprove/alfa-web@^0.119.0
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The Alfa packages share one minor version. axe-core needs 4.11.1 or later to
|
|
329
|
+
read OKLCH colours, which every shipped theme uses. A test whose engine is
|
|
330
|
+
missing reports as inapplicable, and its coverage names the package. A
|
|
331
|
+
missing engine leaves the exit status to the other rules, under `--strict`
|
|
332
|
+
too.
|
|
333
|
+
|
|
334
|
+
## Limits
|
|
335
|
+
|
|
336
|
+
- The page's text spacing test measures widths, so it reports horizontal
|
|
337
|
+
overflow alone. The component check measures both axes.
|
|
338
|
+
- The page's reduced motion test reports animations still running 200 ms
|
|
339
|
+
after the page loads. A short entrance animation finishes first and goes
|
|
340
|
+
unseen.
|
|
341
|
+
- The component's reduced motion check reads the preference within each
|
|
342
|
+
view. A script that reads it only when the component mounts goes unseen.
|
|
343
|
+
- The page scan leaves contrast to `page-contrast` and focus visibility to
|
|
344
|
+
the component suite.
|
|
345
|
+
- SIA-R65 reads the focused element, its descendants, and its ancestors, so
|
|
346
|
+
it reports a focus ring painted on a sibling as missing. The focus
|
|
347
|
+
indicator check measures that ring; confirm it by eye.
|
|
348
|
+
- A view that forces the focused look at rest, such as Input's focused
|
|
349
|
+
state, leaves real focus nothing to change, so SIA-R65 reports it there.
|
|
350
|
+
The same element passes in the views that leave it at rest.
|
|
351
|
+
- A finding traces to the component's own semantic properties and
|
|
352
|
+
intrinsics. A ring or colour that a theme token paints directly, such as
|
|
353
|
+
SegmentedControl's `--border-neutral-strong` focus ring, traces to
|
|
354
|
+
nothing. A finding on a component nested
|
|
355
|
+
inside another, such as a Button inside a Dialog, marks the host and names
|
|
356
|
+
none of the nested component's properties.
|
|
357
|
+
- A literal value in the component's code, such as a transition's duration,
|
|
358
|
+
has no property to name. The finding traces to whatever property governs
|
|
359
|
+
the same declaration, or to none.
|
|
360
|
+
- A project's own components get no recorded accessibility tree.
|
|
361
|
+
- Automated checks find part of what WCAG asks. Reading order, the meaning of
|
|
362
|
+
labels and alt text, and screen reader behaviour still need a person.
|