@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.
- package/DESIGN.md +12 -2
- package/README.md +3 -3
- package/agent/DESIGN.md +12 -2
- package/agent/START-HERE.md +4 -4
- package/agent/components/Anchor.json +1 -1
- package/agent/components/ChatSuggestion.json +1 -1
- package/agent/components/LegalDocumentShell.json +7 -1
- package/agent/components/PageContainer.json +1 -2
- package/agent/components/Progress.json +1 -1
- package/agent/components-index.json +1 -1
- package/agent/components.json +11 -6
- package/agent/index.json +5 -5
- package/agent/llms.txt +6 -6
- package/agent/tokens.json +12 -0
- package/dist/components/layout/page-container.js +14 -8
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +2 -1
- package/dist/i18n/messages/ja.json +2 -1
- package/dist/i18n/messages/vi.json +2 -1
- package/dist/lab/legal-document-shell.js +7 -3
- package/dist/props/components/layout.prop.d.ts +13 -1
- package/dist/props/registry.d.ts +4 -0
- package/dist/props/registry.js +5 -0
- package/dist/styles/layers.json +1 -1
- package/dist/styles/layout.css +26 -0
- package/dist/styles.css +1 -1
- package/dist/tokens/components/legal-document.css +4 -0
- package/docs/CONSUMER-RULES.md +3 -3
- package/docs/migrations/v32.md +10 -5
- package/package.json +2 -2
- package/scripts/_agent-setup.mjs +110 -17
- package/scripts/cli.mjs +8 -0
- package/scripts/consumer-rule.md +9 -9
- package/scripts/ui-audit.mjs +44 -10
|
@@ -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));
|
package/docs/CONSUMER-RULES.md
CHANGED
|
@@ -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
|
|
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 `<
|
|
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 `
|
|
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
|
|
package/docs/migrations/v32.md
CHANGED
|
@@ -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
|
|
32
|
-
|
|
33
|
-
|
|
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` |
|
|
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
|
|
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
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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\`
|
|
181
|
-
|
|
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. \`
|
|
200
|
-
|
|
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: \`
|
|
204
|
-
|
|
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\` (
|
|
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. \`
|
|
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
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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
|
-
|
|
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 - '${
|
|
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*$/, "") +
|
|
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
|
|
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
|
|
package/scripts/consumer-rule.md
CHANGED
|
@@ -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 `
|
|
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
|
|
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ỉ
|
|
98
|
-
|
|
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
|
|
108
|
-
| ----------------------------------------------------------- |
|
|
109
|
-
| ≥2 `FormField` không có `Form`, hoặc xếp hàng bằng `<Flex>` | `<Form layout="horizontal" labelWidth controlWidth>`; hàng =
|
|
110
|
-
| ≥3 `FormField` trong `DialogBody` | page riêng (route riêng)
|
|
111
|
-
| `Select` hai lựa chọn kéo full width | `controlWidth` trên field hoặc một lần trên `Form`
|
|
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ế.
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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}
|
|
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.
|