@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.
- package/CHANGELOG.md +67 -0
- package/bin/contractRunner.mjs +2 -2
- package/bin/rules/a11y.mjs +1 -1
- package/dist-plugin/index.cjs +71 -15
- package/dist-plugin/index.js +71 -15
- package/package.json +1 -1
- package/src/editor/bootstrap.ts +3 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +9 -10
- package/src/editor/core/a11y/a11yReport.ts +85 -22
- package/src/editor/core/a11y/a11yReportTypes.ts +11 -3
- package/src/editor/core/a11y/contrastFixes.ts +41 -0
- package/src/editor/core/palettes/contrastPairs.ts +4 -1
- package/src/editor/docs/content/accessibility.md +67 -18
- package/src/editor/docs/content/editing-tokens.md +4 -0
- package/src/editor/docs/content.generated.ts +2 -2
- package/src/editor/pages/ComponentEditorPage.svelte +4 -3
- package/src/editor/pages/EditorShell.svelte +10 -8
- package/src/editor/styles/ui-editor.css +2 -0
- package/src/editor/ui/a11y/A11yByEye.svelte +264 -0
- package/src/editor/ui/a11y/A11yComponentIssues.svelte +34 -16
- package/src/editor/ui/a11y/A11yFindingRow.svelte +66 -4
- package/src/editor/ui/a11y/A11yIssueRow.svelte +55 -24
- package/src/editor/ui/a11y/A11yOverview.svelte +72 -68
- package/src/editor/ui/a11y/A11yPanel.svelte +29 -4
- package/src/editor/ui/a11y/A11yPropertyMark.svelte +8 -8
- package/src/editor/ui/a11y/A11yRailMark.svelte +11 -4
- package/src/editor/ui/a11y/AccessibilityTab.svelte +22 -35
- package/src/editor/ui/a11y/a11yView.ts +50 -11
- package/src/live-tokens/data/colors-and-type/halloween.json +1 -1
- package/src/live-tokens/data/themes/autumn.json +16 -16
- package/src/live-tokens/data/themes/halloween.json +17 -17
- package/src/live-tokens/data/themes/midnight-study.json +16 -16
- package/src/live-tokens/data/themes/ocean.json +16 -16
- package/src/live-tokens/data/themes/royal-velvet.json +16 -16
- package/src/live-tokens/data/themes/sketchy.json +16 -16
- package/src/live-tokens/data/themes/spring-meadow.json +16 -16
- package/src/live-tokens/data/themes/sunset.json +16 -16
- package/src/system/components/Button.svelte +3 -3
- package/src/system/components/InlineEditActions.svelte +1 -1
- package/src/system/components/Input.svelte +1 -1
- package/src/system/components/MenuSelect.svelte +1 -1
- package/src/system/components/ProgressBar.svelte +1 -1
- package/src/system/components/SlidePager.svelte +1 -1
- package/src/system/components/Slider.svelte +6 -6
- package/src/system/components/TabBar.svelte +1 -1
- package/src/system/components/Toggle.svelte +1 -1
- package/src/testing-js/{chunk-RWH46IU5.js → chunk-7QXUJRSO.js} +2 -2
- package/src/testing-js/chunk-GZKUYLDW.js +193 -0
- package/src/testing-js/chunk-GZKUYLDW.js.map +1 -0
- package/src/testing-js/{chunk-SHLPDNAQ.js → chunk-RTZQLW3F.js} +172 -181
- package/src/testing-js/chunk-RTZQLW3F.js.map +1 -0
- package/src/testing-js/{chunk-5HNZHIHG.js → chunk-RWOT2XDB.js} +2 -2
- package/src/testing-js/{chunk-5HNZHIHG.js.map → chunk-RWOT2XDB.js.map} +1 -1
- package/src/testing-js/{chunk-R54I7YL6.js → chunk-THWGMSRH.js} +2 -2
- package/src/testing-js/component-a11y.contract.js +53 -23
- package/src/testing-js/component-a11y.contract.js.map +1 -1
- package/src/testing-js/component-behavior.contract.js +1 -1
- package/src/testing-js/component-editor.contract.js +3 -3
- package/src/testing-js/component-render.contract.js +3 -3
- package/src/testing-js/index.js +4 -4
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +3 -3
- package/src/testing-js/vitest.js +2 -2
- package/template/_gitignore +3 -0
- package/src/testing-js/chunk-EWSGE27V.js +0 -168
- package/src/testing-js/chunk-EWSGE27V.js.map +0 -1
- package/src/testing-js/chunk-SHLPDNAQ.js.map +0 -1
- /package/src/testing-js/{chunk-RWH46IU5.js.map → chunk-7QXUJRSO.js.map} +0 -0
- /package/src/testing-js/{chunk-R54I7YL6.js.map → chunk-THWGMSRH.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.99.0 — Accessibility reports by theme
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Each theme keeps its own accessibility report, in the data directory.**
|
|
8
|
+
The dev server writes a theme's last finished run to
|
|
9
|
+
`<dataDir>/_a11y-reports/<theme>.json`, and the editor shows the open
|
|
10
|
+
theme's report. The single report used to sit in `test-results/live-tokens/`,
|
|
11
|
+
which Playwright and other test tools clear, so a test run could erase it.
|
|
12
|
+
A failed run now leaves the theme's last report in place, and deleting a
|
|
13
|
+
theme deletes its report. `GET /a11y/report` answers `{ running, reports }`.
|
|
14
|
+
Add `_a11y-reports/` to the project's `.gitignore`; projects made with
|
|
15
|
+
`create` now ignore it. Test runs leave it out of their copy of the data
|
|
16
|
+
tree.
|
|
17
|
+
- **A contrast failure fixed since the run turns green.** The editor
|
|
18
|
+
re-measures each contrast failure of the last run as you edit. Once one
|
|
19
|
+
passes, its row in the component's panel and in the Accessibility view
|
|
20
|
+
shows the run's ratio and the ratio now, and it no longer counts as an
|
|
21
|
+
issue.
|
|
22
|
+
- **Component defaults and the shipped themes pass WCAG AA contrast.**
|
|
23
|
+
Every contrast pair the editor checks now passes in the default theme and
|
|
24
|
+
all eight presets. Slider: fill `--border-brand-medium`, thumb and hover
|
|
25
|
+
thumb `--text-primary`. Toggle: resting thumb `--text-primary`, as its
|
|
26
|
+
other states already were. ProgressBar: fill `--border-special-medium`.
|
|
27
|
+
SlidePager: hover icon `--text-primary`. TabBar: selected underline
|
|
28
|
+
`--border-brand-strong`. Input: focus ring `--border-brand-strong`.
|
|
29
|
+
Button secondary, success and warning hovers and the MenuSelect hover sit
|
|
30
|
+
one surface step lighter, and InlineEditActions' cancel hover icon is
|
|
31
|
+
`--text-primary`. Halloween's page gradient ends on canvas 800. Themes you
|
|
32
|
+
saved keep their own values.
|
|
33
|
+
- **The Accessibility overview table lists components by the fixes a theme
|
|
34
|
+
can make.** Rows sort by "Fix with a property", then by "Fix in the
|
|
35
|
+
component's code". The bar column and its colour key are gone.
|
|
36
|
+
- **Results to check by eye carry an eye.** The component list, the
|
|
37
|
+
component panel, and each property row draw a grey eye for them in place of
|
|
38
|
+
the warning triangle or the "?". They no longer count toward a component's
|
|
39
|
+
issues.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Looks right marks a result to check by eye as judged.** Each such row in
|
|
44
|
+
the Accessibility view and the component panel has the button at its right
|
|
45
|
+
edge. A marked row dims, stops counting, and drops its marks from the
|
|
46
|
+
editor. The open theme's report keeps the mark through every later run
|
|
47
|
+
that finds the same result. The dev server stores it in the report and
|
|
48
|
+
answers `PUT /a11y/confirmed` with `{ theme, messages, confirmed }`.
|
|
49
|
+
- **The By eye list gathers every result to check by eye.** It closes the
|
|
50
|
+
Accessibility view: the results to check first, under a tally and a
|
|
51
|
+
progress bar, then the marked ones in a closed group to revisit. Marking one moves focus to
|
|
52
|
+
the next. **Mark all as looking right** marks the rest in one step. A
|
|
53
|
+
result marked as looking right raises nothing elsewhere: no sidebar mark,
|
|
54
|
+
no component panel, and no property mark. A component whose results all
|
|
55
|
+
look right leaves the overview table, the component sections, and the
|
|
56
|
+
sidebar.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Contrast checks pass parts that paint nothing or only decorate.** A non-text part
|
|
61
|
+
painted fully transparent, such as an unselected tab's underline, passes.
|
|
62
|
+
Section divider hairlines are decorative and carry no contrast pair.
|
|
63
|
+
- **The focus checks judge what focus changes.** A state view that forces
|
|
64
|
+
the focused look, such as Input's, lifts it while focus is measured, so
|
|
65
|
+
SIA-R65 sees the change and the focus ring's contrast is measured. A ring
|
|
66
|
+
drawn on a sibling, as Slider draws it, settles SIA-R65's "can't tell". A
|
|
67
|
+
ring that spills past the preview is measured against `--page-bg` and no
|
|
68
|
+
longer against the editor's own surfaces.
|
|
69
|
+
|
|
3
70
|
## 0.98.2 — Accessibility testing
|
|
4
71
|
|
|
5
72
|
The v0.98.0 and v0.98.1 tags never reached npm. Release checks stopped both:
|
package/bin/contractRunner.mjs
CHANGED
|
@@ -59,7 +59,7 @@ function testsTimeoutMs() {
|
|
|
59
59
|
// Mirrors src/testing/isolation.ts's own list. Duplicated rather than shared:
|
|
60
60
|
// that module compiles into src/testing-js, which does not exist until
|
|
61
61
|
// `build:testing` runs, and this one ships unbuilt.
|
|
62
|
-
const SESSION_FILES = new Set(['_working.json', '_active.json', '_production.json']);
|
|
62
|
+
const SESSION_FILES = new Set(['_working.json', '_active.json', '_production.json', '_a11y-reports']);
|
|
63
63
|
const THEME_POINTER = join('themes', '_active.json');
|
|
64
64
|
|
|
65
65
|
const REQUIRED_TOOLS = [
|
|
@@ -229,7 +229,7 @@ export function copyIsolatedDataDir(sourceDataDir, { keepActive = false } = {})
|
|
|
229
229
|
cpSync(sourceDataDir, dataDir, { recursive: true });
|
|
230
230
|
for (const entry of readdirSync(dataDir, { recursive: true })) {
|
|
231
231
|
if (keepActive && entry === THEME_POINTER) continue;
|
|
232
|
-
if (SESSION_FILES.has(basename(entry))) rmSync(join(dataDir, entry), { force: true });
|
|
232
|
+
if (SESSION_FILES.has(basename(entry))) rmSync(join(dataDir, entry), { recursive: true, force: true });
|
|
233
233
|
}
|
|
234
234
|
return dataDir;
|
|
235
235
|
}
|
package/bin/rules/a11y.mjs
CHANGED
|
@@ -32,7 +32,7 @@ export const componentRules = {
|
|
|
32
32
|
'a11y-focus-visible': {
|
|
33
33
|
severity: 'warn',
|
|
34
34
|
repair: 'authored',
|
|
35
|
-
guidance: `Siteimprove Alfa rule SIA-R65 found no visible change when the element takes focus. ${HELP} Give the element a :focus-visible style, such as an outline, that differs from its resting style. Alfa reads only the element, its descendants, and its ancestors
|
|
35
|
+
guidance: `Siteimprove Alfa rule SIA-R65 found no visible change when the element takes focus. ${HELP} Give the element a :focus-visible style, such as an outline, that differs from its resting style. Alfa reads only the element, its descendants, and its ancestors. The suite's focus probe also reads the element's siblings and drops the finding when it finds the ring there.`,
|
|
36
36
|
},
|
|
37
37
|
'a11y-keyboard': {
|
|
38
38
|
severity: 'warn',
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -5624,7 +5624,7 @@ var import_node_child_process = require("child_process");
|
|
|
5624
5624
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
5625
5625
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
5626
5626
|
var A11Y_RUN_ARGS = ["check-component", "--tests", "--a11y", "--no-fix", "--json", "--open-theme"];
|
|
5627
|
-
var
|
|
5627
|
+
var A11Y_REPORTS_DIR_NAME = "_a11y-reports";
|
|
5628
5628
|
var STDERR_TAIL = 2e3;
|
|
5629
5629
|
var CONTRAST_ROLES = /* @__PURE__ */ new Set(["text", "non-text"]);
|
|
5630
5630
|
function spawnCli(cliPath) {
|
|
@@ -5657,12 +5657,30 @@ function parseCliReport(stdout) {
|
|
|
5657
5657
|
return null;
|
|
5658
5658
|
}
|
|
5659
5659
|
}
|
|
5660
|
-
function a11yRunServer({ root,
|
|
5660
|
+
function a11yRunServer({ root, reportsDir, openTheme, runCli, broadcast }) {
|
|
5661
5661
|
let running = null;
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
return JSON.parse(import_node_fs.default.readFileSync(reportFile, "utf-8"));
|
|
5662
|
+
const reportFile = (theme) => import_node_path2.default.join(reportsDir, `${theme}.json`);
|
|
5663
|
+
function readReport(theme) {
|
|
5664
|
+
const listed = import_node_fs.default.existsSync(reportsDir) && import_node_fs.default.readdirSync(reportsDir).includes(`${theme}.json`);
|
|
5665
|
+
return listed ? JSON.parse(import_node_fs.default.readFileSync(reportFile(theme), "utf-8")) : null;
|
|
5666
|
+
}
|
|
5667
|
+
function writeReport(report) {
|
|
5668
|
+
import_node_fs.default.mkdirSync(reportsDir, { recursive: true });
|
|
5669
|
+
import_node_fs.default.writeFileSync(reportFile(report.theme), `${JSON.stringify(report, null, 2)}
|
|
5670
|
+
`);
|
|
5671
|
+
}
|
|
5672
|
+
function withConfirmed(report, confirmed) {
|
|
5673
|
+
const review = new Set(report.findings.filter((f) => f.rule === "a11y-review").map((f) => f.message));
|
|
5674
|
+
const kept = [...new Set(confirmed)].filter((message) => review.has(message)).sort();
|
|
5675
|
+
const out = { ...report, confirmed: kept };
|
|
5676
|
+
if (kept.length === 0) delete out.confirmed;
|
|
5677
|
+
return out;
|
|
5678
|
+
}
|
|
5679
|
+
function reports() {
|
|
5680
|
+
if (!import_node_fs.default.existsSync(reportsDir)) return {};
|
|
5681
|
+
return Object.fromEntries(
|
|
5682
|
+
import_node_fs.default.readdirSync(reportsDir).filter((name) => name.endsWith(".json")).map((name) => [name.slice(0, -".json".length), JSON.parse(import_node_fs.default.readFileSync(import_node_path2.default.join(reportsDir, name), "utf-8"))])
|
|
5683
|
+
);
|
|
5666
5684
|
}
|
|
5667
5685
|
function settle(started, outcome) {
|
|
5668
5686
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5681,7 +5699,6 @@ ${stderr}` : ""}`
|
|
|
5681
5699
|
...started,
|
|
5682
5700
|
state: "finished",
|
|
5683
5701
|
finishedAt,
|
|
5684
|
-
...cli.theme ? { theme: cli.theme } : {},
|
|
5685
5702
|
findings: cli.findings.filter((f) => f.details?.engineRule !== "color-contrast")
|
|
5686
5703
|
};
|
|
5687
5704
|
}
|
|
@@ -5696,23 +5713,45 @@ ${stderr}` : ""}`
|
|
|
5696
5713
|
body: { error: "Body must be { contrast: [...] } with one entry per failing pair", code: "INVALID_BODY" }
|
|
5697
5714
|
};
|
|
5698
5715
|
}
|
|
5699
|
-
const report = { state: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), contrast };
|
|
5716
|
+
const report = { state: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: openTheme(), contrast };
|
|
5700
5717
|
const run = runCli(A11Y_RUN_ARGS, root);
|
|
5701
5718
|
running = { report, stop: run.stop };
|
|
5702
5719
|
broadcast(report);
|
|
5703
5720
|
void run.done.then((outcome) => {
|
|
5704
5721
|
const settled = settle(report, outcome);
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
`);
|
|
5722
|
+
const final = settled.state === "finished" ? withConfirmed(settled, readReport(settled.theme)?.confirmed ?? []) : settled;
|
|
5723
|
+
if (final.state === "finished") writeReport(final);
|
|
5708
5724
|
running = null;
|
|
5709
|
-
broadcast(
|
|
5725
|
+
broadcast(final);
|
|
5710
5726
|
});
|
|
5711
5727
|
return { status: 202, body: report };
|
|
5712
5728
|
}
|
|
5729
|
+
function confirm(body) {
|
|
5730
|
+
const { theme, messages, confirmed } = body ?? {};
|
|
5731
|
+
if (typeof theme !== "string" || typeof confirmed !== "boolean" || !Array.isArray(messages) || !messages.every((m) => typeof m === "string")) {
|
|
5732
|
+
return {
|
|
5733
|
+
status: 400,
|
|
5734
|
+
body: { error: "Body must be { theme, messages: [...], confirmed: true | false }", code: "INVALID_BODY" }
|
|
5735
|
+
};
|
|
5736
|
+
}
|
|
5737
|
+
const report = readReport(theme);
|
|
5738
|
+
if (!report) return { status: 404, body: { error: `No report for theme "${theme}"`, code: "NO_REPORT" } };
|
|
5739
|
+
const marked = new Set(report.confirmed ?? []);
|
|
5740
|
+
for (const message of messages) {
|
|
5741
|
+
if (confirmed) marked.add(message);
|
|
5742
|
+
else marked.delete(message);
|
|
5743
|
+
}
|
|
5744
|
+
const updated = withConfirmed(report, marked);
|
|
5745
|
+
writeReport(updated);
|
|
5746
|
+
broadcast(updated);
|
|
5747
|
+
return { status: 200, body: updated };
|
|
5748
|
+
}
|
|
5713
5749
|
return {
|
|
5714
5750
|
start,
|
|
5715
|
-
|
|
5751
|
+
confirm,
|
|
5752
|
+
report: () => ({ status: 200, body: { running: running?.report ?? null, reports: reports() } }),
|
|
5753
|
+
/** Removes a deleted theme's report. */
|
|
5754
|
+
forget: (theme) => import_node_fs.default.rmSync(reportFile(theme), { force: true }),
|
|
5716
5755
|
stop: () => running?.stop()
|
|
5717
5756
|
};
|
|
5718
5757
|
}
|
|
@@ -5780,7 +5819,7 @@ function themeFileApi(opts) {
|
|
|
5780
5819
|
const ISOLATED_DATA_DIR = testDataDir();
|
|
5781
5820
|
const GENERATED_CSS_PATH = ISOLATED_DATA_DIR ? import_path5.default.join(ISOLATED_DATA_DIR, "tokens.generated.css") : opts.tokensGeneratedCssPath ? import_path5.default.resolve(opts.tokensGeneratedCssPath) : import_path5.default.join(dataDirs.dataDir, "tokens.generated.css");
|
|
5782
5821
|
const FONTS_CSS_PATH = ISOLATED_DATA_DIR ? import_path5.default.join(ISOLATED_DATA_DIR, "fonts.css") : opts.fontsCssPath ? import_path5.default.resolve(opts.fontsCssPath) : import_path5.default.join(import_path5.default.dirname(CSS_PATH), "fonts.css");
|
|
5783
|
-
const
|
|
5822
|
+
const A11Y_REPORTS_DIR = import_path5.default.join(dataDirs.dataDir, A11Y_REPORTS_DIR_NAME);
|
|
5784
5823
|
const API_BASE = opts.apiBase ?? "/api/live-tokens";
|
|
5785
5824
|
const consumerComponentDirs = opts.componentsSrcDir ? [import_path5.default.resolve(opts.componentsSrcDir)] : [import_path5.default.resolve("src/components"), import_path5.default.resolve("src/system/components")];
|
|
5786
5825
|
const packageComponentsDir = import_path5.default.resolve(
|
|
@@ -6464,6 +6503,7 @@ ${lines.join("\n")}
|
|
|
6464
6503
|
const EVENTS_ROUTE = `${API_BASE}/events`;
|
|
6465
6504
|
const A11Y_RUN_ROUTE = `${API_BASE}/a11y/run`;
|
|
6466
6505
|
const A11Y_REPORT_ROUTE = `${API_BASE}/a11y/report`;
|
|
6506
|
+
const A11Y_CONFIRMED_ROUTE = `${API_BASE}/a11y/confirmed`;
|
|
6467
6507
|
async function handleListColorsAndType(_ctx) {
|
|
6468
6508
|
const files = colorsAndTypeResource.listNames().map((fileName) => {
|
|
6469
6509
|
const data = colorsAndTypeResource.readJson(fileName);
|
|
@@ -6802,6 +6842,7 @@ ${lines.join("\n")}
|
|
|
6802
6842
|
}
|
|
6803
6843
|
}
|
|
6804
6844
|
import_fs5.default.unlinkSync(filePath);
|
|
6845
|
+
a11yRuns.forget(fileName);
|
|
6805
6846
|
if (themesResource.existingPath(fileName) === null && deletingActive) {
|
|
6806
6847
|
themesResource.setActiveName("default");
|
|
6807
6848
|
}
|
|
@@ -6899,7 +6940,8 @@ data: ${JSON.stringify(state)}
|
|
|
6899
6940
|
const eventClients = /* @__PURE__ */ new Set();
|
|
6900
6941
|
const a11yRuns = a11yRunServer({
|
|
6901
6942
|
root: process.cwd(),
|
|
6902
|
-
|
|
6943
|
+
reportsDir: A11Y_REPORTS_DIR,
|
|
6944
|
+
openTheme: () => themesResource.getActiveName(),
|
|
6903
6945
|
runCli: spawnCli(PACKAGE_CLI_PATH),
|
|
6904
6946
|
broadcast: (report) => {
|
|
6905
6947
|
const frame = `event: ${A11Y_EVENT}
|
|
@@ -6919,6 +6961,16 @@ data: ${JSON.stringify(report)}
|
|
|
6919
6961
|
const answer = a11yRuns.start(body);
|
|
6920
6962
|
jsonResponse(res, answer.status, answer.body);
|
|
6921
6963
|
}
|
|
6964
|
+
async function handleConfirmA11yReview({ req, res }) {
|
|
6965
|
+
let body;
|
|
6966
|
+
try {
|
|
6967
|
+
body = JSON.parse(await readBody(req));
|
|
6968
|
+
} catch {
|
|
6969
|
+
body = void 0;
|
|
6970
|
+
}
|
|
6971
|
+
const answer = a11yRuns.confirm(body);
|
|
6972
|
+
jsonResponse(res, answer.status, answer.body);
|
|
6973
|
+
}
|
|
6922
6974
|
async function handleGetA11yReport({ res }) {
|
|
6923
6975
|
const answer = a11yRuns.report();
|
|
6924
6976
|
jsonResponse(res, answer.status, answer.body);
|
|
@@ -7202,6 +7254,10 @@ data: ${JSON.stringify(report)}
|
|
|
7202
7254
|
{ method: "PUT", pattern: A11Y_REPORT_ROUTE, handler: methodNotAllowed },
|
|
7203
7255
|
{ method: "POST", pattern: A11Y_REPORT_ROUTE, handler: methodNotAllowed },
|
|
7204
7256
|
{ method: "DELETE", pattern: A11Y_REPORT_ROUTE, handler: methodNotAllowed },
|
|
7257
|
+
{ method: "PUT", pattern: A11Y_CONFIRMED_ROUTE, handler: handleConfirmA11yReview },
|
|
7258
|
+
{ method: "GET", pattern: A11Y_CONFIRMED_ROUTE, handler: methodNotAllowed },
|
|
7259
|
+
{ method: "POST", pattern: A11Y_CONFIRMED_ROUTE, handler: methodNotAllowed },
|
|
7260
|
+
{ method: "DELETE", pattern: A11Y_CONFIRMED_ROUTE, handler: methodNotAllowed },
|
|
7205
7261
|
{ method: "PUT", pattern: PRODUCTION_ROUTE, handler: handleAdoptTheme },
|
|
7206
7262
|
{ method: "GET", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
|
|
7207
7263
|
{ method: "POST", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
|
package/dist-plugin/index.js
CHANGED
|
@@ -753,7 +753,7 @@ import { spawn } from "child_process";
|
|
|
753
753
|
import fs2 from "fs";
|
|
754
754
|
import path3 from "path";
|
|
755
755
|
var A11Y_RUN_ARGS = ["check-component", "--tests", "--a11y", "--no-fix", "--json", "--open-theme"];
|
|
756
|
-
var
|
|
756
|
+
var A11Y_REPORTS_DIR_NAME = "_a11y-reports";
|
|
757
757
|
var STDERR_TAIL = 2e3;
|
|
758
758
|
var CONTRAST_ROLES = /* @__PURE__ */ new Set(["text", "non-text"]);
|
|
759
759
|
function spawnCli(cliPath) {
|
|
@@ -786,12 +786,30 @@ function parseCliReport(stdout) {
|
|
|
786
786
|
return null;
|
|
787
787
|
}
|
|
788
788
|
}
|
|
789
|
-
function a11yRunServer({ root,
|
|
789
|
+
function a11yRunServer({ root, reportsDir, openTheme, runCli, broadcast }) {
|
|
790
790
|
let running = null;
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return JSON.parse(fs2.readFileSync(reportFile, "utf-8"));
|
|
791
|
+
const reportFile = (theme) => path3.join(reportsDir, `${theme}.json`);
|
|
792
|
+
function readReport(theme) {
|
|
793
|
+
const listed = fs2.existsSync(reportsDir) && fs2.readdirSync(reportsDir).includes(`${theme}.json`);
|
|
794
|
+
return listed ? JSON.parse(fs2.readFileSync(reportFile(theme), "utf-8")) : null;
|
|
795
|
+
}
|
|
796
|
+
function writeReport(report) {
|
|
797
|
+
fs2.mkdirSync(reportsDir, { recursive: true });
|
|
798
|
+
fs2.writeFileSync(reportFile(report.theme), `${JSON.stringify(report, null, 2)}
|
|
799
|
+
`);
|
|
800
|
+
}
|
|
801
|
+
function withConfirmed(report, confirmed) {
|
|
802
|
+
const review = new Set(report.findings.filter((f) => f.rule === "a11y-review").map((f) => f.message));
|
|
803
|
+
const kept = [...new Set(confirmed)].filter((message) => review.has(message)).sort();
|
|
804
|
+
const out = { ...report, confirmed: kept };
|
|
805
|
+
if (kept.length === 0) delete out.confirmed;
|
|
806
|
+
return out;
|
|
807
|
+
}
|
|
808
|
+
function reports() {
|
|
809
|
+
if (!fs2.existsSync(reportsDir)) return {};
|
|
810
|
+
return Object.fromEntries(
|
|
811
|
+
fs2.readdirSync(reportsDir).filter((name) => name.endsWith(".json")).map((name) => [name.slice(0, -".json".length), JSON.parse(fs2.readFileSync(path3.join(reportsDir, name), "utf-8"))])
|
|
812
|
+
);
|
|
795
813
|
}
|
|
796
814
|
function settle(started, outcome) {
|
|
797
815
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -810,7 +828,6 @@ ${stderr}` : ""}`
|
|
|
810
828
|
...started,
|
|
811
829
|
state: "finished",
|
|
812
830
|
finishedAt,
|
|
813
|
-
...cli.theme ? { theme: cli.theme } : {},
|
|
814
831
|
findings: cli.findings.filter((f) => f.details?.engineRule !== "color-contrast")
|
|
815
832
|
};
|
|
816
833
|
}
|
|
@@ -825,23 +842,45 @@ ${stderr}` : ""}`
|
|
|
825
842
|
body: { error: "Body must be { contrast: [...] } with one entry per failing pair", code: "INVALID_BODY" }
|
|
826
843
|
};
|
|
827
844
|
}
|
|
828
|
-
const report = { state: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), contrast };
|
|
845
|
+
const report = { state: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: openTheme(), contrast };
|
|
829
846
|
const run = runCli(A11Y_RUN_ARGS, root);
|
|
830
847
|
running = { report, stop: run.stop };
|
|
831
848
|
broadcast(report);
|
|
832
849
|
void run.done.then((outcome) => {
|
|
833
850
|
const settled = settle(report, outcome);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
`);
|
|
851
|
+
const final = settled.state === "finished" ? withConfirmed(settled, readReport(settled.theme)?.confirmed ?? []) : settled;
|
|
852
|
+
if (final.state === "finished") writeReport(final);
|
|
837
853
|
running = null;
|
|
838
|
-
broadcast(
|
|
854
|
+
broadcast(final);
|
|
839
855
|
});
|
|
840
856
|
return { status: 202, body: report };
|
|
841
857
|
}
|
|
858
|
+
function confirm(body) {
|
|
859
|
+
const { theme, messages, confirmed } = body ?? {};
|
|
860
|
+
if (typeof theme !== "string" || typeof confirmed !== "boolean" || !Array.isArray(messages) || !messages.every((m) => typeof m === "string")) {
|
|
861
|
+
return {
|
|
862
|
+
status: 400,
|
|
863
|
+
body: { error: "Body must be { theme, messages: [...], confirmed: true | false }", code: "INVALID_BODY" }
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const report = readReport(theme);
|
|
867
|
+
if (!report) return { status: 404, body: { error: `No report for theme "${theme}"`, code: "NO_REPORT" } };
|
|
868
|
+
const marked = new Set(report.confirmed ?? []);
|
|
869
|
+
for (const message of messages) {
|
|
870
|
+
if (confirmed) marked.add(message);
|
|
871
|
+
else marked.delete(message);
|
|
872
|
+
}
|
|
873
|
+
const updated = withConfirmed(report, marked);
|
|
874
|
+
writeReport(updated);
|
|
875
|
+
broadcast(updated);
|
|
876
|
+
return { status: 200, body: updated };
|
|
877
|
+
}
|
|
842
878
|
return {
|
|
843
879
|
start,
|
|
844
|
-
|
|
880
|
+
confirm,
|
|
881
|
+
report: () => ({ status: 200, body: { running: running?.report ?? null, reports: reports() } }),
|
|
882
|
+
/** Removes a deleted theme's report. */
|
|
883
|
+
forget: (theme) => fs2.rmSync(reportFile(theme), { force: true }),
|
|
845
884
|
stop: () => running?.stop()
|
|
846
885
|
};
|
|
847
886
|
}
|
|
@@ -908,7 +947,7 @@ function themeFileApi(opts) {
|
|
|
908
947
|
const ISOLATED_DATA_DIR = testDataDir();
|
|
909
948
|
const GENERATED_CSS_PATH = ISOLATED_DATA_DIR ? path4.join(ISOLATED_DATA_DIR, "tokens.generated.css") : opts.tokensGeneratedCssPath ? path4.resolve(opts.tokensGeneratedCssPath) : path4.join(dataDirs.dataDir, "tokens.generated.css");
|
|
910
949
|
const FONTS_CSS_PATH = ISOLATED_DATA_DIR ? path4.join(ISOLATED_DATA_DIR, "fonts.css") : opts.fontsCssPath ? path4.resolve(opts.fontsCssPath) : path4.join(path4.dirname(CSS_PATH), "fonts.css");
|
|
911
|
-
const
|
|
950
|
+
const A11Y_REPORTS_DIR = path4.join(dataDirs.dataDir, A11Y_REPORTS_DIR_NAME);
|
|
912
951
|
const API_BASE = opts.apiBase ?? "/api/live-tokens";
|
|
913
952
|
const consumerComponentDirs = opts.componentsSrcDir ? [path4.resolve(opts.componentsSrcDir)] : [path4.resolve("src/components"), path4.resolve("src/system/components")];
|
|
914
953
|
const packageComponentsDir = path4.resolve(
|
|
@@ -1592,6 +1631,7 @@ ${lines.join("\n")}
|
|
|
1592
1631
|
const EVENTS_ROUTE = `${API_BASE}/events`;
|
|
1593
1632
|
const A11Y_RUN_ROUTE = `${API_BASE}/a11y/run`;
|
|
1594
1633
|
const A11Y_REPORT_ROUTE = `${API_BASE}/a11y/report`;
|
|
1634
|
+
const A11Y_CONFIRMED_ROUTE = `${API_BASE}/a11y/confirmed`;
|
|
1595
1635
|
async function handleListColorsAndType(_ctx) {
|
|
1596
1636
|
const files = colorsAndTypeResource.listNames().map((fileName) => {
|
|
1597
1637
|
const data = colorsAndTypeResource.readJson(fileName);
|
|
@@ -1930,6 +1970,7 @@ ${lines.join("\n")}
|
|
|
1930
1970
|
}
|
|
1931
1971
|
}
|
|
1932
1972
|
fs3.unlinkSync(filePath);
|
|
1973
|
+
a11yRuns.forget(fileName);
|
|
1933
1974
|
if (themesResource.existingPath(fileName) === null && deletingActive) {
|
|
1934
1975
|
themesResource.setActiveName("default");
|
|
1935
1976
|
}
|
|
@@ -2027,7 +2068,8 @@ data: ${JSON.stringify(state)}
|
|
|
2027
2068
|
const eventClients = /* @__PURE__ */ new Set();
|
|
2028
2069
|
const a11yRuns = a11yRunServer({
|
|
2029
2070
|
root: process.cwd(),
|
|
2030
|
-
|
|
2071
|
+
reportsDir: A11Y_REPORTS_DIR,
|
|
2072
|
+
openTheme: () => themesResource.getActiveName(),
|
|
2031
2073
|
runCli: spawnCli(PACKAGE_CLI_PATH),
|
|
2032
2074
|
broadcast: (report) => {
|
|
2033
2075
|
const frame = `event: ${A11Y_EVENT}
|
|
@@ -2047,6 +2089,16 @@ data: ${JSON.stringify(report)}
|
|
|
2047
2089
|
const answer = a11yRuns.start(body);
|
|
2048
2090
|
jsonResponse(res, answer.status, answer.body);
|
|
2049
2091
|
}
|
|
2092
|
+
async function handleConfirmA11yReview({ req, res }) {
|
|
2093
|
+
let body;
|
|
2094
|
+
try {
|
|
2095
|
+
body = JSON.parse(await readBody(req));
|
|
2096
|
+
} catch {
|
|
2097
|
+
body = void 0;
|
|
2098
|
+
}
|
|
2099
|
+
const answer = a11yRuns.confirm(body);
|
|
2100
|
+
jsonResponse(res, answer.status, answer.body);
|
|
2101
|
+
}
|
|
2050
2102
|
async function handleGetA11yReport({ res }) {
|
|
2051
2103
|
const answer = a11yRuns.report();
|
|
2052
2104
|
jsonResponse(res, answer.status, answer.body);
|
|
@@ -2330,6 +2382,10 @@ data: ${JSON.stringify(report)}
|
|
|
2330
2382
|
{ method: "PUT", pattern: A11Y_REPORT_ROUTE, handler: methodNotAllowed },
|
|
2331
2383
|
{ method: "POST", pattern: A11Y_REPORT_ROUTE, handler: methodNotAllowed },
|
|
2332
2384
|
{ method: "DELETE", pattern: A11Y_REPORT_ROUTE, handler: methodNotAllowed },
|
|
2385
|
+
{ method: "PUT", pattern: A11Y_CONFIRMED_ROUTE, handler: handleConfirmA11yReview },
|
|
2386
|
+
{ method: "GET", pattern: A11Y_CONFIRMED_ROUTE, handler: methodNotAllowed },
|
|
2387
|
+
{ method: "POST", pattern: A11Y_CONFIRMED_ROUTE, handler: methodNotAllowed },
|
|
2388
|
+
{ method: "DELETE", pattern: A11Y_CONFIRMED_ROUTE, handler: methodNotAllowed },
|
|
2333
2389
|
{ method: "PUT", pattern: PRODUCTION_ROUTE, handler: handleAdoptTheme },
|
|
2334
2390
|
{ method: "GET", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
|
|
2335
2391
|
{ method: "POST", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
|
package/package.json
CHANGED
package/src/editor/bootstrap.ts
CHANGED
|
@@ -50,11 +50,12 @@ export async function bootLiveTokens(
|
|
|
50
50
|
if (import.meta.env.DEV) {
|
|
51
51
|
// Imported here, never statically, so a build drops the editor. The
|
|
52
52
|
// registry alone imports every shipped component and editor.
|
|
53
|
-
const [editorStore, themeDocumentSync, liveStateStream, a11yReport, { initializeTheme }] = await Promise.all([
|
|
53
|
+
const [editorStore, themeDocumentSync, liveStateStream, a11yReport, contrastFixes, { initializeTheme }] = await Promise.all([
|
|
54
54
|
import('./core/store/editorStore'),
|
|
55
55
|
import('./core/themes/themeDocumentSync'),
|
|
56
56
|
import('./core/themes/liveStateStream'),
|
|
57
57
|
import('./core/a11y/a11yReport'),
|
|
58
|
+
import('./core/a11y/contrastFixes'),
|
|
58
59
|
import('./core/themes/themeInit'),
|
|
59
60
|
import('./component-editor/registry'),
|
|
60
61
|
]);
|
|
@@ -62,6 +63,7 @@ export async function bootLiveTokens(
|
|
|
62
63
|
themeDocumentSync.init();
|
|
63
64
|
liveStateStream.init();
|
|
64
65
|
void a11yReport.init();
|
|
66
|
+
contrastFixes.init();
|
|
65
67
|
if (opts.components) {
|
|
66
68
|
for (const entry of opts.components) {
|
|
67
69
|
registerComponent(entry);
|
|
@@ -195,18 +195,17 @@
|
|
|
195
195
|
];
|
|
196
196
|
const INTRINSIC_BY_KEY = new Map(intrinsics.map((i) => [i.key, i]));
|
|
197
197
|
|
|
198
|
+
// The hairline is decorative: the heading carries the section break, so
|
|
199
|
+
// WCAG 2.2 SC 1.4.11 sets no floor on it and it has no pair.
|
|
198
200
|
function variantContrastPairs(v: Variant): ContrastPair[] {
|
|
199
201
|
const backdrop = [`--sectiondivider-${v}-surface`];
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
})),
|
|
208
|
-
{ foreground: `--sectiondivider-${v}-hairline-color`, backdrop, role: 'non-text' },
|
|
209
|
-
];
|
|
202
|
+
return variantTypeGroups(v).map((g): ContrastPair => ({
|
|
203
|
+
foreground: g.colorVariable,
|
|
204
|
+
backdrop,
|
|
205
|
+
role: 'text',
|
|
206
|
+
fontSize: g.sizeVariable,
|
|
207
|
+
fontWeight: g.weightVariable,
|
|
208
|
+
}));
|
|
210
209
|
}
|
|
211
210
|
|
|
212
211
|
export const contrastPairs: ContrastPair[] = variants.flatMap((v) => variantContrastPairs(v.key));
|