@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.
Files changed (60) hide show
  1. package/dist/components/charts/area-chart.d.ts +8 -0
  2. package/dist/components/charts/area-chart.js +8 -0
  3. package/dist/components/charts/bar-chart.d.ts +8 -0
  4. package/dist/components/charts/bar-chart.js +8 -0
  5. package/dist/components/charts/chart-cartesian.d.ts +31 -0
  6. package/dist/components/charts/chart-cartesian.js +178 -0
  7. package/dist/components/charts/chart-frame.d.ts +44 -0
  8. package/dist/components/charts/chart-frame.js +72 -0
  9. package/dist/components/charts/chart-summary.d.ts +21 -0
  10. package/dist/components/charts/chart-summary.js +23 -0
  11. package/dist/components/charts/index.d.ts +16 -0
  12. package/dist/components/charts/index.js +12 -0
  13. package/dist/components/charts/line-chart.d.ts +8 -0
  14. package/dist/components/charts/line-chart.js +8 -0
  15. package/dist/components/charts/pie-chart.d.ts +8 -0
  16. package/dist/components/charts/pie-chart.js +73 -0
  17. package/dist/components/data-display/index.d.ts +2 -0
  18. package/dist/components/data-display/index.js +2 -0
  19. package/dist/components/data-display/list-row.d.ts +24 -0
  20. package/dist/components/data-display/list-row.js +31 -0
  21. package/dist/components/data-entry/command.d.ts +10 -10
  22. package/dist/components/data-entry/transfer.js +1 -1
  23. package/dist/components/general/button.d.ts +1 -0
  24. package/dist/components/general/button.js +3 -0
  25. package/dist/components/general/index.d.ts +2 -0
  26. package/dist/components/general/index.js +4 -1
  27. package/dist/components/general/logo.d.ts +28 -0
  28. package/dist/components/general/logo.js +45 -0
  29. package/dist/components/layout/index.d.ts +1 -1
  30. package/dist/components/layout/sidebar.js +32 -24
  31. package/dist/components/layout/topbar.d.ts +19 -2
  32. package/dist/components/layout/topbar.js +7 -128
  33. package/dist/i18n/messages/en.json +5 -0
  34. package/dist/i18n/messages/ja.json +5 -0
  35. package/dist/i18n/messages/vi.json +5 -0
  36. package/dist/props/components/charts.prop.d.ts +89 -0
  37. package/dist/props/components/charts.prop.js +0 -0
  38. package/dist/props/components/general.prop.d.ts +5 -0
  39. package/dist/props/components/index.d.ts +2 -1
  40. package/dist/props/components/layout.prop.d.ts +16 -33
  41. package/dist/props/registry.d.ts +170 -11
  42. package/dist/props/registry.js +112 -11
  43. package/dist/styles/chart-layout.css +37 -0
  44. package/dist/styles/data-display-layout.css +38 -0
  45. package/dist/styles/index.css +1 -0
  46. package/dist/styles/shell-layout.css +39 -0
  47. package/dist/styles/text-layout.css +3 -0
  48. package/dist/tokens/base.css +2 -0
  49. package/dist/tokens/components/list-row.css +9 -0
  50. package/dist/tokens/components/logo.css +11 -0
  51. package/dist/tokens/foundation.css +13 -0
  52. package/package.json +42 -7
  53. package/scripts/_agent-setup.mjs +145 -0
  54. package/scripts/audit-hook.mjs +66 -0
  55. package/scripts/cli.mjs +26 -0
  56. package/scripts/init-agent-kit.mjs +52 -0
  57. package/scripts/postinstall.mjs +31 -0
  58. package/scripts/ui-audit.mjs +165 -3
  59. package/scripts/visual-audit-rules.mjs +119 -0
  60. package/scripts/visual-audit.mjs +261 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "13.17.4",
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-audit": "scripts/ui-audit.mjs"
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
@@ -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);
@@ -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
- /** @type {{id:string, severity:'error'|'warn', test:RegExp, message:string}[]} */
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(