@godxjp/ui 19.6.0 → 20.0.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 (137) hide show
  1. package/dist/components/data-display/card.d.ts +2 -2
  2. package/dist/components/data-display/collapsible.d.ts +23 -4
  3. package/dist/components/data-display/collapsible.js +119 -4
  4. package/dist/components/data-display/index.d.ts +3 -1
  5. package/dist/components/data-display/index.js +2 -0
  6. package/dist/components/data-display/legend.d.ts +15 -0
  7. package/dist/components/data-display/legend.js +11 -0
  8. package/dist/components/data-display/list-row.js +1 -1
  9. package/dist/components/data-display/popover.d.ts +58 -5
  10. package/dist/components/data-display/popover.js +206 -27
  11. package/dist/components/data-display/progress.d.ts +50 -2
  12. package/dist/components/data-display/progress.js +57 -9
  13. package/dist/components/data-entry/calendar.d.ts +1 -1
  14. package/dist/components/data-entry/calendar.js +67 -32
  15. package/dist/components/data-entry/checkbox.d.ts +19 -6
  16. package/dist/components/data-entry/checkbox.js +55 -16
  17. package/dist/components/data-entry/choice-option.d.ts +1 -1
  18. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  19. package/dist/components/data-entry/field.js +0 -1
  20. package/dist/components/data-entry/form.js +1 -1
  21. package/dist/components/data-entry/label.d.ts +3 -2
  22. package/dist/components/data-entry/label.js +23 -10
  23. package/dist/components/data-entry/month-picker.d.ts +1 -1
  24. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  25. package/dist/components/data-entry/transfer.js +16 -6
  26. package/dist/components/data-entry/tree-select-strategy.d.ts +1 -1
  27. package/dist/components/data-entry/tree-select.js +29 -12
  28. package/dist/components/data-entry/tree-utils.d.ts +1 -1
  29. package/dist/components/data-entry/upload.js +7 -2
  30. package/dist/components/feedback/dialog.d.ts +105 -38
  31. package/dist/components/feedback/dialog.js +272 -194
  32. package/dist/components/feedback/overlay-close-focus.d.ts +31 -0
  33. package/dist/components/feedback/overlay-close-focus.js +31 -0
  34. package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
  35. package/dist/components/feedback/sheet.d.ts +53 -11
  36. package/dist/components/feedback/sheet.js +150 -81
  37. package/dist/components/feedback/tooltip.d.ts +51 -7
  38. package/dist/components/feedback/tooltip.js +107 -25
  39. package/dist/components/general/button.js +2 -2
  40. package/dist/components/general/logo.d.ts +17 -0
  41. package/dist/components/general/logo.js +22 -16
  42. package/dist/components/layout/app-shell.d.ts +1 -1
  43. package/dist/components/layout/app-shell.js +52 -8
  44. package/dist/components/layout/aspect-ratio.js +0 -1
  45. package/dist/components/layout/auth-divider.js +0 -1
  46. package/dist/components/layout/flex.d.ts +1 -1
  47. package/dist/components/layout/flex.js +19 -2
  48. package/dist/components/layout/nav-surface.d.ts +26 -0
  49. package/dist/components/layout/nav-surface.js +17 -0
  50. package/dist/components/layout/org-switcher.d.ts +5 -1
  51. package/dist/components/layout/org-switcher.js +5 -3
  52. package/dist/components/layout/separator.js +0 -1
  53. package/dist/components/layout/sidebar.js +4 -1
  54. package/dist/components/layout/split-pane.d.ts +14 -1
  55. package/dist/components/layout/split-pane.js +26 -13
  56. package/dist/components/layout/topbar-item.js +1 -2
  57. package/dist/components/navigation/app-setting-picker.js +21 -2
  58. package/dist/components/navigation/app-setting-toggle.d.ts +16 -0
  59. package/dist/components/navigation/app-setting-toggle.js +96 -0
  60. package/dist/components/navigation/dropdown-menu.d.ts +199 -18
  61. package/dist/components/navigation/dropdown-menu.js +344 -117
  62. package/dist/components/navigation/index.d.ts +2 -0
  63. package/dist/components/navigation/index.js +2 -0
  64. package/dist/components/navigation/pagination-utils.d.ts +2 -1
  65. package/dist/components/navigation/tabs.d.ts +47 -6
  66. package/dist/components/navigation/tabs.js +152 -90
  67. package/dist/components/ui/accordion.d.ts +50 -5
  68. package/dist/components/ui/accordion.js +239 -33
  69. package/dist/components/ui/aspect-ratio.d.ts +23 -2
  70. package/dist/components/ui/aspect-ratio.js +15 -13
  71. package/dist/components/ui/avatar.d.ts +29 -4
  72. package/dist/components/ui/avatar.js +111 -25
  73. package/dist/components/ui/hover-card.d.ts +42 -4
  74. package/dist/components/ui/hover-card.js +183 -27
  75. package/dist/components/ui/label.js +0 -1
  76. package/dist/components/ui/segmented.d.ts +3 -3
  77. package/dist/components/ui/separator.d.ts +8 -2
  78. package/dist/components/ui/separator.js +17 -9
  79. package/dist/components/ui/toggle-group.d.ts +50 -5
  80. package/dist/components/ui/toggle-group.js +79 -20
  81. package/dist/components/ui/toggle.d.ts +31 -5
  82. package/dist/components/ui/toggle.js +42 -3
  83. package/dist/i18n/messages/en.json +8 -0
  84. package/dist/i18n/messages/ja.json +8 -0
  85. package/dist/i18n/messages/vi.json +8 -0
  86. package/dist/lib/control-styles.d.ts +1 -1
  87. package/dist/lib/control-styles.js +1 -1
  88. package/dist/lib/slot.d.ts +32 -0
  89. package/dist/lib/slot.js +22 -0
  90. package/dist/lib/variants.d.ts +22 -3
  91. package/dist/lib/variants.js +56 -1
  92. package/dist/props/components/app.prop.d.ts +25 -1
  93. package/dist/props/components/data-display.prop.d.ts +17 -0
  94. package/dist/props/components/data-entry.prop.d.ts +32 -4
  95. package/dist/props/components/index.d.ts +1 -1
  96. package/dist/props/components/layout.prop.d.ts +118 -5
  97. package/dist/props/components/navigation.prop.d.ts +1 -1
  98. package/dist/props/registry.d.ts +22 -2
  99. package/dist/props/registry.js +33 -2
  100. package/dist/props/vocabulary/content.prop.d.ts +1 -1
  101. package/dist/props/vocabulary/index.d.ts +2 -2
  102. package/dist/props/vocabulary/interaction.prop.d.ts +17 -1
  103. package/dist/props/vocabulary/layout.prop.d.ts +57 -1
  104. package/dist/styles/control.css +142 -1
  105. package/dist/styles/core.css +5 -2
  106. package/dist/styles/data-display-layout.css +72 -0
  107. package/dist/styles/focus-ring.css +4 -0
  108. package/dist/styles/index.css +5 -2
  109. package/dist/styles/layout.css +106 -0
  110. package/dist/styles/shell-layout.css +140 -8
  111. package/dist/styles/text-layout.css +5 -4
  112. package/dist/tokens/base.css +1 -1
  113. package/dist/tokens/components/data-display.css +11 -0
  114. package/dist/tokens/components/shell.css +3 -1
  115. package/dist/tokens/foundation.css +2 -0
  116. package/docs/COMPONENTS.md +9 -3
  117. package/docs/DESIGN-AUTHORITY.md +96 -71
  118. package/docs/FRAME-COVERAGE-REPORT.md +5 -3
  119. package/docs/README.md +14 -14
  120. package/docs/WHAT-BELONGS-HERE.md +179 -0
  121. package/docs/data-display/legend.tsx +145 -0
  122. package/docs/data-display/progress.tsx +32 -0
  123. package/docs/data-entry/segmented.tsx +1 -1
  124. package/docs/data-entry/select.tsx +2 -2
  125. package/docs/feedback/sheet.tsx +1 -1
  126. package/docs/layout/app-shell-arrangements.tsx +225 -0
  127. package/docs/layout/aspect-ratio.tsx +1 -1
  128. package/docs/navigation/app-setting-picker.tsx +26 -1
  129. package/docs/navigation/app-setting-toggle.tsx +111 -0
  130. package/package.json +11 -8
  131. package/scripts/_agent-setup.mjs +165 -3
  132. package/scripts/consumer-rule.md +98 -0
  133. package/scripts/guinea-pig-skill.md +322 -0
  134. package/scripts/init-guinea-pig.mjs +62 -0
  135. package/scripts/postinstall.mjs +13 -2
  136. package/scripts/ui-audit.mjs +115 -14
  137. /package/dist/tokens/{antd.generated.css → derived.css} +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "19.6.0",
4
- "godxUiMcp": "19.6.0",
3
+ "version": "20.0.0",
4
+ "godxUiMcp": "20.0.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -30,6 +30,9 @@
30
30
  "scripts/visual-audit-rules.mjs",
31
31
  "scripts/audit-hook.mjs",
32
32
  "scripts/init-agent-kit.mjs",
33
+ "scripts/init-guinea-pig.mjs",
34
+ "scripts/guinea-pig-skill.md",
35
+ "scripts/consumer-rule.md",
33
36
  "scripts/postinstall.mjs",
34
37
  "scripts/_agent-setup.mjs",
35
38
  "scripts/cli.mjs",
@@ -308,6 +311,7 @@
308
311
  "format": "prettier --check .",
309
312
  "format:fix": "prettier --write .",
310
313
  "test": "vitest run",
314
+ "test:manual": "vitest run --config vitest.manual.config.ts",
311
315
  "test:visual:auth-login": "node scripts/auth-shell-login-visual.mjs",
312
316
  "test:visual:auth-registration": "node scripts/auth-shell-registration-visual.mjs",
313
317
  "test:visual:topbar-collision": "node scripts/topbar-collision-visual.mjs",
@@ -335,8 +339,8 @@
335
339
  "check:frame-runtime": "pnpm check:data-entry-frame-runtime && pnpm check:data-entry-touch-aria && pnpm check:layout-nav-frames && pnpm check:provider-feedback-query-runtime && pnpm check:layout-nav-closure && pnpm check:final-touch-rtl && pnpm check:data-table-pagination-wrap && pnpm check:button-icon-xs",
336
340
  "verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
337
341
  "verify:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm check:visual-audit && pnpm test",
338
- "verify:ci": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm test",
339
- "verify:ci:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync",
342
+ "verify:ci": "pnpm verify:ci:static && pnpm check:frame-contracts && pnpm test",
343
+ "verify:ci:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:registry && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:no-antd-runtime && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:gate-coverage && pnpm check:mcp-prop-sync",
340
344
  "verify:browser": "pnpm check:contrast && pnpm check:visual-audit",
341
345
  "verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage && pnpm check:frame-axe",
342
346
  "verify:publish-tree": "pnpm build && pnpm check:packed-public-contract && pnpm check:use-client && pnpm check:dist-tokens-resolve",
@@ -365,9 +369,6 @@
365
369
  "check:rtl": "node scripts/check-rtl.mjs",
366
370
  "check:typography": "node scripts/check-typography.mjs",
367
371
  "gen:component-tokens": "node scripts/gen-component-tokens.mjs",
368
- "gen:antd-tokens": "node scripts/gen-antd-tokens.mjs",
369
- "check:antd-tokens": "node scripts/gen-antd-tokens.mjs --check",
370
- "report:antd-tokens": "node scripts/gen-antd-tokens.mjs --report",
371
372
  "check:no-antd-runtime": "node scripts/check-no-antd-runtime.mjs",
372
373
  "gen:email-tokens": "node scripts/gen-email-tokens.mjs",
373
374
  "check:core-isolation": "node scripts/check-core-isolation.mjs",
@@ -384,6 +385,7 @@
384
385
  "dev": "node scripts/dev.mjs",
385
386
  "prepublishOnly": "pnpm run build",
386
387
  "check:audit-sync": "node scripts/check-audit-sync.mjs",
388
+ "check:gate-coverage": "node scripts/check-gate-coverage.mjs",
387
389
  "check:contrast": "node scripts/check-contrast.mjs",
388
390
  "visual-audit": "node scripts/visual-audit.mjs",
389
391
  "check:visual-audit": "node scripts/visual-audit-smoke.mjs",
@@ -451,6 +453,7 @@
451
453
  "@radix-ui/react-toggle": "^1.1.18",
452
454
  "@radix-ui/react-toggle-group": "^1.1.19",
453
455
  "@radix-ui/react-tooltip": "^1.2.16",
456
+ "@react-aria/utils": "^3.34.1",
454
457
  "@tanstack/react-table": "^9.2.4",
455
458
  "class-variance-authority": "^0.7.1",
456
459
  "clsx": "^2.1.1",
@@ -460,6 +463,7 @@
460
463
  "input-otp": "^1.5.0",
461
464
  "lucide-react": "^1.37.0",
462
465
  "qrcode.react": "4.2.0",
466
+ "react-aria-components": "^1.21.1",
463
467
  "react-day-picker": "^10.0.1",
464
468
  "react-resizable-panels": "^4.12.3",
465
469
  "sonner": "^2.0.8",
@@ -483,7 +487,6 @@
483
487
  "@types/react-dom": "^19.2.5",
484
488
  "@vitejs/plugin-react": "^6.1.1",
485
489
  "@vitest/coverage-v8": "^4.1.11",
486
- "antd": "6.6.2",
487
490
  "axe-core": "^4.13.0",
488
491
  "eslint": "^10.9.1",
489
492
  "eslint-plugin-react": "^7.37.5",
@@ -4,10 +4,15 @@
4
4
  * NON-DESTRUCTIVE: it only creates a missing file or ADDS a missing key, never
5
5
  * overwrites existing config.
6
6
  */
7
+ import { createHash } from "node:crypto";
7
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
9
  import { dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
9
11
 
10
12
  /** The godxjp-ui MCP server — pulled on demand via npx (no extra dependency to ship). */
13
+ /** This package's own root — the source of the version we stamp with. */
14
+ const SELF_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
15
+
11
16
  export const MCP_SERVER = { command: "npx", args: ["@godxjp/ui-mcp"] };
12
17
  export const MCP_KEY = "godx-ui";
13
18
 
@@ -16,7 +21,47 @@ export const AUDIT_HOOK_CMD = "node node_modules/@godxjp/ui/scripts/audit-hook.m
16
21
  export const PRIMER_CMD = "cat .claude/godxjp-ui-workflow.md";
17
22
 
18
23
  /** The per-session workflow mandate the SessionStart hook injects into the agent. */
19
- export const WORKFLOW_MD = `# @godxjp/ui mandatory workflow (read every session)
24
+ export const KIT_VERSION = readJson(join(SELF_ROOT, "package.json"))?.version ?? "0.0.0";
25
+
26
+ const STAMP = (v) => `<!-- godxjp-ui:version ${v} -->`;
27
+ const STAMP_RE = /<!-- godxjp-ui:version ([^\s]+) -->/;
28
+
29
+ /** The version stamped in `text`, or null when it predates stamping. */
30
+ export function stampedVersion(text) {
31
+ return text?.match(STAMP_RE)?.[1] ?? null;
32
+ }
33
+
34
+ /**
35
+ * A stamp that tracks the CONTENT, for the files this package owns outright.
36
+ *
37
+ * A version stamp answers "which release wrote this", which is the wrong question for a file whose
38
+ * whole job is to carry current guidance. Measured: editing `consumer-rule.md` without bumping the
39
+ * package left two of three consumers holding the previous text, with a stamp that read as current
40
+ * and nothing anywhere reporting a difference. The rules are edited far more often than the
41
+ * version moves — most of all while they are being written, which is exactly when a stale copy
42
+ * does the most damage.
43
+ *
44
+ * So the digest goes in alongside the version: the version stays for humans reading the file, and
45
+ * the digest is what the refresh actually compares. Identical body → still a no-op.
46
+ */
47
+ const DIGEST_RE = /<!-- godxjp-ui:digest ([0-9a-f]{12}) -->/;
48
+ const digestOf = (body) => createHash("sha256").update(body).digest("hex").slice(0, 12);
49
+ const OWNED_STAMP = (body) => `${STAMP(KIT_VERSION)}\n<!-- godxjp-ui:digest ${digestOf(body)} -->`;
50
+
51
+ /** The content digest stamped in `text`, or null when it predates digest stamping. */
52
+ export function stampedDigest(text) {
53
+ return text?.match(DIGEST_RE)?.[1] ?? null;
54
+ }
55
+
56
+ /**
57
+ * Replace the MANAGED region of a file and leave everything else alone.
58
+ *
59
+ * Refreshing is only safe if it cannot eat hand-written content, so the contract is narrow: the
60
+ * region is delimited, and anything outside the delimiters survives byte-for-byte. A consumer that
61
+ * appended repo-specific sections keeps them.
62
+ */
63
+ export const WORKFLOW_MD = `${STAMP(KIT_VERSION)}
64
+ # @godxjp/ui — mandatory workflow (read every session)
20
65
 
21
66
  You are building UI in an app that uses @godxjp/ui. Follow this EVERY time you create
22
67
  or change a component, page, or form — no exceptions.
@@ -50,6 +95,7 @@ or change a component, page, or form — no exceptions.
50
95
  /** Delimited block appended to the consumer's CLAUDE.md — loaded into the agent's context
51
96
  * every turn (the most reliable "ensure it reads the rules"). Markers keep it idempotent. */
52
97
  export const CLAUDE_MD_BLOCK = `<!-- godxjp-ui:start (managed by @godxjp/ui — edit .claude/godxjp-ui-workflow.md instead) -->
98
+ ${STAMP(KIT_VERSION)}
53
99
  ## @godxjp/ui — mandatory UI workflow (do NOT skip)
54
100
 
55
101
  This app uses @godxjp/ui. EVERY time you build or change UI:
@@ -82,6 +128,24 @@ function readJson(path) {
82
128
  }
83
129
 
84
130
  /** Ensure `.mcp.json` registers the godx-ui MCP server. Returns 'created' | 'added' | 'present'. */
131
+ /**
132
+ * The version that wrote each managed artefact, stamped so `postinstall` can tell "already there"
133
+ * apart from "already there and STALE".
134
+ *
135
+ * Everything below used to be install-once: `ensureMcpJson` returned early on a present key,
136
+ * `writeWorkflowMd` on a present file, `ensureClaudeMd` on a present marker. So `npm update
137
+ * @godxjp/ui` brought new components, new audit rules and new catalog entries — and left the
138
+ * agent reading whatever guidance shipped the day the package was FIRST installed. The library
139
+ * moved; the instructions for using it did not.
140
+ */
141
+ export function refreshBlock(current, next, startMarker, endMarker) {
142
+ const i = current.indexOf(startMarker);
143
+ if (i < 0) return current.replace(/\s*$/, "") + "\n\n" + next;
144
+ const j = endMarker ? current.indexOf(endMarker, i) : -1;
145
+ const tail = j < 0 ? "" : current.slice(j + endMarker.length);
146
+ return current.slice(0, i) + next + tail;
147
+ }
148
+
85
149
  export function ensureMcpJson(root) {
86
150
  const path = join(root, ".mcp.json");
87
151
  const json = readJson(path) ?? {};
@@ -127,7 +191,14 @@ export function ensureClaudeHooks(root) {
127
191
  export function writeWorkflowMd(root) {
128
192
  const path = join(root, ".claude", "godxjp-ui-workflow.md");
129
193
  mkdirSync(dirname(path), { recursive: true });
130
- if (existsSync(path)) return false;
194
+ // This file is owned entirely by the package — the CLAUDE.md block tells the consumer to edit
195
+ // it, but nothing else reads from it, so a stale copy is pure loss. Rewrite when the stamp moves.
196
+ if (existsSync(path)) {
197
+ const cur = readFileSync(path, "utf8");
198
+ if (stampedVersion(cur) === KIT_VERSION) return false;
199
+ writeFileSync(path, WORKFLOW_MD);
200
+ return "refreshed";
201
+ }
131
202
  writeFileSync(path, WORKFLOW_MD);
132
203
  return true;
133
204
  }
@@ -139,7 +210,16 @@ export function writeWorkflowMd(root) {
139
210
  export function ensureClaudeMd(root) {
140
211
  const path = join(root, "CLAUDE.md");
141
212
  const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
142
- if (existing?.includes("godxjp-ui:start")) return "present";
213
+ if (existing?.includes("godxjp-ui:start")) {
214
+ // Present — but at WHICH version? Refresh only the delimited block; anything the consumer
215
+ // wrote around it is untouched.
216
+ if (stampedVersion(existing) === KIT_VERSION) return "present";
217
+ writeFileSync(
218
+ path,
219
+ refreshBlock(existing, CLAUDE_MD_BLOCK, "<!-- godxjp-ui:start", "<!-- godxjp-ui:end -->"),
220
+ );
221
+ return "refreshed";
222
+ }
143
223
  if (existing == null) {
144
224
  writeFileSync(path, CLAUDE_MD_BLOCK);
145
225
  return "created";
@@ -157,3 +237,85 @@ export function shouldSkip(root) {
157
237
  if (pkg.name === "@godxjp/ui" || pkg.name === "@godxjp/ui-mcp") return "self";
158
238
  return null;
159
239
  }
240
+
241
+ /**
242
+ * Keep an opted-in guinea-pig skill current.
243
+ *
244
+ * The file is co-authored: sections 0–7 come from this package, and section 8 onward is whatever
245
+ * the repo wrote about ITSELF — its package manager, its audit baseline, its deliberate
246
+ * exceptions. So the refresh replaces the head and keeps the tail, which is the only split that
247
+ * lets the guidance move without eating the consumer's own notes.
248
+ *
249
+ * Only runs where `.guinea-pig-optin` exists: the skill carries an obligation to fix things
250
+ * UPSTREAM, and pushing that into a repo that never asked for it would tell its agent to go edit
251
+ * a library it has no mandate over.
252
+ */
253
+ export function refreshGuineaPigSkill(root) {
254
+ const dir = join(root, ".claude", "skills", "godx-ui-guinea-pig");
255
+ const target = join(dir, "SKILL.md");
256
+ const optin = join(dir, ".guinea-pig-optin");
257
+ if (!existsSync(optin) || !existsSync(target)) return false;
258
+ if (readFileSync(optin, "utf8").trim() === KIT_VERSION) return false;
259
+
260
+ const base = readFileSync(join(SELF_ROOT, "scripts", "guinea-pig-skill.md"), "utf8");
261
+ const current = readFileSync(target, "utf8");
262
+ const marker = "\n---\n\n# 8. ";
263
+ const i = current.indexOf(marker);
264
+ writeFileSync(target, base.replace(/\s*$/, "") + "\n" + (i < 0 ? "" : current.slice(i)));
265
+ writeFileSync(optin, `${KIT_VERSION}\n`);
266
+ return true;
267
+ }
268
+
269
+ /**
270
+ * Install the common consumer rules as a PATH-TRIGGERED file, and wire them into `.ai/rules`.
271
+ *
272
+ * The skill and this file say overlapping things on purpose, because they fire at different
273
+ * moments: a skill loads when the TASK matches its description — once, at the start — while an
274
+ * `.ai/rules` entry loads every time an agent touches a file under its glob. Measured over one
275
+ * session: a dashboard file was edited dozens of times and the skill was never re-read, so the
276
+ * laws that mattered were out of context for every edit after the first.
277
+ *
278
+ * The glob is DETECTED, not assumed. A rule wired to a directory the repo does not have is a rule
279
+ * that never fires, which is worse than no rule at all — it looks installed.
280
+ *
281
+ * Unlike the skill, this file is owned OUTRIGHT by the package and is rewritten whole. That is the
282
+ * honest shape for `.ai/rules`, where the convention is one file per concern and the index loads
283
+ * them all: a repo with something of its own to say writes its own rule file instead of editing
284
+ * this one. The file says so at the top, because the first draft preserved nothing and silently
285
+ * ate a note left inside it — measured, and the reason for that banner.
286
+ */
287
+ export function ensureConsumerRules(root) {
288
+ const uiDir = ["resources/js", "app/javascript", "src/components", "src", "app"].find((d) =>
289
+ existsSync(join(root, d)),
290
+ );
291
+ if (!uiDir) return false;
292
+
293
+ const dir = join(root, ".ai", "rules");
294
+ const target = join(dir, "godxjp-ui.md");
295
+ const body = readFileSync(join(SELF_ROOT, "scripts", "consumer-rule.md"), "utf8");
296
+ const front = `---\npaths:\n - '${uiDir}/**'\n---\n\n`;
297
+ // The digest covers the FRONT MATTER too: the detected glob is part of what makes this file
298
+ // correct, and a repo that grows a `resources/js` after shipping with `src` needs the rewrite.
299
+ const managed = `${front}${body}`;
300
+ const next = `${OWNED_STAMP(managed)}\n${managed}`;
301
+
302
+ if (existsSync(target) && stampedDigest(readFileSync(target, "utf8")) === digestOf(managed)) {
303
+ return false;
304
+ }
305
+ mkdirSync(dir, { recursive: true });
306
+ writeFileSync(target, next);
307
+
308
+ // Only touch the index when the repo keeps one; a missing index means the repo reads rule files
309
+ // directly, and inventing one would change how it loads everything else.
310
+ const index = join(dir, "index.md");
311
+ if (existsSync(index)) {
312
+ const cur = readFileSync(index, "utf8");
313
+ if (!cur.includes(".ai/rules/godxjp-ui.md")) {
314
+ writeFileSync(
315
+ index,
316
+ cur.replace(/\s*$/, "") + `\n| ${uiDir}/** | .ai/rules/godxjp-ui.md |\n`,
317
+ );
318
+ }
319
+ }
320
+ return uiDir;
321
+ }
@@ -0,0 +1,98 @@
1
+ # @godxjp/ui
2
+
3
+ > **Tệp này do gói `@godxjp/ui` sở hữu và bị GHI ĐÈ mỗi lần nâng cấp.**
4
+ > Đừng sửa ở đây — luật của riêng kho thuộc về một tệp khác trong `.ai/rules/`,
5
+ > và index sẽ nạp cả hai. (Khác với `.claude/skills/.../SKILL.md`, nơi mục §8
6
+ > trở đi là của kho và được giữ lại.)
7
+
8
+ Đây là **danh sách kiểm** bắn mỗi lần chạm một tệp UI. Lý do đầy đủ nằm ở
9
+ `docs/CONSUMER-RULES.md` (10 luật) và, với kho chuột bạch, ở
10
+ `.claude/skills/godx-ui-guinea-pig/SKILL.md`.
11
+
12
+ ## Bố cục chuẩn của platform: BA CỘT, và ba cột là BA PHẠM VI
13
+
14
+ Vỏ mặc định của mọi app trên platform là ba cột, dựng bằng một `AppShell`:
15
+
16
+ ```
17
+ navRail (3.5rem) │ sidebar (16rem) │ content
18
+ ```
19
+
20
+ Không tự dựng ba cột bằng cách nhét hai cột vào một khe `sidebar` rồi nới
21
+ `--app-shell-sidebar-width`. Hai bẫy đã đo được: `Sidebar` render
22
+ `.sb-root { display: contents }` nên hai `Sidebar` đặt cạnh nhau **tan vào một
23
+ flex row** và cùng co về 0; và nới token dùng chung khiến mép nội dung **nhảy
24
+ 64px giữa các route**. `navRail` sở hữu track riêng nên không cần cả hai.
25
+
26
+ **Đặt một control vào cột nào là câu hỏi về PHẠM VI, không phải về chỗ trống:**
27
+
28
+ | Cột | Phạm vi | Chứa gì |
29
+ | --------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
30
+ | `navRail` | **platform** — đúng với cả tổ chức, sống sót qua việc đổi app | đổi tổ chức · đổi app · thông báo · tin nhắn · sự kiện · cài đặt tổ chức · lối tắt liên-app |
31
+ | `sidebar` | **app** — của riêng app đang mở | mục/kênh/route của chính app này |
32
+ | `topbar` | **trang** — bạn đang ở đâu, làm được gì ở đây | breadcrumb · hành động của trang · menu tài khoản |
33
+
34
+ Hai luật phủ định, và chúng làm được việc:
35
+
36
+ - Điều hướng của app **không bao giờ** vào rail. Một rail lặp lại mục của
37
+ sidebar là dải chrome thứ hai mang thứ hạng của dải thứ nhất, chỉ dựng đứng.
38
+ - Công tắc cấp platform **không bao giờ** vào sidebar — đổi app xong nó biến
39
+ mất, trong khi nó vẫn phải ở đó.
40
+ - Đích nào hợp cả hai thì thuộc **rail**: nó sống sót qua việc đổi app.
41
+
42
+ `sidebarCollapsed` chỉ gập cột `sidebar`; rail giữ nguyên bề rộng, nên đích cấp
43
+ platform vẫn với tới được lúc gập. Đừng dựng lại hành vi này bằng CSS của kho.
44
+
45
+ Bề rộng rail là token `--app-shell-nav-rail-width` — kho nào muốn rail rộng kiểu
46
+ Slack thì đặt lại **một dòng**, không fork `.app-nav-rail`.
47
+
48
+ ## Trước khi viết bố cục: TRA, đừng dựng
49
+
50
+ Hỏi MCP `godxjp-ui` (`search_components`, `get_component`). Đo được trong một
51
+ ngày: năm thứ cần đều ĐÃ CÓ và vẫn bị dựng lại bằng thứ khác —
52
+
53
+ | Cần | Đã có |
54
+ | ---------------------------------------- | --------------------- |
55
+ | đường kẻ chạm mép Card | `<CardContent flush>` |
56
+ | header có kẻ khi thân là danh sách flush | `<CardHeader banded>` |
57
+ | một hàng LÀ liên kết (thay cho nút rời) | `<ListRow asChild>` |
58
+ | kẻ ô từng ngày trong lịch | `<Calendar bordered>` |
59
+ | dải giữa hai vùng, tự kẻ theo VỊ TRÍ | `<CardBar>` |
60
+
61
+ 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
62
+ hỏi**.
63
+
64
+ ## Catalog chở PROP, không chở LUẬT BỐ CỤC
65
+
66
+ `CardBar` trong manifest có đúng một prop (`extra`) — không dòng nào nói nó tự
67
+ kẻ theo vị trí (đầu: kẻ dưới · cuối: kẻ trên · giữa: cả hai). Luật ấy chỉ nằm
68
+ trong chú thích `node_modules/@godxjp/ui/src/styles/card-layout.css`.
69
+
70
+ **Làm bố cục trong một component của DS → mở tệp `*-layout.css` của nó ra đọc.**
71
+
72
+ ## Card không lồng Card
73
+
74
+ Một `<Card>` trong `<Card>` cho hai mép bo cách nhau 16px và hai lớp padding
75
+ chồng lên. Cần viền cho thứ bên trong thì tìm trục của chính nó
76
+ (`Calendar bordered`), đừng bọc thêm một mặt phẳng nữa.
77
+
78
+ Cùng lý do: đừng xếp `<Alert>` thành danh sách trong Card — mỗi Alert là một mặt
79
+ phẳng, và `Alert` còn phát `role="alert"` nên cả danh sách sẽ tự đọc to lên khi
80
+ tải trang. Danh sách là `ListRow`.
81
+
82
+ ## Màu chữ đọc tầng CHỮ, không đọc tầng TÔ
83
+
84
+ `--success/--warning/--info/--destructive` là màu **TÔ** (nền badge, thanh, viền
85
+ alert). Chữ đọc `--text-success/-warning/-info/-error`.
86
+
87
+ Đo được: `Text tone="warning"` đọc nhầm tầng cho **1,74:1**; đúng tầng cho
88
+ **5,90:1**. Cùng `tone` ấy trong `Badge` vẫn đạt 5,52:1 — nên cùng một prop
89
+ hiện đọc được ở chỗ này và không đọc được ở chỗ kia, trên cùng một màn hình.
90
+
91
+ ## Audit xanh ≠ chạy đúng
92
+
93
+ `ui-audit` xanh chỉ nghĩa là **không có gì cấm** bạn. Đo được: một consumer viết
94
+ `modifiers` + `modifiersClassNames` cho màu cuối tuần — audit xanh, tsc xanh,
95
+ build xanh, và **số màu chữ trên cả lưới vẫn là 1**, vì class rơi vào `<td>` còn
96
+ `<button>` tự đặt màu.
97
+
98
+ Một API chết im lặng trông y hệt một API đang chạy. **Mở trang và đo** mới biết.