@godxjp/ui 32.5.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.
Files changed (46) hide show
  1. package/DESIGN.md +210 -0
  2. package/README.md +3 -2
  3. package/agent/DESIGN.md +210 -0
  4. package/agent/START-HERE.md +25 -29
  5. package/agent/anti-ai-tells.json +56 -2
  6. package/agent/components/Anchor.json +1 -1
  7. package/agent/components/ChatSuggestion.json +1 -1
  8. package/agent/components/LegalDocumentShell.json +7 -1
  9. package/agent/components/PageContainer.json +1 -2
  10. package/agent/components/Progress.json +1 -1
  11. package/agent/components-index.json +1 -1
  12. package/agent/components.json +11 -6
  13. package/agent/index.json +11 -5
  14. package/agent/llms.txt +7 -6
  15. package/agent/patterns/badge-coloring.json +2 -2
  16. package/agent/patterns/common-fixes.json +1 -1
  17. package/agent/patterns/confirm-destructive.json +2 -2
  18. package/agent/patterns/inertia-detail-page.json +2 -2
  19. package/agent/patterns/inertia-list-page.json +2 -2
  20. package/agent/patterns/page-sections.json +5 -3
  21. package/agent/patterns/settings-section-rows.json +2 -2
  22. package/agent/patterns-index.json +9 -7
  23. package/agent/patterns.json +16 -14
  24. package/agent/tokens.json +12 -0
  25. package/dist/components/data-entry/select.js +5 -4
  26. package/dist/components/layout/page-container.js +14 -8
  27. package/dist/contracts/measurement.json +1 -1
  28. package/dist/i18n/messages/en.json +2 -1
  29. package/dist/i18n/messages/ja.json +2 -1
  30. package/dist/i18n/messages/vi.json +2 -1
  31. package/dist/lab/legal-document-shell.js +7 -3
  32. package/dist/props/components/layout.prop.d.ts +13 -1
  33. package/dist/props/registry.d.ts +4 -0
  34. package/dist/props/registry.js +5 -0
  35. package/dist/styles/layers.json +1 -1
  36. package/dist/styles/layout.css +26 -0
  37. package/dist/styles.css +1 -1
  38. package/dist/tokens/components/legal-document.css +4 -0
  39. package/docs/CONSUMER-RULES.md +6 -5
  40. package/docs/README.md +1 -1
  41. package/docs/migrations/v32.md +10 -5
  42. package/package.json +3 -2
  43. package/scripts/_agent-setup.mjs +149 -85
  44. package/scripts/cli.mjs +8 -0
  45. package/scripts/consumer-rule.md +41 -77
  46. package/scripts/ui-audit.mjs +189 -29
@@ -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));
@@ -1,12 +1,13 @@
1
- # @godxjp/ui — the ten consumer rules
1
+ # @godxjp/ui — the consumer rules
2
2
 
3
- Read this once; the audit enforces it. Everything else in `docs/` is for contributors.
3
+ Read this once; the audit enforces it. What the audit cannot see (how a screen is composed, what it
4
+ should look like) is in the package's `DESIGN.md`. Everything else in `docs/` is for contributors.
4
5
 
5
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`.
6
- 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 subtitle extra footer>`; 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>`.
7
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>`.
8
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`.
9
- 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).
10
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`.
11
12
  7. Colours are semantic tokens (`tone="destructive"`, `bg-primary`), never palette names, hex, or `bg-black` / `text-white`.
12
13
  8. Sizes come from props (`size`, `width`, `columns`), never `w-[240px]` / `max-h-[420px]`.
@@ -16,7 +17,7 @@ Read this once; the audit enforces it. Everything else in `docs/` is for contrib
16
17
  the table reads as a second box inside the card, and a wide one runs past the card entirely.
17
18
  (`flush` alone is enough: it drops the border, the radius and the inline padding. `tight` is a
18
19
  different knob — it governs the header band, not the body's edges.)
19
- 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.
20
21
 
21
22
  **Opting a deliberate exception out.** Name the rule; the block form also has to say why.
22
23
 
package/docs/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Purpose |
4
4
  | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5
- | [CONSUMER-RULES.md](./CONSUMER-RULES.md) | **Start here if you build an app with @godxjp/ui** — the ten rules the audit enforces (styles entry, layout owners, real primitives, tokens) |
5
+ | [CONSUMER-RULES.md](./CONSUMER-RULES.md) | **Start here if you build an app with @godxjp/ui** — the rules the audit enforces (styles entry, layout owners, real primitives, tokens) |
6
6
  | [DEVELOPMENT.md](./DEVELOPMENT.md) | **Contributor guideline** — role/boundary, architecture, how to add/extend, verify |
7
7
  | [COMPONENTS.md](./COMPONENTS.md) | Component catalog by taxonomy group |
8
8
  | [PROPS-VOCABULARY.md](./PROPS-VOCABULARY.md) | Atomic prop types (`*Prop` suffix) |
@@ -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.5.0",
4
- "godxUiMcp": "32.5.0",
3
+ "version": "32.7.0",
4
+ "godxUiMcp": "32.7.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -28,6 +28,7 @@
28
28
  "description": "React UI framework built on design tokens: accessible components, a controlled prop vocabulary, and a machine-readable catalog for coding agents.",
29
29
  "files": [
30
30
  "LICENSE",
31
+ "DESIGN.md",
31
32
  "agent",
32
33
  "dist",
33
34
  "scripts/ui-audit.mjs",
@@ -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),
@@ -167,87 +177,62 @@ function resyncVersionStamp(text) {
167
177
  * appended repo-specific sections keeps them.
168
178
  */
169
179
  export const WORKFLOW_MD = `${STAMP(KIT_VERSION)}
170
- # @godxjp/ui — mandatory workflow (read every session)
171
-
172
- You are building UI in an app that uses @godxjp/ui. Follow this EVERY time you create
173
- or change a component, page, or form — no exceptions.
174
-
175
- ## The rules the audit enforces (docs/CONSUMER-RULES.md in node_modules/@godxjp/ui)
176
- - Styles: \`@import "@godxjp/ui/styles"\` or \`@godxjp/ui/styles/core\` (no fonts). Never cherry-pick layers.
177
- - Layout: sections of a page are spaced by \`PageContainer\`; siblings go in \`<Flex direction="col" gap>\` /
180
+ # @godxjp/ui — workflow (read every session)
181
+
182
+ Follow this every time you create or change a page, component or form.
183
+
184
+ ## Before JSX
185
+ 1. **Design target.** Read \`node_modules/@godxjp/ui/DESIGN.md\` (MCP \`get_design\`): tokens,
186
+ scarcity rules, screen anatomy, do/don't.
187
+ 2. **Route.** \`route_consumer_task "<task>"\` → \`compose-a-screen\` for app screens. The
188
+ marketing skills (soft, minimalist, brutalist, gpt-tasteskill, imagegen-*) are for landing
189
+ pages only.
190
+ 3. **Exemplar.** \`get_exemplar\` with archetype list | detail | form | settings | dashboard |
191
+ approval. Match its structure; it is the visual target.
192
+ 4. **Discover, then look up.** \`search_components "<area>"\` first, \`get_component <Name>\`
193
+ second — \`get_component\` cannot find a name you do not already have (a consumer hand-rolled
194
+ the shipped \`OrgSwitcher\` and \`TopbarItem\` this way). Code already in this repo is not the
195
+ authority: search even when a neighbouring file seems to answer. Never guess a prop.
196
+
197
+ ## Contract (ui-audit enforces it; full text in node_modules/@godxjp/ui/docs/CONSUMER-RULES.md)
198
+ - Styles: \`@import "@godxjp/ui/styles"\` (or \`import "@godxjp/ui/styles.css"\`); fonts are opt-in
199
+ via \`@godxjp/ui/styles/fonts.css\`. Never cherry-pick layers.
200
+ - Layout: \`PageContainer\` spaces sections; siblings go in \`<Flex direction="col" gap>\` /
178
201
  \`<ResponsiveGrid>\`. No \`flex\` / \`grid\` / \`gap-*\` / \`p-*\` / \`m-*\` on your own elements.
179
- - Surfaces: \`Card\` / \`Badge\` / \`Avatar\` / \`ListRow\` / \`Descriptions\` / \`EmptyState\` — never a
180
- \`rounded border\` div. Text is \`<Text tone size weight truncate>\`, never utility classes.
181
-
182
- ## Before writing UI
183
- 1. **DISCOVERY BEFORE LOOKUP — in this order, every time.**
184
- 1. \`search_components "<the area you are about to build>"\` FIRST. This is the only call that
185
- answers "what already exists here". Ask it before the first line of JSX, not after.
186
- 2. \`get_component <Name>\` second, once you know the name.
187
- \`get_component\` answers "how do I use X" and is USELESS for finding X — you can only look up a
188
- name you already have. Skipping step 1 is how a consumer hand-rolled an organization switcher
189
- the package ships (\`OrgSwitcher\`) and built three topbar cells out of Buttons instead of
190
- \`TopbarItem\`: both were one \`search_components\` call away, and neither was ever searched for.
191
- **Existing code in this repo is NOT the authority.** Reading a neighbouring file and copying it
192
- is the most common way this rule gets skipped — measured: of four fresh agents given a vague UI
193
- task, all four answered correctly but two never called the MCP at all, saying the answer was
194
- already in the code. They were only right because that code had just been fixed; a week earlier
195
- the same files hand-rolled a switcher and put Buttons in topbar slots, and both agents would
196
- have copied it. Search the catalog even when a local file looks like it already answers you.
197
- Also \`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
198
- primitive already does.
199
- 2. **Real primitives only.** No raw \`<input>/<select>/<button>/<textarea>/<table>\`, no
200
- styled-div fakes. A labelled control ALWAYS goes in \`<FormField label=…>\` (it owns the
201
- label↔control wiring, error/aria, AND the field rhythm). Never pair a bare \`<Label>\`
202
- with an \`<Input>\`.
203
- 3. **Tokens, not literals.** Semantic colour tokens (bg-primary, text-muted-foreground),
204
- token spacing/size/radius/type — no raw palette (bg-blue-600), no hex, no arbitrary
205
- \`[13px]\`. No emoji in product UI. Logical CSS (ms-/me-/ps-/pe-) for RTL.
206
-
207
- ## After writing UI
208
- 4. **Audit (runs automatically too).** \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs\`
209
- — fix every finding. A PostToolUse hook also runs it on each edit and feeds findings back.
210
- 5. **Before a visual review**, run the runtime audit against the running app:
211
- \`node node_modules/@godxjp/ui/scripts/visual-audit.mjs <url>\` (axe + contrast + layout).
202
+ - Surfaces: \`Card\` / \`Badge\` / \`Avatar\` / \`ListRow\` / \`Descriptions\` / \`EmptyState\`, never a
203
+ \`rounded border\` div. Text is \`<Text tone size weight>\`.
204
+ - Real controls only; a labelled control sits in \`<FormField label>\` inside a \`<Form>\`.
205
+ - Semantic tokens only: no hex, palette names, arbitrary \`[13px]\`, emoji. Logical CSS (ms-/me-).
206
+ - Screen composition (what the audit cannot see) is in \`.ai/rules/godxjp-ui.md\`.
207
+
208
+ ## After JSX
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.
211
+ 6. Screenshot at 390 / 768 / 1280 next to the exemplar, list the top 3 differences, fix them,
212
+ repeat — at most 3 rounds.
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\`).
212
217
  `;
213
218
 
214
219
  /** Delimited block appended to the consumer's CLAUDE.md — loaded into the agent's context
215
- * every turn (the most reliable "ensure it reads the rules"). Markers keep it idempotent. */
216
- export const CLAUDE_MD_BLOCK = `<!-- godxjp-ui:start (managed by @godxjp/ui — edit .claude/godxjp-ui-workflow.md instead) -->
220
+ * every turn, so it carries only the default path and points at the workflow for the rest.
221
+ * Markers keep it idempotent. */
222
+ export const CLAUDE_MD_BLOCK = `<!-- godxjp-ui:start (managed by @godxjp/ui — rewritten by npx godxjp-ui sync-rules) -->
217
223
  ${STAMP(KIT_VERSION)}
218
- ## @godxjp/ui — mandatory UI workflow (do NOT skip)
219
-
220
- This app uses @godxjp/ui. EVERY time you build or change UI:
221
-
222
- 0. **Ten rules** — read \`node_modules/@godxjp/ui/docs/CONSUMER-RULES.md\` once. Styles via
223
- \`@godxjp/ui/styles\` or \`styles/core\` (never cherry-pick layers). Page sections are spaced by
224
- \`PageContainer\`; siblings go in \`<Flex direction="col" gap>\` / \`<ResponsiveGrid>\`; no
225
- \`flex\`/\`grid\`/\`gap-*\`/\`p-*\`/\`m-*\` on your own elements; boxes are \`Card\`/\`Badge\`/
226
- \`ListRow\`/\`Descriptions\`, never a \`rounded border\` div; text is \`<Text tone size weight>\`.
227
- 1. **DISCOVERY BEFORE LOOKUP — in this order, every time.**
228
- 1. \`search_components "<the area you are about to build>"\` FIRST. This is the only call that
229
- answers "what already exists here". Ask it before the first line of JSX, not after.
230
- 2. \`get_component <Name>\` second, once you know the name.
231
- \`get_component\` answers "how do I use X" and is USELESS for finding X — you can only look up a
232
- name you already have. Skipping step 1 is how a consumer hand-rolled an organization switcher
233
- the package ships (\`OrgSwitcher\`) and built three topbar cells out of Buttons instead of
234
- \`TopbarItem\`: both were one \`search_components\` call away, and neither was ever searched for.
235
- **Existing code in this repo is NOT the authority.** Reading a neighbouring file and copying it
236
- is the most common way this rule gets skipped — measured: of four fresh agents given a vague UI
237
- task, all four answered correctly but two never called the MCP at all, saying the answer was
238
- already in the code. They were only right because that code had just been fixed; a week earlier
239
- the same files hand-rolled a switcher and put Buttons in topbar slots, and both agents would
240
- have copied it. Search the catalog even when a local file looks like it already answers you.
241
- Also \`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
242
- primitive already does.
243
- 2. **Real primitives only.** No raw \`<input>/<select>/<button>/<textarea>/<table>\`. A labelled
244
- control ALWAYS goes in \`<FormField label=…>\` — never a bare \`<Label>\`+\`<Input>\`. Semantic
245
- tokens, not raw palette/hex/arbitrary values. No emoji in product UI. Logical CSS for RTL.
246
- 3. **Scan after writing.** \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs <files>\` and fix
247
- every finding (a PostToolUse hook runs this automatically and feeds findings back). Before a
248
- visual review: \`node node_modules/@godxjp/ui/scripts/visual-audit.mjs <url>\` (axe + layout).
249
-
250
- Full guide: \`.claude/godxjp-ui-workflow.md\`.
224
+ ## @godxjp/ui — UI workflow
225
+
226
+ Before any JSX for a screen:
227
+ 1. Read \`node_modules/@godxjp/ui/DESIGN.md\` (MCP \`get_design\`).
228
+ 2. \`route_consumer_task\` → \`compose-a-screen\`; \`search_components\` before hand-rolling anything.
229
+ 3. \`get_exemplar\` (list | detail | form | settings | dashboard | approval) — match its structure.
230
+
231
+ After building:
232
+ 4. \`npx @godxjp/ui audit <files>\` — zero errors.
233
+ 5. Screenshot at 390 / 768 / 1280 beside the exemplar; fix the top 3 differences; max 3 rounds.
234
+
235
+ Rules: \`node_modules/@godxjp/ui/docs/CONSUMER-RULES.md\`. Details: \`.claude/godxjp-ui-workflow.md\`.
251
236
  <!-- godxjp-ui:end -->
252
237
  `;
253
238
 
@@ -550,7 +535,13 @@ export function ensureClaudeHooks(root) {
550
535
  (arr ?? []).some((g) => (g.hooks ?? []).some((h) => (h.command ?? "").includes(cmd)));
551
536
 
552
537
  json.hooks.PostToolUse = json.hooks.PostToolUse ?? [];
553
- 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")) {
554
545
  json.hooks.PostToolUse.push({
555
546
  matcher: "Write|Edit|MultiEdit",
556
547
  hooks: [{ type: "command", command: AUDIT_HOOK_CMD }],
@@ -719,16 +710,86 @@ export function guineaPigStamp() {
719
710
  * this one. The file says so at the top, because the first draft preserved nothing and silently
720
711
  * ate a note left inside it — measured, and the reason for that banner.
721
712
  */
722
- export function ensureConsumerRules(root) {
723
- 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) =>
724
779
  existsSync(join(root, d)),
725
780
  );
726
- 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(", ");
727
788
 
728
789
  const dir = join(root, ".ai", "rules");
729
790
  const target = join(dir, "godxjp-ui.md");
730
791
  const body = readFileSync(join(SELF_ROOT, "scripts", "consumer-rule.md"), "utf8");
731
- const front = `---\npaths:\n - '${uiDir}/**'\n---\n\n`;
792
+ const front = `---\npaths:\n${uiDirs.map((d) => ` - '${d}/**'\n`).join("")}---\n\n`;
732
793
  // The digest covers the FRONT MATTER too: the detected glob is part of what makes this file
733
794
  // correct, and a repo that grows a `resources/js` after shipping with `src` needs the rewrite.
734
795
  const managed = `${front}${body}`;
@@ -772,7 +833,8 @@ export function ensureConsumerRules(root) {
772
833
  if (!cur.includes(".ai/rules/godxjp-ui.md")) {
773
834
  writeFileAtomic(
774
835
  index,
775
- 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`,
776
838
  );
777
839
  }
778
840
  }
@@ -807,7 +869,9 @@ export function runSyncRules(root, { dryRun = false } = {}) {
807
869
  if (!refused && !(r === "present" && md === "present" && !wf && !skill && !rules)) {
808
870
  lines.push(
809
871
  `\n @godxjp/ui → MCP in .mcp.json (${r}); workflow mandate in CLAUDE.md (${md}).\n` +
810
- (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
+ : "") +
811
875
  (skill ? " guinea-pig skill refreshed to this version (your section 8 kept).\n" : "") +
812
876
  " Your agent now has live component + audit guidance. Restart it to pick up the MCP.\n" +
813
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