@godxjp/ui 13.17.3 → 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 (63) 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/card.d.ts +2 -1
  18. package/dist/components/data-display/index.d.ts +2 -0
  19. package/dist/components/data-display/index.js +2 -0
  20. package/dist/components/data-display/list-row.d.ts +24 -0
  21. package/dist/components/data-display/list-row.js +31 -0
  22. package/dist/components/data-entry/command.d.ts +10 -10
  23. package/dist/components/data-entry/transfer.js +1 -1
  24. package/dist/components/general/button.d.ts +1 -0
  25. package/dist/components/general/button.js +3 -0
  26. package/dist/components/general/index.d.ts +2 -0
  27. package/dist/components/general/index.js +4 -1
  28. package/dist/components/general/logo.d.ts +28 -0
  29. package/dist/components/general/logo.js +45 -0
  30. package/dist/components/layout/index.d.ts +1 -1
  31. package/dist/components/layout/sidebar.js +32 -24
  32. package/dist/components/layout/topbar.d.ts +19 -2
  33. package/dist/components/layout/topbar.js +7 -128
  34. package/dist/i18n/messages/en.json +5 -0
  35. package/dist/i18n/messages/ja.json +5 -0
  36. package/dist/i18n/messages/vi.json +5 -0
  37. package/dist/props/components/charts.prop.d.ts +89 -0
  38. package/dist/props/components/charts.prop.js +0 -0
  39. package/dist/props/components/general.prop.d.ts +5 -0
  40. package/dist/props/components/index.d.ts +2 -1
  41. package/dist/props/components/layout.prop.d.ts +16 -33
  42. package/dist/props/registry.d.ts +170 -11
  43. package/dist/props/registry.js +112 -11
  44. package/dist/styles/card-layout.css +5 -3
  45. package/dist/styles/chart-layout.css +37 -0
  46. package/dist/styles/data-display-layout.css +38 -0
  47. package/dist/styles/index.css +1 -0
  48. package/dist/styles/shell-layout.css +39 -0
  49. package/dist/styles/text-layout.css +3 -0
  50. package/dist/tokens/base.css +2 -0
  51. package/dist/tokens/components/card.css +5 -0
  52. package/dist/tokens/components/list-row.css +9 -0
  53. package/dist/tokens/components/logo.css +11 -0
  54. package/dist/tokens/foundation.css +13 -0
  55. package/package.json +42 -7
  56. package/scripts/_agent-setup.mjs +145 -0
  57. package/scripts/audit-hook.mjs +66 -0
  58. package/scripts/cli.mjs +26 -0
  59. package/scripts/init-agent-kit.mjs +52 -0
  60. package/scripts/postinstall.mjs +31 -0
  61. package/scripts/ui-audit.mjs +165 -3
  62. package/scripts/visual-audit-rules.mjs +119 -0
  63. package/scripts/visual-audit.mjs +261 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Visual-audit decision logic — PURE functions, zero browser/DOM dependency.
3
+ *
4
+ * `scripts/visual-audit.mjs` collects measurements from a REAL rendered page
5
+ * (Playwright) and feeds them here; the browser glue stays thin and these rules
6
+ * are fully unit-testable without a browser. The same functions are injected into
7
+ * the page for in-browser heuristics.
8
+ *
9
+ * Each rule cites the international standard it enforces — see VISUAL_RULES.
10
+ */
11
+
12
+ /** Catalog (agent-facing mirror lives in mcp/src/data/visual-rules.ts; kept in sync by a guard). */
13
+ export const VISUAL_RULES = [
14
+ {
15
+ id: "axe-violations",
16
+ severity: "warn",
17
+ category: "a11y",
18
+ standard: "WCAG 2.2 A/AA · WAI-ARIA 1.2 (axe-core engine)",
19
+ fix: "Fix each axe violation at its node — contrast (1.4.3), name/role/value (4.1.2), ARIA, landmarks. axe runs on the REAL DOM, catching what static analysis cannot.",
20
+ },
21
+ {
22
+ id: "target-size-min",
23
+ severity: "warn",
24
+ category: "a11y",
25
+ standard: "WCAG 2.2 SC 2.5.8 (24×24 AA) · 2.5.5 (44×44 AAA)",
26
+ fix: "Interactive targets must be ≥24×24 CSS px. Size controls from the --control-height tier; don't shrink icon buttons below it.",
27
+ },
28
+ {
29
+ id: "oversaturated-accent",
30
+ severity: "warn",
31
+ category: "color",
32
+ standard: "@godxjp/ui dxs-kintai 渋み (OKLCH chroma ≤ 0.18)",
33
+ fix: "Desaturate brand/primary surfaces — keep OKLCH chroma ≤ 0.18. Read --primary tokens; never paint a full-width bar in raw vivid blue.",
34
+ },
35
+ {
36
+ id: "emoji-rendered",
37
+ severity: "warn",
38
+ category: "i18n",
39
+ standard: "Unicode UTS #51 · WCAG 2.2 SC 1.1.1",
40
+ fix: "Remove emoji from rendered product text; use quiet i18n copy + a Lucide icon + a Badge tone for status.",
41
+ },
42
+ {
43
+ id: "alert-controls-misplaced",
44
+ severity: "warn",
45
+ category: "layout",
46
+ standard: "@godxjp/ui Alert anatomy · WAI-ARIA 1.2 · WCAG 2.2 SC 4.1.2",
47
+ fix: "Use <Alert>: ONE leading tone icon, <Alert.Actions> normal-width trailing-right, onDismiss × top-right, ONE horizontal row — never a vertical stack with a full-width action bar or a centered ✕.",
48
+ },
49
+ ];
50
+
51
+ /** sRGB channel (0–255) → linear-light component. */
52
+ function srgbToLinear(c) {
53
+ const x = c / 255;
54
+ return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
55
+ }
56
+
57
+ /**
58
+ * OKLCH chroma of an sRGB color — the perceptual saturation the 渋み rule bounds.
59
+ * sRGB → linear → OKLab (Björn Ottosson) → chroma = hypot(a, b).
60
+ * @param {{r:number,g:number,b:number}} rgb 0–255 channels
61
+ * @returns {number} OKLCH chroma (~0 grey … ~0.37 max sRGB)
62
+ */
63
+ export function oklchChroma({ r, g, b }) {
64
+ const lr = srgbToLinear(r);
65
+ const lg = srgbToLinear(g);
66
+ const lb = srgbToLinear(b);
67
+ const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
68
+ const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
69
+ const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
70
+ const l_ = Math.cbrt(l);
71
+ const m_ = Math.cbrt(m);
72
+ const s_ = Math.cbrt(s);
73
+ const oa = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
74
+ const ob = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
75
+ return Math.hypot(oa, ob);
76
+ }
77
+
78
+ /** dxs-kintai restraint bound. A signal/brand surface above this chroma "screams". */
79
+ export const CHROMA_LIMIT = 0.18;
80
+
81
+ /** @returns {boolean} true when an accent surface exceeds the 渋み chroma limit. */
82
+ export function isOversaturated(rgb, limit = CHROMA_LIMIT) {
83
+ return oklchChroma(rgb) > limit;
84
+ }
85
+
86
+ /** WCAG 2.2 SC 2.5.8 — a tappable target smaller than 24×24 CSS px. */
87
+ export function isUndersizedTarget({ width, height }, min = 24) {
88
+ return width > 0 && height > 0 && (width < min || height < min);
89
+ }
90
+
91
+ /** Unicode UTS #51 — any emoji pictograph in rendered text (excludes · — × ✓). */
92
+ const EMOJI_RE = /\p{Extended_Pictographic}/u;
93
+ export function hasEmoji(text) {
94
+ return typeof text === "string" && EMOJI_RE.test(text);
95
+ }
96
+
97
+ /**
98
+ * Alert/banner anatomy — given measurements of a notification region, list the
99
+ * structural mistakes (the "stacked notification banner" tell).
100
+ *
101
+ * @param {{iconCount:number, actionWidthRatio:number, direction:'row'|'column',
102
+ * hasDismiss:boolean, dismissCorner:'top-right'|'other'|null}} m
103
+ * @returns {string[]} human-readable issues (empty = correct anatomy)
104
+ */
105
+ export function alertControlIssues(m) {
106
+ const issues = [];
107
+ if (m.iconCount > 1) issues.push("multiple icons — Alert takes ONE leading tone icon");
108
+ if (m.actionWidthRatio > 0.85)
109
+ issues.push("action is a full-width bar — use a normal-width Button in <Alert.Actions>");
110
+ if (m.direction === "column")
111
+ issues.push("controls stacked vertically — Alert is ONE horizontal row");
112
+ if (m.hasDismiss && m.dismissCorner !== "top-right")
113
+ issues.push("dismiss ✕ not in the top-right corner — pass onDismiss to <Alert>");
114
+ return issues;
115
+ }
116
+
117
+ export function findVisualRule(id) {
118
+ return VISUAL_RULES.find((r) => r.id === id);
119
+ }
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * godxjp-ui VISUAL audit — the runtime counterpart to the static ui-audit.mjs.
4
+ *
5
+ * Static audit (ui-audit.mjs) reads SOURCE with regexes — zero deps, fast, runs on
6
+ * every save. This audit drives a REAL browser (Playwright) over the running app and
7
+ * runs axe-core + computed-style/layout heuristics, catching what source can't see:
8
+ * colour contrast, target size, OKLCH chroma of a rendered accent, emoji that survived
9
+ * to the DOM, and a mis-laid-out notification banner. Warnings by default (agent
10
+ * guidance) — pass --strict to exit non-zero. Run it BEFORE an AI/human visual review.
11
+ *
12
+ * Playwright + @axe-core/playwright are OPTIONAL peer deps — installed only by apps that
13
+ * run this audit, so the static audit and the library stay browser-free.
14
+ *
15
+ * Usage (from the consuming app, against its running dev/preview server):
16
+ * node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 /invoices /settings
17
+ * node node_modules/@godxjp/ui/scripts/visual-audit.mjs --format json http://localhost:5173
18
+ * node node_modules/@godxjp/ui/scripts/visual-audit.mjs --strict http://localhost:5173 # CI gate
19
+ * PLAYWRIGHT_CHROMIUM_EXECUTABLE=/usr/bin/chromium node …/visual-audit.mjs http://… # system browser
20
+ */
21
+ import {
22
+ VISUAL_RULES,
23
+ isOversaturated,
24
+ oklchChroma,
25
+ isUndersizedTarget,
26
+ hasEmoji,
27
+ alertControlIssues,
28
+ } from "./visual-audit-rules.mjs";
29
+
30
+ const args = process.argv.slice(2);
31
+ const asJson = args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
32
+ const strict = args.includes("--strict");
33
+ const listRules = args.includes("--rules");
34
+
35
+ if (listRules) {
36
+ process.stdout.write(JSON.stringify(VISUAL_RULES, null, 2) + "\n");
37
+ process.exit(0);
38
+ }
39
+
40
+ const C = {
41
+ red: "\x1b[31m",
42
+ yellow: "\x1b[33m",
43
+ dim: "\x1b[2m",
44
+ bold: "\x1b[1m",
45
+ reset: "\x1b[0m",
46
+ };
47
+
48
+ const urls = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--format");
49
+ if (urls.length === 0) {
50
+ console.error(
51
+ "usage: visual-audit.mjs [--format json] [--strict] <baseUrl> [route …]\n" +
52
+ " (baseUrl is the running app; extra args are routes appended to it)",
53
+ );
54
+ process.exit(2);
55
+ }
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
+
62
+ /** Optional-peer loader — print a clear install hint instead of a stack trace. */
63
+ async function loadDeps() {
64
+ try {
65
+ const [{ chromium }, axe] = await Promise.all([
66
+ import("playwright"),
67
+ import("@axe-core/playwright"),
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
+ }
79
+ }
80
+
81
+ /** Collect raw measurements from the rendered page; the PURE rules run back in node. */
82
+ /* c8 ignore start — runs inside the browser, exercised in the consumer/CI env. */
83
+ function collectInPage() {
84
+ const rgb = (s) => {
85
+ const m = s && s.match(/rgba?\(([^)]+)\)/);
86
+ if (!m) return null;
87
+ const [r, g, b, a = "1"] = m[1].split(",").map((x) => x.trim());
88
+ return { r: +r, g: +g, b: +b, a: +a };
89
+ };
90
+ const visible = (el) => {
91
+ const r = el.getBoundingClientRect();
92
+ const cs = getComputedStyle(el);
93
+ return r.width > 0 && r.height > 0 && cs.visibility !== "hidden" && cs.display !== "none";
94
+ };
95
+
96
+ // Accent surfaces — buttons / primary CTAs / banners whose background carries colour.
97
+ const accents = [];
98
+ for (const el of document.querySelectorAll(
99
+ "button, [role=button], a[class*=primary], [data-slot=alert], [class*=banner], [class*=notification]",
100
+ )) {
101
+ if (!visible(el)) continue;
102
+ const c = rgb(getComputedStyle(el).backgroundColor);
103
+ if (c && c.a > 0.5)
104
+ accents.push({ rgb: { r: c.r, g: c.g, b: c.b }, tag: el.tagName.toLowerCase() });
105
+ }
106
+
107
+ // Interactive targets — for the 24×24 minimum.
108
+ const targets = [];
109
+ for (const el of document.querySelectorAll(
110
+ "a[href], button, [role=button], input:not([type=hidden]), select, [tabindex]:not([tabindex='-1'])",
111
+ )) {
112
+ if (!visible(el)) continue;
113
+ const r = el.getBoundingClientRect();
114
+ const name = (el.getAttribute("aria-label") || el.textContent || "").trim().slice(0, 40);
115
+ targets.push({ width: Math.round(r.width), height: Math.round(r.height), name });
116
+ }
117
+
118
+ // Emoji that survived to the rendered DOM.
119
+ const text = document.body ? document.body.innerText : "";
120
+
121
+ // Notification banners — measure their anatomy.
122
+ const alerts = [];
123
+ for (const el of document.querySelectorAll(
124
+ "[role=alert], [role=status], [data-slot=alert], [class*=banner], [class*=notification]",
125
+ )) {
126
+ if (!visible(el)) continue;
127
+ const box = el.getBoundingClientRect();
128
+ const icons = el.querySelectorAll("svg, img").length;
129
+ const btns = [...el.querySelectorAll("button, [role=button], a[href]")];
130
+ const actionWidthRatio = btns.length
131
+ ? Math.max(...btns.map((b) => b.getBoundingClientRect().width)) / (box.width || 1)
132
+ : 0;
133
+ const cs = getComputedStyle(el);
134
+ const direction =
135
+ cs.display.includes("flex") && cs.flexDirection.startsWith("column")
136
+ ? "column"
137
+ : cs.display.includes("flex")
138
+ ? "row"
139
+ : "column";
140
+ const dismiss = btns.find((b) =>
141
+ /close|dismiss|閉じる|×|✕/i.test(b.getAttribute("aria-label") || b.textContent || ""),
142
+ );
143
+ let dismissCorner = null;
144
+ if (dismiss) {
145
+ const d = dismiss.getBoundingClientRect();
146
+ const nearTop = d.top - box.top < box.height * 0.4;
147
+ const nearRight = box.right - d.right < box.width * 0.25;
148
+ dismissCorner = nearTop && nearRight ? "top-right" : "other";
149
+ }
150
+ alerts.push({
151
+ iconCount: icons,
152
+ actionWidthRatio,
153
+ direction,
154
+ hasDismiss: !!dismiss,
155
+ dismissCorner,
156
+ });
157
+ }
158
+
159
+ return { accents, targets, text, alerts };
160
+ }
161
+ /* c8 ignore stop */
162
+
163
+ async function run() {
164
+ const { chromium, AxeBuilder } = await loadDeps();
165
+ const browser = await chromium.launch({
166
+ executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined,
167
+ });
168
+ const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
169
+ const findings = [];
170
+ const add = (ruleId, url, message) => {
171
+ const rule = VISUAL_RULES.find((r) => r.id === ruleId);
172
+ findings.push({
173
+ url,
174
+ rule: ruleId,
175
+ severity: rule?.severity ?? "warn",
176
+ standard: rule?.standard,
177
+ message,
178
+ });
179
+ };
180
+
181
+ for (const url of targets) {
182
+ try {
183
+ await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
184
+ } catch (e) {
185
+ add("axe-violations", url, `could not load page: ${e.message}`);
186
+ continue;
187
+ }
188
+
189
+ // 1) axe-core — real WCAG/ARIA engine.
190
+ const { violations } = await new AxeBuilder({ page }).analyze();
191
+ for (const v of violations) {
192
+ add(
193
+ "axe-violations",
194
+ url,
195
+ `${v.id} (${v.impact}) — ${v.help} [${v.nodes.length} node(s)] ${v.helpUrl}`,
196
+ );
197
+ }
198
+
199
+ // 2) computed-style / layout heuristics via the PURE rules.
200
+ const m = await page.evaluate(collectInPage);
201
+ for (const a of m.accents) {
202
+ if (isOversaturated(a.rgb))
203
+ add(
204
+ "oversaturated-accent",
205
+ url,
206
+ `<${a.tag}> background OKLCH chroma ${oklchChroma(a.rgb).toFixed(3)} > 0.18 (rgb ${a.rgb.r},${a.rgb.g},${a.rgb.b})`,
207
+ );
208
+ }
209
+ for (const t of m.targets) {
210
+ if (isUndersizedTarget(t))
211
+ add(
212
+ "target-size-min",
213
+ url,
214
+ `target "${t.name || "(unnamed)"}" is ${t.width}×${t.height}px (< 24×24)`,
215
+ );
216
+ }
217
+ if (hasEmoji(m.text)) {
218
+ const found = (m.text.match(/\p{Extended_Pictographic}/gu) || []).slice(0, 8).join(" ");
219
+ add("emoji-rendered", url, `emoji rendered in product text: ${found}`);
220
+ }
221
+ m.alerts.forEach((al, i) => {
222
+ for (const issue of alertControlIssues(al))
223
+ add("alert-controls-misplaced", url, `banner #${i + 1}: ${issue}`);
224
+ });
225
+ }
226
+
227
+ await browser.close();
228
+ report(findings);
229
+ }
230
+
231
+ function report(findings) {
232
+ const errors = findings.filter((f) => f.severity === "error");
233
+ const warnings = findings.filter((f) => f.severity === "warn");
234
+ if (asJson) {
235
+ process.stdout.write(
236
+ JSON.stringify(
237
+ { summary: { errors: errors.length, warnings: warnings.length }, findings },
238
+ null,
239
+ 2,
240
+ ) + "\n",
241
+ );
242
+ } else {
243
+ for (const f of findings) {
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}`);
248
+ }
249
+ console.log(
250
+ `\ngodxjp-ui visual-audit: ${C.red}${errors.length} error(s)${C.reset}, ${C.yellow}${warnings.length} warning(s)${C.reset} across ${targets.length} page(s).`,
251
+ );
252
+ if (findings.length === 0) console.log("✓ No visual/runtime a11y violations found.");
253
+ }
254
+ // Warnings never block; --strict turns any finding into a non-zero exit (CI gate).
255
+ process.exit(strict && findings.length > 0 ? 1 : errors.length > 0 ? 1 : 0);
256
+ }
257
+
258
+ run().catch((e) => {
259
+ console.error(`visual-audit failed: ${e.message}`);
260
+ process.exit(2);
261
+ });