@godxjp/ui 32.6.0 → 32.7.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.
@@ -8,6 +8,10 @@
8
8
 
9
9
  --legal-document-toc-width: 15rem;
10
10
 
11
+ --legal-document-page-padding-inline: initial;
12
+
13
+ --legal-document-title-font-size: initial;
14
+
11
15
  --legal-document-toc-inset-block-start: var(--space-6);
12
16
 
13
17
  --legal-document-toc-max-height: calc(100dvh - var(--space-12));
@@ -4,10 +4,10 @@ Read this once; the audit enforces it. What the audit cannot see (how a screen i
4
4
  should look like) is in the package's `DESIGN.md`. Everything else in `docs/` is for contributors.
5
5
 
6
6
  1. Load styles with `import "@godxjp/ui/styles.css"` (precompiled, no Tailwind needed) or `@import "@godxjp/ui/styles"` (Tailwind v4). Neither ships font files: fonts are opt-in since v32 (`pnpm add @fontsource/noto-sans-jp @fontsource/m-plus-2`, then `@import "@godxjp/ui/styles/fonts.css"` after the styles import; the GoDX preset does this for you). Never cherry-pick `*-layout.css`.
7
- 2. **Mount `<AppProvider defaultLocale="…">` once, at the root.** Without it the kit's OWN strings (dialog close/cancel/delete, counters, picker labels) fall to the resting locale `vi` and pickers to UTC — measured in a Japanese app as Vietnamese buttons, silently (gh#1005). A development build now warns once when a kit component renders outside it; `syncI18nLocale(locale, fallback)` is the escape for an app that truly cannot mount the provider. Every page is `<PageContainer title extra footer>` (a `subtitle` only when it adds a fact the title does not); its sections are spaced by the page. Group items inside a section with `<Flex direction="col" gap>` or `<ResponsiveGrid>`.
7
+ 2. **Mount `<AppProvider defaultLocale="…">` once, at the root.** Without it the kit's OWN strings (dialog close/cancel/delete, counters, picker labels) fall to the resting locale (`<html lang>` when it names a registered locale, else `en`) and pickers to UTC (gh#1005). A development build now warns once when a kit component renders outside it; `syncI18nLocale(locale, fallback)` is the escape for an app that truly cannot mount the provider. Every page is `<PageContainer title extra footer>` (a `subtitle` only when it adds a fact the title does not); its sections are spaced by the page. Group items inside a section with `<Flex direction="col" gap>` or `<ResponsiveGrid>`.
8
8
  3. No Tailwind layout on your own elements: no `flex`, `grid`, `gap-*`, `p-*`, `m-*`, `space-*`. Rows are `<Flex>` (default row), stacks are `<Flex direction="col">`, grids are `<ResponsiveGrid>`.
9
9
  4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`. A LIST of those rows is `<Flex as="ul" marker="none" direction="col" gap="none">` with `<ListRow as="li">` children — `marker="none"` keeps the `<ul>`, the `<li>` semantics and the gap token while dropping the bullet and the indent. Never a raw `<ul>`/`<ol>` (it carries no gap token), never `<div role="list">` + `<div role="listitem">`, and never a wrapper around each row: the divider is `:not(:last-child)` among siblings, so a row alone in its own wrapper is always the last one and EVERY divider disappears silently. A bulleted prose list is the same `<Flex as="ul">` without `marker`.
10
- 5. Real controls only: `Button`, `Input`, `Select`, `Textarea`, `Checkbox`… never raw `<button>`/`<input>`; a labelled control lives in `<FormField label>`. A Select outside a form takes `width="auto"`. **A disabled control's reason is visible text, never a tooltip** — see below. **Fields live in a `<Form layout="horizontal" labelWidth controlWidth>`** — never a group of `FormField`s without one, never a hand-rolled `<Flex>` row of fields (a row that belongs together is `<SpaceCompact>` or `<Form columns>`); size each control for its content with `controlWidth` (GOV.UK text-input width: a port ~7rem, a short enum ~10rem); three or more fields is a page, not a Dialog. The audit rules are `formfield-needs-form`, `dialog-form-too-big` and `select-width-hint`. **A case the kit cannot express is an issue, not a hand-roll:** open it on godx-jp/godxjp-ui, use the nearest valid composition, and mark it `// TODO(godxjp-ui#<n>)` (gh#998). **One action row, one Button size:** every `Button` under one parent takes the same `size` — a missing size is `default`, and `icon-sm` pairs with `sm`, `icon-xs` with `xs`, `icon` with default. Never `size="sm"` beside a default Button. The audit rules are `mixed-button-size` (static) and `mixed-button-height` (visual-audit, within 0.5px) (gh#1084).
10
+ 5. Real controls only: `Button`, `Input`, `Select`, `Textarea`, `Checkbox`… never raw `<button>`/`<input>`; a labelled control lives in `<FormField label>`. A Select outside a form takes `width="auto"`. **A disabled control's reason is visible text, never a tooltip** — see below. **Fields live in a `<Form layout="horizontal" labelWidth controlWidth>`** — never a group of `FormField`s without one, never a hand-rolled `<Flex>` row of fields (a row that belongs together is `<Flex attached>` or `<Form columns>`); size each control for its content with `controlWidth` (GOV.UK text-input width: a port ~7rem, a short enum ~10rem); three or more fields is a page, not a Dialog. The audit rules are `formfield-needs-form`, `dialog-form-too-big` and `select-width-hint`. **A case the kit cannot express is an issue, not a hand-roll:** open it on godx-jp/godxjp-ui, use the nearest valid composition, and mark it `// TODO(godxjp-ui#<n>)` (gh#998). **One action row, one Button size:** every `Button` under one parent takes the same `size` — a missing size is `default`, and `icon-sm` pairs with `sm`, `icon-xs` with `xs`, `icon` with default. Never `size="sm"` beside a default Button. The audit rules are `mixed-button-size` (static) and `mixed-button-height` (visual-audit, within 0.5px) (gh#1084).
11
11
  6. Text is `<Text>` / `<Heading>` with `tone`, `size`, `weight`, `truncate`, `mono` — not `className="text-muted-foreground font-semibold"`. A STANDALONE glyph is `<Icon as={Lock} size="sm" tone="muted" />` from `@godxjp/ui/general`: a bare lucide icon carries its own `width="24" height="24"` and only four places re-size it for you (`Button`, a `DropdownMenuItem`, a `TopbarItem`, a `ListRow` leading slot), so everywhere else — in a `Text`, a table cell, an `<a>` — it draws at 24px beside 14px type. `size-4` and `w-[16px]` are closed to you by rules 3 and 8, and `size={16}` hard-codes a number the theme owns; `Icon` puts the glyph on the `--icon-size-*` scale (`2xs…4xl`) instead, and is `aria-hidden` unless you give it a `label` (then it is `role="img"` with that name). The audit rule is `lucide-icon-needs-size`.
12
12
  7. Colours are semantic tokens (`tone="destructive"`, `bg-primary`), never palette names, hex, or `bg-black` / `text-white`.
13
13
  8. Sizes come from props (`size`, `width`, `columns`), never `w-[240px]` / `max-h-[420px]`.
@@ -17,7 +17,7 @@ should look like) is in the package's `DESIGN.md`. Everything else in `docs/` is
17
17
  the table reads as a second box inside the card, and a wide one runs past the card entirely.
18
18
  (`flush` alone is enough: it drops the border, the radius and the inline padding. `tight` is a
19
19
  different knob — it governs the header band, not the body's edges.)
20
- 11. Run `node node_modules/@godxjp/ui/scripts/ui-audit.mjs --changed` before every review; a browser sweep with `visual-audit.mjs` is a separate, heavier run — do it when the work is a visual review, not on every change; `visual-audit.mjs <url>` on the running app. Zero errors is the bar.
20
+ 11. Run `npx @godxjp/ui audit --changed` before every review (the package bin, so it works from a workspace package whose `node_modules` is hoisted to the repo root); a browser sweep with `npx @godxjp/ui visual-audit <url>` on the running app is a separate, heavier run — do it when the work is a visual review, not on every change. Zero errors is the bar.
21
21
 
22
22
  **Opting a deliberate exception out.** Name the rule; the block form also has to say why.
23
23
 
@@ -28,9 +28,11 @@ Off-cycle approved: owner, 2026-10-09 — the single v32 batch decided on godx-j
28
28
  renders none.
29
29
  - The email brand mark has no default label.
30
30
  - The GoDX look ships as `godxPreset` (`@godxjp/ui/themes/godx`) plus `@godxjp/ui/themes/godx.css`.
31
- - Since 32.0.1, `@godxjp/ui/themes/godx-tokens.css` holds the GoDX colours without the fonts, for a
32
- shadow root that embeds `@godxjp/ui/styles/core` (`@font-face` only works in the document). Add it
33
- after `styles/core` there; `godx.css` imports it, so a page needs only `godx.css`.
31
+ - Since 32.0.1, `@godxjp/ui/themes/godx-tokens.css` holds the GoDX colours without the fonts.
32
+ `godx.css` is `fonts.css` (which imports `@fontsource/noto-sans-jp` and `@fontsource/m-plus-2`)
33
+ plus `godx-tokens.css`. Import `godx-tokens.css` instead of `godx.css` when the app bundles or
34
+ hosts its own fonts, and in a shadow root that embeds `@godxjp/ui/styles/core` (`@font-face`
35
+ only works in the document). Never import both. Put it after the base stylesheet.
34
36
  - A script that READS the GoDX colours from `themes/godx.css` (a token sync, a design-system
35
37
  export) must read `themes/godx-tokens.css` instead: since 32.0.1, `godx.css` holds only two
36
38
  `@import` lines.
@@ -84,7 +86,7 @@ packages. The codemod rewrites every import below; the "you finish" column is pr
84
86
  | `Title`, `Paragraph`, `Typography` | `Heading`, `Text as="p"`, `Prose` | the antd compatibility shims are removed |
85
87
  | `SkeletonAvatar`, `SkeletonButton`, `SkeletonImage`, `SkeletonNode` | — | removed: use `SkeletonArticle avatar` or a sized `Skeleton` |
86
88
  | `FloatButton`, `DraggablePanel`, `PageCover`, `Masonry`, `LegalDocumentShell`, `Marquee`, `TextDiff`, `OrgChart`, `Carousel`, `TimelineGrid`, `RangeTimeline`, `EmojiPicker`, `SortableList`, `MegaMenu`, `Anchor` | same names from `@godxjp/ui/lab` | nothing: same API, opt-in subpath |
87
- | `ChatBubble`, `ChatBubbleList`, `ChatComposer`, `ChatSuggestion`, `Conversations`, `ThoughtChain`, `Welcome`, `Attachments` | same names from `@godxjp/chat` | install `@godxjp/chat` |
89
+ | `ChatBubble`, `ChatBubbleList`, `ChatComposer`, `ChatSuggestion`, `Conversations`, `ThoughtChain`, `Welcome`, `Attachments` | same names from `@godxjp/chat` | add `@godxjp/chat` as a NEW dependency, same version as `@godxjp/ui` |
88
90
 
89
91
  Unchanged: `SplitPane` stays (its merge into `MasterDetail` failed the parity test), `Actions` stays
90
92
  in `@godxjp/ui/general`, and `FilterBar` stays an alias of `Toolbar`. The `./ui/*` subpaths are
@@ -92,7 +94,10 @@ deprecated re-exports and will be removed in a later major.
92
94
 
93
95
  ## How to upgrade
94
96
 
95
- 1. Upgrade all six packages to the latest 32.0.x together (`@godxjp/chat` only if you use the chat components).
97
+ 1. Upgrade all six packages to the latest 32.0.x together. An app that imports `ChatSuggestion` or
98
+ any other chat component from `@godxjp/ui` must ADD `@godxjp/chat` to its `package.json` — it
99
+ is a new dependency, not a rename — at the same version as `@godxjp/ui` (the packages release
100
+ in lockstep). Without it the build fails on the first chat import.
96
101
  2. Run `npx godxjp-ui codemod v32 <src> --dry-run`, review, then run it without `--dry-run`. Use
97
102
  `--godx` for GoDX products.
98
103
  The 32.0.0 codemod imported the fonts twice under `--godx` (`fonts.css` and `godx.css`, which
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "32.6.0",
4
- "godxUiMcp": "32.6.0",
3
+ "version": "32.7.0",
4
+ "godxUiMcp": "32.7.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -15,6 +15,7 @@ import {
15
15
  chmodSync,
16
16
  existsSync,
17
17
  mkdirSync,
18
+ readdirSync,
18
19
  readFileSync,
19
20
  realpathSync,
20
21
  renameSync,
@@ -78,7 +79,11 @@ function mcpConfigMismatchMessage(root, existing, expected, key = MCP_KEY) {
78
79
  }
79
80
 
80
81
  /** Commands wired into the consumer's .claude/settings.json. */
81
- export const AUDIT_HOOK_CMD = "node node_modules/@godxjp/ui/scripts/audit-hook.mjs";
82
+ // `npx --no-install` resolves @godxjp/ui up the node_modules chain, so the hook also works from a
83
+ // workspace package whose install is hoisted to the repo root (gh#1296); it never downloads.
84
+ export const AUDIT_HOOK_CMD = "npx --no-install @godxjp/ui audit-hook";
85
+ /** The pre-#1296 default, rewritten to AUDIT_HOOK_CMD when found verbatim (a custom command is kept). */
86
+ const LEGACY_AUDIT_HOOK_CMD = "node node_modules/@godxjp/ui/scripts/audit-hook.mjs";
82
87
  export const PRIMER_CMD = "cat .claude/godxjp-ui-workflow.md";
83
88
 
84
89
  /** What `.claude/settings.json` would get, used only for the `.godxjp-ui-suggested` sidecar. */
@@ -97,7 +102,12 @@ export const KIT_VERSION = readJson(join(SELF_ROOT, "package.json"))?.version ??
97
102
  * node_modules). Returns null when absent — caller falls back to this package's release train.
98
103
  */
99
104
  export function readConsumerUiMetadata(root) {
100
- const uiPkg = readJson(join(root, "node_modules/@godxjp/ui/package.json"));
105
+ // Up the node_modules chain: a workspace package has @godxjp/ui hoisted to the repo root (gh#1296).
106
+ let uiPkg;
107
+ for (let dir = resolve(root); ; dir = dirname(dir)) {
108
+ uiPkg = readJson(join(dir, "node_modules/@godxjp/ui/package.json"));
109
+ if (uiPkg || dirname(dir) === dir) break;
110
+ }
101
111
  if (!uiPkg?.version) return null;
102
112
  return {
103
113
  version: String(uiPkg.version),
@@ -196,12 +206,14 @@ Follow this every time you create or change a page, component or form.
196
206
  - Screen composition (what the audit cannot see) is in \`.ai/rules/godxjp-ui.md\`.
197
207
 
198
208
  ## After JSX
199
- 5. \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs <files>\` — zero errors (a PostToolUse hook
200
- also runs it on each edit).
209
+ 5. \`npx @godxjp/ui audit <files>\` — zero errors (a PostToolUse hook also runs it on each edit).
210
+ Run it from the package that owns the files; \`--help\` lists the flags.
201
211
  6. Screenshot at 390 / 768 / 1280 next to the exemplar, list the top 3 differences, fix them,
202
212
  repeat — at most 3 rounds.
203
- 7. Before a visual review: \`node node_modules/@godxjp/ui/scripts/visual-audit.mjs <url>\`
204
- (axe + contrast + layout).
213
+ 7. Before a visual review: \`npx @godxjp/ui visual-audit <url>\` (axe + contrast + layout).
214
+
215
+ In a workspace, \`node_modules/@godxjp/ui\` may be hoisted to the repo root: read its files there,
216
+ or through the MCP (\`get_design\`, \`get_rule\`).
205
217
  `;
206
218
 
207
219
  /** Delimited block appended to the consumer's CLAUDE.md — loaded into the agent's context
@@ -217,7 +229,7 @@ Before any JSX for a screen:
217
229
  3. \`get_exemplar\` (list | detail | form | settings | dashboard | approval) — match its structure.
218
230
 
219
231
  After building:
220
- 4. \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs <files>\` — zero errors.
232
+ 4. \`npx @godxjp/ui audit <files>\` — zero errors.
221
233
  5. Screenshot at 390 / 768 / 1280 beside the exemplar; fix the top 3 differences; max 3 rounds.
222
234
 
223
235
  Rules: \`node_modules/@godxjp/ui/docs/CONSUMER-RULES.md\`. Details: \`.claude/godxjp-ui-workflow.md\`.
@@ -523,7 +535,13 @@ export function ensureClaudeHooks(root) {
523
535
  (arr ?? []).some((g) => (g.hooks ?? []).some((h) => (h.command ?? "").includes(cmd)));
524
536
 
525
537
  json.hooks.PostToolUse = json.hooks.PostToolUse ?? [];
526
- if (!has(json.hooks.PostToolUse, "audit-hook.mjs")) {
538
+ for (const group of json.hooks.PostToolUse)
539
+ for (const hook of group.hooks ?? [])
540
+ if (hook.command === LEGACY_AUDIT_HOOK_CMD) {
541
+ hook.command = AUDIT_HOOK_CMD;
542
+ added.push("PostToolUse:auto-audit (workspace-safe command)");
543
+ }
544
+ if (!has(json.hooks.PostToolUse, "audit-hook")) {
527
545
  json.hooks.PostToolUse.push({
528
546
  matcher: "Write|Edit|MultiEdit",
529
547
  hooks: [{ type: "command", command: AUDIT_HOOK_CMD }],
@@ -692,16 +710,86 @@ export function guineaPigStamp() {
692
710
  * this one. The file says so at the top, because the first draft preserved nothing and silently
693
711
  * ate a note left inside it — measured, and the reason for that banner.
694
712
  */
695
- export function ensureConsumerRules(root) {
696
- const uiDir = ["resources/js", "app/javascript", "src/components", "src", "app"].find((d) =>
713
+ const UI_FILE = /\.(tsx|jsx)$/;
714
+ const NOT_UI_DIR = new Set([
715
+ "node_modules",
716
+ "dist",
717
+ "build",
718
+ "out",
719
+ "vendor",
720
+ "coverage",
721
+ "public",
722
+ "storage",
723
+ ]);
724
+
725
+ /** Does `dir` hold a .tsx/.jsx file within a few levels? Bounded, so a large tree costs little. */
726
+ function holdsUi(dir, depth = 5) {
727
+ let entries;
728
+ try {
729
+ entries = readdirSync(dir, { withFileTypes: true });
730
+ } catch {
731
+ return false;
732
+ }
733
+ if (entries.some((e) => e.isFile() && UI_FILE.test(e.name))) return true;
734
+ if (depth === 0) return false;
735
+ return entries.some(
736
+ (e) =>
737
+ e.isDirectory() &&
738
+ !e.name.startsWith(".") &&
739
+ !NOT_UI_DIR.has(e.name) &&
740
+ holdsUi(join(dir, e.name), depth - 1),
741
+ );
742
+ }
743
+
744
+ /**
745
+ * Every directory that actually holds the consumer's UI, as `paths:` globs without the `/**`
746
+ * (gh#1296). A fixed `src` never fired in an app whose screens live in `web/` or
747
+ * `resources/js/`, so the rules looked installed and loaded on no edit. Conventional nested roots
748
+ * first (`resources/js`, `apps/*\/src`, …), then any other top-level directory holding .tsx/.jsx.
749
+ * A repo with no UI files yet (a fresh scaffold) falls back to the first conventional directory
750
+ * that exists.
751
+ */
752
+ function detectUiDirs(root) {
753
+ const subdirs = (rel) => {
754
+ try {
755
+ return readdirSync(join(root, rel), { withFileTypes: true })
756
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && !NOT_UI_DIR.has(e.name))
757
+ .map((e) => e.name)
758
+ .sort();
759
+ } catch {
760
+ return [];
761
+ }
762
+ };
763
+ const found = [];
764
+ const add = (glob, dirs) => {
765
+ if (dirs.some((d) => holdsUi(join(root, d)))) found.push(glob);
766
+ };
767
+ for (const rel of ["resources/js", "app/javascript"]) add(rel, [rel]);
768
+ for (const parent of ["apps", "packages"]) {
769
+ const dirs = subdirs(parent)
770
+ .map((name) => `${parent}/${name}/src`)
771
+ .filter((d) => existsSync(join(root, d)));
772
+ if (dirs.length) add(`${parent}/*/src`, dirs);
773
+ }
774
+ for (const name of subdirs("")) {
775
+ if (!found.some((glob) => glob.split("/")[0] === name)) add(name, [name]);
776
+ }
777
+ if (found.length) return found;
778
+ const scaffold = ["resources/js", "app/javascript", "src/components", "src", "app"].find((d) =>
697
779
  existsSync(join(root, d)),
698
780
  );
699
- if (!uiDir) return false;
781
+ return scaffold ? [scaffold] : [];
782
+ }
783
+
784
+ export function ensureConsumerRules(root) {
785
+ const uiDirs = detectUiDirs(root);
786
+ if (uiDirs.length === 0) return false;
787
+ const uiDir = uiDirs.join(", ");
700
788
 
701
789
  const dir = join(root, ".ai", "rules");
702
790
  const target = join(dir, "godxjp-ui.md");
703
791
  const body = readFileSync(join(SELF_ROOT, "scripts", "consumer-rule.md"), "utf8");
704
- const front = `---\npaths:\n - '${uiDir}/**'\n---\n\n`;
792
+ const front = `---\npaths:\n${uiDirs.map((d) => ` - '${d}/**'\n`).join("")}---\n\n`;
705
793
  // The digest covers the FRONT MATTER too: the detected glob is part of what makes this file
706
794
  // correct, and a repo that grows a `resources/js` after shipping with `src` needs the rewrite.
707
795
  const managed = `${front}${body}`;
@@ -745,7 +833,8 @@ export function ensureConsumerRules(root) {
745
833
  if (!cur.includes(".ai/rules/godxjp-ui.md")) {
746
834
  writeFileAtomic(
747
835
  index,
748
- cur.replace(/\s*$/, "") + `\n| ${uiDir}/** | .ai/rules/godxjp-ui.md |\n`,
836
+ cur.replace(/\s*$/, "") +
837
+ `\n| ${uiDirs.map((d) => `${d}/**`).join(", ")} | .ai/rules/godxjp-ui.md |\n`,
749
838
  );
750
839
  }
751
840
  }
@@ -780,7 +869,9 @@ export function runSyncRules(root, { dryRun = false } = {}) {
780
869
  if (!refused && !(r === "present" && md === "present" && !wf && !skill && !rules)) {
781
870
  lines.push(
782
871
  `\n @godxjp/ui → MCP in .mcp.json (${r}); workflow mandate in CLAUDE.md (${md}).\n` +
783
- (rules ? ` common consumer rules in .ai/rules/godxjp-ui.md (glob ${rules}/**).\n` : "") +
872
+ (rules
873
+ ? ` common consumer rules in .ai/rules/godxjp-ui.md (UI dirs: ${rules}).\n`
874
+ : "") +
784
875
  (skill ? " guinea-pig skill refreshed to this version (your section 8 kept).\n" : "") +
785
876
  " Your agent now has live component + audit guidance. Restart it to pick up the MCP.\n" +
786
877
  " For auto-audit on every edit (PostToolUse + SessionStart hooks):\n" +
package/scripts/cli.mjs CHANGED
@@ -16,6 +16,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
16
16
  const MAP = {
17
17
  "init-agent": "init-agent-kit.mjs",
18
18
  audit: "ui-audit.mjs",
19
+ "audit-hook": "audit-hook.mjs",
19
20
  "visual-audit": "visual-audit.mjs",
20
21
  "prune-css": "prune-css.mjs",
21
22
  codemod: "codemod.mjs",
@@ -55,8 +56,14 @@ const HELP = {
55
56
  fails rather than reporting clean when origin/main cannot be resolved
56
57
  --format json machine-readable findings
57
58
  --quiet print errors only (warnings hidden)
59
+ --base=<ref> with --changed, diff against <ref> instead of origin/main
58
60
  --rules print the rule catalog as JSON and exit
61
+ --consumer inside the @godxjp/ui repo only: apply the consumer rules (always on in an app)
59
62
  Pre-commit: npx godxjp-ui audit resources/js || exit 1`,
63
+ "audit-hook": `godxjp-ui audit-hook
64
+
65
+ The Claude Code PostToolUse hook init-agent wires in: reads the hook payload on stdin and audits
66
+ the .tsx file that was just written or edited, feeding findings back to the agent. Not for humans.`,
60
67
  "prune-css": `godxjp-ui prune-css <src dir/glob …> [--out <file>] [--fonts]
61
68
 
62
69
  Emit a stylesheet with only the component CSS layers your app uses (gh#971). Scans the given
@@ -95,6 +102,7 @@ commands:
95
102
  init-agent install the agent forcing-kit (MCP + auto-audit hook + CLAUDE.md mandate)
96
103
  sync-rules write/refresh package-owned agent rules (--dry-run previews)
97
104
  audit static UI audit over source
105
+ audit-hook the per-edit audit hook (wired by init-agent)
98
106
  visual-audit runtime audit (Playwright + axe-core) against a running app
99
107
  prune-css emit a stylesheet with only the CSS layers your app uses
100
108
 
@@ -77,7 +77,7 @@ ngày: năm thứ cần đều ĐÃ CÓ và vẫn bị dựng lại bằng thứ
77
77
  | một hàng LÀ liên kết (thay cho nút rời) | `<ListRow asChild>` |
78
78
  | kẻ ô từng ngày trong lịch | `<Calendar bordered>` |
79
79
  | dải giữa hai vùng, tự kẻ theo VỊ TRÍ | `<CardBar>` (`border` để ép khi xếp chồng) |
80
- | chip "điều kiện đang bật" có dấu × để bỏ | `<Badge onRemove>` — KHÔNG phải `TagInput` |
80
+ | chip "điều kiện đang bật" có dấu × để bỏ | `<Badge onRemove>` — KHÔNG phải `Select mode="tags"` |
81
81
  | dải tab nằm TRONG đầu Card | `<Card tabList activeTabKey onTabChange>` |
82
82
  | tiêu đề / đoạn văn / liên kết trong văn bản | `<Heading level>` · `<Text as="p">` · `<Link>` |
83
83
  | nút nổi góc màn (quay lên đầu, hành động nhanh) | `<FloatButton>` |
@@ -91,11 +91,11 @@ ngày: năm thứ cần đều ĐÃ CÓ và vẫn bị dựng lại bằng thứ
91
91
  Lỗi không phải "đoán sai tên prop" mà là **cho rằng nó không tồn tại nên không
92
92
  hỏi**.
93
93
 
94
- ## `AppProvider` là bắt buộc — thiếu nó, chuỗi của kit nói tiếng Việt
94
+ ## `AppProvider` là bắt buộc — thiếu nó, chuỗi của kit không theo ngôn ngữ của app
95
95
 
96
96
  Mount `<AppProvider defaultLocale="ja">` MỘT lần ở gốc app. Thiếu nó, chuỗi của chính kit (nút
97
- đóng/hủy/xóa của dialog, upload, badge đếm, nhãn picker) rơi về locale nghỉ `vi` và picker rơi về
98
- UTC — godx-mailer đo được nút tiếng Việt giữa UI tiếng Nhật, không một cảnh báo (gh#1005). Bản dev
97
+ đóng/hủy/xóa của dialog, upload, badge đếm, nhãn picker) rơi về locale nghỉ — `<html lang>` nếu
98
+ locale đó đã đăng ký, không thì `en` — và picker rơi về UTC (gh#1005). Bản dev
99
99
  giờ cảnh báo MỘT lần khi component của kit render ngoài provider. Không mount được provider thì gọi
100
100
  `syncI18nLocale(locale, fallback)` lúc khởi động.
101
101
 
@@ -104,11 +104,11 @@ giờ cảnh báo MỘT lần khi component của kit render ngoài provider. Kh
104
104
  godx-mailer dính bốn lỗi form trong một ngày, cả bốn đều là kit ĐÃ CÓ đồ đúng
105
105
  (gh#998). Giờ ui-audit chặn:
106
106
 
107
- | Sai | Đúng | rule |
108
- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------- |
109
- | ≥2 `FormField` không có `Form`, hoặc xếp hàng bằng `<Flex>` | `<Form layout="horizontal" labelWidth controlWidth>`; hàng = `SpaceCompact` / `Form columns` | `formfield-needs-form` |
110
- | ≥3 `FormField` trong `DialogBody` | page riêng (route riêng) | `dialog-form-too-big` |
111
- | `Select` hai lựa chọn kéo full width | `controlWidth` trên field hoặc một lần trên `Form` | `select-width-hint` |
107
+ | Sai | Đúng | rule |
108
+ | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------- |
109
+ | ≥2 `FormField` không có `Form`, hoặc xếp hàng bằng `<Flex>` | `<Form layout="horizontal" labelWidth controlWidth>`; hàng = `<Flex attached>` / `Form columns` | `formfield-needs-form` |
110
+ | ≥3 `FormField` trong `DialogBody` | page riêng (route riêng) | `dialog-form-too-big` |
111
+ | `Select` hai lựa chọn kéo full width | `controlWidth` trên field hoặc một lần trên `Form` | `select-width-hint` |
112
112
 
113
113
  Ca kit không diễn đạt được thì **mở issue ở godx-jp/godxjp-ui**, tạm dùng cách
114
114
  hợp lệ gần nhất kèm `// TODO(godxjp-ui#<n>)` — không tự chế.
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
10
- import { isAbsolute, join, relative } from "node:path";
10
+ import { dirname, isAbsolute, join, relative } from "node:path";
11
11
 
12
12
  const CWD = process.cwd();
13
13
  const SELF = (() => {
@@ -18,6 +18,29 @@ const SELF = (() => {
18
18
  }
19
19
  })();
20
20
  const args = process.argv.slice(2);
21
+
22
+ // Before anything reads the arguments: `--help` used to be filtered out as a flag and ran a full
23
+ // audit of the working directory instead (gh#1296).
24
+ if (args.includes("--help") || args.includes("-h")) {
25
+ console.log(`usage: ui-audit [dir|file …] [--changed [--base=<ref>]] [--format json] [--quiet]
26
+ ui-audit --rules
27
+
28
+ Static UI-standardization audit of @godxjp/ui consumer source. Exits 1 on any error.
29
+ Run it from the package that owns the files (a workspace package works; the installed
30
+ @godxjp/ui is found up the node_modules chain). Same as: npx @godxjp/ui audit …
31
+
32
+ dir|file … what to scan (default: the conventional UI directories under the cwd)
33
+ --changed scan what this branch changed (committed, staged, untracked)
34
+ --base=<ref> the base for --changed (default origin/HEAD, then origin/main)
35
+ --format json machine-readable findings
36
+ --quiet errors only
37
+ --rules print the rule catalog as JSON and exit
38
+ --consumer library repo only: apply the consumer rules inside @godxjp/ui itself.
39
+ A consumer app never needs it; there those rules always apply.
40
+ -h, --help this text`);
41
+ process.exit(0);
42
+ }
43
+
21
44
  const asJson = args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
22
45
  const quiet = args.includes("--quiet");
23
46
  const dirArgs = args.filter((a) => !a.startsWith("--") && a !== "json");
@@ -1899,13 +1922,20 @@ function staleOwnedRules() {
1899
1922
  const contents = readFileSync(target, "utf8");
1900
1923
  const stamped = /<!-- godxjp-ui:version ([^\s]+) -->/.exec(contents)?.[1];
1901
1924
 
1925
+ // Up the node_modules chain, as Node resolves it: a workspace package has its @godxjp/ui hoisted
1926
+ // to the repo root, and reading only `CWD/node_modules` reported a correct install as
1927
+ // `owned-rules-unknown` (gh#1296). The package's `exports` map does not expose package.json,
1928
+ // so `require.resolve` cannot do this walk for us.
1902
1929
  let installed;
1903
- try {
1904
- installed = JSON.parse(
1905
- readFileSync(join(CWD, "node_modules", "@godxjp", "ui", "package.json"), "utf8"),
1906
- ).version;
1907
- } catch {
1908
- installed = undefined;
1930
+ for (let dir = CWD; installed === undefined; dir = dirname(dir)) {
1931
+ try {
1932
+ installed = JSON.parse(
1933
+ readFileSync(join(dir, "node_modules", "@godxjp", "ui", "package.json"), "utf8"),
1934
+ ).version;
1935
+ } catch {
1936
+ // Not here — keep walking.
1937
+ }
1938
+ if (dirname(dir) === dir) break;
1909
1939
  }
1910
1940
 
1911
1941
  if (!stamped || !installed) {