@godxjp/ui 19.0.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/app-provider.d.ts +1 -1
- package/dist/app/app-provider.js +3 -0
- 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/cascader.js +3 -1
- package/dist/components/data-entry/checkbox-group.js +12 -5
- package/dist/components/data-entry/checkbox.js +30 -21
- 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 +12 -3
- package/dist/components/data-entry/date-range-picker.d.ts +1 -1
- package/dist/components/data-entry/date-range-picker.js +17 -6
- package/dist/components/data-entry/form-errors.d.ts +7 -18
- package/dist/components/data-entry/form-field.d.ts +1 -1
- package/dist/components/data-entry/form-field.js +15 -6
- 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 +13 -3
- package/dist/components/data-entry/month-picker.js +6 -4
- package/dist/components/data-entry/month-range-picker.d.ts +4 -6
- package/dist/components/data-entry/month-range-picker.js +11 -5
- 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 +19 -6
- package/dist/components/data-entry/search-select.d.ts +3 -5
- package/dist/components/data-entry/search-select.js +8 -2
- package/dist/components/data-entry/select.d.ts +15 -2
- package/dist/components/data-entry/select.js +57 -10
- package/dist/components/data-entry/switch.js +9 -2
- package/dist/components/data-entry/textarea.d.ts +10 -16
- package/dist/components/data-entry/textarea.js +8 -1
- package/dist/components/data-entry/time-picker.d.ts +3 -5
- package/dist/components/data-entry/tree-select.js +3 -1
- 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 +31 -36
- package/dist/lib/field-a11y.js +16 -2
- 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 +19 -27
- 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 +41 -30
- 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 +10 -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,1072 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Showcase · case6 — 代理店ハンディ (Agency Handy, mobile)
|
|
3
|
+
*
|
|
4
|
+
* The Acme consolidation-warehouse handheld app (390×844, gloves-on, scan-heavy),
|
|
5
|
+
* rebuilt ENTIRELY from real @godxjp/ui primitives — the reference-design "Handy Agency"
|
|
6
|
+
* design recreated as a skeleton (intent + look), not a transcription of its prototype DOM.
|
|
7
|
+
*
|
|
8
|
+
* Composition map (Handy prototype block → @godxjp/ui primitive):
|
|
9
|
+
* phone shell ................. composed frame (status bar / header / scroll / sticky bar / tabbar)
|
|
10
|
+
* header text-action .......... Button(ghost, size=sm) — iOS "Chọn" select-mode entry
|
|
11
|
+
* filter chips ................ ToggleGroup(type=single) — 32px count pills
|
|
12
|
+
* outbound segmented .......... ToggleGroup(type=single) — Chờ niêm phong / Chờ bàn giao / Đã bàn giao
|
|
13
|
+
* list-card recipe ............ Card + composed 3-line row + Badge(tone) + select-mode Checkbox
|
|
14
|
+
* status badge (1:1) .......... Badge tone — attention/info/muted/success/warning, color-locked
|
|
15
|
+
* select-mode bar ............. primary-soft toolbar strip (count + actions) above tabbar
|
|
16
|
+
* big scan Button ............. sticky-actions primary "Quét / Tìm mã" (flex-2, dominant)
|
|
17
|
+
* ItemLookupSheet ............. Sheet(side=bottom) — viewfinder placeholder + manual mono input
|
|
18
|
+
* package-picker Sheet ........ Sheet(side=bottom) — list of PKG rows + create-new row
|
|
19
|
+
* ItemFormModal Sheet ......... Sheet(side=bottom) — barcode + qty + radio-row "Đích đến"
|
|
20
|
+
* detail rows ................. Descriptions + Descriptions.Item(mono) — item detail
|
|
21
|
+
* confirm Dialog .............. AlertDialog — Seal / Handoff (verb confirm, not "OK")
|
|
22
|
+
* toast ....................... Toaster + toast.success — "Đã bàn giao … · 16:24"
|
|
23
|
+
* four list states ............ Loading(Skeleton) · Empty(EmptyState) · Error(Alert) · populated
|
|
24
|
+
*
|
|
25
|
+
* DNA applied: comfortable 44px touch density, mono codes (RC-/JAN/PKG-/A-03-02),
|
|
26
|
+
* fixed color signaling (attention 朱 = Chưa phân loại + fixable errors, info 群青 = open,
|
|
27
|
+
* muted = in-package/draft, success 若竹 = sealed, warning 山吹 = delivered), tabular-nums,
|
|
28
|
+
* scan-first (primary action widest), bottom-sheet-driven, vi-first quiet copy, no emoji.
|
|
29
|
+
*/
|
|
30
|
+
import * as React from "react";
|
|
31
|
+
import {
|
|
32
|
+
Boxes,
|
|
33
|
+
Check,
|
|
34
|
+
Inbox,
|
|
35
|
+
Package,
|
|
36
|
+
PackagePlus,
|
|
37
|
+
Plus,
|
|
38
|
+
RefreshCw,
|
|
39
|
+
ScanLine,
|
|
40
|
+
Search,
|
|
41
|
+
Truck,
|
|
42
|
+
X,
|
|
43
|
+
} from "lucide-react";
|
|
44
|
+
|
|
45
|
+
import { Button, Heading, Text } from "@godxjp/ui/general";
|
|
46
|
+
import { Flex } from "@godxjp/ui/layout";
|
|
47
|
+
import {
|
|
48
|
+
Badge,
|
|
49
|
+
type BadgeProps,
|
|
50
|
+
Card,
|
|
51
|
+
CardContent,
|
|
52
|
+
Descriptions,
|
|
53
|
+
EmptyState,
|
|
54
|
+
} from "@godxjp/ui/data-display";
|
|
55
|
+
import {
|
|
56
|
+
Checkbox,
|
|
57
|
+
Input,
|
|
58
|
+
RadioGroupRoot,
|
|
59
|
+
RadioItem,
|
|
60
|
+
ToggleGroup,
|
|
61
|
+
ToggleGroupItem,
|
|
62
|
+
} from "@godxjp/ui/data-entry";
|
|
63
|
+
import {
|
|
64
|
+
AlertDialog,
|
|
65
|
+
Alert,
|
|
66
|
+
AlertTitle,
|
|
67
|
+
AlertDescription,
|
|
68
|
+
Sheet,
|
|
69
|
+
SheetBody,
|
|
70
|
+
SheetContent,
|
|
71
|
+
SheetFooter,
|
|
72
|
+
SheetHeader,
|
|
73
|
+
SheetTitle,
|
|
74
|
+
Skeleton,
|
|
75
|
+
Toaster,
|
|
76
|
+
toast,
|
|
77
|
+
} from "@godxjp/ui/feedback";
|
|
78
|
+
|
|
79
|
+
type BadgeTone = NonNullable<BadgeProps["tone"]>;
|
|
80
|
+
|
|
81
|
+
// ── Status vocabulary — 1:1 color-locked (Handy §6b: "same word in different colors is a bug") ──
|
|
82
|
+
type ItemStatus = "unsorted" | "shelf" | "packed";
|
|
83
|
+
const ITEM_STATUS: Record<ItemStatus, { label: string; tone: BadgeTone }> = {
|
|
84
|
+
unsorted: { label: "Chưa phân loại", tone: "destructive" }, // 朱 attention via destructive-adjacent? -> use warning? see note
|
|
85
|
+
shelf: { label: "Trên giá hàng", tone: "info" }, // 群青 open
|
|
86
|
+
packed: { label: "Trong kiện", tone: "muted" }, // draft / in-package
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Attention (朱) is the fixable-but-needs-action signal. Badge tones expose
|
|
90
|
+
// success/warning/destructive/info/muted/neutral — there is no dedicated "attention"
|
|
91
|
+
// tone, so non-destructive "Chưa phân loại" maps to warning (山吹), reserving
|
|
92
|
+
// destructive (茜) for the irreversible logout confirm only. (see gapNotes)
|
|
93
|
+
ITEM_STATUS.unsorted.tone = "warning";
|
|
94
|
+
|
|
95
|
+
type PackingStatus = "active" | "open" | "draft" | "sealed" | "delivered";
|
|
96
|
+
const PACKING_STATUS: Record<PackingStatus, { label: string; tone: BadgeTone }> = {
|
|
97
|
+
active: { label: "Đang làm", tone: "info" },
|
|
98
|
+
open: { label: "Đang mở", tone: "info" },
|
|
99
|
+
draft: { label: "Nháp", tone: "muted" },
|
|
100
|
+
sealed: { label: "Đã niêm phong", tone: "success" },
|
|
101
|
+
delivered: { label: "Đã bàn giao", tone: "muted" },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ── Mock data ───────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
interface HandyItem {
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
rc: string;
|
|
110
|
+
jan: string;
|
|
111
|
+
status: ItemStatus;
|
|
112
|
+
qty: number;
|
|
113
|
+
receivedAt: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ITEMS: HandyItem[] = [
|
|
117
|
+
{
|
|
118
|
+
id: "1",
|
|
119
|
+
name: "Sữa rửa mặt Hada Labo",
|
|
120
|
+
rc: "RC-204881",
|
|
121
|
+
jan: "4987241135219",
|
|
122
|
+
status: "unsorted",
|
|
123
|
+
qty: 3,
|
|
124
|
+
receivedAt: "14:02",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "2",
|
|
128
|
+
name: "Kem chống nắng Anessa",
|
|
129
|
+
rc: "RC-204882",
|
|
130
|
+
jan: "4909978141004",
|
|
131
|
+
status: "shelf",
|
|
132
|
+
qty: 1,
|
|
133
|
+
receivedAt: "13:48",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "3",
|
|
137
|
+
name: "Vitamin DHC B-Complex",
|
|
138
|
+
rc: "RC-204879",
|
|
139
|
+
jan: "4511413404164",
|
|
140
|
+
status: "shelf",
|
|
141
|
+
qty: 2,
|
|
142
|
+
receivedAt: "13:31",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "4",
|
|
146
|
+
name: "Bàn chải Ora2 Me",
|
|
147
|
+
rc: "RC-204875",
|
|
148
|
+
jan: "4903301242697",
|
|
149
|
+
status: "packed",
|
|
150
|
+
qty: 4,
|
|
151
|
+
receivedAt: "11:20",
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
interface Packing {
|
|
156
|
+
id: string;
|
|
157
|
+
code: string;
|
|
158
|
+
customer: string;
|
|
159
|
+
city: string;
|
|
160
|
+
items: number;
|
|
161
|
+
status: PackingStatus;
|
|
162
|
+
slot?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const PACKINGS: Packing[] = [
|
|
166
|
+
{
|
|
167
|
+
id: "p1",
|
|
168
|
+
code: "PKG-000041",
|
|
169
|
+
customer: "Bùi Hà",
|
|
170
|
+
city: "Hà Nội",
|
|
171
|
+
items: 8,
|
|
172
|
+
status: "active",
|
|
173
|
+
slot: "A-03-02",
|
|
174
|
+
},
|
|
175
|
+
{ id: "p2", code: "PKG-000040", customer: "Lê Minh", city: "TP.HCM", items: 5, status: "open" },
|
|
176
|
+
{
|
|
177
|
+
id: "p3",
|
|
178
|
+
code: "PKG-000038",
|
|
179
|
+
customer: "Trần Linh",
|
|
180
|
+
city: "Đà Nẵng",
|
|
181
|
+
items: 12,
|
|
182
|
+
status: "draft",
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const OUTBOUND: Packing[] = [
|
|
187
|
+
{
|
|
188
|
+
id: "o1",
|
|
189
|
+
code: "PKG-000037",
|
|
190
|
+
customer: "Phạm An",
|
|
191
|
+
city: "Hà Nội",
|
|
192
|
+
items: 9,
|
|
193
|
+
status: "sealed",
|
|
194
|
+
slot: "B-01-04",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "o2",
|
|
198
|
+
code: "PKG-000036",
|
|
199
|
+
customer: "Vũ Nga",
|
|
200
|
+
city: "TP.HCM",
|
|
201
|
+
items: 6,
|
|
202
|
+
status: "sealed",
|
|
203
|
+
slot: "B-02-01",
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
const TABS = [
|
|
208
|
+
{ id: "inbound", label: "Nhập kho", icon: Inbox },
|
|
209
|
+
{ id: "packing", label: "Đóng gói", icon: Package },
|
|
210
|
+
{ id: "outbound", label: "Xuất kho", icon: Truck },
|
|
211
|
+
] as const;
|
|
212
|
+
|
|
213
|
+
const FILTERS = [
|
|
214
|
+
{ id: "all", label: "Tất cả", count: 42 },
|
|
215
|
+
{ id: "unsorted", label: "Chưa phân loại", count: 9 },
|
|
216
|
+
{ id: "shelf", label: "Trên giá hàng", count: 33 },
|
|
217
|
+
] as const;
|
|
218
|
+
|
|
219
|
+
// ── Small composed parts ─────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
function MonoCode({ children }: { children: React.ReactNode }) {
|
|
222
|
+
return (
|
|
223
|
+
<Text size="xs" mono tone="muted" tabular>
|
|
224
|
+
{children}
|
|
225
|
+
</Text>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The Handy list-card recipe: full-tap row, name + mono codes + badge/meta. */
|
|
230
|
+
function ItemListCard({
|
|
231
|
+
item,
|
|
232
|
+
selectMode,
|
|
233
|
+
selected,
|
|
234
|
+
onToggle,
|
|
235
|
+
}: {
|
|
236
|
+
item: HandyItem;
|
|
237
|
+
selectMode: boolean;
|
|
238
|
+
selected: boolean;
|
|
239
|
+
onToggle: () => void;
|
|
240
|
+
}) {
|
|
241
|
+
const st = ITEM_STATUS[item.status];
|
|
242
|
+
return (
|
|
243
|
+
<Card
|
|
244
|
+
density="tight"
|
|
245
|
+
className={
|
|
246
|
+
"rounded-xl transition-colors " +
|
|
247
|
+
(selected
|
|
248
|
+
? "border-primary bg-[color-mix(in_oklch,var(--primary)_8%,transparent)]"
|
|
249
|
+
: "hover:border-primary")
|
|
250
|
+
}
|
|
251
|
+
>
|
|
252
|
+
<CardContent solo className="flex items-start gap-3">
|
|
253
|
+
{selectMode ? (
|
|
254
|
+
<Checkbox
|
|
255
|
+
checked={selected}
|
|
256
|
+
onCheckedChange={onToggle}
|
|
257
|
+
aria-label={`選択 ${item.name}`}
|
|
258
|
+
className="mt-0.5 size-5"
|
|
259
|
+
/>
|
|
260
|
+
) : null}
|
|
261
|
+
<div className="min-w-0 flex-1">
|
|
262
|
+
<Text as="div" size="sm" weight="bold" truncate>
|
|
263
|
+
{item.name}
|
|
264
|
+
</Text>
|
|
265
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5">
|
|
266
|
+
<MonoCode>{item.rc}</MonoCode>
|
|
267
|
+
<MonoCode>JAN {item.jan}</MonoCode>
|
|
268
|
+
</div>
|
|
269
|
+
<Flex align="center" justify="between" gap="sm" className="mt-1.5">
|
|
270
|
+
<Badge tone={st.tone} variant="outline" className="rounded-full">
|
|
271
|
+
{st.label}
|
|
272
|
+
</Badge>
|
|
273
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
274
|
+
×{item.qty} · {item.receivedAt}
|
|
275
|
+
</Text>
|
|
276
|
+
</Flex>
|
|
277
|
+
</div>
|
|
278
|
+
</CardContent>
|
|
279
|
+
</Card>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function PackingListCard({ packing, onTap }: { packing: Packing; onTap?: () => void }) {
|
|
284
|
+
const st = PACKING_STATUS[packing.status];
|
|
285
|
+
return (
|
|
286
|
+
<Card
|
|
287
|
+
density="tight"
|
|
288
|
+
role={onTap ? "button" : undefined}
|
|
289
|
+
tabIndex={onTap ? 0 : undefined}
|
|
290
|
+
onClick={onTap}
|
|
291
|
+
className={
|
|
292
|
+
"rounded-xl transition-colors " + (onTap ? "hover:border-primary cursor-pointer" : "")
|
|
293
|
+
}
|
|
294
|
+
>
|
|
295
|
+
<CardContent solo>
|
|
296
|
+
<Flex align="center" justify="between" gap="sm">
|
|
297
|
+
<Text as="code" size="sm" weight="bold" tabular>
|
|
298
|
+
{packing.code}
|
|
299
|
+
</Text>
|
|
300
|
+
<Badge tone={st.tone} variant="outline" className="rounded-full">
|
|
301
|
+
{st.label}
|
|
302
|
+
</Badge>
|
|
303
|
+
</Flex>
|
|
304
|
+
<Flex align="center" justify="between" gap="sm" className="mt-1">
|
|
305
|
+
<Text size="xs" tone="muted" truncate>
|
|
306
|
+
{packing.customer} · {packing.city}
|
|
307
|
+
</Text>
|
|
308
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
309
|
+
×{packing.items}
|
|
310
|
+
{packing.slot ? ` · ${packing.slot}` : ""}
|
|
311
|
+
</Text>
|
|
312
|
+
</Flex>
|
|
313
|
+
</CardContent>
|
|
314
|
+
</Card>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Three uppercase muted section header + right-aligned mono count. */
|
|
319
|
+
function SectionHeader({ children, count }: { children: React.ReactNode; count?: number }) {
|
|
320
|
+
return (
|
|
321
|
+
<Flex align="center" justify="between" className="px-0.5">
|
|
322
|
+
<Text size="2xs" weight="medium" tone="muted" className="tracking-wider uppercase">
|
|
323
|
+
{children}
|
|
324
|
+
</Text>
|
|
325
|
+
{count != null ? (
|
|
326
|
+
<Text size="2xs" mono tone="muted" tabular>
|
|
327
|
+
{count}
|
|
328
|
+
</Text>
|
|
329
|
+
) : null}
|
|
330
|
+
</Flex>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Phone shell ──────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
function PhoneFrame({ children }: { children: React.ReactNode }) {
|
|
337
|
+
return (
|
|
338
|
+
<div className="mx-auto w-96 max-w-full shrink-0">
|
|
339
|
+
<Flex
|
|
340
|
+
direction="col"
|
|
341
|
+
className="bg-background max-h-[calc(100vh-2rem)] min-h-[844px] overflow-hidden rounded-2xl border"
|
|
342
|
+
>
|
|
343
|
+
{children}
|
|
344
|
+
</Flex>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function StatusBar() {
|
|
350
|
+
return (
|
|
351
|
+
<div className="flex h-9 shrink-0 items-center justify-between px-5">
|
|
352
|
+
<Text size="sm" weight="medium" tabular>
|
|
353
|
+
9:41
|
|
354
|
+
</Text>
|
|
355
|
+
<Text size="sm" weight="medium" tabular tone="muted">
|
|
356
|
+
Acme Handy
|
|
357
|
+
</Text>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function TabBar({ active, onChange }: { active: string; onChange: (id: string) => void }) {
|
|
363
|
+
return (
|
|
364
|
+
<nav className="grid shrink-0 grid-cols-3 border-t" aria-label="ワークフロー">
|
|
365
|
+
{TABS.map((t) => {
|
|
366
|
+
const isActive = t.id === active;
|
|
367
|
+
const Icon = t.icon;
|
|
368
|
+
return (
|
|
369
|
+
<Button
|
|
370
|
+
key={t.id}
|
|
371
|
+
variant="ghost"
|
|
372
|
+
onClick={() => onChange(t.id)}
|
|
373
|
+
aria-current={isActive ? "page" : undefined}
|
|
374
|
+
className={
|
|
375
|
+
"h-16 flex-col gap-1 rounded-none font-medium text-[var(--font-size-2xs)] " +
|
|
376
|
+
(isActive ? "text-primary" : "text-muted-foreground")
|
|
377
|
+
}
|
|
378
|
+
>
|
|
379
|
+
<Icon className="size-5" aria-hidden="true" strokeWidth={1.5} />
|
|
380
|
+
<span className="whitespace-nowrap">{t.label}</span>
|
|
381
|
+
</Button>
|
|
382
|
+
);
|
|
383
|
+
})}
|
|
384
|
+
</nav>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── Sheets ───────────────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
function ItemLookupSheet({
|
|
391
|
+
open,
|
|
392
|
+
onOpenChange,
|
|
393
|
+
}: {
|
|
394
|
+
open: boolean;
|
|
395
|
+
onOpenChange: (v: boolean) => void;
|
|
396
|
+
}) {
|
|
397
|
+
const [code, setCode] = React.useState("");
|
|
398
|
+
return (
|
|
399
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
400
|
+
<SheetContent side="bottom" className="rounded-t-2xl">
|
|
401
|
+
<SheetHeader>
|
|
402
|
+
<SheetTitle>Quét hoặc nhập mã</SheetTitle>
|
|
403
|
+
</SheetHeader>
|
|
404
|
+
<SheetBody className="flex flex-col gap-4">
|
|
405
|
+
{/* Viewfinder placeholder — a Card surface, not a hand-rolled illustration */}
|
|
406
|
+
<Flex
|
|
407
|
+
align="center"
|
|
408
|
+
justify="center"
|
|
409
|
+
className="border-border bg-secondary/40 h-40 rounded-xl border-2 border-dashed"
|
|
410
|
+
>
|
|
411
|
+
<Flex direction="col" align="center" gap="sm">
|
|
412
|
+
<ScanLine
|
|
413
|
+
className="size-7 text-[color:var(--attention,var(--warning))]"
|
|
414
|
+
aria-hidden="true"
|
|
415
|
+
strokeWidth={1.5}
|
|
416
|
+
/>
|
|
417
|
+
<Text size="sm" tone="muted">
|
|
418
|
+
Đưa mã vạch vào khung
|
|
419
|
+
</Text>
|
|
420
|
+
</Flex>
|
|
421
|
+
</Flex>
|
|
422
|
+
<Flex direction="col" gap="sm">
|
|
423
|
+
<Text size="sm" weight="medium">
|
|
424
|
+
Hoặc nhập mã thủ công
|
|
425
|
+
</Text>
|
|
426
|
+
<Flex gap="sm">
|
|
427
|
+
<Input
|
|
428
|
+
value={code}
|
|
429
|
+
onChange={(e) => setCode(e.target.value)}
|
|
430
|
+
placeholder="RC- / PKG- / JAN"
|
|
431
|
+
className="font-mono"
|
|
432
|
+
inputMode="text"
|
|
433
|
+
autoComplete="off"
|
|
434
|
+
/>
|
|
435
|
+
<Button
|
|
436
|
+
onClick={() => {
|
|
437
|
+
onOpenChange(false);
|
|
438
|
+
setCode("");
|
|
439
|
+
toast.success("Đã tìm thấy RC-204881 · Sữa rửa mặt Hada Labo");
|
|
440
|
+
}}
|
|
441
|
+
disabled={code.trim() === ""}
|
|
442
|
+
>
|
|
443
|
+
<Search aria-hidden="true" />
|
|
444
|
+
Tìm
|
|
445
|
+
</Button>
|
|
446
|
+
</Flex>
|
|
447
|
+
</Flex>
|
|
448
|
+
</SheetBody>
|
|
449
|
+
<SheetFooter>
|
|
450
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
451
|
+
Đóng
|
|
452
|
+
</Button>
|
|
453
|
+
</SheetFooter>
|
|
454
|
+
</SheetContent>
|
|
455
|
+
</Sheet>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function PackagePickerSheet({
|
|
460
|
+
open,
|
|
461
|
+
onOpenChange,
|
|
462
|
+
count,
|
|
463
|
+
}: {
|
|
464
|
+
open: boolean;
|
|
465
|
+
onOpenChange: (v: boolean) => void;
|
|
466
|
+
count: number;
|
|
467
|
+
}) {
|
|
468
|
+
return (
|
|
469
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
470
|
+
<SheetContent side="bottom" className="rounded-t-2xl">
|
|
471
|
+
<SheetHeader>
|
|
472
|
+
<SheetTitle>Gán vào kiện</SheetTitle>
|
|
473
|
+
</SheetHeader>
|
|
474
|
+
<SheetBody className="flex flex-col gap-2">
|
|
475
|
+
<SectionHeader count={PACKINGS.length}>Kiện đang mở</SectionHeader>
|
|
476
|
+
{PACKINGS.map((p) => (
|
|
477
|
+
<PackingListCard
|
|
478
|
+
key={p.id}
|
|
479
|
+
packing={p}
|
|
480
|
+
onTap={() => {
|
|
481
|
+
onOpenChange(false);
|
|
482
|
+
toast.success(`Đã gán ${count} item vào ${p.code}`);
|
|
483
|
+
}}
|
|
484
|
+
/>
|
|
485
|
+
))}
|
|
486
|
+
<Button
|
|
487
|
+
variant="outline"
|
|
488
|
+
onClick={() => {
|
|
489
|
+
onOpenChange(false);
|
|
490
|
+
toast.success(`Đã tạo kiện mới với ${count} item`);
|
|
491
|
+
}}
|
|
492
|
+
className="text-primary hover:border-primary h-11 w-full border-2 border-dashed"
|
|
493
|
+
>
|
|
494
|
+
<PackagePlus aria-hidden="true" strokeWidth={1.5} />
|
|
495
|
+
Tạo kiện mới với {count} item này
|
|
496
|
+
</Button>
|
|
497
|
+
</SheetBody>
|
|
498
|
+
<SheetFooter>
|
|
499
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
500
|
+
Huỷ
|
|
501
|
+
</Button>
|
|
502
|
+
</SheetFooter>
|
|
503
|
+
</SheetContent>
|
|
504
|
+
</Sheet>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const DESTINATIONS = [
|
|
509
|
+
{ id: "unsorted", label: "Chưa phân loại", hint: "Để xử lý sau" },
|
|
510
|
+
{ id: "shelf", label: "Giá hàng", hint: "Lên kệ lưu trữ" },
|
|
511
|
+
{ id: "open", label: "Kiện đang mở", hint: "Gói cùng đơn hiện tại" },
|
|
512
|
+
{ id: "new", label: "Tạo kiện mới", hint: "Mở kiện mới cho item" },
|
|
513
|
+
] as const;
|
|
514
|
+
|
|
515
|
+
function ItemFormSheet({
|
|
516
|
+
open,
|
|
517
|
+
onOpenChange,
|
|
518
|
+
}: {
|
|
519
|
+
open: boolean;
|
|
520
|
+
onOpenChange: (v: boolean) => void;
|
|
521
|
+
}) {
|
|
522
|
+
const [dest, setDest] = React.useState<string>("unsorted");
|
|
523
|
+
const [qty, setQty] = React.useState("1");
|
|
524
|
+
return (
|
|
525
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
526
|
+
<SheetContent side="bottom" className="max-h-[calc(80vh)] rounded-t-2xl">
|
|
527
|
+
<SheetHeader>
|
|
528
|
+
<SheetTitle>Thêm hàng mới</SheetTitle>
|
|
529
|
+
</SheetHeader>
|
|
530
|
+
<SheetBody className="flex flex-col gap-4">
|
|
531
|
+
<Flex direction="col" gap="xs">
|
|
532
|
+
<Text size="sm" weight="medium">
|
|
533
|
+
Mã vạch
|
|
534
|
+
</Text>
|
|
535
|
+
<Input defaultValue="JAN 4987241135219" readOnly className="font-mono" />
|
|
536
|
+
</Flex>
|
|
537
|
+
<Flex direction="col" gap="xs">
|
|
538
|
+
<Text size="sm" weight="medium">
|
|
539
|
+
Tên hàng (tùy chọn)
|
|
540
|
+
</Text>
|
|
541
|
+
<Input placeholder="Nhập tên hàng" autoComplete="off" />
|
|
542
|
+
</Flex>
|
|
543
|
+
<Flex direction="col" gap="xs">
|
|
544
|
+
<Text size="sm" weight="medium">
|
|
545
|
+
Số lượng
|
|
546
|
+
</Text>
|
|
547
|
+
<Input
|
|
548
|
+
type="number"
|
|
549
|
+
value={qty}
|
|
550
|
+
min={1}
|
|
551
|
+
onChange={(e) => setQty(e.target.value)}
|
|
552
|
+
className="w-24 tabular-nums"
|
|
553
|
+
/>
|
|
554
|
+
</Flex>
|
|
555
|
+
<Flex direction="col" gap="sm">
|
|
556
|
+
<Text size="sm" weight="medium">
|
|
557
|
+
Đích đến
|
|
558
|
+
</Text>
|
|
559
|
+
<RadioGroupRoot value={dest} onValueChange={setDest} className="flex flex-col gap-2">
|
|
560
|
+
{DESTINATIONS.map((d) => {
|
|
561
|
+
const checked = dest === d.id;
|
|
562
|
+
const rowId = `handy-dest-${d.id}`;
|
|
563
|
+
return (
|
|
564
|
+
<label
|
|
565
|
+
key={d.id}
|
|
566
|
+
htmlFor={rowId}
|
|
567
|
+
className={
|
|
568
|
+
"flex cursor-pointer items-center gap-3 rounded-xl border p-3 transition-colors " +
|
|
569
|
+
(checked
|
|
570
|
+
? "border-primary bg-[color-mix(in_oklch,var(--primary)_8%,transparent)]"
|
|
571
|
+
: "hover:border-primary")
|
|
572
|
+
}
|
|
573
|
+
>
|
|
574
|
+
<RadioItem id={rowId} value={d.id} className="mt-0.5" />
|
|
575
|
+
<span className="min-w-0">
|
|
576
|
+
<Text as="span" size="sm" weight="medium" className="block">
|
|
577
|
+
{d.label}
|
|
578
|
+
</Text>
|
|
579
|
+
<Text as="span" size="xs" tone="muted" className="block">
|
|
580
|
+
{d.hint}
|
|
581
|
+
</Text>
|
|
582
|
+
</span>
|
|
583
|
+
</label>
|
|
584
|
+
);
|
|
585
|
+
})}
|
|
586
|
+
</RadioGroupRoot>
|
|
587
|
+
</Flex>
|
|
588
|
+
</SheetBody>
|
|
589
|
+
<SheetFooter className="flex-row gap-2">
|
|
590
|
+
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>
|
|
591
|
+
Huỷ
|
|
592
|
+
</Button>
|
|
593
|
+
<Button
|
|
594
|
+
className="flex-[2]"
|
|
595
|
+
onClick={() => {
|
|
596
|
+
onOpenChange(false);
|
|
597
|
+
toast.success("Đã lưu hàng mới");
|
|
598
|
+
}}
|
|
599
|
+
>
|
|
600
|
+
Lưu
|
|
601
|
+
</Button>
|
|
602
|
+
</SheetFooter>
|
|
603
|
+
</SheetContent>
|
|
604
|
+
</Sheet>
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ── Tab bodies ───────────────────────────────────────────────────────────────
|
|
609
|
+
|
|
610
|
+
type ListState = "ready" | "loading" | "empty" | "error";
|
|
611
|
+
|
|
612
|
+
function InboundTab({
|
|
613
|
+
state,
|
|
614
|
+
onRetry,
|
|
615
|
+
selectMode,
|
|
616
|
+
setSelectMode,
|
|
617
|
+
selected,
|
|
618
|
+
setSelected,
|
|
619
|
+
onScan,
|
|
620
|
+
onAdd,
|
|
621
|
+
onAssign,
|
|
622
|
+
}: {
|
|
623
|
+
state: ListState;
|
|
624
|
+
onRetry: () => void;
|
|
625
|
+
selectMode: boolean;
|
|
626
|
+
setSelectMode: (v: boolean) => void;
|
|
627
|
+
selected: Set<string>;
|
|
628
|
+
setSelected: React.Dispatch<React.SetStateAction<Set<string>>>;
|
|
629
|
+
onScan: () => void;
|
|
630
|
+
onAdd: () => void;
|
|
631
|
+
onAssign: () => void;
|
|
632
|
+
}) {
|
|
633
|
+
const [filter, setFilter] = React.useState<string>("all");
|
|
634
|
+
|
|
635
|
+
const visible = ITEMS.filter((i) =>
|
|
636
|
+
filter === "all"
|
|
637
|
+
? true
|
|
638
|
+
: filter === "unsorted"
|
|
639
|
+
? i.status === "unsorted"
|
|
640
|
+
: i.status === "shelf",
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<>
|
|
645
|
+
<div className="flex flex-1 flex-col gap-3 overflow-y-auto p-4">
|
|
646
|
+
{/* Filter chips — horizontal scroll, count pills */}
|
|
647
|
+
<ToggleGroup
|
|
648
|
+
type="single"
|
|
649
|
+
value={filter}
|
|
650
|
+
onValueChange={(v) => {
|
|
651
|
+
if (v) setFilter(v);
|
|
652
|
+
}}
|
|
653
|
+
className="flex w-full justify-start gap-2 overflow-x-auto"
|
|
654
|
+
>
|
|
655
|
+
{FILTERS.map((f) => (
|
|
656
|
+
<ToggleGroupItem
|
|
657
|
+
key={f.id}
|
|
658
|
+
value={f.id}
|
|
659
|
+
className="h-9 shrink-0 gap-1.5 rounded-full px-3 whitespace-nowrap text-[var(--font-size-xs)]"
|
|
660
|
+
>
|
|
661
|
+
{f.label}
|
|
662
|
+
<Text size="xs" mono tabular className="opacity-70">
|
|
663
|
+
{f.count}
|
|
664
|
+
</Text>
|
|
665
|
+
</ToggleGroupItem>
|
|
666
|
+
))}
|
|
667
|
+
</ToggleGroup>
|
|
668
|
+
|
|
669
|
+
{state === "loading" ? (
|
|
670
|
+
<Flex direction="col" gap="md" aria-busy="true">
|
|
671
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
672
|
+
<Card key={i} density="tight" className="rounded-xl">
|
|
673
|
+
<CardContent solo>
|
|
674
|
+
<Skeleton className="h-4 w-3/5" />
|
|
675
|
+
<Skeleton className="mt-2 h-3 w-2/5" />
|
|
676
|
+
<Skeleton className="mt-3 h-5 w-24 rounded-full" />
|
|
677
|
+
</CardContent>
|
|
678
|
+
</Card>
|
|
679
|
+
))}
|
|
680
|
+
</Flex>
|
|
681
|
+
) : state === "error" ? (
|
|
682
|
+
<Alert tone="warning">
|
|
683
|
+
<AlertTitle>Không tải được danh sách</AlertTitle>
|
|
684
|
+
<AlertDescription className="flex flex-col items-start gap-2">
|
|
685
|
+
Kiểm tra kết nối rồi thử lại.
|
|
686
|
+
<Button variant="outline" size="sm" onClick={onRetry}>
|
|
687
|
+
<RefreshCw aria-hidden="true" />
|
|
688
|
+
Thử lại
|
|
689
|
+
</Button>
|
|
690
|
+
</AlertDescription>
|
|
691
|
+
</Alert>
|
|
692
|
+
) : state === "empty" || visible.length === 0 ? (
|
|
693
|
+
<EmptyState
|
|
694
|
+
icon={Boxes}
|
|
695
|
+
title="Chưa có hàng trong mục này"
|
|
696
|
+
description="Quét mã để nhập hàng mới vào kho."
|
|
697
|
+
action={
|
|
698
|
+
<Button size="sm" onClick={onScan}>
|
|
699
|
+
<ScanLine aria-hidden="true" />
|
|
700
|
+
Quét / Tìm mã
|
|
701
|
+
</Button>
|
|
702
|
+
}
|
|
703
|
+
/>
|
|
704
|
+
) : (
|
|
705
|
+
<Flex direction="col" gap="md">
|
|
706
|
+
{visible.map((item) => (
|
|
707
|
+
<ItemListCard
|
|
708
|
+
key={item.id}
|
|
709
|
+
item={item}
|
|
710
|
+
selectMode={selectMode}
|
|
711
|
+
selected={selected.has(item.id)}
|
|
712
|
+
onToggle={() =>
|
|
713
|
+
setSelected((prev) => {
|
|
714
|
+
const next = new Set(prev);
|
|
715
|
+
if (next.has(item.id)) next.delete(item.id);
|
|
716
|
+
else next.add(item.id);
|
|
717
|
+
return next;
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
/>
|
|
721
|
+
))}
|
|
722
|
+
</Flex>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
{/* Sticky action bar (scan-first) OR select-mode contextual bar */}
|
|
727
|
+
{selectMode ? (
|
|
728
|
+
<div className="shrink-0 border-t bg-[color-mix(in_oklch,var(--primary)_5%,var(--background))] p-3">
|
|
729
|
+
<Flex align="center" justify="between" className="mb-2">
|
|
730
|
+
<Text size="sm" tabular>
|
|
731
|
+
<Text as="span" weight="bold">
|
|
732
|
+
{selected.size}
|
|
733
|
+
</Text>{" "}
|
|
734
|
+
item đã chọn
|
|
735
|
+
</Text>
|
|
736
|
+
<Button
|
|
737
|
+
variant="ghost"
|
|
738
|
+
size="sm"
|
|
739
|
+
onClick={() => {
|
|
740
|
+
setSelectMode(false);
|
|
741
|
+
setSelected(new Set());
|
|
742
|
+
}}
|
|
743
|
+
>
|
|
744
|
+
Xong
|
|
745
|
+
</Button>
|
|
746
|
+
</Flex>
|
|
747
|
+
<Flex gap="sm">
|
|
748
|
+
<Button className="flex-[2]" disabled={selected.size === 0} onClick={onAssign}>
|
|
749
|
+
<Package aria-hidden="true" />
|
|
750
|
+
Gán vào kiện…
|
|
751
|
+
</Button>
|
|
752
|
+
<Button variant="outline" className="flex-1" disabled={selected.size === 0}>
|
|
753
|
+
Kiện mới
|
|
754
|
+
</Button>
|
|
755
|
+
</Flex>
|
|
756
|
+
</div>
|
|
757
|
+
) : (
|
|
758
|
+
<div className="flex shrink-0 gap-2 border-t p-3">
|
|
759
|
+
<Button className="flex-[2]" onClick={onScan}>
|
|
760
|
+
<ScanLine aria-hidden="true" />
|
|
761
|
+
Quét / Tìm mã
|
|
762
|
+
</Button>
|
|
763
|
+
<Button variant="outline" className="flex-1" onClick={onAdd}>
|
|
764
|
+
<Plus aria-hidden="true" />
|
|
765
|
+
Thêm hàng
|
|
766
|
+
</Button>
|
|
767
|
+
</div>
|
|
768
|
+
)}
|
|
769
|
+
</>
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function PackingTab({ onScan }: { onScan: () => void }) {
|
|
774
|
+
const active = PACKINGS.find((p) => p.status === "active");
|
|
775
|
+
const others = PACKINGS.filter((p) => p.status !== "active");
|
|
776
|
+
return (
|
|
777
|
+
<>
|
|
778
|
+
<div className="flex flex-1 flex-col gap-3 overflow-y-auto p-4">
|
|
779
|
+
{active ? (
|
|
780
|
+
<Card
|
|
781
|
+
density="tight"
|
|
782
|
+
accent="primary"
|
|
783
|
+
className="border-primary rounded-xl bg-[color-mix(in_oklch,var(--primary)_6%,transparent)]"
|
|
784
|
+
>
|
|
785
|
+
<CardContent solo>
|
|
786
|
+
<Flex align="center" justify="between">
|
|
787
|
+
<Text size="2xs" weight="medium" tone="muted" className="tracking-wider uppercase">
|
|
788
|
+
Kiện đang làm
|
|
789
|
+
</Text>
|
|
790
|
+
<Badge tone="info" variant="outline" className="rounded-full">
|
|
791
|
+
{PACKING_STATUS.active.label}
|
|
792
|
+
</Badge>
|
|
793
|
+
</Flex>
|
|
794
|
+
<Text as="div" size="lg" weight="bold" mono tabular className="mt-1">
|
|
795
|
+
{active.code}
|
|
796
|
+
</Text>
|
|
797
|
+
<Text size="xs" tone="muted" tabular as="div" className="mt-0.5">
|
|
798
|
+
{active.customer} · {active.city} · ×{active.items} · {active.slot}
|
|
799
|
+
</Text>
|
|
800
|
+
</CardContent>
|
|
801
|
+
</Card>
|
|
802
|
+
) : null}
|
|
803
|
+
|
|
804
|
+
<SectionHeader count={others.length}>Kiện đang mở</SectionHeader>
|
|
805
|
+
<Flex direction="col" gap="md">
|
|
806
|
+
{others.map((p) => (
|
|
807
|
+
<PackingListCard key={p.id} packing={p} onTap={() => undefined} />
|
|
808
|
+
))}
|
|
809
|
+
</Flex>
|
|
810
|
+
</div>
|
|
811
|
+
<div className="flex shrink-0 flex-col gap-2 border-t p-3">
|
|
812
|
+
<Button onClick={onScan}>
|
|
813
|
+
<ScanLine aria-hidden="true" />
|
|
814
|
+
Quét item vào kiện
|
|
815
|
+
</Button>
|
|
816
|
+
<Button variant="outline">
|
|
817
|
+
<Plus aria-hidden="true" />
|
|
818
|
+
Tạo kiện trống
|
|
819
|
+
</Button>
|
|
820
|
+
</div>
|
|
821
|
+
</>
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: () => void }) {
|
|
826
|
+
const [seg, setSeg] = React.useState<string>("seal");
|
|
827
|
+
return (
|
|
828
|
+
<>
|
|
829
|
+
<div className="flex flex-1 flex-col gap-3 overflow-y-auto p-4">
|
|
830
|
+
{/* Segmented — outbound status */}
|
|
831
|
+
<ToggleGroup
|
|
832
|
+
type="single"
|
|
833
|
+
value={seg}
|
|
834
|
+
onValueChange={(v) => {
|
|
835
|
+
if (v) setSeg(v);
|
|
836
|
+
}}
|
|
837
|
+
className="bg-secondary/60 grid w-full grid-cols-3 gap-1 rounded-xl p-1"
|
|
838
|
+
>
|
|
839
|
+
<ToggleGroupItem
|
|
840
|
+
value="seal"
|
|
841
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
842
|
+
>
|
|
843
|
+
Chờ niêm phong
|
|
844
|
+
</ToggleGroupItem>
|
|
845
|
+
<ToggleGroupItem
|
|
846
|
+
value="handoff"
|
|
847
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
848
|
+
>
|
|
849
|
+
Chờ bàn giao
|
|
850
|
+
</ToggleGroupItem>
|
|
851
|
+
<ToggleGroupItem
|
|
852
|
+
value="done"
|
|
853
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
854
|
+
>
|
|
855
|
+
Đã bàn giao
|
|
856
|
+
</ToggleGroupItem>
|
|
857
|
+
</ToggleGroup>
|
|
858
|
+
|
|
859
|
+
<SectionHeader count={OUTBOUND.length}>
|
|
860
|
+
{seg === "seal"
|
|
861
|
+
? "Sẵn sàng niêm phong"
|
|
862
|
+
: seg === "handoff"
|
|
863
|
+
? "Chờ bàn giao"
|
|
864
|
+
: "Đã bàn giao"}
|
|
865
|
+
</SectionHeader>
|
|
866
|
+
<Flex direction="col" gap="md">
|
|
867
|
+
{OUTBOUND.map((p) => (
|
|
868
|
+
<Card key={p.id} density="tight" className="rounded-xl">
|
|
869
|
+
<CardContent solo>
|
|
870
|
+
<Flex align="center" justify="between" gap="sm">
|
|
871
|
+
<Text as="code" size="sm" weight="bold" tabular>
|
|
872
|
+
{p.code}
|
|
873
|
+
</Text>
|
|
874
|
+
<Badge tone="success" variant="outline" className="rounded-full">
|
|
875
|
+
Sẵn sàng niêm phong
|
|
876
|
+
</Badge>
|
|
877
|
+
</Flex>
|
|
878
|
+
<Flex align="center" justify="between" gap="sm" className="mt-1">
|
|
879
|
+
<Text size="xs" tone="muted" tabular truncate>
|
|
880
|
+
{p.customer} · {p.city}
|
|
881
|
+
</Text>
|
|
882
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
883
|
+
×{p.items} · {p.slot}
|
|
884
|
+
</Text>
|
|
885
|
+
</Flex>
|
|
886
|
+
<div className="mt-3">
|
|
887
|
+
<Descriptions columns={1} className="gap-y-1">
|
|
888
|
+
<Descriptions.Item label="Vị trí" mono>
|
|
889
|
+
{p.slot}
|
|
890
|
+
</Descriptions.Item>
|
|
891
|
+
<Descriptions.Item label="Số kiện" mono>
|
|
892
|
+
×{p.items}
|
|
893
|
+
</Descriptions.Item>
|
|
894
|
+
</Descriptions>
|
|
895
|
+
</div>
|
|
896
|
+
</CardContent>
|
|
897
|
+
</Card>
|
|
898
|
+
))}
|
|
899
|
+
</Flex>
|
|
900
|
+
</div>
|
|
901
|
+
<div className="flex shrink-0 gap-2 border-t p-3">
|
|
902
|
+
{seg === "handoff" ? (
|
|
903
|
+
<Button className="flex-1" onClick={onHandoff}>
|
|
904
|
+
<Truck aria-hidden="true" />
|
|
905
|
+
Xác nhận bàn giao
|
|
906
|
+
</Button>
|
|
907
|
+
) : (
|
|
908
|
+
<Button className="flex-1" onClick={onSeal}>
|
|
909
|
+
<Check aria-hidden="true" />
|
|
910
|
+
Quét mã kiện · Niêm phong
|
|
911
|
+
</Button>
|
|
912
|
+
)}
|
|
913
|
+
</div>
|
|
914
|
+
</>
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ── Root ─────────────────────────────────────────────────────────────────────
|
|
919
|
+
|
|
920
|
+
export default function AgencyHandyShowcase() {
|
|
921
|
+
const [tab, setTab] = React.useState<string>("inbound");
|
|
922
|
+
const [listState, setListState] = React.useState<ListState>("ready");
|
|
923
|
+
|
|
924
|
+
const [selectMode, setSelectMode] = React.useState(false);
|
|
925
|
+
const [selected, setSelected] = React.useState<Set<string>>(new Set());
|
|
926
|
+
|
|
927
|
+
const [lookupOpen, setLookupOpen] = React.useState(false);
|
|
928
|
+
const [formOpen, setFormOpen] = React.useState(false);
|
|
929
|
+
const [pickerOpen, setPickerOpen] = React.useState(false);
|
|
930
|
+
|
|
931
|
+
const [sealOpen, setSealOpen] = React.useState(false);
|
|
932
|
+
const [handoffOpen, setHandoffOpen] = React.useState(false);
|
|
933
|
+
|
|
934
|
+
const headerTitle = tab === "inbound" ? "Nhập kho" : tab === "packing" ? "Đóng gói" : "Xuất kho";
|
|
935
|
+
|
|
936
|
+
return (
|
|
937
|
+
<div className="bg-secondary/30 min-h-screen py-4">
|
|
938
|
+
<Toaster />
|
|
939
|
+
<PhoneFrame>
|
|
940
|
+
<StatusBar />
|
|
941
|
+
|
|
942
|
+
{/* App header (52px) — title + iOS text-action select-mode entry (inbound only) */}
|
|
943
|
+
<header className="flex h-14 shrink-0 items-center justify-between border-b px-4">
|
|
944
|
+
<Heading level={3} as="h1" className="whitespace-nowrap">
|
|
945
|
+
{headerTitle}
|
|
946
|
+
</Heading>
|
|
947
|
+
<Flex align="center" gap="xs">
|
|
948
|
+
{tab === "inbound" && !selectMode ? (
|
|
949
|
+
<Button
|
|
950
|
+
variant="ghost"
|
|
951
|
+
size="sm"
|
|
952
|
+
onClick={() => {
|
|
953
|
+
setSelectMode(true);
|
|
954
|
+
setSelected(new Set());
|
|
955
|
+
}}
|
|
956
|
+
>
|
|
957
|
+
<Check aria-hidden="true" />
|
|
958
|
+
Chọn
|
|
959
|
+
</Button>
|
|
960
|
+
) : null}
|
|
961
|
+
{/* State switcher — exposes the 4 list states at rest (showcase affordance) */}
|
|
962
|
+
{tab === "inbound" && !selectMode ? (
|
|
963
|
+
<ToggleGroup
|
|
964
|
+
type="single"
|
|
965
|
+
value={listState}
|
|
966
|
+
onValueChange={(v) => {
|
|
967
|
+
if (v) setListState(v as ListState);
|
|
968
|
+
}}
|
|
969
|
+
aria-label="List state (showcase)"
|
|
970
|
+
className="flex gap-0.5"
|
|
971
|
+
>
|
|
972
|
+
{(["ready", "loading", "empty", "error"] as const).map((s) => (
|
|
973
|
+
<ToggleGroupItem
|
|
974
|
+
key={s}
|
|
975
|
+
value={s}
|
|
976
|
+
className="size-7 rounded-md p-0 text-[var(--font-size-2xs)] uppercase"
|
|
977
|
+
title={s}
|
|
978
|
+
>
|
|
979
|
+
{s[0]}
|
|
980
|
+
</ToggleGroupItem>
|
|
981
|
+
))}
|
|
982
|
+
</ToggleGroup>
|
|
983
|
+
) : null}
|
|
984
|
+
</Flex>
|
|
985
|
+
</header>
|
|
986
|
+
|
|
987
|
+
{/* Select-mode header strip — replaces the standard header context (inbound) */}
|
|
988
|
+
{selectMode ? (
|
|
989
|
+
<div className="flex h-10 shrink-0 items-center justify-between border-b bg-[color-mix(in_oklch,var(--primary)_5%,var(--background))] px-3">
|
|
990
|
+
<Button
|
|
991
|
+
variant="ghost"
|
|
992
|
+
size="icon-sm"
|
|
993
|
+
aria-label="閉じる"
|
|
994
|
+
onClick={() => {
|
|
995
|
+
setSelectMode(false);
|
|
996
|
+
setSelected(new Set());
|
|
997
|
+
}}
|
|
998
|
+
>
|
|
999
|
+
<X className="size-4" aria-hidden="true" strokeWidth={1.5} />
|
|
1000
|
+
</Button>
|
|
1001
|
+
<Text size="sm" mono tabular>
|
|
1002
|
+
{selected.size} đã chọn
|
|
1003
|
+
</Text>
|
|
1004
|
+
<Button
|
|
1005
|
+
variant="ghost"
|
|
1006
|
+
size="sm"
|
|
1007
|
+
onClick={() => setSelected(new Set(ITEMS.map((i) => i.id)))}
|
|
1008
|
+
>
|
|
1009
|
+
Chọn tất cả
|
|
1010
|
+
</Button>
|
|
1011
|
+
</div>
|
|
1012
|
+
) : null}
|
|
1013
|
+
|
|
1014
|
+
{tab === "inbound" ? (
|
|
1015
|
+
<InboundTab
|
|
1016
|
+
state={listState}
|
|
1017
|
+
onRetry={() => setListState("ready")}
|
|
1018
|
+
selectMode={selectMode}
|
|
1019
|
+
setSelectMode={setSelectMode}
|
|
1020
|
+
selected={selected}
|
|
1021
|
+
setSelected={setSelected}
|
|
1022
|
+
onScan={() => setLookupOpen(true)}
|
|
1023
|
+
onAdd={() => setFormOpen(true)}
|
|
1024
|
+
onAssign={() => setPickerOpen(true)}
|
|
1025
|
+
/>
|
|
1026
|
+
) : tab === "packing" ? (
|
|
1027
|
+
<PackingTab onScan={() => setLookupOpen(true)} />
|
|
1028
|
+
) : (
|
|
1029
|
+
<OutboundTab onSeal={() => setSealOpen(true)} onHandoff={() => setHandoffOpen(true)} />
|
|
1030
|
+
)}
|
|
1031
|
+
|
|
1032
|
+
<TabBar
|
|
1033
|
+
active={tab}
|
|
1034
|
+
onChange={(id) => {
|
|
1035
|
+
setTab(id);
|
|
1036
|
+
setSelectMode(false);
|
|
1037
|
+
setSelected(new Set());
|
|
1038
|
+
}}
|
|
1039
|
+
/>
|
|
1040
|
+
</PhoneFrame>
|
|
1041
|
+
|
|
1042
|
+
{/* Sheets */}
|
|
1043
|
+
<ItemLookupSheet open={lookupOpen} onOpenChange={setLookupOpen} />
|
|
1044
|
+
<ItemFormSheet open={formOpen} onOpenChange={setFormOpen} />
|
|
1045
|
+
<PackagePickerSheet open={pickerOpen} onOpenChange={setPickerOpen} count={selected.size} />
|
|
1046
|
+
|
|
1047
|
+
{/* Confirm dialogs — verb confirm, surface summary; forward step = primary */}
|
|
1048
|
+
<AlertDialog
|
|
1049
|
+
open={sealOpen}
|
|
1050
|
+
onOpenChange={setSealOpen}
|
|
1051
|
+
title="Niêm phong kiện?"
|
|
1052
|
+
description="PKG-000037 · 9 item · vị trí B-01-04. Sau khi niêm phong không thể thêm item."
|
|
1053
|
+
confirmLabel="Niêm phong"
|
|
1054
|
+
cancelLabel="Huỷ"
|
|
1055
|
+
onConfirm={() => {
|
|
1056
|
+
toast.success("Đã niêm phong PKG-000037 · 16:24");
|
|
1057
|
+
}}
|
|
1058
|
+
/>
|
|
1059
|
+
<AlertDialog
|
|
1060
|
+
open={handoffOpen}
|
|
1061
|
+
onOpenChange={setHandoffOpen}
|
|
1062
|
+
title="Xác nhận bàn giao?"
|
|
1063
|
+
description="PKG-000037 lên xe giao. Thao tác này có thể hoàn tác trong 10 phút."
|
|
1064
|
+
confirmLabel="Xác nhận"
|
|
1065
|
+
cancelLabel="Huỷ"
|
|
1066
|
+
onConfirm={() => {
|
|
1067
|
+
toast.success("Đã bàn giao PKG-000037 · 16:24");
|
|
1068
|
+
}}
|
|
1069
|
+
/>
|
|
1070
|
+
</div>
|
|
1071
|
+
);
|
|
1072
|
+
}
|