@motion-proto/live-tokens 0.97.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +339 -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 +144 -19
- package/bin/lib/findings.mjs +20 -4
- package/bin/rules/a11y.mjs +53 -0
- package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
- package/dist-plugin/index.cjs +204 -3
- package/dist-plugin/index.js +306 -105
- package/dist-plugin/setColors/index.js +1 -1
- package/package.json +47 -2
- package/src/demo/LabeledSelect.svelte +1 -1
- package/src/editor/bootstrap.ts +5 -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 +40 -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 -3
- package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
- package/src/editor/core/a11y/a11yReport.ts +164 -0
- package/src/editor/core/a11y/a11yReportTypes.ts +77 -0
- package/src/editor/core/a11y/contrastFailures.ts +22 -0
- package/src/editor/core/a11y/contrastFixes.ts +41 -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 +82 -0
- package/src/editor/core/palettes/resolveColor.ts +77 -0
- package/src/editor/core/store/editorStore.ts +4 -0
- package/src/editor/core/store/editorViewStore.ts +2 -2
- package/src/editor/core/store/unsavedEdits.ts +10 -0
- package/src/editor/core/themes/liveStateStream.ts +7 -1
- package/src/editor/core/themes/parsers/gradient.ts +5 -2
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/accessibility.md +411 -0
- package/src/editor/docs/content/editing-tokens.md +32 -1
- package/src/editor/docs/content.generated.ts +2 -1
- package/src/editor/pages/ComponentEditorPage.svelte +11 -1
- package/src/editor/pages/EditorShell.svelte +98 -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/styles/ui-editor.css +2 -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 +9 -0
- package/src/editor/ui/UITokenSelector.svelte +26 -2
- package/src/editor/ui/a11y/A11yByEye.svelte +264 -0
- package/src/editor/ui/a11y/A11yComponentIssues.svelte +121 -0
- package/src/editor/ui/a11y/A11yFindingRow.svelte +119 -0
- package/src/editor/ui/a11y/A11yIssueRow.svelte +152 -0
- package/src/editor/ui/a11y/A11yOverview.svelte +246 -0
- package/src/editor/ui/a11y/A11yPanel.svelte +153 -0
- package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
- package/src/editor/ui/a11y/A11yRailMark.svelte +41 -0
- package/src/editor/ui/a11y/AccessibilityTab.svelte +321 -0
- package/src/editor/ui/a11y/a11yView.ts +257 -0
- package/src/editor/ui/contrastReadout.ts +45 -0
- package/src/live-tokens/data/colors-and-type/halloween.json +1 -1
- package/src/live-tokens/data/themes/autumn.json +35 -35
- package/src/live-tokens/data/themes/halloween.json +36 -36
- package/src/live-tokens/data/themes/midnight-study.json +35 -35
- package/src/live-tokens/data/themes/ocean.json +35 -35
- package/src/live-tokens/data/themes/royal-velvet.json +35 -35
- package/src/live-tokens/data/themes/sketchy.json +35 -35
- package/src/live-tokens/data/themes/spring-meadow.json +35 -35
- package/src/live-tokens/data/themes/sunset.json +35 -35
- package/src/system/components/Badge.svelte +3 -3
- package/src/system/components/Button.svelte +12 -6
- 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 +18 -25
- package/src/system/components/InlineEditActions.svelte +1 -1
- package/src/system/components/Input.svelte +9 -2
- package/src/system/components/MenuSelect.svelte +31 -3
- package/src/system/components/Notification.svelte +1 -1
- package/src/system/components/ProgressBar.svelte +7 -1
- package/src/system/components/RadioButton.svelte +36 -3
- package/src/system/components/SegmentedControl.svelte +36 -2
- package/src/system/components/SlidePager.svelte +3 -3
- package/src/system/components/Slider.svelte +19 -15
- package/src/system/components/TabBar.svelte +33 -3
- package/src/system/components/Toggle.svelte +3 -3
- 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-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
- package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
- package/src/testing-js/{chunk-X6KHEVIG.js → chunk-7QXUJRSO.js} +97 -29
- package/src/testing-js/chunk-7QXUJRSO.js.map +1 -0
- package/src/testing-js/chunk-GZKUYLDW.js +193 -0
- package/src/testing-js/chunk-GZKUYLDW.js.map +1 -0
- package/src/testing-js/chunk-RTZQLW3F.js +649 -0
- package/src/testing-js/chunk-RTZQLW3F.js.map +1 -0
- package/src/testing-js/{chunk-SHBYZUPD.js → chunk-RWOT2XDB.js} +212 -15
- package/src/testing-js/chunk-RWOT2XDB.js.map +1 -0
- package/src/testing-js/{chunk-GKO27NGN.js → chunk-THWGMSRH.js} +2 -2
- 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 +313 -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 +11 -11
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +132 -13
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/testing-js/vitest.js +3 -3
- package/template/_gitignore +3 -0
- package/template/package.json +9 -0
- 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-THWGMSRH.js.map} +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { a11yIssues, confirmReview } from '../../core/a11y/a11yReport';
|
|
4
|
+
import UIPillButton from '../UIPillButton.svelte';
|
|
5
|
+
import A11yFindingRow from './A11yFindingRow.svelte';
|
|
6
|
+
import { A11Y_REVIEW_SECTION, openComponentAt, reviewResults, type A11yReviewResult } from './a11yView';
|
|
7
|
+
|
|
8
|
+
const titleId = $props.id();
|
|
9
|
+
|
|
10
|
+
let results = $derived(reviewResults($a11yIssues));
|
|
11
|
+
let open = $derived(results.filter((r) => !r.confirmed));
|
|
12
|
+
let confirmed = $derived(results.filter((r) => r.confirmed));
|
|
13
|
+
let tally = $derived(
|
|
14
|
+
[open.length > 0 ? `${open.length} to check` : '', confirmed.length > 0 ? `${confirmed.length} look right` : '']
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.join(', '),
|
|
17
|
+
);
|
|
18
|
+
let done = $derived(results.length > 0 ? confirmed.length / results.length : 0);
|
|
19
|
+
|
|
20
|
+
let openList: HTMLUListElement | undefined = $state();
|
|
21
|
+
let confirmedList: HTMLUListElement | undefined = $state();
|
|
22
|
+
let heading: HTMLElement | undefined = $state();
|
|
23
|
+
let markingAll = $state(false);
|
|
24
|
+
let markAllError: string | null = $state(null);
|
|
25
|
+
|
|
26
|
+
const keyOf = (r: A11yReviewResult) => `${r.component}\n${r.row.findings.map((f) => f.message).join('\n')}`;
|
|
27
|
+
|
|
28
|
+
/** Moves focus to the toggle that took the pressed row's place, so Enter or Space walks the list. */
|
|
29
|
+
async function focusAt(list: HTMLUListElement | undefined, index: number) {
|
|
30
|
+
await tick();
|
|
31
|
+
const toggles = [...(list?.querySelectorAll<HTMLButtonElement>('.a11y-confirm button') ?? [])];
|
|
32
|
+
(toggles[index] ?? toggles[index - 1] ?? heading)?.focus();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function markAll() {
|
|
36
|
+
markingAll = true;
|
|
37
|
+
markAllError = null;
|
|
38
|
+
try {
|
|
39
|
+
await confirmReview(open.flatMap((r) => r.row.findings), true);
|
|
40
|
+
await focusAt(undefined, 0);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
markAllError = (err as Error).message;
|
|
43
|
+
} finally {
|
|
44
|
+
markingAll = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<section id={A11Y_REVIEW_SECTION} class="editor-section-card a11y-byeye" aria-labelledby={titleId}>
|
|
50
|
+
<header class="a11y-byeye-head">
|
|
51
|
+
<i class="fa-solid fa-eye a11y-byeye-mark" aria-hidden="true"></i>
|
|
52
|
+
<h3 id={titleId} class="editor-subsection-title" tabindex="-1" bind:this={heading}>Check by eye</h3>
|
|
53
|
+
<span class="a11y-byeye-tally">{tally}</span>
|
|
54
|
+
{#if open.length > 0}
|
|
55
|
+
<UIPillButton size="compact" variant="outline" icon="fa-check-double" disabled={markingAll} onclick={markAll}>
|
|
56
|
+
Mark all as looking right
|
|
57
|
+
</UIPillButton>
|
|
58
|
+
{/if}
|
|
59
|
+
</header>
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
class="a11y-byeye-meter"
|
|
63
|
+
role="progressbar"
|
|
64
|
+
aria-label="Results marked as looking right"
|
|
65
|
+
aria-valuemin={0}
|
|
66
|
+
aria-valuemax={results.length}
|
|
67
|
+
aria-valuenow={confirmed.length}
|
|
68
|
+
>
|
|
69
|
+
<span class="a11y-byeye-meter-fill" style:transform={`scaleX(${done})`}></span>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{#if markAllError}
|
|
73
|
+
<p class="a11y-byeye-error" role="alert">{markAllError}</p>
|
|
74
|
+
{/if}
|
|
75
|
+
|
|
76
|
+
{#if open.length > 0}
|
|
77
|
+
<p class="a11y-byeye-lead">
|
|
78
|
+
The tools could not decide these. Judge each element against its help, then press <strong>Looks right</strong>.
|
|
79
|
+
The next result moves up under the same button.
|
|
80
|
+
</p>
|
|
81
|
+
<ul class="a11y-byeye-list" bind:this={openList}>
|
|
82
|
+
{#each open as result, i (keyOf(result))}
|
|
83
|
+
<A11yFindingRow
|
|
84
|
+
component={result.component}
|
|
85
|
+
row={result.row}
|
|
86
|
+
review
|
|
87
|
+
prefix={result.label}
|
|
88
|
+
onreveal={(variable) => openComponentAt(result.component, variable)}
|
|
89
|
+
onopen={() => openComponentAt(result.component)}
|
|
90
|
+
onconfirmchange={() => focusAt(openList, i)}
|
|
91
|
+
/>
|
|
92
|
+
{/each}
|
|
93
|
+
</ul>
|
|
94
|
+
{:else}
|
|
95
|
+
<p class="a11y-byeye-clear">
|
|
96
|
+
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
|
97
|
+
Every result looks right. Open <strong>Looks right</strong> below to check one again.
|
|
98
|
+
</p>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
{#if confirmed.length > 0}
|
|
102
|
+
<details class="a11y-byeye-done">
|
|
103
|
+
<summary class="a11y-byeye-group">
|
|
104
|
+
<i class="fa-solid fa-chevron-right a11y-byeye-chevron" aria-hidden="true"></i>
|
|
105
|
+
Looks right
|
|
106
|
+
<span class="a11y-byeye-group-count">{confirmed.length}</span>
|
|
107
|
+
</summary>
|
|
108
|
+
<ul class="a11y-byeye-list" bind:this={confirmedList}>
|
|
109
|
+
{#each confirmed as result, i (keyOf(result))}
|
|
110
|
+
<A11yFindingRow
|
|
111
|
+
component={result.component}
|
|
112
|
+
row={result.row}
|
|
113
|
+
review
|
|
114
|
+
confirmed
|
|
115
|
+
prefix={result.label}
|
|
116
|
+
onreveal={(variable) => openComponentAt(result.component, variable)}
|
|
117
|
+
onopen={() => openComponentAt(result.component)}
|
|
118
|
+
onconfirmchange={() => focusAt(confirmedList, i)}
|
|
119
|
+
/>
|
|
120
|
+
{/each}
|
|
121
|
+
</ul>
|
|
122
|
+
</details>
|
|
123
|
+
{/if}
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<style>
|
|
127
|
+
.a11y-byeye {
|
|
128
|
+
gap: var(--ui-space-12);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.a11y-byeye-head {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-wrap: wrap;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: var(--ui-space-8) var(--ui-space-12);
|
|
136
|
+
color: var(--ui-text-primary);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.a11y-byeye-head :global(.ui-pill) {
|
|
140
|
+
margin-left: auto;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.a11y-byeye-head h3:focus {
|
|
144
|
+
outline: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.a11y-byeye-mark {
|
|
148
|
+
color: var(--ui-text-tertiary);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.a11y-byeye-tally {
|
|
152
|
+
font-size: var(--ui-font-size-sm);
|
|
153
|
+
color: var(--ui-text-tertiary);
|
|
154
|
+
font-variant-numeric: tabular-nums;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.a11y-byeye-meter {
|
|
158
|
+
height: 2px;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
background: var(--ui-border-low);
|
|
161
|
+
border-radius: var(--ui-radius-full);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.a11y-byeye-meter-fill {
|
|
165
|
+
display: block;
|
|
166
|
+
height: 100%;
|
|
167
|
+
background: var(--ui-text-primary);
|
|
168
|
+
transform-origin: left;
|
|
169
|
+
transition: transform 240ms ease;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@media (prefers-reduced-motion: reduce) {
|
|
173
|
+
.a11y-byeye-meter-fill {
|
|
174
|
+
transition: none;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.a11y-byeye-lead,
|
|
179
|
+
.a11y-byeye-clear,
|
|
180
|
+
.a11y-byeye-error {
|
|
181
|
+
margin: 0;
|
|
182
|
+
font-size: var(--ui-font-size-sm);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.a11y-byeye-lead {
|
|
186
|
+
color: var(--ui-text-tertiary);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.a11y-byeye-lead strong,
|
|
190
|
+
.a11y-byeye-clear strong {
|
|
191
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
192
|
+
color: var(--ui-text-secondary);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.a11y-byeye-clear {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: var(--ui-space-8);
|
|
199
|
+
padding: var(--ui-space-12) 0;
|
|
200
|
+
color: var(--ui-text-secondary);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.a11y-byeye-error {
|
|
204
|
+
color: var(--ui-highlight);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.a11y-byeye-done {
|
|
208
|
+
margin-top: var(--ui-space-12);
|
|
209
|
+
border-top: 1px solid var(--ui-border);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.a11y-byeye-group {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
gap: var(--ui-space-8);
|
|
216
|
+
padding-top: var(--ui-space-12);
|
|
217
|
+
font-size: var(--ui-font-size-xs);
|
|
218
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
219
|
+
color: var(--ui-text-secondary);
|
|
220
|
+
list-style: none;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
transition: color var(--ui-transition-fast);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.a11y-byeye-group::-webkit-details-marker {
|
|
226
|
+
display: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.a11y-byeye-group:hover {
|
|
230
|
+
color: var(--ui-text-primary);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.a11y-byeye-chevron {
|
|
234
|
+
font-size: var(--ui-font-size-xs);
|
|
235
|
+
transition: transform var(--ui-transition-fast);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.a11y-byeye-done[open] .a11y-byeye-chevron {
|
|
239
|
+
transform: rotate(90deg);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@media (prefers-reduced-motion: reduce) {
|
|
243
|
+
.a11y-byeye-chevron {
|
|
244
|
+
transition: none;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.a11y-byeye-group-count {
|
|
249
|
+
font-weight: var(--ui-font-weight-normal);
|
|
250
|
+
color: var(--ui-text-tertiary);
|
|
251
|
+
font-variant-numeric: tabular-nums;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.a11y-byeye-list {
|
|
255
|
+
margin: 0;
|
|
256
|
+
padding: 0;
|
|
257
|
+
list-style: none;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.a11y-byeye-list :global(code) {
|
|
261
|
+
font-family: var(--ui-font-mono);
|
|
262
|
+
font-size: var(--ui-font-size-sm);
|
|
263
|
+
}
|
|
264
|
+
</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ComponentA11yIssues } from '../../core/a11y/a11yReport';
|
|
3
|
+
import type { A11yContrastFailure } from '../../core/a11y/a11yReportTypes';
|
|
4
|
+
import { CONTRAST_RULES } from '../../core/palettes/contrast';
|
|
5
|
+
import { formatFloor, formatRatio } from '../contrastReadout';
|
|
6
|
+
import A11yFindingRow from './A11yFindingRow.svelte';
|
|
7
|
+
import A11yIssueRow from './A11yIssueRow.svelte';
|
|
8
|
+
import { findingRows, propertyLabel } from './a11yView';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
component: string;
|
|
12
|
+
issues: ComponentA11yIssues;
|
|
13
|
+
/** Lists the results to check by eye as a third group. The Accessibility view lists them apart. */
|
|
14
|
+
withReview?: boolean;
|
|
15
|
+
onreveal?: (variable: string) => void;
|
|
16
|
+
/** Opens the component, for a row that traced no property. Absent on the component's own page. */
|
|
17
|
+
onopen?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { component, issues, withReview = false, onreveal, onopen }: Props = $props();
|
|
21
|
+
|
|
22
|
+
let propertyRows = $derived(findingRows(issues.property));
|
|
23
|
+
let codeRows = $derived(findingRows(issues.code));
|
|
24
|
+
let reviewRows = $derived(withReview ? findingRows(issues.review) : []);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#snippet contrastRow(failure: A11yContrastFailure, fixedRatio: number | undefined)}
|
|
28
|
+
{@const label = propertyLabel(component, failure.variable) ?? failure.variable}
|
|
29
|
+
{@const rule = CONTRAST_RULES[failure.role]}
|
|
30
|
+
<A11yIssueRow fixed={fixedRatio !== undefined} href={rule.url} linkLabel={`Success criterion ${rule.criterion}`}>
|
|
31
|
+
{#if fixedRatio === undefined}
|
|
32
|
+
{label} {formatRatio(failure.ratio)}, needs {formatFloor(failure.floor)}
|
|
33
|
+
{:else}
|
|
34
|
+
<i class="fa-solid fa-check a11y-fixed-mark" aria-hidden="true"></i>
|
|
35
|
+
{label} {formatRatio(failure.ratio)} → {formatRatio(fixedRatio)}, meets {formatFloor(failure.floor)}
|
|
36
|
+
{/if}
|
|
37
|
+
{#snippet detail()}
|
|
38
|
+
<code>{failure.variable}</code>{#if failure.context}, {failure.context}{/if}
|
|
39
|
+
{/snippet}
|
|
40
|
+
{#snippet links()}
|
|
41
|
+
{#if onreveal}
|
|
42
|
+
<button type="button" class="a11y-link" aria-label={`Go to ${label}`} title={failure.variable} onclick={() => onreveal(failure.variable)}>
|
|
43
|
+
{label} <i class="fas fa-arrow-right" aria-hidden="true"></i>
|
|
44
|
+
</button>
|
|
45
|
+
{/if}
|
|
46
|
+
{/snippet}
|
|
47
|
+
</A11yIssueRow>
|
|
48
|
+
{/snippet}
|
|
49
|
+
|
|
50
|
+
{#if issues.contrast.length > 0 || propertyRows.length > 0 || issues.fixed.length > 0}
|
|
51
|
+
<div class="a11y-group">
|
|
52
|
+
<h4 class="a11y-group-title">Fix with a property</h4>
|
|
53
|
+
<ul class="a11y-issues">
|
|
54
|
+
{#each issues.contrast as failure, i (i)}
|
|
55
|
+
{@render contrastRow(failure, undefined)}
|
|
56
|
+
{/each}
|
|
57
|
+
{#each propertyRows as row, i (i)}
|
|
58
|
+
<A11yFindingRow {component} {row} {onreveal} {onopen} />
|
|
59
|
+
{/each}
|
|
60
|
+
{#each issues.fixed as { failure, ratio }, i (i)}
|
|
61
|
+
{@render contrastRow(failure, ratio)}
|
|
62
|
+
{/each}
|
|
63
|
+
</ul>
|
|
64
|
+
</div>
|
|
65
|
+
{/if}
|
|
66
|
+
|
|
67
|
+
{#if codeRows.length > 0}
|
|
68
|
+
<div class="a11y-group">
|
|
69
|
+
<h4 class="a11y-group-title">Fix in the component's code</h4>
|
|
70
|
+
<ul class="a11y-issues">
|
|
71
|
+
{#each codeRows as row, i (i)}
|
|
72
|
+
<A11yFindingRow {component} {row} {onreveal} {onopen} />
|
|
73
|
+
{/each}
|
|
74
|
+
</ul>
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
77
|
+
|
|
78
|
+
{#if reviewRows.length > 0}
|
|
79
|
+
<div class="a11y-group">
|
|
80
|
+
<h4 class="a11y-group-title">Check by eye</h4>
|
|
81
|
+
<ul class="a11y-issues">
|
|
82
|
+
{#each reviewRows as row, i (i)}
|
|
83
|
+
<A11yFindingRow {component} {row} review {onreveal} {onopen} />
|
|
84
|
+
{/each}
|
|
85
|
+
</ul>
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.a11y-group {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: var(--ui-space-4);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.a11y-group + .a11y-group {
|
|
97
|
+
margin-top: var(--ui-space-12);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.a11y-group-title {
|
|
101
|
+
margin: 0;
|
|
102
|
+
font-size: var(--ui-font-size-xs);
|
|
103
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
104
|
+
color: var(--ui-text-secondary);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.a11y-group code {
|
|
108
|
+
font-family: var(--ui-font-mono);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.a11y-fixed-mark {
|
|
112
|
+
margin-right: var(--ui-space-4);
|
|
113
|
+
font-size: var(--ui-font-size-sm);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.a11y-issues {
|
|
117
|
+
margin: 0;
|
|
118
|
+
padding: 0;
|
|
119
|
+
list-style: none;
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { confirmReview } from '../../core/a11y/a11yReport';
|
|
3
|
+
import UIPillButton from '../UIPillButton.svelte';
|
|
4
|
+
import A11yIssueRow from './A11yIssueRow.svelte';
|
|
5
|
+
import {
|
|
6
|
+
componentLabel,
|
|
7
|
+
elementsLabel,
|
|
8
|
+
findingText,
|
|
9
|
+
guidanceLabel,
|
|
10
|
+
propertyLabel,
|
|
11
|
+
type A11yFindingRow,
|
|
12
|
+
} from './a11yView';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
component: string;
|
|
16
|
+
row: A11yFindingRow;
|
|
17
|
+
review?: boolean;
|
|
18
|
+
/** A result to check by eye that someone marked as looking right. */
|
|
19
|
+
confirmed?: boolean;
|
|
20
|
+
/** Leads the text, as the component's name does in a list that spans components. */
|
|
21
|
+
prefix?: string;
|
|
22
|
+
onreveal?: (variable: string) => void;
|
|
23
|
+
/** Opens the component, for a row that traced no property. Absent on the component's own page. */
|
|
24
|
+
onopen?: () => void;
|
|
25
|
+
/** Runs once a press of Looks right has saved, with the row's new state. */
|
|
26
|
+
onconfirmchange?: (confirmed: boolean) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let { component, row, review = false, confirmed = false, prefix, onreveal, onopen, onconfirmchange }: Props = $props();
|
|
30
|
+
|
|
31
|
+
let properties = $derived(row.variables.map((variable) => ({ variable, label: propertyLabel(component, variable) ?? variable })));
|
|
32
|
+
let where = $derived([row.targets.length > 1 ? elementsLabel(row.targets.length) : '', row.views].filter(Boolean).join(', '));
|
|
33
|
+
let helpUrl = $derived(row.finding.details?.helpUrl);
|
|
34
|
+
let hasNavigation = $derived((properties.length > 0 && onreveal !== undefined) || (properties.length === 0 && onopen !== undefined));
|
|
35
|
+
|
|
36
|
+
let saving = $state(false);
|
|
37
|
+
let saveError: string | null = $state(null);
|
|
38
|
+
|
|
39
|
+
async function toggleConfirmed() {
|
|
40
|
+
saving = true;
|
|
41
|
+
saveError = null;
|
|
42
|
+
// Read before the save: the save moves the row to the other list, which unmounts this one.
|
|
43
|
+
const next = !confirmed;
|
|
44
|
+
const done = onconfirmchange;
|
|
45
|
+
try {
|
|
46
|
+
await confirmReview(row.findings, next);
|
|
47
|
+
done?.(next);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
saveError = (err as Error).message;
|
|
50
|
+
} finally {
|
|
51
|
+
saving = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
{#snippet location()}
|
|
57
|
+
{#if row.targets.length === 1}<code>{row.targets[0]}</code>{#if row.views}{`, ${row.views}`}{/if}{:else}{where}{/if}
|
|
58
|
+
{#if saveError}<span class="a11y-confirm-error" role="alert">{saveError}</span>{/if}
|
|
59
|
+
{/snippet}
|
|
60
|
+
|
|
61
|
+
{#snippet confirmToggle()}
|
|
62
|
+
<span class="a11y-confirm">
|
|
63
|
+
<UIPillButton
|
|
64
|
+
size="compact"
|
|
65
|
+
variant="outline"
|
|
66
|
+
icon={confirmed ? 'fa-circle-check' : 'fa-check'}
|
|
67
|
+
ariaPressed={confirmed}
|
|
68
|
+
disabled={saving}
|
|
69
|
+
title={confirmed ? 'Marked as looking right. Press to clear the mark.' : 'Mark this result as looking right'}
|
|
70
|
+
onclick={toggleConfirmed}
|
|
71
|
+
>Looks right</UIPillButton>
|
|
72
|
+
</span>
|
|
73
|
+
{/snippet}
|
|
74
|
+
|
|
75
|
+
{#snippet links()}
|
|
76
|
+
{#if properties.length > 0 && onreveal}
|
|
77
|
+
{#each properties as { variable, label } (variable)}
|
|
78
|
+
<button type="button" class="a11y-link" aria-label={`Go to ${label}`} title={variable} onclick={() => onreveal(variable)}>
|
|
79
|
+
{label} <i class="fas fa-arrow-right" aria-hidden="true"></i>
|
|
80
|
+
</button>
|
|
81
|
+
{/each}
|
|
82
|
+
{:else if onopen}
|
|
83
|
+
<button type="button" class="a11y-link" onclick={onopen}>
|
|
84
|
+
Open {componentLabel(component)} <i class="fas fa-arrow-right" aria-hidden="true"></i>
|
|
85
|
+
</button>
|
|
86
|
+
{/if}
|
|
87
|
+
{/snippet}
|
|
88
|
+
|
|
89
|
+
<A11yIssueRow
|
|
90
|
+
{review}
|
|
91
|
+
{confirmed}
|
|
92
|
+
href={helpUrl}
|
|
93
|
+
linkLabel={helpUrl ? guidanceLabel(helpUrl) : undefined}
|
|
94
|
+
detail={row.targets.length > 0 || row.views || saveError ? location : undefined}
|
|
95
|
+
links={hasNavigation ? links : undefined}
|
|
96
|
+
action={review ? confirmToggle : undefined}
|
|
97
|
+
>
|
|
98
|
+
{#if review}<i class="fa-solid {confirmed ? 'fa-check' : 'fa-eye'} a11y-review-mark" aria-hidden="true"></i>{/if}
|
|
99
|
+
{#if prefix}{prefix}: {/if}{findingText(row.finding)}
|
|
100
|
+
</A11yIssueRow>
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
.a11y-review-mark {
|
|
104
|
+
margin-right: var(--ui-space-4);
|
|
105
|
+
font-size: var(--ui-font-size-sm);
|
|
106
|
+
color: var(--ui-text-tertiary);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.a11y-confirm-error {
|
|
110
|
+
margin-left: var(--ui-space-8);
|
|
111
|
+
color: var(--ui-highlight);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* One width for both states, so a column of toggles lines up. */
|
|
115
|
+
.a11y-confirm :global(.ui-pill) {
|
|
116
|
+
min-width: 8.5rem;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** A result to check by eye: its text in grey, where an issue's is amber. */
|
|
6
|
+
review?: boolean;
|
|
7
|
+
/** A result to check by eye marked as looking right: its text dimmer still. */
|
|
8
|
+
confirmed?: boolean;
|
|
9
|
+
/** An issue the editor now measures as passing: its text in green. */
|
|
10
|
+
fixed?: boolean;
|
|
11
|
+
href?: string;
|
|
12
|
+
/** Names what `href` points at, such as "Rule" or "Success criterion 1.4.11". */
|
|
13
|
+
linkLabel?: string;
|
|
14
|
+
children: Snippet;
|
|
15
|
+
detail?: Snippet;
|
|
16
|
+
/** The in-editor links that lead the link group, such as the traced properties. */
|
|
17
|
+
links?: Snippet;
|
|
18
|
+
/** Sits in its own column at the row's right edge, so it keeps its place from row to row. */
|
|
19
|
+
action?: Snippet;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { review = false, confirmed = false, fixed = false, href, linkLabel = 'Guidance', children, detail, links, action }: Props = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<li class="a11y-issue" class:review class:confirmed class:fixed>
|
|
26
|
+
<div class="a11y-issue-body">
|
|
27
|
+
<div class="a11y-issue-head">
|
|
28
|
+
<p class="a11y-issue-text">{@render children()}</p>
|
|
29
|
+
{#if links || href}
|
|
30
|
+
<div class="a11y-issue-links">
|
|
31
|
+
{@render links?.()}
|
|
32
|
+
{#if href}
|
|
33
|
+
<a
|
|
34
|
+
class="a11y-link a11y-issue-guidance"
|
|
35
|
+
{href}
|
|
36
|
+
target="_blank"
|
|
37
|
+
rel="noopener"
|
|
38
|
+
title={href}
|
|
39
|
+
aria-label={`${linkLabel} (opens in a new tab)`}
|
|
40
|
+
>
|
|
41
|
+
{linkLabel}
|
|
42
|
+
<i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
|
43
|
+
</a>
|
|
44
|
+
{/if}
|
|
45
|
+
</div>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
{#if detail}
|
|
49
|
+
<p class="a11y-issue-detail">{@render detail()}</p>
|
|
50
|
+
{/if}
|
|
51
|
+
</div>
|
|
52
|
+
{#if action}
|
|
53
|
+
<div class="a11y-issue-action">{@render action()}</div>
|
|
54
|
+
{/if}
|
|
55
|
+
</li>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.a11y-issue {
|
|
59
|
+
display: flex;
|
|
60
|
+
gap: var(--ui-space-16);
|
|
61
|
+
padding: var(--ui-space-12) 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.a11y-issue-body {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex: 1;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: var(--ui-space-4);
|
|
69
|
+
min-width: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.a11y-issue-action {
|
|
73
|
+
flex: none;
|
|
74
|
+
align-self: flex-start;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.a11y-issue + :global(.a11y-issue) {
|
|
78
|
+
border-top: 1px solid var(--ui-border-low);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.a11y-issue-head {
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-wrap: wrap;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: var(--ui-space-8) var(--ui-space-16);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.a11y-issue-text,
|
|
89
|
+
.a11y-issue-detail {
|
|
90
|
+
margin: 0;
|
|
91
|
+
overflow-wrap: anywhere;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.a11y-issue-text {
|
|
95
|
+
flex: 1 1 16rem;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
font-size: var(--ui-font-size-md);
|
|
98
|
+
color: var(--ui-highlight);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.review .a11y-issue-text {
|
|
102
|
+
color: var(--ui-text-secondary);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.confirmed .a11y-issue-text {
|
|
106
|
+
color: var(--ui-text-tertiary);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.fixed .a11y-issue-text {
|
|
110
|
+
color: var(--ui-fixed);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.a11y-issue-detail {
|
|
114
|
+
font-size: var(--ui-font-size-sm);
|
|
115
|
+
color: var(--ui-text-tertiary);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.a11y-issue-links {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: var(--ui-space-6);
|
|
123
|
+
margin-left: auto;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.a11y-issue-links :global(.a11y-link) {
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: var(--ui-space-6);
|
|
130
|
+
padding: var(--ui-space-2) var(--ui-space-10);
|
|
131
|
+
background: none;
|
|
132
|
+
border: 1px solid var(--ui-border);
|
|
133
|
+
border-radius: var(--ui-radius-full);
|
|
134
|
+
font: inherit;
|
|
135
|
+
font-size: var(--ui-font-size-sm);
|
|
136
|
+
color: var(--ui-text-secondary);
|
|
137
|
+
text-decoration: none;
|
|
138
|
+
white-space: nowrap;
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
transition: background var(--ui-transition-fast), border-color var(--ui-transition-fast), color var(--ui-transition-fast);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.a11y-issue-links :global(.a11y-link:hover) {
|
|
144
|
+
background: var(--ui-surface-higher);
|
|
145
|
+
border-color: var(--ui-border-high);
|
|
146
|
+
color: var(--ui-text-primary);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.a11y-issue-links :global(.a11y-link i) {
|
|
150
|
+
font-size: var(--ui-font-size-xs);
|
|
151
|
+
}
|
|
152
|
+
</style>
|