@godxjp/ui 23.0.0 → 23.2.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 (93) hide show
  1. package/dist/app/date-format-labels.d.ts +14 -2
  2. package/dist/app/date-format-labels.js +1 -1
  3. package/dist/app/date-formats.d.ts +14 -2
  4. package/dist/app/date-formats.js +9 -2
  5. package/dist/components/data-display/card.d.ts +45 -2
  6. package/dist/components/data-display/card.js +4 -2
  7. package/dist/components/data-display/data-table.d.ts +8 -1
  8. package/dist/components/data-display/data-table.js +9 -2
  9. package/dist/components/data-display/feature-list.d.ts +26 -0
  10. package/dist/components/data-display/feature-list.js +29 -0
  11. package/dist/components/data-display/index.d.ts +6 -0
  12. package/dist/components/data-display/index.js +6 -0
  13. package/dist/components/data-display/swatch.d.ts +24 -0
  14. package/dist/components/data-display/swatch.js +19 -0
  15. package/dist/components/data-display/thumbnail.d.ts +37 -0
  16. package/dist/components/data-display/thumbnail.js +20 -0
  17. package/dist/components/data-entry/checkbox.js +1 -1
  18. package/dist/components/data-entry/form-field.js +2 -2
  19. package/dist/components/data-entry/input.js +1 -1
  20. package/dist/components/data-entry/number-input.js +27 -6
  21. package/dist/components/data-entry/radio.js +1 -1
  22. package/dist/components/data-entry/switch.js +1 -1
  23. package/dist/components/feedback/dialog.js +2 -2
  24. package/dist/components/feedback/index.d.ts +2 -2
  25. package/dist/components/feedback/index.js +19 -1
  26. package/dist/components/feedback/sheet.js +2 -22
  27. package/dist/components/feedback/skeleton.d.ts +33 -3
  28. package/dist/components/feedback/skeleton.js +154 -3
  29. package/dist/components/layout/flex.d.ts +1 -1
  30. package/dist/components/layout/flex.js +17 -5
  31. package/dist/components/layout/sidebar.js +9 -2
  32. package/dist/components/navigation/filter-bar.js +2 -1
  33. package/dist/components/navigation/tabs-scroll.d.ts +27 -0
  34. package/dist/components/navigation/tabs-scroll.js +36 -1
  35. package/dist/components/navigation/tabs.d.ts +2 -2
  36. package/dist/components/navigation/tabs.js +60 -16
  37. package/dist/i18n/messages/en.json +10 -1
  38. package/dist/i18n/messages/ja.json +10 -1
  39. package/dist/i18n/messages/vi.json +10 -1
  40. package/dist/lib/breakpoint-token.d.ts +8 -0
  41. package/dist/lib/breakpoint-token.js +29 -0
  42. package/dist/props/components/data-display.prop.d.ts +101 -1
  43. package/dist/props/components/feedback.prop.d.ts +85 -1
  44. package/dist/props/components/layout.prop.d.ts +72 -1
  45. package/dist/props/components/navigation.prop.d.ts +80 -2
  46. package/dist/props/registry.d.ts +124 -1
  47. package/dist/props/registry.js +140 -0
  48. package/dist/props/vocabulary/data.prop.d.ts +5 -0
  49. package/dist/props/vocabulary/index.d.ts +1 -1
  50. package/dist/styles/alert-layout.css +139 -0
  51. package/dist/styles/card-layout.css +48 -0
  52. package/dist/styles/control.css +41 -5
  53. package/dist/styles/data-display-layout.css +88 -0
  54. package/dist/styles/focus-ring.css +28 -0
  55. package/dist/styles/form-layout.css +4 -0
  56. package/dist/styles/layout.css +2 -1
  57. package/dist/styles/navigation-layout.css +89 -5
  58. package/dist/styles/shell-layout.css +22 -0
  59. package/dist/styles/table-layout.css +5 -2
  60. package/dist/tokens/components/card.css +2 -0
  61. package/dist/tokens/components/control.css +8 -3
  62. package/dist/tokens/components/data-display.css +23 -0
  63. package/dist/tokens/components/feedback.css +18 -0
  64. package/dist/tokens/components/navigation.css +4 -0
  65. package/dist/tokens/foundation.css +18 -16
  66. package/docs/CONSUMER-RULES.md +8 -3
  67. package/docs/CUSTOMER-THEMING.md +18 -8
  68. package/docs/DATETIME.md +11 -2
  69. package/docs/DESIGN-AUTHORITY.md +2 -1
  70. package/docs/FRAME-COVERAGE-REPORT.md +29 -3
  71. package/docs/SPACING.md +35 -5
  72. package/docs/TOKENS.md +1 -1
  73. package/docs/assets/shot-landscape.svg +12 -0
  74. package/docs/assets/shot-portrait.svg +10 -0
  75. package/docs/data-display/card/index.tsx +40 -0
  76. package/docs/data-display/data-table/index.tsx +17 -2
  77. package/docs/data-display/feature-list.tsx +159 -0
  78. package/docs/data-display/swatch.tsx +107 -0
  79. package/docs/data-display/table.tsx +1 -1
  80. package/docs/data-display/thumbnail.tsx +128 -0
  81. package/docs/data-entry/segmented-in-filter-row.tsx +77 -0
  82. package/docs/feedback/skeleton.tsx +57 -0
  83. package/docs/layout/flex.tsx +29 -0
  84. package/docs/layout/sidebar.tsx +66 -10
  85. package/docs/navigation/dropdown-menu.tsx +4 -4
  86. package/docs/navigation/tabs.tsx +131 -0
  87. package/docs/roadmap/ai-chat-components.md +1 -2
  88. package/docs/roadmap/tree-components.md +0 -1
  89. package/package.json +12 -21
  90. package/scripts/_agent-setup.mjs +18 -0
  91. package/scripts/consumer-rule.md +16 -0
  92. package/scripts/guinea-pig-skill.md +12 -6
  93. package/scripts/ui-audit.mjs +184 -7
@@ -5,7 +5,8 @@
5
5
  * .claude/skills/frontend-design/rules/ui-standardization.md): use the design system, never
6
6
  * hand-roll.
7
7
  */
8
- import { readdirSync, readFileSync, statSync } from "node:fs";
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
10
  import { isAbsolute, join, relative } from "node:path";
10
11
 
11
12
  const CWD = process.cwd();
@@ -20,11 +21,45 @@ const args = process.argv.slice(2);
20
21
  const asJson = args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
21
22
  const quiet = args.includes("--quiet");
22
23
  const dirArgs = args.filter((a) => !a.startsWith("--") && a !== "json");
23
- const SCAN_DIRS = dirArgs.length
24
- ? dirArgs
25
- : SELF
26
- ? ["src", "docs"]
27
- : ["resources/js/components", "resources/js/pages", "resources/js/layouts"];
24
+
25
+ /**
26
+ * `--changed` — audit what this branch touched, whatever tool touched it.
27
+ *
28
+ * The PostToolUse hook fires on `Write|Edit|MultiEdit`, so an agent editing through a shell
29
+ * (`sed -i`, a heredoc, `cat >`) never triggers it. A consumer measured exactly that: across a
30
+ * long session EVERY `.tsx` edit went through Bash and the audit fired not once (godx-jp/id#497).
31
+ * The hook cannot close it — a Bash call carries no `file_path`. A diff can, and one line in a
32
+ * local gate or in CI then covers every edit path, including the ones nobody thought of.
33
+ */
34
+ function changedFiles() {
35
+ const base =
36
+ spawnSync("git", ["merge-base", "HEAD", "origin/main"], { encoding: "utf8" }).stdout.trim() ||
37
+ "HEAD";
38
+ const run = (a) => spawnSync("git", a, { encoding: "utf8" }).stdout ?? "";
39
+
40
+ return [
41
+ ...new Set(
42
+ [
43
+ run(["diff", "--name-only", "--diff-filter=ACMR", base, "--"]),
44
+ run(["diff", "--name-only", "--diff-filter=ACMR", "--cached"]),
45
+ run(["ls-files", "--others", "--exclude-standard"]),
46
+ ]
47
+ .join("\n")
48
+ .split("\n")
49
+ .map((f) => f.trim())
50
+ .filter((f) => /\.(tsx|jsx)$/.test(f) && existsSync(join(CWD, f))),
51
+ ),
52
+ ];
53
+ }
54
+
55
+ const CHANGED = args.includes("--changed");
56
+ const SCAN_DIRS = CHANGED
57
+ ? changedFiles()
58
+ : dirArgs.length
59
+ ? dirArgs
60
+ : SELF
61
+ ? ["src", "docs"]
62
+ : ["resources/js/components", "resources/js/pages", "resources/js/layouts"];
28
63
 
29
64
  const PALETTE =
30
65
  "red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|gray|grey|slate|zinc|neutral|stone";
@@ -89,6 +124,7 @@ const ATTRS = String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|=>|[^>"'])*`;
89
124
  const RULES = [
90
125
  {
91
126
  id: "no-utility-spacing",
127
+ replacement: "Flex gap / ResponsiveGrid / PageContainer",
92
128
  classOnly: true,
93
129
  scope: "consumer",
94
130
  severity: "error",
@@ -101,6 +137,7 @@ const RULES = [
101
137
  },
102
138
  {
103
139
  id: "no-utility-layout",
140
+ replacement: "Flex (row), Flex direction col (stack), ResponsiveGrid",
104
141
  scope: "consumer",
105
142
  severity: "error",
106
143
  // A hand-rolled flex/grid container. The primitives exist precisely so a row is <Flex>, a
@@ -111,6 +148,7 @@ const RULES = [
111
148
  },
112
149
  {
113
150
  id: "no-hand-rolled-surface",
151
+ replacement: "Card / Badge / Avatar / ListRow / Descriptions / EmptyState / Progress",
114
152
  scope: "consumer",
115
153
  severity: "warn",
116
154
  // rounded + border/bg on the consumer's own element = a fake Card / Badge / Avatar / ListRow
@@ -135,10 +173,11 @@ const RULES = [
135
173
  return /\brounded(?:-(?:full|sm|md|lg|xl|2xl))?\b/.test(cls) && /\b(?:border|bg-)/.test(cls);
136
174
  },
137
175
  message:
138
- "Hand-rolled surface (rounded + border/bg) — use Card, Badge, Avatar, ListRow, Descriptions or EmptyState so height, padding and radius come from the tokens (docs/CONSUMER-RULES.md §4).",
176
+ "Hand-rolled surface (rounded + border/bg) — use Card, Badge, Avatar, ListRow, Descriptions or EmptyState so height, padding and radius come from the tokens (docs/CONSUMER-RULES.md §4). A read-only sample of a colour a USER chose is Swatch, which takes that value as a prop.",
139
177
  },
140
178
  {
141
179
  id: "no-space-xy",
180
+ replacement: "Flex gap",
142
181
  classOnly: true,
143
182
  severity: "error",
144
183
  test: /\bspace-[xy]-\d/,
@@ -146,6 +185,7 @@ const RULES = [
146
185
  },
147
186
  {
148
187
  id: "no-raw-palette-color",
188
+ replacement: "a semantic token (--primary, --muted-foreground, --border)",
149
189
  classOnly: true,
150
190
  severity: "error",
151
191
  test: new RegExp(
@@ -156,6 +196,7 @@ const RULES = [
156
196
  },
157
197
  {
158
198
  id: "status-tone-not-variant",
199
+ replacement: "tone=",
159
200
  severity: "error",
160
201
  spansElement: true,
161
202
  // Only the tone-driven status components (Badge/Tag/StatCard) are wrong here — they expose a
@@ -178,6 +219,7 @@ const RULES = [
178
219
  },
179
220
  {
180
221
  id: "value-callback-on-value-change",
222
+ replacement: "onValueChange",
181
223
  severity: "error",
182
224
  spansElement: true,
183
225
  test: new RegExp(
@@ -190,6 +232,7 @@ const RULES = [
190
232
  },
191
233
  {
192
234
  id: "no-arbitrary-hex",
235
+ replacement: "a semantic token",
193
236
  classOnly: true,
194
237
  severity: "error",
195
238
  test: /(bg|text|border|ring|fill|stroke|from|to|via)-\[#[0-9a-fA-F]{3,8}\]/,
@@ -200,6 +243,7 @@ const RULES = [
200
243
  // (they start with a letter), so token-driven values stay legal (rules §4–§5).
201
244
  {
202
245
  id: "no-arbitrary-spacing",
246
+ replacement: "Flex gap / the spacing scale",
203
247
  classOnly: true,
204
248
  severity: "error",
205
249
  test: /\b(p|px|py|pt|pb|pl|pr|pe|ps|m|mx|my|mt|mb|ml|mr|me|ms|gap|gap-x|gap-y|inset|inset-x|inset-y|top|right|bottom|left)-\[-?\.?\d/,
@@ -208,6 +252,7 @@ const RULES = [
208
252
  },
209
253
  {
210
254
  id: "no-arbitrary-size",
255
+ replacement: "a size token or the component's size prop",
211
256
  classOnly: true,
212
257
  severity: "error",
213
258
  // `min-w-[…]` / `min-h-[…]` are allowed: a MINIMUM dimension is the legit responsive
@@ -220,6 +265,7 @@ const RULES = [
220
265
  },
221
266
  {
222
267
  id: "no-arbitrary-typography",
268
+ replacement: "Text size / weight / tone",
223
269
  classOnly: true,
224
270
  severity: "error",
225
271
  test: /\b(text|leading|tracking|font)-\[-?\.?\d/,
@@ -228,6 +274,7 @@ const RULES = [
228
274
  },
229
275
  {
230
276
  id: "no-arbitrary-radius",
277
+ replacement: "a radius token",
231
278
  classOnly: true,
232
279
  severity: "error",
233
280
  test: /\brounded(?:-[a-z]+)?-\[-?\.?\d/,
@@ -267,6 +314,7 @@ const RULES = [
267
314
  },
268
315
  {
269
316
  id: "no-raw-select",
317
+ replacement: "Select",
270
318
  scope: "consumer-control",
271
319
  severity: "error",
272
320
  test: /<select[\s>]/,
@@ -274,12 +322,14 @@ const RULES = [
274
322
  },
275
323
  {
276
324
  id: "no-raw-table",
325
+ replacement: "DataTable / Table",
277
326
  severity: "error",
278
327
  test: /<table[\s>]/,
279
328
  message: "Use the <Table> family from @godxjp/ui, not a raw <table> (rules §3).",
280
329
  },
281
330
  {
282
331
  id: "no-raw-textarea",
332
+ replacement: "Textarea",
283
333
  scope: "consumer-control",
284
334
  severity: "warn",
285
335
  test: /<textarea[\s>]/,
@@ -287,6 +337,7 @@ const RULES = [
287
337
  },
288
338
  {
289
339
  id: "no-raw-input",
340
+ replacement: "Input (Upload for a file picker)",
290
341
  scope: "consumer-control",
291
342
  severity: "error",
292
343
  spansElement: true,
@@ -295,6 +346,7 @@ const RULES = [
295
346
  },
296
347
  {
297
348
  id: "no-raw-button",
349
+ replacement: "Button",
298
350
  scope: "consumer-control",
299
351
  severity: "error",
300
352
  test: /<button[\s>]/,
@@ -302,6 +354,7 @@ const RULES = [
302
354
  },
303
355
  {
304
356
  id: "card-manual-padding",
357
+ replacement: "CardContent (or CardContent flush)",
305
358
  severity: "error",
306
359
  spansElement: true,
307
360
  test: new RegExp(`<Card\\b${ATTRS}\\bp-[1-9]`, "g"),
@@ -310,6 +363,7 @@ const RULES = [
310
363
  },
311
364
  {
312
365
  id: "no-dark-color-override",
366
+ replacement: "a semantic token, which already answers to the theme",
313
367
  classOnly: true,
314
368
  severity: "warn",
315
369
  test: /\bdark:(bg|text|border|ring|fill|stroke)-/,
@@ -342,6 +396,7 @@ const RULES = [
342
396
  // ─── International-standard a11y / i18n / RTL rules (WARN — guide the agent, never block) ───
343
397
  {
344
398
  id: "no-emoji-in-ui",
399
+ replacement: "a lucide icon",
345
400
  severity: "warn",
346
401
  test: EMOJI,
347
402
  standard: "Unicode UTS #51 (emoji) · WCAG 2.2 SC 1.1.1",
@@ -358,6 +413,7 @@ const RULES = [
358
413
  },
359
414
  {
360
415
  id: "no-physical-direction",
416
+ replacement: "the logical property (ms-/me-, inline-start/inline-end)",
361
417
  classOnly: true,
362
418
  severity: "warn",
363
419
  // Physical-edge utilities break RTL. Logical equivalents: ms-/me-/ps-/pe-, start-/end-,
@@ -465,6 +521,7 @@ if (args.includes("--rules")) {
465
521
  severity: r.severity,
466
522
  standard: r.standard ?? null,
467
523
  message: r.message,
524
+ replacement: r.replacement,
468
525
  })),
469
526
  null,
470
527
  2,
@@ -730,6 +787,22 @@ function jsxOpeningEnd(source, start) {
730
787
  // Structural: a <Card> (without p-0) whose first child is body content rather than a Card
731
788
  // sub-component sits FLUSH (no padding). Per-line regexes can't see across lines, so this is a
732
789
  // whole-file pass. The body must be wrapped in <CardContent> (titles in <CardHeader>).
790
+ /**
791
+ * A `Card` whose entire body is a table. `CardContent` pads 16px inline while
792
+ * `.ui-data-table-surface` draws its own 1px border, so the default composition renders a box
793
+ * inside a box — and a wide table runs PAST the card's edge instead of stopping at it (measured in
794
+ * a consumer at −85px and −519px, godx-jp/id#498). `<CardContent flush>` already solves it. What
795
+ * was missing was the rule that says so.
796
+ *
797
+ * Deliberately narrow: the table must be the DIRECT child. A `<Flex direction="col">` holding
798
+ * filters AND a table is a mixed body, where the padding is correct — the reporter hit that trap
799
+ * with a `childElementCount === 1` test.
800
+ */
801
+ const CARD_TABLE_FLUSH = new RegExp(
802
+ `<CardContent(?![^>]*\\bflush\\b)(?:\\s${ATTRS})?>\\s*<(?:DataTable|Table)\\b`,
803
+ "g",
804
+ );
805
+
733
806
  const CARD_FLUSH = new RegExp(
734
807
  `<Card(?!${ATTRS}\\bp-0\\b)(?:\\s${ATTRS})?>\\s*<(?!CardContent|CardHeader|CardCover|CardFooter|CardBar|\\/Card)`,
735
808
  "g",
@@ -760,7 +833,52 @@ const SIBLING_CARDS = /^([ \t]*)<\/Card>\s*\n\1<Card\b/gm;
760
833
  const ACTIVE_RULES =
761
834
  SELF && !args.includes("--consumer") ? RULES.filter((r) => r.scope !== "consumer") : RULES;
762
835
 
836
+ /**
837
+ * The rule file this package OWNS, and whether the consumer's copy is the one this package writes.
838
+ *
839
+ * `.ai/rules/godxjp-ui.md` opens by promising it is rewritten on every upgrade. A consumer with
840
+ * `ignore-scripts=true` — a sane, increasingly common default — never runs our postinstall, so the
841
+ * promise silently fails: measured in a consumer at `godxjp-ui:version 19.6.0` while the installed
842
+ * package was **23.0.0**, three majors apart (godx-jp/id#513). An agent then follows a rule file
843
+ * describing components that were removed, props that no longer exist and a `role` attribute that
844
+ * is gone — and nothing anywhere says the ground moved.
845
+ *
846
+ * postinstall cannot fix this: it is the thing that did not run. This can, because a consumer runs
847
+ * the audit.
848
+ */
849
+ function staleOwnedRules() {
850
+ if (SELF) return null;
851
+ const target = join(CWD, ".ai", "rules", "godxjp-ui.md");
852
+ if (!existsSync(target)) return null;
853
+
854
+ const stamped = /<!-- godxjp-ui:version ([^\s]+) -->/.exec(readFileSync(target, "utf8"))?.[1];
855
+ let installed;
856
+ try {
857
+ installed = JSON.parse(
858
+ readFileSync(join(CWD, "node_modules", "@godxjp", "ui", "package.json"), "utf8"),
859
+ ).version;
860
+ } catch {
861
+ return null;
862
+ }
863
+ if (!stamped || !installed || stamped === installed) return null;
864
+
865
+ return {
866
+ file: ".ai/rules/godxjp-ui.md",
867
+ line: 1,
868
+ rule: "owned-rules-stale",
869
+ severity: "error",
870
+ message:
871
+ `This file is written by @godxjp/ui and says version ${stamped}, but the installed package ` +
872
+ `is ${installed}. Its rules describe a different library than the one you are building ` +
873
+ "against — most likely because `ignore-scripts=true` kept our postinstall from running.",
874
+ replacement: 'INIT_CWD="$PWD" node node_modules/@godxjp/ui/scripts/postinstall.mjs',
875
+ snippet: `<!-- godxjp-ui:version ${stamped} --> vs installed ${installed}`,
876
+ };
877
+ }
878
+
763
879
  const findings = [];
880
+ const stale = staleOwnedRules();
881
+ if (stale) findings.push(stale);
764
882
  let filesScanned = 0;
765
883
  for (const dir of SCAN_DIRS) {
766
884
  for (const file of walk(isAbsolute(dir) ? dir : join(CWD, dir))) {
@@ -809,6 +927,7 @@ for (const dir of SCAN_DIRS) {
809
927
  severity: rule.severity,
810
928
  standard: rule.standard,
811
929
  message: rule.message,
930
+ replacement: rule.replacement,
812
931
  snippet: (origLines[i] ?? line).trim().slice(0, 120),
813
932
  });
814
933
  }
@@ -844,7 +963,38 @@ for (const dir of SCAN_DIRS) {
844
963
  }
845
964
  }
846
965
  if (!isJsx) continue;
966
+ /**
967
+ * `CARD_FLUSH` wants a Card slot as the IMMEDIATE next element, so any legitimate wrapper
968
+ * trips it. The one that matters is `<Form>`: a submit button in `<CardFooter>` only submits
969
+ * when the `<form>` wraps BOTH the content and the footer, so `<Card><Form><CardContent>` is
970
+ * the only correct shape for that card — and it was reported as an error three times in one
971
+ * consumer file (godx-jp/id#496), on its newest code. A confident error against correct code
972
+ * teaches the reader that the audit is noise.
973
+ *
974
+ * The rule's intent is "the body must not sit flush against the card edges", so ask that:
975
+ * is there a `<CardContent>` before this Card's own `</Card>`? Depth is counted, because a
976
+ * Card can hold a Card.
977
+ */
978
+ const hasBodyBeforeClose = (from) => {
979
+ let depth = 0;
980
+ const tag = /<(\/?)Card(Content|Header|Cover|Footer|Bar)?\b/g;
981
+ tag.lastIndex = from;
982
+ for (let m; (m = tag.exec(scanContent));) {
983
+ const [, closing, slot] = m;
984
+ if (!slot) {
985
+ if (closing) {
986
+ if (depth === 0) return false;
987
+ depth -= 1;
988
+ } else depth += 1;
989
+ continue;
990
+ }
991
+ if (!closing && slot === "Content" && depth <= 1) return true;
992
+ }
993
+ return false;
994
+ };
995
+
847
996
  for (const match of scanContent.matchAll(CARD_FLUSH)) {
997
+ if (hasBodyBeforeClose(match.index)) continue;
848
998
  const lineNo = scanContent.slice(0, match.index).split("\n").length;
849
999
  if (suppressed("card-needs-content", lineNo - 1)) continue;
850
1000
  findings.push({
@@ -857,6 +1007,24 @@ for (const dir of SCAN_DIRS) {
857
1007
  snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
858
1008
  });
859
1009
  }
1010
+ for (const match of scanContent.matchAll(CARD_TABLE_FLUSH)) {
1011
+ const lineNo = scanContent.slice(0, match.index).split("\n").length + 1;
1012
+
1013
+ if (suppressed("card-table-needs-flush", lineNo - 1)) continue;
1014
+
1015
+ findings.push({
1016
+ file: rel,
1017
+ line: lineNo,
1018
+ rule: "card-table-needs-flush",
1019
+ severity: "error",
1020
+ message:
1021
+ "A Card whose whole body is a table must let the table touch the card's inner edge — " +
1022
+ "use <CardContent flush>. A default CardContent pads 16px while the table draws its own " +
1023
+ "border, so the table reads as a second box inside the card and a wide one overflows it.",
1024
+ replacement: "CardContent flush",
1025
+ snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
1026
+ });
1027
+ }
860
1028
  if (
861
1029
  !(SELF && !args.includes("--consumer")) &&
862
1030
  !/<(?:Flex|ResponsiveGrid|PageContainer)\b/.test(scanContent)
@@ -899,6 +1067,12 @@ const warnings = findings.filter((f) => f.severity === "warn");
899
1067
  // `resources/js/{components,pages,layouts}`; `walk()` swallows ENOENT and returns [], so in any
900
1068
  // tree without that layout — this repo included — `pnpm audit` printed
901
1069
  // "✓ No UI-standardization violations found." and exited 0 having read nothing at all.
1070
+ // …except under `--changed`, where "this branch touched no .tsx" is a clean run, not a
1071
+ // misconfigured path. Failing there would make the gate unusable on every backend-only commit.
1072
+ if (filesScanned === 0 && CHANGED) {
1073
+ if (!quiet && !asJson) console.log("✓ ui-audit --changed: no .tsx/.jsx changed on this branch.");
1074
+ process.exit(0);
1075
+ }
902
1076
  if (filesScanned === 0) {
903
1077
  const message =
904
1078
  `ui-audit scanned 0 files — none of [${SCAN_DIRS.join(", ")}] exists (or all were filtered). ` +
@@ -937,6 +1111,9 @@ if (filesScanned === 0) {
937
1111
  const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
938
1112
  console.log(`${tag} ${C.bold}${f.file}:${f.line}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
939
1113
  console.log(` ${f.message}`);
1114
+ // Name the primitive HERE: without it every finding costs a round-trip to the catalog plus a
1115
+ // guess about what to look up (godx-jp/id#497). The audit already knows the answer.
1116
+ if (f.replacement) console.log(` ${C.bold}use: ${f.replacement}${C.reset}`);
940
1117
  if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
941
1118
  console.log(` ${C.dim}${f.snippet}${C.reset}`);
942
1119
  }