@motion-proto/live-tokens 0.98.2 → 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.
Files changed (69) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/bin/contractRunner.mjs +2 -2
  3. package/bin/rules/a11y.mjs +1 -1
  4. package/dist-plugin/index.cjs +71 -15
  5. package/dist-plugin/index.js +71 -15
  6. package/package.json +1 -1
  7. package/src/editor/bootstrap.ts +3 -1
  8. package/src/editor/component-editor/SectionDividerEditor.svelte +9 -10
  9. package/src/editor/core/a11y/a11yReport.ts +85 -22
  10. package/src/editor/core/a11y/a11yReportTypes.ts +11 -3
  11. package/src/editor/core/a11y/contrastFixes.ts +41 -0
  12. package/src/editor/core/palettes/contrastPairs.ts +4 -1
  13. package/src/editor/docs/content/accessibility.md +67 -18
  14. package/src/editor/docs/content/editing-tokens.md +4 -0
  15. package/src/editor/docs/content.generated.ts +2 -2
  16. package/src/editor/pages/ComponentEditorPage.svelte +4 -3
  17. package/src/editor/pages/EditorShell.svelte +10 -8
  18. package/src/editor/styles/ui-editor.css +2 -0
  19. package/src/editor/ui/a11y/A11yByEye.svelte +264 -0
  20. package/src/editor/ui/a11y/A11yComponentIssues.svelte +34 -16
  21. package/src/editor/ui/a11y/A11yFindingRow.svelte +66 -4
  22. package/src/editor/ui/a11y/A11yIssueRow.svelte +55 -24
  23. package/src/editor/ui/a11y/A11yOverview.svelte +72 -68
  24. package/src/editor/ui/a11y/A11yPanel.svelte +29 -4
  25. package/src/editor/ui/a11y/A11yPropertyMark.svelte +8 -8
  26. package/src/editor/ui/a11y/A11yRailMark.svelte +11 -4
  27. package/src/editor/ui/a11y/AccessibilityTab.svelte +22 -35
  28. package/src/editor/ui/a11y/a11yView.ts +50 -11
  29. package/src/live-tokens/data/colors-and-type/halloween.json +1 -1
  30. package/src/live-tokens/data/themes/autumn.json +16 -16
  31. package/src/live-tokens/data/themes/halloween.json +17 -17
  32. package/src/live-tokens/data/themes/midnight-study.json +16 -16
  33. package/src/live-tokens/data/themes/ocean.json +16 -16
  34. package/src/live-tokens/data/themes/royal-velvet.json +16 -16
  35. package/src/live-tokens/data/themes/sketchy.json +16 -16
  36. package/src/live-tokens/data/themes/spring-meadow.json +16 -16
  37. package/src/live-tokens/data/themes/sunset.json +16 -16
  38. package/src/system/components/Button.svelte +3 -3
  39. package/src/system/components/InlineEditActions.svelte +1 -1
  40. package/src/system/components/Input.svelte +1 -1
  41. package/src/system/components/MenuSelect.svelte +1 -1
  42. package/src/system/components/ProgressBar.svelte +1 -1
  43. package/src/system/components/SlidePager.svelte +1 -1
  44. package/src/system/components/Slider.svelte +6 -6
  45. package/src/system/components/TabBar.svelte +1 -1
  46. package/src/system/components/Toggle.svelte +1 -1
  47. package/src/testing-js/{chunk-RWH46IU5.js → chunk-7QXUJRSO.js} +2 -2
  48. package/src/testing-js/chunk-GZKUYLDW.js +193 -0
  49. package/src/testing-js/chunk-GZKUYLDW.js.map +1 -0
  50. package/src/testing-js/{chunk-SHLPDNAQ.js → chunk-RTZQLW3F.js} +172 -181
  51. package/src/testing-js/chunk-RTZQLW3F.js.map +1 -0
  52. package/src/testing-js/{chunk-5HNZHIHG.js → chunk-RWOT2XDB.js} +2 -2
  53. package/src/testing-js/{chunk-5HNZHIHG.js.map → chunk-RWOT2XDB.js.map} +1 -1
  54. package/src/testing-js/{chunk-R54I7YL6.js → chunk-THWGMSRH.js} +2 -2
  55. package/src/testing-js/component-a11y.contract.js +53 -23
  56. package/src/testing-js/component-a11y.contract.js.map +1 -1
  57. package/src/testing-js/component-behavior.contract.js +1 -1
  58. package/src/testing-js/component-editor.contract.js +3 -3
  59. package/src/testing-js/component-render.contract.js +3 -3
  60. package/src/testing-js/index.js +4 -4
  61. package/src/testing-js/index.js.map +1 -1
  62. package/src/testing-js/page-compliance.contract.js +3 -3
  63. package/src/testing-js/vitest.js +2 -2
  64. package/template/_gitignore +3 -0
  65. package/src/testing-js/chunk-EWSGE27V.js +0 -168
  66. package/src/testing-js/chunk-EWSGE27V.js.map +0 -1
  67. package/src/testing-js/chunk-SHLPDNAQ.js.map +0 -1
  68. /package/src/testing-js/{chunk-RWH46IU5.js.map → chunk-7QXUJRSO.js.map} +0 -0
  69. /package/src/testing-js/{chunk-R54I7YL6.js.map → chunk-THWGMSRH.js.map} +0 -0
@@ -1,12 +1,14 @@
1
1
  <script lang="ts">
2
- import { issueTotals, issuesLabel, type A11yComponentRow, type A11yIssueTotals } from './a11yView';
2
+ import { issueTotals, type A11yComponentRow, type A11yIssueTotals } from './a11yView';
3
3
 
4
4
  interface Props {
5
5
  rows: A11yComponentRow[];
6
6
  onjump: (component: string) => void;
7
+ /** Scrolls to the By eye list. */
8
+ onreview?: () => void;
7
9
  }
8
10
 
9
- let { rows, onjump }: Props = $props();
11
+ let { rows, onjump, onreview }: Props = $props();
10
12
 
11
13
  const KINDS: { key: keyof A11yIssueTotals; label: string; column: string }[] = [
12
14
  { key: 'property', label: 'Fix with a property', column: 'Property' },
@@ -14,15 +16,25 @@
14
16
  { key: 'review', label: 'Check by eye', column: 'By eye' },
15
17
  ];
16
18
 
17
- let tallies = $derived(rows.map((row) => ({ row, totals: issueTotals(row.issues) })));
19
+ let tallies = $derived(
20
+ rows
21
+ .map((row) => ({ row, totals: issueTotals(row.issues) }))
22
+ .sort(
23
+ (a, b) =>
24
+ b.totals.property - a.totals.property ||
25
+ b.totals.code - a.totals.code ||
26
+ a.row.label.localeCompare(b.row.label),
27
+ ),
28
+ );
18
29
  let sums = $derived(
19
30
  tallies.reduce(
20
31
  (acc, { totals }) => ({ property: acc.property + totals.property, code: acc.code + totals.code, review: acc.review + totals.review }),
21
32
  { property: 0, code: 0, review: 0 },
22
33
  ),
23
34
  );
24
- let total = $derived(sums.property + sums.code + sums.review);
25
- let widest = $derived(Math.max(1, ...rows.map((row) => row.count)));
35
+ let total = $derived(sums.property + sums.code);
36
+ let withIssues = $derived(rows.filter((row) => row.count > 0).length);
37
+ let fixed = $derived(rows.reduce((sum, row) => sum + row.issues.fixed.length, 0));
26
38
  </script>
27
39
 
28
40
  <section class="editor-section-card a11y-overview" aria-label="Overview">
@@ -30,17 +42,30 @@
30
42
  <div class="a11y-stat a11y-stat-total">
31
43
  <span class="a11y-stat-value">{total}</span>
32
44
  <span class="a11y-stat-label">
33
- {total === 1 ? 'issue' : 'issues'} in {rows.length} {rows.length === 1 ? 'component' : 'components'}
45
+ {total === 1 ? 'issue' : 'issues'} in {withIssues} {withIssues === 1 ? 'component' : 'components'}
34
46
  </span>
35
47
  </div>
36
48
  {#each KINDS as kind (kind.key)}
37
- <div class="a11y-stat">
38
- <span class="a11y-stat-value" class:zero={sums[kind.key] === 0}>{sums[kind.key]}</span>
49
+ {#if kind.key === 'review' && onreview}
50
+ <button type="button" class="a11y-stat a11y-stat-jump" onclick={onreview}>
51
+ <span class="a11y-stat-value" class:zero={sums[kind.key] === 0}>{sums[kind.key]}</span>
52
+ <span class="a11y-stat-label">{kind.label} <i class="fas fa-arrow-down" aria-hidden="true"></i></span>
53
+ </button>
54
+ {:else}
55
+ <div class="a11y-stat">
56
+ <span class="a11y-stat-value" class:zero={sums[kind.key] === 0}>{sums[kind.key]}</span>
57
+ <span class="a11y-stat-label">{kind.label}</span>
58
+ </div>
59
+ {/if}
60
+ {/each}
61
+ {#if fixed > 0}
62
+ <div class="a11y-stat a11y-stat-fixed">
63
+ <span class="a11y-stat-value">{fixed}</span>
39
64
  <span class="a11y-stat-label">
40
- <span class="a11y-swatch {kind.key}" aria-hidden="true"></span>{kind.label}
65
+ <i class="fa-solid fa-check" aria-hidden="true"></i>Fixed since the run
41
66
  </span>
42
67
  </div>
43
- {/each}
68
+ {/if}
44
69
  </div>
45
70
 
46
71
  <table class="a11y-table">
@@ -50,7 +75,9 @@
50
75
  {#each KINDS as kind (kind.key)}
51
76
  <th scope="col" class="num">{kind.column}</th>
52
77
  {/each}
53
- <th scope="col" class="bar-col"><span class="a11y-hidden">Issues</span></th>
78
+ {#if fixed > 0}
79
+ <th scope="col" class="num">Fixed</th>
80
+ {/if}
54
81
  </tr>
55
82
  </thead>
56
83
  <tbody>
@@ -64,15 +91,9 @@
64
91
  {#each KINDS as kind (kind.key)}
65
92
  <td class="num" class:zero={totals[kind.key] === 0}>{totals[kind.key]}</td>
66
93
  {/each}
67
- <td class="bar-col">
68
- <span class="a11y-bar" style:width={`${(row.count / widest) * 100}%`} title={issuesLabel(row.count)}>
69
- {#each KINDS as kind (kind.key)}
70
- {#if totals[kind.key] > 0}
71
- <span class="a11y-bar-part {kind.key}" style:flex-grow={totals[kind.key]}></span>
72
- {/if}
73
- {/each}
74
- </span>
75
- </td>
94
+ {#if fixed > 0}
95
+ <td class="num fixed" class:zero={row.issues.fixed.length === 0}>{row.issues.fixed.length}</td>
96
+ {/if}
76
97
  </tr>
77
98
  {/each}
78
99
  </tbody>
@@ -98,6 +119,33 @@
98
119
  border-left: 1px solid var(--ui-border-low);
99
120
  }
100
121
 
122
+ .a11y-stat-jump {
123
+ margin: 0;
124
+ background: none;
125
+ border-top: none;
126
+ border-right: none;
127
+ border-bottom: none;
128
+ font: inherit;
129
+ text-align: left;
130
+ cursor: pointer;
131
+ }
132
+
133
+ .a11y-stat-jump i {
134
+ font-size: var(--ui-font-size-xs);
135
+ opacity: 0;
136
+ transition: opacity var(--ui-transition-fast);
137
+ }
138
+
139
+ .a11y-stat-jump:hover .a11y-stat-label,
140
+ .a11y-stat-jump:focus-visible .a11y-stat-label {
141
+ color: var(--ui-text-primary);
142
+ }
143
+
144
+ .a11y-stat-jump:hover i,
145
+ .a11y-stat-jump:focus-visible i {
146
+ opacity: 1;
147
+ }
148
+
101
149
  .a11y-stat-total {
102
150
  padding-left: 0;
103
151
  border-left: none;
@@ -127,23 +175,10 @@
127
175
  color: var(--ui-text-muted);
128
176
  }
129
177
 
130
- .a11y-swatch {
131
- flex: none;
132
- width: var(--ui-space-8);
133
- height: var(--ui-space-8);
134
- border-radius: var(--ui-radius-full);
135
- }
136
-
137
- .property {
138
- background: var(--ui-text-primary);
139
- }
140
-
141
- .code {
142
- background: var(--ui-border-high);
143
- }
144
-
145
- .review {
146
- border: 1px solid var(--ui-border-high);
178
+ .a11y-stat-fixed .a11y-stat-value,
179
+ .a11y-stat-fixed i,
180
+ .a11y-table td.fixed:not(.zero) {
181
+ color: var(--ui-fixed);
147
182
  }
148
183
 
149
184
  .a11y-table {
@@ -185,10 +220,6 @@
185
220
  color: var(--ui-text-muted);
186
221
  }
187
222
 
188
- .bar-col {
189
- width: 40%;
190
- }
191
-
192
223
  .a11y-jump {
193
224
  display: inline-flex;
194
225
  align-items: center;
@@ -212,31 +243,4 @@
212
243
  text-decoration: underline;
213
244
  text-underline-offset: var(--ui-space-2);
214
245
  }
215
-
216
- .a11y-bar {
217
- display: flex;
218
- gap: var(--ui-space-2);
219
- height: var(--ui-space-8);
220
- min-width: var(--ui-space-8);
221
- }
222
-
223
- .a11y-bar-part {
224
- flex-basis: 0;
225
- border-radius: var(--ui-radius-full);
226
- }
227
-
228
- .a11y-hidden {
229
- position: absolute;
230
- width: 1px;
231
- height: 1px;
232
- overflow: hidden;
233
- clip-path: inset(50%);
234
- white-space: nowrap;
235
- }
236
-
237
- @media (max-width: 600px) {
238
- .bar-col {
239
- display: none;
240
- }
241
- }
242
246
  </style>
@@ -4,7 +4,14 @@
4
4
  import { FLASH_DURATION_MS } from '../../core/flashStatus';
5
5
  import { getEditorContext, type RevealRequest } from '../../component-editor/scaffolding/editorContext';
6
6
  import A11yComponentIssues from './A11yComponentIssues.svelte';
7
- import { accessibilityIssuesLabel, formatRunTime, issueCount, pendingA11yReveal } from './a11yView';
7
+ import {
8
+ accessibilityIssuesLabel,
9
+ fixedLabel,
10
+ formatRunTime,
11
+ issueCount,
12
+ pendingA11yReveal,
13
+ reviewLabel,
14
+ } from './a11yView';
8
15
 
9
16
  let { component }: { component: string } = $props();
10
17
  const titleId = $props.id();
@@ -14,6 +21,8 @@
14
21
  let panel: HTMLElement | undefined = $state();
15
22
  let issues = $derived($a11yIssues[component]);
16
23
  let count = $derived(issues ? issueCount(issues) : 0);
24
+ let fixedCount = $derived(issues?.fixed.length ?? 0);
25
+ let reviewCount = $derived(issues?.review.length ?? 0);
17
26
 
18
27
  /** Rendered rows for `variable`. The linked block can repeat a variant's row, so the variant's own row comes first. */
19
28
  function renderedRows(variable: string): HTMLElement[] {
@@ -70,11 +79,19 @@
70
79
  });
71
80
  </script>
72
81
 
73
- {#if issues && count > 0 && $a11yFinishedReport}
82
+ {#if issues && count + fixedCount + reviewCount > 0 && $a11yFinishedReport}
74
83
  <section class="editor-section-card a11y-panel" aria-labelledby={titleId} bind:this={panel}>
75
84
  <header class="a11y-panel-head">
76
- <i class="fa-solid fa-triangle-exclamation a11y-panel-mark" aria-hidden="true"></i>
77
- <h3 id={titleId} class="a11y-panel-title">{accessibilityIssuesLabel(count)}</h3>
85
+ {#if count > 0}
86
+ <i class="fa-solid fa-triangle-exclamation a11y-panel-mark" aria-hidden="true"></i>
87
+ <h3 id={titleId} class="a11y-panel-title">{accessibilityIssuesLabel(count)}</h3>
88
+ {:else if reviewCount > 0}
89
+ <i class="fa-solid fa-eye a11y-panel-mark review" aria-hidden="true"></i>
90
+ <h3 id={titleId} class="a11y-panel-title">{reviewLabel(reviewCount)}</h3>
91
+ {:else}
92
+ <i class="fa-solid fa-check a11y-panel-mark fixed" aria-hidden="true"></i>
93
+ <h3 id={titleId} class="a11y-panel-title">{fixedLabel(fixedCount)}</h3>
94
+ {/if}
78
95
  <span class="a11y-panel-run">Last run {formatRunTime($a11yFinishedReport.finishedAt)}</span>
79
96
  </header>
80
97
  <A11yComponentIssues {component} {issues} withReview onreveal={reveal} />
@@ -113,6 +130,14 @@
113
130
  color: var(--ui-highlight);
114
131
  }
115
132
 
133
+ .a11y-panel-mark.fixed {
134
+ color: var(--ui-fixed);
135
+ }
136
+
137
+ .a11y-panel-mark.review {
138
+ color: var(--ui-text-tertiary);
139
+ }
140
+
116
141
  .a11y-panel-title {
117
142
  margin: 0;
118
143
  font-size: var(--ui-font-size-lg);
@@ -13,8 +13,12 @@
13
13
  let { variables, placement = 'inline' }: Props = $props();
14
14
 
15
15
  let findings = $derived([...new Set(variables.flatMap((variable) => $a11yPropertyFindings.get(variable) ?? []))]);
16
- let review = $derived(findings.every((finding) => finding.rule === 'a11y-review'));
17
- let label = $derived(review ? reviewLabel(findings.length) : accessibilityIssuesLabel(findings.length));
16
+ let reviewCount = $derived(findings.filter((finding) => finding.rule === 'a11y-review').length);
17
+ let review = $derived(reviewCount === findings.length);
18
+ let label = $derived([
19
+ review ? '' : accessibilityIssuesLabel(findings.length - reviewCount),
20
+ reviewCount > 0 ? reviewLabel(reviewCount) : '',
21
+ ].filter(Boolean).join(', '));
18
22
  let rows = $derived(findingRows([
19
23
  ...findings.filter((finding) => finding.rule !== 'a11y-review'),
20
24
  ...findings.filter((finding) => finding.rule === 'a11y-review'),
@@ -26,7 +30,7 @@
26
30
  <UIInfoPopover title={label} ariaLabel={label}>
27
31
  {#snippet trigger()}
28
32
  {#if review}
29
- <span class="a11y-property-mark-glyph review" aria-hidden="true"></span>
33
+ <i class="fa-solid fa-eye a11y-property-mark-glyph review" aria-hidden="true"></i>
30
34
  {:else}
31
35
  <i class="fa-solid fa-triangle-exclamation a11y-property-mark-glyph" aria-hidden="true"></i>
32
36
  {/if}
@@ -63,12 +67,8 @@
63
67
  font-size: var(--ui-font-size-xs);
64
68
  }
65
69
 
66
- /* The "?" is generated content because the contract harness matches rows by their text. */
67
- .a11y-property-mark-glyph.review::after {
68
- content: '?';
70
+ .a11y-property-mark-glyph.review {
69
71
  color: var(--ui-text-tertiary);
70
- font-size: var(--ui-font-size-sm);
71
- font-weight: var(--ui-font-weight-bold);
72
72
  }
73
73
 
74
74
  .a11y-property-findings {
@@ -2,16 +2,19 @@
2
2
  interface Props {
3
3
  /** `corner` overlaps the icon of a condensed rail, where the label and its inline mark are hidden. */
4
4
  placement?: 'inline' | 'corner';
5
+ /** Draws the eye of a component whose only results are to check by eye. */
6
+ review?: boolean;
5
7
  }
6
8
 
7
- let { placement = 'inline' }: Props = $props();
8
- const label = 'Accessibility issues';
9
+ let { placement = 'inline', review = false }: Props = $props();
10
+ let label = $derived(review ? 'Results to check by eye' : 'Accessibility issues');
11
+ let glyph = $derived(review ? 'fa-eye' : 'fa-triangle-exclamation');
9
12
  </script>
10
13
 
11
14
  {#if placement === 'inline'}
12
- <i class="fa-solid fa-triangle-exclamation a11y-rail-mark" role="img" aria-label={label} title={label}></i>
15
+ <i class="fa-solid {glyph} a11y-rail-mark" class:review role="img" aria-label={label} title={label}></i>
13
16
  {:else}
14
- <i class="fa-solid fa-triangle-exclamation a11y-rail-mark corner" aria-hidden="true"></i>
17
+ <i class="fa-solid {glyph} a11y-rail-mark corner" class:review aria-hidden="true"></i>
15
18
  {/if}
16
19
 
17
20
  <style>
@@ -21,6 +24,10 @@
21
24
  font-size: var(--ui-font-size-sm);
22
25
  }
23
26
 
27
+ .review {
28
+ color: var(--ui-text-tertiary);
29
+ }
30
+
24
31
  /* Bottom-right of the icon, clear of the unsaved dot at its top-right. */
25
32
  .corner {
26
33
  position: absolute;
@@ -6,15 +6,17 @@
6
6
  import { unsavedEdits } from '../../core/store/unsavedEdits';
7
7
  import { listThemes } from '../../core/themes/themeService';
8
8
  import UIPillButton from '../UIPillButton.svelte';
9
+ import A11yByEye from './A11yByEye.svelte';
9
10
  import A11yComponentIssues from './A11yComponentIssues.svelte';
10
- import A11yFindingRow from './A11yFindingRow.svelte';
11
11
  import A11yOverview from './A11yOverview.svelte';
12
12
  import {
13
13
  A11Y_REVIEW_SECTION,
14
14
  A11Y_RUN_SECTION,
15
15
  a11ySectionId,
16
16
  codeSpans,
17
- componentsWithIssues,
17
+ componentSections,
18
+ componentsInReport,
19
+ fixedLabel,
18
20
  formatElapsed,
19
21
  formatRunTime,
20
22
  getA11yViewLink,
@@ -26,9 +28,9 @@
26
28
 
27
29
  const openView = getA11yViewLink();
28
30
 
29
- let rows = $derived(componentsWithIssues($a11yIssues));
31
+ let tableRows = $derived(componentsInReport($a11yIssues));
32
+ let sectionRows = $derived(componentSections($a11yIssues));
30
33
  let review = $derived(reviewResults($a11yIssues));
31
- let reviewTotal = $derived(rows.reduce((sum, row) => sum + row.issues.review.length, 0));
32
34
  let running = $derived($a11yReport?.state === 'running' ? $a11yReport : null);
33
35
  let failed = $derived($a11yReport?.state === 'failed' ? $a11yReport : null);
34
36
  let finished = $derived($a11yFinishedReport);
@@ -128,7 +130,7 @@
128
130
  </div>
129
131
  {/if}
130
132
  {/if}
131
- {#if !notInstalled && rows.length === 0}
133
+ {#if !notInstalled && sectionRows.length === 0}
132
134
  <p class="a11y-summary">Every component passes the automated checks.</p>
133
135
  {/if}
134
136
  {:else if !running && !failed}
@@ -137,21 +139,28 @@
137
139
  Run test checks every component in the open theme as saved. It scans each one with axe and Alfa,
138
140
  presses the keys each one answers, and measures the contrast of every color pair.
139
141
  </p>
140
- <p>A run takes about three minutes. The report stays here until the next run.</p>
142
+ <p>A run takes about three minutes. Each theme keeps its report until its next run.</p>
141
143
  </div>
142
144
  {/if}
143
145
  </section>
144
146
 
145
- {#if rows.length > 0}
146
- <A11yOverview {rows} onjump={(component) => openView?.(a11ySectionId(component))} />
147
+ {#if tableRows.length > 0}
148
+ <A11yOverview
149
+ rows={tableRows}
150
+ onjump={(component) => openView?.(a11ySectionId(component))}
151
+ onreview={() => openView?.(A11Y_REVIEW_SECTION)}
152
+ />
147
153
  {/if}
148
154
 
149
- {#each rows as row (row.component)}
155
+ {#each sectionRows as row (row.component)}
150
156
  <section id={a11ySectionId(row.component)} class="editor-section-card a11y-component">
151
157
  <header class="a11y-component-head">
152
158
  <i class={row.icon} aria-hidden="true"></i>
153
159
  <h3 class="editor-subsection-title">{row.label}</h3>
154
- <span class="a11y-count">{issuesLabel(row.count)}</span>
160
+ <span class="a11y-count">{[
161
+ row.count > 0 ? issuesLabel(row.count) : '',
162
+ row.issues.fixed.length > 0 ? fixedLabel(row.issues.fixed.length) : '',
163
+ ].filter(Boolean).join(', ')}</span>
155
164
  <UIPillButton
156
165
  size="compact"
157
166
  variant="outline"
@@ -166,32 +175,16 @@
166
175
  onreveal={(variable) => openComponentAt(row.component, variable)}
167
176
  onopen={() => openComponentAt(row.component)}
168
177
  />
169
- {#if row.issues.review.length > 0}
178
+ {#if row.review > 0}
170
179
  <button type="button" class="a11y-review-link" onclick={() => openView?.(A11Y_REVIEW_SECTION)}>
171
- {reviewLabel(row.issues.review.length)}
180
+ {reviewLabel(row.review)}
172
181
  </button>
173
182
  {/if}
174
183
  </section>
175
184
  {/each}
176
185
 
177
186
  {#if review.length > 0}
178
- <section id={A11Y_REVIEW_SECTION} class="a11y-review">
179
- <details class="a11y-disclosure">
180
- <summary>{reviewLabel(reviewTotal)}</summary>
181
- <ul class="a11y-review-list">
182
- {#each review as { component, label, row }, i (i)}
183
- <A11yFindingRow
184
- {component}
185
- {row}
186
- review
187
- prefix={label}
188
- onreveal={(variable) => openComponentAt(component, variable)}
189
- onopen={() => openComponentAt(component)}
190
- />
191
- {/each}
192
- </ul>
193
- </details>
194
- </section>
187
+ <A11yByEye />
195
188
  {/if}
196
189
  </div>
197
190
 
@@ -321,12 +314,6 @@
321
314
  color: var(--ui-text-primary);
322
315
  }
323
316
 
324
- .a11y-review-list {
325
- margin: var(--ui-space-8) 0 0;
326
- padding: 0;
327
- list-style: none;
328
- }
329
-
330
317
  .a11y-tab code {
331
318
  font-family: var(--ui-font-mono);
332
319
  font-size: var(--ui-font-size-sm);
@@ -84,18 +84,24 @@ export function accessibilityIssuesLabel(count: number): string {
84
84
  return `${count} accessibility ${count === 1 ? 'issue' : 'issues'}`;
85
85
  }
86
86
 
87
+ export function fixedLabel(count: number): string {
88
+ return `${count} ${count === 1 ? 'issue' : 'issues'} fixed`;
89
+ }
90
+
87
91
  export function reviewLabel(count: number): string {
88
92
  return `${count} ${count === 1 ? 'result' : 'results'} to check by eye`;
89
93
  }
90
94
 
91
- /** Every finding of the run counts, results to check by eye included (decision 6). */
95
+ /** The findings a fix clears. A result to check by eye counts apart, since only a person can settle it. */
92
96
  export function issueCount(issues: ComponentA11yIssues): number {
93
- return issues.contrast.length + issues.property.length + issues.code.length + issues.review.length;
97
+ return issues.contrast.length + issues.property.length + issues.code.length;
94
98
  }
95
99
 
96
100
  /** One panel row: the findings that share an engine rule, a help text and the properties they trace to. */
97
101
  export interface A11yFindingRow {
98
102
  finding: A11yFinding;
103
+ /** Every finding the row merges, `finding` first. */
104
+ findings: A11yFinding[];
99
105
  targets: string[];
100
106
  views: string;
101
107
  variables: string[];
@@ -131,6 +137,7 @@ export function findingRows(findings: A11yFinding[]): A11yFindingRow[] {
131
137
  }
132
138
  return [...rows.values()].map((group) => ({
133
139
  finding: group[0],
140
+ findings: group,
134
141
  targets: [...new Set(group.flatMap((f) => (f.details ? [f.details.target] : [])))],
135
142
  views: group.length === 1 ? (group[0].details?.view ?? '') : mergeViews(group.map((f) => f.details?.view)),
136
143
  variables: group[0].details?.variables ?? [],
@@ -155,24 +162,56 @@ export interface A11yComponentRow {
155
162
  component: string;
156
163
  label: string;
157
164
  icon: string;
165
+ /** `issueCount`: the findings a fix clears. */
158
166
  count: number;
167
+ /** The results to check by eye no one has marked. */
168
+ review: number;
159
169
  issues: ComponentA11yIssues;
160
170
  }
161
171
 
162
- /** Every registered component with a finding, in the component rail's order. */
163
- export function componentsWithIssues(all: Record<string, ComponentA11yIssues>): A11yComponentRow[] {
172
+ function componentRows(
173
+ all: Record<string, ComponentA11yIssues>,
174
+ listed: (row: A11yComponentRow) => boolean,
175
+ ): A11yComponentRow[] {
164
176
  return getComponentRegistryEntries().flatMap(({ id, label, icon }) => {
165
177
  const issues = all[id];
166
- const count = issues ? issueCount(issues) : 0;
167
- return count > 0 ? [{ component: id, label, icon, count, issues }] : [];
178
+ if (!issues) return [];
179
+ const row = { component: id, label, icon, count: issueCount(issues), review: issues.review.length, issues };
180
+ return listed(row) ? [row] : [];
168
181
  });
169
182
  }
170
183
 
171
- /** Every result to check by eye, merged into rows per component, in the rail's order. */
172
- export function reviewResults(all: Record<string, ComponentA11yIssues>): { component: string; label: string; row: A11yFindingRow }[] {
173
- return getComponentRegistryEntries().flatMap(({ id, label }) =>
174
- findingRows(all[id]?.review ?? []).map((row) => ({ component: id, label, row })),
175
- );
184
+ /** Every registered component with an issue or an unmarked result to check by eye, in the component rail's order. */
185
+ export function componentsWithIssues(all: Record<string, ComponentA11yIssues>): A11yComponentRow[] {
186
+ return componentRows(all, (row) => row.count > 0 || row.review > 0);
187
+ }
188
+
189
+ /** Every registered component the overview table lists: an issue, a fix, or an unmarked result to check by eye.
190
+ * A result marked as looking right is acknowledged, so it alone lists nothing. */
191
+ export function componentsInReport(all: Record<string, ComponentA11yIssues>): A11yComponentRow[] {
192
+ return componentRows(all, (row) => row.count > 0 || row.review > 0 || row.issues.fixed.length > 0);
193
+ }
194
+
195
+ /** Every component with its own section in the Accessibility view: an issue or a fix. The By eye list holds the rest. */
196
+ export function componentSections(all: Record<string, ComponentA11yIssues>): A11yComponentRow[] {
197
+ return componentRows(all, (row) => row.count > 0 || row.issues.fixed.length > 0);
198
+ }
199
+
200
+ export interface A11yReviewResult {
201
+ component: string;
202
+ label: string;
203
+ row: A11yFindingRow;
204
+ confirmed: boolean;
205
+ }
206
+
207
+ /** Every result to check by eye, merged into rows per component in the rail's order, the unmarked ones first. */
208
+ export function reviewResults(all: Record<string, ComponentA11yIssues>): A11yReviewResult[] {
209
+ const entries = getComponentRegistryEntries();
210
+ const results = (confirmed: boolean) =>
211
+ entries.flatMap(({ id, label }) =>
212
+ findingRows((confirmed ? all[id]?.confirmed : all[id]?.review) ?? []).map((row) => ({ component: id, label, row, confirmed })),
213
+ );
214
+ return [...results(false), ...results(true)];
176
215
  }
177
216
 
178
217
  /** The words of `variable` after the component's prefix that `label` lacks,
@@ -897,7 +897,7 @@
897
897
  },
898
898
  {
899
899
  "position": 100,
900
- "paletteLabel": "700"
900
+ "paletteLabel": "800"
901
901
  }
902
902
  ],
903
903
  "gradientSize": "window"