@godxjp/ui 32.6.0 → 32.7.1

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.1",
4
+ "godxUiMcp": "32.7.1",
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),
@@ -177,8 +187,9 @@ Follow this every time you create or change a page, component or form.
177
187
  2. **Route.** \`route_consumer_task "<task>"\` → \`compose-a-screen\` for app screens. The
178
188
  marketing skills (soft, minimalist, brutalist, gpt-tasteskill, imagegen-*) are for landing
179
189
  pages only.
180
- 3. **Exemplar.** \`get_exemplar\` with archetype list | detail | form | settings | dashboard |
181
- approval. Match its structure; it is the visual target.
190
+ 3. **Exemplar.** \`get_exemplar\` for the closest archetype (call it with no argument for the
191
+ list: list, detail, editor/form, reader/document, settings, dashboard, approval-queue). Match
192
+ its structure; it is the visual target.
182
193
  4. **Discover, then look up.** \`search_components "<area>"\` first, \`get_component <Name>\`
183
194
  second — \`get_component\` cannot find a name you do not already have (a consumer hand-rolled
184
195
  the shipped \`OrgSwitcher\` and \`TopbarItem\` this way). Code already in this repo is not the
@@ -196,12 +207,14 @@ Follow this every time you create or change a page, component or form.
196
207
  - Screen composition (what the audit cannot see) is in \`.ai/rules/godxjp-ui.md\`.
197
208
 
198
209
  ## 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).
210
+ 5. \`npx @godxjp/ui audit <files>\` — zero errors (a PostToolUse hook also runs it on each edit).
211
+ Run it from the package that owns the files; \`--help\` lists the flags.
201
212
  6. Screenshot at 390 / 768 / 1280 next to the exemplar, list the top 3 differences, fix them,
202
213
  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).
214
+ 7. Before a visual review: \`npx @godxjp/ui visual-audit <url>\` (axe + contrast + layout).
215
+
216
+ In a workspace, \`node_modules/@godxjp/ui\` may be hoisted to the repo root: read its files there,
217
+ or through the MCP (\`get_design\`, \`get_rule\`).
205
218
  `;
206
219
 
207
220
  /** Delimited block appended to the consumer's CLAUDE.md — loaded into the agent's context
@@ -214,10 +227,11 @@ ${STAMP(KIT_VERSION)}
214
227
  Before any JSX for a screen:
215
228
  1. Read \`node_modules/@godxjp/ui/DESIGN.md\` (MCP \`get_design\`).
216
229
  2. \`route_consumer_task\` → \`compose-a-screen\`; \`search_components\` before hand-rolling anything.
217
- 3. \`get_exemplar\` (list | detail | form | settings | dashboard | approval) — match its structure.
230
+ 3. \`get_exemplar\` (no argument lists the archetypes: list, detail, editor, reader, settings,
231
+ dashboard, approval-queue) — match its structure.
218
232
 
219
233
  After building:
220
- 4. \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs <files>\` — zero errors.
234
+ 4. \`npx @godxjp/ui audit <files>\` — zero errors.
221
235
  5. Screenshot at 390 / 768 / 1280 beside the exemplar; fix the top 3 differences; max 3 rounds.
222
236
 
223
237
  Rules: \`node_modules/@godxjp/ui/docs/CONSUMER-RULES.md\`. Details: \`.claude/godxjp-ui-workflow.md\`.
@@ -523,7 +537,13 @@ export function ensureClaudeHooks(root) {
523
537
  (arr ?? []).some((g) => (g.hooks ?? []).some((h) => (h.command ?? "").includes(cmd)));
524
538
 
525
539
  json.hooks.PostToolUse = json.hooks.PostToolUse ?? [];
526
- if (!has(json.hooks.PostToolUse, "audit-hook.mjs")) {
540
+ for (const group of json.hooks.PostToolUse)
541
+ for (const hook of group.hooks ?? [])
542
+ if (hook.command === LEGACY_AUDIT_HOOK_CMD) {
543
+ hook.command = AUDIT_HOOK_CMD;
544
+ added.push("PostToolUse:auto-audit (workspace-safe command)");
545
+ }
546
+ if (!has(json.hooks.PostToolUse, "audit-hook")) {
527
547
  json.hooks.PostToolUse.push({
528
548
  matcher: "Write|Edit|MultiEdit",
529
549
  hooks: [{ type: "command", command: AUDIT_HOOK_CMD }],
@@ -692,16 +712,86 @@ export function guineaPigStamp() {
692
712
  * this one. The file says so at the top, because the first draft preserved nothing and silently
693
713
  * ate a note left inside it — measured, and the reason for that banner.
694
714
  */
695
- export function ensureConsumerRules(root) {
696
- const uiDir = ["resources/js", "app/javascript", "src/components", "src", "app"].find((d) =>
715
+ const UI_FILE = /\.(tsx|jsx)$/;
716
+ const NOT_UI_DIR = new Set([
717
+ "node_modules",
718
+ "dist",
719
+ "build",
720
+ "out",
721
+ "vendor",
722
+ "coverage",
723
+ "public",
724
+ "storage",
725
+ ]);
726
+
727
+ /** Does `dir` hold a .tsx/.jsx file within a few levels? Bounded, so a large tree costs little. */
728
+ function holdsUi(dir, depth = 5) {
729
+ let entries;
730
+ try {
731
+ entries = readdirSync(dir, { withFileTypes: true });
732
+ } catch {
733
+ return false;
734
+ }
735
+ if (entries.some((e) => e.isFile() && UI_FILE.test(e.name))) return true;
736
+ if (depth === 0) return false;
737
+ return entries.some(
738
+ (e) =>
739
+ e.isDirectory() &&
740
+ !e.name.startsWith(".") &&
741
+ !NOT_UI_DIR.has(e.name) &&
742
+ holdsUi(join(dir, e.name), depth - 1),
743
+ );
744
+ }
745
+
746
+ /**
747
+ * Every directory that actually holds the consumer's UI, as `paths:` globs without the `/**`
748
+ * (gh#1296). A fixed `src` never fired in an app whose screens live in `web/` or
749
+ * `resources/js/`, so the rules looked installed and loaded on no edit. Conventional nested roots
750
+ * first (`resources/js`, `apps/*\/src`, …), then any other top-level directory holding .tsx/.jsx.
751
+ * A repo with no UI files yet (a fresh scaffold) falls back to the first conventional directory
752
+ * that exists.
753
+ */
754
+ function detectUiDirs(root) {
755
+ const subdirs = (rel) => {
756
+ try {
757
+ return readdirSync(join(root, rel), { withFileTypes: true })
758
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && !NOT_UI_DIR.has(e.name))
759
+ .map((e) => e.name)
760
+ .sort();
761
+ } catch {
762
+ return [];
763
+ }
764
+ };
765
+ const found = [];
766
+ const add = (glob, dirs) => {
767
+ if (dirs.some((d) => holdsUi(join(root, d)))) found.push(glob);
768
+ };
769
+ for (const rel of ["resources/js", "app/javascript"]) add(rel, [rel]);
770
+ for (const parent of ["apps", "packages"]) {
771
+ const dirs = subdirs(parent)
772
+ .map((name) => `${parent}/${name}/src`)
773
+ .filter((d) => existsSync(join(root, d)));
774
+ if (dirs.length) add(`${parent}/*/src`, dirs);
775
+ }
776
+ for (const name of subdirs("")) {
777
+ if (!found.some((glob) => glob.split("/")[0] === name)) add(name, [name]);
778
+ }
779
+ if (found.length) return found;
780
+ const scaffold = ["resources/js", "app/javascript", "src/components", "src", "app"].find((d) =>
697
781
  existsSync(join(root, d)),
698
782
  );
699
- if (!uiDir) return false;
783
+ return scaffold ? [scaffold] : [];
784
+ }
785
+
786
+ export function ensureConsumerRules(root) {
787
+ const uiDirs = detectUiDirs(root);
788
+ if (uiDirs.length === 0) return false;
789
+ const uiDir = uiDirs.join(", ");
700
790
 
701
791
  const dir = join(root, ".ai", "rules");
702
792
  const target = join(dir, "godxjp-ui.md");
703
793
  const body = readFileSync(join(SELF_ROOT, "scripts", "consumer-rule.md"), "utf8");
704
- const front = `---\npaths:\n - '${uiDir}/**'\n---\n\n`;
794
+ const front = `---\npaths:\n${uiDirs.map((d) => ` - '${d}/**'\n`).join("")}---\n\n`;
705
795
  // The digest covers the FRONT MATTER too: the detected glob is part of what makes this file
706
796
  // correct, and a repo that grows a `resources/js` after shipping with `src` needs the rewrite.
707
797
  const managed = `${front}${body}`;
@@ -745,7 +835,8 @@ export function ensureConsumerRules(root) {
745
835
  if (!cur.includes(".ai/rules/godxjp-ui.md")) {
746
836
  writeFileAtomic(
747
837
  index,
748
- cur.replace(/\s*$/, "") + `\n| ${uiDir}/** | .ai/rules/godxjp-ui.md |\n`,
838
+ cur.replace(/\s*$/, "") +
839
+ `\n| ${uiDirs.map((d) => `${d}/**`).join(", ")} | .ai/rules/godxjp-ui.md |\n`,
749
840
  );
750
841
  }
751
842
  }
@@ -780,7 +871,9 @@ export function runSyncRules(root, { dryRun = false } = {}) {
780
871
  if (!refused && !(r === "present" && md === "present" && !wf && !skill && !rules)) {
781
872
  lines.push(
782
873
  `\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` : "") +
874
+ (rules
875
+ ? ` common consumer rules in .ai/rules/godxjp-ui.md (UI dirs: ${rules}).\n`
876
+ : "") +
784
877
  (skill ? " guinea-pig skill refreshed to this version (your section 8 kept).\n" : "") +
785
878
  " Your agent now has live component + audit guidance. Restart it to pick up the MCP.\n" +
786
879
  " 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");
@@ -86,8 +109,12 @@ function changedFiles() {
86
109
  }
87
110
 
88
111
  const parts = [
89
- run(["diff", "--name-only", "--diff-filter=ACMR", mergeBase.trim(), "--"]),
90
- run(["diff", "--name-only", "--diff-filter=ACMR", "--cached"]),
112
+ // `--relative` (gh#1296): `git diff` names paths from the REPO root, `ls-files` from the cwd.
113
+ // Run from a workspace package (pages/) the root-relative names failed the existsSync below
114
+ // and were dropped, so 16 changed .tsx read as "no .tsx/.jsx changed" — a false clean.
115
+ // `--relative` makes both cwd-relative and limits them to the package being audited.
116
+ run(["diff", "--relative", "--name-only", "--diff-filter=ACMR", mergeBase.trim(), "--"]),
117
+ run(["diff", "--relative", "--name-only", "--diff-filter=ACMR", "--cached"]),
91
118
  run(["ls-files", "--others", "--exclude-standard"]),
92
119
  ];
93
120
  if (parts.some((out) => out === null)) {
@@ -1899,13 +1926,20 @@ function staleOwnedRules() {
1899
1926
  const contents = readFileSync(target, "utf8");
1900
1927
  const stamped = /<!-- godxjp-ui:version ([^\s]+) -->/.exec(contents)?.[1];
1901
1928
 
1929
+ // Up the node_modules chain, as Node resolves it: a workspace package has its @godxjp/ui hoisted
1930
+ // to the repo root, and reading only `CWD/node_modules` reported a correct install as
1931
+ // `owned-rules-unknown` (gh#1296). The package's `exports` map does not expose package.json,
1932
+ // so `require.resolve` cannot do this walk for us.
1902
1933
  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;
1934
+ for (let dir = CWD; installed === undefined; dir = dirname(dir)) {
1935
+ try {
1936
+ installed = JSON.parse(
1937
+ readFileSync(join(dir, "node_modules", "@godxjp", "ui", "package.json"), "utf8"),
1938
+ ).version;
1939
+ } catch {
1940
+ // Not here — keep walking.
1941
+ }
1942
+ if (dirname(dir) === dir) break;
1909
1943
  }
1910
1944
 
1911
1945
  if (!stamped || !installed) {
@@ -2521,7 +2555,7 @@ if (filesScanned === 0 && !changedNoFiles) {
2521
2555
  // Dưới `--changed`, NÓI RA base đã so. Một base sai không làm chương trình chết — nó cho ra một
2522
2556
  // con số hợp lý, và đó là thứ duy nhất người đọc có thể dùng để nghi ngờ (gh#948).
2523
2557
  if (CHANGED && changed?.base) {
2524
- console.log(`${C.dim}so với: ${changed.base}${C.reset}`);
2558
+ console.log(`${C.dim}compared with: ${changed.base}${C.reset}`);
2525
2559
  }
2526
2560
  // Bỏ file trong im lặng là cách một gate thu hẹp dần mà không ai thấy (cùng lý do với dòng
2527
2561
  // `so với:` ngay trên). Nói ra số file và lãnh thổ đã dùng.