@godxjp/ui 19.1.0 → 19.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/README.md +25 -128
- package/dist/app/theme-axes.d.ts +4 -15
- package/dist/app/timezones.d.ts +1 -4
- package/dist/components/charts/area-chart.d.ts +2 -3
- package/dist/components/charts/bar-chart.d.ts +2 -3
- package/dist/components/charts/chart-frame.d.ts +7 -16
- package/dist/components/charts/chart-summary.d.ts +2 -2
- package/dist/components/charts/compact-bar-trend.d.ts +9 -24
- package/dist/components/charts/index.d.ts +1 -6
- package/dist/components/data-display/badge.d.ts +2 -12
- package/dist/components/data-display/card.d.ts +21 -41
- package/dist/components/data-display/carousel.d.ts +1 -2
- package/dist/components/data-display/code-block.d.ts +4 -0
- package/dist/components/data-display/code-block.js +24 -0
- package/dist/components/data-display/data-table.d.ts +15 -37
- package/dist/components/data-display/data-table.js +1 -3
- package/dist/components/data-display/descriptions.d.ts +2 -6
- package/dist/components/data-display/descriptions.js +0 -2
- package/dist/components/data-display/index.d.ts +4 -0
- package/dist/components/data-display/index.js +4 -0
- package/dist/components/data-display/list-row.d.ts +3 -22
- package/dist/components/data-display/permission-matrix.d.ts +7 -20
- package/dist/components/data-display/progress.d.ts +1 -1
- package/dist/components/data-display/prose.d.ts +4 -0
- package/dist/components/data-display/prose.js +20 -0
- package/dist/components/data-display/service-launcher-card.d.ts +3 -10
- package/dist/components/data-display/table.d.ts +20 -80
- package/dist/components/data-display/table.js +0 -3
- package/dist/components/data-display/timeline.d.ts +3 -5
- package/dist/components/data-entry/branch-scope-picker.d.ts +2 -19
- package/dist/components/data-entry/branch-scope-picker.js +0 -1
- package/dist/components/data-entry/calendar.d.ts +3 -2
- package/dist/components/data-entry/calendar.js +58 -3
- package/dist/components/data-entry/command-palette.d.ts +8 -22
- package/dist/components/data-entry/date-picker.d.ts +4 -5
- package/dist/components/data-entry/date-picker.js +6 -1
- package/dist/components/data-entry/date-range-picker.d.ts +1 -1
- package/dist/components/data-entry/date-range-picker.js +8 -3
- package/dist/components/data-entry/form-errors.d.ts +7 -18
- package/dist/components/data-entry/form-field.js +1 -5
- package/dist/components/data-entry/form.d.ts +1 -9
- package/dist/components/data-entry/input.d.ts +16 -14
- package/dist/components/data-entry/input.js +3 -0
- package/dist/components/data-entry/month-picker.js +2 -2
- package/dist/components/data-entry/month-range-picker.d.ts +4 -6
- package/dist/components/data-entry/month-range-picker.js +2 -2
- package/dist/components/data-entry/number-input.d.ts +1 -5
- package/dist/components/data-entry/number-input.js +1 -0
- package/dist/components/data-entry/radio.js +0 -1
- package/dist/components/data-entry/search-select.d.ts +2 -4
- package/dist/components/data-entry/select.d.ts +11 -6
- package/dist/components/data-entry/select.js +4 -2
- package/dist/components/data-entry/switch.js +0 -1
- package/dist/components/data-entry/textarea.d.ts +10 -16
- package/dist/components/data-entry/time-picker.d.ts +3 -5
- package/dist/components/data-entry/upload-types.d.ts +1 -5
- package/dist/components/data-entry/use-upload-draft.d.ts +0 -4
- package/dist/components/feedback/dialog.d.ts +6 -15
- package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
- package/dist/components/feedback/sheet.d.ts +3 -10
- package/dist/components/feedback/sheet.js +1 -1
- package/dist/components/feedback/sonner.js +2 -3
- package/dist/components/general/activity.d.ts +4 -30
- package/dist/components/general/button.js +1 -10
- package/dist/components/general/logo.d.ts +6 -28
- package/dist/components/general/reveal.d.ts +3 -9
- package/dist/components/general/typography.d.ts +1 -3
- package/dist/components/layout/account-chip.d.ts +8 -0
- package/dist/components/layout/account-chip.js +45 -0
- package/dist/components/layout/auth-divider.d.ts +2 -9
- package/dist/components/layout/auth-shell.d.ts +1 -14
- package/dist/components/layout/breadcrumb.d.ts +1 -5
- package/dist/components/layout/centered-shell.d.ts +4 -18
- package/dist/components/layout/error-surface.d.ts +11 -28
- package/dist/components/layout/error-surface.js +4 -1
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/layout/legal-document-shell.d.ts +5 -28
- package/dist/components/layout/master-detail.d.ts +2 -18
- package/dist/components/layout/page-container.js +0 -1
- package/dist/components/layout/responsive-grid.d.ts +6 -1
- package/dist/components/layout/responsive-grid.js +9 -2
- package/dist/components/layout/service-role-panel.d.ts +7 -19
- package/dist/components/layout/sidebar.d.ts +5 -27
- package/dist/components/layout/sidebar.js +0 -1
- package/dist/components/layout/split-pane.d.ts +0 -7
- package/dist/components/layout/topbar.d.ts +1 -14
- package/dist/components/navigation/app-setting-picker.js +1 -3
- package/dist/components/navigation/filter-bar.d.ts +1 -6
- package/dist/components/navigation/pagination-utils.d.ts +2 -12
- package/dist/components/navigation/tabs-scroll.d.ts +2 -36
- package/dist/components/navigation/tabs.js +4 -8
- package/dist/components/query/data-state.d.ts +2 -2
- package/dist/components/ui/credential-reveal.d.ts +1 -4
- package/dist/components/ui/hover-card.d.ts +1 -2
- package/dist/components/ui/separator.d.ts +2 -15
- package/dist/components/ui/toggle-group.d.ts +1 -5
- package/dist/components/ui/toggle.d.ts +6 -31
- package/dist/email/brand-mark.d.ts +6 -34
- package/dist/email/color.d.ts +7 -17
- package/dist/email/geometry.d.ts +4 -8
- package/dist/email/index.d.ts +4 -48
- package/dist/email/inline-style.d.ts +3 -9
- package/dist/email/urgency.d.ts +2 -7
- package/dist/form/form-context.d.ts +2 -5
- package/dist/form/form-root.d.ts +1 -2
- package/dist/i18n/messages/en.json +4 -0
- package/dist/i18n/messages/ja.json +4 -0
- package/dist/i18n/messages/vi.json +4 -0
- package/dist/inertia/index.d.ts +2 -10
- package/dist/lib/control-styles.d.ts +10 -21
- package/dist/lib/control-styles.js +4 -6
- package/dist/lib/datetime/format-date.d.ts +1 -4
- package/dist/lib/field-a11y.d.ts +11 -68
- package/dist/lib/field-a11y.js +0 -1
- package/dist/lib/format.d.ts +4 -8
- package/dist/lib/hooks.d.ts +6 -12
- package/dist/lib/permission-grid.d.ts +3 -18
- package/dist/lib/query-error.d.ts +3 -3
- package/dist/props/components/app.prop.d.ts +15 -38
- package/dist/props/components/charts.prop.d.ts +2 -3
- package/dist/props/components/data-display.prop.d.ts +48 -72
- package/dist/props/components/data-entry.prop.d.ts +35 -40
- package/dist/props/components/form.prop.d.ts +2 -6
- package/dist/props/components/general.prop.d.ts +11 -27
- package/dist/props/components/layout.prop.d.ts +89 -260
- package/dist/props/components/navigation.prop.d.ts +27 -42
- package/dist/props/registry.d.ts +31 -0
- package/dist/props/registry.js +33 -0
- package/dist/props/vocabulary/data.prop.d.ts +18 -49
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +10 -12
- package/dist/props/vocabulary/layout.prop.d.ts +8 -30
- package/dist/props/vocabulary/shared.prop.d.ts +3 -2
- package/dist/styles/alert-layout.css +1 -32
- package/dist/styles/badge-layout.css +4 -18
- package/dist/styles/base.css +14 -113
- package/dist/styles/card-layout.css +6 -128
- package/dist/styles/chart-layout.css +2 -17
- package/dist/styles/control.css +380 -458
- package/dist/styles/core.css +21 -0
- package/dist/styles/data-display-layout.css +175 -94
- package/dist/styles/data-entry-layout.css +0 -45
- package/dist/styles/density.css +1 -30
- package/dist/styles/dialog-layout.css +5 -69
- package/dist/styles/focus-ring.css +13 -87
- package/dist/styles/fonts.css +1 -31
- package/dist/styles/form-layout.css +7 -91
- package/dist/styles/index.css +2 -20
- package/dist/styles/layout.css +38 -456
- package/dist/styles/logo-layout.css +1 -35
- package/dist/styles/motion.css +3 -42
- package/dist/styles/navigation-layout.css +3 -50
- package/dist/styles/shell-layout.css +37 -403
- package/dist/styles/table-layout.css +16 -274
- package/dist/styles/text-layout.css +7 -21
- package/dist/styles/toggle.css +1 -23
- package/dist/theme/dxs.canonical.css +1 -55
- package/dist/theme/example.service.css +1 -26
- package/dist/theme/famgia.service.css +5 -15
- package/dist/tokens/axes.css +3 -40
- package/dist/tokens/base.css +1 -4
- package/dist/tokens/components/activity.css +6 -42
- package/dist/tokens/components/badge.css +4 -13
- package/dist/tokens/components/banner.css +4 -9
- package/dist/tokens/components/card.css +32 -88
- package/dist/tokens/components/chart.css +9 -26
- package/dist/tokens/components/control.css +37 -222
- package/dist/tokens/components/data-display.css +28 -79
- package/dist/tokens/components/data-entry.css +5 -19
- package/dist/tokens/components/descriptions.css +7 -27
- package/dist/tokens/components/email.css +38 -84
- package/dist/tokens/components/error-surface.css +2 -18
- package/dist/tokens/components/feedback.css +27 -124
- package/dist/tokens/components/form.css +13 -45
- package/dist/tokens/components/legal-document.css +16 -41
- package/dist/tokens/components/list-row.css +11 -36
- package/dist/tokens/components/logo.css +2 -57
- package/dist/tokens/components/navigation.css +16 -94
- package/dist/tokens/components/scroll-area.css +2 -18
- package/dist/tokens/components/separator.css +8 -30
- package/dist/tokens/components/sheet.css +3 -14
- package/dist/tokens/components/shell.css +53 -307
- package/dist/tokens/components/sidebar.css +5 -22
- package/dist/tokens/components/table.css +19 -140
- package/dist/tokens/components/toggle.css +3 -65
- package/dist/tokens/components/upload.css +7 -43
- package/dist/tokens/foundation.css +108 -383
- package/dist/tokens/semantic/layout.css +33 -207
- package/docs/CANONICAL-CONTRACTS.md +206 -0
- package/docs/COMPONENTS.md +157 -0
- package/docs/COMPOSITION-VS-COMPONENT.md +105 -0
- package/docs/CONSUMER-RULES.md +14 -0
- package/docs/CUSTOMER-THEMING.md +162 -0
- package/docs/DATETIME.md +69 -0
- package/docs/DEVELOPMENT.md +173 -0
- package/docs/FORMS.md +143 -0
- package/docs/FRAME-A11Y-CI.md +267 -0
- package/docs/FRAME-COVERAGE-LEDGER.md +123 -0
- package/docs/FRAME-COVERAGE-REPORT.md +140 -0
- package/docs/FRAME-COVERAGE-STANDARD.md +65 -0
- package/docs/FRAME-COVERAGE.md +29 -0
- package/docs/PROPS-REGISTRY.md +61 -0
- package/docs/PROPS-VOCABULARY.md +100 -0
- package/docs/README.md +60 -0
- package/docs/REGISTRY.md +70 -0
- package/docs/SPACING.md +56 -0
- package/docs/STANDARDS-vocabulary-tokens.md +48 -0
- package/docs/TESTING.md +98 -0
- package/docs/TOKENS.md +347 -0
- package/docs/asset-modules.d.ts +46 -0
- package/docs/assets/cover-terrain.svg +6 -0
- package/docs/assets/portrait-amber.svg +5 -0
- package/docs/assets/portrait-clay.svg +5 -0
- package/docs/assets/portrait-indigo.svg +5 -0
- package/docs/assets/portrait-iris.svg +5 -0
- package/docs/assets/portrait-moss.svg +5 -0
- package/docs/assets/portrait-plum.svg +5 -0
- package/docs/assets/portrait-slate.svg +5 -0
- package/docs/charts/compact-bar-trend.tsx +328 -0
- package/docs/charts/size-tiers.tsx +192 -0
- package/docs/data-display/accordion.tsx +99 -0
- package/docs/data-display/avatar.tsx +355 -0
- package/docs/data-display/badge.tsx +247 -0
- package/docs/data-display/card/examples/detail-panel.tsx +106 -0
- package/docs/data-display/card/index.md +3 -0
- package/docs/data-display/card/index.tsx +285 -0
- package/docs/data-display/carousel.tsx +156 -0
- package/docs/data-display/charts.tsx +198 -0
- package/docs/data-display/code-block.tsx +90 -0
- package/docs/data-display/collapsible.tsx +162 -0
- package/docs/data-display/credential-reveal.tsx +145 -0
- package/docs/data-display/data-table/examples/approval-queue.tsx +212 -0
- package/docs/data-display/data-table/examples/grid-features.tsx +158 -0
- package/docs/data-display/data-table/examples/list-page.tsx +175 -0
- package/docs/data-display/data-table/index.md +5 -0
- package/docs/data-display/data-table/index.tsx +295 -0
- package/docs/data-display/descriptions.tsx +143 -0
- package/docs/data-display/empty-state.tsx +164 -0
- package/docs/data-display/hover-card.tsx +181 -0
- package/docs/data-display/list-row.tsx +420 -0
- package/docs/data-display/permission-matrix.tsx +184 -0
- package/docs/data-display/popover-touch.tsx +27 -0
- package/docs/data-display/popover.tsx +245 -0
- package/docs/data-display/progress.tsx +84 -0
- package/docs/data-display/prose.tsx +105 -0
- package/docs/data-display/qr-code.tsx +68 -0
- package/docs/data-display/scroll-area.tsx +271 -0
- package/docs/data-display/service-launcher-card.tsx +145 -0
- package/docs/data-display/stat-card.tsx +99 -0
- package/docs/data-display/table.tsx +251 -0
- package/docs/data-display/timeline.tsx +112 -0
- package/docs/data-display/touch-actions.tsx +36 -0
- package/docs/data-display/tree-list.tsx +107 -0
- package/docs/data-entry/branch-scope-picker.tsx +123 -0
- package/docs/data-entry/calendar.tsx +180 -0
- package/docs/data-entry/cascader.tsx +378 -0
- package/docs/data-entry/checkbox.tsx +256 -0
- package/docs/data-entry/color-picker.tsx +117 -0
- package/docs/data-entry/command-palette.tsx +179 -0
- package/docs/data-entry/command.tsx +262 -0
- package/docs/data-entry/country-picker-recipe.tsx +170 -0
- package/docs/data-entry/date-picker.tsx +191 -0
- package/docs/data-entry/date-range-picker.tsx +149 -0
- package/docs/data-entry/form/examples/employee-settings.tsx +267 -0
- package/docs/data-entry/form/examples/invoice-form.tsx +387 -0
- package/docs/data-entry/form-field/examples/a11y-contract.tsx +240 -0
- package/docs/data-entry/form-field/examples/create-form.tsx +105 -0
- package/docs/data-entry/form-field/index.md +8 -0
- package/docs/data-entry/form-field/index.tsx +326 -0
- package/docs/data-entry/form-inertia.tsx +80 -0
- package/docs/data-entry/form-root.tsx +102 -0
- package/docs/data-entry/form.tsx +724 -0
- package/docs/data-entry/input-otp.tsx +231 -0
- package/docs/data-entry/input.tsx +114 -0
- package/docs/data-entry/label.tsx +100 -0
- package/docs/data-entry/month-picker.tsx +101 -0
- package/docs/data-entry/month-range-picker.tsx +115 -0
- package/docs/data-entry/number-input.tsx +240 -0
- package/docs/data-entry/password-input.tsx +106 -0
- package/docs/data-entry/password-strength.tsx +97 -0
- package/docs/data-entry/radio-group.tsx +95 -0
- package/docs/data-entry/rating.tsx +87 -0
- package/docs/data-entry/search-input.tsx +120 -0
- package/docs/data-entry/select-async-contracts.tsx +108 -0
- package/docs/data-entry/select-matrix.tsx +204 -0
- package/docs/data-entry/select.tsx +369 -0
- package/docs/data-entry/slider.tsx +130 -0
- package/docs/data-entry/switch.tsx +67 -0
- package/docs/data-entry/tag-input.tsx +108 -0
- package/docs/data-entry/textarea.tsx +209 -0
- package/docs/data-entry/time-picker.tsx +99 -0
- package/docs/data-entry/toggle-count.tsx +293 -0
- package/docs/data-entry/toggle-group.tsx +225 -0
- package/docs/data-entry/toggle.tsx +127 -0
- package/docs/data-entry/transfer.tsx +97 -0
- package/docs/data-entry/tree-select.tsx +204 -0
- package/docs/data-entry/upload.tsx +184 -0
- package/docs/feedback/alert-dialog.tsx +270 -0
- package/docs/feedback/alert.tsx +248 -0
- package/docs/feedback/banner.tsx +183 -0
- package/docs/feedback/danger-confirm.tsx +88 -0
- package/docs/feedback/dialog-touch.tsx +24 -0
- package/docs/feedback/dialog.tsx +193 -0
- package/docs/feedback/sheet.tsx +410 -0
- package/docs/feedback/skeleton.tsx +111 -0
- package/docs/feedback/toast.tsx +135 -0
- package/docs/feedback/tooltip.tsx +171 -0
- package/docs/feedback/two-factor-setup.tsx +68 -0
- package/docs/foundation/_email-specimen.ts +274 -0
- package/docs/foundation/colors.tsx +202 -0
- package/docs/foundation/density.tsx +232 -0
- package/docs/foundation/email-tokens.tsx +306 -0
- package/docs/foundation/radius-shadow.tsx +152 -0
- package/docs/foundation/spacing.tsx +211 -0
- package/docs/foundation/typography.tsx +147 -0
- package/docs/general/activity.tsx +224 -0
- package/docs/general/button/examples/destructive-confirm.tsx +90 -0
- package/docs/general/button/examples/form-actions.tsx +85 -0
- package/docs/general/button/examples/toolbar.tsx +160 -0
- package/docs/general/button/index.md +12 -0
- package/docs/general/button/index.tsx +211 -0
- package/docs/general/logo.tsx +131 -0
- package/docs/general/reveal.tsx +41 -0
- package/docs/general/typography.tsx +152 -0
- package/docs/layout/account-chip.tsx +72 -0
- package/docs/layout/admin-collection.tsx +122 -0
- package/docs/layout/app-shell.tsx +374 -0
- package/docs/layout/aspect-ratio.tsx +119 -0
- package/docs/layout/auth-account-summary.tsx +103 -0
- package/docs/layout/auth-divider.tsx +28 -0
- package/docs/layout/auth-footer.tsx +37 -0
- package/docs/layout/auth-identity.tsx +36 -0
- package/docs/layout/auth-recovery/examples/mfa-challenge.md +41 -0
- package/docs/layout/auth-recovery/examples/mfa-challenge.tsx +254 -0
- package/docs/layout/auth-recovery/examples/mobile-390.md +25 -0
- package/docs/layout/auth-recovery/examples/mobile-390.tsx +82 -0
- package/docs/layout/auth-recovery/examples/password-recovery.md +24 -0
- package/docs/layout/auth-recovery/examples/password-recovery.tsx +197 -0
- package/docs/layout/auth-recovery/examples/wrapping-ja-en-vi.md +27 -0
- package/docs/layout/auth-recovery/examples/wrapping-ja-en-vi.tsx +156 -0
- package/docs/layout/auth-recovery/index.md +140 -0
- package/docs/layout/auth-recovery/index.tsx +96 -0
- package/docs/layout/auth-shell-context.tsx +103 -0
- package/docs/layout/auth-shell-device.tsx +112 -0
- package/docs/layout/auth-shell-registration.tsx +335 -0
- package/docs/layout/auth-shell-variants.tsx +139 -0
- package/docs/layout/auth-shell.tsx +59 -0
- package/docs/layout/auth-stack.tsx +40 -0
- package/docs/layout/centered-shell.tsx +102 -0
- package/docs/layout/error-surface/examples/application-403.tsx +84 -0
- package/docs/layout/error-surface/examples/application-404.tsx +77 -0
- package/docs/layout/error-surface/examples/system-500.tsx +47 -0
- package/docs/layout/error-surface/examples/system-503.tsx +48 -0
- package/docs/layout/error-surface/index.md +153 -0
- package/docs/layout/error-surface/index.tsx +239 -0
- package/docs/layout/flex.tsx +262 -0
- package/docs/layout/legal-document-shell/_data.ts +110 -0
- package/docs/layout/legal-document-shell/examples/desktop-1440.md +12 -0
- package/docs/layout/legal-document-shell/examples/desktop-1440.tsx +83 -0
- package/docs/layout/legal-document-shell/examples/mobile-390.md +22 -0
- package/docs/layout/legal-document-shell/examples/mobile-390.tsx +70 -0
- package/docs/layout/legal-document-shell/examples/tablet-1024.md +3 -0
- package/docs/layout/legal-document-shell/examples/tablet-1024.tsx +84 -0
- package/docs/layout/legal-document-shell/examples/wrapping-ja-en-vi.md +18 -0
- package/docs/layout/legal-document-shell/examples/wrapping-ja-en-vi.tsx +61 -0
- package/docs/layout/legal-document-shell/index.md +51 -0
- package/docs/layout/legal-document-shell/index.tsx +101 -0
- package/docs/layout/master-detail.tsx +387 -0
- package/docs/layout/org-switcher.tsx +131 -0
- package/docs/layout/page-container.tsx +946 -0
- package/docs/layout/resizable-panel.tsx +419 -0
- package/docs/layout/responsive-grid.tsx +235 -0
- package/docs/layout/separator.tsx +327 -0
- package/docs/layout/service-role-panel.tsx +144 -0
- package/docs/layout/sidebar.tsx +586 -0
- package/docs/layout/split-pane.tsx +451 -0
- package/docs/layout/topbar.tsx +347 -0
- package/docs/navigation/app-setting-picker.tsx +141 -0
- package/docs/navigation/breadcrumb.tsx +128 -0
- package/docs/navigation/context-menu.tsx +117 -0
- package/docs/navigation/dropdown-menu.tsx +213 -0
- package/docs/navigation/filter-bar.tsx +227 -0
- package/docs/navigation/menubar.tsx +141 -0
- package/docs/navigation/navigation-menu.tsx +165 -0
- package/docs/navigation/pagination.tsx +241 -0
- package/docs/navigation/steps.tsx +263 -0
- package/docs/navigation/tabs-rtl.tsx +28 -0
- package/docs/navigation/tabs.tsx +308 -0
- package/docs/navigation/toolbar.tsx +339 -0
- package/docs/providers/app-provider.tsx +191 -0
- package/docs/providers/format-date.tsx +230 -0
- package/docs/query/button-refetch.tsx +160 -0
- package/docs/query/data-state.tsx +192 -0
- package/docs/query/infinite-query-state.tsx +93 -0
- package/docs/query/mutation-feedback.tsx +70 -0
- package/docs/query/prefetch-link.tsx +262 -0
- package/docs/query/touch-actions.tsx +68 -0
- package/docs/showcase/acme-portal.tsx +441 -0
- package/docs/showcase/acme-website.tsx +455 -0
- package/docs/showcase/case1-warehouse-dashboard.tsx +343 -0
- package/docs/showcase/case2-employee-me.tsx +441 -0
- package/docs/showcase/case3-approval-workflow.tsx +527 -0
- package/docs/showcase/case4-login.tsx +307 -0
- package/docs/showcase/case5-shift-calendar.tsx +795 -0
- package/docs/showcase/case6-agency-handy.tsx +1072 -0
- package/docs/showcase/futurelastic-web.tsx +390 -0
- package/docs/showcase/org-switcher.tsx +313 -0
- package/docs/showcase/permission-matrix.tsx +343 -0
- package/docs/showcase/public-landing.tsx +210 -0
- package/docs/showcase/settings-account-sections.tsx +559 -0
- package/docs/showcase/settings-security-mfa.tsx +145 -0
- package/docs/showcase/table-approval-queue.tsx +199 -0
- package/docs/showcase/table-bulk-actions.tsx +460 -0
- package/docs/showcase/table-compact-kintone.tsx +438 -0
- package/docs/showcase/table-conditional-format.tsx +367 -0
- package/docs/showcase/table-crud-list.tsx +544 -0
- package/docs/showcase/table-density.tsx +340 -0
- package/docs/showcase/table-expandable-rows.tsx +471 -0
- package/docs/showcase/table-filter-chips.tsx +461 -0
- package/docs/showcase/table-footer-totals.tsx +311 -0
- package/docs/showcase/table-grouped-subtotals.tsx +388 -0
- package/docs/showcase/table-master-detail.tsx +479 -0
- package/docs/showcase/table-pagination.tsx +320 -0
- package/docs/showcase/table-states.tsx +363 -0
- package/docs/showcase/table-sticky-columns.tsx +373 -0
- package/docs/showcase/table-tree-rows.tsx +444 -0
- package/docs/showcase/table-view-tabs.tsx +488 -0
- package/package.json +9 -11
- package/scripts/_agent-setup.mjs +16 -2
- package/scripts/audit-hook.mjs +3 -13
- package/scripts/init-agent-kit.mjs +1 -14
- package/scripts/postinstall.mjs +12 -12
- package/scripts/ui-audit.mjs +93 -31
- package/scripts/visual-audit-rules.mjs +51 -7
- package/scripts/visual-audit.mjs +118 -47
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Development Guideline — `@godxjp/ui`
|
|
2
|
+
|
|
3
|
+
How to work **on** the design system (not just with it). If you only consume the framework from the app, read [the ten consumer rules](./CONSUMER-RULES.md) instead — this file is for people **editing `packages/godx-ui/`**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 0. What this package IS — and the boundary it must keep
|
|
8
|
+
|
|
9
|
+
`@godxjp/ui` is **the** UI framework for every godx surface (admin, agency portal, handheld). It is shared, versioned infrastructure: one change here ripples to every screen and every consumer. Two consequences:
|
|
10
|
+
|
|
11
|
+
1. **Editing it needs explicit session permission** — the hard gate in [CLAUDE.md](../CLAUDE.md) (MANDATORY: read the component skill before touching UI). Treat the package as off-limits by default. 2. **It is generic and presentational only.** The framework knows about _tokens, layout, accessibility, and interaction_ — never about the app's data, routes, language files, or business rules.
|
|
12
|
+
|
|
13
|
+
### The hard boundary — consumer-layer concerns MUST NOT leak in
|
|
14
|
+
|
|
15
|
+
A component in this package may **not** import or assume any of the following. These belong to the **consumer** (the app), which composes framework primitives around them:
|
|
16
|
+
|
|
17
|
+
| ❌ Never inside `packages/godx-ui/` | ✅ Where it belongs |
|
|
18
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
19
|
+
| App i18n — `useTranslation`, `t()`, `resources/js/i18n` | App passes already-translated strings in as props |
|
|
20
|
+
| Inertia — `router`, `usePage`, `<Form>`, `@inertiajs/*` | App wires navigation/submit; pass `onClick`/`href`/`onSubmit` props |
|
|
21
|
+
| Wayfinder routes — `@/routes`, `@/actions`, `.url()` | App resolves the URL, passes a plain string |
|
|
22
|
+
| Business entities / domain logic (Item, Packing, shipment rules) | App-level component in `resources/js/components/**` |
|
|
23
|
+
| App copy / labels / placeholder text baked into the component | Comes in via props; framework ships no product wording |
|
|
24
|
+
| Raw colors / hex / `hsl(...)` literals, `dark:` overrides | Semantic tokens (`var(--…)`, `bg-primary`, …) — see §3 |
|
|
25
|
+
| A consumer's theme values redefined inside a component | Theme lives in `src/tokens/` (the framework default ships it) |
|
|
26
|
+
|
|
27
|
+
> **Litmus test:** if a component can't render in a Storybook-style preview with
|
|
28
|
+
> plain props and zero app context, it does **not** belong in this package. Keep it
|
|
29
|
+
> app-level (`resources/js/components/admin/…`) and have it _compose_ framework
|
|
30
|
+
> primitives. See the **`godx-ui-component-placement`** skill for the full decision.
|
|
31
|
+
|
|
32
|
+
The framework ships its **own theme** (colors, fonts, type scale, wa-iro palette) in `src/tokens/foundation.css`, so consumers import `@godxjp/ui/styles` and need **zero** extra theme config. Do not push theme decisions back onto the consumer.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 1. Architecture — the layers, bottom-up
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
src/tokens/ Design tokens (the single source of values)
|
|
40
|
+
foundation.css :root + .dark — colors, fonts, type scale, spacing, radius, wa-iro
|
|
41
|
+
primitives/*.css Per-domain primitive tokens (card, table, control, badge, …)
|
|
42
|
+
base.css Coordinates the token layer
|
|
43
|
+
src/styles/ CSS that styles components by [data-slot] + density.css (the density knob)
|
|
44
|
+
index.css Entry: @import fontsource → tailwindcss → @theme (token→utility map) → layout css
|
|
45
|
+
src/components/ React components, grouped (data-display, data-entry, layout, …)
|
|
46
|
+
src/props/ Prop type system — vocabulary/ (atomic) + components/ + registry.ts
|
|
47
|
+
src/lib/ cn(), control-styles, variants — shared helpers
|
|
48
|
+
examples/ *.preview.tsx — Storybook-style stories (preview app)
|
|
49
|
+
docs/primitives/ <component>/index.tsx demo + examples/ + generated .md
|
|
50
|
+
preview/ The preview app (vite) on :6008 that renders examples + docs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Token → utility flow:** a value is defined once as a CSS var in `tokens/foundation.css` (e.g. `--primary`), mapped to a Tailwind utility in the `@theme` block of `styles/index.css` (`--color-primary: hsl(var(--primary))`), and consumed as `bg-primary` / `hsl(var(--primary))`. Never skip a layer with a literal.
|
|
54
|
+
|
|
55
|
+
### The component pattern — markup emits slots, CSS owns styling
|
|
56
|
+
|
|
57
|
+
Components render semantic structure and `data-slot` / `data-*` flags; **the spacing, padding, and chrome live in `src/styles/*-layout.css`**, keyed on those slots. This keeps density and theming centralized.
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// component: emits slots + flags only
|
|
61
|
+
<div data-slot="card" data-variant={variant} data-density={density} />
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```css
|
|
65
|
+
/* styles/card-layout.css: owns the look */
|
|
66
|
+
[data-slot="card"] {
|
|
67
|
+
border: 1px solid hsl(var(--card-border));
|
|
68
|
+
border-radius: var(--card-radius);
|
|
69
|
+
}
|
|
70
|
+
[data-slot="card"][data-density="tight"] {
|
|
71
|
+
--card-space-inset: var(--space-3);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Prefer this over hardcoding Tailwind padding inside the component. Use Tailwind utility classes for **one-off layout** (flex/grid/gap), not for re-theming.
|
|
76
|
+
|
|
77
|
+
### Density
|
|
78
|
+
|
|
79
|
+
One knob — `.ui-density-{compact,default,comfortable}` in `styles/density.css` — retunes `--phi-unit`, control heights, and table row heights together. Components read the resulting tokens; never branch on density in component JS.
|
|
80
|
+
|
|
81
|
+
### The `ui/` layer
|
|
82
|
+
|
|
83
|
+
`src/components/ui/*` are thin **re-exports** of the canonical implementation (`export * from "../data-display/card"`). They exist for shadcn-style import paths. Edit the canonical file under its group; the `ui/` path follows automatically.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 2. Adding or changing a component (after you have §0 permission)
|
|
88
|
+
|
|
89
|
+
Work in this order; only advance when the previous step genuinely can't express the need:
|
|
90
|
+
|
|
91
|
+
1. **Use** an existing primitive. 2. **Compose** primitives (Card + FormField + Flex…). 3. **Extend** an existing component — add a prop/slot (e.g. `labelAddon`, `accent`). Prefer this: one more prop beats one more component. 4. **Create** a new component — last resort.
|
|
92
|
+
|
|
93
|
+
Document the decision (which promotion criteria it met) so review can check it — see [COMPOSITION-VS-COMPONENT.md](./COMPOSITION-VS-COMPONENT.md).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 3. Rules for framework code
|
|
98
|
+
|
|
99
|
+
- **Semantic tokens only.** No raw hex / `rgb()` / `hsl()` literals, no palette utilities (`bg-blue-500`), no `dark:` overrides — tokens adapt automatically. Structural literals (a `3px` accent stripe, `1px` borders) are fine; _color/size scale_ values come from tokens. - **No app coupling** (the §0 boundary table above). - **Props live in `src/props/`** — atomic concepts in `vocabulary/`, per-component interfaces in `components/`. Check `registry.ts` + `PROP_ALIASES_FORBIDDEN` before inventing a prop name.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 4. Every component change ships its story + docs
|
|
104
|
+
|
|
105
|
+
A change isn't done until its documentation reflects it:
|
|
106
|
+
|
|
107
|
+
1. **Preview story** — `examples/<group>/<Component>.preview.tsx` (Storybook-style). New props get a story (see `examples/data-display/Card.preview.tsx`: Surfaces / Density / AccentEdges). 2. **Docs demo** — `docs/primitives/<group>/<component>/index.tsx` shows the new capability; `examples/` holds focused per-feature demos. 3. **Regenerate props docs** — `pnpm docs:sync-primitives` regenerates the `.md` from source. Run it after prop changes so the tables stay accurate.
|
|
108
|
+
|
|
109
|
+
> Docs are mid-migration to the `<component>/index.tsx (+ examples/)` shape. Do **not**
|
|
110
|
+
> resurrect flat `docs/primitives/<component>.tsx` demos — they were dead orphans and
|
|
111
|
+
> were removed.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 5. Verify before finishing
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pnpm lint # eslint — self-contained flat config
|
|
119
|
+
pnpm typecheck # tsc --noEmit
|
|
120
|
+
pnpm test # vitest per component
|
|
121
|
+
pnpm preview:build # integration test: examples + docs must build
|
|
122
|
+
pnpm audit # godxjp-ui-audit — 0 errors for touched files
|
|
123
|
+
pnpm check:mcp-sync # MCP registry ↔ library export drift guard
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`pnpm verify` and `pnpm verify:release` run these together (verify:release also builds). It needs `pnpm exec playwright install chromium` once locally; see [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) for how to run/scope it, read the evidence, and regenerate its baseline after an accessibility fix.
|
|
127
|
+
|
|
128
|
+
All gates are **self-contained** — no internal/external tooling package required. The eslint, prettier, and vitest setup live in the package (`eslint.config.js`, `prettier.config.mjs`, `vitest.config.ts`, `src/test/`), so a fresh checkout can lint/type-check/test without anything beyond the declared devDependencies.
|
|
129
|
+
|
|
130
|
+
The app side additionally runs **`npm run ui:audit`** (the design-system linter) and must report 0 errors for touched files.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 6. Releasing — the lib and its MCP, in lockstep
|
|
135
|
+
|
|
136
|
+
This repo publishes **two packages** that must agree: `@godxjp/ui` (the browser component library, root `package.json`) and `@godxjp/ui-mcp` (the Node MCP server that tells agents how to use it, `mcp/`).
|
|
137
|
+
|
|
138
|
+
### The lockstep contract (issue #140)
|
|
139
|
+
|
|
140
|
+
A consumer that installs `@godxjp/ui@16.10.x` but points its agent at `@godxjp/ui-mcp@16.7.x` gets prop/token/pattern guidance for a build it never installed. To make that impossible, the two packages carry **mutual, machine-readable compatibility metadata**:
|
|
141
|
+
|
|
142
|
+
- root `package.json` → `"godxUiMcp": "<mcp version>"` — the catalog version this library ships with.
|
|
143
|
+
- `mcp/package.json` → `"version"` (identical to the library) **and** `"godxUiCompatibility": "<maj>.<min>.x"` — the UI minor this catalog describes.
|
|
144
|
+
- the MCP exposes it at runtime: `serverInfo.version` == package version, the `godx-ui://compatibility` resource, and the `check_compatibility` tool (an agent calls it with the app's installed `@godxjp/ui` version to get a match/mismatch verdict).
|
|
145
|
+
|
|
146
|
+
- **Lockstep guard** (`pnpm check:mcp-lockstep` → `scripts/check-release-lockstep.mjs`) — asserts ui.version == mcp.version, ui.godxUiMcp == mcp.version, and mcp.godxUiCompatibility covers the UI version. Any split fails CI. - **Catalog drift guard** (`pnpm check:mcp-sync` / `check:mcp-orphans`) — every component the MCP catalogs must still be a real library export, and vice-versa. - **Packed-artifact guard** (`release-integrity` CI, step 4) — packs both tarballs and re-asserts lockstep on the **packed** manifests, i.e. the bytes that would actually ship.
|
|
147
|
+
|
|
148
|
+
### Coordinated release (`pnpm release`)
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pnpm release --ui minor --mcp sync # bump ui, republish mcp at the SAME new version — the norm
|
|
152
|
+
pnpm release --mcp sync # mcp-only fix at the current ui version
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`--ui <bump>` **requires** `--mcp sync` (the tool refuses a ui-only release) so the two packages can never split. On a bump the tool refreshes both compatibility fields, runs `verify:release` (incl. all three guards) before publishing the lib, `npm publish`es each package, re-runs the lockstep check as a final fail-closed gate, then commits the version bumps.
|
|
156
|
+
|
|
157
|
+
### Recovery from a partial publish
|
|
158
|
+
|
|
159
|
+
If a run publishes `@godxjp/ui` but the `@godxjp/ui-mcp` publish fails (npm hiccup, expired token), the two are momentarily split on npm. Recover:
|
|
160
|
+
|
|
161
|
+
1. Fix the cause (e.g. re-auth `npm whoami`), keep the working tree at the just-published version. 2. Re-run **only** the MCP half at that same version: `pnpm --dir mcp build && npm --prefix mcp version <ui-version> --allow-same-version && npm --prefix mcp publish --access public`. 3.
|
|
162
|
+
|
|
163
|
+
npm versions are immutable, so you can never "fix" a bad published version in place — you can only publish the matching partner at the same number, or move both forward with a fresh `pnpm release --ui patch --mcp sync`. Never hand-publish one package and forget the other.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## See also
|
|
168
|
+
|
|
169
|
+
- [README](../README.md) — overview, component groups, consumer setup.
|
|
170
|
+
- [CONSUMER-RULES.md](./CONSUMER-RULES.md) — the ten rules every consumer follows (the audit enforces them).
|
|
171
|
+
- `docs/TOKENS.md`, `docs/SPACING.md`, `docs/PROPS-VOCABULARY.md`, `docs/PROPS-REGISTRY.md`.
|
|
172
|
+
- [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) — per-frame axe a11y + geometry + coverage CI gates.
|
|
173
|
+
- **`godx-ui-component-placement`** skill — decide whether a component belongs here.
|
package/docs/FORMS.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Forms — react-hook-form + Zod 4 (mandatory)
|
|
2
|
+
|
|
3
|
+
All forms in admin apps **must** use:
|
|
4
|
+
|
|
5
|
+
| Package | Version | Role |
|
|
6
|
+
| --------------------- | ------- | -------------------- |
|
|
7
|
+
| `react-hook-form` | ^7.76 | Form state |
|
|
8
|
+
| `zod` | ^4.4 | Schema validation |
|
|
9
|
+
| `@hookform/resolvers` | ^5.2 | `zodResolver` bridge |
|
|
10
|
+
|
|
11
|
+
Import from `@godxjp/ui/form` — **never** call `useForm()` without `zodResolver`.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { useZodForm, FormRoot, FormFieldControl } from "@godxjp/ui/form";
|
|
18
|
+
import { Input } from "@godxjp/ui/data-entry";
|
|
19
|
+
import { Button } from "@godxjp/ui/general";
|
|
20
|
+
|
|
21
|
+
const schema = z.object({
|
|
22
|
+
name: z.string().min(1, "Bắt buộc"),
|
|
23
|
+
email: z.string().email("Email không hợp lệ"),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type Values = z.infer<typeof schema>;
|
|
27
|
+
|
|
28
|
+
export function CreateCustomerForm({ onSubmit }: { onSubmit: (v: Values) => void }) {
|
|
29
|
+
const form = useZodForm(schema, { defaultValues: { name: "", email: "" } });
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<FormRoot form={form} onSubmit={onSubmit}>
|
|
33
|
+
<FormFieldControl name="name" label="Tên" required>
|
|
34
|
+
{(field) => <Input {...field} value={String(field.value ?? "")} />}
|
|
35
|
+
</FormFieldControl>
|
|
36
|
+
<FormFieldControl name="email" label="Email" required>
|
|
37
|
+
{(field) => <Input {...field} type="email" value={String(field.value ?? "")} />}
|
|
38
|
+
</FormFieldControl>
|
|
39
|
+
<Button type="submit">Lưu</Button>
|
|
40
|
+
</FormRoot>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
| Export | Prop type | Purpose |
|
|
48
|
+
| ------------------ | ----------------------------------------- | -------------------------------------- |
|
|
49
|
+
| `useZodForm` | `ZodSchemaProp` + `UseZodFormOptionsProp` | Hook — injects zodResolver |
|
|
50
|
+
| `FormRoot` | `FormRootProp` | FormProvider + `<form onSubmit>` |
|
|
51
|
+
| `FormFieldControl` | `FormFieldControlProp` | Controller + FormField + error display |
|
|
52
|
+
|
|
53
|
+
## Forbidden
|
|
54
|
+
|
|
55
|
+
- ❌ `useForm()` without Zod resolver
|
|
56
|
+
- ❌ `useState` for form field values in admin pages
|
|
57
|
+
- ❌ Manual validation with `if (!email.includes('@'))`
|
|
58
|
+
- ❌ Yup, Valibot, or other schema libraries
|
|
59
|
+
- ❌ Uncontrolled forms without schema
|
|
60
|
+
|
|
61
|
+
## App dependencies
|
|
62
|
+
|
|
63
|
+
Each app `package.json` must include:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"react-hook-form": "^7.76.0",
|
|
69
|
+
"zod": "^4.4.3",
|
|
70
|
+
"@hookform/resolvers": "^5.2.2"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Peer-enforced by `@godxjp/ui`.
|
|
76
|
+
|
|
77
|
+
## Server error bags — `Form errors` + `FormField name` + `<FormErrors />`
|
|
78
|
+
|
|
79
|
+
Server-driven forms (Inertia's `useForm`) return a Laravel error bag whose keys may include
|
|
80
|
+
**hidden/derived fields** (`action_mode`, `page`, a source-record id) that no visible field can
|
|
81
|
+
display — without a summary the submit fails silently. Pass the WHOLE bag once and let fields
|
|
82
|
+
claim their own keys:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Form, FormErrors, FormField, Input } from "@godxjp/ui/data-entry";
|
|
86
|
+
import { useForm } from "@inertiajs/react";
|
|
87
|
+
|
|
88
|
+
const form = useForm({ customer_nm: "", action_mode: "regist" });
|
|
89
|
+
|
|
90
|
+
<Form asChild layout="horizontal" labelWidth={140} errors={form.errors}>
|
|
91
|
+
<form onSubmit={submit}>
|
|
92
|
+
<FormErrors />
|
|
93
|
+
<FormField name="customer_nm" label="顧客名" required>
|
|
94
|
+
<Input
|
|
95
|
+
value={form.data.customer_nm}
|
|
96
|
+
onChange={(e) => form.setData("customer_nm", e.target.value)}
|
|
97
|
+
/>
|
|
98
|
+
</FormField>
|
|
99
|
+
</form>
|
|
100
|
+
</Form>;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- A `FormField name="…"` resolves its message from `errors[name]` automatically (an explicit
|
|
104
|
+
`error` prop wins; a `string[]` entry surfaces its FIRST message — `$errors->first()`), and
|
|
105
|
+
**claims** the key.
|
|
106
|
+
- `<FormErrors />` renders only the **unclaimed** remainder as a destructive `Alert`
|
|
107
|
+
(`role="alert"`, localized default title) — and nothing when every entry is claimed.
|
|
108
|
+
- Never hand-filter the bag per page; that except-list is exactly what this mechanism removes.
|
|
109
|
+
- `FormFieldControl` forwards its `name`, so `FormRoot` + adapter fields claim their keys too;
|
|
110
|
+
give `<FormErrors errors={form.errors} />` the bag explicitly when there is no surrounding
|
|
111
|
+
`Form errors`.
|
|
112
|
+
|
|
113
|
+
### Sibling Forms — one bag over several Card+Form sections
|
|
114
|
+
|
|
115
|
+
An edit screen split into several sibling `Card` + `Form` sections still has ONE server bag.
|
|
116
|
+
Wrap the region in `FormErrorsProvider` instead of passing `errors` to each Form — a Form
|
|
117
|
+
**without** its own `errors` joins the surrounding registry, so claims from every section
|
|
118
|
+
subtract from the same `<FormErrors />`:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { Form, FormErrors, FormErrorsProvider, FormField, Input } from "@godxjp/ui/data-entry";
|
|
122
|
+
|
|
123
|
+
<FormErrorsProvider errors={form.errors}>
|
|
124
|
+
<FormErrors />
|
|
125
|
+
<Card>
|
|
126
|
+
<Form layout="horizontal" labelWidth={170}>
|
|
127
|
+
<FormField name="customer_nm" label="顧客名">
|
|
128
|
+
…
|
|
129
|
+
</FormField>
|
|
130
|
+
</Form>
|
|
131
|
+
</Card>
|
|
132
|
+
<Card>
|
|
133
|
+
<Form layout="horizontal" labelWidth={170}>
|
|
134
|
+
<FormField name="mail_subject" label="件名">
|
|
135
|
+
…
|
|
136
|
+
</FormField>
|
|
137
|
+
</Form>
|
|
138
|
+
</Card>
|
|
139
|
+
</FormErrorsProvider>;
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A nested Form **with** its own `errors` starts a new registry that shadows the provider — its
|
|
143
|
+
claims and messages stay inside it (use this for an embedded sub-form with a separate bag).
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Per-frame accessibility & coverage CI
|
|
2
|
+
|
|
3
|
+
They render every catalog frame in real Chromium and hold the line on accessibility, responsive geometry and contract coverage. Static checks (`audit:examples`, source regexes) cannot see rendered colour/layout/ARIA — these gates do.
|
|
4
|
+
|
|
5
|
+
## The three gates
|
|
6
|
+
|
|
7
|
+
| Script | `pnpm` | Drives | Blocking? |
|
|
8
|
+
| ----------------------------- | ---------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
9
|
+
| `scripts/check-frame-axe.mjs` | `check:frame-axe` | axe over **every frame** at desktop 1440×900 + mobile-sm 375×667 | **Preview-chrome: yes.** Component/demo: allowlisted (regression = fail) |
|
|
10
|
+
| `scripts/frame-geometry.mjs` | `check:frame-geometry` | overflow + clipped-control sweep across **320·375·390·768·1024·1280·1440·1920** | Allowlisted (regression = fail) |
|
|
11
|
+
| `scripts/frame-coverage.mjs` | `check:frame-coverage` | component-inventory ↔ frame ↔ contract-axis cross-check (browser-free) | Report-only (`--strict` fails on any zero-frame component) |
|
|
12
|
+
|
|
13
|
+
`check:frame-geometry` works identically for responsive overflow (see its own header comment); `check:frame-coverage` is covered briefly at the end and fully in [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
|
|
14
|
+
|
|
15
|
+
### What geometry counts as a "clipped control"
|
|
16
|
+
|
|
17
|
+
A control the **user cannot bring into the frame** — not merely one whose box currently sticks out. Everything inside a deliberately scrollable surface sticks out by design (a `DataTable`'s `.ui-data-table-scroll` at 320px, a `FilterBar overflow="scroll"` strip, a tabs list), and those controls are reachable: a control entirely out of the scrollport is scrolled fully in the moment it takes keyboard focus, and a partially visible one keeps its focus ring on screen (Chromium's `CenterIfNeeded` focus alignment does not move a partially visible target — browser behaviour, and still inside 2.4.7 / 2.4.11 AA). So an out-of-frame focusable is re-probed: the sweep scrolls **only** its user-scrollable ancestors (computed `overflow: auto|scroll|overlay` that actually has scroll room — never `hidden`/`clip`, which no user can scroll) and re-measures.
|
|
18
|
+
|
|
19
|
+
## Why a per-frame gate, not the old hand-picked checks
|
|
20
|
+
|
|
21
|
+
- `check:contrast` (`scripts/check-contrast.mjs`) — a hand-picked set of ~6 pages checked for WCAG-AA text contrast. It never saw the other ~100 component frames. - `audit:examples` (`pnpm audit docs`) — a static source-regex linter. It can catch a hard-coded hex value but cannot see what actually renders: computed contrast, DOM landmark structure, focus order, or anything a Radix portal produces at runtime.
|
|
22
|
+
|
|
23
|
+
## Chrome vs component: two independently-tracked scopes
|
|
24
|
+
|
|
25
|
+
`check:frame-axe` runs axe **twice** per frame/viewport:
|
|
26
|
+
|
|
27
|
+
- **Preview chrome** = the zoom / dimension **toolbar** (`.demo-block-toolbar`, `preview/src/demo-block.tsx`). We own this. It **must be 0** — a chrome violation fails the build.
|
|
28
|
+
|
|
29
|
+
## The component allowlist — and how it shrinks to zero
|
|
30
|
+
|
|
31
|
+
`scripts/frame-axe.baseline.json` is the allowlist of **pre-existing** component violations, keyed `frame → [axe-rule-id]` — the **set of violation types** still firing on each frame (union across desktop + mobile). We key on rule _presence_, not exact node counts, because axe counts jitter run-to-run as a Radix portal (Dialog/Popover/Sheet) opens or closes — an exact-count gate would be flaky. `--update-baseline` unions **two passes** by default (`AXE_RUNS`) so an intermittently-mounted overlay is captured.
|
|
32
|
+
|
|
33
|
+
- **fails** if a frame gains a rule **not** in its baseline set (a genuinely new violation type);
|
|
34
|
+
- **passes** rule sets equal to or a subset of baseline, printing exactly what remains (nothing hidden);
|
|
35
|
+
|
|
36
|
+
The baseline can **only shrink**.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
git pull # get the merged component fixes
|
|
40
|
+
pnpm check:frame-axe --update-baseline # re-snapshot: rule sets only shrink
|
|
41
|
+
git add scripts/frame-axe.baseline.json && git commit -m "chore(a11y): tighten frame-axe baseline"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
There is **no blanket suppression**: a documented axe false positive would be an explicit per-rule entry with a comment, never a mute.
|
|
45
|
+
|
|
46
|
+
`0` chrome violations, `0` regressions. See [Remaining baseline debt](#remaining-baseline-debt--the-12-frames) below for exactly what's left and the path to zero.
|
|
47
|
+
|
|
48
|
+
## Running it locally
|
|
49
|
+
|
|
50
|
+
### Prerequisites
|
|
51
|
+
|
|
52
|
+
`playwright` and `@axe-core/playwright` are **optional peers** — `check:frame-axe` detects a missing/unbuilt peer and skips with a warning instead of failing the build (so a browser-less environment, e.g. a docs-only contributor, never gets blocked). To actually run it:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pnpm install
|
|
56
|
+
pnpm exec playwright install chromium # local dev — downloads Playwright's managed Chromium
|
|
57
|
+
# CI uses: pnpm exec playwright install --with-deps chromium (adds OS-level deps)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`frame-harness.mjs` looks for a Chromium binary at `PLAYWRIGHT_CHROMIUM_EXECUTABLE`, defaulting to a **CI-only** path (`/opt/pw-browsers/chromium-1228/chrome-linux64/chrome`). That path won't exist on a laptop — when it's absent the harness falls back to Playwright's own managed browser (from `playwright install` above), so a plain local checkout works without setting anything.
|
|
61
|
+
|
|
62
|
+
### Commands
|
|
63
|
+
|
|
64
|
+
| Command | What it does |
|
|
65
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `pnpm check:frame-axe` | The gate: 1 pass over every frame × 2 viewports, compares to the baseline, exits non-zero on fail |
|
|
67
|
+
| `AXE_FRAMES_LIMIT=8 pnpm check:frame-axe` | Quick local smoke — only the first 8 frames (manifest order) |
|
|
68
|
+
| `pnpm check:frame-axe --format json` | Machine-readable result on stdout (same exit-code semantics) — no colored human report |
|
|
69
|
+
| `pnpm check:frame-axe --update-baseline` | Regenerate the allowlist (2-pass union by default) — see below |
|
|
70
|
+
| `AXE_RUNS=3 pnpm check:frame-axe --update-baseline` | More union passes, for a component with flaky portal timing |
|
|
71
|
+
| `pnpm check:frame-axe http://localhost:6008` | Point at an already-running preview instead of building+serving one |
|
|
72
|
+
|
|
73
|
+
If no preview server answers at the base URL (`http://localhost:6008` by default), the script builds one itself: `pnpm preview:build` then serves the static output with `vite preview` (`ensurePreviewServer` in `scripts/frame-harness.mjs`) — deterministic and stable under a long headless sweep, unlike the dev server's per-request recompilation. If a `pnpm preview` dev server (or a remote base) is already reachable, it's reused as-is and never rebuilt.
|
|
74
|
+
|
|
75
|
+
### Reading the output
|
|
76
|
+
|
|
77
|
+
Terminal report (human mode) has two blocks:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Preview-chrome axe (blocking — must be 0):
|
|
81
|
+
✓ 0 chrome violations across all 107 frame(s).
|
|
82
|
+
|
|
83
|
+
Component/demo axe (allowlisted — baseline may only shrink):
|
|
84
|
+
47 component violation node(s) remaining across 12 frame(s).
|
|
85
|
+
✓ no new component violation types (rule-set within baseline).
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- A **red `✗ <rule>`** under chrome is always blocking.
|
|
89
|
+
- A **red `✗ N NEW violation-type(s) not in baseline`** under component means a frame gained a
|
|
90
|
+
rule the baseline didn't have — that's the actual CI failure mode for a component regression.
|
|
91
|
+
- A **yellow `↓ shrink hint`** means a baseline rule no longer fires anywhere it's listed — free
|
|
92
|
+
baseline tightening available via `--update-baseline`.
|
|
93
|
+
|
|
94
|
+
### Evidence JSON
|
|
95
|
+
|
|
96
|
+
Every non-`--update-baseline` run writes `audit-evidence/frame-axe/frame-axe-results.json`
|
|
97
|
+
(gitignored — a CI artifact, uploaded by `frame-a11y.yml` under the `frame-axe-evidence` name).
|
|
98
|
+
Shape:
|
|
99
|
+
|
|
100
|
+
```jsonc
|
|
101
|
+
{
|
|
102
|
+
"generatedAt": "2026-07-13T00:45:34.614Z",
|
|
103
|
+
"base": "http://localhost:6008",
|
|
104
|
+
"summary": {
|
|
105
|
+
"status": "ok", // "ok" | "fail"
|
|
106
|
+
"frames": 107,
|
|
107
|
+
"viewports": ["desktop", "mobile"],
|
|
108
|
+
"chromeViolations": 0, // total chrome violation NODE count
|
|
109
|
+
"componentViolations": 47, // total component violation NODE count (reporting only)
|
|
110
|
+
"componentRegressions": 0, // rules present now but not in the baseline
|
|
111
|
+
"infrastructureErrors": 0, // frames that failed to load / axe crashed
|
|
112
|
+
},
|
|
113
|
+
"chrome": {/* rule → { impact, nodes, frames: ["<frameId>@<viewport>", …] } */},
|
|
114
|
+
"componentCurrent": {/* frameId → viewport → ruleId → node count, this run */},
|
|
115
|
+
"regressions": [/* { frame, rule } — new rules, the actual gate failure */],
|
|
116
|
+
"shrinkHints": [/* { frame, rule } — baseline rules that no longer fire */],
|
|
117
|
+
"infraErrors": [/* { frame, viewport, message } */],
|
|
118
|
+
"results": [/* per-frame { id, viewports: { desktop: { chrome, component }, mobile: {…} } } */],
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Node counts are recorded for triage but are **not** what the gate compares — the gate compares
|
|
123
|
+
rule _presence_ per frame (`currentRules` vs `baseline.component[frame]`), because node counts
|
|
124
|
+
jitter with portal open/close timing (see "The component allowlist" above). The evidence file does
|
|
125
|
+
**not** carry the underlying axe `nodes[].any[].data` (e.g. the exact contrast ratio or CSS
|
|
126
|
+
selector) — `scan()` in `check-frame-axe.mjs` only keeps `{ id, impact, nodes: v.nodes.length }`.
|
|
127
|
+
To see the raw axe payload for a specific violation while triaging, run `@axe-core/playwright`
|
|
128
|
+
directly against `http://localhost:6008/frame/<id>` in a small throwaway script, or use the
|
|
129
|
+
Playwright Inspector / browser devtools with the `axe-core` extension against the same URL.
|
|
130
|
+
|
|
131
|
+
### Regenerating / shrinking the baseline after a component fix
|
|
132
|
+
|
|
133
|
+
1. Fix the root cause in the component (or the docs demo, if the violation is demo-content-only —
|
|
134
|
+
see the categories below for which is which).
|
|
135
|
+
2. `pnpm check:frame-axe --update-baseline` — re-snapshots every frame's current rule set (2-pass
|
|
136
|
+
union). Frames that are now clean are **removed** from the JSON entirely; frames whose rule set
|
|
137
|
+
shrank keep only what still fires.
|
|
138
|
+
3. `git diff scripts/frame-axe.baseline.json` — confirm the diff is a **strict shrink** (no rule
|
|
139
|
+
should reappear on a frame that previously didn't have it — if one does, you introduced a new
|
|
140
|
+
violation, not fixed an old one).
|
|
141
|
+
4. Commit the baseline alongside the fix in the same PR.
|
|
142
|
+
|
|
143
|
+
`AXE_FRAMES_LIMIT` + `--update-baseline` together only touch the frames actually run — the merge
|
|
144
|
+
logic in `check-frame-axe.mjs` overwrites `runFrameIds` and leaves every other frame's baseline
|
|
145
|
+
entry untouched, so a scoped re-snapshot (e.g. after fixing just `Select`) never silently drops
|
|
146
|
+
unrelated frames from the allowlist.
|
|
147
|
+
|
|
148
|
+
## Violation categories & how each was root-caused (reference)
|
|
149
|
+
|
|
150
|
+
The original #157 audit's 101-frame baseline collapsed to 12 through structural fixes, not
|
|
151
|
+
whack-a-mole per-frame patches. Each category below is a pattern — recognize it the next time a
|
|
152
|
+
new frame trips the same rule.
|
|
153
|
+
|
|
154
|
+
| Category | axe rule(s) | Root cause | Fix pattern |
|
|
155
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
156
|
+
| **Heading order** | `heading-order` | Every docs demo nests `PageContainer`'s `<h1>` directly around sibling `<Card>` sections whose `CardTitle` defaulted to `level={3}` (`<h3>`) — skipping `<h2>` (91 frames). | AST codemod bumped every **top-level** `CardTitle` to `level={2}`; nested Card-in-Card titles correctly kept the `h3` default (`CardTitle`'s own jsdoc already documents this contract). Commit `030e82a`. |
|
|
157
|
+
| **Landmark — main** | `landmark-one-main`, `landmark-no-duplicate-main`, `landmark-main-is-top-level` | `preview/src/frame-main.tsx` hardcoded a `<main>` around every frame. A story that renders its own page shell (`AppShell`/`AuthShell`, which already emit their own `<main>`) got a nested/duplicate main. Story metadata can't disambiguate — every catalog entry defaults to `layout: "fullscreen"` regardless of content. | `FrameLandmark` (`preview/src/frame-main.tsx`) detects it **after mount**: `useLayoutEffect` checks `node.querySelector('main, [role="main"]')` — stays a plain `<div>` if the subtree already owns a main, otherwise renders the real `<main>` **tag** itself (not `role="main"` — several axe landmark checks walk ancestors by native tag name, not computed role). Commit `030e82a`, `281c8d4`. |
|
|
158
|
+
| **Landmark — banner (chrome)** | `landmark-no-duplicate-banner` | The demo toolbar was a candidate `<header>`, which resolves to an implicit `banner` landmark whenever the rendered story also provides its own (`AppShell` demos). | `demo-block.tsx`'s toolbar is a plain `<div className="demo-block-toolbar">`, never a `<header>` — it's preview chrome, not page banner content (see the comment right above it in the file). |
|
|
159
|
+
| **Landmark — uniqueness** | `landmark-unique` | `Pagination`, `Breadcrumb` (standalone + `PageContainer`'s built-in slot), and `Carousel` each hardcoded ONE localized `aria-label` with no override. Fine for a real screen (one instance); the component-gallery docs stack several instances of the same component on one page, so same-role + same-name landmarks become indistinguishable in a screen-reader landmark list (WCAG 2.4.1 / 1.3.1). | Added an `aria-label` override prop (`PaginationProp`, `BreadcrumbProps`, `PageContainer`'s `breadcrumbAriaLabel`); every repeated docs instance now passes a distinguishing label. Also fixed `PageContainer`'s breadcrumb slot to route its label through `t()` instead of a hardcoded English `"Breadcrumb"` literal (an i18n-contract bug found in the same pass). Commit `741ccf5`. |
|
|
160
|
+
| **Scrollable region focusable** | `scrollable-region-focusable` | An `overflow: auto`/`overflow-x: auto` container was in the tab order for mouse users but unreachable by keyboard (WCAG 2.1.1). Hit three places: `.demo-block-canvas`/`.demo-block-frame` (the preview harness's own scroll wrappers), and the `Table` primitive's/`DataTable`'s/`ScrollArea`'s overflow wrappers. | Add `tabIndex={0}` to the scrolling element — **no `role="region"`**: a landmark role there would break `landmark-*-is-top-level` for any story that renders its own regions (e.g. `AppShell` demos). See the inline comments in `demo-block.tsx` (`.demo-block-canvas`) and `src/components/data-display/table.tsx`. Commits `030e82a`, `8675570`. |
|
|
161
|
+
| **List semantics** | `listitem` | `docs/data-display/list-row.tsx` rendered two `<ListRow as="li">` siblings directly inside `CardContent` (a plain `<div>`) — axe requires an `<li>`'s parent to actually be a list container (`ul`/`ol`/`role="list"`). `ListRow`'s own jsdoc already documents `as="li"` for "when the rows are a semantic list" — the demo just hadn't supplied the wrapping list it implied. | Wrap the `<li>` rows in a real `<ul>` in the demo. Commit `30d22f1`. |
|
|
162
|
+
| **Accessible name — button** | `button-name` | A `<button>` (native or a `SelectTrigger`) with no discernible text (WCAG 4.1.2). | Case-by-case: give the control real text, an `aria-label`, or route it through `FormField` (see below). Remaining `button-name` entries all trace to the `SelectTrigger`/`DataSelect` prop-forwarding gap fixed by #177 — see [Remaining baseline debt](#remaining-baseline-debt--the-12-frames). |
|
|
163
|
+
| **Accessible name — form control / label** | (implicit via `FormField` wiring, WCAG 1.3.1/4.1.2) | A control inside `FormField` was named only via `aria-labelledby`, which some AT/axe paths don't resolve as reliably as a direct name. | `FormField` (`src/components/data-entry/form-field.tsx`) now **also** injects a redundant `aria-label` mirroring the visible label text (when it's plain text), alongside its existing `aria-labelledby` wiring — belt-and-suspenders, only when the child hasn't already set its own `aria-label`. Commit `8675570`. |
|
|
164
|
+
| **Accessible name — dialog** | `aria-dialog-name` / `aria-hidden-focus` | A `role="dialog"` (Popover in `modal` mode) needs a discernible name; a modal Popover **shown open at rest** in a demo (`Popover modal defaultOpen`) also puts Radix's `aria-hide` on everything outside the portal, which can hide the page's own `<main>`/`<h1>` from the accessibility tree scan. | Label the titled/modal Popover demos with `aria-label` on `PopoverContent`. The `aria-hidden-focus`/`landmark-one-main`/`page-has-heading-one` combination on an **open-at-rest modal demo** is the deferred exception — see below. Commit `8675570`. |
|
|
165
|
+
| **Color contrast** | `color-contrast` | `Calendar`'s selected-day ghost `<button>` kept dark text on the blue fill through hover/focus (insufficient contrast on the selected date). | Force `text-primary-foreground` on the day `<button>` through hover/focus states. Commit `8675570`. Some `color-contrast` entries remain on `text-muted-foreground` small text — tracked, not yet fixed (see below and [color-extensibility.md](./roadmap/color-extensibility.md)). |
|
|
166
|
+
| **Stray heading from empty state** | `heading-order` | `DataTable`'s built-in empty state rendered its "no rows" message as an `<h3>` (`EmptyState`'s default `titleAs`), injecting a heading into the outline for a state message, not a section. | `DataTable`'s empty state now renders `titleAs="p"` (plain text) — `EmptyState`/`CardTitle` both expose `titleLevel`/`titleAs` (`level`/`as` for `CardTitle`) precisely so callers can pick outline position without changing visual size. Commit `8675570`. |
|
|
167
|
+
|
|
168
|
+
## Remaining baseline debt — the 12 frames
|
|
169
|
+
|
|
170
|
+
As of `frame-axe.baseline.json` (`generatedAt: 2026-07-13`), 12 frames still carry an allowlisted
|
|
171
|
+
component violation. Grouped by root cause (per the tracking note in commit `8675570`):
|
|
172
|
+
|
|
173
|
+
| Frame(s) | Rule(s) | Why it's still open | Path to zero |
|
|
174
|
+
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| `data-entry-select`, `data-entry-select-matrix`, `feedback-sheet`, `navigation-toolbar` | `button-name` (+ `feedback-sheet` also `color-contrast`) | All four demos use `Select`/`SelectTrigger`. `#177` (merged, commit `1ef05be`) shipped the `DataSelect` prop/aria-forwarding + `SelectTrigger showIndicator` fix this was tracked against, but the axe baseline was **not re-verified/re-snapshotted** against `main` after that PR merged. | Run `pnpm check:frame-axe --update-baseline` on current `main`; if `#177` closed the underlying gap, these 4 entries drop out automatically. If any remain, re-triage against the current `SelectTrigger`/`FormField` DOM. |
|
|
176
|
+
| `navigation-tabs` | `color-contrast` | `#176` (merged, commit `78c1c3b`) fixed `Tabs`' scrollable-overflow + disabled-fallback selection; the `color-contrast` entry here was tracked against that same PR but likewise **not re-verified** since. | `pnpm check:frame-axe --update-baseline` after confirming the fix; re-triage if it persists. |
|
|
177
|
+
| `data-entry-transfer`, `feedback-sheet` | `color-contrast` | Both demos use `text-muted-foreground` on small text (`transfer.tsx:83,101,121`, `sheet.tsx:226`). This is the same **default-theme contrast tradeoff** already tracked in [`docs/roadmap/color-extensibility.md`](./roadmap/color-extensibility.md) — resolving it is a brand-colour decision (darken the token or add a dedicated `--text-muted` variant), deliberately deferred pre-release rather than shipped as an unflagged palette shift. | Land the token decision in `color-extensibility.md`, then re-run `--update-baseline`. |
|
|
178
|
+
| `data-entry-calendar` | `landmark-unique` | `Calendar` wraps `react-day-picker` (`^10.0.1`), which renders its **own internal `<nav>`** for month navigation inside every `DayPicker` instance. The docs demo renders 4 `Calendar` instances on one page (each with a distinguishing container `aria-label`, but the _nested_ `nav` isn't currently overridable through this wrapper's `classNames` config) — 4 identical unnamed `nav` landmarks on one page. | Needs either upstream `react-day-picker` support for a per-instance nav label, or wrapping its nav slot with a custom, labelled replacement in `src/components/data-entry/calendar.tsx`. |
|
|
179
|
+
| `layout-split-pane` | `landmark-unique` | `SplitPane` (`src/components/layout/split-pane.tsx`) renders a bare `<aside className="ui-split-pane-aside">` with **no `aria-label` prop at all**. `docs/layout/split-pane.tsx` renders 4 `SplitPane` instances on one page → 4 identical unnamed `complementary` landmarks. | Add an `aria-label`/`asideAriaLabel` override prop to `SplitPane`, same pattern as `Pagination`/`Breadcrumb` (commit `741ccf5`), then give each docs instance a distinguishing label. |
|
|
180
|
+
| `data-display-popover` | `aria-hidden-focus`, `landmark-one-main`, `page-has-heading-one` | `docs/data-display/popover.tsx` (line ~198) renders `<Popover modal defaultOpen>` — a modal Popover **shown open at rest** so the demo illustrates the modal behavior without a click. Radix's modal mode `aria-hide`s everything outside the portal while open, which hides the frame's own `<main>`/`<h1>` from axe's accessibility-tree scan (and leaves focusable content behind the hidden boundary). | Either stop shipping this specific demo `defaultOpen` (illustrate modal-ness a different way, e.g. a screenshot/GIF or a "click to open" note) or accept it as a **known, deliberate** demo-only artifact of showing a Radix modal-trap mid-interaction and add a per-rule documented exception (not a blanket mute) once the maintainers decide which. |
|
|
181
|
+
| `layout-resizable-panel` | `scrollable-region-focusable` | `docs/layout/resizable-panel.tsx:72` — a demo panel with an inline `style={{ overflowY: "auto" }}`, added directly in the doc content, not through a primitive that already carries the `tabIndex={0}` fix. | Add `tabIndex={0}` to that panel's `<div>` in the docs demo (same fix pattern as `.demo-block-canvas`/`Table`, just not yet applied to this hand-rolled demo wrapper). |
|
|
182
|
+
| `navigation-breadcrumb` | `scrollable-region-focusable` | `docs/navigation/breadcrumb.tsx:89,95` — two `<pre className="overflow-x-auto">` code-snippet blocks rendered directly in the demo body (not the footer "View code" panel, which is disabled in frame mode). | Add `tabIndex={0}` to the `<pre>` elements in the docs demo. |
|
|
183
|
+
| `navigation-pagination` | `scrollable-region-focusable` | This one is a **real component gap**, not docs-only: `.ui-pagination-list` (`src/styles/layout.css`, the `#153`/`#165` no-wrap pagination CSS) sets `overflow-x: auto` so the page-number strip scrolls instead of wrapping to a second line — but `PaginationContent`'s `<ul>` (`src/components/navigation/pagination.tsx`) never received a `tabIndex={0}` to match. | Add `tabIndex={0}` to `PaginationContent`'s `<ul>` in `pagination.tsx` (mirrors the `Table`/`ScrollArea` fix), then `--update-baseline`. |
|
|
184
|
+
|
|
185
|
+
Overall rule histogram across the 12 frames: `button-name` ×4, `color-contrast` ×3,
|
|
186
|
+
`scrollable-region-focusable` ×3, `landmark-unique` ×2, `aria-hidden-focus` ×1,
|
|
187
|
+
`landmark-one-main` ×1, `page-has-heading-one` ×1 (the last three all on the same
|
|
188
|
+
`data-display-popover` frame).
|
|
189
|
+
|
|
190
|
+
**Fastest path to a fully green baseline:** re-run `pnpm check:frame-axe --update-baseline` on
|
|
191
|
+
current `main` first (it's plausible `#176`/`#177` already closed 5 of the 12 entries and nobody's
|
|
192
|
+
re-snapshotted since) — then tackle the remaining `SplitPane`/`Calendar` landmark props, the two
|
|
193
|
+
docs-only `tabIndex` additions, the one real `pagination.tsx` `tabIndex` fix, the muted-foreground
|
|
194
|
+
token decision, and finally decide the modal-Popover-at-rest exception.
|
|
195
|
+
|
|
196
|
+
## Infra frame modes (issue #163)
|
|
197
|
+
|
|
198
|
+
Any `/frame/<id>` accepts query params (implemented in `preview/src/frame-main.tsx`) so a route can
|
|
199
|
+
be exercised across contract axes without editing per-component example content:
|
|
200
|
+
|
|
201
|
+
| Param | Values | Effect |
|
|
202
|
+
| --------- | --------------------------------------- | --------------------------------------------------------- |
|
|
203
|
+
| `dir` | `ltr` \| `rtl` | flips logical CSS for the whole subtree (global RTL mode) |
|
|
204
|
+
| `density` | `compact` \| `default` \| `comfortable` | drives `AppProvider` density |
|
|
205
|
+
| `theme` | `light` \| `dark` | drives `AppProvider` theme |
|
|
206
|
+
| `locale` | `ja` \| `vi` \| `en` \| `ar` … | drives `AppProvider` locale (BCP-47) |
|
|
207
|
+
|
|
208
|
+
Example: `/frame/data-entry-select?dir=rtl&density=compact&theme=dark`.
|
|
209
|
+
|
|
210
|
+
`check:frame-axe` itself always runs the default chrome (`ltr`/`default`/`light`/`ja`) — these
|
|
211
|
+
params are for manual/exploratory testing (e.g. checking a component under RTL + dark in a real
|
|
212
|
+
browser), not currently swept by the automated gate.
|
|
213
|
+
|
|
214
|
+
## Coverage tracker
|
|
215
|
+
|
|
216
|
+
`check:frame-coverage` reads the public inventory (`mcp/src/data/components.ts`), the frames that
|
|
217
|
+
exist (`docs/` tsx), and the declared ledger (`preview/frame-coverage.ledger.json`), then emits
|
|
218
|
+
`docs/FRAME-COVERAGE-REPORT.md` + `audit-evidence/frame-coverage/coverage.json`. Every contract axis
|
|
219
|
+
is `covered` / `N/A:<reason>` / **`UNTESTED`** — a missing axis is never a silent pass. Those axes
|
|
220
|
+
are rolled up from the 14 contract dimensions the ledger tracks per export.
|
|
221
|
+
|
|
222
|
+
`check:frame-coverage-ledger` is the enforcing gate: it regenerates the ledger from the real public
|
|
223
|
+
surface, recomputes every dimension from the evidence, and fails on regression (coverage falling, a
|
|
224
|
+
new export with no frame, a weakened viewport matrix, a growing geometry/axe baseline). Dimension
|
|
225
|
+
totals climb toward full coverage (the #163 exit criterion). See
|
|
226
|
+
[FRAME-COVERAGE-LEDGER.md](./FRAME-COVERAGE-LEDGER.md) and
|
|
227
|
+
[FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
|
|
228
|
+
|
|
229
|
+
## Adding a new component/frame without regressing the gate
|
|
230
|
+
|
|
231
|
+
Before opening a PR that adds or meaningfully changes a `/frame/**` example:
|
|
232
|
+
|
|
233
|
+
1. **Give every top-level `Card` a `CardTitle level={2}`** when the frame sits directly under a
|
|
234
|
+
`PageContainer` (`<h1>`) — nested Card-in-Card titles keep the `h3` default. Skipping this
|
|
235
|
+
reintroduces `heading-order`.
|
|
236
|
+
2. **Don't hand-roll a scroll container.** If a demo needs `overflow: auto`/`overflow-x: auto`,
|
|
237
|
+
add `tabIndex={0}` to the scrolling element and do **not** add `role="region"` (that turns it
|
|
238
|
+
into a landmark and can trip `landmark-*-is-top-level` for stories with their own regions).
|
|
239
|
+
Prefer reusing `Table`/`DataTable`/`ScrollArea`, which already carry this.
|
|
240
|
+
3. **Stacking more than one instance of a landmark-owning component** (`Pagination`, `Breadcrumb`,
|
|
241
|
+
`SplitPane`'s `aside`, custom `<nav>`/`<aside>`/`<section aria-label>`) **on one page** — give
|
|
242
|
+
each instance a distinguishing `aria-label` (use the `aria-label`/`breadcrumbAriaLabel` override
|
|
243
|
+
props already on `Pagination`/`Breadcrumb`/`PageContainer` where they exist), or the frame trips
|
|
244
|
+
`landmark-unique`.
|
|
245
|
+
4. **`<li>` needs a real list parent.** `ListRow as="li"` (or any bare `<li>`) must be inside a
|
|
246
|
+
`<ul>`/`<ol>`/`role="list"` container, never a plain `<div>`.
|
|
247
|
+
5. **Every interactive control needs a discernible name.** Route form controls through `FormField`
|
|
248
|
+
(it injects both `aria-labelledby` and a redundant `aria-label`); give icon-only buttons/triggers
|
|
249
|
+
an explicit `aria-label`.
|
|
250
|
+
6. **A `role="dialog"` (Popover `modal`, custom dialogs) needs a name**, and avoid shipping a demo
|
|
251
|
+
with a **modal** dialog `defaultOpen`/open-at-rest unless you've checked what it hides from the
|
|
252
|
+
rest of the page (see the `data-display-popover` entry above) — prefer "click to open."
|
|
253
|
+
7. **Run the gate before pushing:** `pnpm exec playwright install chromium` (once) then
|
|
254
|
+
`AXE_FRAMES_LIMIT=<n> pnpm check:frame-axe` scoped to your new/changed frame(s), or the full
|
|
255
|
+
`pnpm check:frame-axe` if you have time. A brand-new frame with **zero** violations needs no
|
|
256
|
+
baseline entry at all — it's only added if you're knowingly landing a pre-existing violation
|
|
257
|
+
that a follow-up will fix (and even then, prefer fixing it before merge).
|
|
258
|
+
8. If you do land a frame with a tracked violation, add it to `scripts/frame-axe.baseline.json` via
|
|
259
|
+
`--update-baseline` (never hand-edit the JSON) and note _why_ + the follow-up issue in the PR
|
|
260
|
+
description — the baseline file's own `note` field explains it may only shrink from here.
|
|
261
|
+
9. Register the case you authored in `preview/frame-coverage.ledger.json` so
|
|
262
|
+
`check:frame-coverage-ledger` stops reporting that dimension `UNTESTED`. You **cannot** hand-write
|
|
263
|
+
a verdict — add an entry to the ledger's `cases` array (frame path + case heading + resolvable
|
|
264
|
+
evidence paths + reviewer + HTTPS review link + ISO timestamp) and run
|
|
265
|
+
`pnpm gen:frame-coverage-ledger`; the gate recomputes every cell from that evidence and rejects
|
|
266
|
+
any verdict it cannot reproduce. See [FRAME-COVERAGE-LEDGER.md](./FRAME-COVERAGE-LEDGER.md) and
|
|
267
|
+
[FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
|