@godxjp/ui 19.1.0 → 19.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) 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 +2 -12
  11. package/dist/components/data-display/card.d.ts +21 -41
  12. package/dist/components/data-display/carousel.d.ts +1 -2
  13. package/dist/components/data-display/code-block.d.ts +4 -0
  14. package/dist/components/data-display/code-block.js +24 -0
  15. package/dist/components/data-display/data-table.d.ts +15 -37
  16. package/dist/components/data-display/data-table.js +1 -3
  17. package/dist/components/data-display/descriptions.d.ts +2 -6
  18. package/dist/components/data-display/descriptions.js +0 -2
  19. package/dist/components/data-display/index.d.ts +4 -0
  20. package/dist/components/data-display/index.js +4 -0
  21. package/dist/components/data-display/list-row.d.ts +3 -22
  22. package/dist/components/data-display/permission-matrix.d.ts +7 -20
  23. package/dist/components/data-display/progress.d.ts +1 -1
  24. package/dist/components/data-display/prose.d.ts +4 -0
  25. package/dist/components/data-display/prose.js +20 -0
  26. package/dist/components/data-display/service-launcher-card.d.ts +3 -10
  27. package/dist/components/data-display/table.d.ts +20 -80
  28. package/dist/components/data-display/table.js +0 -3
  29. package/dist/components/data-display/timeline.d.ts +3 -5
  30. package/dist/components/data-entry/branch-scope-picker.d.ts +2 -19
  31. package/dist/components/data-entry/branch-scope-picker.js +0 -1
  32. package/dist/components/data-entry/calendar.d.ts +3 -2
  33. package/dist/components/data-entry/calendar.js +58 -3
  34. package/dist/components/data-entry/command-palette.d.ts +8 -22
  35. package/dist/components/data-entry/date-picker.d.ts +4 -5
  36. package/dist/components/data-entry/date-picker.js +6 -1
  37. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  38. package/dist/components/data-entry/date-range-picker.js +8 -3
  39. package/dist/components/data-entry/form-errors.d.ts +7 -18
  40. package/dist/components/data-entry/form-field.js +1 -5
  41. package/dist/components/data-entry/form.d.ts +1 -9
  42. package/dist/components/data-entry/input.d.ts +16 -14
  43. package/dist/components/data-entry/input.js +3 -0
  44. package/dist/components/data-entry/month-picker.js +2 -2
  45. package/dist/components/data-entry/month-range-picker.d.ts +4 -6
  46. package/dist/components/data-entry/month-range-picker.js +2 -2
  47. package/dist/components/data-entry/number-input.d.ts +1 -5
  48. package/dist/components/data-entry/number-input.js +1 -0
  49. package/dist/components/data-entry/radio.js +0 -1
  50. package/dist/components/data-entry/search-select.d.ts +2 -4
  51. package/dist/components/data-entry/select.d.ts +11 -6
  52. package/dist/components/data-entry/select.js +4 -2
  53. package/dist/components/data-entry/switch.js +0 -1
  54. package/dist/components/data-entry/textarea.d.ts +10 -16
  55. package/dist/components/data-entry/time-picker.d.ts +3 -5
  56. package/dist/components/data-entry/upload-types.d.ts +1 -5
  57. package/dist/components/data-entry/use-upload-draft.d.ts +0 -4
  58. package/dist/components/feedback/dialog.d.ts +6 -15
  59. package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
  60. package/dist/components/feedback/sheet.d.ts +3 -10
  61. package/dist/components/feedback/sheet.js +1 -1
  62. package/dist/components/feedback/sonner.js +2 -3
  63. package/dist/components/general/activity.d.ts +4 -30
  64. package/dist/components/general/button.js +1 -10
  65. package/dist/components/general/logo.d.ts +6 -28
  66. package/dist/components/general/reveal.d.ts +3 -9
  67. package/dist/components/general/typography.d.ts +1 -3
  68. package/dist/components/layout/account-chip.d.ts +8 -0
  69. package/dist/components/layout/account-chip.js +45 -0
  70. package/dist/components/layout/auth-divider.d.ts +2 -9
  71. package/dist/components/layout/auth-shell.d.ts +1 -14
  72. package/dist/components/layout/breadcrumb.d.ts +1 -5
  73. package/dist/components/layout/centered-shell.d.ts +4 -18
  74. package/dist/components/layout/error-surface.d.ts +11 -28
  75. package/dist/components/layout/error-surface.js +4 -1
  76. package/dist/components/layout/index.d.ts +2 -0
  77. package/dist/components/layout/index.js +2 -0
  78. package/dist/components/layout/legal-document-shell.d.ts +5 -28
  79. package/dist/components/layout/master-detail.d.ts +2 -18
  80. package/dist/components/layout/page-container.js +0 -1
  81. package/dist/components/layout/responsive-grid.d.ts +6 -1
  82. package/dist/components/layout/responsive-grid.js +9 -2
  83. package/dist/components/layout/service-role-panel.d.ts +7 -19
  84. package/dist/components/layout/sidebar.d.ts +5 -27
  85. package/dist/components/layout/sidebar.js +0 -1
  86. package/dist/components/layout/split-pane.d.ts +0 -7
  87. package/dist/components/layout/topbar.d.ts +1 -14
  88. package/dist/components/navigation/app-setting-picker.js +1 -3
  89. package/dist/components/navigation/filter-bar.d.ts +1 -6
  90. package/dist/components/navigation/pagination-utils.d.ts +2 -12
  91. package/dist/components/navigation/tabs-scroll.d.ts +2 -36
  92. package/dist/components/navigation/tabs.js +4 -8
  93. package/dist/components/query/data-state.d.ts +2 -2
  94. package/dist/components/ui/credential-reveal.d.ts +1 -4
  95. package/dist/components/ui/hover-card.d.ts +1 -2
  96. package/dist/components/ui/separator.d.ts +2 -15
  97. package/dist/components/ui/toggle-group.d.ts +1 -5
  98. package/dist/components/ui/toggle.d.ts +6 -31
  99. package/dist/email/brand-mark.d.ts +6 -34
  100. package/dist/email/color.d.ts +7 -17
  101. package/dist/email/geometry.d.ts +4 -8
  102. package/dist/email/index.d.ts +4 -48
  103. package/dist/email/inline-style.d.ts +3 -9
  104. package/dist/email/urgency.d.ts +2 -7
  105. package/dist/form/form-context.d.ts +2 -5
  106. package/dist/form/form-root.d.ts +1 -2
  107. package/dist/i18n/messages/en.json +4 -0
  108. package/dist/i18n/messages/ja.json +4 -0
  109. package/dist/i18n/messages/vi.json +4 -0
  110. package/dist/inertia/index.d.ts +2 -10
  111. package/dist/lib/control-styles.d.ts +10 -21
  112. package/dist/lib/control-styles.js +4 -6
  113. package/dist/lib/datetime/format-date.d.ts +1 -4
  114. package/dist/lib/field-a11y.d.ts +11 -68
  115. package/dist/lib/field-a11y.js +0 -1
  116. package/dist/lib/format.d.ts +4 -8
  117. package/dist/lib/hooks.d.ts +6 -12
  118. package/dist/lib/permission-grid.d.ts +3 -18
  119. package/dist/lib/query-error.d.ts +3 -3
  120. package/dist/props/components/app.prop.d.ts +15 -38
  121. package/dist/props/components/charts.prop.d.ts +2 -3
  122. package/dist/props/components/data-display.prop.d.ts +48 -72
  123. package/dist/props/components/data-entry.prop.d.ts +35 -40
  124. package/dist/props/components/form.prop.d.ts +2 -6
  125. package/dist/props/components/general.prop.d.ts +11 -27
  126. package/dist/props/components/layout.prop.d.ts +89 -260
  127. package/dist/props/components/navigation.prop.d.ts +27 -42
  128. package/dist/props/registry.d.ts +31 -0
  129. package/dist/props/registry.js +33 -0
  130. package/dist/props/vocabulary/data.prop.d.ts +18 -49
  131. package/dist/props/vocabulary/index.d.ts +1 -1
  132. package/dist/props/vocabulary/interaction.prop.d.ts +10 -12
  133. package/dist/props/vocabulary/layout.prop.d.ts +8 -30
  134. package/dist/props/vocabulary/shared.prop.d.ts +3 -2
  135. package/dist/styles/alert-layout.css +1 -32
  136. package/dist/styles/badge-layout.css +4 -18
  137. package/dist/styles/base.css +14 -113
  138. package/dist/styles/card-layout.css +6 -128
  139. package/dist/styles/chart-layout.css +2 -17
  140. package/dist/styles/control.css +380 -458
  141. package/dist/styles/core.css +21 -0
  142. package/dist/styles/data-display-layout.css +175 -94
  143. package/dist/styles/data-entry-layout.css +0 -45
  144. package/dist/styles/density.css +1 -30
  145. package/dist/styles/dialog-layout.css +5 -69
  146. package/dist/styles/focus-ring.css +13 -87
  147. package/dist/styles/fonts.css +1 -31
  148. package/dist/styles/form-layout.css +7 -91
  149. package/dist/styles/index.css +2 -20
  150. package/dist/styles/layout.css +38 -456
  151. package/dist/styles/logo-layout.css +1 -35
  152. package/dist/styles/motion.css +3 -42
  153. package/dist/styles/navigation-layout.css +3 -50
  154. package/dist/styles/shell-layout.css +37 -403
  155. package/dist/styles/table-layout.css +16 -274
  156. package/dist/styles/text-layout.css +7 -21
  157. package/dist/styles/toggle.css +1 -23
  158. package/dist/theme/dxs.canonical.css +1 -55
  159. package/dist/theme/example.service.css +1 -26
  160. package/dist/theme/famgia.service.css +5 -15
  161. package/dist/tokens/axes.css +3 -40
  162. package/dist/tokens/base.css +1 -4
  163. package/dist/tokens/components/activity.css +6 -42
  164. package/dist/tokens/components/badge.css +4 -13
  165. package/dist/tokens/components/banner.css +4 -9
  166. package/dist/tokens/components/card.css +32 -88
  167. package/dist/tokens/components/chart.css +9 -26
  168. package/dist/tokens/components/control.css +37 -222
  169. package/dist/tokens/components/data-display.css +28 -79
  170. package/dist/tokens/components/data-entry.css +5 -19
  171. package/dist/tokens/components/descriptions.css +7 -27
  172. package/dist/tokens/components/email.css +38 -84
  173. package/dist/tokens/components/error-surface.css +2 -18
  174. package/dist/tokens/components/feedback.css +27 -124
  175. package/dist/tokens/components/form.css +13 -45
  176. package/dist/tokens/components/legal-document.css +16 -41
  177. package/dist/tokens/components/list-row.css +11 -36
  178. package/dist/tokens/components/logo.css +2 -57
  179. package/dist/tokens/components/navigation.css +16 -94
  180. package/dist/tokens/components/scroll-area.css +2 -18
  181. package/dist/tokens/components/separator.css +8 -30
  182. package/dist/tokens/components/sheet.css +3 -14
  183. package/dist/tokens/components/shell.css +53 -307
  184. package/dist/tokens/components/sidebar.css +5 -22
  185. package/dist/tokens/components/table.css +19 -140
  186. package/dist/tokens/components/toggle.css +3 -65
  187. package/dist/tokens/components/upload.css +7 -43
  188. package/dist/tokens/foundation.css +108 -383
  189. package/dist/tokens/semantic/layout.css +33 -207
  190. package/docs/CANONICAL-CONTRACTS.md +206 -0
  191. package/docs/COMPONENTS.md +157 -0
  192. package/docs/COMPOSITION-VS-COMPONENT.md +105 -0
  193. package/docs/CONSUMER-RULES.md +14 -0
  194. package/docs/CUSTOMER-THEMING.md +162 -0
  195. package/docs/DATETIME.md +69 -0
  196. package/docs/DEVELOPMENT.md +173 -0
  197. package/docs/FORMS.md +143 -0
  198. package/docs/FRAME-A11Y-CI.md +267 -0
  199. package/docs/FRAME-COVERAGE-LEDGER.md +123 -0
  200. package/docs/FRAME-COVERAGE-REPORT.md +140 -0
  201. package/docs/FRAME-COVERAGE-STANDARD.md +65 -0
  202. package/docs/FRAME-COVERAGE.md +29 -0
  203. package/docs/PROPS-REGISTRY.md +61 -0
  204. package/docs/PROPS-VOCABULARY.md +100 -0
  205. package/docs/README.md +60 -0
  206. package/docs/REGISTRY.md +70 -0
  207. package/docs/SPACING.md +56 -0
  208. package/docs/STANDARDS-vocabulary-tokens.md +48 -0
  209. package/docs/TESTING.md +98 -0
  210. package/docs/TOKENS.md +347 -0
  211. package/docs/asset-modules.d.ts +46 -0
  212. package/docs/assets/cover-terrain.svg +6 -0
  213. package/docs/assets/portrait-amber.svg +5 -0
  214. package/docs/assets/portrait-clay.svg +5 -0
  215. package/docs/assets/portrait-indigo.svg +5 -0
  216. package/docs/assets/portrait-iris.svg +5 -0
  217. package/docs/assets/portrait-moss.svg +5 -0
  218. package/docs/assets/portrait-plum.svg +5 -0
  219. package/docs/assets/portrait-slate.svg +5 -0
  220. package/docs/charts/compact-bar-trend.tsx +328 -0
  221. package/docs/charts/size-tiers.tsx +192 -0
  222. package/docs/data-display/accordion.tsx +99 -0
  223. package/docs/data-display/avatar.tsx +355 -0
  224. package/docs/data-display/badge.tsx +247 -0
  225. package/docs/data-display/card/examples/detail-panel.tsx +106 -0
  226. package/docs/data-display/card/index.md +3 -0
  227. package/docs/data-display/card/index.tsx +285 -0
  228. package/docs/data-display/carousel.tsx +156 -0
  229. package/docs/data-display/charts.tsx +198 -0
  230. package/docs/data-display/code-block.tsx +90 -0
  231. package/docs/data-display/collapsible.tsx +162 -0
  232. package/docs/data-display/credential-reveal.tsx +145 -0
  233. package/docs/data-display/data-table/examples/approval-queue.tsx +212 -0
  234. package/docs/data-display/data-table/examples/grid-features.tsx +158 -0
  235. package/docs/data-display/data-table/examples/list-page.tsx +175 -0
  236. package/docs/data-display/data-table/index.md +5 -0
  237. package/docs/data-display/data-table/index.tsx +295 -0
  238. package/docs/data-display/descriptions.tsx +143 -0
  239. package/docs/data-display/empty-state.tsx +164 -0
  240. package/docs/data-display/hover-card.tsx +181 -0
  241. package/docs/data-display/list-row.tsx +420 -0
  242. package/docs/data-display/permission-matrix.tsx +184 -0
  243. package/docs/data-display/popover-touch.tsx +27 -0
  244. package/docs/data-display/popover.tsx +245 -0
  245. package/docs/data-display/progress.tsx +84 -0
  246. package/docs/data-display/prose.tsx +105 -0
  247. package/docs/data-display/qr-code.tsx +68 -0
  248. package/docs/data-display/scroll-area.tsx +271 -0
  249. package/docs/data-display/service-launcher-card.tsx +145 -0
  250. package/docs/data-display/stat-card.tsx +99 -0
  251. package/docs/data-display/table.tsx +251 -0
  252. package/docs/data-display/timeline.tsx +112 -0
  253. package/docs/data-display/touch-actions.tsx +36 -0
  254. package/docs/data-display/tree-list.tsx +107 -0
  255. package/docs/data-entry/branch-scope-picker.tsx +123 -0
  256. package/docs/data-entry/calendar.tsx +180 -0
  257. package/docs/data-entry/cascader.tsx +378 -0
  258. package/docs/data-entry/checkbox.tsx +256 -0
  259. package/docs/data-entry/color-picker.tsx +117 -0
  260. package/docs/data-entry/command-palette.tsx +179 -0
  261. package/docs/data-entry/command.tsx +262 -0
  262. package/docs/data-entry/country-picker-recipe.tsx +170 -0
  263. package/docs/data-entry/date-picker.tsx +191 -0
  264. package/docs/data-entry/date-range-picker.tsx +149 -0
  265. package/docs/data-entry/form/examples/employee-settings.tsx +267 -0
  266. package/docs/data-entry/form/examples/invoice-form.tsx +387 -0
  267. package/docs/data-entry/form-field/examples/a11y-contract.tsx +240 -0
  268. package/docs/data-entry/form-field/examples/create-form.tsx +105 -0
  269. package/docs/data-entry/form-field/index.md +8 -0
  270. package/docs/data-entry/form-field/index.tsx +326 -0
  271. package/docs/data-entry/form-inertia.tsx +80 -0
  272. package/docs/data-entry/form-root.tsx +102 -0
  273. package/docs/data-entry/form.tsx +724 -0
  274. package/docs/data-entry/input-otp.tsx +231 -0
  275. package/docs/data-entry/input.tsx +114 -0
  276. package/docs/data-entry/label.tsx +100 -0
  277. package/docs/data-entry/month-picker.tsx +101 -0
  278. package/docs/data-entry/month-range-picker.tsx +115 -0
  279. package/docs/data-entry/number-input.tsx +240 -0
  280. package/docs/data-entry/password-input.tsx +106 -0
  281. package/docs/data-entry/password-strength.tsx +97 -0
  282. package/docs/data-entry/radio-group.tsx +95 -0
  283. package/docs/data-entry/rating.tsx +87 -0
  284. package/docs/data-entry/search-input.tsx +120 -0
  285. package/docs/data-entry/select-async-contracts.tsx +108 -0
  286. package/docs/data-entry/select-matrix.tsx +204 -0
  287. package/docs/data-entry/select.tsx +369 -0
  288. package/docs/data-entry/slider.tsx +130 -0
  289. package/docs/data-entry/switch.tsx +67 -0
  290. package/docs/data-entry/tag-input.tsx +108 -0
  291. package/docs/data-entry/textarea.tsx +209 -0
  292. package/docs/data-entry/time-picker.tsx +99 -0
  293. package/docs/data-entry/toggle-count.tsx +293 -0
  294. package/docs/data-entry/toggle-group.tsx +225 -0
  295. package/docs/data-entry/toggle.tsx +127 -0
  296. package/docs/data-entry/transfer.tsx +97 -0
  297. package/docs/data-entry/tree-select.tsx +204 -0
  298. package/docs/data-entry/upload.tsx +184 -0
  299. package/docs/feedback/alert-dialog.tsx +270 -0
  300. package/docs/feedback/alert.tsx +248 -0
  301. package/docs/feedback/banner.tsx +183 -0
  302. package/docs/feedback/danger-confirm.tsx +88 -0
  303. package/docs/feedback/dialog-touch.tsx +24 -0
  304. package/docs/feedback/dialog.tsx +193 -0
  305. package/docs/feedback/sheet.tsx +410 -0
  306. package/docs/feedback/skeleton.tsx +111 -0
  307. package/docs/feedback/toast.tsx +135 -0
  308. package/docs/feedback/tooltip.tsx +171 -0
  309. package/docs/feedback/two-factor-setup.tsx +68 -0
  310. package/docs/foundation/_email-specimen.ts +274 -0
  311. package/docs/foundation/colors.tsx +202 -0
  312. package/docs/foundation/density.tsx +232 -0
  313. package/docs/foundation/email-tokens.tsx +306 -0
  314. package/docs/foundation/radius-shadow.tsx +152 -0
  315. package/docs/foundation/spacing.tsx +211 -0
  316. package/docs/foundation/typography.tsx +147 -0
  317. package/docs/general/activity.tsx +224 -0
  318. package/docs/general/button/examples/destructive-confirm.tsx +90 -0
  319. package/docs/general/button/examples/form-actions.tsx +85 -0
  320. package/docs/general/button/examples/toolbar.tsx +160 -0
  321. package/docs/general/button/index.md +12 -0
  322. package/docs/general/button/index.tsx +211 -0
  323. package/docs/general/logo.tsx +131 -0
  324. package/docs/general/reveal.tsx +41 -0
  325. package/docs/general/typography.tsx +152 -0
  326. package/docs/layout/account-chip.tsx +72 -0
  327. package/docs/layout/admin-collection.tsx +122 -0
  328. package/docs/layout/app-shell.tsx +374 -0
  329. package/docs/layout/aspect-ratio.tsx +119 -0
  330. package/docs/layout/auth-account-summary.tsx +103 -0
  331. package/docs/layout/auth-divider.tsx +28 -0
  332. package/docs/layout/auth-footer.tsx +37 -0
  333. package/docs/layout/auth-identity.tsx +36 -0
  334. package/docs/layout/auth-recovery/examples/mfa-challenge.md +41 -0
  335. package/docs/layout/auth-recovery/examples/mfa-challenge.tsx +254 -0
  336. package/docs/layout/auth-recovery/examples/mobile-390.md +25 -0
  337. package/docs/layout/auth-recovery/examples/mobile-390.tsx +82 -0
  338. package/docs/layout/auth-recovery/examples/password-recovery.md +24 -0
  339. package/docs/layout/auth-recovery/examples/password-recovery.tsx +197 -0
  340. package/docs/layout/auth-recovery/examples/wrapping-ja-en-vi.md +27 -0
  341. package/docs/layout/auth-recovery/examples/wrapping-ja-en-vi.tsx +156 -0
  342. package/docs/layout/auth-recovery/index.md +140 -0
  343. package/docs/layout/auth-recovery/index.tsx +96 -0
  344. package/docs/layout/auth-shell-context.tsx +103 -0
  345. package/docs/layout/auth-shell-device.tsx +112 -0
  346. package/docs/layout/auth-shell-registration.tsx +335 -0
  347. package/docs/layout/auth-shell-variants.tsx +139 -0
  348. package/docs/layout/auth-shell.tsx +59 -0
  349. package/docs/layout/auth-stack.tsx +40 -0
  350. package/docs/layout/centered-shell.tsx +102 -0
  351. package/docs/layout/error-surface/examples/application-403.tsx +84 -0
  352. package/docs/layout/error-surface/examples/application-404.tsx +77 -0
  353. package/docs/layout/error-surface/examples/system-500.tsx +47 -0
  354. package/docs/layout/error-surface/examples/system-503.tsx +48 -0
  355. package/docs/layout/error-surface/index.md +153 -0
  356. package/docs/layout/error-surface/index.tsx +239 -0
  357. package/docs/layout/flex.tsx +262 -0
  358. package/docs/layout/legal-document-shell/_data.ts +110 -0
  359. package/docs/layout/legal-document-shell/examples/desktop-1440.md +12 -0
  360. package/docs/layout/legal-document-shell/examples/desktop-1440.tsx +83 -0
  361. package/docs/layout/legal-document-shell/examples/mobile-390.md +22 -0
  362. package/docs/layout/legal-document-shell/examples/mobile-390.tsx +70 -0
  363. package/docs/layout/legal-document-shell/examples/tablet-1024.md +3 -0
  364. package/docs/layout/legal-document-shell/examples/tablet-1024.tsx +84 -0
  365. package/docs/layout/legal-document-shell/examples/wrapping-ja-en-vi.md +18 -0
  366. package/docs/layout/legal-document-shell/examples/wrapping-ja-en-vi.tsx +61 -0
  367. package/docs/layout/legal-document-shell/index.md +51 -0
  368. package/docs/layout/legal-document-shell/index.tsx +101 -0
  369. package/docs/layout/master-detail.tsx +387 -0
  370. package/docs/layout/org-switcher.tsx +131 -0
  371. package/docs/layout/page-container.tsx +946 -0
  372. package/docs/layout/resizable-panel.tsx +419 -0
  373. package/docs/layout/responsive-grid.tsx +235 -0
  374. package/docs/layout/separator.tsx +327 -0
  375. package/docs/layout/service-role-panel.tsx +144 -0
  376. package/docs/layout/sidebar.tsx +586 -0
  377. package/docs/layout/split-pane.tsx +451 -0
  378. package/docs/layout/topbar.tsx +347 -0
  379. package/docs/navigation/app-setting-picker.tsx +141 -0
  380. package/docs/navigation/breadcrumb.tsx +128 -0
  381. package/docs/navigation/context-menu.tsx +117 -0
  382. package/docs/navigation/dropdown-menu.tsx +213 -0
  383. package/docs/navigation/filter-bar.tsx +227 -0
  384. package/docs/navigation/menubar.tsx +141 -0
  385. package/docs/navigation/navigation-menu.tsx +165 -0
  386. package/docs/navigation/pagination.tsx +241 -0
  387. package/docs/navigation/steps.tsx +263 -0
  388. package/docs/navigation/tabs-rtl.tsx +28 -0
  389. package/docs/navigation/tabs.tsx +308 -0
  390. package/docs/navigation/toolbar.tsx +339 -0
  391. package/docs/providers/app-provider.tsx +191 -0
  392. package/docs/providers/format-date.tsx +230 -0
  393. package/docs/query/button-refetch.tsx +160 -0
  394. package/docs/query/data-state.tsx +192 -0
  395. package/docs/query/infinite-query-state.tsx +93 -0
  396. package/docs/query/mutation-feedback.tsx +70 -0
  397. package/docs/query/prefetch-link.tsx +262 -0
  398. package/docs/query/touch-actions.tsx +68 -0
  399. package/docs/showcase/acme-portal.tsx +441 -0
  400. package/docs/showcase/acme-website.tsx +455 -0
  401. package/docs/showcase/case1-warehouse-dashboard.tsx +343 -0
  402. package/docs/showcase/case2-employee-me.tsx +441 -0
  403. package/docs/showcase/case3-approval-workflow.tsx +527 -0
  404. package/docs/showcase/case4-login.tsx +307 -0
  405. package/docs/showcase/case5-shift-calendar.tsx +795 -0
  406. package/docs/showcase/case6-agency-handy.tsx +1072 -0
  407. package/docs/showcase/futurelastic-web.tsx +390 -0
  408. package/docs/showcase/org-switcher.tsx +313 -0
  409. package/docs/showcase/permission-matrix.tsx +343 -0
  410. package/docs/showcase/public-landing.tsx +210 -0
  411. package/docs/showcase/settings-account-sections.tsx +559 -0
  412. package/docs/showcase/settings-security-mfa.tsx +145 -0
  413. package/docs/showcase/table-approval-queue.tsx +199 -0
  414. package/docs/showcase/table-bulk-actions.tsx +460 -0
  415. package/docs/showcase/table-compact-kintone.tsx +438 -0
  416. package/docs/showcase/table-conditional-format.tsx +367 -0
  417. package/docs/showcase/table-crud-list.tsx +544 -0
  418. package/docs/showcase/table-density.tsx +340 -0
  419. package/docs/showcase/table-expandable-rows.tsx +471 -0
  420. package/docs/showcase/table-filter-chips.tsx +461 -0
  421. package/docs/showcase/table-footer-totals.tsx +311 -0
  422. package/docs/showcase/table-grouped-subtotals.tsx +388 -0
  423. package/docs/showcase/table-master-detail.tsx +479 -0
  424. package/docs/showcase/table-pagination.tsx +320 -0
  425. package/docs/showcase/table-states.tsx +363 -0
  426. package/docs/showcase/table-sticky-columns.tsx +373 -0
  427. package/docs/showcase/table-tree-rows.tsx +444 -0
  428. package/docs/showcase/table-view-tabs.tsx +488 -0
  429. package/package.json +9 -11
  430. package/scripts/_agent-setup.mjs +16 -2
  431. package/scripts/audit-hook.mjs +3 -13
  432. package/scripts/init-agent-kit.mjs +1 -14
  433. package/scripts/postinstall.mjs +12 -12
  434. package/scripts/ui-audit.mjs +93 -31
  435. package/scripts/visual-audit-rules.mjs +51 -7
  436. 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
+ }