@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.
- package/dist/app/app-provider.js +37 -21
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +2 -0
- package/dist/app/storage.d.ts +17 -0
- package/dist/app/storage.js +28 -0
- package/dist/components/data-display/popover.d.ts +7 -1
- package/dist/components/data-display/popover.js +4 -0
- package/dist/components/data-display/service-launcher-card.js +4 -2
- package/dist/components/data-display/table.d.ts +60 -0
- package/dist/components/data-display/table.js +8 -0
- package/dist/components/data-entry/calendar.d.ts +1 -1
- package/dist/components/data-entry/calendar.js +11 -2
- package/dist/components/feedback/dialog.js +3 -0
- package/dist/components/feedback/sheet.js +3 -0
- package/dist/components/feedback/tooltip.js +3 -0
- package/dist/components/general/button.d.ts +1 -0
- package/dist/components/general/button.js +2 -0
- package/dist/components/layout/app-launcher.d.ts +1 -1
- package/dist/components/layout/app-launcher.js +43 -10
- package/dist/components/layout/app-shell.d.ts +1 -1
- package/dist/components/layout/app-shell.js +14 -7
- package/dist/components/layout/auth-shell.d.ts +1 -1
- package/dist/components/layout/auth-shell.js +2 -0
- package/dist/components/layout/sidebar.js +2 -1
- package/dist/components/navigation/dropdown-menu.js +5 -0
- package/dist/components/ui/hover-card.js +3 -0
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +3 -1
- package/dist/i18n/messages/en.json +3 -1
- package/dist/i18n/messages/ja.json +3 -1
- package/dist/i18n/messages/vi.json +3 -1
- package/dist/i18n/translate.d.ts +19 -0
- package/dist/i18n/translate.js +24 -0
- package/dist/lib/overlay-portal.d.ts +18 -0
- package/dist/lib/overlay-portal.js +17 -0
- package/dist/props/components/app.prop.d.ts +13 -2
- package/dist/props/components/general.prop.d.ts +13 -0
- package/dist/props/components/layout.prop.d.ts +95 -5
- package/dist/styles/card-layout.css +10 -1
- package/dist/styles/chart-layout.css +2 -1
- package/dist/styles/control.css +5 -0
- package/dist/styles/data-display-layout.css +7 -1
- package/dist/styles/navigation-layout.css +3 -1
- package/dist/styles/shell-layout.css +465 -20
- package/dist/tokens/components/control.css +2 -1
- package/dist/tokens/components/shell.css +50 -3
- package/dist/tokens/foundation.css +2 -0
- package/docs/data-display/list-row.tsx +6 -6
- package/docs/layout/app-launcher.tsx +44 -0
- package/docs/layout/app-shell-arrangements.tsx +5 -5
- package/docs/showcase/settings-security-mfa.tsx +3 -3
- package/package.json +7 -4
- package/scripts/_agent-setup.mjs +57 -7
- 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:
|
|
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:
|
|
225
|
-
--auth-shell-registration-main-padding-block-start-mobile:
|
|
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
|
|
|
@@ -57,13 +57,13 @@ export default function Demo() {
|
|
|
57
57
|
</CardHeader>
|
|
58
58
|
<CardContent flush>
|
|
59
59
|
<ListRow
|
|
60
|
-
leading={<Smartphone aria-hidden="true"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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="icon"` は正方形の 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="fullscreen"` は 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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
</
|
|
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"
|
|
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"
|
|
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"
|
|
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.
|
|
4
|
-
"godxUiMcp": "20.
|
|
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",
|
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -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. **
|
|
78
|
-
\`
|
|
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. **
|
|
109
|
-
\`
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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 -->"),
|
package/scripts/visual-audit.mjs
CHANGED
|
@@ -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
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|