@comitor/ui 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +67 -24
  3. package/dist/chart/chart.d.ts +6 -1
  4. package/dist/chart/chart.js +3 -1
  5. package/dist/chart/chart.js.map +1 -1
  6. package/dist/chart/comitor-chart.d.ts +4 -1
  7. package/dist/chart/comitor-chart.js +3 -1
  8. package/dist/chart/comitor-chart.js.map +1 -1
  9. package/dist/components/composite/avatar-group.d.ts +9 -6
  10. package/dist/components/composite/avatar-group.js +6 -2
  11. package/dist/components/composite/avatar-group.js.map +1 -1
  12. package/dist/components/composite/combobox.d.ts +11 -11
  13. package/dist/components/composite/combobox.js +25 -11
  14. package/dist/components/composite/combobox.js.map +1 -1
  15. package/dist/components/composite/command-palette-constants.d.ts +15 -5
  16. package/dist/components/composite/command-palette-constants.js +3 -2
  17. package/dist/components/composite/command-palette-constants.js.map +1 -1
  18. package/dist/components/composite/command-palette.d.ts +2 -2
  19. package/dist/components/composite/command-palette.js +8 -5
  20. package/dist/components/composite/command-palette.js.map +1 -1
  21. package/dist/components/composite/confirm-dialog.d.ts +8 -8
  22. package/dist/components/composite/confirm-dialog.js +8 -3
  23. package/dist/components/composite/confirm-dialog.js.map +1 -1
  24. package/dist/components/composite/copy-button.d.ts +7 -3
  25. package/dist/components/composite/copy-button.js +7 -3
  26. package/dist/components/composite/copy-button.js.map +1 -1
  27. package/dist/components/composite/data-table-constants.d.ts +7 -3
  28. package/dist/components/composite/data-table-constants.js +2 -9
  29. package/dist/components/composite/data-table-constants.js.map +1 -1
  30. package/dist/components/composite/data-table.js +3 -3
  31. package/dist/components/composite/data-table.js.map +1 -1
  32. package/dist/components/composite/date-picker-constants.d.ts +32 -9
  33. package/dist/components/composite/date-picker-constants.js +16 -11
  34. package/dist/components/composite/date-picker-constants.js.map +1 -1
  35. package/dist/components/composite/date-picker.d.ts +19 -10
  36. package/dist/components/composite/date-picker.js +33 -9
  37. package/dist/components/composite/date-picker.js.map +1 -1
  38. package/dist/components/composite/filter-chips.d.ts +4 -3
  39. package/dist/components/composite/filter-chips.js +6 -2
  40. package/dist/components/composite/filter-chips.js.map +1 -1
  41. package/dist/components/composite/inline-edit.d.ts +14 -3
  42. package/dist/components/composite/inline-edit.js +8 -4
  43. package/dist/components/composite/inline-edit.js.map +1 -1
  44. package/dist/components/composite/relative-time.d.ts +4 -4
  45. package/dist/components/composite/relative-time.js +4 -2
  46. package/dist/components/composite/relative-time.js.map +1 -1
  47. package/dist/components/composite/route-tabs-nav.d.ts +9 -0
  48. package/dist/components/composite/route-tabs-nav.js +11 -0
  49. package/dist/components/composite/route-tabs-nav.js.map +1 -0
  50. package/dist/components/composite/route-tabs.d.ts +4 -1
  51. package/dist/components/composite/route-tabs.js +3 -2
  52. package/dist/components/composite/route-tabs.js.map +1 -1
  53. package/dist/components/composite/search-filter-bar-constants.d.ts +18 -8
  54. package/dist/components/composite/search-filter-bar-constants.js +2 -14
  55. package/dist/components/composite/search-filter-bar-constants.js.map +1 -1
  56. package/dist/components/composite/search-filter-bar.d.ts +5 -2
  57. package/dist/components/composite/search-filter-bar.js +2 -2
  58. package/dist/components/composite/search-filter-bar.js.map +1 -1
  59. package/dist/components/composite/stat-card.d.ts +1 -1
  60. package/dist/components/composite/table-pagination-constants.d.ts +16 -11
  61. package/dist/components/composite/table-pagination-constants.js +2 -12
  62. package/dist/components/composite/table-pagination-constants.js.map +1 -1
  63. package/dist/components/composite/table-pagination.d.ts +6 -3
  64. package/dist/components/composite/table-pagination.js +3 -5
  65. package/dist/components/composite/table-pagination.js.map +1 -1
  66. package/dist/components/composite/toast.d.ts +4 -2
  67. package/dist/components/composite/toast.js +9 -1
  68. package/dist/components/composite/toast.js.map +1 -1
  69. package/dist/components/ui/action-bar-constants.d.ts +35 -0
  70. package/dist/components/ui/action-bar-constants.js +6 -0
  71. package/dist/components/ui/action-bar-constants.js.map +1 -0
  72. package/dist/components/ui/action-bar.d.ts +68 -0
  73. package/dist/components/ui/action-bar.js +522 -0
  74. package/dist/components/ui/action-bar.js.map +1 -0
  75. package/dist/components/ui/alert.d.ts +1 -1
  76. package/dist/components/ui/badge-overflow.d.ts +30 -0
  77. package/dist/components/ui/badge-overflow.js +181 -0
  78. package/dist/components/ui/badge-overflow.js.map +1 -0
  79. package/dist/components/ui/badge.d.ts +1 -1
  80. package/dist/components/ui/banner.d.ts +80 -0
  81. package/dist/components/ui/banner.js +520 -0
  82. package/dist/components/ui/banner.js.map +1 -0
  83. package/dist/components/ui/breadcrumb.d.ts +11 -4
  84. package/dist/components/ui/breadcrumb.js +7 -2
  85. package/dist/components/ui/breadcrumb.js.map +1 -1
  86. package/dist/components/ui/button.d.ts +2 -2
  87. package/dist/components/ui/calendar.d.ts +12 -5
  88. package/dist/components/ui/calendar.js +38 -3
  89. package/dist/components/ui/calendar.js.map +1 -1
  90. package/dist/components/ui/carousel.d.ts +4 -3
  91. package/dist/components/ui/carousel.js +11 -4
  92. package/dist/components/ui/carousel.js.map +1 -1
  93. package/dist/components/ui/circular-progress.d.ts +50 -0
  94. package/dist/components/ui/circular-progress.js +266 -0
  95. package/dist/components/ui/circular-progress.js.map +1 -0
  96. package/dist/components/ui/command.d.ts +5 -4
  97. package/dist/components/ui/command.js +6 -2
  98. package/dist/components/ui/command.js.map +1 -1
  99. package/dist/components/ui/cropper-constants.d.ts +39 -0
  100. package/dist/components/ui/cropper-constants.js +6 -0
  101. package/dist/components/ui/cropper-constants.js.map +1 -0
  102. package/dist/components/ui/cropper.d.ts +107 -0
  103. package/dist/components/ui/cropper.js +1269 -0
  104. package/dist/components/ui/cropper.js.map +1 -0
  105. package/dist/components/ui/dialog.d.ts +2 -2
  106. package/dist/components/ui/dialog.js +4 -1
  107. package/dist/components/ui/dialog.js.map +1 -1
  108. package/dist/components/ui/drawer.d.ts +1 -1
  109. package/dist/components/ui/drawer.js +2 -2
  110. package/dist/components/ui/drawer.js.map +1 -1
  111. package/dist/components/ui/item.d.ts +2 -2
  112. package/dist/components/ui/kanban-constants.d.ts +67 -0
  113. package/dist/components/ui/kanban-constants.js +6 -0
  114. package/dist/components/ui/kanban-constants.js.map +1 -0
  115. package/dist/components/ui/kanban.d.ts +72 -0
  116. package/dist/components/ui/kanban.js +787 -0
  117. package/dist/components/ui/kanban.js.map +1 -0
  118. package/dist/components/ui/key-value-constants.d.ts +37 -0
  119. package/dist/components/ui/key-value-constants.js +6 -0
  120. package/dist/components/ui/key-value-constants.js.map +1 -0
  121. package/dist/components/ui/key-value.d.ts +96 -0
  122. package/dist/components/ui/key-value.js +641 -0
  123. package/dist/components/ui/key-value.js.map +1 -0
  124. package/dist/components/ui/mask-input-constants.d.ts +98 -0
  125. package/dist/components/ui/mask-input-constants.js +600 -0
  126. package/dist/components/ui/mask-input-constants.js.map +1 -0
  127. package/dist/components/ui/mask-input.d.ts +39 -0
  128. package/dist/components/ui/mask-input.js +560 -0
  129. package/dist/components/ui/mask-input.js.map +1 -0
  130. package/dist/components/ui/mention.d.ts +32 -0
  131. package/dist/components/ui/mention.js +126 -0
  132. package/dist/components/ui/mention.js.map +1 -0
  133. package/dist/components/ui/pagination.d.ts +13 -9
  134. package/dist/components/ui/pagination.js +15 -6
  135. package/dist/components/ui/pagination.js.map +1 -1
  136. package/dist/components/ui/phone-input-constants.d.ts +65 -0
  137. package/dist/components/ui/phone-input-constants.js +6 -0
  138. package/dist/components/ui/phone-input-constants.js.map +1 -0
  139. package/dist/components/ui/phone-input.d.ts +92 -0
  140. package/dist/components/ui/phone-input.js +742 -0
  141. package/dist/components/ui/phone-input.js.map +1 -0
  142. package/dist/components/ui/progress.d.ts +2 -2
  143. package/dist/components/ui/rating.d.ts +57 -0
  144. package/dist/components/ui/rating.js +648 -0
  145. package/dist/components/ui/rating.js.map +1 -0
  146. package/dist/components/ui/responsive-dialog.d.ts +35 -0
  147. package/dist/components/ui/responsive-dialog.js +193 -0
  148. package/dist/components/ui/responsive-dialog.js.map +1 -0
  149. package/dist/components/ui/scroll-spy.d.ts +42 -0
  150. package/dist/components/ui/scroll-spy.js +336 -0
  151. package/dist/components/ui/scroll-spy.js.map +1 -0
  152. package/dist/components/ui/sheet.d.ts +2 -2
  153. package/dist/components/ui/sheet.js +10 -1
  154. package/dist/components/ui/sheet.js.map +1 -1
  155. package/dist/components/ui/spinner.js +4 -1
  156. package/dist/components/ui/spinner.js.map +1 -1
  157. package/dist/components/ui/stepper-constants.d.ts +34 -0
  158. package/dist/components/ui/stepper-constants.js +6 -0
  159. package/dist/components/ui/stepper-constants.js.map +1 -0
  160. package/dist/components/ui/stepper.d.ts +81 -0
  161. package/dist/components/ui/stepper.js +902 -0
  162. package/dist/components/ui/stepper.js.map +1 -0
  163. package/dist/components/ui/tags-input.d.ts +42 -0
  164. package/dist/components/ui/tags-input.js +129 -0
  165. package/dist/components/ui/tags-input.js.map +1 -0
  166. package/dist/components/ui/time-picker-constants.d.ts +51 -0
  167. package/dist/components/ui/time-picker-constants.js +6 -0
  168. package/dist/components/ui/time-picker-constants.js.map +1 -0
  169. package/dist/components/ui/time-picker.d.ts +120 -0
  170. package/dist/components/ui/time-picker.js +1618 -0
  171. package/dist/components/ui/time-picker.js.map +1 -0
  172. package/dist/components/ui/timeline.d.ts +32 -0
  173. package/dist/components/ui/timeline.js +559 -0
  174. package/dist/components/ui/timeline.js.map +1 -0
  175. package/dist/components/ui/toggle.d.ts +1 -1
  176. package/dist/components/ui/tour-constants.d.ts +44 -0
  177. package/dist/components/ui/tour-constants.js +6 -0
  178. package/dist/components/ui/tour-constants.js.map +1 -0
  179. package/dist/components/ui/tour.d.ts +125 -0
  180. package/dist/components/ui/tour.js +1207 -0
  181. package/dist/components/ui/tour.js.map +1 -0
  182. package/dist/data-grid/data-grid-cell-variants.js +4 -4
  183. package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
  184. package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
  185. package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
  186. package/dist/data-grid/data-grid-column-header.js +1 -1
  187. package/dist/data-grid/data-grid-column-header.js.map +1 -1
  188. package/dist/data-grid/data-grid-context-menu.js +1 -1
  189. package/dist/data-grid/data-grid-context-menu.js.map +1 -1
  190. package/dist/data-grid/data-grid-filter-menu.d.ts +5 -4
  191. package/dist/data-grid/data-grid-filter-menu.js +13 -8
  192. package/dist/data-grid/data-grid-filter-menu.js.map +1 -1
  193. package/dist/data-grid/data-grid-keyboard-shortcuts.d.ts +4 -4
  194. package/dist/data-grid/data-grid-keyboard-shortcuts.js.map +1 -1
  195. package/dist/data-grid/data-grid-locale.d.ts +58 -6
  196. package/dist/data-grid/data-grid-locale.js +41 -9
  197. package/dist/data-grid/data-grid-locale.js.map +1 -1
  198. package/dist/data-grid/data-grid-paste-dialog.js +1 -1
  199. package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
  200. package/dist/data-grid/data-grid-row-height-menu.d.ts +4 -3
  201. package/dist/data-grid/data-grid-row-height-menu.js.map +1 -1
  202. package/dist/data-grid/data-grid-row.js +1 -1
  203. package/dist/data-grid/data-grid-row.js.map +1 -1
  204. package/dist/data-grid/data-grid-search.js +1 -1
  205. package/dist/data-grid/data-grid-search.js.map +1 -1
  206. package/dist/data-grid/data-grid-sort-menu.d.ts +4 -4
  207. package/dist/data-grid/data-grid-sort-menu.js +1 -1
  208. package/dist/data-grid/data-grid-sort-menu.js.map +1 -1
  209. package/dist/data-grid/data-grid-view-menu.d.ts +4 -4
  210. package/dist/data-grid/data-grid-view-menu.js.map +1 -1
  211. package/dist/data-grid/data-grid.d.ts +4 -3
  212. package/dist/data-grid/data-grid.js +3 -4
  213. package/dist/data-grid/data-grid.js.map +1 -1
  214. package/dist/data-grid/index.d.ts +1 -0
  215. package/dist/data-grid/index.js.map +1 -1
  216. package/dist/data-grid/messages-en.d.ts +23 -4
  217. package/dist/data-grid/messages-en.js +20 -0
  218. package/dist/data-grid/messages-en.js.map +1 -1
  219. package/dist/data-grid/messages-vi.d.ts +3 -1
  220. package/dist/data-grid/messages-vi.js +14 -0
  221. package/dist/data-grid/messages-vi.js.map +1 -1
  222. package/dist/data-grid/messages.d.ts +29 -3
  223. package/dist/data-grid/messages.js.map +1 -1
  224. package/dist/data-grid/sortable.js +17 -20
  225. package/dist/data-grid/sortable.js.map +1 -1
  226. package/dist/data-grid/types.d.ts +1 -1
  227. package/dist/data-grid/use-data-grid-undo-redo.d.ts +5 -4
  228. package/dist/data-grid/use-data-grid-undo-redo.js +4 -4
  229. package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
  230. package/dist/data-grid/use-data-grid.d.ts +21 -6
  231. package/dist/data-grid/use-data-grid.js +11 -9
  232. package/dist/data-grid/use-data-grid.js.map +1 -1
  233. package/dist/data-grid/utils.d.ts +24 -4
  234. package/dist/data-grid/utils.js +15 -5
  235. package/dist/data-grid/utils.js.map +1 -1
  236. package/dist/i18n/locale-provider.d.ts +109 -0
  237. package/dist/i18n/locale-provider.js +47 -0
  238. package/dist/i18n/locale-provider.js.map +1 -0
  239. package/dist/i18n/locale.d.ts +27 -0
  240. package/dist/i18n/locale.js +10 -0
  241. package/dist/i18n/locale.js.map +1 -0
  242. package/dist/i18n/messages.d.ts +116 -0
  243. package/dist/i18n/messages.js +149 -0
  244. package/dist/i18n/messages.js.map +1 -0
  245. package/dist/i18n/namespaces/action-bar.d.ts +7 -0
  246. package/dist/i18n/namespaces/action-bar.js +15 -0
  247. package/dist/i18n/namespaces/action-bar.js.map +1 -0
  248. package/dist/i18n/namespaces/avatar-group.d.ts +14 -0
  249. package/dist/i18n/namespaces/avatar-group.js +13 -0
  250. package/dist/i18n/namespaces/avatar-group.js.map +1 -0
  251. package/dist/i18n/namespaces/badge-overflow.d.ts +12 -0
  252. package/dist/i18n/namespaces/badge-overflow.js +11 -0
  253. package/dist/i18n/namespaces/badge-overflow.js.map +1 -0
  254. package/dist/i18n/namespaces/banner.d.ts +9 -0
  255. package/dist/i18n/namespaces/banner.js +7 -0
  256. package/dist/i18n/namespaces/banner.js.map +1 -0
  257. package/dist/i18n/namespaces/breadcrumb.d.ts +11 -0
  258. package/dist/i18n/namespaces/breadcrumb.js +13 -0
  259. package/dist/i18n/namespaces/breadcrumb.js.map +1 -0
  260. package/dist/i18n/namespaces/calendar.d.ts +40 -0
  261. package/dist/i18n/namespaces/calendar.js +25 -0
  262. package/dist/i18n/namespaces/calendar.js.map +1 -0
  263. package/dist/i18n/namespaces/carousel.d.ts +15 -0
  264. package/dist/i18n/namespaces/carousel.js +17 -0
  265. package/dist/i18n/namespaces/carousel.js.map +1 -0
  266. package/dist/i18n/namespaces/combobox.d.ts +21 -0
  267. package/dist/i18n/namespaces/combobox.js +23 -0
  268. package/dist/i18n/namespaces/combobox.js.map +1 -0
  269. package/dist/i18n/namespaces/command-palette.d.ts +20 -0
  270. package/dist/i18n/namespaces/command-palette.js +19 -0
  271. package/dist/i18n/namespaces/command-palette.js.map +1 -0
  272. package/dist/i18n/namespaces/command.d.ts +11 -0
  273. package/dist/i18n/namespaces/command.js +13 -0
  274. package/dist/i18n/namespaces/command.js.map +1 -0
  275. package/dist/i18n/namespaces/confirm-dialog.d.ts +13 -0
  276. package/dist/i18n/namespaces/confirm-dialog.js +15 -0
  277. package/dist/i18n/namespaces/confirm-dialog.js.map +1 -0
  278. package/dist/i18n/namespaces/contrast.d.ts +7 -0
  279. package/dist/i18n/namespaces/contrast.js +13 -0
  280. package/dist/i18n/namespaces/contrast.js.map +1 -0
  281. package/dist/i18n/namespaces/copy-button.d.ts +11 -0
  282. package/dist/i18n/namespaces/copy-button.js +13 -0
  283. package/dist/i18n/namespaces/copy-button.js.map +1 -0
  284. package/dist/i18n/namespaces/cropper.d.ts +7 -0
  285. package/dist/i18n/namespaces/cropper.js +17 -0
  286. package/dist/i18n/namespaces/cropper.js.map +1 -0
  287. package/dist/i18n/namespaces/data-table.d.ts +7 -0
  288. package/dist/i18n/namespaces/data-table.js +23 -0
  289. package/dist/i18n/namespaces/data-table.js.map +1 -0
  290. package/dist/i18n/namespaces/date-picker.d.ts +42 -0
  291. package/dist/i18n/namespaces/date-picker.js +35 -0
  292. package/dist/i18n/namespaces/date-picker.js.map +1 -0
  293. package/dist/i18n/namespaces/dialog.d.ts +9 -0
  294. package/dist/i18n/namespaces/dialog.js +7 -0
  295. package/dist/i18n/namespaces/dialog.js.map +1 -0
  296. package/dist/i18n/namespaces/display-axis.d.ts +25 -0
  297. package/dist/i18n/namespaces/display-axis.js +21 -0
  298. package/dist/i18n/namespaces/display-axis.js.map +1 -0
  299. package/dist/i18n/namespaces/file-upload.d.ts +11 -0
  300. package/dist/i18n/namespaces/file-upload.js +19 -0
  301. package/dist/i18n/namespaces/file-upload.js.map +1 -0
  302. package/dist/i18n/namespaces/filter-chips.d.ts +11 -0
  303. package/dist/i18n/namespaces/filter-chips.js +13 -0
  304. package/dist/i18n/namespaces/filter-chips.js.map +1 -0
  305. package/dist/i18n/namespaces/image-upload-field.d.ts +12 -0
  306. package/dist/i18n/namespaces/image-upload-field.js +23 -0
  307. package/dist/i18n/namespaces/image-upload-field.js.map +1 -0
  308. package/dist/i18n/namespaces/inline-edit.d.ts +15 -0
  309. package/dist/i18n/namespaces/inline-edit.js +17 -0
  310. package/dist/i18n/namespaces/inline-edit.js.map +1 -0
  311. package/dist/i18n/namespaces/kanban.d.ts +11 -0
  312. package/dist/i18n/namespaces/kanban.js +39 -0
  313. package/dist/i18n/namespaces/kanban.js.map +1 -0
  314. package/dist/i18n/namespaces/key-value.d.ts +10 -0
  315. package/dist/i18n/namespaces/key-value.js +19 -0
  316. package/dist/i18n/namespaces/key-value.js.map +1 -0
  317. package/dist/i18n/namespaces/mention.d.ts +9 -0
  318. package/dist/i18n/namespaces/mention.js +7 -0
  319. package/dist/i18n/namespaces/mention.js.map +1 -0
  320. package/dist/i18n/namespaces/pagination.d.ts +19 -0
  321. package/dist/i18n/namespaces/pagination.js +21 -0
  322. package/dist/i18n/namespaces/pagination.js.map +1 -0
  323. package/dist/i18n/namespaces/phone-input.d.ts +10 -0
  324. package/dist/i18n/namespaces/phone-input.js +17 -0
  325. package/dist/i18n/namespaces/phone-input.js.map +1 -0
  326. package/dist/i18n/namespaces/rating.d.ts +12 -0
  327. package/dist/i18n/namespaces/rating.js +11 -0
  328. package/dist/i18n/namespaces/rating.js.map +1 -0
  329. package/dist/i18n/namespaces/route-tabs.d.ts +9 -0
  330. package/dist/i18n/namespaces/route-tabs.js +7 -0
  331. package/dist/i18n/namespaces/route-tabs.js.map +1 -0
  332. package/dist/i18n/namespaces/scroll-spy.d.ts +9 -0
  333. package/dist/i18n/namespaces/scroll-spy.js +7 -0
  334. package/dist/i18n/namespaces/scroll-spy.js.map +1 -0
  335. package/dist/i18n/namespaces/search-filter-bar.d.ts +15 -0
  336. package/dist/i18n/namespaces/search-filter-bar.js +32 -0
  337. package/dist/i18n/namespaces/search-filter-bar.js.map +1 -0
  338. package/dist/i18n/namespaces/sheet.d.ts +9 -0
  339. package/dist/i18n/namespaces/sheet.js +7 -0
  340. package/dist/i18n/namespaces/sheet.js.map +1 -0
  341. package/dist/i18n/namespaces/shell.d.ts +17 -0
  342. package/dist/i18n/namespaces/shell.js +75 -0
  343. package/dist/i18n/namespaces/shell.js.map +1 -0
  344. package/dist/i18n/namespaces/spinner.d.ts +9 -0
  345. package/dist/i18n/namespaces/spinner.js +7 -0
  346. package/dist/i18n/namespaces/spinner.js.map +1 -0
  347. package/dist/i18n/namespaces/stepper.d.ts +7 -0
  348. package/dist/i18n/namespaces/stepper.js +15 -0
  349. package/dist/i18n/namespaces/stepper.js.map +1 -0
  350. package/dist/i18n/namespaces/table-pagination.d.ts +7 -0
  351. package/dist/i18n/namespaces/table-pagination.js +29 -0
  352. package/dist/i18n/namespaces/table-pagination.js.map +1 -0
  353. package/dist/i18n/namespaces/tags-input.d.ts +12 -0
  354. package/dist/i18n/namespaces/tags-input.js +11 -0
  355. package/dist/i18n/namespaces/tags-input.js.map +1 -0
  356. package/dist/i18n/namespaces/time-picker.d.ts +15 -0
  357. package/dist/i18n/namespaces/time-picker.js +25 -0
  358. package/dist/i18n/namespaces/time-picker.js.map +1 -0
  359. package/dist/i18n/namespaces/toast.d.ts +15 -0
  360. package/dist/i18n/namespaces/toast.js +7 -0
  361. package/dist/i18n/namespaces/toast.js.map +1 -0
  362. package/dist/i18n/namespaces/tour.d.ts +7 -0
  363. package/dist/i18n/namespaces/tour.js +21 -0
  364. package/dist/i18n/namespaces/tour.js.map +1 -0
  365. package/dist/index.d.ts +63 -0
  366. package/dist/index.js +66 -1
  367. package/dist/index.js.map +1 -1
  368. package/dist/lib/compose-refs.js.map +1 -0
  369. package/dist/lib/use-as-ref.js.map +1 -0
  370. package/dist/lib/use-is-mobile.d.ts +10 -0
  371. package/dist/lib/use-is-mobile.js +21 -0
  372. package/dist/lib/use-is-mobile.js.map +1 -0
  373. package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
  374. package/dist/lib/use-lazy-ref.js.map +1 -0
  375. package/dist/lib/visually-hidden-input.d.ts +12 -0
  376. package/dist/lib/visually-hidden-input.js +105 -0
  377. package/dist/lib/visually-hidden-input.js.map +1 -0
  378. package/dist/shell/app-header.js.map +1 -1
  379. package/dist/shell/app-shell.d.ts +20 -2
  380. package/dist/shell/app-shell.js +4 -1
  381. package/dist/shell/app-shell.js.map +1 -1
  382. package/dist/shell/contrast-constants.d.ts +9 -1
  383. package/dist/shell/contrast-constants.js +2 -4
  384. package/dist/shell/contrast-constants.js.map +1 -1
  385. package/dist/shell/contrast-toggle.d.ts +4 -1
  386. package/dist/shell/contrast-toggle.js +2 -2
  387. package/dist/shell/contrast-toggle.js.map +1 -1
  388. package/dist/shell/display-axis-constants.d.ts +2 -1
  389. package/dist/shell/display-axis-constants.js.map +1 -1
  390. package/dist/shell/display-axis-controls.d.ts +15 -6
  391. package/dist/shell/display-axis-controls.js +11 -9
  392. package/dist/shell/display-axis-controls.js.map +1 -1
  393. package/dist/shell/mobile-menu.js.map +1 -1
  394. package/dist/shell/notifications-button.d.ts +1 -1
  395. package/dist/shell/notifications-button.js +3 -2
  396. package/dist/shell/notifications-button.js.map +1 -1
  397. package/dist/shell/shell-context.d.ts +4 -1
  398. package/dist/shell/shell-context.js +2 -4
  399. package/dist/shell/shell-context.js.map +1 -1
  400. package/dist/shell/sidebar.d.ts +1 -1
  401. package/dist/shell/sidebar.js.map +1 -1
  402. package/dist/shell/theme-toggle.d.ts +6 -3
  403. package/dist/shell/theme-toggle.js +2 -2
  404. package/dist/shell/theme-toggle.js.map +1 -1
  405. package/dist/shell/types.d.ts +14 -8
  406. package/dist/shell/types.js +2 -35
  407. package/dist/shell/types.js.map +1 -1
  408. package/dist/shell/user-menu.d.ts +6 -6
  409. package/dist/shell/user-menu.js.map +1 -1
  410. package/dist/shell/utils.d.ts +1 -1
  411. package/dist/shell/utils.js.map +1 -1
  412. package/dist/shell/workspace-switcher.js +1 -1
  413. package/dist/shell/workspace-switcher.js.map +1 -1
  414. package/dist/uploader/file-upload-constants.d.ts +13 -0
  415. package/dist/uploader/file-upload-constants.js +4 -16
  416. package/dist/uploader/file-upload-constants.js.map +1 -1
  417. package/dist/uploader/file-upload.d.ts +4 -1
  418. package/dist/uploader/file-upload.js +2 -2
  419. package/dist/uploader/file-upload.js.map +1 -1
  420. package/dist/uploader/image-upload-field.d.ts +5 -1
  421. package/dist/uploader/image-upload-field.js +2 -2
  422. package/dist/uploader/image-upload-field.js.map +1 -1
  423. package/dist/uploader/use-file-upload.d.ts +4 -0
  424. package/dist/uploader/use-file-upload.js +6 -2
  425. package/dist/uploader/use-file-upload.js.map +1 -1
  426. package/package.json +8 -3
  427. package/styles.css +19 -0
  428. package/dist/data-grid/compose-refs.js.map +0 -1
  429. package/dist/data-grid/use-as-ref.js.map +0 -1
  430. package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
  431. package/dist/data-grid/use-lazy-ref.js.map +0 -1
  432. /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
  433. /package/dist/{data-grid → lib}/compose-refs.js +0 -0
  434. /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
  435. /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
  436. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
  437. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
  438. /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
  439. /package/dist/{data-grid → lib}/use-lazy-ref.js +0 -0
@@ -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
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị (mặc định tiếng Việt). */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (Linear-style: nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell — app tiếng Anh (hoặc ngôn ngữ khác) override\n * từng khoá qua prop `labels`. Mặc định là tiếng Việt.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * 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.\n *\n * ⚠ 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\n * \"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ệ đó\n * khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơ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à\n * 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\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * 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\n * `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = {\n collapseSidebar: \"Thu gọn thanh bên\",\n collapseSidebarShort: \"Thu gọn\",\n expandSidebar: \"Mở rộng thanh bên\",\n openMenu: \"Mở menu\",\n closeMenu: \"Đóng menu\",\n search: \"Tìm kiếm\",\n searchPlaceholder: \"Tìm kiếm…\",\n notifications: \"Thông báo\",\n navigationLabel: \"Điều hướng chính\",\n workspace: \"Không gian làm việc\",\n workspaceSwitcherLabel: \"Đổi không gian làm việc\",\n workspaceSearchPlaceholder: \"Tìm không gian làm việc…\",\n workspaceEmpty: \"Không tìm thấy không gian làm việc\",\n createWorkspace: \"Tạo không gian làm việc\",\n workspaceSettings: \"Cài đặt không gian làm việc\",\n inviteMembers: \"Mời thành viên\",\n members: (count) => `${count} thành viên`,\n apps: \"Ứng dụng\",\n appLauncherLabel: \"Mở danh sách ứng dụng\",\n myApps: \"Ứng dụng của bạn\",\n discoverApps: \"Khám phá thêm\",\n locked: \"Chưa mở khoá\",\n lockedHint: \"Ứng dụng chưa có trong gói hiện tại\",\n account: \"Tài khoản\",\n profile: \"Hồ sơ\",\n settings: \"Cài đặt\",\n appearance: \"Giao diện\",\n help: \"Trợ giúp\",\n logout: \"Đăng xuất\",\n themeLight: \"Sáng\",\n themeDark: \"Tối\",\n themeSystem: \"Hệ thống\",\n toggleTheme: \"Đổi giao diện\"\n};\n"],"mappings":"AAqOO,MAAM,uBAAoC;AAAA,EAC/C,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AACf;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\nimport { shellEn } from \"../i18n/namespaces/shell.js\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (Linear-style: nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định\n * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của\n * `ShellProvider`/`AppShell`.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * 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.\n *\n * ⚠ 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\n * \"bấm Collapse\" sẽ không khớp tên truy cập. \"Collapse\" ⊂ \"Collapse sidebar\" ✓, \"Thu gọn\" ⊂\n * \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơ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à\n * 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\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * 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\n * (\"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\n * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n}\n\n/**\n * 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:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AA4NjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
@@ -17,16 +17,16 @@ interface UserMenuProps {
17
17
  className?: string;
18
18
  /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */
19
19
  showName?: boolean;
20
- /** Link "Hồ sơ" — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */
20
+ /** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */
21
21
  profileHref?: string;
22
- /** Link "Cài đặt". */
22
+ /** Link cài đặt (nhãn `settings`). */
23
23
  settingsHref?: string;
24
- /** Link "Trợ giúp". */
24
+ /** Link trợ giúp (nhãn `help`). */
25
25
  helpHref?: string;
26
- /** Mục bổ sung của app — chèn ngay trên "Đăng xuất". */
26
+ /** Mục bổ sung của app — chèn ngay trên mục đăng xuất. */
27
27
  items?: UserMenuItem[];
28
28
  /**
29
- * Submenu "Giao diện" trong menu người dùng.
29
+ * Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.
30
30
  *
31
31
  * `"mobile"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có
32
32
  * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.
@@ -45,7 +45,7 @@ interface UserMenuProps {
45
45
  }
46
46
  /**
47
47
  * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,
48
- * submenu đổi giao diện, và "Đăng xuất" (tô màu destructive).
48
+ * submenu đổi giao diện, và mục đăng xuất (tô màu destructive).
49
49
  *
50
50
  * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.
51
51
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/user-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown, CircleUser, HelpCircle, LogOut, Monitor, Moon, Palette, Settings, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { ShellUser, UserMenuItem } from \"./types.js\";\nimport { getVietnameseInitials } from \"./utils.js\";\n\nexport interface UserAvatarProps {\n user: ShellUser;\n className?: string;\n}\n\n/**\n * Avatar người dùng: ảnh nếu có, không thì chữ cái kiểu VN (HỌ + TÊN) tô màu theo tên.\n *\n * Tô màu theo `user.name` chứ KHÔNG theo email: `LetterAvatar`/`AvatarGroup` ở Tier 2 seed\n * bằng tên, nên lấy khoá khác ở đây là cùng một người ra hai màu trên cùng một trang.\n */\nexport function UserAvatar({ user, className }: UserAvatarProps) {\n return (\n <Avatar className={cn(\"size-8\", className)}>\n {user.avatarUrl ? <AvatarImage src={user.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback className={cn(\"text-xs font-semibold\", getAvatarToneClasses(user.name))}>\n {getVietnameseInitials(user.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface UserMenuProps {\n className?: string;\n /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */\n showName?: boolean;\n /** Link \"Hồ sơ\" — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */\n profileHref?: string;\n /** Link \"Cài đặt\". */\n settingsHref?: string;\n /** Link \"Trợ giúp\". */\n helpHref?: string;\n /** Mục bổ sung của app — chèn ngay trên \"Đăng xuất\". */\n items?: UserMenuItem[];\n /**\n * Submenu \"Giao diện\" trong menu người dùng.\n *\n * `\"mobile\"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có\n * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.\n * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn\n * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.\n * `false` — không bao giờ hiện.\n *\n * ⚠ Nhánh `\"mobile\"` ẩn bằng CSS (`md:hidden`), không phải bằng điều kiện render: nút gạt theo\n * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại\n * phần tử khỏi CẢ cây trợ năng nên trình đọc màn hình ở desktop cũng không thấy mục thừa.\n */\n showThemeSubmenu?: boolean | \"mobile\";\n /** Ghi đè handler đăng xuất của context. */\n onLogout?: () => void;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,\n * submenu đổi giao diện, và \"Đăng xuất\" (tô màu destructive).\n *\n * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.\n */\nexport function UserMenu({\n className,\n showName = false,\n profileHref,\n settingsHref,\n helpHref,\n items = [],\n showThemeSubmenu = \"mobile\",\n onLogout,\n align = \"end\"\n}: UserMenuProps) {\n const shell = useShell();\n const { theme, setTheme } = useTheme();\n const { user, labels } = shell;\n\n if (!user) return null;\n\n const handleLogout = onLogout ?? shell.onLogout;\n\n const renderItem = (item: UserMenuItem) => {\n const Icon = item.icon;\n const content = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n <span className=\"truncate\">{item.label}</span>\n {item.shortcut ? <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut> : null}\n </>\n );\n\n if (item.href) {\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} asChild>\n <ShellNavLink href={item.href} onClick={() => item.onSelect?.()}>\n {content}\n </ShellNavLink>\n </DropdownMenuItem>\n );\n }\n\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} onClick={() => item.onSelect?.()}>\n {content}\n </DropdownMenuItem>\n );\n };\n\n const builtInItems: (UserMenuItem | null)[] = [\n profileHref ? { id: \"profile\", label: labels.profile, icon: CircleUser, href: profileHref } : null,\n settingsHref ? { id: \"settings\", label: labels.settings, icon: Settings, href: settingsHref } : null,\n helpHref ? { id: \"help\", label: labels.help, icon: HelpCircle, href: helpHref } : null\n ];\n const visibleBuiltIns = builtInItems.filter((item): item is UserMenuItem => item !== null);\n\n const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.account}\n className={cn(\n \"flex items-center gap-2 rounded-md p-1 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-muted\",\n className\n )}\n >\n <UserAvatar user={user} />\n {showName ? (\n <>\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">{user.name}</span>\n {user.role ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{user.role}</span>\n ) : null}\n </span>\n <ChevronDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n </button>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align={align} className=\"w-60\">\n <DropdownMenuLabel className=\"flex items-center gap-2 font-normal\">\n <UserAvatar user={user} />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email ? <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span> : null}\n {user.role ? <span className=\"truncate text-[11px] text-muted-foreground\">{user.role}</span> : null}\n </span>\n </DropdownMenuLabel>\n\n {hasEntries ? <DropdownMenuSeparator /> : null}\n\n {visibleBuiltIns.map(renderItem)}\n {items.map(renderItem)}\n\n {showThemeSubmenu !== false ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className={cn(showThemeSubmenu === \"mobile\" && \"md:hidden\")}>\n <Palette className=\"size-4\" />\n <span>{labels.appearance}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n <DropdownMenuItem onClick={() => setTheme(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n\n {handleLogout ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n <span>{labels.logout}</span>\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/user-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown, CircleUser, HelpCircle, LogOut, Monitor, Moon, Palette, Settings, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { ShellUser, UserMenuItem } from \"./types.js\";\nimport { getVietnameseInitials } from \"./utils.js\";\n\nexport interface UserAvatarProps {\n user: ShellUser;\n className?: string;\n}\n\n/**\n * Avatar người dùng: ảnh nếu có, không thì chữ cái kiểu VN (HỌ + TÊN) tô màu theo tên.\n *\n * Tô màu theo `user.name` chứ KHÔNG theo email: `LetterAvatar`/`AvatarGroup` ở Tier 2 seed\n * bằng tên, nên lấy khoá khác ở đây là cùng một người ra hai màu trên cùng một trang.\n */\nexport function UserAvatar({ user, className }: UserAvatarProps) {\n return (\n <Avatar className={cn(\"size-8\", className)}>\n {user.avatarUrl ? <AvatarImage src={user.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback className={cn(\"text-xs font-semibold\", getAvatarToneClasses(user.name))}>\n {getVietnameseInitials(user.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface UserMenuProps {\n className?: string;\n /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */\n showName?: boolean;\n /** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */\n profileHref?: string;\n /** Link cài đặt (nhãn `settings`). */\n settingsHref?: string;\n /** Link trợ giúp (nhãn `help`). */\n helpHref?: string;\n /** Mục bổ sung của app — chèn ngay trên mục đăng xuất. */\n items?: UserMenuItem[];\n /**\n * Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.\n *\n * `\"mobile\"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có\n * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.\n * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn\n * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.\n * `false` — không bao giờ hiện.\n *\n * ⚠ Nhánh `\"mobile\"` ẩn bằng CSS (`md:hidden`), không phải bằng điều kiện render: nút gạt theo\n * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại\n * phần tử khỏi CẢ cây trợ năng nên trình đọc màn hình ở desktop cũng không thấy mục thừa.\n */\n showThemeSubmenu?: boolean | \"mobile\";\n /** Ghi đè handler đăng xuất của context. */\n onLogout?: () => void;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,\n * submenu đổi giao diện, và mục đăng xuất (tô màu destructive).\n *\n * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.\n */\nexport function UserMenu({\n className,\n showName = false,\n profileHref,\n settingsHref,\n helpHref,\n items = [],\n showThemeSubmenu = \"mobile\",\n onLogout,\n align = \"end\"\n}: UserMenuProps) {\n const shell = useShell();\n const { theme, setTheme } = useTheme();\n const { user, labels } = shell;\n\n if (!user) return null;\n\n const handleLogout = onLogout ?? shell.onLogout;\n\n const renderItem = (item: UserMenuItem) => {\n const Icon = item.icon;\n const content = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n <span className=\"truncate\">{item.label}</span>\n {item.shortcut ? <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut> : null}\n </>\n );\n\n if (item.href) {\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} asChild>\n <ShellNavLink href={item.href} onClick={() => item.onSelect?.()}>\n {content}\n </ShellNavLink>\n </DropdownMenuItem>\n );\n }\n\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} onClick={() => item.onSelect?.()}>\n {content}\n </DropdownMenuItem>\n );\n };\n\n const builtInItems: (UserMenuItem | null)[] = [\n profileHref ? { id: \"profile\", label: labels.profile, icon: CircleUser, href: profileHref } : null,\n settingsHref ? { id: \"settings\", label: labels.settings, icon: Settings, href: settingsHref } : null,\n helpHref ? { id: \"help\", label: labels.help, icon: HelpCircle, href: helpHref } : null\n ];\n const visibleBuiltIns = builtInItems.filter((item): item is UserMenuItem => item !== null);\n\n const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.account}\n className={cn(\n \"flex items-center gap-2 rounded-md p-1 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-muted\",\n className\n )}\n >\n <UserAvatar user={user} />\n {showName ? (\n <>\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">{user.name}</span>\n {user.role ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{user.role}</span>\n ) : null}\n </span>\n <ChevronDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n </button>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align={align} className=\"w-60\">\n <DropdownMenuLabel className=\"flex items-center gap-2 font-normal\">\n <UserAvatar user={user} />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email ? <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span> : null}\n {user.role ? <span className=\"truncate text-[11px] text-muted-foreground\">{user.role}</span> : null}\n </span>\n </DropdownMenuLabel>\n\n {hasEntries ? <DropdownMenuSeparator /> : null}\n\n {visibleBuiltIns.map(renderItem)}\n {items.map(renderItem)}\n\n {showThemeSubmenu !== false ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className={cn(showThemeSubmenu === \"mobile\" && \"md:hidden\")}>\n <Palette className=\"size-4\" />\n <span>{labels.appearance}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n <DropdownMenuItem onClick={() => setTheme(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n\n {handleLogout ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n <span>{labels.logout}</span>\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
@@ -60,7 +60,7 @@ declare function isNavItemActive(item: NavItem, pathname: string, siblings?: Nav
60
60
  declare function isNavBranchActive(item: NavItem, pathname: string, siblings?: NavItem[], searchParams?: NavSearchParams | null): boolean;
61
61
  /** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */
62
62
  declare function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[];
63
- /** App chưa mở khoá — hiển thị ở nhóm "Khám phá thêm" của AppLauncher. */
63
+ /** App chưa mở khoá — hiển thị ở nhóm `discoverApps` của AppLauncher. */
64
64
  declare function getLockedApps(apps: AppDescriptor[]): AppDescriptor[];
65
65
  /**
66
66
  * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để