@godxjp/ui 22.0.0 → 23.1.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/README.md +4 -4
- package/dist/app/date-format-labels.d.ts +14 -2
- package/dist/app/date-format-labels.js +1 -1
- package/dist/app/date-formats.d.ts +14 -2
- package/dist/app/date-formats.js +9 -2
- package/dist/components/data-display/data-table.d.ts +8 -1
- package/dist/components/data-display/data-table.js +9 -2
- package/dist/components/data-display/scroll-area.d.ts +24 -4
- package/dist/components/data-display/scroll-area.js +34 -72
- package/dist/components/data-entry/cascader.js +8 -6
- package/dist/components/data-entry/checkbox.js +3 -1
- package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
- package/dist/components/data-entry/choice-hit-target.js +12 -0
- package/dist/components/data-entry/control-surface.d.ts +8 -0
- package/dist/components/data-entry/control-surface.js +10 -1
- package/dist/components/data-entry/form-field.js +5 -4
- package/dist/components/data-entry/input.js +1 -1
- package/dist/components/data-entry/number-input.js +27 -6
- package/dist/components/data-entry/radio.js +4 -3
- package/dist/components/data-entry/search-select.js +225 -95
- package/dist/components/data-entry/select.d.ts +76 -14
- package/dist/components/data-entry/select.js +495 -163
- package/dist/components/data-entry/slider.d.ts +3 -11
- package/dist/components/data-entry/slider.js +582 -93
- package/dist/components/data-entry/switch.js +3 -1
- package/dist/components/feedback/dialog.js +2 -2
- package/dist/components/feedback/sheet.js +2 -22
- package/dist/components/navigation/dropdown-menu.d.ts +11 -3
- package/dist/components/navigation/dropdown-menu.js +190 -15
- package/dist/components/navigation/filter-bar.js +2 -1
- package/dist/components/navigation/index.d.ts +0 -3
- package/dist/components/navigation/index.js +0 -78
- package/dist/components/navigation/tabs.js +16 -4
- package/dist/components/ui/index.d.ts +0 -3
- package/dist/components/ui/index.js +0 -3
- package/dist/components/ui/input-otp.d.ts +1 -1
- package/dist/components/ui/segmented.js +3 -1
- package/dist/components/ui/tag-input.js +3 -5
- package/dist/i18n/messages/en.json +5 -1
- package/dist/i18n/messages/ja.json +5 -1
- package/dist/i18n/messages/vi.json +5 -1
- package/dist/lib/breakpoint-token.d.ts +8 -0
- package/dist/lib/breakpoint-token.js +29 -0
- package/dist/lib/select-options.d.ts +21 -0
- package/dist/lib/select-options.js +46 -0
- package/dist/props/components/data-display.prop.d.ts +17 -7
- package/dist/props/components/data-entry.prop.d.ts +323 -30
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/navigation.prop.d.ts +25 -1
- package/dist/props/registry.d.ts +69 -1
- package/dist/props/registry.js +75 -0
- package/dist/props/vocabulary/data.prop.d.ts +5 -0
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/styles/control.css +246 -28
- package/dist/styles/data-display-layout.css +18 -10
- package/dist/styles/focus-ring.css +28 -0
- package/dist/styles/fonts.css +88 -4
- package/dist/styles/form-layout.css +4 -0
- package/dist/styles/layout.css +2 -1
- package/dist/styles/navigation-layout.css +24 -238
- package/dist/styles/shell-layout.css +148 -139
- package/dist/styles/table-layout.css +12 -5
- package/dist/tokens/components/control.css +12 -3
- package/dist/tokens/components/data-display.css +0 -5
- package/dist/tokens/components/navigation.css +5 -5
- package/dist/tokens/components/scroll-area.css +7 -0
- package/docs/CONSUMER-RULES.md +7 -2
- package/docs/CUSTOMER-THEMING.md +25 -9
- package/docs/DATETIME.md +11 -2
- package/docs/FRAME-COVERAGE-REPORT.md +4 -7
- package/docs/SPACING.md +35 -5
- package/docs/TOKENS.md +1 -1
- package/docs/data-display/data-table/index.tsx +44 -3
- package/docs/data-display/popover.tsx +4 -6
- package/docs/data-display/scroll-area.tsx +51 -45
- package/docs/data-display/table.tsx +1 -1
- package/docs/data-entry/date-picker.tsx +2 -3
- package/docs/data-entry/input-otp.tsx +5 -2
- package/docs/data-entry/segmented-in-filter-row.tsx +77 -0
- package/docs/data-entry/segmented.tsx +40 -1
- package/docs/data-entry/select-matrix.tsx +1 -2
- package/docs/data-entry/select.tsx +108 -3
- package/docs/data-entry/slider.tsx +125 -47
- package/docs/feedback/dialog.tsx +1 -2
- package/docs/feedback/sheet.tsx +1 -2
- package/docs/layout/app-shell-states.tsx +143 -0
- package/docs/navigation/dropdown-menu.tsx +77 -6
- package/docs/navigation/tabs.tsx +72 -0
- package/docs/roadmap/ai-chat-components.md +1 -2
- package/docs/roadmap/parity-audit-data-entry.md +44 -33
- package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
- package/docs/roadmap/parity-backlog.md +2 -3
- package/docs/roadmap/tree-components.md +0 -1
- package/package.json +25 -55
- package/scripts/_agent-setup.mjs +18 -0
- package/scripts/guinea-pig-skill.md +12 -6
- package/scripts/ui-audit.mjs +185 -7
- package/scripts/visual-audit-rules.mjs +0 -7
- package/scripts/visual-audit.mjs +6 -27
- package/dist/components/navigation/context-menu.d.ts +0 -21
- package/dist/components/navigation/context-menu.js +0 -149
- package/dist/components/navigation/menubar.d.ts +0 -28
- package/dist/components/navigation/menubar.js +0 -136
- package/dist/components/navigation/navigation-menu.d.ts +0 -10
- package/dist/components/navigation/navigation-menu.js +0 -91
- package/dist/components/ui/context-menu.d.ts +0 -1
- package/dist/components/ui/context-menu.js +0 -2
- package/dist/components/ui/menubar.d.ts +0 -1
- package/dist/components/ui/menubar.js +0 -2
- package/dist/components/ui/navigation-menu.d.ts +0 -1
- package/dist/components/ui/navigation-menu.js +0 -2
- package/docs/FRAME-A11Y-CI.md +0 -349
- package/docs/navigation/context-menu.tsx +0 -128
- package/docs/navigation/menubar.tsx +0 -141
- package/docs/navigation/navigation-menu.tsx +0 -158
|
@@ -31,7 +31,7 @@ compared against, or successive waves will silently measure against different ta
|
|
|
31
31
|
|
|
32
32
|
## P1 — grouped by owning file, so one agent owns one group
|
|
33
33
|
|
|
34
|
-
**`data-entry`** — `
|
|
34
|
+
**`data-entry`** — `SearchInput` Enter does
|
|
35
35
|
not commit (no `onKeyDown` in the file; `:59-63`) · `ColorPicker` has no `size`/`status`/`variant`
|
|
36
36
|
and no `presets` (`data-entry.prop.ts:893-908`) · `PasswordStrength` uses `role="img"` instead of
|
|
37
37
|
`role="meter"` (`:74-76`) · `Upload showUploadList` boolean-only (`:1196`) ·
|
|
@@ -56,8 +56,7 @@ derives delta tone from a regex on a stringified node (`card.tsx:233-246`) · `Q
|
|
|
56
56
|
`isDismissable`) · `Pagination` is controlled-only, breaking the full-triad rule · `AppShell` has no
|
|
57
57
|
`onSidebarCollapsedChange` and no built-in trigger (`app-shell.tsx:30`) · shell collapse breakpoint
|
|
58
58
|
hardcoded (`shell-layout.css:1119`) while the overlay family's equivalent is a token · `Sidebar`
|
|
59
|
-
submenu open state is unreachable local state (`sidebar.tsx:271`) · `
|
|
60
|
-
affordance (WCAG 2.4.3 / 1.4.10) · no `copyable` anywhere despite `ErrorSurface` rendering a
|
|
59
|
+
submenu open state is unreachable local state (`sidebar.tsx:271`) · no `copyable` anywhere despite `ErrorSurface` rendering a
|
|
61
60
|
`requestId` "so it can be read out or copied accurately" (`layout.prop.ts:787`) · clamped text
|
|
62
61
|
cannot expand.
|
|
63
62
|
|
|
@@ -100,7 +100,6 @@ Plus the house rules: forward `ref`, spread `...props`, accept `className` + `id
|
|
|
100
100
|
(`margin-inline-start`, `padding-inline-start`), never `ml-`/`pl-`/`left-`.
|
|
101
101
|
- Tri-state parent checkbox: checked / unchecked / **indeterminate** when only some descendants
|
|
102
102
|
are checked (skip propagation entirely when `checkStrictly`).
|
|
103
|
-
- Ships `tree.a11y.test.tsx` with `expectNoA11yViolations` — 0 violations.
|
|
104
103
|
|
|
105
104
|
## 5. Tokens (tier: `src/tokens/components/tree.css`, `@import`ed from `src/tokens/base.css`)
|
|
106
105
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "
|
|
4
|
-
"godxUiMcp": "
|
|
3
|
+
"version": "23.1.0",
|
|
4
|
+
"godxUiMcp": "23.1.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -245,18 +245,6 @@
|
|
|
245
245
|
"types": "./dist/components/data-entry/command.d.ts",
|
|
246
246
|
"import": "./dist/components/data-entry/command.js"
|
|
247
247
|
},
|
|
248
|
-
"./ui/context-menu": {
|
|
249
|
-
"types": "./dist/components/navigation/context-menu.d.ts",
|
|
250
|
-
"import": "./dist/components/navigation/context-menu.js"
|
|
251
|
-
},
|
|
252
|
-
"./ui/menubar": {
|
|
253
|
-
"types": "./dist/components/navigation/menubar.d.ts",
|
|
254
|
-
"import": "./dist/components/navigation/menubar.js"
|
|
255
|
-
},
|
|
256
|
-
"./ui/navigation-menu": {
|
|
257
|
-
"types": "./dist/components/navigation/navigation-menu.d.ts",
|
|
258
|
-
"import": "./dist/components/navigation/navigation-menu.js"
|
|
259
|
-
},
|
|
260
248
|
"./ui/resizable": {
|
|
261
249
|
"types": "./dist/components/layout/resizable.d.ts",
|
|
262
250
|
"import": "./dist/components/layout/resizable.js"
|
|
@@ -338,27 +326,19 @@
|
|
|
338
326
|
"check:screen-reader-evidence": "node scripts/check-screen-reader-evidence.mjs",
|
|
339
327
|
"check:voiceover-capture": "node scripts/check-voiceover-capture.mjs",
|
|
340
328
|
"capture:voiceover": "node scripts/capture-voiceover-evidence.mjs",
|
|
341
|
-
"check:data-entry-frame-runtime": "node scripts/check-data-entry-frame-runtime.mjs",
|
|
342
329
|
"check:data-entry-owner-source": "node scripts/check-data-entry-owner-source.mjs",
|
|
343
330
|
"check:data-entry-touch-aria": "node scripts/check-data-entry-touch-aria.mjs",
|
|
344
|
-
"check:layout-nav-frames": "node scripts/check-layout-nav-frames.mjs",
|
|
345
|
-
"check:provider-feedback-query-runtime": "for frames in navigation-app-setting-picker,feedback-dialog,feedback-sheet,feedback-alert-dialog feedback-tooltip,feedback-toast,feedback-skeleton,query-data-state query-infinite-query-state,query-mutation-feedback,query-prefetch-link; do FRAMES=$frames node scripts/check-provider-feedback-query-runtime.mjs || exit 1; done",
|
|
346
|
-
"check:layout-nav-closure": "for frames in layout-flex,layout-app-shell,layout-aspect-ratio,layout-auth-shell layout-page-container,layout-resizable-panel,layout-separator,layout-sidebar layout-split-pane,layout-topbar,navigation-breadcrumb,navigation-app-setting-picker navigation-context-menu,navigation-dropdown-menu,navigation-menubar,navigation-navigation-menu navigation-steps,navigation-toolbar,navigation-tabs,navigation-pagination; do FRAMES=$frames node scripts/check-layout-nav-closure.mjs || exit 1; done",
|
|
347
331
|
"check:display-runtime-evidence": "node scripts/check-display-runtime-evidence.mjs",
|
|
348
|
-
"check:
|
|
349
|
-
"check:
|
|
350
|
-
"check:button-icon-xs": "node scripts/check-button-icon-xs.mjs",
|
|
351
|
-
"check:timeline-title-fill": "node scripts/check-timeline-title-fill.mjs",
|
|
352
|
-
"check:descriptions-label-value-step": "node scripts/check-descriptions-label-value-step.mjs",
|
|
332
|
+
"check:choice-hit-target": "node scripts/check-choice-hit-target.mjs",
|
|
333
|
+
"check:app-shell-narrow-grid": "node scripts/check-app-shell-narrow-grid.mjs",
|
|
353
334
|
"check:frame-contracts": "pnpm check:component-api-manifest && pnpm audit:component-cases && pnpm check:frame-coverage && pnpm check:frame-coverage-ledger && pnpm check:screen-reader-evidence && pnpm check:data-entry-owner-source && pnpm check:display-runtime-evidence",
|
|
354
|
-
"check:
|
|
355
|
-
"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:tabs-overflow-menu && pnpm check:button-icon-xs && pnpm check:timeline-title-fill && pnpm check:descriptions-label-value-step",
|
|
335
|
+
"check:frame-runtime": "pnpm check:data-entry-touch-aria && pnpm check:app-shell-narrow-grid && pnpm check:choice-hit-target",
|
|
356
336
|
"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:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && 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",
|
|
357
337
|
"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:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && 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",
|
|
358
338
|
"verify:ci": "pnpm verify:ci:static && pnpm check:frame-contracts && pnpm test",
|
|
359
|
-
"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:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && 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 && pnpm check:catalog-snippets",
|
|
360
|
-
"verify:browser": "pnpm check:contrast && pnpm check:text-ink-clip && pnpm check:visual-audit",
|
|
361
|
-
"verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage
|
|
339
|
+
"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:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && 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-token-sync && pnpm check:email-token-sync && 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 && pnpm check:catalog-snippets",
|
|
340
|
+
"verify:browser": "pnpm check:contrast && pnpm check:text-ink-clip && pnpm check:font-fallback-metrics && pnpm check:visual-audit",
|
|
341
|
+
"verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage",
|
|
362
342
|
"verify:publish-tree": "pnpm build && pnpm check:packed-public-contract && pnpm check:use-client && pnpm check:dist-tokens-resolve",
|
|
363
343
|
"check:release-plan": "node scripts/check-release-command-plan.mjs",
|
|
364
344
|
"check:packed-public-contract": "node scripts/check-packed-public-contract.mjs",
|
|
@@ -407,20 +387,24 @@
|
|
|
407
387
|
"check:gate-coverage": "node scripts/check-gate-coverage.mjs",
|
|
408
388
|
"check:contrast": "node scripts/check-contrast.mjs",
|
|
409
389
|
"check:text-ink-clip": "node scripts/check-text-ink-clip.mjs",
|
|
390
|
+
"check:font-fallback-metrics": "node scripts/check-font-fallback-metrics.mjs",
|
|
410
391
|
"visual-audit": "node scripts/visual-audit.mjs",
|
|
411
392
|
"check:visual-audit": "node scripts/visual-audit-smoke.mjs",
|
|
412
|
-
"check:frame-axe": "node scripts/check-frame-axe.mjs",
|
|
413
393
|
"check:frame-geometry": "node scripts/frame-geometry.mjs",
|
|
414
394
|
"postinstall": "node scripts/postinstall.mjs",
|
|
415
395
|
"init-agent": "node scripts/init-agent-kit.mjs",
|
|
416
396
|
"check:use-client": "node scripts/check-use-client.mjs",
|
|
417
|
-
"check:catalog-snippets": "node scripts/audit-catalog-snippets.mjs"
|
|
397
|
+
"check:catalog-snippets": "node scripts/audit-catalog-snippets.mjs",
|
|
398
|
+
"check:data-entry-frame-runtime": "node scripts/check-data-entry-frame-runtime.mjs",
|
|
399
|
+
"check:data-table-empty-inset": "node scripts/check-data-table-empty-inset.mjs",
|
|
400
|
+
"check:number-input-step-target": "node scripts/check-number-input-step-target.mjs",
|
|
401
|
+
"check:mcp-token-sync": "node scripts/gen-component-tokens.mjs --check",
|
|
402
|
+
"check:email-token-sync": "node scripts/gen-email-tokens.mjs --check",
|
|
403
|
+
"check:focus-ring-paint": "node scripts/check-focus-ring-paint.mjs"
|
|
418
404
|
},
|
|
419
405
|
"peerDependencies": {
|
|
420
|
-
"@axe-core/playwright": "*",
|
|
421
406
|
"@hookform/resolvers": "^5.2.0",
|
|
422
407
|
"@tanstack/react-query": ">=5.0.0",
|
|
423
|
-
"axe-core": "*",
|
|
424
408
|
"playwright": "*",
|
|
425
409
|
"react": ">=19.0.0",
|
|
426
410
|
"react-dom": ">=19.0.0",
|
|
@@ -436,32 +420,12 @@
|
|
|
436
420
|
},
|
|
437
421
|
"playwright": {
|
|
438
422
|
"optional": true
|
|
439
|
-
},
|
|
440
|
-
"@axe-core/playwright": {
|
|
441
|
-
"optional": true
|
|
442
|
-
},
|
|
443
|
-
"axe-core": {
|
|
444
|
-
"optional": true
|
|
445
423
|
}
|
|
446
424
|
},
|
|
447
425
|
"dependencies": {
|
|
448
426
|
"@date-fns/tz": "^1.5.0",
|
|
449
427
|
"@fontsource/m-plus-2": "^5.3.0",
|
|
450
428
|
"@fontsource/noto-sans-jp": "^5.3.0",
|
|
451
|
-
"@radix-ui/react-accordion": "^1.2.20",
|
|
452
|
-
"@radix-ui/react-avatar": "^1.2.6",
|
|
453
|
-
"@radix-ui/react-collapsible": "^1.1.20",
|
|
454
|
-
"@radix-ui/react-context-menu": "^2.3.7",
|
|
455
|
-
"@radix-ui/react-label": "^2.1.15",
|
|
456
|
-
"@radix-ui/react-menubar": "^1.1.24",
|
|
457
|
-
"@radix-ui/react-navigation-menu": "^1.2.22",
|
|
458
|
-
"@radix-ui/react-scroll-area": "^1.2.18",
|
|
459
|
-
"@radix-ui/react-select": "^2.3.7",
|
|
460
|
-
"@radix-ui/react-separator": "^1.1.15",
|
|
461
|
-
"@radix-ui/react-slider": "^1.4.7",
|
|
462
|
-
"@radix-ui/react-slot": "^1.3.3",
|
|
463
|
-
"@radix-ui/react-toggle": "^1.1.18",
|
|
464
|
-
"@radix-ui/react-toggle-group": "^1.1.19",
|
|
465
429
|
"@react-aria/utils": "^3.34.1",
|
|
466
430
|
"@tanstack/react-table": "^9.2.4",
|
|
467
431
|
"class-variance-authority": "^0.7.1",
|
|
@@ -483,9 +447,17 @@
|
|
|
483
447
|
"vaul": "^1.1.2"
|
|
484
448
|
},
|
|
485
449
|
"devDependencies": {
|
|
486
|
-
"@axe-core/playwright": "^4.13.0",
|
|
487
450
|
"@eslint/js": "^10.0.1",
|
|
488
451
|
"@hookform/resolvers": "^5.9.1",
|
|
452
|
+
"@radix-ui/react-accordion": "^1.2.20",
|
|
453
|
+
"@radix-ui/react-avatar": "^1.2.6",
|
|
454
|
+
"@radix-ui/react-collapsible": "^1.1.20",
|
|
455
|
+
"@radix-ui/react-label": "^2.1.15",
|
|
456
|
+
"@radix-ui/react-select": "^2.3.7",
|
|
457
|
+
"@radix-ui/react-separator": "^1.1.15",
|
|
458
|
+
"@radix-ui/react-slot": "^1.3.3",
|
|
459
|
+
"@radix-ui/react-toggle": "^1.1.18",
|
|
460
|
+
"@radix-ui/react-toggle-group": "^1.1.19",
|
|
489
461
|
"@tailwindcss/vite": "^4.3.3",
|
|
490
462
|
"@tanstack/react-query": "^5.102.8",
|
|
491
463
|
"@testing-library/jest-dom": "^7.0.1",
|
|
@@ -497,7 +469,6 @@
|
|
|
497
469
|
"@types/react-dom": "^19.2.5",
|
|
498
470
|
"@vitejs/plugin-react": "^6.1.1",
|
|
499
471
|
"@vitest/coverage-v8": "^4.1.11",
|
|
500
|
-
"axe-core": "^4.13.0",
|
|
501
472
|
"eslint": "^10.9.1",
|
|
502
473
|
"eslint-plugin-react": "^7.37.5",
|
|
503
474
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
@@ -520,7 +491,6 @@
|
|
|
520
491
|
"typescript-eslint": "^8.68.0",
|
|
521
492
|
"vite": "^8.2.2",
|
|
522
493
|
"vitest": "^4.1.11",
|
|
523
|
-
"vitest-axe": "1.0.0-pre.5",
|
|
524
494
|
"zod": "^4.5.4"
|
|
525
495
|
},
|
|
526
496
|
"bin": {
|
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -372,6 +372,24 @@ export function ensureConsumerRules(root) {
|
|
|
372
372
|
mkdirSync(dir, { recursive: true });
|
|
373
373
|
writeFileSync(target, next);
|
|
374
374
|
|
|
375
|
+
// Prettier and this writer were fighting over the same file: the body holds aligned markdown
|
|
376
|
+
// tables, Prettier reformats them, the digest changes, the next install writes it back, and
|
|
377
|
+
// `format:check` goes red again — a loop with no winner, and the consumer is who is stuck in it
|
|
378
|
+
// (godx-jp/id#513). The file is OURS; say so to Prettier once, idempotently.
|
|
379
|
+
const ignoreFile = join(root, ".prettierignore");
|
|
380
|
+
if (existsSync(ignoreFile)) {
|
|
381
|
+
const cur = readFileSync(ignoreFile, "utf8");
|
|
382
|
+
const owned = [".ai/rules/godxjp-ui.md", ".claude/godxjp-ui-workflow.md"].filter(
|
|
383
|
+
(line) => !cur.includes(line),
|
|
384
|
+
);
|
|
385
|
+
if (owned.length) {
|
|
386
|
+
writeFileSync(
|
|
387
|
+
ignoreFile,
|
|
388
|
+
`${cur.replace(/\s*$/, "")}\n\n# Owned by @godxjp/ui — rewritten on every install, never hand-formatted.\n${owned.join("\n")}\n`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
375
393
|
// Only touch the index when the repo keeps one; a missing index means the repo reads rule files
|
|
376
394
|
// directly, and inventing one would change how it loads everything else.
|
|
377
395
|
const index = join(dir, "index.md");
|
|
@@ -45,12 +45,17 @@ chúng. Không chép lại ở đây. Ba điều cấm riêng của **chuột b
|
|
|
45
45
|
`w-[240px]`, `text-muted-foreground`.
|
|
46
46
|
3. **Gõ mã màu hex hay số đo ngoài thang token.**
|
|
47
47
|
|
|
48
|
-
Thước đo, chạy trước mọi lần review
|
|
48
|
+
Thước đo, chạy trước mọi lần review — **quét đúng thứ bạn vừa đụng vào**:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
node node_modules/@godxjp/ui/scripts/ui-audit.mjs
|
|
51
|
+
node node_modules/@godxjp/ui/scripts/ui-audit.mjs --changed # 0 lỗi là mức đạt
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
`--changed` lấy danh sách từ diff so với merge-base, nên nó thấy cả file sửa bằng
|
|
55
|
+
shell — thứ hook `PostToolUse` không thấy. Quét cả `resources/js` chỉ khi việc được
|
|
56
|
+
giao ĐÚNG là soát toàn bộ UI: trên một kho cũ, nó trả về hàng trăm phát hiện thuộc
|
|
57
|
+
mã kế thừa và chôn mất cái bạn vừa tạo ra.
|
|
58
|
+
|
|
54
59
|
Một lỗi bạn **không sửa được ở phía consumer** chính là một khoảng trống của DS.
|
|
55
60
|
Nó là đầu vào của §3, không phải một ngoại lệ để nới.
|
|
56
61
|
|
|
@@ -116,10 +121,11 @@ tarball đường dẫn máy bạn lọt vào commit. `main` phải `npm ci` đ
|
|
|
116
121
|
|
|
117
122
|
### Bước 5 — Cổng của DS
|
|
118
123
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
**KHÔNG chạy `pnpm verify:ci` / `pnpm test` của kho DS khi chưa được chủ dự án cho
|
|
125
|
+
phép trong chính lượt trao đổi ấy.** Đó là bộ đầy đủ của một kho KHÁC — hàng nghìn
|
|
126
|
+
test cộng các cổng trình duyệt, hàng chục phút máy mỗi lượt. Chạy đúng test bạn vừa
|
|
127
|
+
viết cho thay đổi này, đẩy nhánh, rồi đọc cổng đỏ trên Actions: `ci.yml` chạy y hệt
|
|
128
|
+
những thứ ấy trên PR. **Tới được bước này KHÔNG phải là được phép.**
|
|
123
129
|
|
|
124
130
|
**Không nới, không tắt, không thêm ngoại lệ để lấy màu xanh.** Một cổng đỏ là
|
|
125
131
|
một câu hỏi, không phải một chướng ngại. Nếu bạn tin cổng ấy sai thì nói ra và
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* .claude/skills/frontend-design/rules/ui-standardization.md): use the design system, never
|
|
6
6
|
* hand-roll.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
9
10
|
import { isAbsolute, join, relative } from "node:path";
|
|
10
11
|
|
|
11
12
|
const CWD = process.cwd();
|
|
@@ -20,11 +21,45 @@ const args = process.argv.slice(2);
|
|
|
20
21
|
const asJson = args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
|
|
21
22
|
const quiet = args.includes("--quiet");
|
|
22
23
|
const dirArgs = args.filter((a) => !a.startsWith("--") && a !== "json");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `--changed` — audit what this branch touched, whatever tool touched it.
|
|
27
|
+
*
|
|
28
|
+
* The PostToolUse hook fires on `Write|Edit|MultiEdit`, so an agent editing through a shell
|
|
29
|
+
* (`sed -i`, a heredoc, `cat >`) never triggers it. A consumer measured exactly that: across a
|
|
30
|
+
* long session EVERY `.tsx` edit went through Bash and the audit fired not once (godx-jp/id#497).
|
|
31
|
+
* The hook cannot close it — a Bash call carries no `file_path`. A diff can, and one line in a
|
|
32
|
+
* local gate or in CI then covers every edit path, including the ones nobody thought of.
|
|
33
|
+
*/
|
|
34
|
+
function changedFiles() {
|
|
35
|
+
const base =
|
|
36
|
+
spawnSync("git", ["merge-base", "HEAD", "origin/main"], { encoding: "utf8" }).stdout.trim() ||
|
|
37
|
+
"HEAD";
|
|
38
|
+
const run = (a) => spawnSync("git", a, { encoding: "utf8" }).stdout ?? "";
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
...new Set(
|
|
42
|
+
[
|
|
43
|
+
run(["diff", "--name-only", "--diff-filter=ACMR", base, "--"]),
|
|
44
|
+
run(["diff", "--name-only", "--diff-filter=ACMR", "--cached"]),
|
|
45
|
+
run(["ls-files", "--others", "--exclude-standard"]),
|
|
46
|
+
]
|
|
47
|
+
.join("\n")
|
|
48
|
+
.split("\n")
|
|
49
|
+
.map((f) => f.trim())
|
|
50
|
+
.filter((f) => /\.(tsx|jsx)$/.test(f) && existsSync(join(CWD, f))),
|
|
51
|
+
),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CHANGED = args.includes("--changed");
|
|
56
|
+
const SCAN_DIRS = CHANGED
|
|
57
|
+
? changedFiles()
|
|
58
|
+
: dirArgs.length
|
|
59
|
+
? dirArgs
|
|
60
|
+
: SELF
|
|
61
|
+
? ["src", "docs"]
|
|
62
|
+
: ["resources/js/components", "resources/js/pages", "resources/js/layouts"];
|
|
28
63
|
|
|
29
64
|
const PALETTE =
|
|
30
65
|
"red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|gray|grey|slate|zinc|neutral|stone";
|
|
@@ -89,6 +124,7 @@ const ATTRS = String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|=>|[^>"'])*`;
|
|
|
89
124
|
const RULES = [
|
|
90
125
|
{
|
|
91
126
|
id: "no-utility-spacing",
|
|
127
|
+
replacement: "Flex gap / ResponsiveGrid / PageContainer",
|
|
92
128
|
classOnly: true,
|
|
93
129
|
scope: "consumer",
|
|
94
130
|
severity: "error",
|
|
@@ -101,6 +137,7 @@ const RULES = [
|
|
|
101
137
|
},
|
|
102
138
|
{
|
|
103
139
|
id: "no-utility-layout",
|
|
140
|
+
replacement: "Flex (row), Flex direction col (stack), ResponsiveGrid",
|
|
104
141
|
scope: "consumer",
|
|
105
142
|
severity: "error",
|
|
106
143
|
// A hand-rolled flex/grid container. The primitives exist precisely so a row is <Flex>, a
|
|
@@ -111,6 +148,7 @@ const RULES = [
|
|
|
111
148
|
},
|
|
112
149
|
{
|
|
113
150
|
id: "no-hand-rolled-surface",
|
|
151
|
+
replacement: "Card / Badge / Avatar / ListRow / Descriptions / EmptyState / Progress",
|
|
114
152
|
scope: "consumer",
|
|
115
153
|
severity: "warn",
|
|
116
154
|
// rounded + border/bg on the consumer's own element = a fake Card / Badge / Avatar / ListRow
|
|
@@ -139,6 +177,7 @@ const RULES = [
|
|
|
139
177
|
},
|
|
140
178
|
{
|
|
141
179
|
id: "no-space-xy",
|
|
180
|
+
replacement: "Flex gap",
|
|
142
181
|
classOnly: true,
|
|
143
182
|
severity: "error",
|
|
144
183
|
test: /\bspace-[xy]-\d/,
|
|
@@ -146,6 +185,7 @@ const RULES = [
|
|
|
146
185
|
},
|
|
147
186
|
{
|
|
148
187
|
id: "no-raw-palette-color",
|
|
188
|
+
replacement: "a semantic token (--primary, --muted-foreground, --border)",
|
|
149
189
|
classOnly: true,
|
|
150
190
|
severity: "error",
|
|
151
191
|
test: new RegExp(
|
|
@@ -156,11 +196,12 @@ const RULES = [
|
|
|
156
196
|
},
|
|
157
197
|
{
|
|
158
198
|
id: "status-tone-not-variant",
|
|
199
|
+
replacement: "tone=",
|
|
159
200
|
severity: "error",
|
|
160
201
|
spansElement: true,
|
|
161
202
|
// Only the tone-driven status components (Badge/Tag/StatCard) are wrong here — they expose a
|
|
162
203
|
// `tone` prop and reserve `variant` for STRUCTURE (default|secondary|outline). Button, Alert,
|
|
163
|
-
// DropdownMenuItem,
|
|
204
|
+
// DropdownMenuItem, AlertDialog etc. legitimately use `variant` for emphasis,
|
|
164
205
|
// so they must NOT be flagged.
|
|
165
206
|
test: new RegExp(
|
|
166
207
|
`<(?:Badge|Tag|StatCard)\\b${ATTRS}\\bvariant=["'](?:success|warning|destructive|info|neutral)["']`,
|
|
@@ -178,6 +219,7 @@ const RULES = [
|
|
|
178
219
|
},
|
|
179
220
|
{
|
|
180
221
|
id: "value-callback-on-value-change",
|
|
222
|
+
replacement: "onValueChange",
|
|
181
223
|
severity: "error",
|
|
182
224
|
spansElement: true,
|
|
183
225
|
test: new RegExp(
|
|
@@ -190,6 +232,7 @@ const RULES = [
|
|
|
190
232
|
},
|
|
191
233
|
{
|
|
192
234
|
id: "no-arbitrary-hex",
|
|
235
|
+
replacement: "a semantic token",
|
|
193
236
|
classOnly: true,
|
|
194
237
|
severity: "error",
|
|
195
238
|
test: /(bg|text|border|ring|fill|stroke|from|to|via)-\[#[0-9a-fA-F]{3,8}\]/,
|
|
@@ -200,6 +243,7 @@ const RULES = [
|
|
|
200
243
|
// (they start with a letter), so token-driven values stay legal (rules §4–§5).
|
|
201
244
|
{
|
|
202
245
|
id: "no-arbitrary-spacing",
|
|
246
|
+
replacement: "Flex gap / the spacing scale",
|
|
203
247
|
classOnly: true,
|
|
204
248
|
severity: "error",
|
|
205
249
|
test: /\b(p|px|py|pt|pb|pl|pr|pe|ps|m|mx|my|mt|mb|ml|mr|me|ms|gap|gap-x|gap-y|inset|inset-x|inset-y|top|right|bottom|left)-\[-?\.?\d/,
|
|
@@ -208,6 +252,7 @@ const RULES = [
|
|
|
208
252
|
},
|
|
209
253
|
{
|
|
210
254
|
id: "no-arbitrary-size",
|
|
255
|
+
replacement: "a size token or the component's size prop",
|
|
211
256
|
classOnly: true,
|
|
212
257
|
severity: "error",
|
|
213
258
|
// `min-w-[…]` / `min-h-[…]` are allowed: a MINIMUM dimension is the legit responsive
|
|
@@ -220,6 +265,7 @@ const RULES = [
|
|
|
220
265
|
},
|
|
221
266
|
{
|
|
222
267
|
id: "no-arbitrary-typography",
|
|
268
|
+
replacement: "Text size / weight / tone",
|
|
223
269
|
classOnly: true,
|
|
224
270
|
severity: "error",
|
|
225
271
|
test: /\b(text|leading|tracking|font)-\[-?\.?\d/,
|
|
@@ -228,6 +274,7 @@ const RULES = [
|
|
|
228
274
|
},
|
|
229
275
|
{
|
|
230
276
|
id: "no-arbitrary-radius",
|
|
277
|
+
replacement: "a radius token",
|
|
231
278
|
classOnly: true,
|
|
232
279
|
severity: "error",
|
|
233
280
|
test: /\brounded(?:-[a-z]+)?-\[-?\.?\d/,
|
|
@@ -267,6 +314,7 @@ const RULES = [
|
|
|
267
314
|
},
|
|
268
315
|
{
|
|
269
316
|
id: "no-raw-select",
|
|
317
|
+
replacement: "Select",
|
|
270
318
|
scope: "consumer-control",
|
|
271
319
|
severity: "error",
|
|
272
320
|
test: /<select[\s>]/,
|
|
@@ -274,12 +322,14 @@ const RULES = [
|
|
|
274
322
|
},
|
|
275
323
|
{
|
|
276
324
|
id: "no-raw-table",
|
|
325
|
+
replacement: "DataTable / Table",
|
|
277
326
|
severity: "error",
|
|
278
327
|
test: /<table[\s>]/,
|
|
279
328
|
message: "Use the <Table> family from @godxjp/ui, not a raw <table> (rules §3).",
|
|
280
329
|
},
|
|
281
330
|
{
|
|
282
331
|
id: "no-raw-textarea",
|
|
332
|
+
replacement: "Textarea",
|
|
283
333
|
scope: "consumer-control",
|
|
284
334
|
severity: "warn",
|
|
285
335
|
test: /<textarea[\s>]/,
|
|
@@ -287,6 +337,7 @@ const RULES = [
|
|
|
287
337
|
},
|
|
288
338
|
{
|
|
289
339
|
id: "no-raw-input",
|
|
340
|
+
replacement: "Input (Upload for a file picker)",
|
|
290
341
|
scope: "consumer-control",
|
|
291
342
|
severity: "error",
|
|
292
343
|
spansElement: true,
|
|
@@ -295,6 +346,7 @@ const RULES = [
|
|
|
295
346
|
},
|
|
296
347
|
{
|
|
297
348
|
id: "no-raw-button",
|
|
349
|
+
replacement: "Button",
|
|
298
350
|
scope: "consumer-control",
|
|
299
351
|
severity: "error",
|
|
300
352
|
test: /<button[\s>]/,
|
|
@@ -302,6 +354,7 @@ const RULES = [
|
|
|
302
354
|
},
|
|
303
355
|
{
|
|
304
356
|
id: "card-manual-padding",
|
|
357
|
+
replacement: "CardContent (or CardContent flush)",
|
|
305
358
|
severity: "error",
|
|
306
359
|
spansElement: true,
|
|
307
360
|
test: new RegExp(`<Card\\b${ATTRS}\\bp-[1-9]`, "g"),
|
|
@@ -310,6 +363,7 @@ const RULES = [
|
|
|
310
363
|
},
|
|
311
364
|
{
|
|
312
365
|
id: "no-dark-color-override",
|
|
366
|
+
replacement: "a semantic token, which already answers to the theme",
|
|
313
367
|
classOnly: true,
|
|
314
368
|
severity: "warn",
|
|
315
369
|
test: /\bdark:(bg|text|border|ring|fill|stroke)-/,
|
|
@@ -342,6 +396,7 @@ const RULES = [
|
|
|
342
396
|
// ─── International-standard a11y / i18n / RTL rules (WARN — guide the agent, never block) ───
|
|
343
397
|
{
|
|
344
398
|
id: "no-emoji-in-ui",
|
|
399
|
+
replacement: "a lucide icon",
|
|
345
400
|
severity: "warn",
|
|
346
401
|
test: EMOJI,
|
|
347
402
|
standard: "Unicode UTS #51 (emoji) · WCAG 2.2 SC 1.1.1",
|
|
@@ -358,6 +413,7 @@ const RULES = [
|
|
|
358
413
|
},
|
|
359
414
|
{
|
|
360
415
|
id: "no-physical-direction",
|
|
416
|
+
replacement: "the logical property (ms-/me-, inline-start/inline-end)",
|
|
361
417
|
classOnly: true,
|
|
362
418
|
severity: "warn",
|
|
363
419
|
// Physical-edge utilities break RTL. Logical equivalents: ms-/me-/ps-/pe-, start-/end-,
|
|
@@ -465,6 +521,7 @@ if (args.includes("--rules")) {
|
|
|
465
521
|
severity: r.severity,
|
|
466
522
|
standard: r.standard ?? null,
|
|
467
523
|
message: r.message,
|
|
524
|
+
replacement: r.replacement,
|
|
468
525
|
})),
|
|
469
526
|
null,
|
|
470
527
|
2,
|
|
@@ -730,6 +787,22 @@ function jsxOpeningEnd(source, start) {
|
|
|
730
787
|
// Structural: a <Card> (without p-0) whose first child is body content rather than a Card
|
|
731
788
|
// sub-component sits FLUSH (no padding). Per-line regexes can't see across lines, so this is a
|
|
732
789
|
// whole-file pass. The body must be wrapped in <CardContent> (titles in <CardHeader>).
|
|
790
|
+
/**
|
|
791
|
+
* A `Card` whose entire body is a table. `CardContent` pads 16px inline while
|
|
792
|
+
* `.ui-data-table-surface` draws its own 1px border, so the default composition renders a box
|
|
793
|
+
* inside a box — and a wide table runs PAST the card's edge instead of stopping at it (measured in
|
|
794
|
+
* a consumer at −85px and −519px, godx-jp/id#498). `<CardContent flush>` already solves it. What
|
|
795
|
+
* was missing was the rule that says so.
|
|
796
|
+
*
|
|
797
|
+
* Deliberately narrow: the table must be the DIRECT child. A `<Flex direction="col">` holding
|
|
798
|
+
* filters AND a table is a mixed body, where the padding is correct — the reporter hit that trap
|
|
799
|
+
* with a `childElementCount === 1` test.
|
|
800
|
+
*/
|
|
801
|
+
const CARD_TABLE_FLUSH = new RegExp(
|
|
802
|
+
`<CardContent(?![^>]*\\bflush\\b)(?:\\s${ATTRS})?>\\s*<(?:DataTable|Table)\\b`,
|
|
803
|
+
"g",
|
|
804
|
+
);
|
|
805
|
+
|
|
733
806
|
const CARD_FLUSH = new RegExp(
|
|
734
807
|
`<Card(?!${ATTRS}\\bp-0\\b)(?:\\s${ATTRS})?>\\s*<(?!CardContent|CardHeader|CardCover|CardFooter|CardBar|\\/Card)`,
|
|
735
808
|
"g",
|
|
@@ -760,7 +833,53 @@ const SIBLING_CARDS = /^([ \t]*)<\/Card>\s*\n\1<Card\b/gm;
|
|
|
760
833
|
const ACTIVE_RULES =
|
|
761
834
|
SELF && !args.includes("--consumer") ? RULES.filter((r) => r.scope !== "consumer") : RULES;
|
|
762
835
|
|
|
836
|
+
/**
|
|
837
|
+
* The rule file this package OWNS, and whether the consumer's copy is the one this package writes.
|
|
838
|
+
*
|
|
839
|
+
* `.ai/rules/godxjp-ui.md` opens by promising it is rewritten on every upgrade. A consumer with
|
|
840
|
+
* `ignore-scripts=true` — a sane, increasingly common default — never runs our postinstall, so the
|
|
841
|
+
* promise silently fails: measured in a consumer at `godxjp-ui:version 19.6.0` while the installed
|
|
842
|
+
* package was **23.0.0**, three majors apart (godx-jp/id#513). An agent then follows a rule file
|
|
843
|
+
* describing components that were removed, props that no longer exist and a `role` attribute that
|
|
844
|
+
* is gone — and nothing anywhere says the ground moved.
|
|
845
|
+
*
|
|
846
|
+
* postinstall cannot fix this: it is the thing that did not run. This can, because a consumer runs
|
|
847
|
+
* the audit.
|
|
848
|
+
*/
|
|
849
|
+
function staleOwnedRules() {
|
|
850
|
+
if (SELF) return null;
|
|
851
|
+
const target = join(CWD, ".ai", "rules", "godxjp-ui.md");
|
|
852
|
+
if (!existsSync(target)) return null;
|
|
853
|
+
|
|
854
|
+
const stamped = /<!-- godxjp-ui:version ([^\s]+) -->/.exec(readFileSync(target, "utf8"))?.[1];
|
|
855
|
+
let installed;
|
|
856
|
+
try {
|
|
857
|
+
installed = JSON.parse(
|
|
858
|
+
readFileSync(join(CWD, "node_modules", "@godxjp", "ui", "package.json"), "utf8"),
|
|
859
|
+
).version;
|
|
860
|
+
} catch {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
if (!stamped || !installed || stamped === installed) return null;
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
file: ".ai/rules/godxjp-ui.md",
|
|
867
|
+
line: 1,
|
|
868
|
+
rule: "owned-rules-stale",
|
|
869
|
+
severity: "error",
|
|
870
|
+
message:
|
|
871
|
+
`This file is written by @godxjp/ui and says version ${stamped}, but the installed package ` +
|
|
872
|
+
`is ${installed}. Its rules describe a different library than the one you are building ` +
|
|
873
|
+
"against — most likely because `ignore-scripts=true` kept our postinstall from running.",
|
|
874
|
+
replacement:
|
|
875
|
+
"INIT_CWD=\"$PWD\" node node_modules/@godxjp/ui/scripts/postinstall.mjs",
|
|
876
|
+
snippet: `<!-- godxjp-ui:version ${stamped} --> vs installed ${installed}`,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
763
880
|
const findings = [];
|
|
881
|
+
const stale = staleOwnedRules();
|
|
882
|
+
if (stale) findings.push(stale);
|
|
764
883
|
let filesScanned = 0;
|
|
765
884
|
for (const dir of SCAN_DIRS) {
|
|
766
885
|
for (const file of walk(isAbsolute(dir) ? dir : join(CWD, dir))) {
|
|
@@ -809,6 +928,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
809
928
|
severity: rule.severity,
|
|
810
929
|
standard: rule.standard,
|
|
811
930
|
message: rule.message,
|
|
931
|
+
replacement: rule.replacement,
|
|
812
932
|
snippet: (origLines[i] ?? line).trim().slice(0, 120),
|
|
813
933
|
});
|
|
814
934
|
}
|
|
@@ -844,7 +964,38 @@ for (const dir of SCAN_DIRS) {
|
|
|
844
964
|
}
|
|
845
965
|
}
|
|
846
966
|
if (!isJsx) continue;
|
|
967
|
+
/**
|
|
968
|
+
* `CARD_FLUSH` wants a Card slot as the IMMEDIATE next element, so any legitimate wrapper
|
|
969
|
+
* trips it. The one that matters is `<Form>`: a submit button in `<CardFooter>` only submits
|
|
970
|
+
* when the `<form>` wraps BOTH the content and the footer, so `<Card><Form><CardContent>` is
|
|
971
|
+
* the only correct shape for that card — and it was reported as an error three times in one
|
|
972
|
+
* consumer file (godx-jp/id#496), on its newest code. A confident error against correct code
|
|
973
|
+
* teaches the reader that the audit is noise.
|
|
974
|
+
*
|
|
975
|
+
* The rule's intent is "the body must not sit flush against the card edges", so ask that:
|
|
976
|
+
* is there a `<CardContent>` before this Card's own `</Card>`? Depth is counted, because a
|
|
977
|
+
* Card can hold a Card.
|
|
978
|
+
*/
|
|
979
|
+
const hasBodyBeforeClose = (from) => {
|
|
980
|
+
let depth = 0;
|
|
981
|
+
const tag = /<(\/?)Card(Content|Header|Cover|Footer|Bar)?\b/g;
|
|
982
|
+
tag.lastIndex = from;
|
|
983
|
+
for (let m; (m = tag.exec(scanContent)); ) {
|
|
984
|
+
const [, closing, slot] = m;
|
|
985
|
+
if (!slot) {
|
|
986
|
+
if (closing) {
|
|
987
|
+
if (depth === 0) return false;
|
|
988
|
+
depth -= 1;
|
|
989
|
+
} else depth += 1;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (!closing && slot === "Content" && depth <= 1) return true;
|
|
993
|
+
}
|
|
994
|
+
return false;
|
|
995
|
+
};
|
|
996
|
+
|
|
847
997
|
for (const match of scanContent.matchAll(CARD_FLUSH)) {
|
|
998
|
+
if (hasBodyBeforeClose(match.index)) continue;
|
|
848
999
|
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
849
1000
|
if (suppressed("card-needs-content", lineNo - 1)) continue;
|
|
850
1001
|
findings.push({
|
|
@@ -857,6 +1008,24 @@ for (const dir of SCAN_DIRS) {
|
|
|
857
1008
|
snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
|
|
858
1009
|
});
|
|
859
1010
|
}
|
|
1011
|
+
for (const match of scanContent.matchAll(CARD_TABLE_FLUSH)) {
|
|
1012
|
+
const lineNo = scanContent.slice(0, match.index).split("\n").length + 1;
|
|
1013
|
+
|
|
1014
|
+
if (suppressed("card-table-needs-flush", lineNo - 1)) continue;
|
|
1015
|
+
|
|
1016
|
+
findings.push({
|
|
1017
|
+
file: rel,
|
|
1018
|
+
line: lineNo,
|
|
1019
|
+
rule: "card-table-needs-flush",
|
|
1020
|
+
severity: "error",
|
|
1021
|
+
message:
|
|
1022
|
+
"A Card whose whole body is a table must let the table touch the card's inner edge — " +
|
|
1023
|
+
"use <CardContent flush>. A default CardContent pads 16px while the table draws its own " +
|
|
1024
|
+
"border, so the table reads as a second box inside the card and a wide one overflows it.",
|
|
1025
|
+
replacement: "CardContent flush",
|
|
1026
|
+
snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
860
1029
|
if (
|
|
861
1030
|
!(SELF && !args.includes("--consumer")) &&
|
|
862
1031
|
!/<(?:Flex|ResponsiveGrid|PageContainer)\b/.test(scanContent)
|
|
@@ -899,6 +1068,12 @@ const warnings = findings.filter((f) => f.severity === "warn");
|
|
|
899
1068
|
// `resources/js/{components,pages,layouts}`; `walk()` swallows ENOENT and returns [], so in any
|
|
900
1069
|
// tree without that layout — this repo included — `pnpm audit` printed
|
|
901
1070
|
// "✓ No UI-standardization violations found." and exited 0 having read nothing at all.
|
|
1071
|
+
// …except under `--changed`, where "this branch touched no .tsx" is a clean run, not a
|
|
1072
|
+
// misconfigured path. Failing there would make the gate unusable on every backend-only commit.
|
|
1073
|
+
if (filesScanned === 0 && CHANGED) {
|
|
1074
|
+
if (!quiet && !asJson) console.log("✓ ui-audit --changed: no .tsx/.jsx changed on this branch.");
|
|
1075
|
+
process.exit(0);
|
|
1076
|
+
}
|
|
902
1077
|
if (filesScanned === 0) {
|
|
903
1078
|
const message =
|
|
904
1079
|
`ui-audit scanned 0 files — none of [${SCAN_DIRS.join(", ")}] exists (or all were filtered). ` +
|
|
@@ -937,6 +1112,9 @@ if (filesScanned === 0) {
|
|
|
937
1112
|
const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
|
|
938
1113
|
console.log(`${tag} ${C.bold}${f.file}:${f.line}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
|
|
939
1114
|
console.log(` ${f.message}`);
|
|
1115
|
+
// Name the primitive HERE: without it every finding costs a round-trip to the catalog plus a
|
|
1116
|
+
// guess about what to look up (godx-jp/id#497). The audit already knows the answer.
|
|
1117
|
+
if (f.replacement) console.log(` ${C.bold}use: ${f.replacement}${C.reset}`);
|
|
940
1118
|
if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
|
|
941
1119
|
console.log(` ${C.dim}${f.snippet}${C.reset}`);
|
|
942
1120
|
}
|