@godxjp/ui 13.17.4 → 14.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/dist/components/charts/area-chart.d.ts +8 -0
- package/dist/components/charts/area-chart.js +8 -0
- package/dist/components/charts/bar-chart.d.ts +8 -0
- package/dist/components/charts/bar-chart.js +8 -0
- package/dist/components/charts/chart-cartesian.d.ts +31 -0
- package/dist/components/charts/chart-cartesian.js +178 -0
- package/dist/components/charts/chart-frame.d.ts +44 -0
- package/dist/components/charts/chart-frame.js +72 -0
- package/dist/components/charts/chart-summary.d.ts +21 -0
- package/dist/components/charts/chart-summary.js +23 -0
- package/dist/components/charts/index.d.ts +16 -0
- package/dist/components/charts/index.js +12 -0
- package/dist/components/charts/line-chart.d.ts +8 -0
- package/dist/components/charts/line-chart.js +8 -0
- package/dist/components/charts/pie-chart.d.ts +8 -0
- package/dist/components/charts/pie-chart.js +73 -0
- package/dist/components/data-display/index.d.ts +2 -0
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/list-row.d.ts +24 -0
- package/dist/components/data-display/list-row.js +31 -0
- package/dist/components/data-entry/command.d.ts +10 -10
- package/dist/components/data-entry/transfer.js +1 -1
- package/dist/components/general/button.d.ts +1 -0
- package/dist/components/general/button.js +3 -0
- package/dist/components/general/index.d.ts +2 -0
- package/dist/components/general/index.js +4 -1
- package/dist/components/general/logo.d.ts +28 -0
- package/dist/components/general/logo.js +45 -0
- package/dist/components/layout/index.d.ts +1 -1
- package/dist/components/layout/sidebar.js +32 -24
- package/dist/components/layout/topbar.d.ts +19 -2
- package/dist/components/layout/topbar.js +7 -128
- package/dist/i18n/messages/en.json +5 -0
- package/dist/i18n/messages/ja.json +5 -0
- package/dist/i18n/messages/vi.json +5 -0
- package/dist/props/components/charts.prop.d.ts +89 -0
- package/dist/props/components/charts.prop.js +0 -0
- package/dist/props/components/general.prop.d.ts +5 -0
- package/dist/props/components/index.d.ts +2 -1
- package/dist/props/components/layout.prop.d.ts +16 -33
- package/dist/props/registry.d.ts +170 -11
- package/dist/props/registry.js +112 -11
- package/dist/styles/chart-layout.css +37 -0
- package/dist/styles/data-display-layout.css +38 -0
- package/dist/styles/index.css +1 -0
- package/dist/styles/shell-layout.css +39 -0
- package/dist/styles/text-layout.css +3 -0
- package/dist/tokens/base.css +2 -0
- package/dist/tokens/components/list-row.css +9 -0
- package/dist/tokens/components/logo.css +11 -0
- package/dist/tokens/foundation.css +13 -0
- package/package.json +42 -7
- package/scripts/_agent-setup.mjs +145 -0
- package/scripts/audit-hook.mjs +66 -0
- package/scripts/cli.mjs +26 -0
- package/scripts/init-agent-kit.mjs +52 -0
- package/scripts/postinstall.mjs +31 -0
- package/scripts/ui-audit.mjs +165 -3
- package/scripts/visual-audit-rules.mjs +119 -0
- package/scripts/visual-audit.mjs +261 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@10.29.1",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
10
|
"scripts/ui-audit.mjs",
|
|
11
|
-
"README.md"
|
|
11
|
+
"README.md",
|
|
12
|
+
"scripts/visual-audit.mjs",
|
|
13
|
+
"scripts/visual-audit-rules.mjs",
|
|
14
|
+
"scripts/audit-hook.mjs",
|
|
15
|
+
"scripts/init-agent-kit.mjs",
|
|
16
|
+
"scripts/postinstall.mjs",
|
|
17
|
+
"scripts/_agent-setup.mjs",
|
|
18
|
+
"scripts/cli.mjs"
|
|
12
19
|
],
|
|
13
20
|
"main": "./dist/index.js",
|
|
14
21
|
"types": "./dist/index.d.ts",
|
|
@@ -51,6 +58,10 @@
|
|
|
51
58
|
"types": "./dist/components/data-grid/index.d.ts",
|
|
52
59
|
"import": "./dist/components/data-grid/index.js"
|
|
53
60
|
},
|
|
61
|
+
"./charts": {
|
|
62
|
+
"types": "./dist/components/charts/index.d.ts",
|
|
63
|
+
"import": "./dist/components/charts/index.js"
|
|
64
|
+
},
|
|
54
65
|
"./ui": {
|
|
55
66
|
"types": "./dist/components/ui/index.d.ts",
|
|
56
67
|
"import": "./dist/components/ui/index.js"
|
|
@@ -266,8 +277,8 @@
|
|
|
266
277
|
"test:watch": "vitest",
|
|
267
278
|
"test:coverage": "vitest run --coverage",
|
|
268
279
|
"check:example-imports": "node scripts/check-example-imports.mjs",
|
|
269
|
-
"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 test",
|
|
270
|
-
"verify:release": "pnpm typecheck && pnpm lint && 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:mcp-prop-sync && pnpm test",
|
|
280
|
+
"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",
|
|
281
|
+
"verify:release": "pnpm typecheck && pnpm lint && 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 check:mcp-prop-sync && pnpm test",
|
|
271
282
|
"check:mcp-sync": "node scripts/check-mcp-sync.mjs",
|
|
272
283
|
"check:mcp-orphans": "node scripts/check-mcp-orphans.mjs",
|
|
273
284
|
"check:mcp-prop-sync": "node scripts/check-mcp-prop-sync.mjs",
|
|
@@ -286,7 +297,11 @@
|
|
|
286
297
|
"audit": "node scripts/ui-audit.mjs",
|
|
287
298
|
"build": "tsup && tsc -p tsconfig.build.json && node scripts/copy-styles.mjs",
|
|
288
299
|
"dev": "node scripts/dev.mjs",
|
|
289
|
-
"prepublishOnly": "pnpm run build"
|
|
300
|
+
"prepublishOnly": "pnpm run build",
|
|
301
|
+
"check:audit-sync": "node scripts/check-audit-sync.mjs",
|
|
302
|
+
"visual-audit": "node scripts/visual-audit.mjs",
|
|
303
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
304
|
+
"init-agent": "node scripts/init-agent-kit.mjs"
|
|
290
305
|
},
|
|
291
306
|
"peerDependencies": {
|
|
292
307
|
"@hookform/resolvers": "^5.2.0",
|
|
@@ -296,12 +311,28 @@
|
|
|
296
311
|
"react-dom": ">=19.0.0",
|
|
297
312
|
"react-hook-form": "^7.76.0",
|
|
298
313
|
"react-router-dom": ">=7.0.0",
|
|
314
|
+
"recharts": "^2.13.0 || ^3.0.0",
|
|
299
315
|
"tailwindcss": ">=4.0.0",
|
|
300
|
-
"zod": "^4.4.0"
|
|
316
|
+
"zod": "^4.4.0",
|
|
317
|
+
"playwright": "*",
|
|
318
|
+
"@axe-core/playwright": "*",
|
|
319
|
+
"axe-core": "*"
|
|
301
320
|
},
|
|
302
321
|
"peerDependenciesMeta": {
|
|
303
322
|
"@tanstack/react-table": {
|
|
304
323
|
"optional": true
|
|
324
|
+
},
|
|
325
|
+
"recharts": {
|
|
326
|
+
"optional": true
|
|
327
|
+
},
|
|
328
|
+
"playwright": {
|
|
329
|
+
"optional": true
|
|
330
|
+
},
|
|
331
|
+
"@axe-core/playwright": {
|
|
332
|
+
"optional": true
|
|
333
|
+
},
|
|
334
|
+
"axe-core": {
|
|
335
|
+
"optional": true
|
|
305
336
|
}
|
|
306
337
|
},
|
|
307
338
|
"dependencies": {
|
|
@@ -348,6 +379,7 @@
|
|
|
348
379
|
"vaul": "^1.1.2"
|
|
349
380
|
},
|
|
350
381
|
"devDependencies": {
|
|
382
|
+
"@axe-core/playwright": "^4.12.1",
|
|
351
383
|
"@eslint/js": "^9.39.4",
|
|
352
384
|
"@hookform/resolvers": "^5.2.2",
|
|
353
385
|
"@tailwindcss/vite": "^4.2.4",
|
|
@@ -361,17 +393,20 @@
|
|
|
361
393
|
"@types/react-dom": "^19.2.3",
|
|
362
394
|
"@vitejs/plugin-react": "^6.0.1",
|
|
363
395
|
"@vitest/coverage-v8": "^4.1.5",
|
|
396
|
+
"axe-core": "^4.12.1",
|
|
364
397
|
"eslint": "^9.39.2",
|
|
365
398
|
"eslint-plugin-react": "^7.37.5",
|
|
366
399
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
367
400
|
"globals": "^15.15.0",
|
|
368
401
|
"jsdom": "^29.1.1",
|
|
402
|
+
"playwright": "^1.61.1",
|
|
369
403
|
"prettier": "^3.6.2",
|
|
370
404
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
371
405
|
"react": "^19.2.6",
|
|
372
406
|
"react-dom": "^19.2.6",
|
|
373
407
|
"react-hook-form": "^7.76.0",
|
|
374
408
|
"react-router-dom": "^7.15.0",
|
|
409
|
+
"recharts": "^3.8.1",
|
|
375
410
|
"tailwindcss": "^4.2.4",
|
|
376
411
|
"tsup": "^8.5.1",
|
|
377
412
|
"typescript": "^6.0.3",
|
|
@@ -382,7 +417,7 @@
|
|
|
382
417
|
"zod": "^4.4.3"
|
|
383
418
|
},
|
|
384
419
|
"bin": {
|
|
385
|
-
"godxjp-ui
|
|
420
|
+
"godxjp-ui": "scripts/cli.mjs"
|
|
386
421
|
},
|
|
387
422
|
"module": "./dist/index.js"
|
|
388
423
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared agent-setup helpers — used by both postinstall.mjs (auto, on install) and
|
|
3
|
+
* init-agent-kit.mjs (explicit, full kit). Every writer is IDEMPOTENT and
|
|
4
|
+
* NON-DESTRUCTIVE: it only creates a missing file or ADDS a missing key, never
|
|
5
|
+
* overwrites existing config.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
|
|
10
|
+
/** The godxjp-ui MCP server — pulled on demand via npx (no extra dependency to ship). */
|
|
11
|
+
export const MCP_SERVER = { command: "npx", args: ["@godxjp/ui-mcp"] };
|
|
12
|
+
export const MCP_KEY = "godx-ui";
|
|
13
|
+
|
|
14
|
+
/** Commands wired into the consumer's .claude/settings.json. */
|
|
15
|
+
export const AUDIT_HOOK_CMD = "node node_modules/@godxjp/ui/scripts/audit-hook.mjs";
|
|
16
|
+
export const PRIMER_CMD = "cat .claude/godxjp-ui-workflow.md";
|
|
17
|
+
|
|
18
|
+
/** The per-session workflow mandate the SessionStart hook injects into the agent. */
|
|
19
|
+
export const WORKFLOW_MD = `# @godxjp/ui — mandatory workflow (read every session)
|
|
20
|
+
|
|
21
|
+
You are building UI in an app that uses @godxjp/ui. Follow this EVERY time you create
|
|
22
|
+
or change a component, page, or form — no exceptions.
|
|
23
|
+
|
|
24
|
+
## Before writing UI
|
|
25
|
+
1. **MCP-first.** Consult the godx-ui MCP: \`get_component <Name>\`, \`search_components\`,
|
|
26
|
+
\`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
|
|
27
|
+
primitive already does.
|
|
28
|
+
2. **Real primitives only.** No raw \`<input>/<select>/<button>/<textarea>/<table>\`, no
|
|
29
|
+
styled-div fakes. A labelled control ALWAYS goes in \`<FormField label=…>\` (it owns the
|
|
30
|
+
label↔control wiring, error/aria, AND the field rhythm). Never pair a bare \`<Label>\`
|
|
31
|
+
with an \`<Input>\`.
|
|
32
|
+
3. **Tokens, not literals.** Semantic colour tokens (bg-primary, text-muted-foreground),
|
|
33
|
+
token spacing/size/radius/type — no raw palette (bg-blue-600), no hex, no arbitrary
|
|
34
|
+
\`[13px]\`. No emoji in product UI. Logical CSS (ms-/me-/ps-/pe-) for RTL.
|
|
35
|
+
|
|
36
|
+
## After writing UI
|
|
37
|
+
4. **Audit (runs automatically too).** \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs\`
|
|
38
|
+
— fix every finding. A PostToolUse hook also runs it on each edit and feeds findings back.
|
|
39
|
+
5. **Before a visual review**, run the runtime audit against the running app:
|
|
40
|
+
\`node node_modules/@godxjp/ui/scripts/visual-audit.mjs <url>\` (axe + contrast + layout).
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
/** Delimited block appended to the consumer's CLAUDE.md — loaded into the agent's context
|
|
44
|
+
* every turn (the most reliable "ensure it reads the rules"). Markers keep it idempotent. */
|
|
45
|
+
export const CLAUDE_MD_BLOCK = `<!-- godxjp-ui:start (managed by @godxjp/ui — edit .claude/godxjp-ui-workflow.md instead) -->
|
|
46
|
+
## @godxjp/ui — mandatory UI workflow (do NOT skip)
|
|
47
|
+
|
|
48
|
+
This app uses @godxjp/ui. EVERY time you build or change UI:
|
|
49
|
+
|
|
50
|
+
1. **MCP-first.** Consult the \`godx-ui\` MCP — \`get_component\`, \`search_components\`,
|
|
51
|
+
\`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
|
|
52
|
+
primitive already does.
|
|
53
|
+
2. **Real primitives only.** No raw \`<input>/<select>/<button>/<textarea>/<table>\`. A labelled
|
|
54
|
+
control ALWAYS goes in \`<FormField label=…>\` — never a bare \`<Label>\`+\`<Input>\`. Semantic
|
|
55
|
+
tokens, not raw palette/hex/arbitrary values. No emoji in product UI. Logical CSS for RTL.
|
|
56
|
+
3. **Scan after writing.** \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs <files>\` and fix
|
|
57
|
+
every finding (a PostToolUse hook runs this automatically and feeds findings back). Before a
|
|
58
|
+
visual review: \`node node_modules/@godxjp/ui/scripts/visual-audit.mjs <url>\` (axe + layout).
|
|
59
|
+
|
|
60
|
+
Full guide: \`.claude/godxjp-ui-workflow.md\`.
|
|
61
|
+
<!-- godxjp-ui:end -->
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
function readJson(path) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Ensure `.mcp.json` registers the godx-ui MCP server. Returns 'created' | 'added' | 'present'. */
|
|
73
|
+
export function ensureMcpJson(root) {
|
|
74
|
+
const path = join(root, ".mcp.json");
|
|
75
|
+
const json = readJson(path) ?? {};
|
|
76
|
+
json.mcpServers = json.mcpServers ?? {};
|
|
77
|
+
if (json.mcpServers[MCP_KEY]) return "present";
|
|
78
|
+
const created = !existsSync(path);
|
|
79
|
+
json.mcpServers[MCP_KEY] = MCP_SERVER;
|
|
80
|
+
writeFileSync(path, JSON.stringify(json, null, 2) + "\n");
|
|
81
|
+
return created ? "created" : "added";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Ensure `.claude/settings.json` has the audit PostToolUse + workflow SessionStart hooks. */
|
|
85
|
+
export function ensureClaudeHooks(root) {
|
|
86
|
+
const path = join(root, ".claude", "settings.json");
|
|
87
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
88
|
+
const json = readJson(path) ?? {};
|
|
89
|
+
json.hooks = json.hooks ?? {};
|
|
90
|
+
const added = [];
|
|
91
|
+
|
|
92
|
+
const has = (arr, cmd) =>
|
|
93
|
+
(arr ?? []).some((g) => (g.hooks ?? []).some((h) => (h.command ?? "").includes(cmd)));
|
|
94
|
+
|
|
95
|
+
json.hooks.PostToolUse = json.hooks.PostToolUse ?? [];
|
|
96
|
+
if (!has(json.hooks.PostToolUse, "audit-hook.mjs")) {
|
|
97
|
+
json.hooks.PostToolUse.push({
|
|
98
|
+
matcher: "Write|Edit|MultiEdit",
|
|
99
|
+
hooks: [{ type: "command", command: AUDIT_HOOK_CMD }],
|
|
100
|
+
});
|
|
101
|
+
added.push("PostToolUse:auto-audit");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
json.hooks.SessionStart = json.hooks.SessionStart ?? [];
|
|
105
|
+
if (!has(json.hooks.SessionStart, "godxjp-ui-workflow.md")) {
|
|
106
|
+
json.hooks.SessionStart.push({ hooks: [{ type: "command", command: PRIMER_CMD }] });
|
|
107
|
+
added.push("SessionStart:workflow-primer");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
writeFileSync(path, JSON.stringify(json, null, 2) + "\n");
|
|
111
|
+
return added;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Write the workflow mandate the SessionStart hook reads (only if absent). */
|
|
115
|
+
export function writeWorkflowMd(root) {
|
|
116
|
+
const path = join(root, ".claude", "godxjp-ui-workflow.md");
|
|
117
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
118
|
+
if (existsSync(path)) return false;
|
|
119
|
+
writeFileSync(path, WORKFLOW_MD);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Append the godxjp-ui mandate to the consumer's CLAUDE.md (created if absent). Idempotent
|
|
124
|
+
* via the markers. Returns 'created' | 'appended' | 'present'. */
|
|
125
|
+
export function ensureClaudeMd(root) {
|
|
126
|
+
const path = join(root, "CLAUDE.md");
|
|
127
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
128
|
+
if (existing?.includes("godxjp-ui:start")) return "present";
|
|
129
|
+
if (existing == null) {
|
|
130
|
+
writeFileSync(path, CLAUDE_MD_BLOCK);
|
|
131
|
+
return "created";
|
|
132
|
+
}
|
|
133
|
+
writeFileSync(path, existing.replace(/\s*$/, "") + "\n\n" + CLAUDE_MD_BLOCK);
|
|
134
|
+
return "appended";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True when setup should be SKIPPED (CI, opted out, or installing the library itself). */
|
|
138
|
+
export function shouldSkip(root) {
|
|
139
|
+
if (process.env.CI || process.env.GODXJP_UI_SKIP_SETUP) return "ci-or-optout";
|
|
140
|
+
if (!root) return "no-consumer";
|
|
141
|
+
const pkg = readJson(join(root, "package.json"));
|
|
142
|
+
if (!pkg) return "no-package";
|
|
143
|
+
if (pkg.name === "@godxjp/ui" || pkg.name === "@godxjp/ui-mcp") return "self";
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code PostToolUse hook — the FORCING FUNCTION.
|
|
4
|
+
*
|
|
5
|
+
* Wired by `init-agent-kit.mjs` into a consumer's `.claude/settings.json` so that
|
|
6
|
+
* after EVERY Write/Edit/MultiEdit of a .tsx/.ts file, the godxjp-ui static audit
|
|
7
|
+
* runs on that file and its findings are fed straight back to the agent. The agent
|
|
8
|
+
* cannot skip it — the harness runs the hook, not the agent's goodwill — so a
|
|
9
|
+
* hand-rolled <label>+<Input>, a raw <button>, a loud palette colour, or an emoji
|
|
10
|
+
* is surfaced the instant it is written, and the agent self-corrects.
|
|
11
|
+
*
|
|
12
|
+
* Contract: reads the hook JSON on stdin ({ tool_input: { file_path } }). Prints
|
|
13
|
+
* findings to stderr and exits 2 when any are found (Claude Code feeds stderr back
|
|
14
|
+
* to the model); exits 0 (silent) when the file is clean or irrelevant. Non-fatal —
|
|
15
|
+
* it never blocks the edit, it just makes the agent SEE the issue.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const AUDIT = join(HERE, "ui-audit.mjs");
|
|
24
|
+
|
|
25
|
+
function readStdin() {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(0, "utf8")); // fd 0 = the hook payload piped by Claude Code
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const payload = readStdin();
|
|
34
|
+
const file = payload?.tool_input?.file_path ?? payload?.tool_input?.filePath;
|
|
35
|
+
if (!file || !/\.(tsx|ts)$/.test(file) || /\.(test|stories|d)\.tsx?$/.test(file)) process.exit(0);
|
|
36
|
+
|
|
37
|
+
// ui-audit exits 1 when it finds errors — execFileSync throws, but the JSON is still on
|
|
38
|
+
// e.stdout. Capture it either way; only a truly broken run (no stdout) is ignored.
|
|
39
|
+
let out;
|
|
40
|
+
try {
|
|
41
|
+
out = execFileSync("node", [AUDIT, "--format", "json", file], { encoding: "utf8" });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
out = e.stdout?.toString();
|
|
44
|
+
}
|
|
45
|
+
let result;
|
|
46
|
+
try {
|
|
47
|
+
result = JSON.parse(out);
|
|
48
|
+
} catch {
|
|
49
|
+
process.exit(0); // never let an audit error break the edit flow
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const findings = result.findings ?? [];
|
|
53
|
+
if (findings.length === 0) process.exit(0);
|
|
54
|
+
|
|
55
|
+
const lines = [
|
|
56
|
+
`godxjp-ui audit flagged ${findings.length} issue(s) in this file — fix before moving on:`,
|
|
57
|
+
];
|
|
58
|
+
for (const f of findings) {
|
|
59
|
+
lines.push(` [${f.severity}] ${f.rule} (line ${f.line})${f.standard ? ` · ${f.standard}` : ""}`);
|
|
60
|
+
lines.push(` ${f.message}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(
|
|
63
|
+
"Consult the godxjp-ui MCP (get_component / list_audit_rules) and use the real primitive — do not hand-roll.",
|
|
64
|
+
);
|
|
65
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
66
|
+
process.exit(2); // exit 2 → Claude Code surfaces stderr to the agent
|
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @godxjp/ui CLI — `npx @godxjp/ui <command>`.
|
|
4
|
+
* init-agent scaffold the agent forcing-kit (MCP + auto-audit hook + mandate)
|
|
5
|
+
* audit static UI audit (regex over source)
|
|
6
|
+
* visual-audit runtime audit (Playwright + axe-core) against a running app
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const MAP = {
|
|
14
|
+
"init-agent": "init-agent-kit.mjs",
|
|
15
|
+
audit: "ui-audit.mjs",
|
|
16
|
+
"visual-audit": "visual-audit.mjs",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
20
|
+
const script = MAP[cmd];
|
|
21
|
+
if (!script) {
|
|
22
|
+
console.error("usage: godxjp-ui <init-agent | audit | visual-audit> [args]");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const r = spawnSync("node", [join(HERE, script), ...rest], { stdio: "inherit" });
|
|
26
|
+
process.exit(r.status ?? 0);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* init-agent-kit — the FULL forcing-kit. Run once from a consumer app:
|
|
4
|
+
* npx @godxjp/ui init-agent
|
|
5
|
+
*
|
|
6
|
+
* Scaffolds (idempotent, non-destructive) the harness-enforced workflow so an agent
|
|
7
|
+
* follows the whole godxjp-ui process every time it touches UI:
|
|
8
|
+
* 1. .mcp.json — registers the godx-ui MCP (component + audit guidance)
|
|
9
|
+
* 2. .claude/settings.json — PostToolUse hook auto-runs the audit on every .tsx edit
|
|
10
|
+
* and feeds findings back; SessionStart injects the mandate
|
|
11
|
+
* 3. .claude/godxjp-ui-workflow.md — the per-session mandate the SessionStart hook reads
|
|
12
|
+
*
|
|
13
|
+
* Existing config is preserved (hooks are appended only if absent). It then prints the
|
|
14
|
+
* optional pre-commit / CI snippets.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
ensureClaudeHooks,
|
|
18
|
+
ensureClaudeMd,
|
|
19
|
+
ensureMcpJson,
|
|
20
|
+
shouldSkip,
|
|
21
|
+
writeWorkflowMd,
|
|
22
|
+
} from "./_agent-setup.mjs";
|
|
23
|
+
|
|
24
|
+
const root = process.env.INIT_CWD || process.cwd();
|
|
25
|
+
|
|
26
|
+
const skip = shouldSkip(root);
|
|
27
|
+
if (skip === "self") {
|
|
28
|
+
console.error("init-agent is for CONSUMER apps, not the @godxjp/ui repo itself.");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const mcp = ensureMcpJson(root);
|
|
33
|
+
const hooks = ensureClaudeHooks(root);
|
|
34
|
+
const md = writeWorkflowMd(root);
|
|
35
|
+
const claudeMd = ensureClaudeMd(root);
|
|
36
|
+
|
|
37
|
+
console.log("\n @godxjp/ui agent-kit installed:");
|
|
38
|
+
console.log(` • .mcp.json — godx-ui MCP (${mcp})`);
|
|
39
|
+
console.log(
|
|
40
|
+
` • .claude/settings.json — ${hooks.length ? hooks.join(", ") : "hooks already present"}`,
|
|
41
|
+
);
|
|
42
|
+
console.log(` • .claude/godxjp-ui-workflow.md — ${md ? "created" : "already present"}`);
|
|
43
|
+
console.log(` • CLAUDE.md — godxjp-ui mandate (${claudeMd})`);
|
|
44
|
+
console.log(`
|
|
45
|
+
The auto-audit hook now runs on every Write/Edit of a .tsx file and feeds findings
|
|
46
|
+
back to the agent — it cannot skip the audit. Restart your agent to load the MCP + hooks.
|
|
47
|
+
|
|
48
|
+
Optional hard gate (block bad UI from landing):
|
|
49
|
+
• pre-commit: node node_modules/@godxjp/ui/scripts/ui-audit.mjs resources/js || exit 1
|
|
50
|
+
• CI step: node node_modules/@godxjp/ui/scripts/ui-audit.mjs --quiet <your-ui-dirs>
|
|
51
|
+
`);
|
|
52
|
+
process.exit(0);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall — runs when a consumer installs @godxjp/ui. Auto-registers the godx-ui
|
|
4
|
+
* MCP server in the consumer's `.mcp.json` so the agent gets live access to the
|
|
5
|
+
* component catalog + audit rules WITHOUT any manual step. Non-destructive (only adds
|
|
6
|
+
* a missing server entry) and guarded so it never runs in CI or in the library's own repo.
|
|
7
|
+
*
|
|
8
|
+
* The heavier forcing-kit (the auto-audit PostToolUse hook + the workflow mandate) is
|
|
9
|
+
* left to an EXPLICIT `npx @godxjp/ui init-agent`, since hooks change the dev loop and
|
|
10
|
+
* should be opted into, not forced on install. Set GODXJP_UI_SKIP_SETUP=1 to disable.
|
|
11
|
+
*/
|
|
12
|
+
import { ensureMcpJson, shouldSkip } from "./_agent-setup.mjs";
|
|
13
|
+
|
|
14
|
+
const root = process.env.INIT_CWD || process.cwd();
|
|
15
|
+
|
|
16
|
+
const skip = shouldSkip(root);
|
|
17
|
+
if (skip) process.exit(0); // silent: CI / opt-out / self-install / no consumer project
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const r = ensureMcpJson(root);
|
|
21
|
+
if (r === "present") process.exit(0); // already configured — stay quiet
|
|
22
|
+
console.log(
|
|
23
|
+
`\n @godxjp/ui → registered the godx-ui MCP in .mcp.json (${r}).\n` +
|
|
24
|
+
" Your agent now has live component + audit guidance. Restart it to pick up the MCP.\n" +
|
|
25
|
+
" For the full forcing-kit (auto-audit on every edit + workflow mandate):\n" +
|
|
26
|
+
" npx @godxjp/ui init-agent\n",
|
|
27
|
+
);
|
|
28
|
+
} catch {
|
|
29
|
+
// Never fail an install over optional setup.
|
|
30
|
+
}
|
|
31
|
+
process.exit(0);
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* node packages/godx-ui/scripts/ui-audit.mjs src/ui lib/ # custom scan dirs
|
|
18
18
|
*/
|
|
19
19
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
20
|
-
import { join, relative } from "node:path";
|
|
20
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
21
21
|
|
|
22
22
|
const CWD = process.cwd();
|
|
23
23
|
const args = process.argv.slice(2);
|
|
@@ -31,7 +31,23 @@ const SCAN_DIRS = dirArgs.length
|
|
|
31
31
|
const PALETTE =
|
|
32
32
|
"red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|gray|grey|slate|zinc|neutral|stone";
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Emoji in product text — Unicode "Extended_Pictographic" set (UTS #51). Catches
|
|
36
|
+
* ✅🎉🔥🚀 etc. but NOT typographic punctuation (· — × ✓), which have their own rules.
|
|
37
|
+
*/
|
|
38
|
+
const EMOJI = /\p{Extended_Pictographic}/u;
|
|
39
|
+
/** Regional-indicator pairs = emoji flags (🇯🇵) — broken on Win/Linux; use Intl.DisplayNames. */
|
|
40
|
+
const EMOJI_FLAG = /\p{Regional_Indicator}/u;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @type {{id:string, severity:'error'|'warn', test:RegExp, message:string, standard?:string}[]}
|
|
44
|
+
*
|
|
45
|
+
* `standard` cites the international spec a rule enforces, so a finding is auditable
|
|
46
|
+
* against a real norm rather than house taste alone:
|
|
47
|
+
* WCAG 2.2 (W3C Rec) · WAI-ARIA 1.2 + APG (W3C) · ECMA-402 Intl (TC39) ·
|
|
48
|
+
* Unicode UTS #51 emoji · BCP-47 · ISO 4217 / 3166 / 8601 · IANA tz ·
|
|
49
|
+
* CSS Logical Properties L1 (W3C) · HTML Living Standard (WHATWG).
|
|
50
|
+
*/
|
|
35
51
|
const RULES = [
|
|
36
52
|
{
|
|
37
53
|
id: "no-space-xy",
|
|
@@ -177,8 +193,118 @@ const RULES = [
|
|
|
177
193
|
message:
|
|
178
194
|
"Prefer semantic tokens (text-primary-foreground, bg-background…) over raw white/black (rules §4).",
|
|
179
195
|
},
|
|
196
|
+
|
|
197
|
+
// ─── International-standard a11y / i18n / RTL rules (WARN — guide the agent, never block) ───
|
|
198
|
+
{
|
|
199
|
+
id: "no-emoji-in-ui",
|
|
200
|
+
severity: "warn",
|
|
201
|
+
test: EMOJI,
|
|
202
|
+
standard: "Unicode UTS #51 (emoji) · WCAG 2.2 SC 1.1.1",
|
|
203
|
+
message:
|
|
204
|
+
"No emoji in product UI (✅🎉🔥…). State the fact quietly in i18n-keyed copy; use a Lucide icon for affordance and a Badge `tone` for status. Emoji break on Win/Linux and pollute the accessible name.",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "no-emoji-flag",
|
|
208
|
+
severity: "warn",
|
|
209
|
+
test: EMOJI_FLAG,
|
|
210
|
+
standard: "ISO 3166-1 · ECMA-402 Intl.DisplayNames · Unicode UTS #51",
|
|
211
|
+
message:
|
|
212
|
+
"No emoji flags (🇯🇵) — they render inconsistently across OSes. Derive country names from Intl.DisplayNames(locale, { type: 'region' }) keyed by ISO 3166-1 alpha-2.",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "no-physical-direction",
|
|
216
|
+
severity: "warn",
|
|
217
|
+
// Physical-edge utilities break RTL. Logical equivalents: ms-/me-/ps-/pe-, start-/end-,
|
|
218
|
+
// text-start/end, border-s/e, rounded-s/e. `-mx-`/`-px-` (both edges) are RTL-safe → not matched.
|
|
219
|
+
test: /\b-?(ml|mr|pl|pr|left|right)-(?:\d|\[|auto|px|full)|\b(?:rounded-[tb]?[lr]|border-[lr]|text-(?:left|right))\b/,
|
|
220
|
+
standard: "W3C CSS Logical Properties & Values L1 · WCAG 2.2 (1.3.2 / reflow)",
|
|
221
|
+
message:
|
|
222
|
+
"Use LOGICAL direction utilities for RTL safety: ms-/me-/ps-/pe-, start-/end-, text-start/end, border-s/e, rounded-s/e — not physical ml-/mr-/pl-/pr-/left-/right-/text-left|right.",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "icon-button-needs-name",
|
|
226
|
+
severity: "warn",
|
|
227
|
+
// An icon-only Button (size="icon") with no author-supplied accessible name. A combobox/icon
|
|
228
|
+
// button's name is computed from author (aria-label / aria-labelledby / title), not glyph content.
|
|
229
|
+
test: /<Button\b(?=[^>]*\bsize=["']icon["'])(?![^>]*\b(?:aria-label|aria-labelledby|title)=)[^>]*>/,
|
|
230
|
+
standard: "WCAG 2.2 SC 4.1.2 · 1.1.1 · WAI-ARIA 1.2",
|
|
231
|
+
message:
|
|
232
|
+
"Icon-only <Button size=\"icon\"> needs an accessible name — add aria-label={t('…')}. The icon is decorative (aria-hidden); the name comes from the author, not the glyph.",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "img-needs-alt",
|
|
236
|
+
severity: "warn",
|
|
237
|
+
test: /<img\b(?![^>]*\balt=)[^>]*>/,
|
|
238
|
+
standard: "WCAG 2.2 SC 1.1.1 · HTML Living Standard (WHATWG)",
|
|
239
|
+
message:
|
|
240
|
+
'Every <img> needs an alt attribute (alt="" for purely decorative images). Prefer the <Avatar>/<AspectRatio> primitives for product imagery.',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "no-positive-tabindex",
|
|
244
|
+
severity: "warn",
|
|
245
|
+
test: /tab[Ii]ndex=(?:["']?[1-9]|\{[1-9])/,
|
|
246
|
+
standard: "WCAG 2.2 SC 2.4.3 · WAI-ARIA APG",
|
|
247
|
+
message:
|
|
248
|
+
"No positive tabIndex — it breaks the natural focus order. Use tabIndex={0} (focusable) or {-1} (programmatic) only; manage roving focus via the primitive.",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "hardcoded-currency",
|
|
252
|
+
severity: "warn",
|
|
253
|
+
// A currency glyph glued to an interpolation in JSX text: `>¥{amount}` / `>{x}円`.
|
|
254
|
+
test: /(?:>[\s]*[¥$€£₫]\s*\{)|(?:\}\s*円)/,
|
|
255
|
+
standard: "ISO 4217 · ECMA-402 Intl.NumberFormat",
|
|
256
|
+
message:
|
|
257
|
+
"Don't hand-format currency (¥{amount}). Use Intl.NumberFormat(locale, { style: 'currency', currency }) — ISO 4217 code drives the symbol and minor units per locale.",
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: "raw-intl-date",
|
|
261
|
+
severity: "warn",
|
|
262
|
+
test: /\.toLocale(?:Date|Time)?String\(\s*\)|new Date\([^)]*\)\.(?:getMonth|getDate|getFullYear)\(\)\s*\+/,
|
|
263
|
+
standard: "ISO 8601 · IANA tz database · ECMA-402 Intl.DateTimeFormat",
|
|
264
|
+
message:
|
|
265
|
+
"Don't hand-build or locale-default dates. Use formatDate from @godxjp/ui/datetime (Intl.DateTimeFormat + IANA timezone + ISO-8601), which respects the AppProvider locale/timezone.",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "hand-rolled-close-glyph",
|
|
269
|
+
severity: "warn",
|
|
270
|
+
// A literal close glyph as JSX text — almost always a hand-rolled dismiss that should be a slot.
|
|
271
|
+
test: />\s*[✕✖×╳]\s*</,
|
|
272
|
+
standard: "WAI-ARIA 1.2 (dialog) · WCAG 2.2 SC 4.1.2 · @godxjp/ui Alert/Dialog anatomy",
|
|
273
|
+
message:
|
|
274
|
+
"Don't hand-roll a ✕ close. Pass onDismiss to <Alert> (renders the × top-right with an aria-label), or use <Dialog>/<Sheet> which ship their own labelled close. A bare glyph has no accessible name.",
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: "no-em-dash-in-copy",
|
|
278
|
+
severity: "warn",
|
|
279
|
+
// Em-dash (U+2014) in JSX text — dxs-kintai uses the middot `·` for JP/EN pairs and calm copy.
|
|
280
|
+
test: /[A-Za-z0-9-ヿ一-鿿]\s*—\s*[A-Za-z0-9-ヿ一-鿿]/,
|
|
281
|
+
standard: "@godxjp/ui dxs-kintai typography (best-ux) · Unicode punctuation",
|
|
282
|
+
message:
|
|
283
|
+
"No em-dash (—) in product copy. Use a middot `·` for JP/EN label pairs, or restructure into two calm sentences. Keep copy factual and quiet.",
|
|
284
|
+
},
|
|
180
285
|
];
|
|
181
286
|
|
|
287
|
+
/**
|
|
288
|
+
* `--rules` — print the full rule catalog (id · severity · standard · message) as JSON and exit.
|
|
289
|
+
* The single source of truth other tooling (the MCP `list_audit_rules`, the sync guard) reads,
|
|
290
|
+
* so the executable rules and the agent-facing docs never drift.
|
|
291
|
+
*/
|
|
292
|
+
if (args.includes("--rules")) {
|
|
293
|
+
process.stdout.write(
|
|
294
|
+
JSON.stringify(
|
|
295
|
+
RULES.map((r) => ({
|
|
296
|
+
id: r.id,
|
|
297
|
+
severity: r.severity,
|
|
298
|
+
standard: r.standard ?? null,
|
|
299
|
+
message: r.message,
|
|
300
|
+
})),
|
|
301
|
+
null,
|
|
302
|
+
2,
|
|
303
|
+
) + "\n",
|
|
304
|
+
);
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
|
|
182
308
|
/**
|
|
183
309
|
* Blank out `//` line comments and block comments (incl. JSDoc) — replacing them with spaces and
|
|
184
310
|
* preserving newlines so line numbers stay accurate — while KEEPING string/template literals (the
|
|
@@ -242,6 +368,15 @@ function isSuppressed(ruleId, sameLine, prevLine) {
|
|
|
242
368
|
}
|
|
243
369
|
|
|
244
370
|
function walk(dir, acc = []) {
|
|
371
|
+
// Accept a FILE path directly (the per-file editor hook passes one), not just a directory.
|
|
372
|
+
try {
|
|
373
|
+
if (statSync(dir).isFile()) {
|
|
374
|
+
if (dir.endsWith(".tsx") || dir.endsWith(".ts")) acc.push(dir);
|
|
375
|
+
return acc;
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
return acc;
|
|
379
|
+
}
|
|
245
380
|
let entries;
|
|
246
381
|
try {
|
|
247
382
|
entries = readdirSync(dir);
|
|
@@ -266,9 +401,17 @@ function walk(dir, acc = []) {
|
|
|
266
401
|
const CARD_FLUSH =
|
|
267
402
|
/<Card(?![^>]*\bp-0\b)(?:\s[^>]*)?>\s*<(?!CardContent|CardHeader|CardCover|CardFooter|\/Card)/g;
|
|
268
403
|
|
|
404
|
+
// Structural: a bare <label>/<Label> paired with a TEXT control (its sibling) instead of a
|
|
405
|
+
// <FormField>. FormField OWNS the label↔control association (htmlFor/id), aria-describedby/
|
|
406
|
+
// error wiring, AND the field rhythm (label gap + field spacing) — a hand-rolled Label+Input
|
|
407
|
+
// loses all of it (the cramped login-form failure mode). Checkbox/Radio/Switch use Field/Label
|
|
408
|
+
// legitimately, so they are NOT matched. Whole-file pass (the pair spans lines).
|
|
409
|
+
const BARE_FIELD =
|
|
410
|
+
/<(?:label|Label)\b[^>]*>[\s\S]{0,240}?<\/(?:label|Label)>\s*<(?:Input|Select|Textarea|NumberInput|SearchInput|SearchSelect|DatePicker|DateRangePicker|TimePicker|MonthPicker|MonthRangePicker|Cascader|TreeSelect|input)\b/g;
|
|
411
|
+
|
|
269
412
|
const findings = [];
|
|
270
413
|
for (const dir of SCAN_DIRS) {
|
|
271
|
-
for (const file of walk(join(CWD, dir))) {
|
|
414
|
+
for (const file of walk(isAbsolute(dir) ? dir : join(CWD, dir))) {
|
|
272
415
|
const rel = relative(CWD, file);
|
|
273
416
|
const content = readFileSync(file, "utf8");
|
|
274
417
|
const origLines = content.split("\n");
|
|
@@ -282,6 +425,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
282
425
|
line: i + 1,
|
|
283
426
|
rule: rule.id,
|
|
284
427
|
severity: rule.severity,
|
|
428
|
+
standard: rule.standard,
|
|
285
429
|
message: rule.message,
|
|
286
430
|
snippet: (origLines[i] ?? line).trim().slice(0, 120),
|
|
287
431
|
});
|
|
@@ -302,6 +446,23 @@ for (const dir of SCAN_DIRS) {
|
|
|
302
446
|
snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
|
|
303
447
|
});
|
|
304
448
|
}
|
|
449
|
+
for (const match of scanContent.matchAll(BARE_FIELD)) {
|
|
450
|
+
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
451
|
+
if (
|
|
452
|
+
isSuppressed("bare-control-needs-formfield", origLines[lineNo - 1], origLines[lineNo - 2])
|
|
453
|
+
)
|
|
454
|
+
continue;
|
|
455
|
+
findings.push({
|
|
456
|
+
file: rel,
|
|
457
|
+
line: lineNo,
|
|
458
|
+
rule: "bare-control-needs-formfield",
|
|
459
|
+
severity: "warn",
|
|
460
|
+
standard: "WCAG 2.2 SC 1.3.1 · 3.3.2 · @godxjp/ui FormField (cardinal rule 227)",
|
|
461
|
+
message:
|
|
462
|
+
"A bare <Label>/<label> paired with a control — wrap the field in <FormField label=…>. FormField owns the label↔control id wiring, aria-describedby/error, AND the field rhythm (label gap + field spacing); a hand-rolled Label+Input loses all of it (the cramped/mis-spaced form).",
|
|
463
|
+
snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
305
466
|
}
|
|
306
467
|
}
|
|
307
468
|
|
|
@@ -329,6 +490,7 @@ if (asJson) {
|
|
|
329
490
|
const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
|
|
330
491
|
console.log(`${tag} ${C.bold}${f.file}:${f.line}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
|
|
331
492
|
console.log(` ${f.message}`);
|
|
493
|
+
if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
|
|
332
494
|
console.log(` ${C.dim}${f.snippet}${C.reset}`);
|
|
333
495
|
}
|
|
334
496
|
console.log(
|