@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 +1 @@
1
- {"version":3,"sources":["../../src/shell/display-axis-controls.tsx"],"sourcesContent":["\"use client\";\n\nimport type * as React from \"react\";\nimport { Switch } from \"../components/ui/switch.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { type DensityMode, FONT_SIZE_MODES, type FontSizeMode } from \"./display-axis-constants.js\";\nimport { useDensity, useFontSize } from \"./display-axis-provider.js\";\n\n/**\n * Hai control của trục MẬT ĐỘ và trục CỠ CHỮ.\n *\n * ─ VÌ SAO CẢ HAI ĐỀU CÓ `disabled={!mounted}` ─\n * Giá trị thật nằm ở localStorage, mà server không đọc được. Render lần đầu ở client PHẢI khớp\n * HTML server sinh ra, nếu không React báo lệch hydration — nên provider giữ giá trị mặc định cho\n * tới khi effect đầu chạy. Trong khoảnh khắc đó control hiện mặc định chứ không hiện lựa chọn\n * thật, và nếu người dùng kịp bấm thì họ ghi đè lên chính lựa chọn của mình. Khoá cho tới khi\n * `mounted` là cách `ContrastToggle` đã dùng.\n *\n * (DOM thật thì không nháy: script chống nháy của provider đã đặt thuộc tính lên `<html>` trước\n * lần sơn đầu tiên. Chỉ CONTROL là phải chờ.)\n */\n\nexport interface DensityToggleProps {\n label?: string;\n description?: React.ReactNode;\n className?: string;\n /**\n * Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.\n *\n * ⚠ Đây là callback THÔNG BÁO, không phải `onChange` của một component controlled. Giá trị CÓ\n * HIỆU LỰC vẫn nằm ở `localStorage` của gói và vẫn do provider quản; prop này chỉ cho app biết\n * \"vừa có thay đổi\" để nó ghi một MẶC ĐỊNH theo tài khoản ở nơi khác (Comitor.Account), hiện một\n * toast, hoặc ghi log.\n *\n * Vì sao KHÔNG làm controlled: trục hiển thị phải có hiệu lực trước lượt sơn đầu tiên, và điều đó\n * chỉ làm được bằng script chống nháy đọc `localStorage` — một giá trị đi từ server xuống prop\n * luôn tới SAU lượt sơn ấy. Xem `display-axis-provider.tsx`.\n */\n onDensityChange?: (mode: DensityMode) => void;\n}\n\n/**\n * Nút gạt \"bố cục gọn\".\n *\n * ⚠ Đây KHÔNG phải một trường của biểu mẫu cài đặt. Nó có hiệu lực NGAY và tự lưu, giống\n * `ThemeToggle`/`ContrastToggle` — đừng đưa nó vào luồng nháp/\"Lưu thay đổi\". Trộn hai kiểu thì\n * thanh \"Có thay đổi chưa lưu\" sáng lên cho một thay đổi đã có hiệu lực, và nút \"Hoàn tác\" trong\n * toast sẽ lùi bản nháp mà KHÔNG lùi được trục — giao diện tự mâu thuẫn với chính nó.\n */\nexport function DensityToggle({\n label = \"Bố cục gọn\",\n description = \"Giảm khoảng cách dọc giữa các dòng trong bảng, danh sách và menu.\",\n className,\n onDensityChange\n}: DensityToggleProps) {\n const { density, setDensity, mounted } = useDensity();\n\n return (\n <Switch\n className={className}\n checked={density === \"compact\"}\n onCheckedChange={(checked) => {\n const next: DensityMode = checked ? \"compact\" : \"comfortable\";\n setDensity(next);\n onDensityChange?.(next);\n }}\n disabled={!mounted}\n label={label}\n description={description}\n />\n );\n}\n\nexport interface FontSizeControlProps {\n /** Nhãn của cả nhóm, đọc lên cho trình đọc màn hình. */\n label?: string;\n /** Nhãn từng mức — THỨ TỰ theo `FONT_SIZE_MODES` (`sm`, `md`, `lg`). */\n optionLabels?: Record<FontSizeMode, string>;\n className?: string;\n /** Gọi SAU khi người dùng đổi — xem giải thích ở `DensityToggleProps.onDensityChange`. */\n onFontSizeChange?: (mode: FontSizeMode) => void;\n}\n\nconst DEFAULT_FONT_SIZE_LABELS: Record<FontSizeMode, string> = {\n sm: \"Nhỏ\",\n md: \"Vừa\",\n lg: \"Lớn\"\n};\n\n/**\n * Chọn cỡ chữ — ba mức, một hàng nút.\n *\n * `role=\"radiogroup\"` + `aria-checked` chứ không phải ba nút thường: đây là chọn MỘT trong ba, và\n * trình đọc màn hình cần nghe \"2 trên 3\" chứ không nghe ba nút rời rạc. Dùng `<button>` thật (bàn\n * phím tới được bằng Tab) thay vì roving focus của Radix ToggleGroup — nhóm chỉ có ba mục nên\n * roving focus không đáng cái giá thêm một phụ thuộc.\n *\n * Cỡ chữ của CHÍNH các nút này cố định `text-sm`: nếu chúng phóng theo lựa chọn thì bấm \"Lớn\" sẽ\n * làm cả hàng nút nhảy chỗ ngay dưới ngón tay.\n */\nexport function FontSizeControl({\n label = \"Cỡ chữ\",\n optionLabels = DEFAULT_FONT_SIZE_LABELS,\n className,\n onFontSizeChange\n}: FontSizeControlProps) {\n const { fontSize, setFontSize, mounted } = useFontSize();\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={label}\n className={cn(\"inline-flex items-center gap-1 rounded-md border border-input p-1 text-sm\", className)}\n >\n {FONT_SIZE_MODES.map((mode) => {\n const selected = fontSize === mode;\n return (\n <button\n key={mode}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n disabled={!mounted}\n onClick={() => {\n setFontSize(mode);\n onFontSizeChange?.(mode);\n }}\n className={cn(\n \"rounded-sm px-3 py-1 text-sm transition-colors duration-150 ease-out\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n selected\n ? \"bg-control-on text-control-check font-medium\"\n : \"text-muted-foreground hover:bg-accent hover:text-foreground\"\n )}\n >\n {optionLabels[mode]}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AA0DI;AAvDJ,SAAS,cAAc;AACvB,SAAS,UAAU;AACnB,SAA2B,uBAA0C;AACrE,SAAS,YAAY,mBAAmB;AA2CjC,SAAS,cAAc;AAAA,EAC5B,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,EAAE,SAAS,YAAY,QAAQ,IAAI,WAAW;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,YAAY;AAAA,MACrB,iBAAiB,CAAC,YAAY;AAC5B,cAAM,OAAoB,UAAU,YAAY;AAChD,mBAAW,IAAI;AACf,0BAAkB,IAAI;AAAA,MACxB;AAAA,MACA,UAAU,CAAC;AAAA,MACX;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;AAYA,MAAM,2BAAyD;AAAA,EAC7D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAaO,SAAS,gBAAgB;AAAA,EAC9B,QAAQ;AAAA,EACR,eAAe;AAAA,EACf;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,EAAE,UAAU,aAAa,QAAQ,IAAI,YAAY;AAEvD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,6EAA6E,SAAS;AAAA,MAEnG,0BAAgB,IAAI,CAAC,SAAS;AAC7B,cAAM,WAAW,aAAa;AAC9B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,UAAU,CAAC;AAAA,YACX,SAAS,MAAM;AACb,0BAAY,IAAI;AAChB,iCAAmB,IAAI;AAAA,YACzB;AAAA,YACA,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA,WACI,iDACA;AAAA,YACN;AAAA,YAEC,uBAAa,IAAI;AAAA;AAAA,UAlBb;AAAA,QAmBP;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/display-axis-controls.tsx"],"sourcesContent":["\"use client\";\n\nimport type * as React from \"react\";\nimport { Switch } from \"../components/ui/switch.js\";\nimport { useComitorMessages } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { type DensityMode, FONT_SIZE_MODES, type FontSizeMode } from \"./display-axis-constants.js\";\nimport { useDensity, useFontSize } from \"./display-axis-provider.js\";\n\n/**\n * Hai control của trục MẬT ĐỘ và trục CỠ CHỮ.\n *\n * ─ VÌ SAO CẢ HAI ĐỀU CÓ `disabled={!mounted}` ─\n * Giá trị thật nằm ở localStorage, mà server không đọc được. Render lần đầu ở client PHẢI khớp\n * HTML server sinh ra, nếu không React báo lệch hydration — nên provider giữ giá trị mặc định cho\n * tới khi effect đầu chạy. Trong khoảnh khắc đó control hiện mặc định chứ không hiện lựa chọn\n * thật, và nếu người dùng kịp bấm thì họ ghi đè lên chính lựa chọn của mình. Khoá cho tới khi\n * `mounted` là cách `ContrastToggle` đã dùng.\n *\n * (DOM thật thì không nháy: script chống nháy của provider đã đặt thuộc tính lên `<html>` trước\n * lần sơn đầu tiên. Chỉ CONTROL là phải chờ.)\n *\n * ─ CHỮ ─\n * Mọi chuỗi mặc định theo namespace `displayAxis` của `ComitorLocaleProvider` (không có provider\n * → tiếng Anh). Prop `label`/`description`/`optionLabels` truyền vào vẫn thắng.\n */\n\nexport interface DensityToggleProps {\n /** Nhãn nút gạt (khoá `densityLabel` của namespace `displayAxis`). */\n label?: string;\n /** Dòng mô tả dưới nhãn (khoá `densityDescription`). */\n description?: React.ReactNode;\n className?: string;\n /**\n * Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.\n *\n * ⚠ Đây là callback THÔNG BÁO, không phải `onChange` của một component controlled. Giá trị CÓ\n * HIỆU LỰC vẫn nằm ở `localStorage` của gói và vẫn do provider quản; prop này chỉ cho app biết\n * \"vừa có thay đổi\" để nó ghi một MẶC ĐỊNH theo tài khoản ở nơi khác (Comitor.Account), hiện một\n * toast, hoặc ghi log.\n *\n * Vì sao KHÔNG làm controlled: trục hiển thị phải có hiệu lực trước lượt sơn đầu tiên, và điều đó\n * chỉ làm được bằng script chống nháy đọc `localStorage` — một giá trị đi từ server xuống prop\n * luôn tới SAU lượt sơn ấy. Xem `display-axis-provider.tsx`.\n */\n onDensityChange?: (mode: DensityMode) => void;\n}\n\n/**\n * Nút gạt \"bố cục gọn\".\n *\n * ⚠ Đây KHÔNG phải một trường của biểu mẫu cài đặt. Nó có hiệu lực NGAY và tự lưu, giống\n * `ThemeToggle`/`ContrastToggle` — đừng đưa nó vào luồng nháp/\"Lưu thay đổi\". Trộn hai kiểu thì\n * thanh \"Có thay đổi chưa lưu\" sáng lên cho một thay đổi đã có hiệu lực, và nút \"Hoàn tác\" trong\n * toast sẽ lùi bản nháp mà KHÔNG lùi được trục — giao diện tự mâu thuẫn với chính nó.\n */\nexport function DensityToggle({\n label: labelProp,\n description: descriptionProp,\n className,\n onDensityChange\n}: DensityToggleProps) {\n const t = useComitorMessages(\"displayAxis\");\n const label = labelProp ?? t.densityLabel;\n const description = descriptionProp ?? t.densityDescription;\n const { density, setDensity, mounted } = useDensity();\n\n return (\n <Switch\n className={className}\n checked={density === \"compact\"}\n onCheckedChange={(checked) => {\n const next: DensityMode = checked ? \"compact\" : \"comfortable\";\n setDensity(next);\n onDensityChange?.(next);\n }}\n disabled={!mounted}\n label={label}\n description={description}\n />\n );\n}\n\nexport interface FontSizeControlProps {\n /** Nhãn của cả nhóm, đọc lên cho trình đọc màn hình (khoá `fontSizeLabel` của `displayAxis`). */\n label?: string;\n /**\n * Nhãn từng mức — THỨ TỰ theo `FONT_SIZE_MODES` (`sm`, `md`, `lg`). Không truyền thì lấy ba khoá\n * `fontSizeSm`/`fontSizeMd`/`fontSizeLg` của namespace `displayAxis`.\n */\n optionLabels?: Record<FontSizeMode, string>;\n className?: string;\n /** Gọi SAU khi người dùng đổi — xem giải thích ở `DensityToggleProps.onDensityChange`. */\n onFontSizeChange?: (mode: FontSizeMode) => void;\n}\n\n/**\n * Chọn cỡ chữ — ba mức, một hàng nút.\n *\n * `role=\"radiogroup\"` + `aria-checked` chứ không phải ba nút thường: đây là chọn MỘT trong ba, và\n * trình đọc màn hình cần nghe \"2 trên 3\" chứ không nghe ba nút rời rạc. Dùng `<button>` thật (bàn\n * phím tới được bằng Tab) thay vì roving focus của Radix ToggleGroup — nhóm chỉ có ba mục nên\n * roving focus không đáng cái giá thêm một phụ thuộc.\n *\n * Cỡ chữ của CHÍNH các nút này cố định `text-sm`: nếu chúng phóng theo lựa chọn thì bấm mức `lg`\n * sẽ làm cả hàng nút nhảy chỗ ngay dưới ngón tay.\n */\nexport function FontSizeControl({\n label: labelProp,\n optionLabels: optionLabelsProp,\n className,\n onFontSizeChange\n}: FontSizeControlProps) {\n const t = useComitorMessages(\"displayAxis\");\n const label = labelProp ?? t.fontSizeLabel;\n const optionLabels = optionLabelsProp ?? { sm: t.fontSizeSm, md: t.fontSizeMd, lg: t.fontSizeLg };\n const { fontSize, setFontSize, mounted } = useFontSize();\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={label}\n className={cn(\"inline-flex items-center gap-1 rounded-md border border-input p-1 text-sm\", className)}\n >\n {FONT_SIZE_MODES.map((mode) => {\n const selected = fontSize === mode;\n return (\n <button\n key={mode}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n disabled={!mounted}\n onClick={() => {\n setFontSize(mode);\n onFontSizeChange?.(mode);\n }}\n className={cn(\n \"rounded-sm px-3 py-1 text-sm transition-colors duration-150 ease-out\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n selected\n ? \"bg-control-on text-control-check font-medium\"\n : \"text-muted-foreground hover:bg-accent hover:text-foreground\"\n )}\n >\n {optionLabels[mode]}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AAoEI;AAjEJ,SAAS,cAAc;AACvB,SAAS,0BAA0B;AACnC,SAAS,UAAU;AACnB,SAA2B,uBAA0C;AACrE,SAAS,YAAY,mBAAmB;AAiDjC,SAAS,cAAc;AAAA,EAC5B,OAAO;AAAA,EACP,aAAa;AAAA,EACb;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,IAAI,mBAAmB,aAAa;AAC1C,QAAM,QAAQ,aAAa,EAAE;AAC7B,QAAM,cAAc,mBAAmB,EAAE;AACzC,QAAM,EAAE,SAAS,YAAY,QAAQ,IAAI,WAAW;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,YAAY;AAAA,MACrB,iBAAiB,CAAC,YAAY;AAC5B,cAAM,OAAoB,UAAU,YAAY;AAChD,mBAAW,IAAI;AACf,0BAAkB,IAAI;AAAA,MACxB;AAAA,MACA,UAAU,CAAC;AAAA,MACX;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;AA0BO,SAAS,gBAAgB;AAAA,EAC9B,OAAO;AAAA,EACP,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,IAAI,mBAAmB,aAAa;AAC1C,QAAM,QAAQ,aAAa,EAAE;AAC7B,QAAM,eAAe,oBAAoB,EAAE,IAAI,EAAE,YAAY,IAAI,EAAE,YAAY,IAAI,EAAE,WAAW;AAChG,QAAM,EAAE,UAAU,aAAa,QAAQ,IAAI,YAAY;AAEvD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,6EAA6E,SAAS;AAAA,MAEnG,0BAAgB,IAAI,CAAC,SAAS;AAC7B,cAAM,WAAW,aAAa;AAC9B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,UAAU,CAAC;AAAA,YACX,SAAS,MAAM;AACb,0BAAY,IAAI;AAChB,iCAAmB,IAAI;AAAA,YACzB;AAAA,YACA,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA,WACI,iDACA;AAAA,YACN;AAAA,YAEC,uBAAa,IAAI;AAAA;AAAA,UAlBb;AAAA,QAmBP;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -37,7 +37,7 @@ declare function useDensity(): {
37
37
  };
38
38
  declare const FontSizeProvider: ({ children, defaultValue, storageKey, nonce }: {
39
39
  children: ReactNode;
40
- defaultValue?: "sm" | "md" | "lg" | undefined;
40
+ defaultValue?: "md" | "sm" | "lg" | undefined;
41
41
  storageKey?: string;
42
42
  /**
43
43
  * Nonce của CSP, cho script chống nháy mà provider này TỰ render.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/mobile-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Sheet, SheetContent, SheetHeader, SheetTitle } from \"../components/ui/sheet.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppLauncher } from \"./app-launcher.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { ThemeToggle } from \"./theme-toggle.js\";\nimport { UserMenu, type UserMenuProps } from \"./user-menu.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface MobileMenuProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher. */\n header?: ReactNode;\n /** Slot đáy (phiên bản app, liên kết hỗ trợ…). */\n footer?: ReactNode;\n showAppLauncher?: boolean;\n showThemeToggle?: boolean;\n /** Chuyển tiếp props cho `UserMenu` ở đáy. */\n userMenuProps?: UserMenuProps;\n}\n\n/**\n * Menu off-canvas cho màn hình hẹp (`Sheet` trượt từ trái):\n * WorkspaceSwitcher (trục 1) → menu app HIỆN TẠI → danh sách app (trục 2) → user.\n *\n * ⚠ Thứ tự đó cố ý KHÁC thanh bên desktop. Trên desktop danh sách app nằm ở header nên nó không\n * chiếm chỗ của menu; ở đây cả hai nằm chung một vùng cuộn, và menu của app đang mở là thứ người\n * dùng mở menu để tìm. Đặt danh sách app lên trước thì một hệ sinh thái 8–10 sản phẩm đẩy toàn bộ\n * menu chính xuống dưới nếp gấp — phải cuộn mới thấy thứ mình cần.\n *\n * Tự đóng khi đổi route (`ShellProvider` lo) và khi bấm vào một link thật trong menu.\n */\nexport function MobileMenu({\n className,\n header,\n footer,\n showAppLauncher = true,\n showThemeToggle = true,\n userMenuProps\n}: MobileMenuProps) {\n const { mobileMenuOpen, setMobileMenuOpen, nav, currentApp, user, labels } = useShell();\n\n const allItems = flattenNavItems(nav);\n\n // Chỉ đóng khi bấm trúng LINK thật — `SidebarMenuItem` không gọi callback này\n // khi người dùng chỉ mở/đóng nhánh con, nên menu giữ nguyên cho họ đi tiếp.\n const closeMenu = () => setMobileMenuOpen(false);\n\n return (\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\n <SheetContent\n side=\"left\"\n aria-describedby={undefined}\n // `closeLabel` PHẢI truyền tay. Mặc định của `SheetContent` là \"Đóng\" ghim cứng trong\n // primitive, mà app đổi ngôn ngữ shell qua ĐÚNG MỘT cửa: prop `labels`. Không nối hai đầu\n // ở đây thì app tiếng Anh khai đủ `ShellLabels` vẫn đẩy tiếng Việt vào cây trợ năng — và\n // họ không có chỗ nào sửa, vì `MobileMenu` không nhận props của `Sheet`.\n // Dùng `closeMenu` (\"Đóng menu\") chứ không phải một nhãn \"Đóng\" chung: trình đọc màn hình\n // đọc nút này TÁCH khỏi ngữ cảnh, nên nó phải nói rõ đóng cái gì — cùng lý do với\n // `collapseSidebar` trong types.ts.\n closeLabel={labels.closeMenu}\n style={appAccentStyle(currentApp)}\n className={cn(\"flex w-80 max-w-[85vw] flex-col gap-0 bg-sidebar p-0 md:hidden\", className)}\n >\n {/*\n `pr-12`: `SheetContent` đặt nút đóng ở `absolute top-4 right-4`, tức nó nằm ĐÈ lên nội\n dung header. Không chừa chỗ thì nó phủ lên mũi tên của `WorkspaceSwitcher` — bấm vào\n tưởng đổi workspace mà hoá ra đóng menu.\n */}\n <SheetHeader className=\"shrink-0 border-b border-sidebar-border p-2 pr-12\">\n <SheetTitle className=\"sr-only\">{labels.navigationLabel}</SheetTitle>\n <WorkspaceSwitcher align=\"start\" />\n </SheetHeader>\n\n {header ? <div className=\"shrink-0 p-2\">{header}</div> : null}\n\n <div className=\"flex-1 overflow-y-auto scrollbar-thin\">\n {/* Menu của app HIỆN TẠI đứng trước — xem lý do thứ tự ở JSDoc đầu file. */}\n <div className=\"flex flex-col gap-4 p-2\">\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} siblings={allItems} onNavigate={closeMenu} />\n ))}\n </div>\n\n {showAppLauncher ? (\n <>\n <Separator className=\"bg-sidebar-border\" />\n <div className=\"p-2\">\n <AppLauncher inline variant=\"list\" onSelected={closeMenu} />\n </div>\n </>\n ) : null}\n </div>\n\n <div className=\"flex shrink-0 items-center gap-2 border-t border-sidebar-border p-2\">\n {user ? <UserMenu showName {...userMenuProps} className=\"min-w-0 flex-1\" /> : null}\n {showThemeToggle ? <ThemeToggle labels={labels} /> : null}\n </div>\n\n {footer ? <div className=\"shrink-0 border-t border-sidebar-border p-2\">{footer}</div> : null}\n </SheetContent>\n </Sheet>\n );\n}\n"],"mappings":";AA0EQ,SAgBI,UAfF,KADF;AAvER,SAAS,iBAAiB;AAC1B,SAAS,OAAO,cAAc,aAAa,kBAAkB;AAC7D,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,gBAAoC;AAC7C,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAyB3B,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB;AACF,GAAoB;AAClB,QAAM,EAAE,gBAAgB,mBAAmB,KAAK,YAAY,MAAM,OAAO,IAAI,SAAS;AAEtF,QAAM,WAAW,gBAAgB,GAAG;AAIpC,QAAM,YAAY,MAAM,kBAAkB,KAAK;AAE/C,SACE,oBAAC,SAAM,MAAM,gBAAgB,cAAc,mBACzC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,oBAAkB;AAAA,MAQlB,YAAY,OAAO;AAAA,MACnB,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW,GAAG,kEAAkE,SAAS;AAAA,MAOzF;AAAA,6BAAC,eAAY,WAAU,qDACrB;AAAA,8BAAC,cAAW,WAAU,WAAW,iBAAO,iBAAgB;AAAA,UACxD,oBAAC,qBAAkB,OAAM,SAAQ;AAAA,WACnC;AAAA,QAEC,SAAS,oBAAC,SAAI,WAAU,gBAAgB,kBAAO,IAAS;AAAA,QAEzD,qBAAC,SAAI,WAAU,yCAEb;AAAA,8BAAC,SAAI,WAAU,2BACZ,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,UAAU,UAAU,YAAY,aAAxD,MAAM,EAA6D,CACvF,GACH;AAAA,UAEC,kBACC,iCACE;AAAA,gCAAC,aAAU,WAAU,qBAAoB;AAAA,YACzC,oBAAC,SAAI,WAAU,OACb,8BAAC,eAAY,QAAM,MAAC,SAAQ,QAAO,YAAY,WAAW,GAC5D;AAAA,aACF,IACE;AAAA,WACN;AAAA,QAEA,qBAAC,SAAI,WAAU,uEACZ;AAAA,iBAAO,oBAAC,YAAS,UAAQ,MAAE,GAAG,eAAe,WAAU,kBAAiB,IAAK;AAAA,UAC7E,kBAAkB,oBAAC,eAAY,QAAgB,IAAK;AAAA,WACvD;AAAA,QAEC,SAAS,oBAAC,SAAI,WAAU,+CAA+C,kBAAO,IAAS;AAAA;AAAA;AAAA,EAC1F,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/mobile-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Sheet, SheetContent, SheetHeader, SheetTitle } from \"../components/ui/sheet.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppLauncher } from \"./app-launcher.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { ThemeToggle } from \"./theme-toggle.js\";\nimport { UserMenu, type UserMenuProps } from \"./user-menu.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface MobileMenuProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher. */\n header?: ReactNode;\n /** Slot đáy (phiên bản app, liên kết hỗ trợ…). */\n footer?: ReactNode;\n showAppLauncher?: boolean;\n showThemeToggle?: boolean;\n /** Chuyển tiếp props cho `UserMenu` ở đáy. */\n userMenuProps?: UserMenuProps;\n}\n\n/**\n * Menu off-canvas cho màn hình hẹp (`Sheet` trượt từ trái):\n * WorkspaceSwitcher (trục 1) → menu app HIỆN TẠI → danh sách app (trục 2) → user.\n *\n * ⚠ Thứ tự đó cố ý KHÁC thanh bên desktop. Trên desktop danh sách app nằm ở header nên nó không\n * chiếm chỗ của menu; ở đây cả hai nằm chung một vùng cuộn, và menu của app đang mở là thứ người\n * dùng mở menu để tìm. Đặt danh sách app lên trước thì một hệ sinh thái 8–10 sản phẩm đẩy toàn bộ\n * menu chính xuống dưới nếp gấp — phải cuộn mới thấy thứ mình cần.\n *\n * Tự đóng khi đổi route (`ShellProvider` lo) và khi bấm vào một link thật trong menu.\n */\nexport function MobileMenu({\n className,\n header,\n footer,\n showAppLauncher = true,\n showThemeToggle = true,\n userMenuProps\n}: MobileMenuProps) {\n const { mobileMenuOpen, setMobileMenuOpen, nav, currentApp, user, labels } = useShell();\n\n const allItems = flattenNavItems(nav);\n\n // Chỉ đóng khi bấm trúng LINK thật — `SidebarMenuItem` không gọi callback này\n // khi người dùng chỉ mở/đóng nhánh con, nên menu giữ nguyên cho họ đi tiếp.\n const closeMenu = () => setMobileMenuOpen(false);\n\n return (\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\n <SheetContent\n side=\"left\"\n aria-describedby={undefined}\n // `closeLabel` PHẢI truyền tay. Mặc định của `SheetContent` là nhãn \"đóng\" CHUNG của\n // primitive (namespace `sheet`), còn app đổi chữ của shell qua prop `labels` của shell.\n // Không nối hai đầu ở đây thì một `closeMenu` app đã đổi không bao giờ tới được cây trợ\n // năng — và họ không có chỗ nào sửa, vì `MobileMenu` không nhận props của `Sheet`.\n // Dùng `closeMenu` (\"Close menu\") chứ không phải một nhãn \"Close\" chung: trình đọc màn hình\n // đọc nút này TÁCH khỏi ngữ cảnh, nên nó phải nói rõ đóng cái gì — cùng lý do với\n // `collapseSidebar` trong types.ts.\n closeLabel={labels.closeMenu}\n style={appAccentStyle(currentApp)}\n className={cn(\"flex w-80 max-w-[85vw] flex-col gap-0 bg-sidebar p-0 md:hidden\", className)}\n >\n {/*\n `pr-12`: `SheetContent` đặt nút đóng ở `absolute top-4 right-4`, tức nó nằm ĐÈ lên nội\n dung header. Không chừa chỗ thì nó phủ lên mũi tên của `WorkspaceSwitcher` — bấm vào\n tưởng đổi workspace mà hoá ra đóng menu.\n */}\n <SheetHeader className=\"shrink-0 border-b border-sidebar-border p-2 pr-12\">\n <SheetTitle className=\"sr-only\">{labels.navigationLabel}</SheetTitle>\n <WorkspaceSwitcher align=\"start\" />\n </SheetHeader>\n\n {header ? <div className=\"shrink-0 p-2\">{header}</div> : null}\n\n <div className=\"flex-1 overflow-y-auto scrollbar-thin\">\n {/* Menu của app HIỆN TẠI đứng trước — xem lý do thứ tự ở JSDoc đầu file. */}\n <div className=\"flex flex-col gap-4 p-2\">\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} siblings={allItems} onNavigate={closeMenu} />\n ))}\n </div>\n\n {showAppLauncher ? (\n <>\n <Separator className=\"bg-sidebar-border\" />\n <div className=\"p-2\">\n <AppLauncher inline variant=\"list\" onSelected={closeMenu} />\n </div>\n </>\n ) : null}\n </div>\n\n <div className=\"flex shrink-0 items-center gap-2 border-t border-sidebar-border p-2\">\n {user ? <UserMenu showName {...userMenuProps} className=\"min-w-0 flex-1\" /> : null}\n {showThemeToggle ? <ThemeToggle labels={labels} /> : null}\n </div>\n\n {footer ? <div className=\"shrink-0 border-t border-sidebar-border p-2\">{footer}</div> : null}\n </SheetContent>\n </Sheet>\n );\n}\n"],"mappings":";AA0EQ,SAgBI,UAfF,KADF;AAvER,SAAS,iBAAiB;AAC1B,SAAS,OAAO,cAAc,aAAa,kBAAkB;AAC7D,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,gBAAoC;AAC7C,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAyB3B,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB;AACF,GAAoB;AAClB,QAAM,EAAE,gBAAgB,mBAAmB,KAAK,YAAY,MAAM,OAAO,IAAI,SAAS;AAEtF,QAAM,WAAW,gBAAgB,GAAG;AAIpC,QAAM,YAAY,MAAM,kBAAkB,KAAK;AAE/C,SACE,oBAAC,SAAM,MAAM,gBAAgB,cAAc,mBACzC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,oBAAkB;AAAA,MAQlB,YAAY,OAAO;AAAA,MACnB,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW,GAAG,kEAAkE,SAAS;AAAA,MAOzF;AAAA,6BAAC,eAAY,WAAU,qDACrB;AAAA,8BAAC,cAAW,WAAU,WAAW,iBAAO,iBAAgB;AAAA,UACxD,oBAAC,qBAAkB,OAAM,SAAQ;AAAA,WACnC;AAAA,QAEC,SAAS,oBAAC,SAAI,WAAU,gBAAgB,kBAAO,IAAS;AAAA,QAEzD,qBAAC,SAAI,WAAU,yCAEb;AAAA,8BAAC,SAAI,WAAU,2BACZ,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,UAAU,UAAU,YAAY,aAAxD,MAAM,EAA6D,CACvF,GACH;AAAA,UAEC,kBACC,iCACE;AAAA,gCAAC,aAAU,WAAU,qBAAoB;AAAA,YACzC,oBAAC,SAAI,WAAU,OACb,8BAAC,eAAY,QAAM,MAAC,SAAQ,QAAO,YAAY,WAAW,GAC5D;AAAA,aACF,IACE;AAAA,WACN;AAAA,QAEA,qBAAC,SAAI,WAAU,uEACZ;AAAA,iBAAO,oBAAC,YAAS,UAAQ,MAAE,GAAG,eAAe,WAAU,kBAAiB,IAAK;AAAA,UAC7E,kBAAkB,oBAAC,eAAY,QAAgB,IAAK;AAAA,WACvD;AAAA,QAEC,SAAS,oBAAC,SAAI,WAAU,+CAA+C,kBAAO,IAAS;AAAA;AAAA;AAAA,EAC1F,GACF;AAEJ;","names":[]}
@@ -4,7 +4,7 @@ import { ComponentProps } from 'react';
4
4
  interface NotificationsButtonProps extends Omit<ComponentProps<"button">, "children"> {
5
5
  /** Số thông báo chưa đọc; 0 hoặc bỏ trống thì không hiện chấm số. */
6
6
  count?: number;
7
- /** Nhãn a11y (mặc định "Thông báo"). */
7
+ /** Nhãn a11y. Không truyền thì lấy khoá `notifications` của namespace `shell`. */
8
8
  label?: string;
9
9
  /** Vượt ngưỡng này thì hiển thị "n+" (mặc định 9). */
10
10
  max?: number;
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Bell } from "lucide-react";
4
+ import { useComitorMessages } from "../i18n/locale-provider.js";
4
5
  import { cn } from "../lib/cn.js";
5
- import { DEFAULT_SHELL_LABELS } from "./types.js";
6
6
  function NotificationsButton({ count = 0, label, max = 9, className, ...props }) {
7
- const text = label ?? DEFAULT_SHELL_LABELS.notifications;
7
+ const t = useComitorMessages("shell");
8
+ const text = label ?? t.notifications;
8
9
  const hasUnread = count > 0;
9
10
  return /* @__PURE__ */ jsxs(
10
11
  "button",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/notifications-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Bell } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport { DEFAULT_SHELL_LABELS } from \"./types.js\";\n\nexport interface NotificationsButtonProps extends Omit<ComponentProps<\"button\">, \"children\"> {\n /** Số thông báo chưa đọc; 0 hoặc bỏ trống thì không hiện chấm số. */\n count?: number;\n /** Nhãn a11y (mặc định \"Thông báo\"). */\n label?: string;\n /** Vượt ngưỡng này thì hiển thị \"n+\" (mặc định 9). */\n max?: number;\n}\n\n/**\n * Nút chuông + số chưa đọc. **Chỉ có cái nút** — nội dung panel thông báo là\n * app-specific: đóng sẵn một NotificationPanel vào package thì app nào cũng phải\n * uốn dữ liệu của mình cho vừa cái khuôn đó.\n *\n * App tự bọc trong Popover/Sheet của mình:\n * `<PopoverTrigger asChild><NotificationsButton count={3} /></PopoverTrigger>`\n */\nexport function NotificationsButton({ count = 0, label, max = 9, className, ...props }: NotificationsButtonProps) {\n const text = label ?? DEFAULT_SHELL_LABELS.notifications;\n const hasUnread = count > 0;\n\n return (\n <button\n type=\"button\"\n aria-label={hasUnread ? `${text} (${count})` : text}\n className={cn(\n \"relative flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n className\n )}\n {...props}\n >\n <Bell className=\"size-4\" />\n {hasUnread ? (\n /*\n * Huy hiệu ĐẨY RA GÓC, không nằm trong lòng nút.\n *\n * `top-0 right-0` — huy hiệu nằm SÁT GÓC nút, không lấn vào trong và cũng không tràn ra\n * ngoài. Hai bản trước đều lệch, đo trên DOM thật (nút 36px, icon 16px căn giữa):\n *\n * right-1 top-1 che 39% cái chuông — thấy khối đỏ, không thấy chuông\n * -right-0.5 -top-0.5 che 6% — hết che, nhưng huy hiệu trôi ra ngoài nút, đọc như\n * một chấm rời không thuộc về cái chuông\n * top-0 right-0 che 14% — dính vào góc icon, vẫn thấy rõ hình chuông ← đang dùng\n *\n * KHÔNG dùng giá trị âm: các nút header cách nhau `gap-1` (4px), tràn ra ngoài là huy\n * hiệu tiến sát nút bên cạnh.\n *\n * `ring-2 ring-background` là nửa còn lại: viền cùng màu nền header cắt một đường sạch\n * giữa huy hiệu và icon, nên 14% chồng lấn kia đọc thành hai hình tách bạch chứ không\n * nhoè vào nhau. Cùng thủ pháp mà mẫu AppHeader ở mục Patterns dùng cho chấm thông báo\n * (`ring-2 ring-card`).\n */\n <span\n className={cn(\n \"absolute right-0 top-0 flex h-4 min-w-4 items-center justify-center rounded-full px-1\",\n \"ring-2 ring-background\",\n \"bg-destructive text-[10px] font-semibold leading-none text-destructive-foreground\"\n )}\n >\n {count > max ? `${max}+` : count}\n </span>\n ) : null}\n </button>\n );\n}\n"],"mappings":";AA6BI,SAUE,KAVF;AA3BJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,4BAA4B;AAmB9B,SAAS,oBAAoB,EAAE,QAAQ,GAAG,OAAO,MAAM,GAAG,WAAW,GAAG,MAAM,GAA6B;AAChH,QAAM,OAAO,SAAS,qBAAqB;AAC3C,QAAM,YAAY,QAAQ;AAE1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,YAAY,GAAG,IAAI,KAAK,KAAK,MAAM;AAAA,MAC/C,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,4BAAC,QAAK,WAAU,UAAS;AAAA,QACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAoBC;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cAEC,kBAAQ,MAAM,GAAG,GAAG,MAAM;AAAA;AAAA,UAC7B;AAAA,YACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/notifications-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Bell } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\nimport { useComitorMessages } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\n\nexport interface NotificationsButtonProps extends Omit<ComponentProps<\"button\">, \"children\"> {\n /** Số thông báo chưa đọc; 0 hoặc bỏ trống thì không hiện chấm số. */\n count?: number;\n /** Nhãn a11y. Không truyền thì lấy khoá `notifications` của namespace `shell`. */\n label?: string;\n /** Vượt ngưỡng này thì hiển thị \"n+\" (mặc định 9). */\n max?: number;\n}\n\n/**\n * Nút chuông + số chưa đọc. **Chỉ có cái nút** — nội dung panel thông báo là\n * app-specific: đóng sẵn một NotificationPanel vào package thì app nào cũng phải\n * uốn dữ liệu của mình cho vừa cái khuôn đó.\n *\n * App tự bọc trong Popover/Sheet của mình:\n * `<PopoverTrigger asChild><NotificationsButton count={3} /></PopoverTrigger>`\n */\nexport function NotificationsButton({ count = 0, label, max = 9, className, ...props }: NotificationsButtonProps) {\n const t = useComitorMessages(\"shell\");\n const text = label ?? t.notifications;\n const hasUnread = count > 0;\n\n return (\n <button\n type=\"button\"\n aria-label={hasUnread ? `${text} (${count})` : text}\n className={cn(\n \"relative flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n className\n )}\n {...props}\n >\n <Bell className=\"size-4\" />\n {hasUnread ? (\n /*\n * Huy hiệu ĐẨY RA GÓC, không nằm trong lòng nút.\n *\n * `top-0 right-0` — huy hiệu nằm SÁT GÓC nút, không lấn vào trong và cũng không tràn ra\n * ngoài. Hai bản trước đều lệch, đo trên DOM thật (nút 36px, icon 16px căn giữa):\n *\n * right-1 top-1 che 39% cái chuông — thấy khối đỏ, không thấy chuông\n * -right-0.5 -top-0.5 che 6% — hết che, nhưng huy hiệu trôi ra ngoài nút, đọc như\n * một chấm rời không thuộc về cái chuông\n * top-0 right-0 che 14% — dính vào góc icon, vẫn thấy rõ hình chuông ← đang dùng\n *\n * KHÔNG dùng giá trị âm: các nút header cách nhau `gap-1` (4px), tràn ra ngoài là huy\n * hiệu tiến sát nút bên cạnh.\n *\n * `ring-2 ring-background` là nửa còn lại: viền cùng màu nền header cắt một đường sạch\n * giữa huy hiệu và icon, nên 14% chồng lấn kia đọc thành hai hình tách bạch chứ không\n * nhoè vào nhau. Cùng thủ pháp mà mẫu AppHeader ở mục Patterns dùng cho chấm thông báo\n * (`ring-2 ring-card`).\n */\n <span\n className={cn(\n \"absolute right-0 top-0 flex h-4 min-w-4 items-center justify-center rounded-full px-1\",\n \"ring-2 ring-background\",\n \"bg-destructive text-[10px] font-semibold leading-none text-destructive-foreground\"\n )}\n >\n {count > max ? `${max}+` : count}\n </span>\n ) : null}\n </button>\n );\n}\n"],"mappings":";AA8BI,SAUE,KAVF;AA5BJ,SAAS,YAAY;AAErB,SAAS,0BAA0B;AACnC,SAAS,UAAU;AAmBZ,SAAS,oBAAoB,EAAE,QAAQ,GAAG,OAAO,MAAM,GAAG,WAAW,GAAG,MAAM,GAA6B;AAChH,QAAM,IAAI,mBAAmB,OAAO;AACpC,QAAM,OAAO,SAAS,EAAE;AACxB,QAAM,YAAY,QAAQ;AAE1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,YAAY,GAAG,IAAI,KAAK,KAAK,MAAM;AAAA,MAC/C,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,4BAAC,QAAK,WAAU,UAAS;AAAA,QACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAoBC;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cAEC,kBAAQ,MAAM,GAAG,GAAG,MAAM;AAAA;AAAA,UAC7B;AAAA,YACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -102,7 +102,10 @@ interface ShellProviderProps {
102
102
  persistSidebarState?: boolean;
103
103
  /** Tắt bộ phím tắt toàn cục. */
104
104
  enableShortcuts?: boolean;
105
- /** Override chuỗi hiển thị (mặc định tiếng Việt). */
105
+ /**
106
+ * Override từng chuỗi hiển thị. Không truyền thì theo namespace `shell` của
107
+ * `ComitorLocaleProvider` gần nhất (không có provider → tiếng Anh).
108
+ */
106
109
  labels?: Partial<ShellLabels>;
107
110
  children: ReactNode;
108
111
  }
@@ -4,10 +4,8 @@ import NextLinkModule from "next/link.js";
4
4
  import { usePathname } from "next/navigation.js";
5
5
  import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
6
6
  import { TooltipProvider } from "../components/ui/tooltip.js";
7
+ import { useComitorLabels } from "../i18n/locale-provider.js";
7
8
  import { layout } from "../tokens.js";
8
- import {
9
- DEFAULT_SHELL_LABELS
10
- } from "./types.js";
11
9
  import { getEntitledApps, resolveCurrentApp, toNavGroups, toSearchParams } from "./utils.js";
12
10
  const NextLink = NextLinkModule.default ?? NextLinkModule;
13
11
  function DefaultShellLink({ href, children, ...props }) {
@@ -77,7 +75,7 @@ function ShellProvider({
77
75
  return resolveCurrentApp(apps, pathname);
78
76
  }, [apps, currentAppId, pathname]);
79
77
  const navGroups = useMemo(() => toNavGroups(nav), [nav]);
80
- const labels = useMemo(() => ({ ...DEFAULT_SHELL_LABELS, ...labelsProp }), [labelsProp]);
78
+ const labels = useComitorLabels("shell", labelsProp);
81
79
  const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);
82
80
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
83
81
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/shell-context.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * ĐUÔI `.js` LÀ CHỦ ĐÍCH, ĐỪNG BỎ.\n *\n * Gói `next` KHÔNG khai `exports` map, nên `next/link` chỉ là đường dẫn file. Bundler\n * (webpack/Turbopack) tự thêm đuôi rồi trỏ đúng `node_modules/next/link.js`; Node ESM thuần thì\n * KHÔNG — nó ném `ERR_MODULE_NOT_FOUND: Cannot find module .../next/link` kèm gợi ý\n * \"Did you mean next/link.js?\". Hậu quả: `node -e \"await import('@comitor/ui/shell')\"` chết ngay\n * lúc nạp, tức là mọi smoke-test entry của CI đều mù với riêng entry shell.\n *\n * Ghi thẳng `next/link.js` / `next/navigation.js` là CÙNG MỘT file mà bundler vẫn resolve tới\n * (đã đối chiếu: `next` không có `resolve.alias` cho hai specifier này trong\n * `dist/build/create-compiler-aliases.js`), nhưng Node cũng nạp được.\n */\nimport NextLinkModule from \"next/link.js\";\nimport { usePathname } from \"next/navigation.js\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport { layout } from \"../tokens.js\";\nimport {\n type AppDescriptor,\n DEFAULT_SHELL_LABELS,\n type NavGroup,\n type ShellLabels,\n type ShellLinkComponent,\n type ShellLinkProps,\n type ShellNav,\n type ShellUser,\n type Workspace\n} from \"./types.js\";\nimport { getEntitledApps, type NavSearchParams, resolveCurrentApp, toNavGroups, toSearchParams } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Chuẩn hoá interop của `next/link.js`.\n *\n * Hai môi trường trả về hai hình dạng khác nhau cho cùng một file CJS:\n * - bundler → đã interop sẵn, biến này CHÍNH LÀ component `Link`\n * - Node ESM → biến này là namespace `{ default: Link, useLinkStatus }`\n * `.default ?? chính nó` đúng ở cả hai (component `Link` không có thuộc tính `default`).\n */\nconst NextLink = ((NextLinkModule as unknown as { default?: ShellLinkComponent }).default ??\n NextLinkModule) as ShellLinkComponent;\n\n/**\n * Bọc `next/link` thay vì truyền thẳng: hình dạng props của NextLink rộng hơn\n * `ShellLinkProps` (href nhận cả UrlObject), bọc lại cho khớp type và để chỗ\n * thay adapter router khác về sau.\n */\nfunction DefaultShellLink({ href, children, ...props }: ShellLinkProps) {\n return (\n <NextLink href={href} {...props}>\n {children}\n </NextLink>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Context\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellContextValue {\n /* Trục 1 — workspace */\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /* Trục 2 — app */\n apps: AppDescriptor[];\n currentApp: AppDescriptor | null;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /* Điều hướng trong app hiện tại */\n nav: NavGroup[];\n pathname: string;\n /**\n * Query hiện tại đã chuẩn hoá, hoặc `null` khi app không truyền.\n * `null` KHÁC rỗng: rỗng nghĩa là \"URL không có query\", `null` nghĩa là \"shell không biết\"\n * — và active matching xử lý hai ca đó khác nhau (xem `isNavItemActive`).\n */\n searchParams: URLSearchParams | null;\n LinkComponent: ShellLinkComponent;\n onNavigate?: (href: string) => void;\n\n /* Người dùng */\n user: ShellUser | null;\n onLogout?: () => void;\n\n /* Trạng thái khung */\n sidebarCollapsed: boolean;\n setSidebarCollapsed: (collapsed: boolean) => void;\n toggleSidebar: () => void;\n mobileMenuOpen: boolean;\n setMobileMenuOpen: (open: boolean) => void;\n commandPaletteOpen: boolean;\n setCommandPaletteOpen: (open: boolean) => void;\n appLauncherOpen: boolean;\n setAppLauncherOpen: (open: boolean) => void;\n\n /* Chuỗi hiển thị (đã trộn mặc định tiếng Việt với override của app) */\n labels: ShellLabels;\n}\n\nconst ShellContext = createContext<ShellContextValue | undefined>(undefined);\n\n/** Truy cập state của shell — phải gọi bên trong `<ShellProvider>`. */\nexport function useShell(): ShellContextValue {\n const context = useContext(ShellContext);\n if (!context) {\n throw new Error(\"useShell phải được dùng bên trong <ShellProvider> (hoặc <AppShell>).\");\n }\n return context;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Ghi nhớ trạng thái thu gọn sidebar\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Khoá localStorage theo workspace — mỗi workspace nhớ riêng trạng thái sidebar\n * (spec §7.6: KHÔNG dùng cookie `sidebar_state` của shadcn để tránh hai nguồn sự thật).\n */\nfunction sidebarStorageKey(workspaceId: string | undefined): string {\n return `comitor-sidebar:${workspaceId ?? \"default\"}`;\n}\n\n/**\n * Đang ở dải TABLET? — từ `breakpoints.mobile` (768px, mốc thanh bên thôi off-canvas) tới ngay\n * dưới `breakpoints.tablet` (1024px).\n *\n * Hai mốc lấy từ `layout.breakpoints` của `@comitor/ui/tokens` chứ không gõ số ở đây: đổi mốc mà\n * quên chỗ này thì thanh bên tự sập ở một dải mà CSS không còn coi là tablet nữa.\n */\nfunction isTabletWidth(): boolean {\n if (typeof window === \"undefined\") return false;\n const { mobile, tablet } = layout.breakpoints;\n return window.matchMedia(`(min-width: ${mobile}px) and (max-width: ${tablet - 1}px)`).matches;\n}\n\nfunction readStoredCollapsed(workspaceId: string | undefined): boolean | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(sidebarStorageKey(workspaceId));\n if (raw === \"1\") return true;\n if (raw === \"0\") return false;\n return null;\n } catch {\n // Safari private mode / storage bị chặn — coi như chưa có giá trị lưu.\n return null;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Provider\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellProviderProps {\n /** Trục 1 — danh sách workspace của user. Rỗng/1 phần tử thì WorkspaceSwitcher tự ẩn danh sách. */\n workspaces?: Workspace[];\n /** Workspace đang mở; bỏ trống → lấy phần tử đầu. */\n currentWorkspaceId?: string;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /** Trục 2 — danh sách app của hệ sinh thái (kể cả app chưa mở khoá). */\n apps?: AppDescriptor[];\n /** App đang mở; bỏ trống → suy từ `pathname` (khớp prefix dài nhất). */\n currentAppId?: string;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /** Menu của app hiện tại — `NavItem[]` hoặc `NavGroup[]`. */\n nav?: ShellNav;\n /**\n * Ghi đè pathname (Storybook, app không-Next). Bỏ trống → dùng `usePathname()`\n * của next/navigation (trả `null` khi không có App Router → coi như \"\").\n */\n pathname?: string;\n /**\n * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query\n * (`/cai-dat?tab=chung` ⇄ `/cai-dat?tab=thanh-vien`). Bỏ trống thì mọi mục cùng đường dẫn\n * cùng sáng.\n *\n * Nhận `useSearchParams()` của Next, chuỗi `\"?tab=x\"`, hoặc object `searchParams` của\n * Server Component.\n *\n * ⚠ Shell KHÔNG tự gọi `useSearchParams()`: hook đó ép trang bọc `<Suspense>` và đẩy trang\n * sang render phía client khi build tĩnh — cái giá đó phải do app tự chọn, không phải do\n * việc lỡ import shell. Truyền vào từ một client component đã có sẵn boundary:\n *\n * ```tsx\n * \"use client\";\n * const searchParams = useSearchParams();\n * return <AppShell searchParams={searchParams} …>{children}</AppShell>;\n * ```\n */\n searchParams?: NavSearchParams | null;\n /**\n * Ở dải TABLET (768–1023px), khi người dùng CHƯA từng gạt thanh bên, mở app lần đầu thì thanh\n * bên thu gọn sẵn. Mặc định `true`.\n *\n * Đặt `false` khi app muốn thanh bên luôn mở rộng ở tablet — vd điều hướng là thứ chính của\n * màn hình đó. Lựa chọn ĐÃ LƯU của người dùng luôn thắng cờ này.\n */\n collapseSidebarOnTablet?: boolean;\n /** Component link thay `next/link` (app không-Next). */\n LinkComponent?: ShellLinkComponent;\n /** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */\n onNavigate?: (href: string) => void;\n\n user?: ShellUser | null;\n onLogout?: () => void;\n\n /** Trạng thái thu gọn ban đầu (SSR). Sau khi mount, localStorage của workspace thắng. */\n defaultSidebarCollapsed?: boolean;\n /** Tắt ghi nhớ localStorage (Storybook, chế độ kiosk). */\n persistSidebarState?: boolean;\n /** Tắt bộ phím tắt toàn cục. */\n enableShortcuts?: boolean;\n\n /** Override chuỗi hiển thị (mặc định tiếng Việt). */\n labels?: Partial<ShellLabels>;\n\n children: ReactNode;\n}\n\n/**\n * Provider của app shell Comitor: giữ hai trục điều hướng (workspace × app),\n * trạng thái khung (sidebar/mobile/command palette/app launcher), phím tắt toàn cục,\n * và bọc sẵn `TooltipProvider`.\n *\n * KHÔNG dùng `useRouter()` của next/navigation: hook đó *ném lỗi* khi thiếu App Router\n * (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback\n * `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.\n */\nexport function ShellProvider({\n workspaces = [],\n currentWorkspaceId,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps = [],\n currentAppId,\n onAppSelect,\n onUpsell,\n nav,\n pathname: pathnameProp,\n searchParams: searchParamsProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n collapseSidebarOnTablet = true,\n persistSidebarState = true,\n enableShortcuts = true,\n labels: labelsProp,\n children\n}: ShellProviderProps) {\n // `usePathname()` trả null khi không có App Router → fallback chuỗi rỗng thay vì crash.\n const nextPathname = usePathname();\n const pathname = pathnameProp ?? nextPathname ?? \"\";\n\n // Chuẩn hoá MỘT lần ở đây thay vì mỗi lần `isNavItemActive` chạy: prop có thể là chuỗi\n // hoặc object, mà mỗi lần dựng `URLSearchParams` mới là một identity mới → mọi memo bên\n // dưới hỏng. Khoá memo là chính prop app truyền vào.\n const searchParams = useMemo(() => toSearchParams(searchParamsProp), [searchParamsProp]);\n\n const currentWorkspace = useMemo(\n () => workspaces.find((workspace) => workspace.id === currentWorkspaceId) ?? workspaces[0] ?? null,\n [workspaces, currentWorkspaceId]\n );\n\n const currentApp = useMemo(() => {\n if (currentAppId) return apps.find((app) => app.id === currentAppId) ?? null;\n return resolveCurrentApp(apps, pathname);\n }, [apps, currentAppId, pathname]);\n\n const navGroups = useMemo(() => toNavGroups(nav), [nav]);\n const labels = useMemo<ShellLabels>(() => ({ ...DEFAULT_SHELL_LABELS, ...labelsProp }), [labelsProp]);\n\n const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);\n const [appLauncherOpen, setAppLauncherOpen] = useState(false);\n\n /*\n * Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n *\n * Nếu CHƯA có lựa chọn nào của người dùng thì ở dải TABLET thanh bên mặc định thu gọn: từ 768px\n * thanh bên đã chiếm chỗ thật trong bố cục (dưới mức đó nó là sheet off-canvas), mà 768–1023px\n * thì 256px của nó ăn tới một phần ba bề ngang — vùng nội dung còn ~512px cho một bảng nhiều cột.\n *\n * ⚠ KHÔNG ghi lựa chọn này vào localStorage. Đây là GIÁ TRỊ MẶC ĐỊNH, không phải quyết định của\n * người dùng: ghi xuống là lần sau mở ở màn hình rộng thanh bên vẫn thu gọn vì \"đã có lựa chọn\n * lưu\", mà người dùng thì chưa bấm gì cả. Chỉ khi họ tự gạt thì `applySidebarCollapsed` mới lưu.\n *\n * ⚠ Chỉ chạy MỘT LẦN lúc mở, không theo dõi resize. \"Mặc định khi mới mở\" khác với \"ép theo bề\n * ngang\": kéo cửa sổ hẹp lại giữa phiên mà thanh bên tự sập là giật thứ người dùng đang cầm.\n *\n * Đọc bề ngang trong effect nên không đụng tới hydration — server không có `window`, và lần\n * render đầu ở client vẫn dùng đúng `defaultSidebarCollapsed` mà server đã render.\n */\n useEffect(() => {\n const stored = persistSidebarState ? readStoredCollapsed(currentWorkspace?.id) : null;\n if (stored !== null) {\n setSidebarCollapsed(stored);\n return;\n }\n if (collapseSidebarOnTablet && isTabletWidth()) setSidebarCollapsed(true);\n }, [persistSidebarState, currentWorkspace?.id, collapseSidebarOnTablet]);\n\n const persistCollapsed = useCallback(\n (collapsed: boolean) => {\n if (!persistSidebarState || typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(sidebarStorageKey(currentWorkspace?.id), collapsed ? \"1\" : \"0\");\n } catch {\n // Bỏ qua: không ghi được localStorage thì trạng thái chỉ sống trong phiên.\n }\n },\n [persistSidebarState, currentWorkspace?.id]\n );\n\n const applySidebarCollapsed = useCallback(\n (collapsed: boolean) => {\n setSidebarCollapsed(collapsed);\n persistCollapsed(collapsed);\n },\n [persistCollapsed]\n );\n\n const toggleSidebar = useCallback(() => {\n setSidebarCollapsed((prev) => {\n const next = !prev;\n persistCollapsed(next);\n return next;\n });\n }, [persistCollapsed]);\n\n // Phím tắt toàn cục (§7.4)\n useEffect(() => {\n if (!enableShortcuts) return;\n\n const entitledApps = getEntitledApps(apps);\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const target = event.target;\n // Đang gõ trong ô nhập → nhường phím cho ô nhập.\n const isTyping =\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable);\n\n if (event.key === \"Escape\") {\n setCommandPaletteOpen(false);\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n return;\n }\n\n if (isTyping) return;\n\n const mod = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n // Cmd/Ctrl + Shift + K — AppLauncher (phải xét TRƯỚC Cmd+K vì cùng phím \"k\")\n if (mod && event.shiftKey && key === \"k\") {\n event.preventDefault();\n setAppLauncherOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + K — Command Palette\n if (mod && !event.shiftKey && key === \"k\") {\n event.preventDefault();\n setCommandPaletteOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + B — thu gọn/mở sidebar\n if (mod && !event.shiftKey && key === \"b\") {\n event.preventDefault();\n toggleSidebar();\n return;\n }\n\n // Alt + 1…9 — nhảy tới app thứ n đã được cấp quyền.\n // Dùng `event.code` chứ không `event.key`: trên macOS Alt+1 cho ra \"¡\".\n if (event.altKey && !event.ctrlKey && !event.metaKey) {\n const match = /^Digit([1-9])$/.exec(event.code);\n if (match?.[1]) {\n const index = Number.parseInt(match[1], 10) - 1;\n const app = entitledApps[index];\n if (app) {\n event.preventDefault();\n onAppSelect?.(app);\n }\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [enableShortcuts, apps, onAppSelect, toggleSidebar]);\n\n // Đổi route → đóng mọi overlay đang mở.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chủ đích chạy theo MỌI lần đổi pathname\n useEffect(() => {\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n setCommandPaletteOpen(false);\n }, [pathname]);\n\n const value = useMemo<ShellContextValue>(\n () => ({\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n nav: navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n setSidebarCollapsed: applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n setMobileMenuOpen,\n commandPaletteOpen,\n setCommandPaletteOpen,\n appLauncherOpen,\n setAppLauncherOpen,\n labels\n }),\n [\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n commandPaletteOpen,\n appLauncherOpen,\n labels\n ]\n );\n\n return (\n <ShellContext.Provider value={value}>\n <TooltipProvider delayDuration={200}>{children}</TooltipProvider>\n </ShellContext.Provider>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/shell-context.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * ĐUÔI `.js` LÀ CHỦ ĐÍCH, ĐỪNG BỎ.\n *\n * Gói `next` KHÔNG khai `exports` map, nên `next/link` chỉ là đường dẫn file. Bundler\n * (webpack/Turbopack) tự thêm đuôi rồi trỏ đúng `node_modules/next/link.js`; Node ESM thuần thì\n * KHÔNG — nó ném `ERR_MODULE_NOT_FOUND: Cannot find module .../next/link` kèm gợi ý\n * \"Did you mean next/link.js?\". Hậu quả: `node -e \"await import('@comitor/ui/shell')\"` chết ngay\n * lúc nạp, tức là mọi smoke-test entry của CI đều mù với riêng entry shell.\n *\n * Ghi thẳng `next/link.js` / `next/navigation.js` là CÙNG MỘT file mà bundler vẫn resolve tới\n * (đã đối chiếu: `next` không có `resolve.alias` cho hai specifier này trong\n * `dist/build/create-compiler-aliases.js`), nhưng Node cũng nạp được.\n */\nimport NextLinkModule from \"next/link.js\";\nimport { usePathname } from \"next/navigation.js\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { layout } from \"../tokens.js\";\nimport type {\n AppDescriptor,\n NavGroup,\n ShellLabels,\n ShellLinkComponent,\n ShellLinkProps,\n ShellNav,\n ShellUser,\n Workspace\n} from \"./types.js\";\nimport { getEntitledApps, type NavSearchParams, resolveCurrentApp, toNavGroups, toSearchParams } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Chuẩn hoá interop của `next/link.js`.\n *\n * Hai môi trường trả về hai hình dạng khác nhau cho cùng một file CJS:\n * - bundler → đã interop sẵn, biến này CHÍNH LÀ component `Link`\n * - Node ESM → biến này là namespace `{ default: Link, useLinkStatus }`\n * `.default ?? chính nó` đúng ở cả hai (component `Link` không có thuộc tính `default`).\n */\nconst NextLink = ((NextLinkModule as unknown as { default?: ShellLinkComponent }).default ??\n NextLinkModule) as ShellLinkComponent;\n\n/**\n * Bọc `next/link` thay vì truyền thẳng: hình dạng props của NextLink rộng hơn\n * `ShellLinkProps` (href nhận cả UrlObject), bọc lại cho khớp type và để chỗ\n * thay adapter router khác về sau.\n */\nfunction DefaultShellLink({ href, children, ...props }: ShellLinkProps) {\n return (\n <NextLink href={href} {...props}>\n {children}\n </NextLink>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Context\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellContextValue {\n /* Trục 1 — workspace */\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /* Trục 2 — app */\n apps: AppDescriptor[];\n currentApp: AppDescriptor | null;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /* Điều hướng trong app hiện tại */\n nav: NavGroup[];\n pathname: string;\n /**\n * Query hiện tại đã chuẩn hoá, hoặc `null` khi app không truyền.\n * `null` KHÁC rỗng: rỗng nghĩa là \"URL không có query\", `null` nghĩa là \"shell không biết\"\n * — và active matching xử lý hai ca đó khác nhau (xem `isNavItemActive`).\n */\n searchParams: URLSearchParams | null;\n LinkComponent: ShellLinkComponent;\n onNavigate?: (href: string) => void;\n\n /* Người dùng */\n user: ShellUser | null;\n onLogout?: () => void;\n\n /* Trạng thái khung */\n sidebarCollapsed: boolean;\n setSidebarCollapsed: (collapsed: boolean) => void;\n toggleSidebar: () => void;\n mobileMenuOpen: boolean;\n setMobileMenuOpen: (open: boolean) => void;\n commandPaletteOpen: boolean;\n setCommandPaletteOpen: (open: boolean) => void;\n appLauncherOpen: boolean;\n setAppLauncherOpen: (open: boolean) => void;\n\n /* Chuỗi hiển thị (bảng `shell` của `ComitorLocaleProvider`, đã trộn override của app) */\n labels: ShellLabels;\n}\n\nconst ShellContext = createContext<ShellContextValue | undefined>(undefined);\n\n/** Truy cập state của shell — phải gọi bên trong `<ShellProvider>`. */\nexport function useShell(): ShellContextValue {\n const context = useContext(ShellContext);\n if (!context) {\n throw new Error(\"useShell phải được dùng bên trong <ShellProvider> (hoặc <AppShell>).\");\n }\n return context;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Ghi nhớ trạng thái thu gọn sidebar\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Khoá localStorage theo workspace — mỗi workspace nhớ riêng trạng thái sidebar\n * (spec §7.6: KHÔNG dùng cookie `sidebar_state` của shadcn để tránh hai nguồn sự thật).\n */\nfunction sidebarStorageKey(workspaceId: string | undefined): string {\n return `comitor-sidebar:${workspaceId ?? \"default\"}`;\n}\n\n/**\n * Đang ở dải TABLET? — từ `breakpoints.mobile` (768px, mốc thanh bên thôi off-canvas) tới ngay\n * dưới `breakpoints.tablet` (1024px).\n *\n * Hai mốc lấy từ `layout.breakpoints` của `@comitor/ui/tokens` chứ không gõ số ở đây: đổi mốc mà\n * quên chỗ này thì thanh bên tự sập ở một dải mà CSS không còn coi là tablet nữa.\n */\nfunction isTabletWidth(): boolean {\n if (typeof window === \"undefined\") return false;\n const { mobile, tablet } = layout.breakpoints;\n return window.matchMedia(`(min-width: ${mobile}px) and (max-width: ${tablet - 1}px)`).matches;\n}\n\nfunction readStoredCollapsed(workspaceId: string | undefined): boolean | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(sidebarStorageKey(workspaceId));\n if (raw === \"1\") return true;\n if (raw === \"0\") return false;\n return null;\n } catch {\n // Safari private mode / storage bị chặn — coi như chưa có giá trị lưu.\n return null;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Provider\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellProviderProps {\n /** Trục 1 — danh sách workspace của user. Rỗng/1 phần tử thì WorkspaceSwitcher tự ẩn danh sách. */\n workspaces?: Workspace[];\n /** Workspace đang mở; bỏ trống → lấy phần tử đầu. */\n currentWorkspaceId?: string;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /** Trục 2 — danh sách app của hệ sinh thái (kể cả app chưa mở khoá). */\n apps?: AppDescriptor[];\n /** App đang mở; bỏ trống → suy từ `pathname` (khớp prefix dài nhất). */\n currentAppId?: string;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /** Menu của app hiện tại — `NavItem[]` hoặc `NavGroup[]`. */\n nav?: ShellNav;\n /**\n * Ghi đè pathname (Storybook, app không-Next). Bỏ trống → dùng `usePathname()`\n * của next/navigation (trả `null` khi không có App Router → coi như \"\").\n */\n pathname?: string;\n /**\n * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query\n * (`/cai-dat?tab=chung` ⇄ `/cai-dat?tab=thanh-vien`). Bỏ trống thì mọi mục cùng đường dẫn\n * cùng sáng.\n *\n * Nhận `useSearchParams()` của Next, chuỗi `\"?tab=x\"`, hoặc object `searchParams` của\n * Server Component.\n *\n * ⚠ Shell KHÔNG tự gọi `useSearchParams()`: hook đó ép trang bọc `<Suspense>` và đẩy trang\n * sang render phía client khi build tĩnh — cái giá đó phải do app tự chọn, không phải do\n * việc lỡ import shell. Truyền vào từ một client component đã có sẵn boundary:\n *\n * ```tsx\n * \"use client\";\n * const searchParams = useSearchParams();\n * return <AppShell searchParams={searchParams} …>{children}</AppShell>;\n * ```\n */\n searchParams?: NavSearchParams | null;\n /**\n * Ở dải TABLET (768–1023px), khi người dùng CHƯA từng gạt thanh bên, mở app lần đầu thì thanh\n * bên thu gọn sẵn. Mặc định `true`.\n *\n * Đặt `false` khi app muốn thanh bên luôn mở rộng ở tablet — vd điều hướng là thứ chính của\n * màn hình đó. Lựa chọn ĐÃ LƯU của người dùng luôn thắng cờ này.\n */\n collapseSidebarOnTablet?: boolean;\n /** Component link thay `next/link` (app không-Next). */\n LinkComponent?: ShellLinkComponent;\n /** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */\n onNavigate?: (href: string) => void;\n\n user?: ShellUser | null;\n onLogout?: () => void;\n\n /** Trạng thái thu gọn ban đầu (SSR). Sau khi mount, localStorage của workspace thắng. */\n defaultSidebarCollapsed?: boolean;\n /** Tắt ghi nhớ localStorage (Storybook, chế độ kiosk). */\n persistSidebarState?: boolean;\n /** Tắt bộ phím tắt toàn cục. */\n enableShortcuts?: boolean;\n\n /**\n * Override từng chuỗi hiển thị. Không truyền thì theo namespace `shell` của\n * `ComitorLocaleProvider` gần nhất (không có provider → tiếng Anh).\n */\n labels?: Partial<ShellLabels>;\n\n children: ReactNode;\n}\n\n/**\n * Provider của app shell Comitor: giữ hai trục điều hướng (workspace × app),\n * trạng thái khung (sidebar/mobile/command palette/app launcher), phím tắt toàn cục,\n * và bọc sẵn `TooltipProvider`.\n *\n * KHÔNG dùng `useRouter()` của next/navigation: hook đó *ném lỗi* khi thiếu App Router\n * (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback\n * `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.\n */\nexport function ShellProvider({\n workspaces = [],\n currentWorkspaceId,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps = [],\n currentAppId,\n onAppSelect,\n onUpsell,\n nav,\n pathname: pathnameProp,\n searchParams: searchParamsProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n collapseSidebarOnTablet = true,\n persistSidebarState = true,\n enableShortcuts = true,\n labels: labelsProp,\n children\n}: ShellProviderProps) {\n // `usePathname()` trả null khi không có App Router → fallback chuỗi rỗng thay vì crash.\n const nextPathname = usePathname();\n const pathname = pathnameProp ?? nextPathname ?? \"\";\n\n // Chuẩn hoá MỘT lần ở đây thay vì mỗi lần `isNavItemActive` chạy: prop có thể là chuỗi\n // hoặc object, mà mỗi lần dựng `URLSearchParams` mới là một identity mới → mọi memo bên\n // dưới hỏng. Khoá memo là chính prop app truyền vào.\n const searchParams = useMemo(() => toSearchParams(searchParamsProp), [searchParamsProp]);\n\n const currentWorkspace = useMemo(\n () => workspaces.find((workspace) => workspace.id === currentWorkspaceId) ?? workspaces[0] ?? null,\n [workspaces, currentWorkspaceId]\n );\n\n const currentApp = useMemo(() => {\n if (currentAppId) return apps.find((app) => app.id === currentAppId) ?? null;\n return resolveCurrentApp(apps, pathname);\n }, [apps, currentAppId, pathname]);\n\n const navGroups = useMemo(() => toNavGroups(nav), [nav]);\n const labels = useComitorLabels(\"shell\", labelsProp);\n\n const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);\n const [appLauncherOpen, setAppLauncherOpen] = useState(false);\n\n /*\n * Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n *\n * Nếu CHƯA có lựa chọn nào của người dùng thì ở dải TABLET thanh bên mặc định thu gọn: từ 768px\n * thanh bên đã chiếm chỗ thật trong bố cục (dưới mức đó nó là sheet off-canvas), mà 768–1023px\n * thì 256px của nó ăn tới một phần ba bề ngang — vùng nội dung còn ~512px cho một bảng nhiều cột.\n *\n * ⚠ KHÔNG ghi lựa chọn này vào localStorage. Đây là GIÁ TRỊ MẶC ĐỊNH, không phải quyết định của\n * người dùng: ghi xuống là lần sau mở ở màn hình rộng thanh bên vẫn thu gọn vì \"đã có lựa chọn\n * lưu\", mà người dùng thì chưa bấm gì cả. Chỉ khi họ tự gạt thì `applySidebarCollapsed` mới lưu.\n *\n * ⚠ Chỉ chạy MỘT LẦN lúc mở, không theo dõi resize. \"Mặc định khi mới mở\" khác với \"ép theo bề\n * ngang\": kéo cửa sổ hẹp lại giữa phiên mà thanh bên tự sập là giật thứ người dùng đang cầm.\n *\n * Đọc bề ngang trong effect nên không đụng tới hydration — server không có `window`, và lần\n * render đầu ở client vẫn dùng đúng `defaultSidebarCollapsed` mà server đã render.\n */\n useEffect(() => {\n const stored = persistSidebarState ? readStoredCollapsed(currentWorkspace?.id) : null;\n if (stored !== null) {\n setSidebarCollapsed(stored);\n return;\n }\n if (collapseSidebarOnTablet && isTabletWidth()) setSidebarCollapsed(true);\n }, [persistSidebarState, currentWorkspace?.id, collapseSidebarOnTablet]);\n\n const persistCollapsed = useCallback(\n (collapsed: boolean) => {\n if (!persistSidebarState || typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(sidebarStorageKey(currentWorkspace?.id), collapsed ? \"1\" : \"0\");\n } catch {\n // Bỏ qua: không ghi được localStorage thì trạng thái chỉ sống trong phiên.\n }\n },\n [persistSidebarState, currentWorkspace?.id]\n );\n\n const applySidebarCollapsed = useCallback(\n (collapsed: boolean) => {\n setSidebarCollapsed(collapsed);\n persistCollapsed(collapsed);\n },\n [persistCollapsed]\n );\n\n const toggleSidebar = useCallback(() => {\n setSidebarCollapsed((prev) => {\n const next = !prev;\n persistCollapsed(next);\n return next;\n });\n }, [persistCollapsed]);\n\n // Phím tắt toàn cục (§7.4)\n useEffect(() => {\n if (!enableShortcuts) return;\n\n const entitledApps = getEntitledApps(apps);\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const target = event.target;\n // Đang gõ trong ô nhập → nhường phím cho ô nhập.\n const isTyping =\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable);\n\n if (event.key === \"Escape\") {\n setCommandPaletteOpen(false);\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n return;\n }\n\n if (isTyping) return;\n\n const mod = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n // Cmd/Ctrl + Shift + K — AppLauncher (phải xét TRƯỚC Cmd+K vì cùng phím \"k\")\n if (mod && event.shiftKey && key === \"k\") {\n event.preventDefault();\n setAppLauncherOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + K — Command Palette\n if (mod && !event.shiftKey && key === \"k\") {\n event.preventDefault();\n setCommandPaletteOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + B — thu gọn/mở sidebar\n if (mod && !event.shiftKey && key === \"b\") {\n event.preventDefault();\n toggleSidebar();\n return;\n }\n\n // Alt + 1…9 — nhảy tới app thứ n đã được cấp quyền.\n // Dùng `event.code` chứ không `event.key`: trên macOS Alt+1 cho ra \"¡\".\n if (event.altKey && !event.ctrlKey && !event.metaKey) {\n const match = /^Digit([1-9])$/.exec(event.code);\n if (match?.[1]) {\n const index = Number.parseInt(match[1], 10) - 1;\n const app = entitledApps[index];\n if (app) {\n event.preventDefault();\n onAppSelect?.(app);\n }\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [enableShortcuts, apps, onAppSelect, toggleSidebar]);\n\n // Đổi route → đóng mọi overlay đang mở.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chủ đích chạy theo MỌI lần đổi pathname\n useEffect(() => {\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n setCommandPaletteOpen(false);\n }, [pathname]);\n\n const value = useMemo<ShellContextValue>(\n () => ({\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n nav: navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n setSidebarCollapsed: applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n setMobileMenuOpen,\n commandPaletteOpen,\n setCommandPaletteOpen,\n appLauncherOpen,\n setAppLauncherOpen,\n labels\n }),\n [\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n commandPaletteOpen,\n appLauncherOpen,\n labels\n ]\n );\n\n return (\n <ShellContext.Provider value={value}>\n <TooltipProvider delayDuration={200}>{children}</TooltipProvider>\n </ShellContext.Provider>\n );\n}\n"],"mappings":"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","names":[]}
@@ -5,7 +5,7 @@ interface SidebarProps {
5
5
  className?: string;
6
6
  /** Slot ngay dưới WorkspaceSwitcher (vd nút "Tạo mới", ô tìm nhanh). */
7
7
  header?: ReactNode;
8
- /** Slot đáy sidebar (dung lượng gói, "Mời thành viên", phiên bản app…). */
8
+ /** Slot đáy sidebar (dung lượng gói, nút mời thành viên, phiên bản app…). */
9
9
  footer?: ReactNode;
10
10
  /** Ẩn WorkspaceSwitcher ở đỉnh — dùng khi app đặt bộ chuyển workspace ở header. */
11
11
  showWorkspaceSwitcher?: boolean;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/sidebar.tsx"],"sourcesContent":["\"use client\";\n\nimport { PanelLeftClose, PanelLeftOpen } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface SidebarProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher (vd nút \"Tạo mới\", ô tìm nhanh). */\n header?: ReactNode;\n /** Slot đáy sidebar (dung lượng gói, \"Mời thành viên\", phiên bản app…). */\n footer?: ReactNode;\n /** Ẩn WorkspaceSwitcher ở đỉnh — dùng khi app đặt bộ chuyển workspace ở header. */\n showWorkspaceSwitcher?: boolean;\n /** Ẩn nút thu gọn ở đáy (khi header đã có nút này). */\n showCollapseButton?: boolean;\n}\n\n/**\n * Sidebar desktop: 256px (`--sidebar-width`) / 56px khi thu gọn (`--sidebar-width-icon`).\n * Đỉnh: `WorkspaceSwitcher` (trục 1). Giữa: menu của app hiện tại. Đáy: slot footer.\n *\n * Ẩn hẳn dưới `md` — màn hình hẹp dùng `MobileMenu`.\n *\n * KHÔNG dùng `ui/sidebar.tsx` của shadcn (spec §7.6): primitive đó quản state bằng\n * cookie + provider riêng, sẽ thành nguồn sự thật thứ hai bên cạnh `ShellProvider`.\n */\nexport function Sidebar({\n className,\n header,\n footer,\n showWorkspaceSwitcher = true,\n showCollapseButton = true\n}: SidebarProps) {\n const { nav, currentApp, workspaces, sidebarCollapsed, toggleSidebar, labels } = useShell();\n\n // Đối chiếu khớp chính xác trên TOÀN BỘ mục của app, không chỉ mục cùng nhóm:\n // \"/tasks\" nằm nhóm A và \"/tasks/inbox\" nhóm B thì vẫn phải sáng đúng một mục.\n const allItems = flattenNavItems(nav);\n\n return (\n <aside\n data-collapsed={sidebarCollapsed || undefined}\n style={appAccentStyle(currentApp)}\n className={cn(\n \"hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex\",\n \"transition-[width] duration-200 ease-out\",\n sidebarCollapsed ? \"w-sidebar-icon\" : \"w-sidebar\",\n className\n )}\n >\n {/* Không có workspace nào (app đơn tổ chức) thì bỏ hẳn dải đỉnh, không để lại ô trống. */}\n {showWorkspaceSwitcher && workspaces.length > 0 ? (\n <div\n className={cn(\n \"flex h-header shrink-0 items-center border-b border-sidebar-border\",\n sidebarCollapsed ? \"justify-center px-1\" : \"px-2\"\n )}\n >\n <WorkspaceSwitcher collapsed={sidebarCollapsed} />\n </div>\n ) : null}\n\n {header ? <div className={cn(\"shrink-0\", sidebarCollapsed ? \"px-1.5 pt-2\" : \"px-2 pt-2\")}>{header}</div> : null}\n\n <nav\n aria-label={labels.navigationLabel}\n className={cn(\"flex-1 overflow-y-auto scrollbar-thin py-3\", sidebarCollapsed ? \"px-1.5\" : \"px-2\")}\n >\n <div className={cn(\"flex flex-col\", sidebarCollapsed ? \"gap-2\" : \"gap-4\")}>\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} collapsed={sidebarCollapsed} siblings={allItems} />\n ))}\n </div>\n </nav>\n\n {footer || showCollapseButton ? (\n <div\n className={cn(\n \"shrink-0 border-t border-sidebar-border\",\n sidebarCollapsed ? \"flex flex-col items-center gap-1 p-1.5\" : \"p-2\"\n )}\n >\n {!sidebarCollapsed && footer ? <div className=\"mb-1\">{footer}</div> : null}\n\n {showCollapseButton ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n /*\n * ⚠ Màu của NÚT giữ nguyên `/60`, còn ICON được tô riêng bằng token ĐẶC. Hai\n * thứ khác nhau ở đây là CỐ Ý, và đây là chỗ duy nhất trong shell phải tách:\n *\n * · nút này chứa CẢ CHỮ khi mở rộng (`text-xs`, nhãn \"Thu gọn\"), nên màu của nó\n * chịu ngưỡng VĂN BẢN 4,5:1. `/60` phẳng ra `#6C6E71` = **4,90:1** ✓, còn\n * `--muted-foreground` chỉ **3,67:1** ✗ — đổi màu cả nút là làm rớt AA đúng\n * cái nhãn đang đạt.\n * · nhưng `/60` là màu có ALPHA, mà icon lấy màu qua `currentColor` — nét cắt\n * nhau sẽ bị tô hai lần và lộ vệt nối (xem `docs/hop-dong-mau.md` §7b, ca KẾ\n * THỪA). Icon chỉ chịu ngưỡng 3:1 nên `--muted-foreground` đủ cho nó.\n *\n * `group` + `group-hover:` là cái giá của việc tách: icon có màu riêng thì\n * `hover:text-sidebar-foreground` của nút không với tới nó nữa.\n */\n className={cn(\n \"group flex items-center gap-2 rounded-md text-sidebar-foreground/60\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent hover:text-sidebar-foreground\",\n sidebarCollapsed ? \"size-9 justify-center\" : \"h-8 w-full px-2 text-xs\"\n )}\n >\n {sidebarCollapsed ? (\n <PanelLeftOpen className=\"size-4 text-muted-foreground group-hover:text-sidebar-foreground\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4 text-muted-foreground group-hover:text-sidebar-foreground\" />\n {/* Chữ NHÌN THẤY dùng bản ngắn; `aria-label` và tooltip ở trên vẫn giữ bản\n dài. Xem hợp đồng của `collapseSidebarShort` trong types.ts. */}\n <span className=\"truncate\">{labels.collapseSidebarShort}</span>\n </>\n )}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"right\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · Ctrl/⌘ + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n ) : null}\n </aside>\n );\n}\n"],"mappings":";AAgEU,SAyDU,UAzDV,KAyDU,YAzDV;AA9DV,SAAS,gBAAgB,qBAAqB;AAE9C,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAuB3B,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,wBAAwB;AAAA,EACxB,qBAAqB;AACvB,GAAiB;AACf,QAAM,EAAE,KAAK,YAAY,YAAY,kBAAkB,eAAe,OAAO,IAAI,SAAS;AAI1F,QAAM,WAAW,gBAAgB,GAAG;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,kBAAgB,oBAAoB;AAAA,MACpC,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,mBAAmB,mBAAmB;AAAA,QACtC;AAAA,MACF;AAAA,MAGC;AAAA,iCAAyB,WAAW,SAAS,IAC5C;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,wBAAwB;AAAA,YAC7C;AAAA,YAEA,8BAAC,qBAAkB,WAAW,kBAAkB;AAAA;AAAA,QAClD,IACE;AAAA,QAEH,SAAS,oBAAC,SAAI,WAAW,GAAG,YAAY,mBAAmB,gBAAgB,WAAW,GAAI,kBAAO,IAAS;AAAA,QAE3G;AAAA,UAAC;AAAA;AAAA,YACC,cAAY,OAAO;AAAA,YACnB,WAAW,GAAG,8CAA8C,mBAAmB,WAAW,MAAM;AAAA,YAEhG,8BAAC,SAAI,WAAW,GAAG,iBAAiB,mBAAmB,UAAU,OAAO,GACrE,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,WAAW,kBAAkB,UAAU,YAA/D,MAAM,EAAmE,CAC7F,GACH;AAAA;AAAA,QACF;AAAA,QAEC,UAAU,qBACT;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,2CAA2C;AAAA,YAChE;AAAA,YAEC;AAAA,eAAC,oBAAoB,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,cAErE,qBACC,qBAAC,WACC;AAAA,oCAAC,kBAAe,SAAO,MACrB;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS;AAAA,oBACT,cAAY,mBAAmB,OAAO,gBAAgB,OAAO;AAAA,oBAgB7D,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA,mBAAmB,0BAA0B;AAAA,oBAC/C;AAAA,oBAEC,6BACC,oBAAC,iBAAc,WAAU,oEAAmE,IAE5F,iCACE;AAAA,0CAAC,kBAAe,WAAU,oEAAmE;AAAA,sBAG7F,oBAAC,UAAK,WAAU,YAAY,iBAAO,sBAAqB;AAAA,uBAC1D;AAAA;AAAA,gBAEJ,GACF;AAAA,gBACA,qBAAC,kBAAe,MAAK,SAClB;AAAA,qCAAmB,OAAO,gBAAgB,OAAO;AAAA,kBAAgB;AAAA,mBACpE;AAAA,iBACF,IACE;AAAA;AAAA;AAAA,QACN,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/sidebar.tsx"],"sourcesContent":["\"use client\";\n\nimport { PanelLeftClose, PanelLeftOpen } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface SidebarProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher (vd nút \"Tạo mới\", ô tìm nhanh). */\n header?: ReactNode;\n /** Slot đáy sidebar (dung lượng gói, nút mời thành viên, phiên bản app…). */\n footer?: ReactNode;\n /** Ẩn WorkspaceSwitcher ở đỉnh — dùng khi app đặt bộ chuyển workspace ở header. */\n showWorkspaceSwitcher?: boolean;\n /** Ẩn nút thu gọn ở đáy (khi header đã có nút này). */\n showCollapseButton?: boolean;\n}\n\n/**\n * Sidebar desktop: 256px (`--sidebar-width`) / 56px khi thu gọn (`--sidebar-width-icon`).\n * Đỉnh: `WorkspaceSwitcher` (trục 1). Giữa: menu của app hiện tại. Đáy: slot footer.\n *\n * Ẩn hẳn dưới `md` — màn hình hẹp dùng `MobileMenu`.\n *\n * KHÔNG dùng `ui/sidebar.tsx` của shadcn (spec §7.6): primitive đó quản state bằng\n * cookie + provider riêng, sẽ thành nguồn sự thật thứ hai bên cạnh `ShellProvider`.\n */\nexport function Sidebar({\n className,\n header,\n footer,\n showWorkspaceSwitcher = true,\n showCollapseButton = true\n}: SidebarProps) {\n const { nav, currentApp, workspaces, sidebarCollapsed, toggleSidebar, labels } = useShell();\n\n // Đối chiếu khớp chính xác trên TOÀN BỘ mục của app, không chỉ mục cùng nhóm:\n // \"/tasks\" nằm nhóm A và \"/tasks/inbox\" nhóm B thì vẫn phải sáng đúng một mục.\n const allItems = flattenNavItems(nav);\n\n return (\n <aside\n data-collapsed={sidebarCollapsed || undefined}\n style={appAccentStyle(currentApp)}\n className={cn(\n \"hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex\",\n \"transition-[width] duration-200 ease-out\",\n sidebarCollapsed ? \"w-sidebar-icon\" : \"w-sidebar\",\n className\n )}\n >\n {/* Không có workspace nào (app đơn tổ chức) thì bỏ hẳn dải đỉnh, không để lại ô trống. */}\n {showWorkspaceSwitcher && workspaces.length > 0 ? (\n <div\n className={cn(\n \"flex h-header shrink-0 items-center border-b border-sidebar-border\",\n sidebarCollapsed ? \"justify-center px-1\" : \"px-2\"\n )}\n >\n <WorkspaceSwitcher collapsed={sidebarCollapsed} />\n </div>\n ) : null}\n\n {header ? <div className={cn(\"shrink-0\", sidebarCollapsed ? \"px-1.5 pt-2\" : \"px-2 pt-2\")}>{header}</div> : null}\n\n <nav\n aria-label={labels.navigationLabel}\n className={cn(\"flex-1 overflow-y-auto scrollbar-thin py-3\", sidebarCollapsed ? \"px-1.5\" : \"px-2\")}\n >\n <div className={cn(\"flex flex-col\", sidebarCollapsed ? \"gap-2\" : \"gap-4\")}>\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} collapsed={sidebarCollapsed} siblings={allItems} />\n ))}\n </div>\n </nav>\n\n {footer || showCollapseButton ? (\n <div\n className={cn(\n \"shrink-0 border-t border-sidebar-border\",\n sidebarCollapsed ? \"flex flex-col items-center gap-1 p-1.5\" : \"p-2\"\n )}\n >\n {!sidebarCollapsed && footer ? <div className=\"mb-1\">{footer}</div> : null}\n\n {showCollapseButton ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n /*\n * ⚠ Màu của NÚT giữ nguyên `/60`, còn ICON được tô riêng bằng token ĐẶC. Hai\n * thứ khác nhau ở đây là CỐ Ý, và đây là chỗ duy nhất trong shell phải tách:\n *\n * · nút này chứa CẢ CHỮ khi mở rộng (`text-xs`, nhãn `collapseSidebarShort`),\n * nên màu của nó chịu ngưỡng VĂN BẢN 4,5:1. `/60` phẳng ra `#6C6E71` =\n * **4,90:1** ✓, còn `--muted-foreground` chỉ **3,67:1** ✗ — đổi màu cả nút là\n * làm rớt AA đúng cái nhãn đang đạt.\n * · nhưng `/60` là màu có ALPHA, mà icon lấy màu qua `currentColor` — nét cắt\n * nhau sẽ bị tô hai lần và lộ vệt nối (xem `docs/hop-dong-mau.md` §7b, ca KẾ\n * THỪA). Icon chỉ chịu ngưỡng 3:1 nên `--muted-foreground` đủ cho nó.\n *\n * `group` + `group-hover:` là cái giá của việc tách: icon có màu riêng thì\n * `hover:text-sidebar-foreground` của nút không với tới nó nữa.\n */\n className={cn(\n \"group flex items-center gap-2 rounded-md text-sidebar-foreground/60\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent hover:text-sidebar-foreground\",\n sidebarCollapsed ? \"size-9 justify-center\" : \"h-8 w-full px-2 text-xs\"\n )}\n >\n {sidebarCollapsed ? (\n <PanelLeftOpen className=\"size-4 text-muted-foreground group-hover:text-sidebar-foreground\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4 text-muted-foreground group-hover:text-sidebar-foreground\" />\n {/* Chữ NHÌN THẤY dùng bản ngắn; `aria-label` và tooltip ở trên vẫn giữ bản\n dài. Xem hợp đồng của `collapseSidebarShort` trong types.ts. */}\n <span className=\"truncate\">{labels.collapseSidebarShort}</span>\n </>\n )}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"right\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · Ctrl/⌘ + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n ) : null}\n </aside>\n );\n}\n"],"mappings":";AAgEU,SAyDU,UAzDV,KAyDU,YAzDV;AA9DV,SAAS,gBAAgB,qBAAqB;AAE9C,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAuB3B,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,wBAAwB;AAAA,EACxB,qBAAqB;AACvB,GAAiB;AACf,QAAM,EAAE,KAAK,YAAY,YAAY,kBAAkB,eAAe,OAAO,IAAI,SAAS;AAI1F,QAAM,WAAW,gBAAgB,GAAG;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,kBAAgB,oBAAoB;AAAA,MACpC,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,mBAAmB,mBAAmB;AAAA,QACtC;AAAA,MACF;AAAA,MAGC;AAAA,iCAAyB,WAAW,SAAS,IAC5C;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,wBAAwB;AAAA,YAC7C;AAAA,YAEA,8BAAC,qBAAkB,WAAW,kBAAkB;AAAA;AAAA,QAClD,IACE;AAAA,QAEH,SAAS,oBAAC,SAAI,WAAW,GAAG,YAAY,mBAAmB,gBAAgB,WAAW,GAAI,kBAAO,IAAS;AAAA,QAE3G;AAAA,UAAC;AAAA;AAAA,YACC,cAAY,OAAO;AAAA,YACnB,WAAW,GAAG,8CAA8C,mBAAmB,WAAW,MAAM;AAAA,YAEhG,8BAAC,SAAI,WAAW,GAAG,iBAAiB,mBAAmB,UAAU,OAAO,GACrE,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,WAAW,kBAAkB,UAAU,YAA/D,MAAM,EAAmE,CAC7F,GACH;AAAA;AAAA,QACF;AAAA,QAEC,UAAU,qBACT;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,2CAA2C;AAAA,YAChE;AAAA,YAEC;AAAA,eAAC,oBAAoB,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,cAErE,qBACC,qBAAC,WACC;AAAA,oCAAC,kBAAe,SAAO,MACrB;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS;AAAA,oBACT,cAAY,mBAAmB,OAAO,gBAAgB,OAAO;AAAA,oBAgB7D,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA,mBAAmB,0BAA0B;AAAA,oBAC/C;AAAA,oBAEC,6BACC,oBAAC,iBAAc,WAAU,oEAAmE,IAE5F,iCACE;AAAA,0CAAC,kBAAe,WAAU,oEAAmE;AAAA,sBAG7F,oBAAC,UAAK,WAAU,YAAY,iBAAO,sBAAqB;AAAA,uBAC1D;AAAA;AAAA,gBAEJ,GACF;AAAA,gBACA,qBAAC,kBAAe,MAAK,SAClB;AAAA,qCAAmB,OAAO,gBAAgB,OAAO;AAAA,kBAAgB;AAAA,mBACpE;AAAA,iBACF,IACE;AAAA;AAAA;AAAA,QACN,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -16,11 +16,14 @@ type ThemeChoice = "light" | "dark" | "system";
16
16
  interface ThemeToggleProps {
17
17
  className?: string;
18
18
  /**
19
- * "icon" — một nút, bấm là lật Sáng ⇄ Tối (mặc định, dùng ở header).
20
- * "menu" — dropdown 3 lựa chọn Sáng / Tối / Hệ thống.
19
+ * "icon" — một nút, bấm là lật sáng ⇄ tối (mặc định, dùng ở header).
20
+ * "menu" — dropdown 3 lựa chọn `themeLight` / `themeDark` / `themeSystem`.
21
21
  */
22
22
  variant?: "icon" | "menu";
23
- /** Override chuỗi hiển thị (mặc định tiếng Việt). `AppHeader` truyền labels của shell xuống. */
23
+ /**
24
+ * Override chuỗi hiển thị. Không truyền thì theo namespace `shell` của `ComitorLocaleProvider`
25
+ * (không có provider → tiếng Anh). `AppHeader` truyền labels của shell xuống.
26
+ */
24
27
  labels?: Partial<ThemeToggleLabels>;
25
28
  /**
26
29
  * Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.
@@ -10,15 +10,15 @@ import {
10
10
  DropdownMenuTrigger
11
11
  } from "../components/ui/dropdown-menu.js";
12
12
  import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
13
+ import { useComitorLabels } from "../i18n/locale-provider.js";
13
14
  import { cn } from "../lib/cn.js";
14
- import { DEFAULT_SHELL_LABELS } from "./types.js";
15
15
  function ThemeToggle({ className, variant = "icon", labels: labelsProp, onThemeChange }) {
16
16
  const { theme, resolvedTheme, setTheme } = useTheme();
17
+ const labels = useComitorLabels("shell", labelsProp);
17
18
  const choose = (next) => {
18
19
  setTheme(next);
19
20
  onThemeChange?.(next);
20
21
  };
21
- const labels = { ...DEFAULT_SHELL_LABELS, ...labelsProp };
22
22
  const [mounted, setMounted] = useState(false);
23
23
  useEffect(() => {
24
24
  setMounted(true);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/theme-toggle.tsx"],"sourcesContent":["\"use client\";\n\nimport { Monitor, Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { DEFAULT_SHELL_LABELS, type ShellLabels } from \"./types.js\";\n\n/** Chỉ những nhãn ThemeToggle thực sự dùng — để nút này KHÔNG buộc phải nằm trong ShellProvider. */\nexport type ThemeToggleLabels = Pick<ShellLabels, \"themeLight\" | \"themeDark\" | \"themeSystem\" | \"toggleTheme\">;\n\n/**\n * Ba lựa chọn mà `ThemeToggle` ghi ra — tập ĐÓNG, và `\"system\"` là một giá trị thật chứ không phải\n * \"chưa chọn\".\n *\n * ⚠ App nào lưu giá trị này (ví dụ làm mặc định theo tài khoản) phải chấp nhận đủ CẢ BA. Ép nó về\n * hai trạng thái sáng/tối là làm mất lựa chọn \"đi theo hệ điều hành\" — thứ mà người bật chế độ tối\n * theo giờ trong ngày sẽ mất ngay khi họ đăng nhập ở một máy khác.\n */\nexport type ThemeChoice = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeToggleProps {\n className?: string;\n /**\n * \"icon\" — một nút, bấm là lật Sáng ⇄ Tối (mặc định, dùng ở header).\n * \"menu\" — dropdown 3 lựa chọn Sáng / Tối / Hệ thống.\n */\n variant?: \"icon\" | \"menu\";\n /** Override chuỗi hiển thị (mặc định tiếng Việt). `AppHeader` truyền labels của shell xuống. */\n labels?: Partial<ThemeToggleLabels>;\n /**\n * Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.\n *\n * ⚠ Callback THÔNG BÁO, không phải `onChange` của component controlled: theme CÓ HIỆU LỰC vẫn do\n * `next-themes` giữ trong `localStorage`. Prop này để app ghi một MẶC ĐỊNH theo tài khoản ở nơi\n * khác. Xem `DensityToggleProps.onDensityChange` cho lý do đầy đủ.\n *\n * ⚠ Nhận đủ ba giá trị kể cả `\"system\"` — và ở `variant=\"icon\"` thì nó KHÔNG BAO GIỜ phát\n * `\"system\"`, vì nút ấy chỉ lật sáng ⇄ tối. Muốn người dùng chọn được \"hệ thống\" thì phải dùng\n * `variant=\"menu\"`.\n */\n onThemeChange?: (theme: ThemeChoice) => void;\n}\n\n/**\n * Nút đổi giao diện — dùng được ĐỘC LẬP (trang đăng nhập, trang marketing),\n * không bắt buộc nằm trong `ShellProvider`.\n *\n * Chống lệch hydration bằng cờ `mounted`: theme thật chỉ biết được ở client\n * (đọc localStorage/`prefers-color-scheme`), render thẳng icon theo theme ngay\n * lần đầu sẽ lệch với HTML do server sinh ra. Trước khi mount hiện ô rỗng cùng\n * kích thước để layout không nhảy.\n */\nexport function ThemeToggle({ className, variant = \"icon\", labels: labelsProp, onThemeChange }: ThemeToggleProps) {\n const { theme, resolvedTheme, setTheme } = useTheme();\n\n /*\n * Gói hai lời gọi lại làm một: bốn chỗ đổi theme trong file này (ba mục menu + nút lật) đều phải\n * báo, và một chỗ quên báo là một đường mà app không bao giờ ghi được mặc định — thứ chỉ lộ ra\n * khi người dùng đăng nhập ở máy khác.\n */\n const choose = (next: ThemeChoice) => {\n setTheme(next);\n onThemeChange?.(next);\n };\n const labels: ThemeToggleLabels = { ...DEFAULT_SHELL_LABELS, ...labelsProp };\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n }, []);\n\n const buttonClass = cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n className\n );\n\n if (!mounted) {\n return <span className={buttonClass} aria-hidden />;\n }\n\n if (variant === \"menu\") {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button type=\"button\" className={buttonClass} aria-label={labels.toggleTheme}>\n {resolvedTheme === \"dark\" ? <Moon className=\"size-4\" /> : <Sun className=\"size-4\" />}\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => choose(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => choose(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => choose(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n const nextTheme: ThemeChoice = resolvedTheme === \"dark\" ? \"light\" : \"dark\";\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button type=\"button\" onClick={() => choose(nextTheme)} className={buttonClass} aria-label={labels.toggleTheme}>\n {resolvedTheme === \"dark\" ? <Sun className=\"size-4\" /> : <Moon className=\"size-4\" />}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">{nextTheme === \"dark\" ? labels.themeDark : labels.themeLight}</TooltipContent>\n </Tooltip>\n );\n}\n"],"mappings":";AAsFW,cAYD,YAZC;AApFX,SAAS,SAAS,MAAM,WAAW;AACnC,SAAS,gBAAgB;AACzB,SAAS,WAAW,gBAAgB;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,4BAA8C;AA+ChD,SAAS,YAAY,EAAE,WAAW,UAAU,QAAQ,QAAQ,YAAY,cAAc,GAAqB;AAChH,QAAM,EAAE,OAAO,eAAe,SAAS,IAAI,SAAS;AAOpD,QAAM,SAAS,CAAC,SAAsB;AACpC,aAAS,IAAI;AACb,oBAAgB,IAAI;AAAA,EACtB;AACA,QAAM,SAA4B,EAAE,GAAG,sBAAsB,GAAG,WAAW;AAC3E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,CAAC,SAAS;AACZ,WAAO,oBAAC,UAAK,WAAW,aAAa,eAAW,MAAC;AAAA,EACnD;AAEA,MAAI,YAAY,QAAQ;AACtB,WACE,qBAAC,gBACC;AAAA,0BAAC,uBAAoB,SAAO,MAC1B,8BAAC,YAAO,MAAK,UAAS,WAAW,aAAa,cAAY,OAAO,aAC9D,4BAAkB,SAAS,oBAAC,QAAK,WAAU,UAAS,IAAK,oBAAC,OAAI,WAAU,UAAS,GACpF,GACF;AAAA,MACA,qBAAC,uBAAoB,OAAM,OACzB;AAAA,6BAAC,oBAAiB,SAAS,MAAM,OAAO,OAAO,GAAG,WAAW,GAAG,UAAU,WAAW,WAAW,GAC9F;AAAA,8BAAC,OAAI,WAAU,UAAS;AAAA,UACvB,OAAO;AAAA,WACV;AAAA,QACA,qBAAC,oBAAiB,SAAS,MAAM,OAAO,MAAM,GAAG,WAAW,GAAG,UAAU,UAAU,WAAW,GAC5F;AAAA,8BAAC,QAAK,WAAU,UAAS;AAAA,UACxB,OAAO;AAAA,WACV;AAAA,QACA,qBAAC,oBAAiB,SAAS,MAAM,OAAO,QAAQ,GAAG,WAAW,GAAG,UAAU,YAAY,WAAW,GAChG;AAAA,8BAAC,WAAQ,WAAU,UAAS;AAAA,UAC3B,OAAO;AAAA,WACV;AAAA,SACF;AAAA,OACF;AAAA,EAEJ;AAEA,QAAM,YAAyB,kBAAkB,SAAS,UAAU;AAEpE,SACE,qBAAC,WACC;AAAA,wBAAC,kBAAe,SAAO,MACrB,8BAAC,YAAO,MAAK,UAAS,SAAS,MAAM,OAAO,SAAS,GAAG,WAAW,aAAa,cAAY,OAAO,aAChG,4BAAkB,SAAS,oBAAC,OAAI,WAAU,UAAS,IAAK,oBAAC,QAAK,WAAU,UAAS,GACpF,GACF;AAAA,IACA,oBAAC,kBAAe,MAAK,UAAU,wBAAc,SAAS,OAAO,YAAY,OAAO,YAAW;AAAA,KAC7F;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/theme-toggle.tsx"],"sourcesContent":["\"use client\";\n\nimport { Monitor, Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport type { ShellLabels } from \"./types.js\";\n\n/** Chỉ những nhãn ThemeToggle thực sự dùng — để nút này KHÔNG buộc phải nằm trong ShellProvider. */\nexport type ThemeToggleLabels = Pick<ShellLabels, \"themeLight\" | \"themeDark\" | \"themeSystem\" | \"toggleTheme\">;\n\n/**\n * Ba lựa chọn mà `ThemeToggle` ghi ra — tập ĐÓNG, và `\"system\"` là một giá trị thật chứ không phải\n * \"chưa chọn\".\n *\n * ⚠ App nào lưu giá trị này (ví dụ làm mặc định theo tài khoản) phải chấp nhận đủ CẢ BA. Ép nó về\n * hai trạng thái sáng/tối là làm mất lựa chọn \"đi theo hệ điều hành\" — thứ mà người bật chế độ tối\n * theo giờ trong ngày sẽ mất ngay khi họ đăng nhập ở một máy khác.\n */\nexport type ThemeChoice = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeToggleProps {\n className?: string;\n /**\n * \"icon\" — một nút, bấm là lật sáng ⇄ tối (mặc định, dùng ở header).\n * \"menu\" — dropdown 3 lựa chọn `themeLight` / `themeDark` / `themeSystem`.\n */\n variant?: \"icon\" | \"menu\";\n /**\n * Override chuỗi hiển thị. Không truyền thì theo namespace `shell` của `ComitorLocaleProvider`\n * (không có provider → tiếng Anh). `AppHeader` truyền labels của shell xuống.\n */\n labels?: Partial<ThemeToggleLabels>;\n /**\n * Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.\n *\n * ⚠ Callback THÔNG BÁO, không phải `onChange` của component controlled: theme CÓ HIỆU LỰC vẫn do\n * `next-themes` giữ trong `localStorage`. Prop này để app ghi một MẶC ĐỊNH theo tài khoản ở nơi\n * khác. Xem `DensityToggleProps.onDensityChange` cho lý do đầy đủ.\n *\n * ⚠ Nhận đủ ba giá trị kể cả `\"system\"` — và ở `variant=\"icon\"` thì nó KHÔNG BAO GIỜ phát\n * `\"system\"`, vì nút ấy chỉ lật sáng ⇄ tối. Muốn người dùng chọn được \"hệ thống\" thì phải dùng\n * `variant=\"menu\"`.\n */\n onThemeChange?: (theme: ThemeChoice) => void;\n}\n\n/**\n * Nút đổi giao diện — dùng được ĐỘC LẬP (trang đăng nhập, trang marketing),\n * không bắt buộc nằm trong `ShellProvider`.\n *\n * Chống lệch hydration bằng cờ `mounted`: theme thật chỉ biết được ở client\n * (đọc localStorage/`prefers-color-scheme`), render thẳng icon theo theme ngay\n * lần đầu sẽ lệch với HTML do server sinh ra. Trước khi mount hiện ô rỗng cùng\n * kích thước để layout không nhảy.\n */\nexport function ThemeToggle({ className, variant = \"icon\", labels: labelsProp, onThemeChange }: ThemeToggleProps) {\n const { theme, resolvedTheme, setTheme } = useTheme();\n const labels: ThemeToggleLabels = useComitorLabels(\"shell\", labelsProp);\n\n /*\n * Gói hai lời gọi lại làm một: bốn chỗ đổi theme trong file này (ba mục menu + nút lật) đều phải\n * báo, và một chỗ quên báo là một đường mà app không bao giờ ghi được mặc định — thứ chỉ lộ ra\n * khi người dùng đăng nhập ở máy khác.\n */\n const choose = (next: ThemeChoice) => {\n setTheme(next);\n onThemeChange?.(next);\n };\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n }, []);\n\n const buttonClass = cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n className\n );\n\n if (!mounted) {\n return <span className={buttonClass} aria-hidden />;\n }\n\n if (variant === \"menu\") {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button type=\"button\" className={buttonClass} aria-label={labels.toggleTheme}>\n {resolvedTheme === \"dark\" ? <Moon className=\"size-4\" /> : <Sun className=\"size-4\" />}\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => choose(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => choose(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => choose(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n const nextTheme: ThemeChoice = resolvedTheme === \"dark\" ? \"light\" : \"dark\";\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button type=\"button\" onClick={() => choose(nextTheme)} className={buttonClass} aria-label={labels.toggleTheme}>\n {resolvedTheme === \"dark\" ? <Sun className=\"size-4\" /> : <Moon className=\"size-4\" />}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">{nextTheme === \"dark\" ? labels.themeDark : labels.themeLight}</TooltipContent>\n </Tooltip>\n );\n}\n"],"mappings":";AA0FW,cAYD,YAZC;AAxFX,SAAS,SAAS,MAAM,WAAW;AACnC,SAAS,gBAAgB;AACzB,SAAS,WAAW,gBAAgB;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,wBAAwB;AACjC,SAAS,UAAU;AAmDZ,SAAS,YAAY,EAAE,WAAW,UAAU,QAAQ,QAAQ,YAAY,cAAc,GAAqB;AAChH,QAAM,EAAE,OAAO,eAAe,SAAS,IAAI,SAAS;AACpD,QAAM,SAA4B,iBAAiB,SAAS,UAAU;AAOtE,QAAM,SAAS,CAAC,SAAsB;AACpC,aAAS,IAAI;AACb,oBAAgB,IAAI;AAAA,EACtB;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,CAAC,SAAS;AACZ,WAAO,oBAAC,UAAK,WAAW,aAAa,eAAW,MAAC;AAAA,EACnD;AAEA,MAAI,YAAY,QAAQ;AACtB,WACE,qBAAC,gBACC;AAAA,0BAAC,uBAAoB,SAAO,MAC1B,8BAAC,YAAO,MAAK,UAAS,WAAW,aAAa,cAAY,OAAO,aAC9D,4BAAkB,SAAS,oBAAC,QAAK,WAAU,UAAS,IAAK,oBAAC,OAAI,WAAU,UAAS,GACpF,GACF;AAAA,MACA,qBAAC,uBAAoB,OAAM,OACzB;AAAA,6BAAC,oBAAiB,SAAS,MAAM,OAAO,OAAO,GAAG,WAAW,GAAG,UAAU,WAAW,WAAW,GAC9F;AAAA,8BAAC,OAAI,WAAU,UAAS;AAAA,UACvB,OAAO;AAAA,WACV;AAAA,QACA,qBAAC,oBAAiB,SAAS,MAAM,OAAO,MAAM,GAAG,WAAW,GAAG,UAAU,UAAU,WAAW,GAC5F;AAAA,8BAAC,QAAK,WAAU,UAAS;AAAA,UACxB,OAAO;AAAA,WACV;AAAA,QACA,qBAAC,oBAAiB,SAAS,MAAM,OAAO,QAAQ,GAAG,WAAW,GAAG,UAAU,YAAY,WAAW,GAChG;AAAA,8BAAC,WAAQ,WAAU,UAAS;AAAA,UAC3B,OAAO;AAAA,WACV;AAAA,SACF;AAAA,OACF;AAAA,EAEJ;AAEA,QAAM,YAAyB,kBAAkB,SAAS,UAAU;AAEpE,SACE,qBAAC,WACC;AAAA,wBAAC,kBAAe,SAAO,MACrB,8BAAC,YAAO,MAAK,UAAS,SAAS,MAAM,OAAO,SAAS,GAAG,WAAW,aAAa,cAAY,OAAO,aAChG,4BAAkB,SAAS,oBAAC,OAAI,WAAU,UAAS,IAAK,oBAAC,QAAK,WAAU,UAAS,GACpF,GACF;AAAA,IACA,oBAAC,kBAAe,MAAK,UAAU,wBAAc,SAAS,OAAO,YAAY,OAAO,YAAW;AAAA,KAC7F;AAEJ;","names":[]}
@@ -39,7 +39,7 @@ interface Workspace {
39
39
  interface AppDescriptor {
40
40
  /** "tasks" | "chat" | "crm" | "hr" | … */
41
41
  id: string;
42
- /** Tên hiển thị (mặc định tiếng Việt). */
42
+ /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */
43
43
  name: string;
44
44
  description?: string;
45
45
  icon: ShellIcon;
@@ -154,8 +154,9 @@ interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "
154
154
  /** Component link thay thế `next/link`. */
155
155
  type ShellLinkComponent = ComponentType<ShellLinkProps>;
156
156
  /**
157
- * Toàn bộ chuỗi hiển thị của shell — app tiếng Anh (hoặc ngôn ngữ khác) override
158
- * từng khoá qua prop `labels`. Mặc định là tiếng Việt.
157
+ * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định
158
+ * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của
159
+ * `ShellProvider`/`AppShell`.
159
160
  */
160
161
  interface ShellLabels {
161
162
  collapseSidebar: string;
@@ -168,8 +169,8 @@ interface ShellLabels {
168
169
  * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.
169
170
  *
170
171
  * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại
171
- * "bấm Thu gọn" sẽ không khớp tên truy cập. "Thu gọn" ⊂ "Thu gọn thanh bên" ✓ — giữ quan hệ đó
172
- * khi dịch sang ngôn ngữ khác.
172
+ * "bấm Collapse" sẽ không khớp tên truy cập. "Collapse" ⊂ "Collapse sidebar" ✓, "Thu gọn" ⊂
173
+ * "Thu gọn thanh bên" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.
173
174
  */
174
175
  collapseSidebarShort: string;
175
176
  expandSidebar: string;
@@ -190,8 +191,9 @@ interface ShellLabels {
190
191
  * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và
191
192
  * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước ("12 thành viên"), tiếng Anh cũng vậy
192
193
  * ("12 members"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component
193
- * là khoá cứng trật tự tiếng Việt vào mọi bản dịch. Cùng khuôn với `range`/`pageStatus` của
194
- * `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).
194
+ * là khoá cứng trật tự của một ngôn ngữ vào mọi bản dịch. Hàm còn chọn được dạng số ít/nhiều
195
+ * ("1 member" ⇄ "2 members"), thứ tiếng Việt không có mà tiếng Anh thì bắt buộc. Cùng khuôn
196
+ * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).
195
197
  */
196
198
  members: (count: number) => string;
197
199
  apps: string;
@@ -211,7 +213,11 @@ interface ShellLabels {
211
213
  themeSystem: string;
212
214
  toggleTheme: string;
213
215
  }
214
- /** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
216
+ /**
217
+ * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:
218
+ * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của
219
+ * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.
220
+ */
215
221
  declare const DEFAULT_SHELL_LABELS: ShellLabels;
216
222
 
217
223
  export { type AppDescriptor, DEFAULT_SHELL_LABELS, type NavBadgeVariant, type NavGroup, type NavItem, type ShellIcon, type ShellLabels, type ShellLinkComponent, type ShellLinkProps, type ShellNav, type ShellUser, type UserMenuItem, type Workspace };
@@ -1,38 +1,5 @@
1
- const DEFAULT_SHELL_LABELS = {
2
- collapseSidebar: "Thu g\u1ECDn thanh b\xEAn",
3
- collapseSidebarShort: "Thu g\u1ECDn",
4
- expandSidebar: "M\u1EDF r\u1ED9ng thanh b\xEAn",
5
- openMenu: "M\u1EDF menu",
6
- closeMenu: "\u0110\xF3ng menu",
7
- search: "T\xECm ki\u1EBFm",
8
- searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
9
- notifications: "Th\xF4ng b\xE1o",
10
- navigationLabel: "\u0110i\u1EC1u h\u01B0\u1EDBng ch\xEDnh",
11
- workspace: "Kh\xF4ng gian l\xE0m vi\u1EC7c",
12
- workspaceSwitcherLabel: "\u0110\u1ED5i kh\xF4ng gian l\xE0m vi\u1EC7c",
13
- workspaceSearchPlaceholder: "T\xECm kh\xF4ng gian l\xE0m vi\u1EC7c\u2026",
14
- workspaceEmpty: "Kh\xF4ng t\xECm th\u1EA5y kh\xF4ng gian l\xE0m vi\u1EC7c",
15
- createWorkspace: "T\u1EA1o kh\xF4ng gian l\xE0m vi\u1EC7c",
16
- workspaceSettings: "C\xE0i \u0111\u1EB7t kh\xF4ng gian l\xE0m vi\u1EC7c",
17
- inviteMembers: "M\u1EDDi th\xE0nh vi\xEAn",
18
- members: (count) => `${count} th\xE0nh vi\xEAn`,
19
- apps: "\u1EE8ng d\u1EE5ng",
20
- appLauncherLabel: "M\u1EDF danh s\xE1ch \u1EE9ng d\u1EE5ng",
21
- myApps: "\u1EE8ng d\u1EE5ng c\u1EE7a b\u1EA1n",
22
- discoverApps: "Kh\xE1m ph\xE1 th\xEAm",
23
- locked: "Ch\u01B0a m\u1EDF kho\xE1",
24
- lockedHint: "\u1EE8ng d\u1EE5ng ch\u01B0a c\xF3 trong g\xF3i hi\u1EC7n t\u1EA1i",
25
- account: "T\xE0i kho\u1EA3n",
26
- profile: "H\u1ED3 s\u01A1",
27
- settings: "C\xE0i \u0111\u1EB7t",
28
- appearance: "Giao di\u1EC7n",
29
- help: "Tr\u1EE3 gi\xFAp",
30
- logout: "\u0110\u0103ng xu\u1EA5t",
31
- themeLight: "S\xE1ng",
32
- themeDark: "T\u1ED1i",
33
- themeSystem: "H\u1EC7 th\u1ED1ng",
34
- toggleTheme: "\u0110\u1ED5i giao di\u1EC7n"
35
- };
1
+ import { shellEn } from "../i18n/namespaces/shell.js";
2
+ const DEFAULT_SHELL_LABELS = { ...shellEn };
36
3
  export {
37
4
  DEFAULT_SHELL_LABELS
38
5
  };