@comitor/ui 2.1.0 → 3.0.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 (405) hide show
  1. package/README.md +63 -24
  2. package/dist/chart/chart.d.ts +6 -1
  3. package/dist/chart/chart.js +3 -1
  4. package/dist/chart/chart.js.map +1 -1
  5. package/dist/chart/comitor-chart.d.ts +4 -1
  6. package/dist/chart/comitor-chart.js +3 -1
  7. package/dist/chart/comitor-chart.js.map +1 -1
  8. package/dist/components/composite/avatar-group.d.ts +9 -6
  9. package/dist/components/composite/avatar-group.js +6 -2
  10. package/dist/components/composite/avatar-group.js.map +1 -1
  11. package/dist/components/composite/combobox.d.ts +11 -11
  12. package/dist/components/composite/combobox.js +25 -11
  13. package/dist/components/composite/combobox.js.map +1 -1
  14. package/dist/components/composite/command-palette-constants.d.ts +15 -5
  15. package/dist/components/composite/command-palette-constants.js +3 -2
  16. package/dist/components/composite/command-palette-constants.js.map +1 -1
  17. package/dist/components/composite/command-palette.d.ts +2 -2
  18. package/dist/components/composite/command-palette.js +8 -5
  19. package/dist/components/composite/command-palette.js.map +1 -1
  20. package/dist/components/composite/confirm-dialog.d.ts +8 -8
  21. package/dist/components/composite/confirm-dialog.js +8 -3
  22. package/dist/components/composite/confirm-dialog.js.map +1 -1
  23. package/dist/components/composite/copy-button.d.ts +7 -3
  24. package/dist/components/composite/copy-button.js +7 -3
  25. package/dist/components/composite/copy-button.js.map +1 -1
  26. package/dist/components/composite/data-table-constants.d.ts +7 -3
  27. package/dist/components/composite/data-table-constants.js +2 -9
  28. package/dist/components/composite/data-table-constants.js.map +1 -1
  29. package/dist/components/composite/data-table.js +3 -3
  30. package/dist/components/composite/data-table.js.map +1 -1
  31. package/dist/components/composite/date-picker-constants.d.ts +32 -9
  32. package/dist/components/composite/date-picker-constants.js +16 -11
  33. package/dist/components/composite/date-picker-constants.js.map +1 -1
  34. package/dist/components/composite/date-picker.d.ts +19 -10
  35. package/dist/components/composite/date-picker.js +33 -9
  36. package/dist/components/composite/date-picker.js.map +1 -1
  37. package/dist/components/composite/filter-chips.d.ts +4 -3
  38. package/dist/components/composite/filter-chips.js +6 -2
  39. package/dist/components/composite/filter-chips.js.map +1 -1
  40. package/dist/components/composite/inline-edit.d.ts +14 -3
  41. package/dist/components/composite/inline-edit.js +8 -4
  42. package/dist/components/composite/inline-edit.js.map +1 -1
  43. package/dist/components/composite/relative-time.d.ts +4 -4
  44. package/dist/components/composite/relative-time.js +4 -2
  45. package/dist/components/composite/relative-time.js.map +1 -1
  46. package/dist/components/composite/route-tabs-nav.d.ts +9 -0
  47. package/dist/components/composite/route-tabs-nav.js +11 -0
  48. package/dist/components/composite/route-tabs-nav.js.map +1 -0
  49. package/dist/components/composite/route-tabs.d.ts +4 -1
  50. package/dist/components/composite/route-tabs.js +3 -2
  51. package/dist/components/composite/route-tabs.js.map +1 -1
  52. package/dist/components/composite/search-filter-bar-constants.d.ts +18 -8
  53. package/dist/components/composite/search-filter-bar-constants.js +2 -14
  54. package/dist/components/composite/search-filter-bar-constants.js.map +1 -1
  55. package/dist/components/composite/search-filter-bar.d.ts +5 -2
  56. package/dist/components/composite/search-filter-bar.js +2 -2
  57. package/dist/components/composite/search-filter-bar.js.map +1 -1
  58. package/dist/components/composite/stat-card.d.ts +1 -1
  59. package/dist/components/composite/table-pagination-constants.d.ts +16 -11
  60. package/dist/components/composite/table-pagination-constants.js +2 -12
  61. package/dist/components/composite/table-pagination-constants.js.map +1 -1
  62. package/dist/components/composite/table-pagination.d.ts +6 -3
  63. package/dist/components/composite/table-pagination.js +3 -5
  64. package/dist/components/composite/table-pagination.js.map +1 -1
  65. package/dist/components/composite/toast.d.ts +5 -2
  66. package/dist/components/composite/toast.js +3 -1
  67. package/dist/components/composite/toast.js.map +1 -1
  68. package/dist/components/ui/action-bar-constants.d.ts +8 -4
  69. package/dist/components/ui/action-bar-constants.js +2 -5
  70. package/dist/components/ui/action-bar-constants.js.map +1 -1
  71. package/dist/components/ui/action-bar.d.ts +5 -4
  72. package/dist/components/ui/action-bar.js +47 -3
  73. package/dist/components/ui/action-bar.js.map +1 -1
  74. package/dist/components/ui/alert.d.ts +1 -1
  75. package/dist/components/ui/badge-overflow.d.ts +2 -2
  76. package/dist/components/ui/badge-overflow.js +4 -2
  77. package/dist/components/ui/badge-overflow.js.map +1 -1
  78. package/dist/components/ui/badge.d.ts +1 -1
  79. package/dist/components/ui/banner.d.ts +6 -5
  80. package/dist/components/ui/banner.js +3 -1
  81. package/dist/components/ui/banner.js.map +1 -1
  82. package/dist/components/ui/breadcrumb.d.ts +11 -4
  83. package/dist/components/ui/breadcrumb.js +7 -2
  84. package/dist/components/ui/breadcrumb.js.map +1 -1
  85. package/dist/components/ui/button.d.ts +2 -2
  86. package/dist/components/ui/calendar.d.ts +12 -5
  87. package/dist/components/ui/calendar.js +38 -3
  88. package/dist/components/ui/calendar.js.map +1 -1
  89. package/dist/components/ui/carousel.d.ts +4 -3
  90. package/dist/components/ui/carousel.js +11 -4
  91. package/dist/components/ui/carousel.js.map +1 -1
  92. package/dist/components/ui/circular-progress.js +10 -2
  93. package/dist/components/ui/circular-progress.js.map +1 -1
  94. package/dist/components/ui/command.d.ts +5 -4
  95. package/dist/components/ui/command.js +6 -2
  96. package/dist/components/ui/command.js.map +1 -1
  97. package/dist/components/ui/cropper-constants.d.ts +8 -3
  98. package/dist/components/ui/cropper-constants.js +2 -6
  99. package/dist/components/ui/cropper-constants.js.map +1 -1
  100. package/dist/components/ui/cropper.d.ts +2 -1
  101. package/dist/components/ui/cropper.js +2 -2
  102. package/dist/components/ui/cropper.js.map +1 -1
  103. package/dist/components/ui/dialog.d.ts +2 -2
  104. package/dist/components/ui/dialog.js +4 -1
  105. package/dist/components/ui/dialog.js.map +1 -1
  106. package/dist/components/ui/item.d.ts +2 -2
  107. package/dist/components/ui/kanban-constants.d.ts +7 -1
  108. package/dist/components/ui/kanban-constants.js +2 -17
  109. package/dist/components/ui/kanban-constants.js.map +1 -1
  110. package/dist/components/ui/kanban.d.ts +2 -2
  111. package/dist/components/ui/kanban.js +11 -5
  112. package/dist/components/ui/kanban.js.map +1 -1
  113. package/dist/components/ui/key-value-constants.d.ts +7 -3
  114. package/dist/components/ui/key-value-constants.js +2 -7
  115. package/dist/components/ui/key-value-constants.js.map +1 -1
  116. package/dist/components/ui/key-value.d.ts +5 -5
  117. package/dist/components/ui/key-value.js +72 -78
  118. package/dist/components/ui/key-value.js.map +1 -1
  119. package/dist/components/ui/mask-input-constants.d.ts +2 -3
  120. package/dist/components/ui/mask-input-constants.js +36 -77
  121. package/dist/components/ui/mask-input-constants.js.map +1 -1
  122. package/dist/components/ui/mask-input.d.ts +7 -4
  123. package/dist/components/ui/mask-input.js +22 -22
  124. package/dist/components/ui/mask-input.js.map +1 -1
  125. package/dist/components/ui/mention.d.ts +3 -2
  126. package/dist/components/ui/mention.js +4 -6
  127. package/dist/components/ui/mention.js.map +1 -1
  128. package/dist/components/ui/pagination.d.ts +13 -9
  129. package/dist/components/ui/pagination.js +15 -6
  130. package/dist/components/ui/pagination.js.map +1 -1
  131. package/dist/components/ui/phone-input-constants.d.ts +17 -6
  132. package/dist/components/ui/phone-input-constants.js +2 -5
  133. package/dist/components/ui/phone-input-constants.js.map +1 -1
  134. package/dist/components/ui/phone-input.d.ts +18 -11
  135. package/dist/components/ui/phone-input.js +14 -12
  136. package/dist/components/ui/phone-input.js.map +1 -1
  137. package/dist/components/ui/progress.d.ts +2 -2
  138. package/dist/components/ui/rating.d.ts +2 -1
  139. package/dist/components/ui/rating.js +8 -4
  140. package/dist/components/ui/rating.js.map +1 -1
  141. package/dist/components/ui/scroll-spy.js +3 -1
  142. package/dist/components/ui/scroll-spy.js.map +1 -1
  143. package/dist/components/ui/sheet.d.ts +2 -2
  144. package/dist/components/ui/sheet.js +10 -1
  145. package/dist/components/ui/sheet.js.map +1 -1
  146. package/dist/components/ui/slider.d.ts +1 -1
  147. package/dist/components/ui/slider.js +15 -1
  148. package/dist/components/ui/slider.js.map +1 -1
  149. package/dist/components/ui/sonner.js +2 -0
  150. package/dist/components/ui/sonner.js.map +1 -1
  151. package/dist/components/ui/spinner.js +4 -1
  152. package/dist/components/ui/spinner.js.map +1 -1
  153. package/dist/components/ui/stepper-constants.d.ts +7 -2
  154. package/dist/components/ui/stepper-constants.js +2 -5
  155. package/dist/components/ui/stepper-constants.js.map +1 -1
  156. package/dist/components/ui/stepper.d.ts +2 -1
  157. package/dist/components/ui/stepper.js +2 -2
  158. package/dist/components/ui/stepper.js.map +1 -1
  159. package/dist/components/ui/tags-input.d.ts +3 -2
  160. package/dist/components/ui/tags-input.js +4 -2
  161. package/dist/components/ui/tags-input.js.map +1 -1
  162. package/dist/components/ui/time-picker-constants.d.ts +12 -9
  163. package/dist/components/ui/time-picker-constants.js +2 -10
  164. package/dist/components/ui/time-picker-constants.js.map +1 -1
  165. package/dist/components/ui/time-picker.d.ts +11 -6
  166. package/dist/components/ui/time-picker.js +5 -7
  167. package/dist/components/ui/time-picker.js.map +1 -1
  168. package/dist/components/ui/toggle-group.js +12 -1
  169. package/dist/components/ui/toggle-group.js.map +1 -1
  170. package/dist/components/ui/toggle.d.ts +1 -1
  171. package/dist/components/ui/tour-constants.d.ts +7 -2
  172. package/dist/components/ui/tour-constants.js +2 -8
  173. package/dist/components/ui/tour-constants.js.map +1 -1
  174. package/dist/components/ui/tour.d.ts +3 -2
  175. package/dist/components/ui/tour.js +5 -4
  176. package/dist/components/ui/tour.js.map +1 -1
  177. package/dist/data-grid/data-grid-cell-variants.js +4 -4
  178. package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
  179. package/dist/data-grid/data-grid-column-header.js +1 -1
  180. package/dist/data-grid/data-grid-column-header.js.map +1 -1
  181. package/dist/data-grid/data-grid-filter-menu.d.ts +5 -4
  182. package/dist/data-grid/data-grid-filter-menu.js +13 -8
  183. package/dist/data-grid/data-grid-filter-menu.js.map +1 -1
  184. package/dist/data-grid/data-grid-keyboard-shortcuts.d.ts +4 -4
  185. package/dist/data-grid/data-grid-keyboard-shortcuts.js.map +1 -1
  186. package/dist/data-grid/data-grid-locale.d.ts +58 -6
  187. package/dist/data-grid/data-grid-locale.js +41 -9
  188. package/dist/data-grid/data-grid-locale.js.map +1 -1
  189. package/dist/data-grid/data-grid-row-height-menu.d.ts +4 -3
  190. package/dist/data-grid/data-grid-row-height-menu.js.map +1 -1
  191. package/dist/data-grid/data-grid-sort-menu.d.ts +4 -4
  192. package/dist/data-grid/data-grid-sort-menu.js +1 -1
  193. package/dist/data-grid/data-grid-sort-menu.js.map +1 -1
  194. package/dist/data-grid/data-grid-view-menu.d.ts +4 -4
  195. package/dist/data-grid/data-grid-view-menu.js.map +1 -1
  196. package/dist/data-grid/data-grid.d.ts +4 -3
  197. package/dist/data-grid/data-grid.js +2 -3
  198. package/dist/data-grid/data-grid.js.map +1 -1
  199. package/dist/data-grid/index.d.ts +1 -0
  200. package/dist/data-grid/index.js.map +1 -1
  201. package/dist/data-grid/messages-en.d.ts +23 -4
  202. package/dist/data-grid/messages-en.js +20 -0
  203. package/dist/data-grid/messages-en.js.map +1 -1
  204. package/dist/data-grid/messages-vi.d.ts +3 -1
  205. package/dist/data-grid/messages-vi.js +14 -0
  206. package/dist/data-grid/messages-vi.js.map +1 -1
  207. package/dist/data-grid/messages.d.ts +29 -3
  208. package/dist/data-grid/messages.js.map +1 -1
  209. package/dist/data-grid/sortable.js +16 -19
  210. package/dist/data-grid/sortable.js.map +1 -1
  211. package/dist/data-grid/types.d.ts +1 -1
  212. package/dist/data-grid/use-data-grid-undo-redo.d.ts +5 -4
  213. package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
  214. package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
  215. package/dist/data-grid/use-data-grid.d.ts +23 -8
  216. package/dist/data-grid/use-data-grid.js +8 -6
  217. package/dist/data-grid/use-data-grid.js.map +1 -1
  218. package/dist/data-grid/utils.d.ts +24 -4
  219. package/dist/data-grid/utils.js +15 -5
  220. package/dist/data-grid/utils.js.map +1 -1
  221. package/dist/i18n/locale-provider.d.ts +127 -0
  222. package/dist/i18n/locale-provider.js +47 -0
  223. package/dist/i18n/locale-provider.js.map +1 -0
  224. package/dist/i18n/locale.d.ts +27 -0
  225. package/dist/i18n/locale.js +10 -0
  226. package/dist/i18n/locale.js.map +1 -0
  227. package/dist/i18n/messages.d.ts +116 -0
  228. package/dist/i18n/messages.js +149 -0
  229. package/dist/i18n/messages.js.map +1 -0
  230. package/dist/i18n/namespaces/action-bar.d.ts +7 -0
  231. package/dist/i18n/namespaces/action-bar.js +15 -0
  232. package/dist/i18n/namespaces/action-bar.js.map +1 -0
  233. package/dist/i18n/namespaces/avatar-group.d.ts +14 -0
  234. package/dist/i18n/namespaces/avatar-group.js +13 -0
  235. package/dist/i18n/namespaces/avatar-group.js.map +1 -0
  236. package/dist/i18n/namespaces/badge-overflow.d.ts +12 -0
  237. package/dist/i18n/namespaces/badge-overflow.js +11 -0
  238. package/dist/i18n/namespaces/badge-overflow.js.map +1 -0
  239. package/dist/i18n/namespaces/banner.d.ts +9 -0
  240. package/dist/i18n/namespaces/banner.js +7 -0
  241. package/dist/i18n/namespaces/banner.js.map +1 -0
  242. package/dist/i18n/namespaces/breadcrumb.d.ts +11 -0
  243. package/dist/i18n/namespaces/breadcrumb.js +13 -0
  244. package/dist/i18n/namespaces/breadcrumb.js.map +1 -0
  245. package/dist/i18n/namespaces/calendar.d.ts +40 -0
  246. package/dist/i18n/namespaces/calendar.js +25 -0
  247. package/dist/i18n/namespaces/calendar.js.map +1 -0
  248. package/dist/i18n/namespaces/carousel.d.ts +15 -0
  249. package/dist/i18n/namespaces/carousel.js +17 -0
  250. package/dist/i18n/namespaces/carousel.js.map +1 -0
  251. package/dist/i18n/namespaces/combobox.d.ts +21 -0
  252. package/dist/i18n/namespaces/combobox.js +23 -0
  253. package/dist/i18n/namespaces/combobox.js.map +1 -0
  254. package/dist/i18n/namespaces/command-palette.d.ts +20 -0
  255. package/dist/i18n/namespaces/command-palette.js +19 -0
  256. package/dist/i18n/namespaces/command-palette.js.map +1 -0
  257. package/dist/i18n/namespaces/command.d.ts +11 -0
  258. package/dist/i18n/namespaces/command.js +13 -0
  259. package/dist/i18n/namespaces/command.js.map +1 -0
  260. package/dist/i18n/namespaces/confirm-dialog.d.ts +13 -0
  261. package/dist/i18n/namespaces/confirm-dialog.js +15 -0
  262. package/dist/i18n/namespaces/confirm-dialog.js.map +1 -0
  263. package/dist/i18n/namespaces/contrast.d.ts +7 -0
  264. package/dist/i18n/namespaces/contrast.js +13 -0
  265. package/dist/i18n/namespaces/contrast.js.map +1 -0
  266. package/dist/i18n/namespaces/copy-button.d.ts +11 -0
  267. package/dist/i18n/namespaces/copy-button.js +13 -0
  268. package/dist/i18n/namespaces/copy-button.js.map +1 -0
  269. package/dist/i18n/namespaces/cropper.d.ts +7 -0
  270. package/dist/i18n/namespaces/cropper.js +17 -0
  271. package/dist/i18n/namespaces/cropper.js.map +1 -0
  272. package/dist/i18n/namespaces/data-table.d.ts +7 -0
  273. package/dist/i18n/namespaces/data-table.js +23 -0
  274. package/dist/i18n/namespaces/data-table.js.map +1 -0
  275. package/dist/i18n/namespaces/date-picker.d.ts +42 -0
  276. package/dist/i18n/namespaces/date-picker.js +35 -0
  277. package/dist/i18n/namespaces/date-picker.js.map +1 -0
  278. package/dist/i18n/namespaces/dialog.d.ts +9 -0
  279. package/dist/i18n/namespaces/dialog.js +7 -0
  280. package/dist/i18n/namespaces/dialog.js.map +1 -0
  281. package/dist/i18n/namespaces/display-axis.d.ts +25 -0
  282. package/dist/i18n/namespaces/display-axis.js +21 -0
  283. package/dist/i18n/namespaces/display-axis.js.map +1 -0
  284. package/dist/i18n/namespaces/file-upload.d.ts +11 -0
  285. package/dist/i18n/namespaces/file-upload.js +19 -0
  286. package/dist/i18n/namespaces/file-upload.js.map +1 -0
  287. package/dist/i18n/namespaces/filter-chips.d.ts +11 -0
  288. package/dist/i18n/namespaces/filter-chips.js +13 -0
  289. package/dist/i18n/namespaces/filter-chips.js.map +1 -0
  290. package/dist/i18n/namespaces/image-upload-field.d.ts +12 -0
  291. package/dist/i18n/namespaces/image-upload-field.js +23 -0
  292. package/dist/i18n/namespaces/image-upload-field.js.map +1 -0
  293. package/dist/i18n/namespaces/inline-edit.d.ts +15 -0
  294. package/dist/i18n/namespaces/inline-edit.js +17 -0
  295. package/dist/i18n/namespaces/inline-edit.js.map +1 -0
  296. package/dist/i18n/namespaces/kanban.d.ts +11 -0
  297. package/dist/i18n/namespaces/kanban.js +39 -0
  298. package/dist/i18n/namespaces/kanban.js.map +1 -0
  299. package/dist/i18n/namespaces/key-value.d.ts +10 -0
  300. package/dist/i18n/namespaces/key-value.js +19 -0
  301. package/dist/i18n/namespaces/key-value.js.map +1 -0
  302. package/dist/i18n/namespaces/mention.d.ts +9 -0
  303. package/dist/i18n/namespaces/mention.js +7 -0
  304. package/dist/i18n/namespaces/mention.js.map +1 -0
  305. package/dist/i18n/namespaces/pagination.d.ts +19 -0
  306. package/dist/i18n/namespaces/pagination.js +21 -0
  307. package/dist/i18n/namespaces/pagination.js.map +1 -0
  308. package/dist/i18n/namespaces/phone-input.d.ts +10 -0
  309. package/dist/i18n/namespaces/phone-input.js +17 -0
  310. package/dist/i18n/namespaces/phone-input.js.map +1 -0
  311. package/dist/i18n/namespaces/rating.d.ts +12 -0
  312. package/dist/i18n/namespaces/rating.js +12 -0
  313. package/dist/i18n/namespaces/rating.js.map +1 -0
  314. package/dist/i18n/namespaces/route-tabs.d.ts +9 -0
  315. package/dist/i18n/namespaces/route-tabs.js +7 -0
  316. package/dist/i18n/namespaces/route-tabs.js.map +1 -0
  317. package/dist/i18n/namespaces/scroll-spy.d.ts +9 -0
  318. package/dist/i18n/namespaces/scroll-spy.js +7 -0
  319. package/dist/i18n/namespaces/scroll-spy.js.map +1 -0
  320. package/dist/i18n/namespaces/search-filter-bar.d.ts +15 -0
  321. package/dist/i18n/namespaces/search-filter-bar.js +32 -0
  322. package/dist/i18n/namespaces/search-filter-bar.js.map +1 -0
  323. package/dist/i18n/namespaces/sheet.d.ts +9 -0
  324. package/dist/i18n/namespaces/sheet.js +7 -0
  325. package/dist/i18n/namespaces/sheet.js.map +1 -0
  326. package/dist/i18n/namespaces/shell.d.ts +21 -0
  327. package/dist/i18n/namespaces/shell.js +75 -0
  328. package/dist/i18n/namespaces/shell.js.map +1 -0
  329. package/dist/i18n/namespaces/spinner.d.ts +9 -0
  330. package/dist/i18n/namespaces/spinner.js +7 -0
  331. package/dist/i18n/namespaces/spinner.js.map +1 -0
  332. package/dist/i18n/namespaces/stepper.d.ts +7 -0
  333. package/dist/i18n/namespaces/stepper.js +15 -0
  334. package/dist/i18n/namespaces/stepper.js.map +1 -0
  335. package/dist/i18n/namespaces/table-pagination.d.ts +7 -0
  336. package/dist/i18n/namespaces/table-pagination.js +29 -0
  337. package/dist/i18n/namespaces/table-pagination.js.map +1 -0
  338. package/dist/i18n/namespaces/tags-input.d.ts +12 -0
  339. package/dist/i18n/namespaces/tags-input.js +11 -0
  340. package/dist/i18n/namespaces/tags-input.js.map +1 -0
  341. package/dist/i18n/namespaces/time-picker.d.ts +15 -0
  342. package/dist/i18n/namespaces/time-picker.js +25 -0
  343. package/dist/i18n/namespaces/time-picker.js.map +1 -0
  344. package/dist/i18n/namespaces/toast.d.ts +16 -0
  345. package/dist/i18n/namespaces/toast.js +7 -0
  346. package/dist/i18n/namespaces/toast.js.map +1 -0
  347. package/dist/i18n/namespaces/tour.d.ts +7 -0
  348. package/dist/i18n/namespaces/tour.js +21 -0
  349. package/dist/i18n/namespaces/tour.js.map +1 -0
  350. package/dist/index.d.ts +32 -0
  351. package/dist/index.js +24 -1
  352. package/dist/index.js.map +1 -1
  353. package/dist/lib/toast-language.d.ts +8 -0
  354. package/dist/lib/toast-language.js +19 -0
  355. package/dist/lib/toast-language.js.map +1 -0
  356. package/dist/shell/app-header.js.map +1 -1
  357. package/dist/shell/app-shell.d.ts +20 -2
  358. package/dist/shell/app-shell.js +4 -1
  359. package/dist/shell/app-shell.js.map +1 -1
  360. package/dist/shell/contrast-constants.d.ts +9 -1
  361. package/dist/shell/contrast-constants.js +2 -4
  362. package/dist/shell/contrast-constants.js.map +1 -1
  363. package/dist/shell/contrast-toggle.d.ts +4 -1
  364. package/dist/shell/contrast-toggle.js +2 -2
  365. package/dist/shell/contrast-toggle.js.map +1 -1
  366. package/dist/shell/display-axis-constants.d.ts +2 -1
  367. package/dist/shell/display-axis-constants.js.map +1 -1
  368. package/dist/shell/display-axis-controls.d.ts +15 -6
  369. package/dist/shell/display-axis-controls.js +11 -9
  370. package/dist/shell/display-axis-controls.js.map +1 -1
  371. package/dist/shell/display-axis-provider.d.ts +1 -1
  372. package/dist/shell/mobile-menu.js.map +1 -1
  373. package/dist/shell/notifications-button.d.ts +1 -1
  374. package/dist/shell/notifications-button.js +3 -2
  375. package/dist/shell/notifications-button.js.map +1 -1
  376. package/dist/shell/shell-context.d.ts +4 -1
  377. package/dist/shell/shell-context.js +2 -4
  378. package/dist/shell/shell-context.js.map +1 -1
  379. package/dist/shell/sidebar.d.ts +1 -1
  380. package/dist/shell/sidebar.js.map +1 -1
  381. package/dist/shell/theme-toggle.d.ts +6 -3
  382. package/dist/shell/theme-toggle.js +2 -2
  383. package/dist/shell/theme-toggle.js.map +1 -1
  384. package/dist/shell/types.d.ts +14 -8
  385. package/dist/shell/types.js +2 -35
  386. package/dist/shell/types.js.map +1 -1
  387. package/dist/shell/user-menu.d.ts +6 -6
  388. package/dist/shell/user-menu.js.map +1 -1
  389. package/dist/shell/utils.d.ts +1 -1
  390. package/dist/shell/utils.js.map +1 -1
  391. package/dist/shell/workspace-switcher.js +1 -1
  392. package/dist/shell/workspace-switcher.js.map +1 -1
  393. package/dist/uploader/file-upload-constants.d.ts +13 -0
  394. package/dist/uploader/file-upload-constants.js +4 -16
  395. package/dist/uploader/file-upload-constants.js.map +1 -1
  396. package/dist/uploader/file-upload.d.ts +4 -1
  397. package/dist/uploader/file-upload.js +2 -2
  398. package/dist/uploader/file-upload.js.map +1 -1
  399. package/dist/uploader/image-upload-field.d.ts +5 -1
  400. package/dist/uploader/image-upload-field.js +2 -2
  401. package/dist/uploader/image-upload-field.js.map +1 -1
  402. package/dist/uploader/use-file-upload.d.ts +4 -0
  403. package/dist/uploader/use-file-upload.js +6 -2
  404. package/dist/uploader/use-file-upload.js.map +1 -1
  405. package/package.json +4 -3
@@ -1,19 +1,38 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { format as formatDate, isAfter, isBefore, isValid, parse, startOfDay } from "date-fns";
4
- import { vi } from "date-fns/locale";
5
4
  import { CalendarDays, X } from "lucide-react";
6
5
  import { useEffect, useState } from "react";
6
+ import { useComitorLabels, useComitorLocale } from "../../i18n/locale-provider.js";
7
7
  import { cn } from "../../lib/cn.js";
8
8
  import { Button } from "../ui/button.js";
9
9
  import { Calendar } from "../ui/calendar.js";
10
10
  import { Input } from "../ui/input.js";
11
11
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
12
12
  import {
13
- DEFAULT_DATE_PICKER_LABELS,
13
+ DEFAULT_DATE_PRESETS,
14
14
  DEFAULT_DATE_RANGE_PRESETS,
15
15
  dateFormatForLocale
16
16
  } from "./date-picker-constants.js";
17
+ const PRESET_MESSAGE_KEY_BY_ID = {
18
+ today: "today",
19
+ yesterday: "yesterday",
20
+ "week-ago": "weekAgo",
21
+ "last-7": "last7Days",
22
+ "last-30": "last30Days",
23
+ "this-month": "thisMonth",
24
+ "last-month": "lastMonth"
25
+ };
26
+ const DEFAULT_PRESET_MESSAGE_KEYS = new Map(
27
+ [...DEFAULT_DATE_PRESETS, ...DEFAULT_DATE_RANGE_PRESETS].flatMap((preset) => {
28
+ const key = PRESET_MESSAGE_KEY_BY_ID[preset.id];
29
+ return key ? [[preset, key]] : [];
30
+ })
31
+ );
32
+ function presetLabel(preset, messages) {
33
+ const key = DEFAULT_PRESET_MESSAGE_KEYS.get(preset);
34
+ return key ? messages[key] : preset.label;
35
+ }
17
36
  function isOutOfRange(date, minDate, maxDate) {
18
37
  if (minDate && isBefore(date, startOfDay(minDate))) return true;
19
38
  if (maxDate && isAfter(date, startOfDay(maxDate))) return true;
@@ -34,7 +53,7 @@ function DatePicker({
34
53
  maxDate,
35
54
  clearable = true,
36
55
  presets = false,
37
- locale = vi,
56
+ locale: localeProp,
38
57
  dateFormat,
39
58
  labels,
40
59
  className,
@@ -42,8 +61,10 @@ function DatePicker({
42
61
  name,
43
62
  ...aria
44
63
  }) {
64
+ const { dateFnsLocale } = useComitorLocale();
65
+ const locale = localeProp ?? dateFnsLocale;
45
66
  const pattern = dateFormat ?? dateFormatForLocale(locale);
46
- const label = { ...DEFAULT_DATE_PICKER_LABELS, ...labels };
67
+ const label = useComitorLabels("datePicker", labels);
47
68
  const [open, setOpen] = useState(false);
48
69
  const [text, setText] = useState(() => value ? formatDate(value, pattern, { locale }) : "");
49
70
  useEffect(() => {
@@ -128,7 +149,7 @@ function DatePicker({
128
149
  onValueChange(preset.getValue());
129
150
  setOpen(false);
130
151
  },
131
- children: preset.label
152
+ children: presetLabel(preset, label.presets)
132
153
  },
133
154
  preset.id
134
155
  )) }),
@@ -161,22 +182,25 @@ function formatRange(range, pattern, locale) {
161
182
  function DateRangePicker({
162
183
  value,
163
184
  onValueChange,
164
- placeholder = "Ch\u1ECDn kho\u1EA3ng ng\xE0y",
185
+ placeholder: placeholderProp,
165
186
  disabled = false,
166
187
  minDate,
167
188
  maxDate,
168
189
  clearable = true,
169
190
  presets = DEFAULT_DATE_RANGE_PRESETS,
170
191
  numberOfMonths = 2,
171
- locale = vi,
192
+ locale: localeProp,
172
193
  dateFormat,
173
194
  labels,
174
195
  className,
175
196
  id,
176
197
  ...aria
177
198
  }) {
199
+ const { dateFnsLocale } = useComitorLocale();
200
+ const locale = localeProp ?? dateFnsLocale;
178
201
  const pattern = dateFormat ?? dateFormatForLocale(locale);
179
- const text = { ...DEFAULT_DATE_PICKER_LABELS, ...labels };
202
+ const text = useComitorLabels("datePicker", labels);
203
+ const placeholder = placeholderProp ?? text.rangePlaceholder;
180
204
  const [open, setOpen] = useState(false);
181
205
  const label = formatRange(value, pattern, locale);
182
206
  const presetList = presets === false ? [] : presets;
@@ -221,7 +245,7 @@ function DateRangePicker({
221
245
  onValueChange(preset.getValue());
222
246
  setOpen(false);
223
247
  },
224
- children: preset.label
248
+ children: presetLabel(preset, text.presets)
225
249
  },
226
250
  preset.id
227
251
  )) }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/date-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport { format as formatDate, isAfter, isBefore, isValid, type Locale, parse, startOfDay } from \"date-fns\";\nimport { vi } from \"date-fns/locale\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Calendar } from \"../ui/calendar.js\";\nimport { Input } from \"../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport {\n type DatePickerLabels,\n type DatePreset,\n type DateRangePreset,\n DEFAULT_DATE_PICKER_LABELS,\n DEFAULT_DATE_RANGE_PRESETS,\n dateFormatForLocale\n} from \"./date-picker-constants.js\";\n\nexport type { DateRange };\n\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n if (minDate && isBefore(date, startOfDay(minDate))) return true;\n if (maxDate && isAfter(date, startOfDay(maxDate))) return true;\n return false;\n}\n\n/**\n * Chặn ngày ngoài khoảng cho phép — trả về MẢNG matcher, không phải một object.\n *\n * Cạm bẫy react-day-picker: `{ before, after }` trong CÙNG một object là `DateInterval`, nghĩa là\n * \"các ngày NẰM GIỮA\" — ngược hẳn ý định. Tách thành hai matcher riêng mới ra \"trước min HOẶC sau max\".\n */\nfunction buildDisabledMatchers(minDate?: Date, maxDate?: Date): Matcher[] | undefined {\n const matchers: Matcher[] = [];\n if (minDate) matchers.push({ before: startOfDay(minDate) });\n if (maxDate) matchers.push({ after: startOfDay(maxDate) });\n return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n value?: Date | null;\n onValueChange: (value: Date | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Ngày nhỏ nhất / lớn nhất được chọn */\n minDate?: Date;\n maxDate?: Date;\n /** Hiện nút xoá khi đã có giá trị (mặc định true) */\n clearable?: boolean;\n /** Cột preset bên trái lịch (mặc định ẩn) */\n presets?: DatePreset[] | false;\n /**\n * Ngôn ngữ của date-fns (mặc định `vi`) — quyết định tên thứ/tháng trong lịch VÀ thứ tự\n * ngày/tháng của ô nhập, qua `dateFormatForLocale()`.\n *\n * App đa ngôn ngữ phải truyền: để mặc định thì giao diện tiếng Anh vẫn hiện lịch tiếng Việt và\n * bắt người dùng gõ `dd/MM/yyyy` — hai lỗi không có gì báo đỏ.\n */\n locale?: Locale;\n /**\n * Ghi đè định dạng gõ/hiển thị. HIẾM khi cần: bỏ trống thì nó suy từ `locale`, và đó gần như\n * luôn là thứ đúng. Chỉ dùng khi một màn hình phải ghim cứng một định dạng bất kể ngôn ngữ.\n */\n dateFormat?: string;\n labels?: Partial<DatePickerLabels>;\n className?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\n/**\n * Ô chọn ngày: gõ tay `dd/MM/yyyy` HOẶC bấm lịch.\n *\n * Vì sao vẫn giữ ô gõ tay thay vì chỉ có nút mở lịch: người nhập liệu chuyên nghiệp (kế toán,\n * nhân sự) gõ nhanh hơn bấm nhiều lần, và ngày sinh năm 1975 mà bắt lật lịch 50 năm thì không\n * ai chịu nổi. Chuỗi gõ dở KHÔNG bị ghi đè trong lúc gõ; sai định dạng thì trả về giá trị cũ\n * khi rời ô, không âm thầm nuốt dữ liệu.\n */\nexport function DatePicker({\n value,\n onValueChange,\n placeholder,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n locale = vi,\n dateFormat,\n labels,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const pattern = dateFormat ?? dateFormatForLocale(locale);\n const label = { ...DEFAULT_DATE_PICKER_LABELS, ...labels };\n\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, pattern, { locale }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về) — VÀ khi đổi\n // ngôn ngữ giữa chừng: chuỗi đang hiện là `dd/MM/yyyy` mà ô nay chờ `MM/dd/yyyy` thì lần rời ô\n // kế tiếp sẽ đọc sai ngày.\n useEffect(() => {\n setText(value ? formatDate(value, pattern, { locale }) : \"\");\n }, [value, pattern, locale]);\n\n const commitText = (raw: string) => {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n onValueChange(null);\n return;\n }\n const parsed = parse(trimmed, pattern, new Date(), { locale });\n if (!isValid(parsed) || isOutOfRange(parsed, minDate, maxDate)) {\n // Không hợp lệ → khôi phục hiển thị theo giá trị đang có (không đổi dữ liệu).\n setText(value ? formatDate(value, pattern, { locale }) : \"\");\n return;\n }\n onValueChange(startOfDay(parsed));\n };\n\n const presetList = presets === false ? [] : presets;\n\n return (\n <div className={cn(\"relative\", className)}>\n <Input\n id={id}\n name={name}\n value={text}\n disabled={disabled}\n placeholder={placeholder ?? pattern}\n inputMode=\"numeric\"\n autoComplete=\"off\"\n onChange={(event) => setText(event.target.value)}\n onBlur={(event) => commitText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commitText(text);\n }\n }}\n className=\"pr-16\"\n {...aria}\n />\n\n {/* Nút `size-6`: glyph 16px nằm giữa ô 24px nên nó thụt thêm 4px so với mép ô — cộng với\n `pr-2` của hàng là glyph rơi đúng mốc 12px của `Select`/`Combobox`. Ba loại control này\n hay đứng chung một cột biểu mẫu, lệch vài px là mép phải nhìn răng cưa.\n\n `border border-transparent`: hàng nằm ngoài `Input` nên đo từ mép BORDER, còn icon nằm\n trong ô thì đo từ mép CONTENT — mượn lại viền 1px là hai mốc trùng nhau.\n\n Icon lấy `text-muted-foreground`: `Button variant=\"ghost\"` mặc định là `text-foreground`,\n đậm hơn hẳn chevron của `Select` (vốn `text-muted-foreground`). Chúng là cùng một hạng\n trang trí của ô nhập thì phải cùng một sắc. */}\n {/* `pointer-events-none` ở hàng, `pointer-events-auto` ở từng nút: phần đệm của hàng phủ lên\n dải sát mép phải của ô nhập, để nguyên là bấm vào đó không đặt được con trỏ vào ô. */}\n <div className=\"pointer-events-none absolute inset-y-0 right-0 flex items-center border border-transparent pr-2\">\n {clearable && value && !disabled && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"pointer-events-auto size-6 text-muted-foreground hover:text-foreground\"\n onClick={() => onValueChange(null)}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{label.clear}</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"pointer-events-auto size-6 text-muted-foreground hover:text-foreground\"\n disabled={disabled}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{label.openCalendar}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"flex w-auto gap-2 p-2\">\n {presetList.length > 0 && (\n <div className=\"flex w-32 flex-col gap-1 border-r border-border pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={locale}\n autoFocus\n selected={value ?? undefined}\n defaultMonth={value ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(date) => {\n onValueChange(date ? startOfDay(date) : null);\n setOpen(false);\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n );\n}\n\nexport interface DateRangePickerProps {\n value?: DateRange | null;\n onValueChange: (value: DateRange | null) => void;\n placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /** Cột preset bên trái (mặc định DEFAULT_DATE_RANGE_PRESETS; `false` để tắt) */\n presets?: DateRangePreset[] | false;\n /** Số tháng hiện cùng lúc (mặc định 2 trên màn rộng) */\n numberOfMonths?: number;\n /** Ngôn ngữ date-fns (mặc định `vi`) — xem `DatePickerProps.locale`. */\n locale?: Locale;\n /** Ghi đè định dạng ngày; bỏ trống thì suy từ `locale`. */\n dateFormat?: string;\n labels?: Partial<DatePickerLabels>;\n className?: string;\n id?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n}\n\nfunction formatRange(range: DateRange | null | undefined, pattern: string, locale: Locale): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, pattern, { locale });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, pattern, { locale })}`;\n}\n\n/**\n * Ô chọn khoảng ngày cho báo cáo và bộ lọc danh sách: cột preset + lịch 2 tháng.\n *\n * Preset đứng trước lịch vì 9/10 lần người dùng muốn \"7 ngày qua\" chứ không phải một khoảng\n * tuỳ ý — bắt họ bấm hai lần trên lịch mỗi lần xem báo cáo là thuế lặp lại vô ích.\n */\nexport function DateRangePicker({\n value,\n onValueChange,\n placeholder = \"Chọn khoảng ngày\",\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n locale = vi,\n dateFormat,\n labels,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const pattern = dateFormat ?? dateFormatForLocale(locale);\n const text = { ...DEFAULT_DATE_PICKER_LABELS, ...labels };\n\n const [open, setOpen] = useState(false);\n const label = formatRange(value, pattern, locale);\n const presetList = presets === false ? [] : presets;\n\n const showClear = clearable && Boolean(label) && !disabled;\n\n return (\n /* Nút xóa nằm NGOÀI trigger (anh em), không lồng trong <button> — vừa hợp lệ về HTML vừa\n đến được bằng phím Tab. */\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n id={id}\n variant=\"outline\"\n disabled={disabled}\n className={cn(\n // `px-3` chứ không phải `px-4` của `Button`: đây là một Ô NHẬP nằm cùng cột với\n // `Input`/`Select`/`Combobox`, và cả ba đều thụt 12px. `pr-10` = 12 + 24 (nút xóa)\n // + 4, chỉ cần khi nút xóa có mặt.\n \"w-full justify-start gap-2 px-3 font-normal\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-10\",\n className\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{label ?? placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"flex w-auto flex-col gap-2 p-2 sm:flex-row\">\n {presetList.length > 0 && (\n <div className=\"flex flex-row flex-wrap gap-1 sm:w-36 sm:flex-col sm:border-r sm:border-border sm:pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={locale}\n autoFocus\n numberOfMonths={numberOfMonths}\n selected={value ?? undefined}\n defaultMonth={value?.from ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(range) => onValueChange(range ?? null)}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n // Cùng hình dạng với nút xóa của `DatePicker`: ô 24px ở `right-2`. Là `<Button>` thật nên\n // có viền focus và vùng bấm đạt 24px — bản `<button>` thô trước đây chỉ 18px.\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"absolute right-2 top-1/2 size-6 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n onClick={() => onValueChange(null)}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{text.clearRange}</span>\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":";AAsIM,cAkCI,YAlCJ;AApIN,SAAS,UAAU,YAAY,SAAS,UAAU,SAAsB,OAAO,kBAAkB;AACjG,SAAS,UAAU;AACnB,SAAS,cAAc,SAAS;AAChC,SAAS,WAAW,gBAAgB;AAEpC,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,aAAa;AACtB,SAAS,SAAS,gBAAgB,sBAAsB;AACxD;AAAA,EAIE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAIP,SAAS,aAAa,MAAY,SAAgB,SAAyB;AACzE,MAAI,WAAW,SAAS,MAAM,WAAW,OAAO,CAAC,EAAG,QAAO;AAC3D,MAAI,WAAW,QAAQ,MAAM,WAAW,OAAO,CAAC,EAAG,QAAO;AAC1D,SAAO;AACT;AAQA,SAAS,sBAAsB,SAAgB,SAAuC;AACpF,QAAM,WAAsB,CAAC;AAC7B,MAAI,QAAS,UAAS,KAAK,EAAE,QAAQ,WAAW,OAAO,EAAE,CAAC;AAC1D,MAAI,QAAS,UAAS,KAAK,EAAE,OAAO,WAAW,OAAO,EAAE,CAAC;AACzD,SAAO,SAAS,SAAS,IAAI,WAAW;AAC1C;AA6CO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,UAAU,cAAc,oBAAoB,MAAM;AACxD,QAAM,QAAQ,EAAE,GAAG,4BAA4B,GAAG,OAAO;AAEzD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAO,QAAQ,WAAW,OAAO,SAAS,EAAE,OAAO,CAAC,IAAI,EAAG;AAK5F,YAAU,MAAM;AACd,YAAQ,QAAQ,WAAW,OAAO,SAAS,EAAE,OAAO,CAAC,IAAI,EAAE;AAAA,EAC7D,GAAG,CAAC,OAAO,SAAS,MAAM,CAAC;AAE3B,QAAM,aAAa,CAAC,QAAgB;AAClC,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,YAAY,IAAI;AAClB,oBAAc,IAAI;AAClB;AAAA,IACF;AACA,UAAM,SAAS,MAAM,SAAS,SAAS,oBAAI,KAAK,GAAG,EAAE,OAAO,CAAC;AAC7D,QAAI,CAAC,QAAQ,MAAM,KAAK,aAAa,QAAQ,SAAS,OAAO,GAAG;AAE9D,cAAQ,QAAQ,WAAW,OAAO,SAAS,EAAE,OAAO,CAAC,IAAI,EAAE;AAC3D;AAAA,IACF;AACA,kBAAc,WAAW,MAAM,CAAC;AAAA,EAClC;AAEA,QAAM,aAAa,YAAY,QAAQ,CAAC,IAAI;AAE5C,SACE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA,aAAa,eAAe;AAAA,QAC5B,WAAU;AAAA,QACV,cAAa;AAAA,QACb,UAAU,CAAC,UAAU,QAAQ,MAAM,OAAO,KAAK;AAAA,QAC/C,QAAQ,CAAC,UAAU,WAAW,MAAM,OAAO,KAAK;AAAA,QAChD,WAAW,CAAC,UAAU;AACpB,cAAI,MAAM,QAAQ,SAAS;AACzB,kBAAM,eAAe;AACrB,uBAAW,IAAI;AAAA,UACjB;AAAA,QACF;AAAA,QACA,WAAU;AAAA,QACT,GAAG;AAAA;AAAA,IACN;AAAA,IAcA,qBAAC,SAAI,WAAU,mGACZ;AAAA,mBAAa,SAAS,CAAC,YACtB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS,MAAM,cAAc,IAAI;AAAA,UAEjC;AAAA,gCAAC,KAAE,eAAY,QAAO,WAAU,YAAW;AAAA,YAC3C,oBAAC,UAAK,WAAU,WAAW,gBAAM,OAAM;AAAA;AAAA;AAAA,MACzC;AAAA,MAEF,qBAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,4BAAC,kBAAe,SAAO,MACrB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV;AAAA,YAEA;AAAA,kCAAC,gBAAa,eAAY,QAAO,WAAU,UAAS;AAAA,cACpD,oBAAC,UAAK,WAAU,WAAW,gBAAM,cAAa;AAAA;AAAA;AAAA,QAChD,GACF;AAAA,QACA,qBAAC,kBAAe,OAAM,OAAM,WAAU,yBACnC;AAAA,qBAAW,SAAS,KACnB,oBAAC,SAAI,WAAU,wDACZ,qBAAW,IAAI,CAAC,WACf;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS,MAAM;AACb,8BAAc,OAAO,SAAS,CAAC;AAC/B,wBAAQ,KAAK;AAAA,cACf;AAAA,cAEC,iBAAO;AAAA;AAAA,YAVH,OAAO;AAAA,UAWd,CACD,GACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL;AAAA,cACA,WAAS;AAAA,cACT,UAAU,SAAS;AAAA,cACnB,cAAc,SAAS;AAAA,cACvB,UAAU,sBAAsB,SAAS,OAAO;AAAA,cAChD,UAAU,CAAC,SAAS;AAClB,8BAAc,OAAO,WAAW,IAAI,IAAI,IAAI;AAC5C,wBAAQ,KAAK;AAAA,cACf;AAAA;AAAA,UACF;AAAA,WACF;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;AA0BA,SAAS,YAAY,OAAqC,SAAiB,QAA+B;AACxG,MAAI,CAAC,OAAO,KAAM,QAAO;AACzB,QAAM,OAAO,WAAW,MAAM,MAAM,SAAS,EAAE,OAAO,CAAC;AACvD,MAAI,CAAC,MAAM,GAAI,QAAO;AACtB,SAAO,GAAG,IAAI,WAAM,WAAW,MAAM,IAAI,SAAS,EAAE,OAAO,CAAC,CAAC;AAC/D;AAQO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,UAAU,cAAc,oBAAoB,MAAM;AACxD,QAAM,OAAO,EAAE,GAAG,4BAA4B,GAAG,OAAO;AAExD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,QAAQ,YAAY,OAAO,SAAS,MAAM;AAChD,QAAM,aAAa,YAAY,QAAQ,CAAC,IAAI;AAE5C,QAAM,YAAY,aAAa,QAAQ,KAAK,KAAK,CAAC;AAElD;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAI,WAAU,mBACb;AAAA,2BAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,4BAAC,kBAAe,SAAO,MACrB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL;AAAA,YACA,SAAQ;AAAA,YACR;AAAA,YACA,WAAW;AAAA;AAAA;AAAA;AAAA,cAIT;AAAA,cACA,CAAC,SAAS;AAAA,cACV,aAAa;AAAA,cACb;AAAA,YACF;AAAA,YACC,GAAG;AAAA,YAEJ;AAAA,kCAAC,gBAAa,eAAY,QAAO,WAAU,yCAAwC;AAAA,cACnF,oBAAC,UAAK,WAAU,YAAY,mBAAS,aAAY;AAAA;AAAA;AAAA,QACnD,GACF;AAAA,QACA,qBAAC,kBAAe,OAAM,SAAQ,WAAU,8CACrC;AAAA,qBAAW,SAAS,KACnB,oBAAC,SAAI,WAAU,0FACZ,qBAAW,IAAI,CAAC,WACf;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS,MAAM;AACb,8BAAc,OAAO,SAAS,CAAC;AAC/B,wBAAQ,KAAK;AAAA,cACf;AAAA,cAEC,iBAAO;AAAA;AAAA,YAVH,OAAO;AAAA,UAWd,CACD,GACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL;AAAA,cACA,WAAS;AAAA,cACT;AAAA,cACA,UAAU,SAAS;AAAA,cACnB,cAAc,OAAO,QAAQ;AAAA,cAC7B,UAAU,sBAAsB,SAAS,OAAO;AAAA,cAChD,UAAU,CAAC,UAAU,cAAc,SAAS,IAAI;AAAA;AAAA,UAClD;AAAA,WACF;AAAA,SACF;AAAA,MAEC;AAAA;AAAA,MAGC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS,MAAM,cAAc,IAAI;AAAA,UAEjC;AAAA,gCAAC,KAAE,eAAY,QAAO,WAAU,YAAW;AAAA,YAC3C,oBAAC,UAAK,WAAU,WAAW,eAAK,YAAW;AAAA;AAAA;AAAA,MAC7C;AAAA,OAEJ;AAAA;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/date-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport { format as formatDate, isAfter, isBefore, isValid, type Locale, parse, startOfDay } from \"date-fns\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\nimport { useComitorLabels, useComitorLocale } from \"../../i18n/locale-provider.js\";\nimport type { DatePresetMessages } from \"../../i18n/namespaces/date-picker.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Calendar } from \"../ui/calendar.js\";\nimport { Input } from \"../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport {\n type DatePickerLabels,\n type DatePreset,\n type DateRangePreset,\n DEFAULT_DATE_PRESETS,\n DEFAULT_DATE_RANGE_PRESETS,\n dateFormatForLocale\n} from \"./date-picker-constants.js\";\n\nexport type { DateRange };\n\n/** `id` của preset mặc định → khoá nhãn trong namespace `datePicker.presets`. */\nconst PRESET_MESSAGE_KEY_BY_ID = {\n today: \"today\",\n yesterday: \"yesterday\",\n \"week-ago\": \"weekAgo\",\n \"last-7\": \"last7Days\",\n \"last-30\": \"last30Days\",\n \"this-month\": \"thisMonth\",\n \"last-month\": \"lastMonth\"\n} satisfies Record<string, keyof DatePresetMessages>;\n\n/**\n * CHÍNH các object preset của gói → khoá nhãn của chúng. Nhận ra bằng THAM CHIẾU, không bằng `id`:\n * · preset app tự dựng hiện đúng `label` app đưa, kể cả khi trùng `id` (\"today\") với mặc định;\n * · app truyền lại object của gói — `presets={DEFAULT_DATE_PRESETS}` (cách DUY NHẤT để\n * `DatePicker` có preset), hay một mảng `.filter()` từ đó — thì nhãn vẫn theo provider thay vì\n * kẹt ở `label` tiếng Anh của hằng.\n */\nconst DEFAULT_PRESET_MESSAGE_KEYS = new Map<DatePreset | DateRangePreset, keyof DatePresetMessages>(\n [...DEFAULT_DATE_PRESETS, ...DEFAULT_DATE_RANGE_PRESETS].flatMap((preset) => {\n const key = PRESET_MESSAGE_KEY_BY_ID[preset.id as keyof typeof PRESET_MESSAGE_KEY_BY_ID];\n return key ? [[preset, key] as const] : [];\n })\n);\n\n/** Nhãn hiện ra của một preset: preset của gói theo provider, preset của app nguyên văn. */\nfunction presetLabel(preset: DatePreset | DateRangePreset, messages: DatePresetMessages): string {\n const key = DEFAULT_PRESET_MESSAGE_KEYS.get(preset);\n return key ? messages[key] : preset.label;\n}\n\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n if (minDate && isBefore(date, startOfDay(minDate))) return true;\n if (maxDate && isAfter(date, startOfDay(maxDate))) return true;\n return false;\n}\n\n/**\n * Chặn ngày ngoài khoảng cho phép — trả về MẢNG matcher, không phải một object.\n *\n * Cạm bẫy react-day-picker: `{ before, after }` trong CÙNG một object là `DateInterval`, nghĩa là\n * \"các ngày NẰM GIỮA\" — ngược hẳn ý định. Tách thành hai matcher riêng mới ra \"trước min HOẶC sau max\".\n */\nfunction buildDisabledMatchers(minDate?: Date, maxDate?: Date): Matcher[] | undefined {\n const matchers: Matcher[] = [];\n if (minDate) matchers.push({ before: startOfDay(minDate) });\n if (maxDate) matchers.push({ after: startOfDay(maxDate) });\n return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n value?: Date | null;\n onValueChange: (value: Date | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Ngày nhỏ nhất / lớn nhất được chọn */\n minDate?: Date;\n maxDate?: Date;\n /** Hiện nút xoá khi đã có giá trị (mặc định true) */\n clearable?: boolean;\n /**\n * Cột preset bên trái lịch (mặc định ẩn). `DEFAULT_DATE_PRESETS` hiện nhãn theo\n * `ComitorLocaleProvider`; preset app tự dựng hiện nguyên `label`.\n */\n presets?: DatePreset[] | false;\n /**\n * Ngôn ngữ của date-fns — quyết định tên thứ/tháng trong lịch VÀ thứ tự ngày/tháng của ô nhập\n * (gõ, hiển thị lẫn placeholder), qua `dateFormatForLocale()`.\n *\n * Không truyền thì theo `ComitorLocaleProvider` (`vi` → `dd/MM/yyyy`, còn lại `enUS` →\n * `MM/dd/yyyy`). Trước `3.0.0` mặc định cứng là `vi`, nên app tiếng Anh phải nhớ truyền.\n */\n locale?: Locale;\n /**\n * Ghi đè định dạng gõ/hiển thị. HIẾM khi cần: bỏ trống thì nó suy từ `locale`, và đó gần như\n * luôn là thứ đúng. Chỉ dùng khi một màn hình phải ghim cứng một định dạng bất kể ngôn ngữ.\n */\n dateFormat?: string;\n /** Đè nhãn `sr-only` — không truyền thì theo namespace `datePicker` của `ComitorLocaleProvider` */\n labels?: Partial<DatePickerLabels>;\n className?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\n/**\n * Ô chọn ngày: gõ tay theo định dạng của `locale` (`dd/MM/yyyy`, `MM/dd/yyyy`…) HOẶC bấm lịch.\n *\n * Vì sao vẫn giữ ô gõ tay thay vì chỉ có nút mở lịch: người nhập liệu chuyên nghiệp (kế toán,\n * nhân sự) gõ nhanh hơn bấm nhiều lần, và ngày sinh năm 1975 mà bắt lật lịch 50 năm thì không\n * ai chịu nổi. Chuỗi gõ dở KHÔNG bị ghi đè trong lúc gõ; sai định dạng thì trả về giá trị cũ\n * khi rời ô, không âm thầm nuốt dữ liệu.\n */\nexport function DatePicker({\n value,\n onValueChange,\n placeholder,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n locale: localeProp,\n dateFormat,\n labels,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const { dateFnsLocale } = useComitorLocale();\n const locale = localeProp ?? dateFnsLocale;\n const pattern = dateFormat ?? dateFormatForLocale(locale);\n const label = useComitorLabels(\"datePicker\", labels);\n\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, pattern, { locale }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về) — VÀ khi đổi\n // ngôn ngữ giữa chừng: chuỗi đang hiện là `dd/MM/yyyy` mà ô nay chờ `MM/dd/yyyy` thì lần rời ô\n // kế tiếp sẽ đọc sai ngày.\n useEffect(() => {\n setText(value ? formatDate(value, pattern, { locale }) : \"\");\n }, [value, pattern, locale]);\n\n const commitText = (raw: string) => {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n onValueChange(null);\n return;\n }\n const parsed = parse(trimmed, pattern, new Date(), { locale });\n if (!isValid(parsed) || isOutOfRange(parsed, minDate, maxDate)) {\n // Không hợp lệ → khôi phục hiển thị theo giá trị đang có (không đổi dữ liệu).\n setText(value ? formatDate(value, pattern, { locale }) : \"\");\n return;\n }\n onValueChange(startOfDay(parsed));\n };\n\n const presetList = presets === false ? [] : presets;\n\n return (\n <div className={cn(\"relative\", className)}>\n <Input\n id={id}\n name={name}\n value={text}\n disabled={disabled}\n placeholder={placeholder ?? pattern}\n inputMode=\"numeric\"\n autoComplete=\"off\"\n onChange={(event) => setText(event.target.value)}\n onBlur={(event) => commitText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commitText(text);\n }\n }}\n className=\"pr-16\"\n {...aria}\n />\n\n {/* Nút `size-6`: glyph 16px nằm giữa ô 24px nên nó thụt thêm 4px so với mép ô — cộng với\n `pr-2` của hàng là glyph rơi đúng mốc 12px của `Select`/`Combobox`. Ba loại control này\n hay đứng chung một cột biểu mẫu, lệch vài px là mép phải nhìn răng cưa.\n\n `border border-transparent`: hàng nằm ngoài `Input` nên đo từ mép BORDER, còn icon nằm\n trong ô thì đo từ mép CONTENT — mượn lại viền 1px là hai mốc trùng nhau.\n\n Icon lấy `text-muted-foreground`: `Button variant=\"ghost\"` mặc định là `text-foreground`,\n đậm hơn hẳn chevron của `Select` (vốn `text-muted-foreground`). Chúng là cùng một hạng\n trang trí của ô nhập thì phải cùng một sắc. */}\n {/* `pointer-events-none` ở hàng, `pointer-events-auto` ở từng nút: phần đệm của hàng phủ lên\n dải sát mép phải của ô nhập, để nguyên là bấm vào đó không đặt được con trỏ vào ô. */}\n <div className=\"pointer-events-none absolute inset-y-0 right-0 flex items-center border border-transparent pr-2\">\n {clearable && value && !disabled && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"pointer-events-auto size-6 text-muted-foreground hover:text-foreground\"\n onClick={() => onValueChange(null)}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{label.clear}</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"pointer-events-auto size-6 text-muted-foreground hover:text-foreground\"\n disabled={disabled}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{label.openCalendar}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"flex w-auto gap-2 p-2\">\n {presetList.length > 0 && (\n <div className=\"flex w-32 flex-col gap-1 border-r border-border pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {presetLabel(preset, label.presets)}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={locale}\n autoFocus\n selected={value ?? undefined}\n defaultMonth={value ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(date) => {\n onValueChange(date ? startOfDay(date) : null);\n setOpen(false);\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n );\n}\n\nexport interface DateRangePickerProps {\n value?: DateRange | null;\n onValueChange: (value: DateRange | null) => void;\n /** Chữ khi chưa chọn — không truyền thì theo khoá `rangePlaceholder` của namespace `datePicker` */\n placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /**\n * Cột preset bên trái (mặc định `DEFAULT_DATE_RANGE_PRESETS`, nhãn theo `ComitorLocaleProvider`;\n * `false` để tắt). Preset app tự dựng hiện nguyên `label`.\n */\n presets?: DateRangePreset[] | false;\n /** Số tháng hiện cùng lúc (mặc định 2 trên màn rộng) */\n numberOfMonths?: number;\n /** Ngôn ngữ date-fns (mặc định theo `ComitorLocaleProvider`) — xem `DatePickerProps.locale`. */\n locale?: Locale;\n /** Ghi đè định dạng ngày; bỏ trống thì suy từ `locale`. */\n dateFormat?: string;\n /** Đè nhãn `sr-only` — không truyền thì theo namespace `datePicker` của `ComitorLocaleProvider` */\n labels?: Partial<DatePickerLabels>;\n className?: string;\n id?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n}\n\nfunction formatRange(range: DateRange | null | undefined, pattern: string, locale: Locale): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, pattern, { locale });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, pattern, { locale })}`;\n}\n\n/**\n * Ô chọn khoảng ngày cho báo cáo và bộ lọc danh sách: cột preset + lịch 2 tháng.\n *\n * Preset đứng trước lịch vì 9/10 lần người dùng muốn \"7 ngày qua\" chứ không phải một khoảng\n * tuỳ ý — bắt họ bấm hai lần trên lịch mỗi lần xem báo cáo là thuế lặp lại vô ích.\n */\nexport function DateRangePicker({\n value,\n onValueChange,\n placeholder: placeholderProp,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n locale: localeProp,\n dateFormat,\n labels,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const { dateFnsLocale } = useComitorLocale();\n const locale = localeProp ?? dateFnsLocale;\n const pattern = dateFormat ?? dateFormatForLocale(locale);\n const text = useComitorLabels(\"datePicker\", labels);\n const placeholder = placeholderProp ?? text.rangePlaceholder;\n\n const [open, setOpen] = useState(false);\n const label = formatRange(value, pattern, locale);\n const presetList = presets === false ? [] : presets;\n\n const showClear = clearable && Boolean(label) && !disabled;\n\n return (\n /* Nút xóa nằm NGOÀI trigger (anh em), không lồng trong <button> — vừa hợp lệ về HTML vừa\n đến được bằng phím Tab. */\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n id={id}\n variant=\"outline\"\n disabled={disabled}\n className={cn(\n // `px-3` chứ không phải `px-4` của `Button`: đây là một Ô NHẬP nằm cùng cột với\n // `Input`/`Select`/`Combobox`, và cả ba đều thụt 12px. `pr-10` = 12 + 24 (nút xóa)\n // + 4, chỉ cần khi nút xóa có mặt.\n \"w-full justify-start gap-2 px-3 font-normal\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-10\",\n className\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{label ?? placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"flex w-auto flex-col gap-2 p-2 sm:flex-row\">\n {presetList.length > 0 && (\n <div className=\"flex flex-row flex-wrap gap-1 sm:w-36 sm:flex-col sm:border-r sm:border-border sm:pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {presetLabel(preset, text.presets)}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={locale}\n autoFocus\n numberOfMonths={numberOfMonths}\n selected={value ?? undefined}\n defaultMonth={value?.from ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(range) => onValueChange(range ?? null)}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n // Cùng hình dạng với nút xóa của `DatePicker`: ô 24px ở `right-2`. Là `<Button>` thật nên\n // có viền focus và vùng bấm đạt 24px — bản `<button>` thô trước đây chỉ 18px.\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"absolute right-2 top-1/2 size-6 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n onClick={() => onValueChange(null)}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{text.clearRange}</span>\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -19,11 +19,12 @@ interface FilterChipsProps<T extends string = string> {
19
19
  onValueChange: (value: T[] | T | null) => void;
20
20
  /** Cho chọn nhiều chip cùng lúc (mặc định false) */
21
21
  multiple?: boolean;
22
- /** Thêm chip "Tất cả" ở đầu để bỏ mọi lựa chọn */
22
+ /** Thêm chip "tất cả" ở đầu để bỏ mọi lựa chọn */
23
23
  showAll?: boolean;
24
+ /** Chữ của chip "tất cả" (khoá `allLabel` của namespace `filterChips`, mặc định tiếng Anh) */
24
25
  allLabel?: string;
25
26
  allCount?: number;
26
- /** Nhãn nhóm cho trình đọc màn hình */
27
+ /** Nhãn nhóm cho trình đọc màn hình (khoá `label` của namespace `filterChips`) */
27
28
  label?: string;
28
29
  className?: string;
29
30
  }
@@ -34,6 +35,6 @@ interface FilterChipsProps<T extends string = string> {
34
35
  * theo kiểu tab, nó bật/tắt điều kiện lọc. Đặt nhầm role sẽ khiến trình đọc màn hình hứa hẹn
35
36
  * một panel không tồn tại.
36
37
  */
37
- declare function FilterChips<T extends string = string>({ items, value, onValueChange, multiple, showAll, allLabel, allCount, label, className }: FilterChipsProps<T>): React.JSX.Element;
38
+ declare function FilterChips<T extends string = string>({ items, value, onValueChange, multiple, showAll, allLabel: allLabelProp, allCount, label: labelProp, className }: FilterChipsProps<T>): React.JSX.Element;
38
39
 
39
40
  export { type FilterChipItem, FilterChips, type FilterChipsProps };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useComitorMessages } from "../../i18n/locale-provider.js";
3
4
  import { cn } from "../../lib/cn.js";
4
5
  function toArray(value) {
5
6
  if (value == null) return [];
@@ -11,11 +12,14 @@ function FilterChips({
11
12
  onValueChange,
12
13
  multiple = false,
13
14
  showAll = false,
14
- allLabel = "T\u1EA5t c\u1EA3",
15
+ allLabel: allLabelProp,
15
16
  allCount,
16
- label = "B\u1ED9 l\u1ECDc nhanh",
17
+ label: labelProp,
17
18
  className
18
19
  }) {
20
+ const t = useComitorMessages("filterChips");
21
+ const allLabel = allLabelProp ?? t.allLabel;
22
+ const label = labelProp ?? t.label;
19
23
  const selected = toArray(value);
20
24
  const noneSelected = selected.length === 0;
21
25
  const toggle = (item) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/filter-chips.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"../../lib/cn.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport type { StatusTone } from \"./status-config.js\";\n\nexport interface FilterChipItem<T extends string = string> {\n value: T;\n label: string;\n /** Số bản ghi thuộc nhóm này — hiện đậm trước nhãn */\n count?: number;\n icon?: IconComponent;\n /** Tone khi chip đang bật (mặc định tone app-accent) */\n tone?: StatusTone;\n disabled?: boolean;\n}\n\nexport interface FilterChipsProps<T extends string = string> {\n items: FilterChipItem<T>[];\n /** Giá trị đang chọn — mảng (đa chọn) hoặc một giá trị/null (đơn chọn) */\n value: T[] | T | null;\n onValueChange: (value: T[] | T | null) => void;\n /** Cho chọn nhiều chip cùng lúc (mặc định false) */\n multiple?: boolean;\n /** Thêm chip \"Tất cả\" ở đầu để bỏ mọi lựa chọn */\n showAll?: boolean;\n allLabel?: string;\n allCount?: number;\n /** Nhãn nhóm cho trình đọc màn hình */\n label?: string;\n className?: string;\n}\n\nfunction toArray<T extends string>(value: T[] | T | null): T[] {\n if (value == null) return [];\n return Array.isArray(value) ? value : [value];\n}\n\n/**\n * Dãy chip lọc bật/tắt kèm số đếm — kiểu Linear/Lark, dùng cho trạng thái, nhãn, mức ưu tiên.\n *\n * Dùng `aria-pressed` trên `<button>` thay vì `role=\"tab\"`: chip lọc không đổi vùng nội dung\n * theo kiểu tab, nó bật/tắt điều kiện lọc. Đặt nhầm role sẽ khiến trình đọc màn hình hứa hẹn\n * một panel không tồn tại.\n */\nexport function FilterChips<T extends string = string>({\n items,\n value,\n onValueChange,\n multiple = false,\n showAll = false,\n allLabel = \"Tất cả\",\n allCount,\n label = \"Bộ lọc nhanh\",\n className\n}: FilterChipsProps<T>) {\n const selected = toArray(value);\n const noneSelected = selected.length === 0;\n\n const toggle = (item: FilterChipItem<T>) => {\n if (item.disabled) return;\n if (!multiple) {\n onValueChange(selected.includes(item.value) ? null : item.value);\n return;\n }\n onValueChange(\n selected.includes(item.value) ? selected.filter((current) => current !== item.value) : [...selected, item.value]\n );\n };\n\n /*\n * Chip lọc là CONTROL (nút bật/tắt có `aria-pressed`), nên ranh giới của nó chịu ngưỡng 3:1 của\n * WCAG 1.4.11 — và ở đây VIỀN là thứ duy nhất khoanh vùng được: ruột `bg-card` trùng đúng nền\n * trang ở light (#FFFFFF ⇄ #FFFFFF) và chỉ lệch 1,10:1 ở dark.\n *\n * chip TẮT `border-border` (bậc TRANG TRÍ) 1,16:1 (L) · 1,10:1 (D) ✗\n * → `border-control-edge` 3,83:1 (L) · 4,64:1 (D) ✓\n * (vẫn 3,50:1 · 4,21:1 khi hover đổi ruột sang `bg-accent`)\n * chip BẬT + tone `border-transparent` để biên rơi về chính tint: 1,07–1,24:1 (L) ·\n * 1,11–1,56:1 (D) ✗ — tệ hơn: chip ĐANG BẬT trông mờ hơn chip đang tắt.\n * → `tone.borderColor` (bậc ink) 5,23–7,15:1 (L) · 5,70–12,67:1 (D) ✓\n * chip BẬT mặc định ruột `bg-app-accent` ĐẶC vẫn chỉ 1,86:1 với nền trắng (gold trên trắng —\n * đúng cái bẫy của hợp đồng §5), nên nền đặc KHÔNG tự khoanh được vùng ở light.\n * → thêm `border-app-accent-ink` 6,51:1 (L) · 12,67:1 (D) so với nền trang.\n * Ở dark `--app-accent-ink` = chính `--app-accent` nên viền trùng ruột, hình\n * không đổi — đúng ý, vì ở dark ruột đã tự nổi 12,67:1.\n *\n * `StatusTone.borderColor` là trường TUỲ CHỌN nên tone do app truyền vào có thể thiếu → lui về\n * `border-control-edge`, tuyệt đối không lui về `border-transparent`.\n *\n * `opacity-50` ở đây KHÔNG cần đạt ngưỡng: `disabled` là thuộc tính thật của <button>, chip tắt\n * rời khỏi tab order và không gọi handler (`pointer-events-none`) — WCAG loại trừ control vô\n * hiệu. Đây là chỗ khác hẳn ô app \"khoá\" của AppLauncher hay mục sidebar còn `tabIndex=0`.\n */\n const chipClass = (active: boolean, tone?: StatusTone) =>\n cn(\n \"inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-sm font-medium transition-colors duration-150 ease-out\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n active\n ? tone\n ? cn(tone.borderColor ?? \"border-control-edge\", tone.bgColor, tone.textColor)\n : // Tô ĐẶC chứ không phải nền vàng 15%: chữ gold trên nền gold nhạt chỉ đạt 1,75:1,\n // dưới chuẩn AA. Nền đặc + `app-accent-foreground` (mực) đọc được ở cả hai theme\n // (10,19:1 L · 12,67:1 D), còn viền lo phần ranh giới.\n \"border-app-accent-ink bg-app-accent text-app-accent-foreground\"\n : \"border-control-edge bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n );\n\n return (\n <div data-slot=\"filter-chips\" role=\"group\" aria-label={label} className={cn(\"flex flex-wrap gap-2\", className)}>\n {showAll && (\n <button\n type=\"button\"\n aria-pressed={noneSelected}\n onClick={() => onValueChange(multiple ? [] : null)}\n className={chipClass(noneSelected)}\n >\n {allCount !== undefined && <span className=\"font-semibold tabular-nums\">{allCount}</span>}\n {allLabel}\n </button>\n )}\n\n {items.map((item) => {\n const active = selected.includes(item.value);\n const Icon = item.icon;\n return (\n <button\n key={item.value}\n type=\"button\"\n aria-pressed={active}\n disabled={item.disabled}\n onClick={() => toggle(item)}\n className={chipClass(active, item.tone)}\n >\n {Icon && <Icon aria-hidden=\"true\" className=\"size-3.5\" />}\n {item.count !== undefined && <span className=\"font-semibold tabular-nums\">{item.count}</span>}\n {item.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AA+GQ,SAM6B,KAN7B;AA7GR,SAAS,UAAU;AA+BnB,SAAS,QAA0B,OAA4B;AAC7D,MAAI,SAAS,KAAM,QAAO,CAAC;AAC3B,SAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAC9C;AASO,SAAS,YAAuC;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAAwB;AACtB,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,eAAe,SAAS,WAAW;AAEzC,QAAM,SAAS,CAAC,SAA4B;AAC1C,QAAI,KAAK,SAAU;AACnB,QAAI,CAAC,UAAU;AACb,oBAAc,SAAS,SAAS,KAAK,KAAK,IAAI,OAAO,KAAK,KAAK;AAC/D;AAAA,IACF;AACA;AAAA,MACE,SAAS,SAAS,KAAK,KAAK,IAAI,SAAS,OAAO,CAAC,YAAY,YAAY,KAAK,KAAK,IAAI,CAAC,GAAG,UAAU,KAAK,KAAK;AAAA,IACjH;AAAA,EACF;AA0BA,QAAM,YAAY,CAAC,QAAiB,SAClC;AAAA,IACE;AAAA,IACA;AAAA,IACA,SACI,OACE,GAAG,KAAK,eAAe,uBAAuB,KAAK,SAAS,KAAK,SAAS;AAAA;AAAA;AAAA;AAAA,MAI1E;AAAA,QACF;AAAA,EACN;AAEF,SACE,qBAAC,SAAI,aAAU,gBAAe,MAAK,SAAQ,cAAY,OAAO,WAAW,GAAG,wBAAwB,SAAS,GAC1G;AAAA,eACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,gBAAc;AAAA,QACd,SAAS,MAAM,cAAc,WAAW,CAAC,IAAI,IAAI;AAAA,QACjD,WAAW,UAAU,YAAY;AAAA,QAEhC;AAAA,uBAAa,UAAa,oBAAC,UAAK,WAAU,8BAA8B,oBAAS;AAAA,UACjF;AAAA;AAAA;AAAA,IACH;AAAA,IAGD,MAAM,IAAI,CAAC,SAAS;AACnB,YAAM,SAAS,SAAS,SAAS,KAAK,KAAK;AAC3C,YAAM,OAAO,KAAK;AAClB,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,gBAAc;AAAA,UACd,UAAU,KAAK;AAAA,UACf,SAAS,MAAM,OAAO,IAAI;AAAA,UAC1B,WAAW,UAAU,QAAQ,KAAK,IAAI;AAAA,UAErC;AAAA,oBAAQ,oBAAC,QAAK,eAAY,QAAO,WAAU,YAAW;AAAA,YACtD,KAAK,UAAU,UAAa,oBAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA,YACrF,KAAK;AAAA;AAAA;AAAA,QATD,KAAK;AAAA,MAUZ;AAAA,IAEJ,CAAC;AAAA,KACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/filter-chips.tsx"],"sourcesContent":["\"use client\";\n\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport type { StatusTone } from \"./status-config.js\";\n\nexport interface FilterChipItem<T extends string = string> {\n value: T;\n label: string;\n /** Số bản ghi thuộc nhóm này — hiện đậm trước nhãn */\n count?: number;\n icon?: IconComponent;\n /** Tone khi chip đang bật (mặc định tone app-accent) */\n tone?: StatusTone;\n disabled?: boolean;\n}\n\nexport interface FilterChipsProps<T extends string = string> {\n items: FilterChipItem<T>[];\n /** Giá trị đang chọn — mảng (đa chọn) hoặc một giá trị/null (đơn chọn) */\n value: T[] | T | null;\n onValueChange: (value: T[] | T | null) => void;\n /** Cho chọn nhiều chip cùng lúc (mặc định false) */\n multiple?: boolean;\n /** Thêm chip \"tất cả\" ở đầu để bỏ mọi lựa chọn */\n showAll?: boolean;\n /** Chữ của chip \"tất cả\" (khoá `allLabel` của namespace `filterChips`, mặc định tiếng Anh) */\n allLabel?: string;\n allCount?: number;\n /** Nhãn nhóm cho trình đọc màn hình (khoá `label` của namespace `filterChips`) */\n label?: string;\n className?: string;\n}\n\nfunction toArray<T extends string>(value: T[] | T | null): T[] {\n if (value == null) return [];\n return Array.isArray(value) ? value : [value];\n}\n\n/**\n * Dãy chip lọc bật/tắt kèm số đếm — kiểu Linear/Lark, dùng cho trạng thái, nhãn, mức ưu tiên.\n *\n * Dùng `aria-pressed` trên `<button>` thay vì `role=\"tab\"`: chip lọc không đổi vùng nội dung\n * theo kiểu tab, nó bật/tắt điều kiện lọc. Đặt nhầm role sẽ khiến trình đọc màn hình hứa hẹn\n * một panel không tồn tại.\n */\nexport function FilterChips<T extends string = string>({\n items,\n value,\n onValueChange,\n multiple = false,\n showAll = false,\n allLabel: allLabelProp,\n allCount,\n label: labelProp,\n className\n}: FilterChipsProps<T>) {\n const t = useComitorMessages(\"filterChips\");\n const allLabel = allLabelProp ?? t.allLabel;\n const label = labelProp ?? t.label;\n const selected = toArray(value);\n const noneSelected = selected.length === 0;\n\n const toggle = (item: FilterChipItem<T>) => {\n if (item.disabled) return;\n if (!multiple) {\n onValueChange(selected.includes(item.value) ? null : item.value);\n return;\n }\n onValueChange(\n selected.includes(item.value) ? selected.filter((current) => current !== item.value) : [...selected, item.value]\n );\n };\n\n /*\n * Chip lọc là CONTROL (nút bật/tắt có `aria-pressed`), nên ranh giới của nó chịu ngưỡng 3:1 của\n * WCAG 1.4.11 — và ở đây VIỀN là thứ duy nhất khoanh vùng được: ruột `bg-card` trùng đúng nền\n * trang ở light (#FFFFFF ⇄ #FFFFFF) và chỉ lệch 1,10:1 ở dark.\n *\n * chip TẮT `border-border` (bậc TRANG TRÍ) 1,16:1 (L) · 1,10:1 (D) ✗\n * → `border-control-edge` 3,83:1 (L) · 4,64:1 (D) ✓\n * (vẫn 3,50:1 · 4,21:1 khi hover đổi ruột sang `bg-accent`)\n * chip BẬT + tone `border-transparent` để biên rơi về chính tint: 1,07–1,24:1 (L) ·\n * 1,11–1,56:1 (D) ✗ — tệ hơn: chip ĐANG BẬT trông mờ hơn chip đang tắt.\n * → `tone.borderColor` (bậc ink) 5,23–7,15:1 (L) · 5,70–12,67:1 (D) ✓\n * chip BẬT mặc định ruột `bg-app-accent` ĐẶC vẫn chỉ 1,86:1 với nền trắng (gold trên trắng —\n * đúng cái bẫy của hợp đồng §5), nên nền đặc KHÔNG tự khoanh được vùng ở light.\n * → thêm `border-app-accent-ink` 6,51:1 (L) · 12,67:1 (D) so với nền trang.\n * Ở dark `--app-accent-ink` = chính `--app-accent` nên viền trùng ruột, hình\n * không đổi — đúng ý, vì ở dark ruột đã tự nổi 12,67:1.\n *\n * `StatusTone.borderColor` là trường TUỲ CHỌN nên tone do app truyền vào có thể thiếu → lui về\n * `border-control-edge`, tuyệt đối không lui về `border-transparent`.\n *\n * `opacity-50` ở đây KHÔNG cần đạt ngưỡng: `disabled` là thuộc tính thật của <button>, chip tắt\n * rời khỏi tab order và không gọi handler (`pointer-events-none`) — WCAG loại trừ control vô\n * hiệu. Đây là chỗ khác hẳn ô app \"khoá\" của AppLauncher hay mục sidebar còn `tabIndex=0`.\n */\n const chipClass = (active: boolean, tone?: StatusTone) =>\n cn(\n \"inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-sm font-medium transition-colors duration-150 ease-out\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n active\n ? tone\n ? cn(tone.borderColor ?? \"border-control-edge\", tone.bgColor, tone.textColor)\n : // Tô ĐẶC chứ không phải nền vàng 15%: chữ gold trên nền gold nhạt chỉ đạt 1,75:1,\n // dưới chuẩn AA. Nền đặc + `app-accent-foreground` (mực) đọc được ở cả hai theme\n // (10,19:1 L · 12,67:1 D), còn viền lo phần ranh giới.\n \"border-app-accent-ink bg-app-accent text-app-accent-foreground\"\n : \"border-control-edge bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n );\n\n return (\n <div data-slot=\"filter-chips\" role=\"group\" aria-label={label} className={cn(\"flex flex-wrap gap-2\", className)}>\n {showAll && (\n <button\n type=\"button\"\n aria-pressed={noneSelected}\n onClick={() => onValueChange(multiple ? [] : null)}\n className={chipClass(noneSelected)}\n >\n {allCount !== undefined && <span className=\"font-semibold tabular-nums\">{allCount}</span>}\n {allLabel}\n </button>\n )}\n\n {items.map((item) => {\n const active = selected.includes(item.value);\n const Icon = item.icon;\n return (\n <button\n key={item.value}\n type=\"button\"\n aria-pressed={active}\n disabled={item.disabled}\n onClick={() => toggle(item)}\n className={chipClass(active, item.tone)}\n >\n {Icon && <Icon aria-hidden=\"true\" className=\"size-3.5\" />}\n {item.count !== undefined && <span className=\"font-semibold tabular-nums\">{item.count}</span>}\n {item.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AAoHQ,SAM6B,KAN7B;AAlHR,SAAS,0BAA0B;AACnC,SAAS,UAAU;AAgCnB,SAAS,QAA0B,OAA4B;AAC7D,MAAI,SAAS,KAAM,QAAO,CAAC;AAC3B,SAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAC9C;AASO,SAAS,YAAuC;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA,OAAO;AAAA,EACP;AACF,GAAwB;AACtB,QAAM,IAAI,mBAAmB,aAAa;AAC1C,QAAM,WAAW,gBAAgB,EAAE;AACnC,QAAM,QAAQ,aAAa,EAAE;AAC7B,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,eAAe,SAAS,WAAW;AAEzC,QAAM,SAAS,CAAC,SAA4B;AAC1C,QAAI,KAAK,SAAU;AACnB,QAAI,CAAC,UAAU;AACb,oBAAc,SAAS,SAAS,KAAK,KAAK,IAAI,OAAO,KAAK,KAAK;AAC/D;AAAA,IACF;AACA;AAAA,MACE,SAAS,SAAS,KAAK,KAAK,IAAI,SAAS,OAAO,CAAC,YAAY,YAAY,KAAK,KAAK,IAAI,CAAC,GAAG,UAAU,KAAK,KAAK;AAAA,IACjH;AAAA,EACF;AA0BA,QAAM,YAAY,CAAC,QAAiB,SAClC;AAAA,IACE;AAAA,IACA;AAAA,IACA,SACI,OACE,GAAG,KAAK,eAAe,uBAAuB,KAAK,SAAS,KAAK,SAAS;AAAA;AAAA;AAAA;AAAA,MAI1E;AAAA,QACF;AAAA,EACN;AAEF,SACE,qBAAC,SAAI,aAAU,gBAAe,MAAK,SAAQ,cAAY,OAAO,WAAW,GAAG,wBAAwB,SAAS,GAC1G;AAAA,eACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,gBAAc;AAAA,QACd,SAAS,MAAM,cAAc,WAAW,CAAC,IAAI,IAAI;AAAA,QACjD,WAAW,UAAU,YAAY;AAAA,QAEhC;AAAA,uBAAa,UAAa,oBAAC,UAAK,WAAU,8BAA8B,oBAAS;AAAA,UACjF;AAAA;AAAA;AAAA,IACH;AAAA,IAGD,MAAM,IAAI,CAAC,SAAS;AACnB,YAAM,SAAS,SAAS,SAAS,KAAK,KAAK;AAC3C,YAAM,OAAO,KAAK;AAClB,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,gBAAc;AAAA,UACd,UAAU,KAAK;AAAA,UACf,SAAS,MAAM,OAAO,IAAI;AAAA,UAC1B,WAAW,UAAU,QAAQ,KAAK,IAAI;AAAA,UAErC;AAAA,oBAAQ,oBAAC,QAAK,eAAY,QAAO,WAAU,YAAW;AAAA,YACtD,KAAK,UAAU,UAAa,oBAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA,YACrF,KAAK;AAAA;AAAA;AAAA,QATD,KAAK;AAAA,MAUZ;AAAA,IAEJ,CAAC;AAAA,KACH;AAEJ;","names":[]}
@@ -8,6 +8,10 @@ interface InlineEditProps {
8
8
  * sửa khi lưu xong — thoát sớm sẽ nháy về giá trị cũ rồi mới nhảy sang giá trị mới.
9
9
  */
10
10
  onSave: (value: string) => void | Promise<void>;
11
+ /**
12
+ * Chữ hiện khi giá trị rỗng, kiêm placeholder của ô nhập (khoá `placeholder` của namespace
13
+ * `inlineEdit` — `ComitorLocaleProvider`, mặc định tiếng Anh).
14
+ */
11
15
  placeholder?: string;
12
16
  /** Ô nhập nhiều dòng (mô tả task) — Enter xuống dòng, Cmd/Ctrl+Enter mới lưu */
13
17
  multiline?: boolean;
@@ -17,9 +21,16 @@ interface InlineEditProps {
17
21
  required?: boolean;
18
22
  /** Rời ô là lưu (mặc định true, kiểu Linear) */
19
23
  saveOnBlur?: boolean;
20
- /** Hiện cặp nút ✓/✕ khi đang sửa (mặc định false — thao tác bằng phím) */
24
+ /**
25
+ * Hiện cặp nút ✓/✕ khi đang sửa (mặc định false — thao tác bằng phím). Tên sr-only của hai nút
26
+ * lấy khoá `save` / `cancel` của namespace `inlineEdit`; không có prop rời — đè qua `messages`
27
+ * của `ComitorLocaleProvider`.
28
+ */
21
29
  showActions?: boolean;
22
- /** Nhãn cho trình đọc màn hình, vd "Sửa tên công việc" */
30
+ /**
31
+ * Nhãn cho trình đọc màn hình, vd "Sửa tên công việc" (không truyền thì lấy khoá `ariaLabel` của
32
+ * namespace `inlineEdit`)
33
+ */
23
34
  ariaLabel?: string;
24
35
  onError?: (error: unknown) => void;
25
36
  className?: string;
@@ -33,6 +44,6 @@ interface InlineEditProps {
33
44
  * form khổng lồ và người dùng dễ sửa nhầm khi chỉ định bấm chọn. Hiển thị dạng chữ cho tới khi
34
45
  * người dùng thật sự muốn sửa là ranh giới an toàn.
35
46
  */
36
- declare function InlineEdit({ value, onSave, placeholder, multiline, maxLength, disabled, required, saveOnBlur, showActions, ariaLabel, onError, className, displayClassName }: InlineEditProps): React.JSX.Element;
47
+ declare function InlineEdit({ value, onSave, placeholder: placeholderProp, multiline, maxLength, disabled, required, saveOnBlur, showActions, ariaLabel: ariaLabelProp, onError, className, displayClassName }: InlineEditProps): React.JSX.Element;
37
48
 
38
49
  export { InlineEdit, type InlineEditProps };
@@ -2,23 +2,27 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Check, X } from "lucide-react";
4
4
  import { useEffect, useRef, useState } from "react";
5
+ import { useComitorMessages } from "../../i18n/locale-provider.js";
5
6
  import { cn } from "../../lib/cn.js";
6
7
  import { Button } from "../ui/button.js";
7
8
  function InlineEdit({
8
9
  value,
9
10
  onSave,
10
- placeholder = "Nh\u1EA5n \u0111\u1EC3 nh\u1EADp\u2026",
11
+ placeholder: placeholderProp,
11
12
  multiline = false,
12
13
  maxLength,
13
14
  disabled = false,
14
15
  required = true,
15
16
  saveOnBlur = true,
16
17
  showActions = false,
17
- ariaLabel = "S\u1EEDa t\u1EA1i ch\u1ED7",
18
+ ariaLabel: ariaLabelProp,
18
19
  onError,
19
20
  className,
20
21
  displayClassName
21
22
  }) {
23
+ const t = useComitorMessages("inlineEdit");
24
+ const placeholder = placeholderProp ?? t.placeholder;
25
+ const ariaLabel = ariaLabelProp ?? t.ariaLabel;
22
26
  const [editing, setEditing] = useState(false);
23
27
  const [draft, setDraft] = useState(value);
24
28
  const [saving, setSaving] = useState(false);
@@ -135,7 +139,7 @@ function InlineEdit({
135
139
  },
136
140
  children: [
137
141
  /* @__PURE__ */ jsx(Check, { "aria-hidden": "true", className: "size-3.5" }),
138
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: "L\u01B0u" })
142
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t.save })
139
143
  ]
140
144
  }
141
145
  ),
@@ -152,7 +156,7 @@ function InlineEdit({
152
156
  },
153
157
  children: [
154
158
  /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" }),
155
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: "H\u1EE7y" })
159
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t.cancel })
156
160
  ]
157
161
  }
158
162
  )
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/inline-edit.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, X } from \"lucide-react\";\nimport { type KeyboardEvent, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\n\nexport interface InlineEditProps {\n /** Giá trị hiện tại (controlled từ phía dữ liệu) */\n value: string;\n /**\n * Lưu giá trị mới. Trả Promise thì component hiện trạng thái đang lưu và chỉ thoát chế độ\n * sửa khi lưu xong — thoát sớm sẽ nháy về giá trị cũ rồi mới nhảy sang giá trị mới.\n */\n onSave: (value: string) => void | Promise<void>;\n placeholder?: string;\n /** Ô nhập nhiều dòng (mô tả task) — Enter xuống dòng, Cmd/Ctrl+Enter mới lưu */\n multiline?: boolean;\n maxLength?: number;\n disabled?: boolean;\n /** Bắt buộc có nội dung: chuỗi rỗng bị coi là huỷ, không lưu (mặc định true) */\n required?: boolean;\n /** Rời ô là lưu (mặc định true, kiểu Linear) */\n saveOnBlur?: boolean;\n /** Hiện cặp nút ✓/✕ khi đang sửa (mặc định false — thao tác bằng phím) */\n showActions?: boolean;\n /** Nhãn cho trình đọc màn hình, vd \"Sửa tên công việc\" */\n ariaLabel?: string;\n onError?: (error: unknown) => void;\n className?: string;\n /** className riêng cho phần hiển thị (khi chưa sửa) — vd chỉnh cỡ chữ tiêu đề */\n displayClassName?: string;\n}\n\n/**\n * Sửa tại chỗ kiểu Linear: bấm vào chữ → thành ô nhập → Enter lưu, Esc huỷ.\n *\n * Vì sao không dùng `<input>` luôn cho gọn: ô nhập luôn hiện làm danh sách trông như một cái\n * form khổng lồ và người dùng dễ sửa nhầm khi chỉ định bấm chọn. Hiển thị dạng chữ cho tới khi\n * người dùng thật sự muốn sửa là ranh giới an toàn.\n */\nexport function InlineEdit({\n value,\n onSave,\n placeholder = \"Nhấn để nhập…\",\n multiline = false,\n maxLength,\n disabled = false,\n required = true,\n saveOnBlur = true,\n showActions = false,\n ariaLabel = \"Sửa tại chỗ\",\n onError,\n className,\n displayClassName\n}: InlineEditProps) {\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(value);\n const [saving, setSaving] = useState(false);\n const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);\n\n // Giá trị đổi từ ngoài (người khác sửa qua realtime) — chỉ đồng bộ khi không sửa dở.\n useEffect(() => {\n if (!editing) setDraft(value);\n }, [value, editing]);\n\n useEffect(() => {\n if (!editing) return;\n const element = inputRef.current;\n if (!element) return;\n element.focus();\n element.setSelectionRange(element.value.length, element.value.length);\n }, [editing]);\n\n const cancel = () => {\n setDraft(value);\n setEditing(false);\n };\n\n const commit = async () => {\n const next = draft.trim();\n if (next === value.trim() || (required && next === \"\")) {\n cancel();\n return;\n }\n\n try {\n setSaving(true);\n await onSave(next);\n setSaving(false);\n setEditing(false);\n } catch (error) {\n setSaving(false);\n // Giữ nguyên chế độ sửa: người dùng không mất chữ vừa gõ khi mạng lỗi.\n if (onError) onError(error);\n else console.error(\"[InlineEdit] onSave thất bại:\", error);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n cancel();\n return;\n }\n if (event.key === \"Enter\" && (!multiline || event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n void commit();\n }\n };\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n disabled={disabled}\n aria-label={ariaLabel}\n onClick={() => setEditing(true)}\n className={cn(\n \"-mx-1 w-full rounded-md px-1 py-0.5 text-left transition-colors duration-150 ease-out\",\n \"hover:bg-muted disabled:pointer-events-none disabled:opacity-60\",\n !value && \"text-muted-foreground\",\n className,\n displayClassName\n )}\n >\n <span className={cn(multiline ? \"whitespace-pre-wrap\" : \"block truncate\")}>{value || placeholder}</span>\n </button>\n );\n }\n\n const sharedProps = {\n value: draft,\n maxLength,\n placeholder,\n disabled: saving,\n \"aria-label\": ariaLabel,\n onKeyDown: handleKeyDown,\n onBlur: saveOnBlur ? () => void commit() : undefined,\n className: cn(\n \"w-full rounded-md border border-input bg-background px-1.5 py-0.5 text-sm text-foreground\",\n \"focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\",\n \"disabled:opacity-60\",\n className\n )\n };\n\n return (\n <div className=\"flex items-start gap-1\">\n {multiline ? (\n <textarea\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n rows={3}\n onChange={(event) => setDraft(event.target.value)}\n />\n ) : (\n <input\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n type=\"text\"\n onChange={(event) => setDraft(event.target.value)}\n />\n )}\n\n {showActions && (\n <div className=\"flex shrink-0 items-center gap-0.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={saving}\n // onMouseDown: chạy TRƯỚC blur của ô nhập, nếu để onClick thì blur đã huỷ/lưu mất rồi.\n onMouseDown={(event) => {\n event.preventDefault();\n void commit();\n }}\n >\n <Check aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Lưu</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={saving}\n onMouseDown={(event) => {\n event.preventDefault();\n cancel();\n }}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Hủy</span>\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/inline-edit.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, X } from \"lucide-react\";\nimport { type KeyboardEvent, useEffect, useRef, useState } from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\n\nexport interface InlineEditProps {\n /** Giá trị hiện tại (controlled từ phía dữ liệu) */\n value: string;\n /**\n * Lưu giá trị mới. Trả Promise thì component hiện trạng thái đang lưu và chỉ thoát chế độ\n * sửa khi lưu xong — thoát sớm sẽ nháy về giá trị cũ rồi mới nhảy sang giá trị mới.\n */\n onSave: (value: string) => void | Promise<void>;\n /**\n * Chữ hiện khi giá trị rỗng, kiêm placeholder của ô nhập (khoá `placeholder` của namespace\n * `inlineEdit` — `ComitorLocaleProvider`, mặc định tiếng Anh).\n */\n placeholder?: string;\n /** Ô nhập nhiều dòng (mô tả task) — Enter xuống dòng, Cmd/Ctrl+Enter mới lưu */\n multiline?: boolean;\n maxLength?: number;\n disabled?: boolean;\n /** Bắt buộc có nội dung: chuỗi rỗng bị coi là huỷ, không lưu (mặc định true) */\n required?: boolean;\n /** Rời ô là lưu (mặc định true, kiểu Linear) */\n saveOnBlur?: boolean;\n /**\n * Hiện cặp nút ✓/✕ khi đang sửa (mặc định false — thao tác bằng phím). Tên sr-only của hai nút\n * lấy khoá `save` / `cancel` của namespace `inlineEdit`; không có prop rời — đè qua `messages`\n * của `ComitorLocaleProvider`.\n */\n showActions?: boolean;\n /**\n * Nhãn cho trình đọc màn hình, vd \"Sửa tên công việc\" (không truyền thì lấy khoá `ariaLabel` của\n * namespace `inlineEdit`)\n */\n ariaLabel?: string;\n onError?: (error: unknown) => void;\n className?: string;\n /** className riêng cho phần hiển thị (khi chưa sửa) — vd chỉnh cỡ chữ tiêu đề */\n displayClassName?: string;\n}\n\n/**\n * Sửa tại chỗ kiểu Linear: bấm vào chữ → thành ô nhập → Enter lưu, Esc huỷ.\n *\n * Vì sao không dùng `<input>` luôn cho gọn: ô nhập luôn hiện làm danh sách trông như một cái\n * form khổng lồ và người dùng dễ sửa nhầm khi chỉ định bấm chọn. Hiển thị dạng chữ cho tới khi\n * người dùng thật sự muốn sửa là ranh giới an toàn.\n */\nexport function InlineEdit({\n value,\n onSave,\n placeholder: placeholderProp,\n multiline = false,\n maxLength,\n disabled = false,\n required = true,\n saveOnBlur = true,\n showActions = false,\n ariaLabel: ariaLabelProp,\n onError,\n className,\n displayClassName\n}: InlineEditProps) {\n const t = useComitorMessages(\"inlineEdit\");\n const placeholder = placeholderProp ?? t.placeholder;\n const ariaLabel = ariaLabelProp ?? t.ariaLabel;\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(value);\n const [saving, setSaving] = useState(false);\n const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);\n\n // Giá trị đổi từ ngoài (người khác sửa qua realtime) — chỉ đồng bộ khi không sửa dở.\n useEffect(() => {\n if (!editing) setDraft(value);\n }, [value, editing]);\n\n useEffect(() => {\n if (!editing) return;\n const element = inputRef.current;\n if (!element) return;\n element.focus();\n element.setSelectionRange(element.value.length, element.value.length);\n }, [editing]);\n\n const cancel = () => {\n setDraft(value);\n setEditing(false);\n };\n\n const commit = async () => {\n const next = draft.trim();\n if (next === value.trim() || (required && next === \"\")) {\n cancel();\n return;\n }\n\n try {\n setSaving(true);\n await onSave(next);\n setSaving(false);\n setEditing(false);\n } catch (error) {\n setSaving(false);\n // Giữ nguyên chế độ sửa: người dùng không mất chữ vừa gõ khi mạng lỗi.\n if (onError) onError(error);\n else console.error(\"[InlineEdit] onSave thất bại:\", error);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n cancel();\n return;\n }\n if (event.key === \"Enter\" && (!multiline || event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n void commit();\n }\n };\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n disabled={disabled}\n aria-label={ariaLabel}\n onClick={() => setEditing(true)}\n className={cn(\n \"-mx-1 w-full rounded-md px-1 py-0.5 text-left transition-colors duration-150 ease-out\",\n \"hover:bg-muted disabled:pointer-events-none disabled:opacity-60\",\n !value && \"text-muted-foreground\",\n className,\n displayClassName\n )}\n >\n <span className={cn(multiline ? \"whitespace-pre-wrap\" : \"block truncate\")}>{value || placeholder}</span>\n </button>\n );\n }\n\n const sharedProps = {\n value: draft,\n maxLength,\n placeholder,\n disabled: saving,\n \"aria-label\": ariaLabel,\n onKeyDown: handleKeyDown,\n onBlur: saveOnBlur ? () => void commit() : undefined,\n className: cn(\n \"w-full rounded-md border border-input bg-background px-1.5 py-0.5 text-sm text-foreground\",\n \"focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\",\n \"disabled:opacity-60\",\n className\n )\n };\n\n return (\n <div className=\"flex items-start gap-1\">\n {multiline ? (\n <textarea\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n rows={3}\n onChange={(event) => setDraft(event.target.value)}\n />\n ) : (\n <input\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n type=\"text\"\n onChange={(event) => setDraft(event.target.value)}\n />\n )}\n\n {showActions && (\n <div className=\"flex shrink-0 items-center gap-0.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={saving}\n // onMouseDown: chạy TRƯỚC blur của ô nhập, nếu để onClick thì blur đã huỷ/lưu mất rồi.\n onMouseDown={(event) => {\n event.preventDefault();\n void commit();\n }}\n >\n <Check aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{t.save}</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={saving}\n onMouseDown={(event) => {\n event.preventDefault();\n cancel();\n }}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{t.cancel}</span>\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -26,10 +26,10 @@ interface RelativeTimeProps extends Omit<ComponentProps<"time">, "dateTime" | "c
26
26
  */
27
27
  updateIntervalMs?: number;
28
28
  /**
29
- * Ngôn ngữ của date-fns (mặc định `vi`) — quyết định CẢ chuỗi tương đối lẫn mốc tuyệt đối trong
30
- * tooltip.
29
+ * Ngôn ngữ của date-fns — quyết định CẢ chuỗi tương đối lẫn mốc tuyệt đối trong tooltip.
31
30
  *
32
- * App đa ngôn ngữ phải truyền: để mặc định thì giao diện tiếng Anh vẫn hiện "3 phút trước".
31
+ * Không truyền thì theo `ComitorLocaleProvider` (`vi` → "3 phút trước", còn lại `enUS` → "3
32
+ * minutes ago"). Trước `3.0.0` mặc định cứng là `vi`, nên app tiếng Anh phải nhớ truyền.
33
33
  */
34
34
  locale?: Locale;
35
35
  /**
@@ -57,6 +57,6 @@ interface RelativeTimeProps extends Omit<ComponentProps<"time">, "dateTime" | "c
57
57
  * Hai lối đo dùng CHUNG một cách diễn đạt (`…Strict` của date-fns, cùng `locale`), nên đổi giữa
58
58
  * `now` và đồng hồ thật không làm chữ đổi giọng: vẫn "12 phút trước", không phải "khoảng 12 phút".
59
59
  */
60
- declare function RelativeTime({ value, now, addSuffix, updateIntervalMs, locale, titleFormat, fallback, className, ...props }: RelativeTimeProps): React.JSX.Element;
60
+ declare function RelativeTime({ value, now, addSuffix, updateIntervalMs, locale: localeProp, titleFormat, fallback, className, ...props }: RelativeTimeProps): React.JSX.Element;
61
61
 
62
62
  export { RelativeTime, type RelativeTimeProps };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { format as formatDate, formatDistanceStrict, formatDistanceToNowStrict, isValid } from "date-fns";
4
- import { vi } from "date-fns/locale";
5
4
  import { useEffect, useState } from "react";
5
+ import { useComitorLocale } from "../../i18n/locale-provider.js";
6
6
  function toDate(value) {
7
7
  return value instanceof Date ? value : new Date(value);
8
8
  }
@@ -11,12 +11,14 @@ function RelativeTime({
11
11
  now,
12
12
  addSuffix = true,
13
13
  updateIntervalMs = 6e4,
14
- locale = vi,
14
+ locale: localeProp,
15
15
  titleFormat = "Pp",
16
16
  fallback = "\u2014",
17
17
  className,
18
18
  ...props
19
19
  }) {
20
+ const { dateFnsLocale } = useComitorLocale();
21
+ const locale = localeProp ?? dateFnsLocale;
20
22
  const date = toDate(value);
21
23
  const valid = isValid(date);
22
24
  const tickIntervalMs = now ? 0 : updateIntervalMs;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/relative-time.tsx"],"sourcesContent":["\"use client\";\n\nimport { format as formatDate, formatDistanceStrict, formatDistanceToNowStrict, isValid, type Locale } from \"date-fns\";\nimport { vi } from \"date-fns/locale\";\nimport { type ComponentProps, useEffect, useState } from \"react\";\n\nexport interface RelativeTimeProps extends Omit<ComponentProps<\"time\">, \"dateTime\" | \"children\"> {\n /** Thời điểm cần hiển thị: Date, ISO string hoặc epoch ms */\n value: Date | string | number;\n /**\n * Mốc để ĐO TỪ ĐÓ. Bỏ trống = đồng hồ thật (hành vi mặc định, đúng cho bản ghi từ backend).\n *\n * Truyền `now` khi dữ liệu ĐỨNG YÊN — bản mẫu, ảnh chụp báo cáo, kết quả test: đồng hồ thật đo\n * trên mốc cố định thì kết quả trôi mỗi ngày, và mốc nào lỡ nằm ở tương lai in ra \"2 giờ nữa\",\n * một câu vô nghĩa mà TypeScript lẫn linter đều thấy hợp lệ.\n *\n * Có `now` thì component KHÔNG tự cập nhật nữa (xem `updateIntervalMs`) và kết quả trở nên tất\n * định — server và client cho ra cùng một chuỗi.\n */\n now?: Date;\n /** Thêm hậu tố \"trước\"/\"nữa\" (mặc định true) */\n addSuffix?: boolean;\n /**\n * Chu kỳ tự cập nhật (ms). 0 = không tự cập nhật. Mặc định 60_000.\n *\n * ⚠ Bị BỎ QUA khi có `now`: đo từ một mốc cố định thì chuỗi hiển thị không bao giờ đổi, nên hẹn\n * giờ chỉ để render lại y nguyên mỗi phút.\n */\n updateIntervalMs?: number;\n /**\n * Ngôn ngữ của date-fns (mặc định `vi`) — quyết định CẢ chuỗi tương đối lẫn mốc tuyệt đối trong\n * tooltip.\n *\n * App đa ngôn ngữ phải truyền: để mặc định thì giao diện tiếng Anh vẫn hiện \"3 phút trước\".\n */\n locale?: Locale;\n /**\n * Định dạng của tooltip `title`. Mặc định `\"Pp\"` — ngày + giờ THEO `locale`, nên nó tự đúng ở\n * mọi ngôn ngữ (vi: `29/08/2026 09:12`; en-US: `08/29/2026, 9:12 AM`).\n *\n * ⚠ Trước `0.9.0` mặc định là `\"HH:mm 'ngày' dd/MM/yyyy\"` — chuỗi đó nhét sẵn chữ \"ngày\" tiếng\n * Việt vào mọi ngôn ngữ. Cần lại đúng câu cũ thì truyền chính chuỗi đó.\n */\n titleFormat?: string;\n /** Nội dung thay thế khi `value` không hợp lệ */\n fallback?: string;\n}\n\nfunction toDate(value: Date | string | number): Date {\n return value instanceof Date ? value : new Date(value);\n}\n\n/**\n * Thời gian tương đối kiểu \"3 phút trước\", tự cập nhật theo chu kỳ.\n *\n * Dùng `formatDistanceToNowStrict` (không phải bản thường): bản thường làm tròn kiểu \"khoảng 1\n * tháng\" cho mọi thứ từ 26 ngày đến 45 ngày — trong danh sách công việc thì \"26 ngày trước\" và\n * \"45 ngày trước\" là hai câu chuyện khác hẳn nhau.\n *\n * `title` luôn giữ mốc thời gian tuyệt đối để người dùng rà chuột là biết chính xác.\n * Có `suppressHydrationWarning` vì đồng hồ server và client gần như luôn lệch vài giây — trừ khi\n * truyền `now`, lúc đó kết quả tất định và thuộc tính này chỉ còn là vô hại.\n *\n * Hai lối đo dùng CHUNG một cách diễn đạt (`…Strict` của date-fns, cùng `locale`), nên đổi giữa\n * `now` và đồng hồ thật không làm chữ đổi giọng: vẫn \"12 phút trước\", không phải \"khoảng 12 phút\".\n */\nexport function RelativeTime({\n value,\n now,\n addSuffix = true,\n updateIntervalMs = 60_000,\n locale = vi,\n titleFormat = \"Pp\",\n fallback = \"—\",\n className,\n ...props\n}: RelativeTimeProps) {\n const date = toDate(value);\n const valid = isValid(date);\n\n // Mốc cố định thì chuỗi không bao giờ đổi — tắt hẳn hẹn giờ, kể cả khi người gọi vẫn truyền\n // `updateIntervalMs`. Nếu không, component render lại mỗi phút để in ra đúng chữ cũ.\n const tickIntervalMs = now ? 0 : updateIntervalMs;\n\n const [, setTick] = useState(0);\n\n useEffect(() => {\n if (!valid || !tickIntervalMs) return;\n const timer = setInterval(() => setTick((current) => current + 1), tickIntervalMs);\n return () => clearInterval(timer);\n }, [valid, tickIntervalMs]);\n\n // Ngày hỏng (chuỗi rỗng, null từ API) → KHÔNG render <time>: thẻ này bắt buộc phải có\n // `datetime` hợp lệ, đưa \"Invalid Date\" vào là tạo dữ liệu sai cho trình đọc màn hình và bộ máy tìm kiếm.\n if (!valid) {\n return <span className={className}>{fallback}</span>;\n }\n\n return (\n <time\n dateTime={date.toISOString()}\n title={formatDate(date, titleFormat, { locale })}\n suppressHydrationWarning\n className={className}\n {...props}\n >\n {now\n ? formatDistanceStrict(date, now, { addSuffix, locale })\n : formatDistanceToNowStrict(date, { addSuffix, locale })}\n </time>\n );\n}\n"],"mappings":";AA+FW;AA7FX,SAAS,UAAU,YAAY,sBAAsB,2BAA2B,eAA4B;AAC5G,SAAS,UAAU;AACnB,SAA8B,WAAW,gBAAgB;AA4CzD,SAAS,OAAO,OAAqC;AACnD,SAAO,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AACvD;AAgBO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,SAAS;AAAA,EACT,cAAc;AAAA,EACd,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,OAAO,OAAO,KAAK;AACzB,QAAM,QAAQ,QAAQ,IAAI;AAI1B,QAAM,iBAAiB,MAAM,IAAI;AAEjC,QAAM,CAAC,EAAE,OAAO,IAAI,SAAS,CAAC;AAE9B,YAAU,MAAM;AACd,QAAI,CAAC,SAAS,CAAC,eAAgB;AAC/B,UAAM,QAAQ,YAAY,MAAM,QAAQ,CAAC,YAAY,UAAU,CAAC,GAAG,cAAc;AACjF,WAAO,MAAM,cAAc,KAAK;AAAA,EAClC,GAAG,CAAC,OAAO,cAAc,CAAC;AAI1B,MAAI,CAAC,OAAO;AACV,WAAO,oBAAC,UAAK,WAAuB,oBAAS;AAAA,EAC/C;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,UAAU,KAAK,YAAY;AAAA,MAC3B,OAAO,WAAW,MAAM,aAAa,EAAE,OAAO,CAAC;AAAA,MAC/C,0BAAwB;AAAA,MACxB;AAAA,MACC,GAAG;AAAA,MAEH,gBACG,qBAAqB,MAAM,KAAK,EAAE,WAAW,OAAO,CAAC,IACrD,0BAA0B,MAAM,EAAE,WAAW,OAAO,CAAC;AAAA;AAAA,EAC3D;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/relative-time.tsx"],"sourcesContent":["\"use client\";\n\nimport { format as formatDate, formatDistanceStrict, formatDistanceToNowStrict, isValid, type Locale } from \"date-fns\";\nimport { type ComponentProps, useEffect, useState } from \"react\";\nimport { useComitorLocale } from \"../../i18n/locale-provider.js\";\n\nexport interface RelativeTimeProps extends Omit<ComponentProps<\"time\">, \"dateTime\" | \"children\"> {\n /** Thời điểm cần hiển thị: Date, ISO string hoặc epoch ms */\n value: Date | string | number;\n /**\n * Mốc để ĐO TỪ ĐÓ. Bỏ trống = đồng hồ thật (hành vi mặc định, đúng cho bản ghi từ backend).\n *\n * Truyền `now` khi dữ liệu ĐỨNG YÊN — bản mẫu, ảnh chụp báo cáo, kết quả test: đồng hồ thật đo\n * trên mốc cố định thì kết quả trôi mỗi ngày, và mốc nào lỡ nằm ở tương lai in ra \"2 giờ nữa\",\n * một câu vô nghĩa mà TypeScript lẫn linter đều thấy hợp lệ.\n *\n * Có `now` thì component KHÔNG tự cập nhật nữa (xem `updateIntervalMs`) và kết quả trở nên tất\n * định — server và client cho ra cùng một chuỗi.\n */\n now?: Date;\n /** Thêm hậu tố \"trước\"/\"nữa\" (mặc định true) */\n addSuffix?: boolean;\n /**\n * Chu kỳ tự cập nhật (ms). 0 = không tự cập nhật. Mặc định 60_000.\n *\n * ⚠ Bị BỎ QUA khi có `now`: đo từ một mốc cố định thì chuỗi hiển thị không bao giờ đổi, nên hẹn\n * giờ chỉ để render lại y nguyên mỗi phút.\n */\n updateIntervalMs?: number;\n /**\n * Ngôn ngữ của date-fns — quyết định CẢ chuỗi tương đối lẫn mốc tuyệt đối trong tooltip.\n *\n * Không truyền thì theo `ComitorLocaleProvider` (`vi` → \"3 phút trước\", còn lại `enUS` → \"3\n * minutes ago\"). Trước `3.0.0` mặc định cứng là `vi`, nên app tiếng Anh phải nhớ truyền.\n */\n locale?: Locale;\n /**\n * Định dạng của tooltip `title`. Mặc định `\"Pp\"` — ngày + giờ THEO `locale`, nên nó tự đúng ở\n * mọi ngôn ngữ (vi: `29/08/2026 09:12`; en-US: `08/29/2026, 9:12 AM`).\n *\n * ⚠ Trước `0.9.0` mặc định là `\"HH:mm 'ngày' dd/MM/yyyy\"` — chuỗi đó nhét sẵn chữ \"ngày\" tiếng\n * Việt vào mọi ngôn ngữ. Cần lại đúng câu cũ thì truyền chính chuỗi đó.\n */\n titleFormat?: string;\n /** Nội dung thay thế khi `value` không hợp lệ */\n fallback?: string;\n}\n\nfunction toDate(value: Date | string | number): Date {\n return value instanceof Date ? value : new Date(value);\n}\n\n/**\n * Thời gian tương đối kiểu \"3 phút trước\", tự cập nhật theo chu kỳ.\n *\n * Dùng `formatDistanceToNowStrict` (không phải bản thường): bản thường làm tròn kiểu \"khoảng 1\n * tháng\" cho mọi thứ từ 26 ngày đến 45 ngày — trong danh sách công việc thì \"26 ngày trước\" và\n * \"45 ngày trước\" là hai câu chuyện khác hẳn nhau.\n *\n * `title` luôn giữ mốc thời gian tuyệt đối để người dùng rà chuột là biết chính xác.\n * Có `suppressHydrationWarning` vì đồng hồ server và client gần như luôn lệch vài giây — trừ khi\n * truyền `now`, lúc đó kết quả tất định và thuộc tính này chỉ còn là vô hại.\n *\n * Hai lối đo dùng CHUNG một cách diễn đạt (`…Strict` của date-fns, cùng `locale`), nên đổi giữa\n * `now` và đồng hồ thật không làm chữ đổi giọng: vẫn \"12 phút trước\", không phải \"khoảng 12 phút\".\n */\nexport function RelativeTime({\n value,\n now,\n addSuffix = true,\n updateIntervalMs = 60_000,\n locale: localeProp,\n titleFormat = \"Pp\",\n fallback = \"—\",\n className,\n ...props\n}: RelativeTimeProps) {\n const { dateFnsLocale } = useComitorLocale();\n const locale = localeProp ?? dateFnsLocale;\n const date = toDate(value);\n const valid = isValid(date);\n\n // Mốc cố định thì chuỗi không bao giờ đổi — tắt hẳn hẹn giờ, kể cả khi người gọi vẫn truyền\n // `updateIntervalMs`. Nếu không, component render lại mỗi phút để in ra đúng chữ cũ.\n const tickIntervalMs = now ? 0 : updateIntervalMs;\n\n const [, setTick] = useState(0);\n\n useEffect(() => {\n if (!valid || !tickIntervalMs) return;\n const timer = setInterval(() => setTick((current) => current + 1), tickIntervalMs);\n return () => clearInterval(timer);\n }, [valid, tickIntervalMs]);\n\n // Ngày hỏng (chuỗi rỗng, null từ API) → KHÔNG render <time>: thẻ này bắt buộc phải có\n // `datetime` hợp lệ, đưa \"Invalid Date\" vào là tạo dữ liệu sai cho trình đọc màn hình và bộ máy tìm kiếm.\n if (!valid) {\n return <span className={className}>{fallback}</span>;\n }\n\n return (\n <time\n dateTime={date.toISOString()}\n title={formatDate(date, titleFormat, { locale })}\n suppressHydrationWarning\n className={className}\n {...props}\n >\n {now\n ? formatDistanceStrict(date, now, { addSuffix, locale })\n : formatDistanceToNowStrict(date, { addSuffix, locale })}\n </time>\n );\n}\n"],"mappings":";AAiGW;AA/FX,SAAS,UAAU,YAAY,sBAAsB,2BAA2B,eAA4B;AAC5G,SAA8B,WAAW,gBAAgB;AACzD,SAAS,wBAAwB;AA4CjC,SAAS,OAAO,OAAqC;AACnD,SAAO,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AACvD;AAgBO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,EAAE,cAAc,IAAI,iBAAiB;AAC3C,QAAM,SAAS,cAAc;AAC7B,QAAM,OAAO,OAAO,KAAK;AACzB,QAAM,QAAQ,QAAQ,IAAI;AAI1B,QAAM,iBAAiB,MAAM,IAAI;AAEjC,QAAM,CAAC,EAAE,OAAO,IAAI,SAAS,CAAC;AAE9B,YAAU,MAAM;AACd,QAAI,CAAC,SAAS,CAAC,eAAgB;AAC/B,UAAM,QAAQ,YAAY,MAAM,QAAQ,CAAC,YAAY,UAAU,CAAC,GAAG,cAAc;AACjF,WAAO,MAAM,cAAc,KAAK;AAAA,EAClC,GAAG,CAAC,OAAO,cAAc,CAAC;AAI1B,MAAI,CAAC,OAAO;AACV,WAAO,oBAAC,UAAK,WAAuB,oBAAS;AAAA,EAC/C;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,UAAU,KAAK,YAAY;AAAA,MAC3B,OAAO,WAAW,MAAM,aAAa,EAAE,OAAO,CAAC;AAAA,MAC/C,0BAAwB;AAAA,MACxB;AAAA,MACC,GAAG;AAAA,MAEH,gBACG,qBAAqB,MAAM,KAAK,EAAE,WAAW,OAAO,CAAC,IACrD,0BAA0B,MAAM,EAAE,WAAW,OAAO,CAAC;AAAA;AAAA,EAC3D;AAEJ;","names":[]}
@@ -0,0 +1,9 @@
1
+ import * as React from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare function RouteTabsNav({ label, children }: {
5
+ label?: string;
6
+ children: ReactNode;
7
+ }): React.JSX.Element;
8
+
9
+ export { RouteTabsNav };
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useComitorMessages } from "../../i18n/locale-provider.js";
4
+ function RouteTabsNav({ label, children }) {
5
+ const t = useComitorMessages("routeTabs");
6
+ return /* @__PURE__ */ jsx("nav", { "aria-label": label ?? t.label, "data-slot": "route-tabs", children });
7
+ }
8
+ export {
9
+ RouteTabsNav
10
+ };
11
+ //# sourceMappingURL=route-tabs-nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/route-tabs-nav.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\n\n/*\n * Lá CLIENT nhỏ nhất có thể của `RouteTabs` — chỉ mỗi thẻ `<nav>` và tên của nó.\n *\n * Vì sao tách ra thay vì cho cả `route-tabs.tsx` thành `\"use client\"` (như `Spinner`):\n * `RouteTabs` nhận prop `LinkComponent` — một COMPONENT — mà Server Component truyền vào (vd\n * `LinkComponent={Link}` ở một layout server). Hàm/component không tuần tự hoá được qua ranh\n * giới RSC, nên file ấy mà thành client thì mọi trang server đang dùng nó gãy ngay. Chỉ cái tên\n * `aria-label` là cần đọc `ComitorLocaleProvider` (context, chỉ có ở client), nên chỉ phần đó\n * sang client: `route-tabs.tsx` vẫn tĩnh, dựng sẵn các link rồi đưa vào đây làm `children`\n * (children là phần tử React đã render — qua được ranh giới).\n *\n * File NỘI BỘ: barrel chỉ xuất `route-tabs.js`, không có dòng nào cho file này.\n */\nexport function RouteTabsNav({ label, children }: { label?: string; children: ReactNode }) {\n const t = useComitorMessages(\"routeTabs\");\n return (\n <nav aria-label={label ?? t.label} data-slot=\"route-tabs\">\n {children}\n </nav>\n );\n}\n"],"mappings":";AAqBI;AAlBJ,SAAS,0BAA0B;AAe5B,SAAS,aAAa,EAAE,OAAO,SAAS,GAA4C;AACzF,QAAM,IAAI,mBAAmB,WAAW;AACxC,SACE,oBAAC,SAAI,cAAY,SAAS,EAAE,OAAO,aAAU,cAC1C,UACH;AAEJ;","names":[]}
@@ -64,7 +64,10 @@ interface RouteTabsProps {
64
64
  variant?: TabsVariant;
65
65
  /** Component link của app. Bỏ trống → \`<a>\` thô, tải lại cả trang. */
66
66
  LinkComponent?: RouteTabsLinkComponent;
67
- /** Nhãn của vùng điều hướng, cho trình đọc màn hình. */
67
+ /**
68
+ * Nhãn của vùng điều hướng, cho trình đọc màn hình. Không truyền thì lấy khoá `label` của
69
+ * namespace `routeTabs` (`ComitorLocaleProvider`, mặc định tiếng Anh).
70
+ */
68
71
  label?: string;
69
72
  className?: string;
70
73
  /** Lớp thêm cho từng link. */
@@ -1,6 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../../lib/cn.js";
3
3
  import { TABS_TRIGGER_BASE, tabsListVariants, tabsTriggerVariants } from "../ui/tabs-variants.js";
4
+ import { RouteTabsNav } from "./route-tabs-nav.js";
4
5
  function DefaultLink({ href, className, children, ...rest }) {
5
6
  return /* @__PURE__ */ jsx("a", { href, className, ...rest, children });
6
7
  }
@@ -13,11 +14,11 @@ function RouteTabs({
13
14
  pathname,
14
15
  variant = "default",
15
16
  LinkComponent = DefaultLink,
16
- label = "M\u1EE5c c\u1EE7a trang",
17
+ label,
17
18
  className,
18
19
  itemClassName
19
20
  }) {
20
- return /* @__PURE__ */ jsx("nav", { "aria-label": label, "data-slot": "route-tabs", children: /* @__PURE__ */ jsx(
21
+ return /* @__PURE__ */ jsx(RouteTabsNav, { label, children: /* @__PURE__ */ jsx(
21
22
  "div",
22
23
  {
23
24
  "data-slot": "route-tabs-list",