@godxjp/ui 19.1.0 → 19.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (437) hide show
  1. package/README.md +25 -128
  2. package/dist/app/theme-axes.d.ts +4 -15
  3. package/dist/app/timezones.d.ts +1 -4
  4. package/dist/components/charts/area-chart.d.ts +2 -3
  5. package/dist/components/charts/bar-chart.d.ts +2 -3
  6. package/dist/components/charts/chart-frame.d.ts +7 -16
  7. package/dist/components/charts/chart-summary.d.ts +2 -2
  8. package/dist/components/charts/compact-bar-trend.d.ts +9 -24
  9. package/dist/components/charts/index.d.ts +1 -6
  10. package/dist/components/data-display/badge.d.ts +20 -14
  11. package/dist/components/data-display/badge.js +18 -4
  12. package/dist/components/data-display/card.d.ts +21 -41
  13. package/dist/components/data-display/carousel.d.ts +1 -2
  14. package/dist/components/data-display/code-block.d.ts +4 -0
  15. package/dist/components/data-display/code-block.js +24 -0
  16. package/dist/components/data-display/data-table.d.ts +15 -37
  17. package/dist/components/data-display/data-table.js +1 -3
  18. package/dist/components/data-display/descriptions.d.ts +2 -6
  19. package/dist/components/data-display/descriptions.js +0 -2
  20. package/dist/components/data-display/index.d.ts +4 -0
  21. package/dist/components/data-display/index.js +4 -0
  22. package/dist/components/data-display/list-row.d.ts +3 -22
  23. package/dist/components/data-display/permission-matrix.d.ts +7 -20
  24. package/dist/components/data-display/progress.d.ts +1 -1
  25. package/dist/components/data-display/prose.d.ts +4 -0
  26. package/dist/components/data-display/prose.js +20 -0
  27. package/dist/components/data-display/service-launcher-card.d.ts +3 -10
  28. package/dist/components/data-display/table.d.ts +20 -80
  29. package/dist/components/data-display/table.js +0 -3
  30. package/dist/components/data-display/timeline.d.ts +3 -5
  31. package/dist/components/data-entry/branch-scope-picker.d.ts +2 -19
  32. package/dist/components/data-entry/branch-scope-picker.js +0 -1
  33. package/dist/components/data-entry/calendar.d.ts +3 -2
  34. package/dist/components/data-entry/calendar.js +58 -3
  35. package/dist/components/data-entry/command-palette.d.ts +8 -22
  36. package/dist/components/data-entry/date-picker.d.ts +4 -5
  37. package/dist/components/data-entry/date-picker.js +6 -1
  38. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  39. package/dist/components/data-entry/date-range-picker.js +8 -3
  40. package/dist/components/data-entry/form-errors.d.ts +7 -18
  41. package/dist/components/data-entry/form-field.js +1 -5
  42. package/dist/components/data-entry/form.d.ts +1 -9
  43. package/dist/components/data-entry/input.d.ts +16 -14
  44. package/dist/components/data-entry/input.js +3 -0
  45. package/dist/components/data-entry/month-picker.js +2 -2
  46. package/dist/components/data-entry/month-range-picker.d.ts +4 -6
  47. package/dist/components/data-entry/month-range-picker.js +2 -2
  48. package/dist/components/data-entry/number-input.d.ts +1 -5
  49. package/dist/components/data-entry/number-input.js +1 -0
  50. package/dist/components/data-entry/radio.js +0 -1
  51. package/dist/components/data-entry/search-select.d.ts +2 -4
  52. package/dist/components/data-entry/select.d.ts +11 -6
  53. package/dist/components/data-entry/select.js +4 -2
  54. package/dist/components/data-entry/switch.js +0 -1
  55. package/dist/components/data-entry/textarea.d.ts +10 -16
  56. package/dist/components/data-entry/time-picker.d.ts +3 -5
  57. package/dist/components/data-entry/upload-types.d.ts +1 -5
  58. package/dist/components/data-entry/use-upload-draft.d.ts +0 -4
  59. package/dist/components/feedback/dialog.d.ts +6 -15
  60. package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
  61. package/dist/components/feedback/sheet.d.ts +3 -10
  62. package/dist/components/feedback/sheet.js +1 -1
  63. package/dist/components/feedback/sonner.js +2 -3
  64. package/dist/components/general/activity.d.ts +4 -30
  65. package/dist/components/general/button.js +1 -10
  66. package/dist/components/general/logo.d.ts +6 -28
  67. package/dist/components/general/reveal.d.ts +3 -9
  68. package/dist/components/general/typography.d.ts +1 -3
  69. package/dist/components/layout/account-chip.d.ts +8 -0
  70. package/dist/components/layout/account-chip.js +45 -0
  71. package/dist/components/layout/auth-divider.d.ts +2 -9
  72. package/dist/components/layout/auth-shell.d.ts +1 -14
  73. package/dist/components/layout/breadcrumb.d.ts +1 -5
  74. package/dist/components/layout/centered-shell.d.ts +4 -18
  75. package/dist/components/layout/error-surface.d.ts +11 -28
  76. package/dist/components/layout/error-surface.js +4 -1
  77. package/dist/components/layout/index.d.ts +2 -0
  78. package/dist/components/layout/index.js +2 -0
  79. package/dist/components/layout/legal-document-shell.d.ts +5 -28
  80. package/dist/components/layout/master-detail.d.ts +2 -18
  81. package/dist/components/layout/page-container.js +0 -1
  82. package/dist/components/layout/responsive-grid.d.ts +6 -1
  83. package/dist/components/layout/responsive-grid.js +9 -2
  84. package/dist/components/layout/service-role-panel.d.ts +7 -19
  85. package/dist/components/layout/sidebar.d.ts +5 -27
  86. package/dist/components/layout/sidebar.js +0 -1
  87. package/dist/components/layout/split-pane.d.ts +0 -7
  88. package/dist/components/layout/topbar.d.ts +1 -14
  89. package/dist/components/navigation/app-setting-picker.js +1 -3
  90. package/dist/components/navigation/filter-bar.d.ts +1 -6
  91. package/dist/components/navigation/pagination-utils.d.ts +2 -12
  92. package/dist/components/navigation/tabs-scroll.d.ts +2 -36
  93. package/dist/components/navigation/tabs.js +4 -8
  94. package/dist/components/query/data-state.d.ts +2 -2
  95. package/dist/components/ui/credential-reveal.d.ts +1 -4
  96. package/dist/components/ui/hover-card.d.ts +1 -2
  97. package/dist/components/ui/separator.d.ts +2 -15
  98. package/dist/components/ui/toggle-group.d.ts +1 -5
  99. package/dist/components/ui/toggle.d.ts +6 -31
  100. package/dist/email/brand-mark.d.ts +6 -34
  101. package/dist/email/color.d.ts +7 -17
  102. package/dist/email/geometry.d.ts +4 -8
  103. package/dist/email/index.d.ts +4 -48
  104. package/dist/email/inline-style.d.ts +3 -9
  105. package/dist/email/urgency.d.ts +2 -7
  106. package/dist/form/form-context.d.ts +2 -5
  107. package/dist/form/form-root.d.ts +1 -2
  108. package/dist/i18n/messages/en.json +4 -0
  109. package/dist/i18n/messages/ja.json +4 -0
  110. package/dist/i18n/messages/vi.json +4 -0
  111. package/dist/inertia/index.d.ts +2 -10
  112. package/dist/lib/control-styles.d.ts +10 -21
  113. package/dist/lib/control-styles.js +4 -6
  114. package/dist/lib/datetime/format-date.d.ts +1 -4
  115. package/dist/lib/field-a11y.d.ts +11 -68
  116. package/dist/lib/field-a11y.js +0 -1
  117. package/dist/lib/format.d.ts +4 -8
  118. package/dist/lib/hooks.d.ts +6 -12
  119. package/dist/lib/permission-grid.d.ts +3 -18
  120. package/dist/lib/query-error.d.ts +3 -3
  121. package/dist/props/components/app.prop.d.ts +15 -38
  122. package/dist/props/components/charts.prop.d.ts +2 -3
  123. package/dist/props/components/data-display.prop.d.ts +55 -72
  124. package/dist/props/components/data-entry.prop.d.ts +35 -40
  125. package/dist/props/components/form.prop.d.ts +2 -6
  126. package/dist/props/components/general.prop.d.ts +11 -27
  127. package/dist/props/components/layout.prop.d.ts +89 -260
  128. package/dist/props/components/navigation.prop.d.ts +27 -42
  129. package/dist/props/registry.d.ts +36 -1
  130. package/dist/props/registry.js +45 -1
  131. package/dist/props/vocabulary/data.prop.d.ts +18 -49
  132. package/dist/props/vocabulary/index.d.ts +1 -1
  133. package/dist/props/vocabulary/interaction.prop.d.ts +10 -12
  134. package/dist/props/vocabulary/layout.prop.d.ts +8 -30
  135. package/dist/props/vocabulary/shared.prop.d.ts +3 -2
  136. package/dist/styles/alert-layout.css +1 -32
  137. package/dist/styles/badge-layout.css +19 -18
  138. package/dist/styles/base.css +14 -113
  139. package/dist/styles/card-layout.css +6 -128
  140. package/dist/styles/chart-layout.css +2 -17
  141. package/dist/styles/control.css +380 -458
  142. package/dist/styles/core.css +21 -0
  143. package/dist/styles/data-display-layout.css +175 -94
  144. package/dist/styles/data-entry-layout.css +0 -45
  145. package/dist/styles/density.css +1 -30
  146. package/dist/styles/dialog-layout.css +5 -69
  147. package/dist/styles/focus-ring.css +13 -87
  148. package/dist/styles/fonts.css +1 -31
  149. package/dist/styles/form-layout.css +7 -91
  150. package/dist/styles/index.css +2 -20
  151. package/dist/styles/layout.css +38 -456
  152. package/dist/styles/logo-layout.css +1 -35
  153. package/dist/styles/motion.css +3 -42
  154. package/dist/styles/navigation-layout.css +3 -50
  155. package/dist/styles/shell-layout.css +37 -403
  156. package/dist/styles/table-layout.css +16 -274
  157. package/dist/styles/text-layout.css +7 -21
  158. package/dist/styles/toggle.css +1 -23
  159. package/dist/theme/dxs.canonical.css +1 -55
  160. package/dist/theme/example.service.css +1 -26
  161. package/dist/theme/famgia.service.css +5 -15
  162. package/dist/tokens/axes.css +3 -40
  163. package/dist/tokens/base.css +1 -4
  164. package/dist/tokens/components/activity.css +6 -42
  165. package/dist/tokens/components/badge.css +12 -13
  166. package/dist/tokens/components/banner.css +4 -9
  167. package/dist/tokens/components/card.css +32 -88
  168. package/dist/tokens/components/chart.css +9 -26
  169. package/dist/tokens/components/control.css +37 -222
  170. package/dist/tokens/components/data-display.css +28 -79
  171. package/dist/tokens/components/data-entry.css +5 -19
  172. package/dist/tokens/components/descriptions.css +7 -27
  173. package/dist/tokens/components/email.css +38 -84
  174. package/dist/tokens/components/error-surface.css +2 -18
  175. package/dist/tokens/components/feedback.css +27 -124
  176. package/dist/tokens/components/form.css +13 -45
  177. package/dist/tokens/components/legal-document.css +16 -41
  178. package/dist/tokens/components/list-row.css +11 -36
  179. package/dist/tokens/components/logo.css +2 -57
  180. package/dist/tokens/components/navigation.css +16 -94
  181. package/dist/tokens/components/scroll-area.css +2 -18
  182. package/dist/tokens/components/separator.css +8 -30
  183. package/dist/tokens/components/sheet.css +3 -14
  184. package/dist/tokens/components/shell.css +53 -307
  185. package/dist/tokens/components/sidebar.css +5 -22
  186. package/dist/tokens/components/table.css +19 -140
  187. package/dist/tokens/components/toggle.css +3 -65
  188. package/dist/tokens/components/upload.css +7 -43
  189. package/dist/tokens/foundation.css +108 -383
  190. package/dist/tokens/semantic/layout.css +33 -207
  191. package/docs/CANONICAL-CONTRACTS.md +206 -0
  192. package/docs/COMPONENTS.md +157 -0
  193. package/docs/COMPOSITION-VS-COMPONENT.md +105 -0
  194. package/docs/CONSUMER-RULES.md +14 -0
  195. package/docs/CUSTOMER-THEMING.md +162 -0
  196. package/docs/DATETIME.md +69 -0
  197. package/docs/DEVELOPMENT.md +173 -0
  198. package/docs/FORMS.md +143 -0
  199. package/docs/FRAME-A11Y-CI.md +267 -0
  200. package/docs/FRAME-COVERAGE-LEDGER.md +123 -0
  201. package/docs/FRAME-COVERAGE-REPORT.md +140 -0
  202. package/docs/FRAME-COVERAGE-STANDARD.md +65 -0
  203. package/docs/FRAME-COVERAGE.md +29 -0
  204. package/docs/PROPS-REGISTRY.md +61 -0
  205. package/docs/PROPS-VOCABULARY.md +100 -0
  206. package/docs/README.md +60 -0
  207. package/docs/REGISTRY.md +70 -0
  208. package/docs/SPACING.md +56 -0
  209. package/docs/STANDARDS-vocabulary-tokens.md +48 -0
  210. package/docs/TESTING.md +98 -0
  211. package/docs/TOKENS.md +347 -0
  212. package/docs/asset-modules.d.ts +46 -0
  213. package/docs/assets/cover-terrain.svg +6 -0
  214. package/docs/assets/portrait-amber.svg +5 -0
  215. package/docs/assets/portrait-clay.svg +5 -0
  216. package/docs/assets/portrait-indigo.svg +5 -0
  217. package/docs/assets/portrait-iris.svg +5 -0
  218. package/docs/assets/portrait-moss.svg +5 -0
  219. package/docs/assets/portrait-plum.svg +5 -0
  220. package/docs/assets/portrait-slate.svg +5 -0
  221. package/docs/charts/compact-bar-trend.tsx +328 -0
  222. package/docs/charts/size-tiers.tsx +192 -0
  223. package/docs/data-display/accordion.tsx +99 -0
  224. package/docs/data-display/avatar.tsx +355 -0
  225. package/docs/data-display/badge.tsx +283 -0
  226. package/docs/data-display/card/examples/detail-panel.tsx +106 -0
  227. package/docs/data-display/card/index.md +3 -0
  228. package/docs/data-display/card/index.tsx +285 -0
  229. package/docs/data-display/carousel.tsx +160 -0
  230. package/docs/data-display/charts.tsx +198 -0
  231. package/docs/data-display/code-block.tsx +90 -0
  232. package/docs/data-display/collapsible.tsx +162 -0
  233. package/docs/data-display/credential-reveal.tsx +145 -0
  234. package/docs/data-display/data-table/examples/approval-queue.tsx +212 -0
  235. package/docs/data-display/data-table/examples/grid-features.tsx +158 -0
  236. package/docs/data-display/data-table/examples/list-page.tsx +175 -0
  237. package/docs/data-display/data-table/index.md +5 -0
  238. package/docs/data-display/data-table/index.tsx +295 -0
  239. package/docs/data-display/descriptions.tsx +143 -0
  240. package/docs/data-display/empty-state.tsx +164 -0
  241. package/docs/data-display/hover-card.tsx +181 -0
  242. package/docs/data-display/list-row.tsx +420 -0
  243. package/docs/data-display/permission-matrix.tsx +188 -0
  244. package/docs/data-display/popover-touch.tsx +27 -0
  245. package/docs/data-display/popover.tsx +246 -0
  246. package/docs/data-display/progress.tsx +84 -0
  247. package/docs/data-display/prose.tsx +105 -0
  248. package/docs/data-display/qr-code.tsx +68 -0
  249. package/docs/data-display/scroll-area.tsx +303 -0
  250. package/docs/data-display/service-launcher-card.tsx +145 -0
  251. package/docs/data-display/stat-card.tsx +99 -0
  252. package/docs/data-display/table.tsx +251 -0
  253. package/docs/data-display/timeline.tsx +112 -0
  254. package/docs/data-display/touch-actions.tsx +36 -0
  255. package/docs/data-display/tree-list.tsx +107 -0
  256. package/docs/data-entry/branch-scope-picker.tsx +123 -0
  257. package/docs/data-entry/calendar.tsx +180 -0
  258. package/docs/data-entry/cascader.tsx +378 -0
  259. package/docs/data-entry/checkbox.tsx +256 -0
  260. package/docs/data-entry/color-picker.tsx +117 -0
  261. package/docs/data-entry/command-palette.tsx +179 -0
  262. package/docs/data-entry/command.tsx +262 -0
  263. package/docs/data-entry/country-picker-recipe.tsx +175 -0
  264. package/docs/data-entry/date-picker.tsx +191 -0
  265. package/docs/data-entry/date-range-picker.tsx +149 -0
  266. package/docs/data-entry/form/examples/employee-settings.tsx +267 -0
  267. package/docs/data-entry/form/examples/invoice-form.tsx +387 -0
  268. package/docs/data-entry/form-field/examples/a11y-contract.tsx +240 -0
  269. package/docs/data-entry/form-field/examples/create-form.tsx +105 -0
  270. package/docs/data-entry/form-field/index.md +8 -0
  271. package/docs/data-entry/form-field/index.tsx +330 -0
  272. package/docs/data-entry/form-inertia.tsx +80 -0
  273. package/docs/data-entry/form-root.tsx +102 -0
  274. package/docs/data-entry/form.tsx +724 -0
  275. package/docs/data-entry/input-otp.tsx +231 -0
  276. package/docs/data-entry/input.tsx +114 -0
  277. package/docs/data-entry/label.tsx +100 -0
  278. package/docs/data-entry/month-picker.tsx +101 -0
  279. package/docs/data-entry/month-range-picker.tsx +115 -0
  280. package/docs/data-entry/number-input.tsx +240 -0
  281. package/docs/data-entry/password-input.tsx +102 -0
  282. package/docs/data-entry/password-strength.tsx +97 -0
  283. package/docs/data-entry/radio-group.tsx +95 -0
  284. package/docs/data-entry/rating.tsx +87 -0
  285. package/docs/data-entry/search-input.tsx +120 -0
  286. package/docs/data-entry/select-async-contracts.tsx +108 -0
  287. package/docs/data-entry/select-matrix.tsx +204 -0
  288. package/docs/data-entry/select.tsx +369 -0
  289. package/docs/data-entry/slider.tsx +130 -0
  290. package/docs/data-entry/switch.tsx +67 -0
  291. package/docs/data-entry/tag-input.tsx +108 -0
  292. package/docs/data-entry/textarea.tsx +209 -0
  293. package/docs/data-entry/time-picker.tsx +99 -0
  294. package/docs/data-entry/toggle-count.tsx +293 -0
  295. package/docs/data-entry/toggle-group.tsx +225 -0
  296. package/docs/data-entry/toggle.tsx +127 -0
  297. package/docs/data-entry/transfer.tsx +97 -0
  298. package/docs/data-entry/tree-select.tsx +204 -0
  299. package/docs/data-entry/upload.tsx +184 -0
  300. package/docs/feedback/alert-dialog.tsx +270 -0
  301. package/docs/feedback/alert.tsx +248 -0
  302. package/docs/feedback/banner.tsx +183 -0
  303. package/docs/feedback/danger-confirm.tsx +88 -0
  304. package/docs/feedback/dialog-touch.tsx +24 -0
  305. package/docs/feedback/dialog.tsx +193 -0
  306. package/docs/feedback/sheet.tsx +420 -0
  307. package/docs/feedback/skeleton.tsx +111 -0
  308. package/docs/feedback/toast.tsx +135 -0
  309. package/docs/feedback/tooltip.tsx +171 -0
  310. package/docs/feedback/two-factor-setup.tsx +68 -0
  311. package/docs/foundation/_email-specimen.ts +274 -0
  312. package/docs/foundation/colors.tsx +204 -0
  313. package/docs/foundation/density.tsx +232 -0
  314. package/docs/foundation/email-tokens.tsx +323 -0
  315. package/docs/foundation/radius-shadow.tsx +154 -0
  316. package/docs/foundation/spacing.tsx +211 -0
  317. package/docs/foundation/typography.tsx +147 -0
  318. package/docs/general/activity.tsx +224 -0
  319. package/docs/general/button/examples/destructive-confirm.tsx +90 -0
  320. package/docs/general/button/examples/form-actions.tsx +85 -0
  321. package/docs/general/button/examples/toolbar.tsx +160 -0
  322. package/docs/general/button/index.md +12 -0
  323. package/docs/general/button/index.tsx +211 -0
  324. package/docs/general/logo.tsx +131 -0
  325. package/docs/general/reveal.tsx +41 -0
  326. package/docs/general/typography.tsx +152 -0
  327. package/docs/layout/account-chip.tsx +72 -0
  328. package/docs/layout/admin-collection.tsx +122 -0
  329. package/docs/layout/app-shell.tsx +374 -0
  330. package/docs/layout/aspect-ratio.tsx +135 -0
  331. package/docs/layout/auth-account-summary.tsx +103 -0
  332. package/docs/layout/auth-divider.tsx +28 -0
  333. package/docs/layout/auth-footer.tsx +37 -0
  334. package/docs/layout/auth-identity.tsx +36 -0
  335. package/docs/layout/auth-recovery/examples/mfa-challenge.md +41 -0
  336. package/docs/layout/auth-recovery/examples/mfa-challenge.tsx +254 -0
  337. package/docs/layout/auth-recovery/examples/mobile-390.md +25 -0
  338. package/docs/layout/auth-recovery/examples/mobile-390.tsx +82 -0
  339. package/docs/layout/auth-recovery/examples/password-recovery.md +24 -0
  340. package/docs/layout/auth-recovery/examples/password-recovery.tsx +197 -0
  341. package/docs/layout/auth-recovery/examples/wrapping-ja-en-vi.md +27 -0
  342. package/docs/layout/auth-recovery/examples/wrapping-ja-en-vi.tsx +156 -0
  343. package/docs/layout/auth-recovery/index.md +140 -0
  344. package/docs/layout/auth-recovery/index.tsx +96 -0
  345. package/docs/layout/auth-shell-context.tsx +103 -0
  346. package/docs/layout/auth-shell-device.tsx +112 -0
  347. package/docs/layout/auth-shell-registration.tsx +335 -0
  348. package/docs/layout/auth-shell-variants.tsx +139 -0
  349. package/docs/layout/auth-shell.tsx +59 -0
  350. package/docs/layout/auth-stack.tsx +40 -0
  351. package/docs/layout/centered-shell.tsx +102 -0
  352. package/docs/layout/error-surface/examples/application-403.tsx +84 -0
  353. package/docs/layout/error-surface/examples/application-404.tsx +77 -0
  354. package/docs/layout/error-surface/examples/system-500.tsx +47 -0
  355. package/docs/layout/error-surface/examples/system-503.tsx +48 -0
  356. package/docs/layout/error-surface/index.md +153 -0
  357. package/docs/layout/error-surface/index.tsx +239 -0
  358. package/docs/layout/flex.tsx +266 -0
  359. package/docs/layout/legal-document-shell/_data.ts +110 -0
  360. package/docs/layout/legal-document-shell/examples/desktop-1440.md +12 -0
  361. package/docs/layout/legal-document-shell/examples/desktop-1440.tsx +83 -0
  362. package/docs/layout/legal-document-shell/examples/mobile-390.md +22 -0
  363. package/docs/layout/legal-document-shell/examples/mobile-390.tsx +70 -0
  364. package/docs/layout/legal-document-shell/examples/tablet-1024.md +3 -0
  365. package/docs/layout/legal-document-shell/examples/tablet-1024.tsx +84 -0
  366. package/docs/layout/legal-document-shell/examples/wrapping-ja-en-vi.md +18 -0
  367. package/docs/layout/legal-document-shell/examples/wrapping-ja-en-vi.tsx +61 -0
  368. package/docs/layout/legal-document-shell/index.md +51 -0
  369. package/docs/layout/legal-document-shell/index.tsx +101 -0
  370. package/docs/layout/master-detail.tsx +387 -0
  371. package/docs/layout/org-switcher.tsx +131 -0
  372. package/docs/layout/page-container.tsx +954 -0
  373. package/docs/layout/resizable-panel.tsx +419 -0
  374. package/docs/layout/responsive-grid.tsx +243 -0
  375. package/docs/layout/separator.tsx +327 -0
  376. package/docs/layout/service-role-panel.tsx +144 -0
  377. package/docs/layout/sidebar.tsx +609 -0
  378. package/docs/layout/split-pane.tsx +468 -0
  379. package/docs/layout/topbar.tsx +349 -0
  380. package/docs/navigation/app-setting-picker.tsx +142 -0
  381. package/docs/navigation/breadcrumb.tsx +135 -0
  382. package/docs/navigation/context-menu.tsx +123 -0
  383. package/docs/navigation/dropdown-menu.tsx +213 -0
  384. package/docs/navigation/filter-bar.tsx +227 -0
  385. package/docs/navigation/menubar.tsx +141 -0
  386. package/docs/navigation/navigation-menu.tsx +158 -0
  387. package/docs/navigation/pagination.tsx +241 -0
  388. package/docs/navigation/steps.tsx +265 -0
  389. package/docs/navigation/tabs-rtl.tsx +28 -0
  390. package/docs/navigation/tabs.tsx +314 -0
  391. package/docs/navigation/toolbar.tsx +339 -0
  392. package/docs/providers/app-provider.tsx +191 -0
  393. package/docs/providers/format-date.tsx +230 -0
  394. package/docs/query/button-refetch.tsx +160 -0
  395. package/docs/query/data-state.tsx +192 -0
  396. package/docs/query/infinite-query-state.tsx +93 -0
  397. package/docs/query/mutation-feedback.tsx +70 -0
  398. package/docs/query/prefetch-link.tsx +262 -0
  399. package/docs/query/touch-actions.tsx +68 -0
  400. package/docs/showcase/acme-portal.tsx +455 -0
  401. package/docs/showcase/acme-website.tsx +461 -0
  402. package/docs/showcase/case1-warehouse-dashboard.tsx +359 -0
  403. package/docs/showcase/case2-employee-me.tsx +444 -0
  404. package/docs/showcase/case3-approval-workflow.tsx +529 -0
  405. package/docs/showcase/case4-login.tsx +313 -0
  406. package/docs/showcase/case5-shift-calendar.tsx +795 -0
  407. package/docs/showcase/case6-agency-handy.tsx +1113 -0
  408. package/docs/showcase/futurelastic-web.tsx +392 -0
  409. package/docs/showcase/org-switcher.tsx +313 -0
  410. package/docs/showcase/permission-matrix.tsx +346 -0
  411. package/docs/showcase/public-landing.tsx +210 -0
  412. package/docs/showcase/settings-account-sections.tsx +559 -0
  413. package/docs/showcase/settings-security-mfa.tsx +145 -0
  414. package/docs/showcase/table-approval-queue.tsx +199 -0
  415. package/docs/showcase/table-bulk-actions.tsx +460 -0
  416. package/docs/showcase/table-compact-kintone.tsx +438 -0
  417. package/docs/showcase/table-conditional-format.tsx +367 -0
  418. package/docs/showcase/table-crud-list.tsx +544 -0
  419. package/docs/showcase/table-density.tsx +347 -0
  420. package/docs/showcase/table-expandable-rows.tsx +480 -0
  421. package/docs/showcase/table-filter-chips.tsx +456 -0
  422. package/docs/showcase/table-footer-totals.tsx +314 -0
  423. package/docs/showcase/table-grouped-subtotals.tsx +391 -0
  424. package/docs/showcase/table-master-detail.tsx +484 -0
  425. package/docs/showcase/table-pagination.tsx +327 -0
  426. package/docs/showcase/table-states.tsx +366 -0
  427. package/docs/showcase/table-sticky-columns.tsx +376 -0
  428. package/docs/showcase/table-tree-rows.tsx +444 -0
  429. package/docs/showcase/table-view-tabs.tsx +488 -0
  430. package/package.json +9 -12
  431. package/scripts/_agent-setup.mjs +16 -2
  432. package/scripts/audit-hook.mjs +3 -13
  433. package/scripts/init-agent-kit.mjs +1 -14
  434. package/scripts/postinstall.mjs +12 -12
  435. package/scripts/ui-audit.mjs +93 -31
  436. package/scripts/visual-audit-rules.mjs +51 -7
  437. package/scripts/visual-audit.mjs +118 -47
@@ -0,0 +1,1113 @@
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, ResponsiveGrid } 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>
253
+ <Flex align="start" gap="md">
254
+ <Flex direction="col" gap="xs">
255
+ {selectMode ? (
256
+ <Checkbox
257
+ checked={selected}
258
+ onCheckedChange={onToggle}
259
+ aria-label={`選択 ${item.name}`}
260
+ className="size-5"
261
+ />
262
+ ) : null}
263
+ <Flex direction="col" gap="sm" className="min-w-0 flex-1">
264
+ <Text as="div" size="sm" weight="bold" truncate>
265
+ {item.name}
266
+ </Text>
267
+ <Flex wrap align="center" gap="sm">
268
+ <MonoCode>{item.rc}</MonoCode>
269
+ <MonoCode>JAN {item.jan}</MonoCode>
270
+ </Flex>
271
+ <Flex align="center" justify="between" gap="sm">
272
+ <Badge tone={st.tone} variant="outline" className="rounded-full">
273
+ {st.label}
274
+ </Badge>
275
+ <Text size="xs" tone="muted" tabular className="shrink-0">
276
+ ×{item.qty} · {item.receivedAt}
277
+ </Text>
278
+ </Flex>
279
+ </Flex>
280
+ </Flex>
281
+ </Flex>
282
+ </CardContent>
283
+ </Card>
284
+ );
285
+ }
286
+
287
+ function PackingListCard({ packing, onTap }: { packing: Packing; onTap?: () => void }) {
288
+ const st = PACKING_STATUS[packing.status];
289
+ return (
290
+ <Card
291
+ density="tight"
292
+ role={onTap ? "button" : undefined}
293
+ tabIndex={onTap ? 0 : undefined}
294
+ onClick={onTap}
295
+ className={
296
+ "rounded-xl transition-colors " + (onTap ? "hover:border-primary cursor-pointer" : "")
297
+ }
298
+ >
299
+ <CardContent solo>
300
+ <Flex direction="col" gap="xs">
301
+ <Flex align="center" justify="between" gap="sm">
302
+ <Text as="code" size="sm" weight="bold" tabular>
303
+ {packing.code}
304
+ </Text>
305
+ <Badge tone={st.tone} variant="outline" className="rounded-full">
306
+ {st.label}
307
+ </Badge>
308
+ </Flex>
309
+ <Flex align="center" justify="between" gap="sm">
310
+ <Text size="xs" tone="muted" truncate>
311
+ {packing.customer} · {packing.city}
312
+ </Text>
313
+ <Text size="xs" tone="muted" tabular className="shrink-0">
314
+ ×{packing.items}
315
+ {packing.slot ? ` · ${packing.slot}` : ""}
316
+ </Text>
317
+ </Flex>
318
+ </Flex>
319
+ </CardContent>
320
+ </Card>
321
+ );
322
+ }
323
+
324
+ /** Three uppercase muted section header + right-aligned mono count. */
325
+ function SectionHeader({ children, count }: { children: React.ReactNode; count?: number }) {
326
+ return (
327
+ <Flex align="center" justify="between" className="px-0.5">
328
+ <Text size="2xs" weight="medium" tone="muted" className="tracking-wider uppercase">
329
+ {children}
330
+ </Text>
331
+ {count != null ? (
332
+ <Text size="2xs" mono tone="muted" tabular>
333
+ {count}
334
+ </Text>
335
+ ) : null}
336
+ </Flex>
337
+ );
338
+ }
339
+
340
+ // ── Phone shell ──────────────────────────────────────────────────────────────
341
+
342
+ function PhoneFrame({ children }: { children: React.ReactNode }) {
343
+ return (
344
+ <div className="mx-auto w-96 max-w-full shrink-0">
345
+ <Flex
346
+ direction="col"
347
+ className="bg-background max-h-[calc(100vh-2rem)] min-h-[844px] overflow-hidden rounded-2xl border"
348
+ >
349
+ {children}
350
+ </Flex>
351
+ </div>
352
+ );
353
+ }
354
+
355
+ function StatusBar() {
356
+ return (
357
+ <Flex align="center" justify="between" gap="xs" className="h-9 shrink-0 px-5">
358
+ <Text size="sm" weight="medium" tabular>
359
+ 9:41
360
+ </Text>
361
+ <Text size="sm" weight="medium" tabular tone="muted">
362
+ Acme Handy
363
+ </Text>
364
+ </Flex>
365
+ );
366
+ }
367
+
368
+ function TabBar({ active, onChange }: { active: string; onChange: (id: string) => void }) {
369
+ return (
370
+ <nav className="grid shrink-0 grid-cols-3 border-t" aria-label="ワークフロー">
371
+ {TABS.map((t) => {
372
+ const isActive = t.id === active;
373
+ const Icon = t.icon;
374
+ return (
375
+ <Button
376
+ key={t.id}
377
+ variant="ghost"
378
+ onClick={() => onChange(t.id)}
379
+ aria-current={isActive ? "page" : undefined}
380
+ className={
381
+ "h-16 flex-col gap-1 rounded-none font-medium text-[var(--font-size-2xs)] " +
382
+ (isActive ? "text-primary" : "text-muted-foreground")
383
+ }
384
+ >
385
+ <Icon className="size-5" aria-hidden="true" strokeWidth={1.5} />
386
+ <span className="whitespace-nowrap">{t.label}</span>
387
+ </Button>
388
+ );
389
+ })}
390
+ </nav>
391
+ );
392
+ }
393
+
394
+ // ── Sheets ───────────────────────────────────────────────────────────────────
395
+
396
+ function ItemLookupSheet({
397
+ open,
398
+ onOpenChange,
399
+ }: {
400
+ open: boolean;
401
+ onOpenChange: (v: boolean) => void;
402
+ }) {
403
+ const [code, setCode] = React.useState("");
404
+ return (
405
+ <Sheet open={open} onOpenChange={onOpenChange}>
406
+ <SheetContent side="bottom" className="rounded-t-2xl">
407
+ <SheetHeader>
408
+ <SheetTitle>Quét hoặc nhập mã</SheetTitle>
409
+ </SheetHeader>
410
+ <SheetBody>
411
+ <Flex direction="col" gap="md">
412
+ {/* Viewfinder placeholder — a Card surface, not a hand-rolled illustration */}
413
+ <Flex
414
+ align="center"
415
+ justify="center"
416
+ className="border-border bg-secondary/40 h-40 rounded-xl border-2 border-dashed"
417
+ >
418
+ <Flex direction="col" align="center" gap="sm">
419
+ <ScanLine
420
+ className="size-7 text-[color:var(--attention,var(--warning))]"
421
+ aria-hidden="true"
422
+ strokeWidth={1.5}
423
+ />
424
+ <Text size="sm" tone="muted">
425
+ Đưa mã vạch vào khung
426
+ </Text>
427
+ </Flex>
428
+ </Flex>
429
+ <Flex direction="col" gap="sm">
430
+ <Text size="sm" weight="medium">
431
+ Hoặc nhập mã thủ công
432
+ </Text>
433
+ <Flex gap="sm">
434
+ <Input
435
+ value={code}
436
+ onChange={(e) => setCode(e.target.value)}
437
+ placeholder="RC- / PKG- / JAN"
438
+ className="font-mono"
439
+ inputMode="text"
440
+ autoComplete="off"
441
+ />
442
+ <Button
443
+ onClick={() => {
444
+ onOpenChange(false);
445
+ setCode("");
446
+ toast.success("Đã tìm thấy RC-204881 · Sữa rửa mặt Hada Labo");
447
+ }}
448
+ disabled={code.trim() === ""}
449
+ >
450
+ <Search aria-hidden="true" />
451
+ Tìm
452
+ </Button>
453
+ </Flex>
454
+ </Flex>
455
+ </Flex>
456
+ </SheetBody>
457
+ <SheetFooter>
458
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
459
+ Đóng
460
+ </Button>
461
+ </SheetFooter>
462
+ </SheetContent>
463
+ </Sheet>
464
+ );
465
+ }
466
+
467
+ function PackagePickerSheet({
468
+ open,
469
+ onOpenChange,
470
+ count,
471
+ }: {
472
+ open: boolean;
473
+ onOpenChange: (v: boolean) => void;
474
+ count: number;
475
+ }) {
476
+ return (
477
+ <Sheet open={open} onOpenChange={onOpenChange}>
478
+ <SheetContent side="bottom" className="rounded-t-2xl">
479
+ <SheetHeader>
480
+ <SheetTitle>Gán vào kiện</SheetTitle>
481
+ </SheetHeader>
482
+ <SheetBody>
483
+ <Flex direction="col" gap="sm">
484
+ <SectionHeader count={PACKINGS.length}>Kiện đang mở</SectionHeader>
485
+ {PACKINGS.map((p) => (
486
+ <PackingListCard
487
+ key={p.id}
488
+ packing={p}
489
+ onTap={() => {
490
+ onOpenChange(false);
491
+ toast.success(`Đã gán ${count} item vào ${p.code}`);
492
+ }}
493
+ />
494
+ ))}
495
+ <Button
496
+ variant="outline"
497
+ onClick={() => {
498
+ onOpenChange(false);
499
+ toast.success(`Đã tạo kiện mới với ${count} item`);
500
+ }}
501
+ className="text-primary hover:border-primary h-11 w-full border-2 border-dashed"
502
+ >
503
+ <PackagePlus aria-hidden="true" strokeWidth={1.5} />
504
+ Tạo kiện mới với {count} item này
505
+ </Button>
506
+ </Flex>
507
+ </SheetBody>
508
+ <SheetFooter>
509
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
510
+ Huỷ
511
+ </Button>
512
+ </SheetFooter>
513
+ </SheetContent>
514
+ </Sheet>
515
+ );
516
+ }
517
+
518
+ const DESTINATIONS = [
519
+ { id: "unsorted", label: "Chưa phân loại", hint: "Để xử lý sau" },
520
+ { id: "shelf", label: "Giá hàng", hint: "Lên kệ lưu trữ" },
521
+ { id: "open", label: "Kiện đang mở", hint: "Gói cùng đơn hiện tại" },
522
+ { id: "new", label: "Tạo kiện mới", hint: "Mở kiện mới cho item" },
523
+ ] as const;
524
+
525
+ function ItemFormSheet({
526
+ open,
527
+ onOpenChange,
528
+ }: {
529
+ open: boolean;
530
+ onOpenChange: (v: boolean) => void;
531
+ }) {
532
+ const [dest, setDest] = React.useState<string>("unsorted");
533
+ const [qty, setQty] = React.useState("1");
534
+ return (
535
+ <Sheet open={open} onOpenChange={onOpenChange}>
536
+ <SheetContent side="bottom" className="max-h-[calc(80vh)] rounded-t-2xl">
537
+ <SheetHeader>
538
+ <SheetTitle>Thêm hàng mới</SheetTitle>
539
+ </SheetHeader>
540
+ <SheetBody>
541
+ <Flex direction="col" gap="md">
542
+ <Flex direction="col" gap="xs">
543
+ <Text size="sm" weight="medium">
544
+ Mã vạch
545
+ </Text>
546
+ <Input defaultValue="JAN 4987241135219" readOnly className="font-mono" />
547
+ </Flex>
548
+ <Flex direction="col" gap="xs">
549
+ <Text size="sm" weight="medium">
550
+ Tên hàng (tùy chọn)
551
+ </Text>
552
+ <Input placeholder="Nhập tên hàng" autoComplete="off" />
553
+ </Flex>
554
+ <Flex direction="col" gap="xs">
555
+ <Text size="sm" weight="medium">
556
+ Số lượng
557
+ </Text>
558
+ <Input
559
+ type="number"
560
+ value={qty}
561
+ min={1}
562
+ onChange={(e) => setQty(e.target.value)}
563
+ className="w-24 tabular-nums"
564
+ />
565
+ </Flex>
566
+ <Flex direction="col" gap="sm">
567
+ <Text size="sm" weight="medium">
568
+ Đích đến
569
+ </Text>
570
+ <RadioGroupRoot value={dest} onValueChange={setDest} className="flex flex-col gap-2">
571
+ {DESTINATIONS.map((d) => {
572
+ const checked = dest === d.id;
573
+ const rowId = `handy-dest-${d.id}`;
574
+ return (
575
+ <label
576
+ key={d.id}
577
+ htmlFor={rowId}
578
+ className={
579
+ "flex cursor-pointer items-center gap-3 rounded-xl border p-3 transition-colors " +
580
+ (checked
581
+ ? "border-primary bg-[color-mix(in_oklch,var(--primary)_8%,transparent)]"
582
+ : "hover:border-primary")
583
+ }
584
+ >
585
+ <RadioItem id={rowId} value={d.id} className="mt-0.5" />
586
+ <span className="min-w-0">
587
+ <Text as="span" size="sm" weight="medium" className="block">
588
+ {d.label}
589
+ </Text>
590
+ <Text as="span" size="xs" tone="muted" className="block">
591
+ {d.hint}
592
+ </Text>
593
+ </span>
594
+ </label>
595
+ );
596
+ })}
597
+ </RadioGroupRoot>
598
+ </Flex>
599
+ </Flex>
600
+ </SheetBody>
601
+ <SheetFooter className="flex-row gap-2">
602
+ <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>
603
+ Huỷ
604
+ </Button>
605
+ <Button
606
+ className="flex-[2]"
607
+ onClick={() => {
608
+ onOpenChange(false);
609
+ toast.success("Đã lưu hàng mới");
610
+ }}
611
+ >
612
+ Lưu
613
+ </Button>
614
+ </SheetFooter>
615
+ </SheetContent>
616
+ </Sheet>
617
+ );
618
+ }
619
+
620
+ // ── Tab bodies ───────────────────────────────────────────────────────────────
621
+
622
+ type ListState = "ready" | "loading" | "empty" | "error";
623
+
624
+ function InboundTab({
625
+ state,
626
+ onRetry,
627
+ selectMode,
628
+ setSelectMode,
629
+ selected,
630
+ setSelected,
631
+ onScan,
632
+ onAdd,
633
+ onAssign,
634
+ }: {
635
+ state: ListState;
636
+ onRetry: () => void;
637
+ selectMode: boolean;
638
+ setSelectMode: (v: boolean) => void;
639
+ selected: Set<string>;
640
+ setSelected: React.Dispatch<React.SetStateAction<Set<string>>>;
641
+ onScan: () => void;
642
+ onAdd: () => void;
643
+ onAssign: () => void;
644
+ }) {
645
+ const [filter, setFilter] = React.useState<string>("all");
646
+
647
+ const visible = ITEMS.filter((i) =>
648
+ filter === "all"
649
+ ? true
650
+ : filter === "unsorted"
651
+ ? i.status === "unsorted"
652
+ : i.status === "shelf",
653
+ );
654
+
655
+ return (
656
+ <>
657
+ <Flex direction="col" gap="md" className="flex-1 overflow-y-auto p-4">
658
+ {/* Filter chips — horizontal scroll, count pills */}
659
+ <ToggleGroup
660
+ type="single"
661
+ value={filter}
662
+ onValueChange={(v) => {
663
+ if (v) setFilter(v);
664
+ }}
665
+ className="w-full overflow-x-auto"
666
+ >
667
+ <Flex justify="start" gap="sm">
668
+ {FILTERS.map((f) => (
669
+ <ToggleGroupItem
670
+ key={f.id}
671
+ value={f.id}
672
+ className="h-9 shrink-0 gap-1.5 rounded-full px-3 whitespace-nowrap text-[var(--font-size-xs)]"
673
+ >
674
+ {f.label}
675
+ <Text size="xs" mono tabular className="opacity-70">
676
+ {f.count}
677
+ </Text>
678
+ </ToggleGroupItem>
679
+ ))}
680
+ </Flex>
681
+ </ToggleGroup>
682
+
683
+ {state === "loading" ? (
684
+ <Flex direction="col" gap="md" aria-busy="true">
685
+ {Array.from({ length: 3 }).map((_, i) => (
686
+ <Card key={i} density="tight" className="rounded-xl">
687
+ <CardContent solo>
688
+ <Flex direction="col" gap="md">
689
+ <Skeleton className="h-4 w-3/5" />
690
+ <Skeleton className="h-3 w-2/5" />
691
+ <Skeleton className="h-5 w-24 rounded-full" />
692
+ </Flex>
693
+ </CardContent>
694
+ </Card>
695
+ ))}
696
+ </Flex>
697
+ ) : state === "error" ? (
698
+ <Alert tone="warning">
699
+ <AlertTitle>Không tải được danh sách</AlertTitle>
700
+ <AlertDescription>
701
+ <Flex direction="col" align="start" gap="sm">
702
+ Kiểm tra kết nối rồi thử lại.
703
+ <Button variant="outline" size="sm" onClick={onRetry}>
704
+ <RefreshCw aria-hidden="true" />
705
+ Thử lại
706
+ </Button>
707
+ </Flex>
708
+ </AlertDescription>
709
+ </Alert>
710
+ ) : state === "empty" || visible.length === 0 ? (
711
+ <EmptyState
712
+ icon={Boxes}
713
+ title="Chưa có hàng trong mục này"
714
+ description="Quét mã để nhập hàng mới vào kho."
715
+ action={
716
+ <Button size="sm" onClick={onScan}>
717
+ <ScanLine aria-hidden="true" />
718
+ Quét / Tìm mã
719
+ </Button>
720
+ }
721
+ />
722
+ ) : (
723
+ <Flex direction="col" gap="md">
724
+ {visible.map((item) => (
725
+ <ItemListCard
726
+ key={item.id}
727
+ item={item}
728
+ selectMode={selectMode}
729
+ selected={selected.has(item.id)}
730
+ onToggle={() =>
731
+ setSelected((prev) => {
732
+ const next = new Set(prev);
733
+ if (next.has(item.id)) next.delete(item.id);
734
+ else next.add(item.id);
735
+ return next;
736
+ })
737
+ }
738
+ />
739
+ ))}
740
+ </Flex>
741
+ )}
742
+ </Flex>
743
+
744
+ {/* Sticky action bar (scan-first) OR select-mode contextual bar */}
745
+ {selectMode ? (
746
+ <Flex
747
+ direction="col"
748
+ gap="sm"
749
+ className="shrink-0 border-t bg-[color-mix(in_oklch,var(--primary)_5%,var(--background))] p-3"
750
+ >
751
+ <Flex align="center" justify="between">
752
+ <Text size="sm" tabular>
753
+ <Text as="span" weight="bold">
754
+ {selected.size}
755
+ </Text>{" "}
756
+ item đã chọn
757
+ </Text>
758
+ <Button
759
+ variant="ghost"
760
+ size="sm"
761
+ onClick={() => {
762
+ setSelectMode(false);
763
+ setSelected(new Set());
764
+ }}
765
+ >
766
+ Xong
767
+ </Button>
768
+ </Flex>
769
+ <Flex gap="sm">
770
+ <Button className="flex-[2]" disabled={selected.size === 0} onClick={onAssign}>
771
+ <Package aria-hidden="true" />
772
+ Gán vào kiện…
773
+ </Button>
774
+ <Button variant="outline" className="flex-1" disabled={selected.size === 0}>
775
+ Kiện mới
776
+ </Button>
777
+ </Flex>
778
+ </Flex>
779
+ ) : (
780
+ <Flex gap="sm" className="shrink-0 border-t p-3">
781
+ <Button className="flex-[2]" onClick={onScan}>
782
+ <ScanLine aria-hidden="true" />
783
+ Quét / Tìm mã
784
+ </Button>
785
+ <Button variant="outline" className="flex-1" onClick={onAdd}>
786
+ <Plus aria-hidden="true" />
787
+ Thêm hàng
788
+ </Button>
789
+ </Flex>
790
+ )}
791
+ </>
792
+ );
793
+ }
794
+
795
+ function PackingTab({ onScan }: { onScan: () => void }) {
796
+ const active = PACKINGS.find((p) => p.status === "active");
797
+ const others = PACKINGS.filter((p) => p.status !== "active");
798
+ return (
799
+ <>
800
+ <Flex direction="col" gap="md" className="flex-1 overflow-y-auto p-4">
801
+ {active ? (
802
+ <Card
803
+ density="tight"
804
+ accent="primary"
805
+ className="border-primary rounded-xl bg-[color-mix(in_oklch,var(--primary)_6%,transparent)]"
806
+ >
807
+ <CardContent solo>
808
+ <Flex direction="col" gap="xs">
809
+ <Flex align="center" justify="between">
810
+ <Text
811
+ size="2xs"
812
+ weight="medium"
813
+ tone="muted"
814
+ className="tracking-wider uppercase"
815
+ >
816
+ Kiện đang làm
817
+ </Text>
818
+ <Badge tone="info" variant="outline" className="rounded-full">
819
+ {PACKING_STATUS.active.label}
820
+ </Badge>
821
+ </Flex>
822
+ <Text as="div" size="lg" weight="bold" mono tabular>
823
+ {active.code}
824
+ </Text>
825
+ <Text size="xs" tone="muted" tabular as="div">
826
+ {active.customer} · {active.city} · ×{active.items} · {active.slot}
827
+ </Text>
828
+ </Flex>
829
+ </CardContent>
830
+ </Card>
831
+ ) : null}
832
+
833
+ <SectionHeader count={others.length}>Kiện đang mở</SectionHeader>
834
+ <Flex direction="col" gap="md">
835
+ {others.map((p) => (
836
+ <PackingListCard key={p.id} packing={p} onTap={() => undefined} />
837
+ ))}
838
+ </Flex>
839
+ </Flex>
840
+ <Flex direction="col" gap="sm" className="shrink-0 border-t p-3">
841
+ <Button onClick={onScan}>
842
+ <ScanLine aria-hidden="true" />
843
+ Quét item vào kiện
844
+ </Button>
845
+ <Button variant="outline">
846
+ <Plus aria-hidden="true" />
847
+ Tạo kiện trống
848
+ </Button>
849
+ </Flex>
850
+ </>
851
+ );
852
+ }
853
+
854
+ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: () => void }) {
855
+ const [seg, setSeg] = React.useState<string>("seal");
856
+ return (
857
+ <>
858
+ <Flex direction="col" gap="md" className="flex-1 overflow-y-auto p-4">
859
+ {/* Segmented — outbound status */}
860
+ <ToggleGroup
861
+ type="single"
862
+ value={seg}
863
+ onValueChange={(v) => {
864
+ if (v) setSeg(v);
865
+ }}
866
+ className="bg-secondary/60 w-full rounded-xl p-1"
867
+ >
868
+ <ResponsiveGrid columns={3} gap="xs">
869
+ <ToggleGroupItem
870
+ value="seal"
871
+ className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
872
+ >
873
+ Chờ niêm phong
874
+ </ToggleGroupItem>
875
+ <ToggleGroupItem
876
+ value="handoff"
877
+ className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
878
+ >
879
+ Chờ bàn giao
880
+ </ToggleGroupItem>
881
+ <ToggleGroupItem
882
+ value="done"
883
+ className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
884
+ >
885
+ Đã bàn giao
886
+ </ToggleGroupItem>
887
+ </ResponsiveGrid>
888
+ </ToggleGroup>
889
+
890
+ <SectionHeader count={OUTBOUND.length}>
891
+ {seg === "seal"
892
+ ? "Sẵn sàng niêm phong"
893
+ : seg === "handoff"
894
+ ? "Chờ bàn giao"
895
+ : "Đã bàn giao"}
896
+ </SectionHeader>
897
+ <Flex direction="col" gap="md">
898
+ {OUTBOUND.map((p) => (
899
+ <Card key={p.id} density="tight" className="rounded-xl">
900
+ <CardContent solo>
901
+ <Flex direction="col" gap="md">
902
+ <Flex align="center" justify="between" gap="sm">
903
+ <Text as="code" size="sm" weight="bold" tabular>
904
+ {p.code}
905
+ </Text>
906
+ <Badge tone="success" variant="outline" className="rounded-full">
907
+ Sẵn sàng niêm phong
908
+ </Badge>
909
+ </Flex>
910
+ <Flex align="center" justify="between" gap="sm">
911
+ <Text size="xs" tone="muted" tabular truncate>
912
+ {p.customer} · {p.city}
913
+ </Text>
914
+ <Text size="xs" tone="muted" tabular className="shrink-0">
915
+ ×{p.items} · {p.slot}
916
+ </Text>
917
+ </Flex>
918
+ <div>
919
+ <Descriptions columns={1} className="gap-y-1">
920
+ <Descriptions.Item label="Vị trí" mono>
921
+ {p.slot}
922
+ </Descriptions.Item>
923
+ <Descriptions.Item label="Số kiện" mono>
924
+ ×{p.items}
925
+ </Descriptions.Item>
926
+ </Descriptions>
927
+ </div>
928
+ </Flex>
929
+ </CardContent>
930
+ </Card>
931
+ ))}
932
+ </Flex>
933
+ </Flex>
934
+ <Flex gap="sm" className="shrink-0 border-t p-3">
935
+ {seg === "handoff" ? (
936
+ <Button className="flex-1" onClick={onHandoff}>
937
+ <Truck aria-hidden="true" />
938
+ Xác nhận bàn giao
939
+ </Button>
940
+ ) : (
941
+ <Button className="flex-1" onClick={onSeal}>
942
+ <Check aria-hidden="true" />
943
+ Quét mã kiện · Niêm phong
944
+ </Button>
945
+ )}
946
+ </Flex>
947
+ </>
948
+ );
949
+ }
950
+
951
+ // ── Root ─────────────────────────────────────────────────────────────────────
952
+
953
+ export default function AgencyHandyShowcase() {
954
+ const [tab, setTab] = React.useState<string>("inbound");
955
+ const [listState, setListState] = React.useState<ListState>("ready");
956
+
957
+ const [selectMode, setSelectMode] = React.useState(false);
958
+ const [selected, setSelected] = React.useState<Set<string>>(new Set());
959
+
960
+ const [lookupOpen, setLookupOpen] = React.useState(false);
961
+ const [formOpen, setFormOpen] = React.useState(false);
962
+ const [pickerOpen, setPickerOpen] = React.useState(false);
963
+
964
+ const [sealOpen, setSealOpen] = React.useState(false);
965
+ const [handoffOpen, setHandoffOpen] = React.useState(false);
966
+
967
+ const headerTitle = tab === "inbound" ? "Nhập kho" : tab === "packing" ? "Đóng gói" : "Xuất kho";
968
+
969
+ return (
970
+ <div className="bg-secondary/30 min-h-screen py-4">
971
+ <Toaster />
972
+ <PhoneFrame>
973
+ <StatusBar />
974
+
975
+ {/* App header (52px) — title + iOS text-action select-mode entry (inbound only) */}
976
+ <header className="h-14 shrink-0 border-b px-4">
977
+ <Flex align="center" justify="between" gap="xs">
978
+ <Heading level={3} as="h1" className="whitespace-nowrap">
979
+ {headerTitle}
980
+ </Heading>
981
+ <Flex align="center" gap="xs">
982
+ {tab === "inbound" && !selectMode ? (
983
+ <Button
984
+ variant="ghost"
985
+ size="sm"
986
+ onClick={() => {
987
+ setSelectMode(true);
988
+ setSelected(new Set());
989
+ }}
990
+ >
991
+ <Check aria-hidden="true" />
992
+ Chọn
993
+ </Button>
994
+ ) : null}
995
+ {/* State switcher — exposes the 4 list states at rest (showcase affordance) */}
996
+ {tab === "inbound" && !selectMode ? (
997
+ <ToggleGroup
998
+ type="single"
999
+ value={listState}
1000
+ onValueChange={(v) => {
1001
+ if (v) setListState(v as ListState);
1002
+ }}
1003
+ aria-label="List state (showcase)"
1004
+ >
1005
+ <Flex gap="xs">
1006
+ {(["ready", "loading", "empty", "error"] as const).map((s) => (
1007
+ <ToggleGroupItem
1008
+ key={s}
1009
+ value={s}
1010
+ className="size-7 rounded-md p-0 text-[var(--font-size-2xs)] uppercase"
1011
+ title={s}
1012
+ >
1013
+ {s[0]}
1014
+ </ToggleGroupItem>
1015
+ ))}
1016
+ </Flex>
1017
+ </ToggleGroup>
1018
+ ) : null}
1019
+ </Flex>
1020
+ </Flex>
1021
+ </header>
1022
+
1023
+ {/* Select-mode header strip — replaces the standard header context (inbound) */}
1024
+ {selectMode ? (
1025
+ <Flex
1026
+ align="center"
1027
+ justify="between"
1028
+ gap="xs"
1029
+ className="h-10 shrink-0 border-b bg-[color-mix(in_oklch,var(--primary)_5%,var(--background))] px-3"
1030
+ >
1031
+ <Button
1032
+ variant="ghost"
1033
+ size="icon-sm"
1034
+ aria-label="閉じる"
1035
+ onClick={() => {
1036
+ setSelectMode(false);
1037
+ setSelected(new Set());
1038
+ }}
1039
+ >
1040
+ <X className="size-4" aria-hidden="true" strokeWidth={1.5} />
1041
+ </Button>
1042
+ <Text size="sm" mono tabular>
1043
+ {selected.size} đã chọn
1044
+ </Text>
1045
+ <Button
1046
+ variant="ghost"
1047
+ size="sm"
1048
+ onClick={() => setSelected(new Set(ITEMS.map((i) => i.id)))}
1049
+ >
1050
+ Chọn tất cả
1051
+ </Button>
1052
+ </Flex>
1053
+ ) : null}
1054
+
1055
+ {tab === "inbound" ? (
1056
+ <InboundTab
1057
+ state={listState}
1058
+ onRetry={() => setListState("ready")}
1059
+ selectMode={selectMode}
1060
+ setSelectMode={setSelectMode}
1061
+ selected={selected}
1062
+ setSelected={setSelected}
1063
+ onScan={() => setLookupOpen(true)}
1064
+ onAdd={() => setFormOpen(true)}
1065
+ onAssign={() => setPickerOpen(true)}
1066
+ />
1067
+ ) : tab === "packing" ? (
1068
+ <PackingTab onScan={() => setLookupOpen(true)} />
1069
+ ) : (
1070
+ <OutboundTab onSeal={() => setSealOpen(true)} onHandoff={() => setHandoffOpen(true)} />
1071
+ )}
1072
+
1073
+ <TabBar
1074
+ active={tab}
1075
+ onChange={(id) => {
1076
+ setTab(id);
1077
+ setSelectMode(false);
1078
+ setSelected(new Set());
1079
+ }}
1080
+ />
1081
+ </PhoneFrame>
1082
+
1083
+ {/* Sheets */}
1084
+ <ItemLookupSheet open={lookupOpen} onOpenChange={setLookupOpen} />
1085
+ <ItemFormSheet open={formOpen} onOpenChange={setFormOpen} />
1086
+ <PackagePickerSheet open={pickerOpen} onOpenChange={setPickerOpen} count={selected.size} />
1087
+
1088
+ {/* Confirm dialogs — verb confirm, surface summary; forward step = primary */}
1089
+ <AlertDialog
1090
+ open={sealOpen}
1091
+ onOpenChange={setSealOpen}
1092
+ title="Niêm phong kiện?"
1093
+ description="PKG-000037 · 9 item · vị trí B-01-04. Sau khi niêm phong không thể thêm item."
1094
+ confirmLabel="Niêm phong"
1095
+ cancelLabel="Huỷ"
1096
+ onConfirm={() => {
1097
+ toast.success("Đã niêm phong PKG-000037 · 16:24");
1098
+ }}
1099
+ />
1100
+ <AlertDialog
1101
+ open={handoffOpen}
1102
+ onOpenChange={setHandoffOpen}
1103
+ title="Xác nhận bàn giao?"
1104
+ description="PKG-000037 lên xe giao. Thao tác này có thể hoàn tác trong 10 phút."
1105
+ confirmLabel="Xác nhận"
1106
+ cancelLabel="Huỷ"
1107
+ onConfirm={() => {
1108
+ toast.success("Đã bàn giao PKG-000037 · 16:24");
1109
+ }}
1110
+ />
1111
+ </div>
1112
+ );
1113
+ }