@godxjp/ui 16.9.4 → 17.0.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/README.md +30 -5
- package/dist/components/charts/chart-cartesian.js +1 -6
- package/dist/components/charts/pie-chart.js +1 -8
- package/dist/components/data-display/empty-state.d.ts +1 -1
- package/dist/components/data-display/empty-state.js +25 -7
- package/dist/components/data-display/progress.d.ts +9 -2
- package/dist/components/data-display/progress.js +6 -3
- package/dist/components/data-entry/form-field.js +13 -4
- package/dist/components/data-entry/search-select.d.ts +1 -1
- package/dist/components/data-entry/search-select.js +97 -19
- package/dist/components/data-entry/select.js +15 -4
- package/dist/components/feedback/alert.d.ts +11 -2
- package/dist/components/feedback/alert.js +51 -9
- package/dist/components/general/button.js +2 -0
- package/dist/components/general/index.d.ts +4 -0
- package/dist/components/general/index.js +4 -0
- package/dist/components/general/logo.d.ts +27 -0
- package/dist/components/general/logo.js +23 -0
- package/dist/components/general/reveal.d.ts +14 -0
- package/dist/components/general/reveal.js +21 -0
- package/dist/components/layout/auth-shell.d.ts +11 -0
- package/dist/components/layout/auth-shell.js +15 -0
- package/dist/components/layout/flex.js +1 -1
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/layout/page-container.js +4 -4
- package/dist/components/navigation/app-setting-picker.js +19 -4
- package/dist/components/navigation/pagination.d.ts +1 -1
- package/dist/components/navigation/pagination.js +2 -1
- package/dist/components/query/data-state.d.ts +1 -1
- package/dist/components/query/data-state.js +27 -4
- package/dist/components/query/index.d.ts +2 -0
- package/dist/components/query/index.js +4 -1
- package/dist/i18n/messages/en.json +30 -2
- package/dist/i18n/messages/ja.json +30 -2
- package/dist/i18n/messages/vi.json +30 -2
- package/dist/lib/query-error.d.ts +17 -0
- package/dist/lib/query-error.js +58 -0
- package/dist/props/components/app.prop.d.ts +9 -1
- package/dist/props/components/data-display.prop.d.ts +15 -0
- package/dist/props/components/data-entry.prop.d.ts +8 -0
- package/dist/props/components/feedback.prop.d.ts +6 -0
- package/dist/props/components/general.prop.d.ts +23 -1
- package/dist/props/components/layout.prop.d.ts +17 -0
- package/dist/props/components/query.prop.d.ts +7 -1
- package/dist/props/registry.d.ts +31 -2
- package/dist/props/registry.js +44 -2
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +16 -0
- package/dist/styles/alert-layout.css +1 -2
- package/dist/styles/card-layout.css +6 -6
- package/dist/styles/data-display-layout.css +18 -0
- package/dist/styles/index.css +2 -0
- package/dist/styles/layout.css +28 -0
- package/dist/styles/logo-layout.css +41 -0
- package/dist/styles/motion.css +52 -0
- package/dist/styles/shell-layout.css +42 -0
- package/dist/theme/famgia.service.css +44 -0
- package/dist/tokens/base.css +1 -0
- package/dist/tokens/components/feedback.css +4 -0
- package/dist/tokens/components/logo.css +15 -0
- package/dist/tokens/components/shell.css +10 -0
- package/dist/tokens/foundation.css +8 -2
- package/package.json +7 -3
- package/scripts/visual-audit.mjs +293 -126
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* Logo component tokens — the product brand-mark box (a glyph on the primary fill), used in the
|
|
2
|
+
* app shell header, auth screens, and topbars. Size + radius + per-tier font-size are knobs so a
|
|
3
|
+
* service theme retunes the mark without forking CSS (rules #44/#45). Colours read the primary
|
|
4
|
+
* role tokens directly, so a re-themed --primary re-tints the mark automatically. */
|
|
5
|
+
:root {
|
|
6
|
+
--logo-radius: var(--radius);
|
|
7
|
+
--logo-size-xs: 1.25rem;
|
|
8
|
+
--logo-size-sm: 1.5rem;
|
|
9
|
+
--logo-size-md: 1.75rem;
|
|
10
|
+
--logo-size-lg: 2.25rem;
|
|
11
|
+
--logo-font-size-xs: var(--font-size-2xs);
|
|
12
|
+
--logo-font-size-sm: var(--font-size-xs);
|
|
13
|
+
--logo-font-size-md: var(--font-size-sm);
|
|
14
|
+
--logo-font-size-lg: var(--font-size-base);
|
|
15
|
+
}
|
|
@@ -32,4 +32,14 @@
|
|
|
32
32
|
* Defaults = hsl(var(--accent)) fill · hsl(var(--foreground)) text. */
|
|
33
33
|
--sidebar-item-active-background: initial;
|
|
34
34
|
--sidebar-item-active-foreground: initial;
|
|
35
|
+
|
|
36
|
+
/* AuthShell — centred auth/login page shell. Comfortable control density (44px, WCAG touch floor)
|
|
37
|
+
* + a larger auth heading, scoped to the shell; a service re-tunes the auth card width, insets
|
|
38
|
+
* and heading size without forking. */
|
|
39
|
+
--auth-shell-control-height: var(--control-height-comfortable);
|
|
40
|
+
--auth-shell-heading-size: var(--font-size-2xl);
|
|
41
|
+
--auth-shell-card-max-width: 24rem;
|
|
42
|
+
--auth-shell-bar-padding: var(--space-5) var(--space-6);
|
|
43
|
+
--auth-shell-main-padding: var(--space-6);
|
|
44
|
+
--auth-shell-footer-padding: var(--space-3) var(--space-6) var(--space-4);
|
|
35
45
|
}
|
|
@@ -83,7 +83,8 @@
|
|
|
83
83
|
--disabled-opacity: 0.5;
|
|
84
84
|
|
|
85
85
|
/* Shape and elevation */
|
|
86
|
-
|
|
86
|
+
/* 6px base × scaling; override to retheme every radius step */
|
|
87
|
+
--radius: calc(0.375rem * var(--scaling));
|
|
87
88
|
--radius-ratio: 1.618; /* φ — radii scale by the golden ratio (steps derive in @theme) */
|
|
88
89
|
--radius-pill: 9999px; /* fully-round: switch / radio / slider / pill badges (NOT --radius-derived) */
|
|
89
90
|
--radius-sharp: 0; /* squared corners: table cells, flush inputs */
|
|
@@ -202,7 +203,9 @@
|
|
|
202
203
|
--font-size-3xl: calc(
|
|
203
204
|
var(--font-size-display) / var(--font-size-display-ratio) / var(--font-size-display-ratio)
|
|
204
205
|
); /* ≈ 33 → 28px feature heading */
|
|
205
|
-
--font-size-4xl: calc(
|
|
206
|
+
--font-size-4xl: calc(
|
|
207
|
+
var(--font-size-display) / var(--font-size-display-ratio)
|
|
208
|
+
); /* ≈ 36px section */
|
|
206
209
|
--font-size-5xl: var(--font-size-display); /* 54px hero */
|
|
207
210
|
|
|
208
211
|
/* Headings — also golden-derived. h1=ratio³, h2=ratio²; h3/h4 reuse body sizes (single source). */
|
|
@@ -332,6 +335,9 @@
|
|
|
332
335
|
--ease-decelerate: cubic-bezier(0, 0, 0, 1);
|
|
333
336
|
--ease-accelerate: cubic-bezier(0.3, 0, 1, 1);
|
|
334
337
|
--reveal-distance: 10px;
|
|
338
|
+
/* One step of the Reveal stagger ladder — `<Reveal delay={n}>` waits n × this before entering,
|
|
339
|
+
* so a column of reveals cascades. Read instead of a literal `.04s`/`.09s` per-item delay. */
|
|
340
|
+
--reveal-stagger-step: 60ms;
|
|
335
341
|
}
|
|
336
342
|
|
|
337
343
|
.dark,
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.0",
|
|
4
|
+
"godxUiMcp": "16.10.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"packageManager": "pnpm@10.29.1",
|
|
6
7
|
"sideEffects": false,
|
|
@@ -275,8 +276,9 @@
|
|
|
275
276
|
"test:watch": "vitest",
|
|
276
277
|
"test:coverage": "vitest run --coverage",
|
|
277
278
|
"check:example-imports": "node scripts/check-example-imports.mjs",
|
|
278
|
-
"verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:mcp-sync && pnpm check:mcp-orphans && pnpm check:audit-sync && pnpm test",
|
|
279
|
-
"verify:release": "pnpm build && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:mcp-sync && pnpm check:mcp-orphans && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm test",
|
|
279
|
+
"verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:mcp-orphans && pnpm check:audit-sync && pnpm test",
|
|
280
|
+
"verify:release": "pnpm build && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:mcp-orphans && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm check:visual-audit && pnpm test",
|
|
281
|
+
"check:mcp-lockstep": "node scripts/check-release-lockstep.mjs",
|
|
280
282
|
"check:mcp-sync": "node scripts/check-mcp-sync.mjs",
|
|
281
283
|
"check:mcp-orphans": "node scripts/check-mcp-orphans.mjs",
|
|
282
284
|
"check:mcp-prop-sync": "node scripts/check-mcp-prop-sync.mjs",
|
|
@@ -288,6 +290,7 @@
|
|
|
288
290
|
"gen:component-tokens": "node scripts/gen-component-tokens.mjs",
|
|
289
291
|
"check:mcp-token-sync": "node scripts/gen-component-tokens.mjs --check",
|
|
290
292
|
"check:core-isolation": "node scripts/check-core-isolation.mjs",
|
|
293
|
+
"check:no-consumer-coupling": "node scripts/check-no-consumer-coupling.mjs",
|
|
291
294
|
"release": "node scripts/release.mjs",
|
|
292
295
|
"preview": "node preview/scripts/kill-port.mjs && vite --config preview/vite.config.ts --port 6008 --strictPort",
|
|
293
296
|
"preview:build": "vite build --config preview/vite.config.ts",
|
|
@@ -300,6 +303,7 @@
|
|
|
300
303
|
"check:audit-sync": "node scripts/check-audit-sync.mjs",
|
|
301
304
|
"check:contrast": "node scripts/check-contrast.mjs",
|
|
302
305
|
"visual-audit": "node scripts/visual-audit.mjs",
|
|
306
|
+
"check:visual-audit": "node scripts/visual-audit-smoke.mjs",
|
|
303
307
|
"postinstall": "node scripts/postinstall.mjs",
|
|
304
308
|
"init-agent": "node scripts/init-agent-kit.mjs",
|
|
305
309
|
"check:use-client": "node scripts/check-use-client.mjs"
|
package/scripts/visual-audit.mjs
CHANGED
|
@@ -12,12 +12,27 @@
|
|
|
12
12
|
* Playwright + @axe-core/playwright are OPTIONAL peer deps — installed only by apps that
|
|
13
13
|
* run this audit, so the static audit and the library stay browser-free.
|
|
14
14
|
*
|
|
15
|
+
* TESTED peer range (see TESTED_VERSIONS + README "Runtime visual audit"):
|
|
16
|
+
* playwright >=1.55 <2 (tested 1.61.1)
|
|
17
|
+
* @axe-core/playwright >=4.10 <5 (tested 4.12.1)
|
|
18
|
+
* axe-core >=4.10 <5 (tested 4.12.1)
|
|
19
|
+
*
|
|
15
20
|
* Usage (from the consuming app, against its running dev/preview server):
|
|
16
21
|
* node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 /invoices /settings
|
|
17
|
-
* node node_modules/@godxjp/ui/scripts/visual-audit.mjs
|
|
22
|
+
* node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 --format json
|
|
18
23
|
* node node_modules/@godxjp/ui/scripts/visual-audit.mjs --strict http://localhost:5173 # CI gate
|
|
19
24
|
* PLAYWRIGHT_CHROMIUM_EXECUTABLE=/usr/bin/chromium node …/visual-audit.mjs http://… # system browser
|
|
25
|
+
*
|
|
26
|
+
* JSON contract (--format json ALWAYS emits valid JSON, even on bootstrap failure):
|
|
27
|
+
* { status: "ok" | "partial" | "error",
|
|
28
|
+
* tool: <TESTED_VERSIONS>,
|
|
29
|
+
* summary: { errors, warnings, pages, infrastructureErrors } | null,
|
|
30
|
+
* findings: [ … product findings … ],
|
|
31
|
+
* errors: [ … infrastructure errors (bootstrap / navigate / analyze) … ] }
|
|
32
|
+
* `status: "ok"` ONLY when every page was audited with zero infrastructure errors —
|
|
33
|
+
* so a tool/bootstrap failure can NEVER be misread as "zero violations".
|
|
20
34
|
*/
|
|
35
|
+
import { pathToFileURL } from "node:url";
|
|
21
36
|
import {
|
|
22
37
|
VISUAL_RULES,
|
|
23
38
|
isOversaturated,
|
|
@@ -27,55 +42,79 @@ import {
|
|
|
27
42
|
alertControlIssues,
|
|
28
43
|
} from "./visual-audit-rules.mjs";
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Peer versions this audit is tested against. Surfaced in the JSON `tool` field, the
|
|
47
|
+
* README, and the MCP `list_visual_checks` command so agents pin a compatible range.
|
|
48
|
+
* Playwright 1.55+ is required for the `browser.newContext()` / `context.newPage()`
|
|
49
|
+
* flow that @axe-core/playwright 4.10+ expects (older `browser.newPage()` throws
|
|
50
|
+
* "Please use browser.newContext()").
|
|
51
|
+
*/
|
|
52
|
+
export const TESTED_VERSIONS = {
|
|
53
|
+
playwright: { range: ">=1.55 <2", tested: "1.61.1" },
|
|
54
|
+
"@axe-core/playwright": { range: ">=4.10 <5", tested: "4.12.1" },
|
|
55
|
+
"axe-core": { range: ">=4.10 <5", tested: "4.12.1" },
|
|
56
|
+
};
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
/** Shape one product finding from a rule id (pure — unit-tested without a browser). */
|
|
59
|
+
export function buildFinding(ruleId, url, message) {
|
|
60
|
+
const rule = VISUAL_RULES.find((r) => r.id === ruleId);
|
|
61
|
+
return {
|
|
62
|
+
url,
|
|
63
|
+
rule: ruleId,
|
|
64
|
+
severity: rule?.severity ?? "warn",
|
|
65
|
+
standard: rule?.standard,
|
|
66
|
+
message,
|
|
67
|
+
};
|
|
38
68
|
}
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
70
|
+
/**
|
|
71
|
+
* Assemble the machine-readable result (pure). `findings` are PRODUCT results; `errors`
|
|
72
|
+
* are INFRASTRUCTURE failures (a page that would not load, axe that would not inject).
|
|
73
|
+
* `status` is "ok" only when no infra error occurred, "error" when nothing could be
|
|
74
|
+
* audited, "partial" when some pages ran and some failed.
|
|
75
|
+
*/
|
|
76
|
+
export function buildResult({ findings = [], errors = [], pages = 0 } = {}) {
|
|
77
|
+
const productErrors = findings.filter((f) => f.severity === "error");
|
|
78
|
+
const warnings = findings.filter((f) => f.severity === "warn");
|
|
79
|
+
const failedToLoad = errors.filter((e) => e.stage === "navigate").length;
|
|
80
|
+
const audited = pages - failedToLoad;
|
|
81
|
+
const status = errors.length === 0 ? "ok" : audited > 0 ? "partial" : "error";
|
|
82
|
+
return {
|
|
83
|
+
status,
|
|
84
|
+
tool: TESTED_VERSIONS,
|
|
85
|
+
summary: {
|
|
86
|
+
errors: productErrors.length,
|
|
87
|
+
warnings: warnings.length,
|
|
88
|
+
pages,
|
|
89
|
+
infrastructureErrors: errors.length,
|
|
90
|
+
},
|
|
91
|
+
findings,
|
|
92
|
+
errors,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
47
95
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Assemble a BOOTSTRAP failure result (pure) — missing peers, no browser, bad args.
|
|
98
|
+
* `summary` is null (never `{ warnings: 0 }`) so a consumer cannot read a bootstrap
|
|
99
|
+
* failure as a clean pass.
|
|
100
|
+
*/
|
|
101
|
+
export function buildBootstrapError(message, hint) {
|
|
102
|
+
return {
|
|
103
|
+
status: "error",
|
|
104
|
+
tool: TESTED_VERSIONS,
|
|
105
|
+
summary: null,
|
|
106
|
+
findings: [],
|
|
107
|
+
errors: [{ stage: "bootstrap", message, ...(hint ? { hint } : {}) }],
|
|
108
|
+
};
|
|
55
109
|
}
|
|
56
|
-
const base = urls[0].replace(/\/$/, "");
|
|
57
|
-
const routes = urls.length > 1 ? urls.slice(1) : ["/"];
|
|
58
|
-
const targets = routes.map((r) =>
|
|
59
|
-
r.startsWith("http") ? r : `${base}${r.startsWith("/") ? r : `/${r}`}`,
|
|
60
|
-
);
|
|
61
110
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return { chromium, AxeBuilder: axe.default ?? axe.AxeBuilder };
|
|
70
|
-
} catch {
|
|
71
|
-
console.error(
|
|
72
|
-
"visual-audit needs the optional peers `playwright` and `@axe-core/playwright`:\n" +
|
|
73
|
-
" pnpm add -D playwright @axe-core/playwright\n" +
|
|
74
|
-
" pnpm exec playwright install chromium\n" +
|
|
75
|
-
"(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).",
|
|
76
|
-
);
|
|
77
|
-
process.exit(2);
|
|
78
|
-
}
|
|
111
|
+
/** Exit code for a result: 2 = could not audit, 1 = findings/partial gate, 0 = clean. */
|
|
112
|
+
export function exitCodeFor(result, { strict = false } = {}) {
|
|
113
|
+
if (result.status === "error") return 2;
|
|
114
|
+
if (result.status === "partial") return 1;
|
|
115
|
+
if (strict && result.findings.length > 0) return 1;
|
|
116
|
+
if (result.summary && result.summary.errors > 0) return 1;
|
|
117
|
+
return 0;
|
|
79
118
|
}
|
|
80
119
|
|
|
81
120
|
/** Collect raw measurements from the rendered page; the PURE rules run back in node. */
|
|
@@ -160,102 +199,230 @@ function collectInPage() {
|
|
|
160
199
|
}
|
|
161
200
|
/* c8 ignore stop */
|
|
162
201
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
/** Optional-peer loader — throws a tagged bootstrap error instead of a raw stack. */
|
|
203
|
+
async function loadDeps() {
|
|
204
|
+
try {
|
|
205
|
+
const [{ chromium }, axe] = await Promise.all([
|
|
206
|
+
import("playwright"),
|
|
207
|
+
import("@axe-core/playwright"),
|
|
208
|
+
]);
|
|
209
|
+
return { chromium, AxeBuilder: axe.default ?? axe.AxeBuilder };
|
|
210
|
+
} catch (cause) {
|
|
211
|
+
const err = new Error(
|
|
212
|
+
"visual-audit needs the optional peers `playwright` and `@axe-core/playwright`",
|
|
213
|
+
);
|
|
214
|
+
err.bootstrap = true;
|
|
215
|
+
err.hint =
|
|
216
|
+
"pnpm add -D playwright @axe-core/playwright && pnpm exec playwright install chromium " +
|
|
217
|
+
"(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).";
|
|
218
|
+
err.cause = cause;
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
180
222
|
|
|
181
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Drive the browser over every target and gather findings + infra errors. Cleanup
|
|
225
|
+
* (page/context/browser) is guaranteed via finally on BOTH success and failure.
|
|
226
|
+
* A launch failure is re-tagged as a bootstrap error so it surfaces as `status:error`.
|
|
227
|
+
*/
|
|
228
|
+
/* c8 ignore start — the browser-driving glue; exercised by scripts/visual-audit-smoke.mjs in CI. */
|
|
229
|
+
async function audit(targets, { chromium, AxeBuilder }) {
|
|
230
|
+
const findings = [];
|
|
231
|
+
const errors = [];
|
|
232
|
+
let browser;
|
|
233
|
+
let context;
|
|
234
|
+
try {
|
|
182
235
|
try {
|
|
183
|
-
await
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
236
|
+
browser = await chromium.launch({
|
|
237
|
+
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined,
|
|
238
|
+
});
|
|
239
|
+
} catch (cause) {
|
|
240
|
+
const err = new Error(`could not launch Chromium: ${cause.message}`);
|
|
241
|
+
err.bootstrap = true;
|
|
242
|
+
err.hint =
|
|
243
|
+
"install a browser: pnpm exec playwright install chromium " +
|
|
244
|
+
"(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).";
|
|
245
|
+
throw err;
|
|
187
246
|
}
|
|
247
|
+
context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
248
|
+
const page = await context.newPage();
|
|
188
249
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
197
|
-
}
|
|
250
|
+
for (const url of targets) {
|
|
251
|
+
try {
|
|
252
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
|
|
253
|
+
} catch (e) {
|
|
254
|
+
errors.push({ url, stage: "navigate", message: `could not load page: ${e.message}` });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
198
257
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
258
|
+
try {
|
|
259
|
+
// 1) axe-core — real WCAG/ARIA engine.
|
|
260
|
+
const { violations } = await new AxeBuilder({ page }).analyze();
|
|
261
|
+
for (const v of violations) {
|
|
262
|
+
findings.push(
|
|
263
|
+
buildFinding(
|
|
264
|
+
"axe-violations",
|
|
265
|
+
url,
|
|
266
|
+
`${v.id} (${v.impact}) — ${v.help} [${v.nodes.length} node(s)] ${v.helpUrl}`,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 2) computed-style / layout heuristics via the PURE rules.
|
|
272
|
+
const m = await page.evaluate(collectInPage);
|
|
273
|
+
for (const a of m.accents) {
|
|
274
|
+
if (isOversaturated(a.rgb))
|
|
275
|
+
findings.push(
|
|
276
|
+
buildFinding(
|
|
277
|
+
"oversaturated-accent",
|
|
278
|
+
url,
|
|
279
|
+
`<${a.tag}> background OKLCH chroma ${oklchChroma(a.rgb).toFixed(3)} > 0.18 (rgb ${a.rgb.r},${a.rgb.g},${a.rgb.b})`,
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
for (const t of m.targets) {
|
|
284
|
+
if (isUndersizedTarget(t))
|
|
285
|
+
findings.push(
|
|
286
|
+
buildFinding(
|
|
287
|
+
"target-size-min",
|
|
288
|
+
url,
|
|
289
|
+
`target "${t.name || "(unnamed)"}" is ${t.width}×${t.height}px (< 24×24)`,
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (hasEmoji(m.text)) {
|
|
294
|
+
const found = (m.text.match(/\p{Extended_Pictographic}/gu) || []).slice(0, 8).join(" ");
|
|
295
|
+
findings.push(
|
|
296
|
+
buildFinding("emoji-rendered", url, `emoji rendered in product text: ${found}`),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
m.alerts.forEach((al, i) => {
|
|
300
|
+
for (const issue of alertControlIssues(al))
|
|
301
|
+
findings.push(
|
|
302
|
+
buildFinding("alert-controls-misplaced", url, `banner #${i + 1}: ${issue}`),
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
} catch (e) {
|
|
306
|
+
errors.push({ url, stage: "analyze", message: `audit engine failed: ${e.message}` });
|
|
307
|
+
}
|
|
220
308
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
309
|
+
} finally {
|
|
310
|
+
await context?.close().catch(() => {});
|
|
311
|
+
await browser?.close().catch(() => {});
|
|
225
312
|
}
|
|
226
313
|
|
|
227
|
-
|
|
228
|
-
report(findings);
|
|
314
|
+
return buildResult({ findings, errors, pages: targets.length });
|
|
229
315
|
}
|
|
316
|
+
/* c8 ignore stop */
|
|
230
317
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
318
|
+
const C = {
|
|
319
|
+
red: "\x1b[31m",
|
|
320
|
+
yellow: "\x1b[33m",
|
|
321
|
+
dim: "\x1b[2m",
|
|
322
|
+
bold: "\x1b[1m",
|
|
323
|
+
reset: "\x1b[0m",
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** Render a result to stdout in the requested format, then exit with the right code. */
|
|
327
|
+
/* c8 ignore start — thin process-exit glue, driven by the smoke/CLI paths. */
|
|
328
|
+
function finish(result, { asJson, strict, pages }) {
|
|
329
|
+
const code = exitCodeFor(result, { strict });
|
|
234
330
|
if (asJson) {
|
|
235
|
-
process.stdout.write(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
|
|
245
|
-
console.log(`${tag} ${C.bold}${f.url}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
|
|
246
|
-
console.log(` ${f.message}`);
|
|
247
|
-
if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
|
|
331
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
332
|
+
process.exit(code);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Bootstrap failure — no page was audited.
|
|
336
|
+
if (result.summary === null) {
|
|
337
|
+
for (const e of result.errors) {
|
|
338
|
+
process.stderr.write(`visual-audit failed: ${e.message}\n`);
|
|
339
|
+
if (e.hint) process.stderr.write(` ${e.hint}\n`);
|
|
248
340
|
}
|
|
341
|
+
process.exit(code);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const f of result.findings) {
|
|
345
|
+
const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
|
|
346
|
+
console.log(`${tag} ${C.bold}${f.url}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
|
|
347
|
+
console.log(` ${f.message}`);
|
|
348
|
+
if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
|
|
349
|
+
}
|
|
350
|
+
for (const e of result.errors) {
|
|
249
351
|
console.log(
|
|
250
|
-
|
|
352
|
+
`${C.red}infra${C.reset} ${C.bold}${e.url ?? "(bootstrap)"}${C.reset} ${C.dim}[${e.stage}]${C.reset}`,
|
|
251
353
|
);
|
|
252
|
-
|
|
354
|
+
console.log(` ${e.message}`);
|
|
253
355
|
}
|
|
254
|
-
|
|
255
|
-
|
|
356
|
+
const { errors, warnings } = result.summary;
|
|
357
|
+
console.log(
|
|
358
|
+
`\ngodxjp-ui visual-audit [${result.status}]: ${C.red}${errors} error(s)${C.reset}, ` +
|
|
359
|
+
`${C.yellow}${warnings} warning(s)${C.reset}` +
|
|
360
|
+
(result.errors.length ? `, ${C.red}${result.errors.length} infra error(s)${C.reset}` : "") +
|
|
361
|
+
` across ${pages} page(s).`,
|
|
362
|
+
);
|
|
363
|
+
if (result.status === "ok" && result.findings.length === 0)
|
|
364
|
+
console.log("✓ No visual/runtime a11y violations found.");
|
|
365
|
+
process.exit(code);
|
|
256
366
|
}
|
|
367
|
+
/* c8 ignore stop */
|
|
368
|
+
|
|
369
|
+
/** CLI entry — parse args, run the audit, print, exit. */
|
|
370
|
+
/* c8 ignore start — CLI driver, exercised via subprocess by the smoke test. */
|
|
371
|
+
async function main() {
|
|
372
|
+
const args = process.argv.slice(2);
|
|
373
|
+
const fmtIdx = args.indexOf("--format");
|
|
374
|
+
const asJson = fmtIdx !== -1 && args[fmtIdx + 1] === "json";
|
|
375
|
+
const strict = args.includes("--strict");
|
|
376
|
+
|
|
377
|
+
if (args.includes("--rules")) {
|
|
378
|
+
process.stdout.write(JSON.stringify(VISUAL_RULES, null, 2) + "\n");
|
|
379
|
+
process.exit(0);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const urls = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--format");
|
|
383
|
+
if (urls.length === 0) {
|
|
384
|
+
const msg =
|
|
385
|
+
"no baseUrl given — usage: visual-audit.mjs [--format json] [--strict] <baseUrl> [route …] " +
|
|
386
|
+
"(baseUrl is the running app; extra args are routes appended to it)";
|
|
387
|
+
finish(buildBootstrapError(msg), { asJson, strict, pages: 0 });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
257
390
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
})
|
|
391
|
+
const base = urls[0].replace(/\/$/, "");
|
|
392
|
+
const routes = urls.length > 1 ? urls.slice(1) : ["/"];
|
|
393
|
+
const targets = routes.map((r) =>
|
|
394
|
+
r.startsWith("http") ? r : `${base}${r.startsWith("/") ? r : `/${r}`}`,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
let deps;
|
|
398
|
+
try {
|
|
399
|
+
deps = await loadDeps();
|
|
400
|
+
} catch (e) {
|
|
401
|
+
finish(buildBootstrapError(e.message, e.hint), { asJson, strict, pages: targets.length });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const result = await audit(targets, deps);
|
|
407
|
+
finish(result, { asJson, strict, pages: targets.length });
|
|
408
|
+
} catch (e) {
|
|
409
|
+
// A launch/setup failure = infrastructure error (never "zero violations").
|
|
410
|
+
finish(buildBootstrapError(e.message, e.hint), { asJson, strict, pages: targets.length });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/* c8 ignore stop */
|
|
414
|
+
|
|
415
|
+
// Run only as a CLI — importing this module (for unit tests) must NOT execute.
|
|
416
|
+
/* c8 ignore start */
|
|
417
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
418
|
+
main().catch((e) => {
|
|
419
|
+
const asJson =
|
|
420
|
+
process.argv.indexOf("--format") !== -1 &&
|
|
421
|
+
process.argv[process.argv.indexOf("--format") + 1] === "json";
|
|
422
|
+
if (asJson) {
|
|
423
|
+
process.stdout.write(JSON.stringify(buildBootstrapError(e.message, e.hint)) + "\n");
|
|
424
|
+
} else console.error(`visual-audit failed: ${e.message}`);
|
|
425
|
+
process.exit(2);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/* c8 ignore stop */
|