@godxjp/ui 20.1.0 → 20.2.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 (54) hide show
  1. package/dist/app/app-provider.js +37 -21
  2. package/dist/app/index.d.ts +2 -0
  3. package/dist/app/index.js +2 -0
  4. package/dist/app/storage.d.ts +17 -0
  5. package/dist/app/storage.js +28 -0
  6. package/dist/components/data-display/popover.d.ts +7 -1
  7. package/dist/components/data-display/popover.js +4 -0
  8. package/dist/components/data-display/service-launcher-card.js +4 -2
  9. package/dist/components/data-display/table.d.ts +60 -0
  10. package/dist/components/data-display/table.js +8 -0
  11. package/dist/components/data-entry/calendar.d.ts +1 -1
  12. package/dist/components/data-entry/calendar.js +11 -2
  13. package/dist/components/feedback/dialog.js +3 -0
  14. package/dist/components/feedback/sheet.js +3 -0
  15. package/dist/components/feedback/tooltip.js +3 -0
  16. package/dist/components/general/button.d.ts +1 -0
  17. package/dist/components/general/button.js +2 -0
  18. package/dist/components/layout/app-launcher.d.ts +1 -1
  19. package/dist/components/layout/app-launcher.js +43 -10
  20. package/dist/components/layout/app-shell.d.ts +1 -1
  21. package/dist/components/layout/app-shell.js +14 -7
  22. package/dist/components/layout/auth-shell.d.ts +1 -1
  23. package/dist/components/layout/auth-shell.js +2 -0
  24. package/dist/components/layout/sidebar.js +2 -1
  25. package/dist/components/navigation/dropdown-menu.js +5 -0
  26. package/dist/components/ui/hover-card.js +3 -0
  27. package/dist/i18n/index.d.ts +1 -1
  28. package/dist/i18n/index.js +3 -1
  29. package/dist/i18n/messages/en.json +3 -1
  30. package/dist/i18n/messages/ja.json +3 -1
  31. package/dist/i18n/messages/vi.json +3 -1
  32. package/dist/i18n/translate.d.ts +19 -0
  33. package/dist/i18n/translate.js +24 -0
  34. package/dist/lib/overlay-portal.d.ts +18 -0
  35. package/dist/lib/overlay-portal.js +17 -0
  36. package/dist/props/components/app.prop.d.ts +13 -2
  37. package/dist/props/components/general.prop.d.ts +13 -0
  38. package/dist/props/components/layout.prop.d.ts +95 -5
  39. package/dist/styles/card-layout.css +10 -1
  40. package/dist/styles/chart-layout.css +2 -1
  41. package/dist/styles/control.css +5 -0
  42. package/dist/styles/data-display-layout.css +7 -1
  43. package/dist/styles/navigation-layout.css +3 -1
  44. package/dist/styles/shell-layout.css +465 -20
  45. package/dist/tokens/components/control.css +2 -1
  46. package/dist/tokens/components/shell.css +50 -3
  47. package/dist/tokens/foundation.css +2 -0
  48. package/docs/data-display/list-row.tsx +6 -6
  49. package/docs/layout/app-launcher.tsx +44 -0
  50. package/docs/layout/app-shell-arrangements.tsx +5 -5
  51. package/docs/showcase/settings-security-mfa.tsx +3 -3
  52. package/package.json +7 -4
  53. package/scripts/_agent-setup.mjs +57 -7
  54. package/scripts/visual-audit.mjs +45 -7
@@ -45,7 +45,13 @@
45
45
  --app-shell-sidebar-width: 16rem;
46
46
  --app-shell-sidebar-collapsed-width: 4rem;
47
47
 
48
- --app-shell-nav-rail-width: 3.5rem;
48
+ --app-shell-nav-rail-width: 2.5rem;
49
+
50
+ --app-shell-nav-rail-item-size: var(--band-height-lg);
51
+
52
+ --app-shell-nav-rail-height: var(--app-shell-nav-rail-width);
53
+
54
+ --app-shell-nav-rail-inset: var(--space-3);
49
55
  --app-shell-page-max-width: 80rem;
50
56
 
51
57
  --app-shell-main-background: initial;
@@ -84,6 +90,7 @@
84
90
  --sidebar-section-label-padding-x: var(--space-2);
85
91
  --sidebar-section-label-padding-bottom: var(--space-1);
86
92
 
93
+ --topbar-search-width: 16rem;
87
94
  --topbar-search-max-width: none;
88
95
 
89
96
  --topbar-height: auto;
@@ -157,6 +164,41 @@
157
164
  --app-launcher-group-label-color: var(--muted-foreground);
158
165
  --app-launcher-state-min-height: 8rem;
159
166
 
167
+ --app-launcher-launchpad-backdrop-blur-size: 28px;
168
+ --app-launcher-launchpad-backdrop-alpha: 42%;
169
+ --app-launcher-launchpad-space-inset: var(--space-8);
170
+ --app-launcher-launchpad-panel-max-width: 60rem;
171
+ --app-launcher-launchpad-panel-gap: var(--space-8);
172
+ --app-launcher-launchpad-grid-gap: var(--space-6);
173
+ --app-launcher-launchpad-tile-inline-size: 7.5rem;
174
+ --app-launcher-launchpad-tile-padding: var(--space-3);
175
+ --app-launcher-launchpad-tile-radius: var(--radius-lg);
176
+ --app-launcher-launchpad-tile-gap: var(--space-3);
177
+ --app-launcher-launchpad-tile-hover-background: color-mix(
178
+ in srgb,
179
+ hsl(var(--foreground)) 10%,
180
+ transparent
181
+ );
182
+ --app-launcher-launchpad-mark-size: 4rem;
183
+ --app-launcher-launchpad-mark-radius: var(--radius-lg);
184
+
185
+ --app-launcher-launchpad-mark-background: color-mix(
186
+ in srgb,
187
+ hsl(var(--card)) 62%,
188
+ transparent
189
+ );
190
+ --app-launcher-launchpad-mark-border-color: color-mix(
191
+ in srgb,
192
+ hsl(var(--foreground)) 14%,
193
+ transparent
194
+ );
195
+ --app-launcher-launchpad-mark-font-size: var(--font-size-lg);
196
+ --app-launcher-launchpad-mark-shadow: var(--shadow-lg);
197
+ --app-launcher-launchpad-name-font-size: var(--font-size-sm);
198
+ --app-launcher-launchpad-title-font-size: var(--font-size-2xs);
199
+ --app-launcher-launchpad-title-letter-spacing: 0.08em;
200
+ --app-launcher-launchpad-close-space-padding: var(--space-2);
201
+
160
202
  --sidebar-item-active-color: initial;
161
203
  --sidebar-item-active-tint: initial;
162
204
 
@@ -221,8 +263,8 @@
221
263
 
222
264
  --auth-shell-registration-card-max-width: 22.5rem;
223
265
 
224
- --auth-shell-registration-main-padding-block-start: 9.5rem;
225
- --auth-shell-registration-main-padding-block-start-mobile: 8.875rem;
266
+ --auth-shell-registration-main-padding-block-start: 1rem;
267
+ --auth-shell-registration-main-padding-block-start-mobile: 0.9375rem;
226
268
  --auth-shell-registration-main-padding-inline: 1rem;
227
269
  --auth-shell-registration-main-padding-inline-mobile: 0.9375rem;
228
270
 
@@ -251,6 +293,8 @@
251
293
  --auth-footer-content-gap: 0.375rem;
252
294
 
253
295
  --auth-footer-text-font-size: var(--font-size-2xs);
296
+
297
+ --auth-footer-target-min-size: var(--touch-target-min);
254
298
  --auth-stack-gap: var(--space-3);
255
299
  --auth-shell-field-label-gap: var(--space-1);
256
300
 
@@ -293,6 +337,9 @@
293
337
  @media (pointer: coarse) {
294
338
  :root {
295
339
  --app-shell-bar-height: var(--band-height-3xl);
340
+
341
+ --app-shell-nav-rail-width: 3rem;
342
+ --app-shell-nav-rail-item-size: var(--band-height-xl);
296
343
  }
297
344
  }
298
345
 
@@ -186,6 +186,8 @@
186
186
 
187
187
  --scaling: 1;
188
188
 
189
+ --touch-target-min: 1.5rem;
190
+
189
191
  --space-0: 0;
190
192
  --space-1: calc(0.25rem * var(--scaling));
191
193
  --space-2: calc(0.5rem * var(--scaling));
@@ -57,13 +57,13 @@ export default function Demo() {
57
57
  </CardHeader>
58
58
  <CardContent flush>
59
59
  <ListRow
60
- leading={<Smartphone aria-hidden="true" className="size-4" />}
60
+ leading={<Smartphone aria-hidden="true" />}
61
61
  title="iPhone 15 · Tokyo"
62
62
  description="最終アクセス 2分前 · 153.240.x.x"
63
63
  trailing={<Badge status="active" />}
64
64
  />
65
65
  <ListRow
66
- leading={<Laptop aria-hidden="true" className="size-4" />}
66
+ leading={<Laptop aria-hidden="true" />}
67
67
  title="MacBook Pro · Osaka"
68
68
  description="最終アクセス 3日前 · 126.18.x.x"
69
69
  trailing={
@@ -73,7 +73,7 @@ export default function Demo() {
73
73
  }
74
74
  />
75
75
  <ListRow
76
- leading={<Laptop aria-hidden="true" className="size-4" />}
76
+ leading={<Laptop aria-hidden="true" />}
77
77
  title="Windows · Nagoya"
78
78
  description="最終アクセス 先月 · 49.98.x.x"
79
79
  trailing={
@@ -110,7 +110,7 @@ export default function Demo() {
110
110
  />
111
111
  <ListRow
112
112
  as="li"
113
- leading={<KeyRound aria-hidden="true" className="size-4" />}
113
+ leading={<KeyRound aria-hidden="true" />}
114
114
  title="Passkey · iCloud Keychain"
115
115
  description="2026年3月12日に追加"
116
116
  trailing={<Badge tone="neutral">未使用</Badge>}
@@ -362,14 +362,14 @@ export default function Demo() {
362
362
  <CardContent flush>
363
363
  <ListRow
364
364
  density="default"
365
- leading={<Laptop aria-hidden="true" className="size-4" />}
365
+ leading={<Laptop aria-hidden="true" />}
366
366
  title="MacBook Pro · Osaka"
367
367
  description={'density="default" · 最終アクセス 3日前'}
368
368
  trailing={<Badge status="active" />}
369
369
  />
370
370
  <ListRow
371
371
  density="compact"
372
- leading={<Laptop aria-hidden="true" className="size-4" />}
372
+ leading={<Laptop aria-hidden="true" />}
373
373
  title="MacBook Pro · Osaka"
374
374
  description={'density="compact" · 最終アクセス 3日前'}
375
375
  trailing={<Badge status="active" />}
@@ -145,6 +145,50 @@ export default function Demo() {
145
145
  </Flex>
146
146
  </CardContent>
147
147
  </Card>
148
+ <Card>
149
+ <CardHeader>
150
+ <CardTitle level={2}>バー以外の chrome — icon とパネルの向き</CardTitle>
151
+ <CardDescription>
152
+ `appearance=&quot;icon&quot;` は正方形の ghost Button。バーではない chrome(nav rail、
153
+ カードのヘッダ、ツールバー)のための箱で、glyph は同じです。 パネルは既定で chrome
154
+ から外へ開きます(bar は下+end、それ以外は inline-end+start)。 位置を変えられる
155
+ chrome だけが向きを知っているので、`side` / `align` で明示できます。
156
+ </CardDescription>
157
+ </CardHeader>
158
+ <CardContent>
159
+ <Flex direction="row" gap="md">
160
+ <AppLauncher apps={apps} groups={groups} labels={labels} appearance="icon" />
161
+ <AppLauncher
162
+ apps={apps}
163
+ groups={groups}
164
+ labels={labels}
165
+ appearance="icon"
166
+ side="bottom"
167
+ align="start"
168
+ />
169
+ </Flex>
170
+ </CardContent>
171
+ </Card>
172
+
173
+ <Card>
174
+ <CardHeader>
175
+ <CardTitle level={2}>Launchpad — 画面いっぱい</CardTitle>
176
+ <CardDescription>
177
+ `responsive=&quot;fullscreen&quot;` は plaform の start bar のための形。背後のページを
178
+ ぼかし、その地の上にタイルが並びます(macOS Launchpad / Windows Start)。 幅に関係なく
179
+ 1つの surface に固定。列数だけが surface に合わせて 3 · 4 · 5 · 6 と上がります。
180
+ </CardDescription>
181
+ </CardHeader>
182
+ <CardContent>
183
+ <AppLauncher
184
+ apps={apps}
185
+ groups={groups}
186
+ labels={labels}
187
+ appearance="icon"
188
+ responsive="fullscreen"
189
+ />
190
+ </CardContent>
191
+ </Card>
148
192
  </ResponsiveGrid>
149
193
  </PageContainer>
150
194
  );
@@ -1,5 +1,5 @@
1
1
  import { useState } from "react";
2
- import { AppShell, Flex, PageContainer, Sidebar, Topbar } from "@godxjp/ui/layout";
2
+ import { AppShell, Flex, PageContainer, Sidebar, Topbar, TopbarItem } from "@godxjp/ui/layout";
3
3
  import type { SidebarSectionProp } from "@godxjp/ui/layout";
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
5
5
  import { Button, Text } from "@godxjp/ui/general";
@@ -95,9 +95,9 @@ export default function Demo() {
95
95
  topbar={
96
96
  <Topbar
97
97
  start={
98
- <Button
99
- size="sm"
100
- variant="ghost"
98
+ // A bar cell is a TopbarItem, not a Button: a Button in a bar is a --control-height
99
+ // pill floating in a taller strip, with its own hover fill and its own focus ring.
100
+ <TopbarItem
101
101
  aria-label={collapsed ? "サイドバーを開く" : "サイドバーを閉じる"}
102
102
  aria-expanded={!collapsed}
103
103
  onClick={() => setCollapsed((c) => !c)}
@@ -107,7 +107,7 @@ export default function Demo() {
107
107
  ) : (
108
108
  <PanelLeftClose aria-hidden="true" />
109
109
  )}
110
- </Button>
110
+ </TopbarItem>
111
111
  }
112
112
  />
113
113
  }
@@ -78,7 +78,7 @@ export default function SecurityMfaShowcase() {
78
78
  </CardHeader>
79
79
  <CardContent flush>
80
80
  <ListRow
81
- leading={<Mail aria-hidden="true" className="size-4" />}
81
+ leading={<Mail aria-hidden="true" />}
82
82
  title="OTP qua email"
83
83
  description="Gửi mã tới t•••@famgia.com"
84
84
  trailing={
@@ -91,7 +91,7 @@ export default function SecurityMfaShowcase() {
91
91
  }
92
92
  />
93
93
  <ListRow
94
- leading={<Smartphone aria-hidden="true" className="size-4" />}
94
+ leading={<Smartphone aria-hidden="true" />}
95
95
  title="Ứng dụng xác thực"
96
96
  description="Chưa thiết lập"
97
97
  trailing={
@@ -101,7 +101,7 @@ export default function SecurityMfaShowcase() {
101
101
  }
102
102
  />
103
103
  <ListRow
104
- leading={<KeyRound aria-hidden="true" className="size-4" />}
104
+ leading={<KeyRound aria-hidden="true" />}
105
105
  title="Passkey"
106
106
  description="1 thiết bị · MacBook Pro"
107
107
  trailing={
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "20.1.0",
4
- "godxUiMcp": "20.1.0",
3
+ "version": "20.2.0",
4
+ "godxUiMcp": "20.2.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -357,8 +357,8 @@
357
357
  "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",
358
358
  "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",
359
359
  "verify:ci": "pnpm verify:ci:static && pnpm check:frame-contracts && pnpm test",
360
- "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",
361
- "verify:browser": "pnpm check:contrast && pnpm check:visual-audit",
360
+ "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 && pnpm check:catalog-contradictions",
361
+ "verify:browser": "pnpm check:contrast && pnpm check:text-ink-clip && pnpm check:visual-audit",
362
362
  "verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage && pnpm check:frame-axe",
363
363
  "verify:publish-tree": "pnpm build && pnpm check:packed-public-contract && pnpm check:use-client && pnpm check:dist-tokens-resolve",
364
364
  "check:release-plan": "node scripts/check-release-command-plan.mjs",
@@ -368,6 +368,7 @@
368
368
  "check:mcp-sync": "node scripts/check-mcp-sync.mjs",
369
369
  "check:mcp-catalog-coverage": "node scripts/check-mcp-catalog-coverage.mjs",
370
370
  "check:mcp-orphans": "node scripts/check-mcp-orphans.mjs",
371
+ "check:catalog-contradictions": "node scripts/check-catalog-contradictions.mjs",
371
372
  "check:mcp-prop-sync": "node scripts/check-mcp-prop-sync.mjs",
372
373
  "check:doc-prop-existence": "node scripts/check-doc-prop-existence.mjs",
373
374
  "gen:component-api-manifest": "node scripts/gen-component-api-manifest.mjs",
@@ -404,6 +405,7 @@
404
405
  "check:audit-sync": "node scripts/check-audit-sync.mjs",
405
406
  "check:gate-coverage": "node scripts/check-gate-coverage.mjs",
406
407
  "check:contrast": "node scripts/check-contrast.mjs",
408
+ "check:text-ink-clip": "node scripts/check-text-ink-clip.mjs",
407
409
  "visual-audit": "node scripts/visual-audit.mjs",
408
410
  "check:visual-audit": "node scripts/visual-audit-smoke.mjs",
409
411
  "check:frame-axe": "node scripts/check-frame-axe.mjs",
@@ -483,6 +485,7 @@
483
485
  "react-aria-components": "^1.21.1",
484
486
  "react-day-picker": "^10.0.1",
485
487
  "react-resizable-panels": "^4.12.3",
488
+ "react-stately": "3.50.0",
486
489
  "sonner": "^2.0.8",
487
490
  "tailwind-merge": "^3.5.0",
488
491
  "tailwindcss-animate": "^1.0.7",
@@ -74,8 +74,21 @@ or change a component, page, or form — no exceptions.
74
74
  \`rounded border\` div. Text is \`<Text tone size weight truncate>\`, never utility classes.
75
75
 
76
76
  ## Before writing UI
77
- 1. **MCP-first.** Consult the godx-ui MCP: \`get_component <Name>\`, \`search_components\`,
78
- \`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
77
+ 1. **DISCOVERY BEFORE LOOKUP in this order, every time.**
78
+ 1. \`search_components "<the area you are about to build>"\` FIRST. This is the only call that
79
+ answers "what already exists here". Ask it before the first line of JSX, not after.
80
+ 2. \`get_component <Name>\` second, once you know the name.
81
+ \`get_component\` answers "how do I use X" and is USELESS for finding X — you can only look up a
82
+ name you already have. Skipping step 1 is how a consumer hand-rolled an organization switcher
83
+ the package ships (\`OrgSwitcher\`) and built three topbar cells out of Buttons instead of
84
+ \`TopbarItem\`: both were one \`search_components\` call away, and neither was ever searched for.
85
+ **Existing code in this repo is NOT the authority.** Reading a neighbouring file and copying it
86
+ is the most common way this rule gets skipped — measured: of four fresh agents given a vague UI
87
+ task, all four answered correctly but two never called the MCP at all, saying the answer was
88
+ already in the code. They were only right because that code had just been fixed; a week earlier
89
+ the same files hand-rolled a switcher and put Buttons in topbar slots, and both agents would
90
+ have copied it. Search the catalog even when a local file looks like it already answers you.
91
+ Also \`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
79
92
  primitive already does.
80
93
  2. **Real primitives only.** No raw \`<input>/<select>/<button>/<textarea>/<table>\`, no
81
94
  styled-div fakes. A labelled control ALWAYS goes in \`<FormField label=…>\` (it owns the
@@ -105,8 +118,21 @@ This app uses @godxjp/ui. EVERY time you build or change UI:
105
118
  \`PageContainer\`; siblings go in \`<Flex direction="col" gap>\` / \`<ResponsiveGrid>\`; no
106
119
  \`flex\`/\`grid\`/\`gap-*\`/\`p-*\`/\`m-*\` on your own elements; boxes are \`Card\`/\`Badge\`/
107
120
  \`ListRow\`/\`Descriptions\`, never a \`rounded border\` div; text is \`<Text tone size weight>\`.
108
- 1. **MCP-first.** Consult the \`godx-ui\` MCP \`get_component\`, \`search_components\`,
109
- \`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
121
+ 1. **DISCOVERY BEFORE LOOKUP in this order, every time.**
122
+ 1. \`search_components "<the area you are about to build>"\` FIRST. This is the only call that
123
+ answers "what already exists here". Ask it before the first line of JSX, not after.
124
+ 2. \`get_component <Name>\` second, once you know the name.
125
+ \`get_component\` answers "how do I use X" and is USELESS for finding X — you can only look up a
126
+ name you already have. Skipping step 1 is how a consumer hand-rolled an organization switcher
127
+ the package ships (\`OrgSwitcher\`) and built three topbar cells out of Buttons instead of
128
+ \`TopbarItem\`: both were one \`search_components\` call away, and neither was ever searched for.
129
+ **Existing code in this repo is NOT the authority.** Reading a neighbouring file and copying it
130
+ is the most common way this rule gets skipped — measured: of four fresh agents given a vague UI
131
+ task, all four answered correctly but two never called the MCP at all, saying the answer was
132
+ already in the code. They were only right because that code had just been fixed; a week earlier
133
+ the same files hand-rolled a switcher and put Buttons in topbar slots, and both agents would
134
+ have copied it. Search the catalog even when a local file looks like it already answers you.
135
+ Also \`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
110
136
  primitive already does.
111
137
  2. **Real primitives only.** No raw \`<input>/<select>/<button>/<textarea>/<table>\`. A labelled
112
138
  control ALWAYS goes in \`<FormField label=…>\` — never a bare \`<Label>\`+\`<Input>\`. Semantic
@@ -192,10 +218,13 @@ export function writeWorkflowMd(root) {
192
218
  const path = join(root, ".claude", "godxjp-ui-workflow.md");
193
219
  mkdirSync(dirname(path), { recursive: true });
194
220
  // 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.
221
+ // it, but nothing else reads from it, so a stale copy is pure loss. It is package-owned end to
222
+ // end, so the comparison is the BODY: a version check froze every consumer whose stamp already
223
+ // matched, which meant guidance edited between releases reached nobody while still reading as
224
+ // current. Same defect as ensureClaudeMd carried, same fix.
196
225
  if (existsSync(path)) {
197
226
  const cur = readFileSync(path, "utf8");
198
- if (stampedVersion(cur) === KIT_VERSION) return false;
227
+ if (cur.trim() === WORKFLOW_MD.trim()) return false;
199
228
  writeFileSync(path, WORKFLOW_MD);
200
229
  return "refreshed";
201
230
  }
@@ -207,13 +236,34 @@ export function writeWorkflowMd(root) {
207
236
  * Append the godxjp-ui mandate to the consumer's CLAUDE.md (created if absent). Idempotent via the
208
237
  * markers.
209
238
  */
239
+
240
+ /**
241
+ * Is the managed region in `existing` already byte-identical to what we would write?
242
+ *
243
+ * Digest first (cheap, and survives whitespace-only rewrites of the surrounding file). When the
244
+ * stamp predates digest stamping there is nothing to compare, so fall back to the region body
245
+ * itself rather than assuming current — assuming current is the bug this replaced.
246
+ */
247
+ function blockIsCurrent(existing, block) {
248
+ const wanted = stampedDigest(block);
249
+ const have = stampedDigest(existing);
250
+ if (wanted && have) return wanted === have;
251
+ const region = (text) => text.slice(text.indexOf("<!-- godxjp-ui:start"), text.indexOf("<!-- godxjp-ui:end -->"));
252
+ return region(existing).trim() === region(block).trim();
253
+ }
254
+
210
255
  export function ensureClaudeMd(root) {
211
256
  const path = join(root, "CLAUDE.md");
212
257
  const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
213
258
  if (existing?.includes("godxjp-ui:start")) {
214
259
  // Present — but at WHICH version? Refresh only the delimited block; anything the consumer
215
260
  // wrote around it is untouched.
216
- if (stampedVersion(existing) === KIT_VERSION) return "present";
261
+ // Compare the DIGEST, not the version — which is what the note above already promised and
262
+ // what this line did not do. Guidance is edited far more often than the package version moves,
263
+ // so a version check freezes every consumer whose stamp already matches: a rewritten mandate
264
+ // reached nobody, and the block still read as current. A file written before digest stamping
265
+ // has no digest at all, so fall back to comparing the rendered body.
266
+ if (blockIsCurrent(existing, CLAUDE_MD_BLOCK)) return "present";
217
267
  writeFileSync(
218
268
  path,
219
269
  refreshBlock(existing, CLAUDE_MD_BLOCK, "<!-- godxjp-ui:start", "<!-- godxjp-ui:end -->"),
@@ -109,16 +109,40 @@ function collectInPage() {
109
109
  accents.push({ rgb: { r: c.r, g: c.g, b: c.b }, tag: el.tagName.toLowerCase() });
110
110
  }
111
111
 
112
- // Interactive targets — for the 24×24 minimum.
113
- const targets = [];
112
+ // Interactive targets — for the 24x24 minimum of WCAG 2.2 SC 2.5.8, INCLUDING the two exceptions
113
+ // the criterion itself grants. Reporting the bare 24x24 rule flagged a 16px checkbox standing
114
+ // alone in white space and every legal link inside a consent sentence — none of which the
115
+ // criterion requires anyone to change. A checker that over-reports is not stricter, it is wrong,
116
+ // and it costs the same as under-reporting: readers learn to skip the finding.
117
+ const rawTargets = [];
114
118
  for (const el of document.querySelectorAll(
115
119
  "a[href], button, [role=button], input:not([type=hidden]), select, [tabindex]:not([tabindex='-1'])",
116
120
  )) {
117
121
  if (!visible(el)) continue;
118
122
  const r = el.getBoundingClientRect();
119
123
  const name = (el.getAttribute("aria-label") || el.textContent || "").trim().slice(0, 40);
120
- targets.push({ width: Math.round(r.width), height: Math.round(r.height), name });
124
+ rawTargets.push({
125
+ width: Math.round(r.width),
126
+ height: Math.round(r.height),
127
+ cx: r.left + r.width / 2,
128
+ cy: r.top + r.height / 2,
129
+ // INLINE exception: "the target is in a sentence or its size is otherwise constrained by the
130
+ // line-height of non-target text". A box participating in a text line is exactly that; an
131
+ // inline-block/flex control is not, so the test is the computed display, not the tag.
132
+ inline: getComputedStyle(el).display === "inline",
133
+ name,
134
+ });
121
135
  }
136
+ const targets = rawTargets.map((t) => {
137
+ if (t.inline) return { ...t, exempt: "inline" };
138
+ // SPACING exception: undersized is allowed when a 24px-DIAMETER circle centred on the target
139
+ // does not intersect the circle of any other target. Two circles of radius 12 intersect when
140
+ // their centres are closer than 24px, so that is the whole test.
141
+ const crowded = rawTargets.some(
142
+ (o) => o !== t && Math.hypot(o.cx - t.cx, o.cy - t.cy) < 24,
143
+ );
144
+ return { ...t, exempt: crowded ? null : "spacing" };
145
+ });
122
146
 
123
147
  // Emoji that survived to the rendered DOM.
124
148
  const text = document.body ? document.body.innerText : "";
@@ -165,7 +189,11 @@ function collectInPage() {
165
189
  const probe = (layer, className, prop, bad) => {
166
190
  const el = document.createElement("div");
167
191
  el.className = className;
168
- el.style.position = "absolute";
192
+ // No inline `position` here. It used to be set to "absolute" to keep the node out of flow, and
193
+ // that silently POISONED the one probe that read `position`: the dialog check asked whether
194
+ // position !== "static" on an element the probe itself had just made absolute, so it passed
195
+ // whether or not the layer was loaded. An empty hidden div contributes no visible layout, so
196
+ // the flow guard was never worth a probe that cannot fail.
169
197
  el.style.visibility = "hidden";
170
198
  document.body.appendChild(el);
171
199
  const v = getComputedStyle(el)[prop];
@@ -175,9 +203,18 @@ function collectInPage() {
175
203
  const layers = [
176
204
  probe("control", "ui-button", "borderRadius", "0px"),
177
205
  probe("navigation-layout", "ui-dropdown-menu-content", "borderTopWidth", "0px"),
178
- probe("card-layout", "ui-card", "borderTopWidth", "0px"),
206
+ // `.ui-card` is NOT a class the library sets rules on — Card renders `cn("group/card", )`
207
+ // and card-layout.css owns `.ui-card-bar` / `.ui-card-cover` / `.ui-card-inset*` /
208
+ // `.ui-card-header--banded`. Probing it could never pass, so every consumer page reported a
209
+ // permanent, meaningless `card-layout is not loaded` error. `.ui-card-inset-x` is a real rule
210
+ // AND resolves --card-space-inset, so it proves the layer and its token chain together.
211
+ probe("card-layout", "ui-card-inset-x", "paddingInline", "0px"),
179
212
  probe("layout", "ui-page-container", "display", "block"),
180
- probe("dialog-layout", "ui-dialog-content", "position", "static"),
213
+ // `.ui-dialog-content` is not a class either — dialog-layout styles the `[data-slot=
214
+ // "dialog-content"]` attribute — so this probe named a class nothing declares AND read the
215
+ // property the probe used to set on itself. `.ui-dialog-overlay` is a real selector and
216
+ // z-index defaults to "auto", which nothing here writes.
217
+ probe("dialog-layout", "ui-dialog-overlay", "zIndex", "auto"),
181
218
  probe("form-layout", "ui-form-field", "display", "inline"),
182
219
  ];
183
220
  // Control rows — every control in a row shares the control tier height.
@@ -319,7 +356,8 @@ async function audit(targets, { chromium, AxeBuilder }) {
319
356
  );
320
357
  }
321
358
  for (const t of m.targets) {
322
- if (isUndersizedTarget(t))
359
+ // `exempt` is set by the SC 2.5.8 Inline / Spacing carve-outs measured in the page.
360
+ if (!t.exempt && isUndersizedTarget(t))
323
361
  findings.push(
324
362
  buildFinding(
325
363
  "target-size-min",