@comitor/ui 0.1.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 (307) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +458 -0
  3. package/dist/chart/chart.d.ts +88 -0
  4. package/dist/chart/chart.js +199 -0
  5. package/dist/chart/chart.js.map +1 -0
  6. package/dist/chart/comitor-chart.d.ts +109 -0
  7. package/dist/chart/comitor-chart.js +189 -0
  8. package/dist/chart/comitor-chart.js.map +1 -0
  9. package/dist/chart/index.d.ts +4 -0
  10. package/dist/chart/index.js +3 -0
  11. package/dist/chart/index.js.map +1 -0
  12. package/dist/components/composite/avatar-group.d.ts +33 -0
  13. package/dist/components/composite/avatar-group.js +86 -0
  14. package/dist/components/composite/avatar-group.js.map +1 -0
  15. package/dist/components/composite/combobox.d.ts +70 -0
  16. package/dist/components/composite/combobox.js +291 -0
  17. package/dist/components/composite/combobox.js.map +1 -0
  18. package/dist/components/composite/command-palette.d.ts +56 -0
  19. package/dist/components/composite/command-palette.js +131 -0
  20. package/dist/components/composite/command-palette.js.map +1 -0
  21. package/dist/components/composite/confirm-dialog.d.ts +40 -0
  22. package/dist/components/composite/confirm-dialog.js +96 -0
  23. package/dist/components/composite/confirm-dialog.js.map +1 -0
  24. package/dist/components/composite/copy-button.d.ts +27 -0
  25. package/dist/components/composite/copy-button.js +80 -0
  26. package/dist/components/composite/copy-button.js.map +1 -0
  27. package/dist/components/composite/data-table.d.ts +136 -0
  28. package/dist/components/composite/data-table.js +300 -0
  29. package/dist/components/composite/data-table.js.map +1 -0
  30. package/dist/components/composite/date-picker.d.ts +77 -0
  31. package/dist/components/composite/date-picker.js +268 -0
  32. package/dist/components/composite/date-picker.js.map +1 -0
  33. package/dist/components/composite/empty-state.d.ts +38 -0
  34. package/dist/components/composite/empty-state.js +35 -0
  35. package/dist/components/composite/empty-state.js.map +1 -0
  36. package/dist/components/composite/filter-chips.d.ts +39 -0
  37. package/dist/components/composite/filter-chips.js +79 -0
  38. package/dist/components/composite/filter-chips.js.map +1 -0
  39. package/dist/components/composite/form-field.d.ts +58 -0
  40. package/dist/components/composite/form-field.js +81 -0
  41. package/dist/components/composite/form-field.js.map +1 -0
  42. package/dist/components/composite/form-section.d.ts +33 -0
  43. package/dist/components/composite/form-section.js +85 -0
  44. package/dist/components/composite/form-section.js.map +1 -0
  45. package/dist/components/composite/inline-edit.d.ts +38 -0
  46. package/dist/components/composite/inline-edit.js +165 -0
  47. package/dist/components/composite/inline-edit.js.map +1 -0
  48. package/dist/components/composite/keyboard-hint.d.ts +31 -0
  49. package/dist/components/composite/keyboard-hint.js +72 -0
  50. package/dist/components/composite/keyboard-hint.js.map +1 -0
  51. package/dist/components/composite/letter-avatar.d.ts +47 -0
  52. package/dist/components/composite/letter-avatar.js +66 -0
  53. package/dist/components/composite/letter-avatar.js.map +1 -0
  54. package/dist/components/composite/page-container.d.ts +25 -0
  55. package/dist/components/composite/page-container.js +23 -0
  56. package/dist/components/composite/page-container.js.map +1 -0
  57. package/dist/components/composite/page-header.d.ts +49 -0
  58. package/dist/components/composite/page-header.js +75 -0
  59. package/dist/components/composite/page-header.js.map +1 -0
  60. package/dist/components/composite/relative-time.d.ts +28 -0
  61. package/dist/components/composite/relative-time.js +44 -0
  62. package/dist/components/composite/relative-time.js.map +1 -0
  63. package/dist/components/composite/search-filter-bar.d.ts +38 -0
  64. package/dist/components/composite/search-filter-bar.js +135 -0
  65. package/dist/components/composite/search-filter-bar.js.map +1 -0
  66. package/dist/components/composite/stat-card.d.ts +48 -0
  67. package/dist/components/composite/stat-card.js +76 -0
  68. package/dist/components/composite/stat-card.js.map +1 -0
  69. package/dist/components/composite/status-config.d.ts +85 -0
  70. package/dist/components/composite/status-config.js +41 -0
  71. package/dist/components/composite/status-config.js.map +1 -0
  72. package/dist/components/composite/status-pill.d.ts +26 -0
  73. package/dist/components/composite/status-pill.js +59 -0
  74. package/dist/components/composite/status-pill.js.map +1 -0
  75. package/dist/components/composite/table-pagination.d.ts +31 -0
  76. package/dist/components/composite/table-pagination.js +103 -0
  77. package/dist/components/composite/table-pagination.js.map +1 -0
  78. package/dist/components/composite/text-utils.d.ts +38 -0
  79. package/dist/components/composite/text-utils.js +40 -0
  80. package/dist/components/composite/text-utils.js.map +1 -0
  81. package/dist/components/composite/toast.d.ts +56 -0
  82. package/dist/components/composite/toast.js +63 -0
  83. package/dist/components/composite/toast.js.map +1 -0
  84. package/dist/components/ui/accordion.d.ts +17 -0
  85. package/dist/components/ui/accordion.js +53 -0
  86. package/dist/components/ui/accordion.js.map +1 -0
  87. package/dist/components/ui/alert-dialog.d.ts +34 -0
  88. package/dist/components/ui/alert-dialog.js +103 -0
  89. package/dist/components/ui/alert-dialog.js.map +1 -0
  90. package/dist/components/ui/alert.d.ts +16 -0
  91. package/dist/components/ui/alert.js +50 -0
  92. package/dist/components/ui/alert.js.map +1 -0
  93. package/dist/components/ui/aspect-ratio.d.ts +7 -0
  94. package/dist/components/ui/aspect-ratio.js +10 -0
  95. package/dist/components/ui/aspect-ratio.js.map +1 -0
  96. package/dist/components/ui/avatar.d.ts +11 -0
  97. package/dist/components/ui/avatar.js +33 -0
  98. package/dist/components/ui/avatar.js.map +1 -0
  99. package/dist/components/ui/badge.d.ts +15 -0
  100. package/dist/components/ui/badge.js +37 -0
  101. package/dist/components/ui/badge.js.map +1 -0
  102. package/dist/components/ui/breadcrumb.d.ts +30 -0
  103. package/dist/components/ui/breadcrumb.js +89 -0
  104. package/dist/components/ui/breadcrumb.js.map +1 -0
  105. package/dist/components/ui/button-group.d.ts +21 -0
  106. package/dist/components/ui/button-group.js +62 -0
  107. package/dist/components/ui/button-group.js.map +1 -0
  108. package/dist/components/ui/button.d.ts +21 -0
  109. package/dist/components/ui/button.js +70 -0
  110. package/dist/components/ui/button.js.map +1 -0
  111. package/dist/components/ui/calendar.d.ts +26 -0
  112. package/dist/components/ui/calendar.js +148 -0
  113. package/dist/components/ui/calendar.js.map +1 -0
  114. package/dist/components/ui/card.d.ts +33 -0
  115. package/dist/components/ui/card.js +70 -0
  116. package/dist/components/ui/card.js.map +1 -0
  117. package/dist/components/ui/carousel.d.ts +46 -0
  118. package/dist/components/ui/carousel.js +173 -0
  119. package/dist/components/ui/carousel.js.map +1 -0
  120. package/dist/components/ui/checkbox.d.ts +17 -0
  121. package/dist/components/ui/checkbox.js +67 -0
  122. package/dist/components/ui/checkbox.js.map +1 -0
  123. package/dist/components/ui/collapsible.d.ts +17 -0
  124. package/dist/components/ui/collapsible.js +18 -0
  125. package/dist/components/ui/collapsible.js.map +1 -0
  126. package/dist/components/ui/command.d.ts +41 -0
  127. package/dist/components/ui/command.js +122 -0
  128. package/dist/components/ui/command.js.map +1 -0
  129. package/dist/components/ui/context-menu.d.ts +51 -0
  130. package/dist/components/ui/context-menu.js +166 -0
  131. package/dist/components/ui/context-menu.js.map +1 -0
  132. package/dist/components/ui/dialog.d.ts +48 -0
  133. package/dist/components/ui/dialog.js +131 -0
  134. package/dist/components/ui/dialog.js.map +1 -0
  135. package/dist/components/ui/drawer.d.ts +32 -0
  136. package/dist/components/ui/drawer.js +102 -0
  137. package/dist/components/ui/drawer.js.map +1 -0
  138. package/dist/components/ui/dropdown-menu.d.ts +52 -0
  139. package/dist/components/ui/dropdown-menu.js +167 -0
  140. package/dist/components/ui/dropdown-menu.js.map +1 -0
  141. package/dist/components/ui/empty.d.ts +22 -0
  142. package/dist/components/ui/empty.js +87 -0
  143. package/dist/components/ui/empty.js.map +1 -0
  144. package/dist/components/ui/field.d.ts +46 -0
  145. package/dist/components/ui/field.js +197 -0
  146. package/dist/components/ui/field.js.map +1 -0
  147. package/dist/components/ui/form.d.ts +50 -0
  148. package/dist/components/ui/form.js +100 -0
  149. package/dist/components/ui/form.js.map +1 -0
  150. package/dist/components/ui/hover-card.d.ts +17 -0
  151. package/dist/components/ui/hover-card.js +31 -0
  152. package/dist/components/ui/hover-card.js.map +1 -0
  153. package/dist/components/ui/input-group.d.ts +26 -0
  154. package/dist/components/ui/input-group.js +148 -0
  155. package/dist/components/ui/input-group.js.map +1 -0
  156. package/dist/components/ui/input-otp.d.ts +23 -0
  157. package/dist/components/ui/input-otp.js +54 -0
  158. package/dist/components/ui/input-otp.js.map +1 -0
  159. package/dist/components/ui/input.d.ts +21 -0
  160. package/dist/components/ui/input.js +62 -0
  161. package/dist/components/ui/input.js.map +1 -0
  162. package/dist/components/ui/item.d.ts +39 -0
  163. package/dist/components/ui/item.js +142 -0
  164. package/dist/components/ui/item.js.map +1 -0
  165. package/dist/components/ui/kbd.d.ts +8 -0
  166. package/dist/components/ui/kbd.js +25 -0
  167. package/dist/components/ui/kbd.js.map +1 -0
  168. package/dist/components/ui/label.d.ts +7 -0
  169. package/dist/components/ui/label.js +21 -0
  170. package/dist/components/ui/label.js.map +1 -0
  171. package/dist/components/ui/menubar.d.ts +51 -0
  172. package/dist/components/ui/menubar.js +196 -0
  173. package/dist/components/ui/menubar.js.map +1 -0
  174. package/dist/components/ui/navigation-menu.d.ts +39 -0
  175. package/dist/components/ui/navigation-menu.js +123 -0
  176. package/dist/components/ui/navigation-menu.js.map +1 -0
  177. package/dist/components/ui/pagination.d.ts +40 -0
  178. package/dist/components/ui/pagination.js +95 -0
  179. package/dist/components/ui/pagination.js.map +1 -0
  180. package/dist/components/ui/popover.d.ts +19 -0
  181. package/dist/components/ui/popover.js +35 -0
  182. package/dist/components/ui/popover.js.map +1 -0
  183. package/dist/components/ui/progress.d.ts +12 -0
  184. package/dist/components/ui/progress.js +27 -0
  185. package/dist/components/ui/progress.js.map +1 -0
  186. package/dist/components/ui/radio-group.d.ts +9 -0
  187. package/dist/components/ui/radio-group.js +34 -0
  188. package/dist/components/ui/radio-group.js.map +1 -0
  189. package/dist/components/ui/resizable.d.ts +19 -0
  190. package/dist/components/ui/resizable.js +38 -0
  191. package/dist/components/ui/resizable.js.map +1 -0
  192. package/dist/components/ui/scroll-area.d.ts +13 -0
  193. package/dist/components/ui/scroll-area.js +46 -0
  194. package/dist/components/ui/scroll-area.js.map +1 -0
  195. package/dist/components/ui/select.d.ts +33 -0
  196. package/dist/components/ui/select.js +133 -0
  197. package/dist/components/ui/select.js.map +1 -0
  198. package/dist/components/ui/separator.d.ts +7 -0
  199. package/dist/components/ui/separator.js +23 -0
  200. package/dist/components/ui/separator.js.map +1 -0
  201. package/dist/components/ui/sheet.d.ts +38 -0
  202. package/dist/components/ui/sheet.js +96 -0
  203. package/dist/components/ui/sheet.js.map +1 -0
  204. package/dist/components/ui/skeleton.d.ts +6 -0
  205. package/dist/components/ui/skeleton.js +9 -0
  206. package/dist/components/ui/skeleton.js.map +1 -0
  207. package/dist/components/ui/slider.d.ts +12 -0
  208. package/dist/components/ui/slider.js +54 -0
  209. package/dist/components/ui/slider.js.map +1 -0
  210. package/dist/components/ui/sonner.d.ts +9 -0
  211. package/dist/components/ui/sonner.js +25 -0
  212. package/dist/components/ui/sonner.js.map +1 -0
  213. package/dist/components/ui/spinner.d.ts +6 -0
  214. package/dist/components/ui/spinner.js +10 -0
  215. package/dist/components/ui/spinner.js.map +1 -0
  216. package/dist/components/ui/switch.d.ts +14 -0
  217. package/dist/components/ui/switch.js +65 -0
  218. package/dist/components/ui/switch.js.map +1 -0
  219. package/dist/components/ui/table.d.ts +30 -0
  220. package/dist/components/ui/table.js +87 -0
  221. package/dist/components/ui/table.js.map +1 -0
  222. package/dist/components/ui/tabs.d.ts +35 -0
  223. package/dist/components/ui/tabs.js +54 -0
  224. package/dist/components/ui/tabs.js.map +1 -0
  225. package/dist/components/ui/textarea.d.ts +6 -0
  226. package/dist/components/ui/textarea.js +19 -0
  227. package/dist/components/ui/textarea.js.map +1 -0
  228. package/dist/components/ui/toggle-group.d.ts +18 -0
  229. package/dist/components/ui/toggle-group.js +52 -0
  230. package/dist/components/ui/toggle-group.js.map +1 -0
  231. package/dist/components/ui/toggle.d.ts +15 -0
  232. package/dist/components/ui/toggle.js +33 -0
  233. package/dist/components/ui/toggle.js.map +1 -0
  234. package/dist/components/ui/tooltip.d.ts +21 -0
  235. package/dist/components/ui/tooltip.js +38 -0
  236. package/dist/components/ui/tooltip.js.map +1 -0
  237. package/dist/form.d.ts +5 -0
  238. package/dist/form.js +2 -0
  239. package/dist/form.js.map +1 -0
  240. package/dist/index.d.ts +111 -0
  241. package/dist/index.js +76 -0
  242. package/dist/index.js.map +1 -0
  243. package/dist/lib/cn.d.ts +16 -0
  244. package/dist/lib/cn.js +9 -0
  245. package/dist/lib/cn.js.map +1 -0
  246. package/dist/shell/app-header.d.ts +41 -0
  247. package/dist/shell/app-header.js +136 -0
  248. package/dist/shell/app-header.js.map +1 -0
  249. package/dist/shell/app-launcher.d.ts +33 -0
  250. package/dist/shell/app-launcher.js +189 -0
  251. package/dist/shell/app-launcher.js.map +1 -0
  252. package/dist/shell/app-shell.d.ts +44 -0
  253. package/dist/shell/app-shell.js +99 -0
  254. package/dist/shell/app-shell.js.map +1 -0
  255. package/dist/shell/comitor-lockup.d.ts +33 -0
  256. package/dist/shell/comitor-lockup.js +112 -0
  257. package/dist/shell/comitor-lockup.js.map +1 -0
  258. package/dist/shell/comitor-logo.d.ts +28 -0
  259. package/dist/shell/comitor-logo.js +92 -0
  260. package/dist/shell/comitor-logo.js.map +1 -0
  261. package/dist/shell/index.d.ts +24 -0
  262. package/dist/shell/index.js +23 -0
  263. package/dist/shell/index.js.map +1 -0
  264. package/dist/shell/mobile-menu.d.ts +26 -0
  265. package/dist/shell/mobile-menu.js +56 -0
  266. package/dist/shell/mobile-menu.js.map +1 -0
  267. package/dist/shell/notifications-button.d.ts +22 -0
  268. package/dist/shell/notifications-button.js +39 -0
  269. package/dist/shell/notifications-button.js.map +1 -0
  270. package/dist/shell/shell-context.d.ts +91 -0
  271. package/dist/shell/shell-context.js +233 -0
  272. package/dist/shell/shell-context.js.map +1 -0
  273. package/dist/shell/shell-link.d.ts +19 -0
  274. package/dist/shell/shell-link.js +18 -0
  275. package/dist/shell/shell-link.js.map +1 -0
  276. package/dist/shell/sidebar-group.d.ts +21 -0
  277. package/dist/shell/sidebar-group.js +43 -0
  278. package/dist/shell/sidebar-group.js.map +1 -0
  279. package/dist/shell/sidebar-menu-item.d.ts +27 -0
  280. package/dist/shell/sidebar-menu-item.js +152 -0
  281. package/dist/shell/sidebar-menu-item.js.map +1 -0
  282. package/dist/shell/sidebar.d.ts +26 -0
  283. package/dist/shell/sidebar.js +92 -0
  284. package/dist/shell/sidebar.js.map +1 -0
  285. package/dist/shell/theme-provider.d.ts +18 -0
  286. package/dist/shell/theme-provider.js +29 -0
  287. package/dist/shell/theme-provider.js.map +1 -0
  288. package/dist/shell/theme-toggle.d.ts +28 -0
  289. package/dist/shell/theme-toggle.js +67 -0
  290. package/dist/shell/theme-toggle.js.map +1 -0
  291. package/dist/shell/types.d.ts +168 -0
  292. package/dist/shell/types.js +38 -0
  293. package/dist/shell/types.js.map +1 -0
  294. package/dist/shell/user-menu.d.ts +42 -0
  295. package/dist/shell/user-menu.js +132 -0
  296. package/dist/shell/user-menu.js.map +1 -0
  297. package/dist/shell/utils.d.ts +62 -0
  298. package/dist/shell/utils.js +73 -0
  299. package/dist/shell/utils.js.map +1 -0
  300. package/dist/shell/workspace-switcher.d.ts +24 -0
  301. package/dist/shell/workspace-switcher.js +219 -0
  302. package/dist/shell/workspace-switcher.js.map +1 -0
  303. package/dist/tokens.d.ts +173 -0
  304. package/dist/tokens.js +161 -0
  305. package/dist/tokens.js.map +1 -0
  306. package/package.json +151 -0
  307. package/styles.css +357 -0
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Monitor, Moon, Sun } from "lucide-react";
4
+ import { useTheme } from "next-themes";
5
+ import { useEffect, useState } from "react";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger
11
+ } from "../components/ui/dropdown-menu.js";
12
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
13
+ import { cn } from "../lib/cn.js";
14
+ import { DEFAULT_SHELL_LABELS } from "./types.js";
15
+ function ThemeToggle({ className, variant = "icon", labels: labelsProp }) {
16
+ const { theme, resolvedTheme, setTheme } = useTheme();
17
+ const labels = { ...DEFAULT_SHELL_LABELS, ...labelsProp };
18
+ const [mounted, setMounted] = useState(false);
19
+ useEffect(() => {
20
+ setMounted(true);
21
+ }, []);
22
+ const buttonClass = cn(
23
+ "flex size-9 items-center justify-center rounded-md text-muted-foreground",
24
+ "transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground",
25
+ className
26
+ );
27
+ if (!mounted) {
28
+ return /* @__PURE__ */ jsx("span", { className: buttonClass, "aria-hidden": true });
29
+ }
30
+ if (variant === "menu") {
31
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
32
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", className: buttonClass, "aria-label": labels.toggleTheme, children: resolvedTheme === "dark" ? /* @__PURE__ */ jsx(Moon, { className: "size-4" }) : /* @__PURE__ */ jsx(Sun, { className: "size-4" }) }) }),
33
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
34
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setTheme("light"), className: cn(theme === "light" && "bg-accent"), children: [
35
+ /* @__PURE__ */ jsx(Sun, { className: "size-4" }),
36
+ labels.themeLight
37
+ ] }),
38
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setTheme("dark"), className: cn(theme === "dark" && "bg-accent"), children: [
39
+ /* @__PURE__ */ jsx(Moon, { className: "size-4" }),
40
+ labels.themeDark
41
+ ] }),
42
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setTheme("system"), className: cn(theme === "system" && "bg-accent"), children: [
43
+ /* @__PURE__ */ jsx(Monitor, { className: "size-4" }),
44
+ labels.themeSystem
45
+ ] })
46
+ ] })
47
+ ] });
48
+ }
49
+ const nextTheme = resolvedTheme === "dark" ? "light" : "dark";
50
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
51
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
52
+ "button",
53
+ {
54
+ type: "button",
55
+ onClick: () => setTheme(nextTheme),
56
+ className: buttonClass,
57
+ "aria-label": labels.toggleTheme,
58
+ children: resolvedTheme === "dark" ? /* @__PURE__ */ jsx(Sun, { className: "size-4" }) : /* @__PURE__ */ jsx(Moon, { className: "size-4" })
59
+ }
60
+ ) }),
61
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: nextTheme === "dark" ? labels.themeDark : labels.themeLight })
62
+ ] });
63
+ }
64
+ export {
65
+ ThemeToggle
66
+ };
67
+ //# sourceMappingURL=theme-toggle.js.map
@@ -0,0 +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\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\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 }: ThemeToggleProps) {\n const { theme, resolvedTheme, setTheme } = useTheme();\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={() => 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 </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n const nextTheme = resolvedTheme === \"dark\" ? \"light\" : \"dark\";\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={() => setTheme(nextTheme)}\n className={buttonClass}\n aria-label={labels.toggleTheme}\n >\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":";AAsDW,cAYD,YAZC;AApDX,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;AAyBhD,SAAS,YAAY,EAAE,WAAW,UAAU,QAAQ,QAAQ,WAAW,GAAqB;AACjG,QAAM,EAAE,OAAO,eAAe,SAAS,IAAI,SAAS;AACpD,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,SAAS,OAAO,GAAG,WAAW,GAAG,UAAU,WAAW,WAAW,GAChG;AAAA,8BAAC,OAAI,WAAU,UAAS;AAAA,UACvB,OAAO;AAAA,WACV;AAAA,QACA,qBAAC,oBAAiB,SAAS,MAAM,SAAS,MAAM,GAAG,WAAW,GAAG,UAAU,UAAU,WAAW,GAC9F;AAAA,8BAAC,QAAK,WAAU,UAAS;AAAA,UACxB,OAAO;AAAA,WACV;AAAA,QACA,qBAAC,oBAAiB,SAAS,MAAM,SAAS,QAAQ,GAAG,WAAW,GAAG,UAAU,YAAY,WAAW,GAClG;AAAA,8BAAC,WAAQ,WAAU,UAAS;AAAA,UAC3B,OAAO;AAAA,WACV;AAAA,SACF;AAAA,OACF;AAAA,EAEJ;AAEA,QAAM,YAAY,kBAAkB,SAAS,UAAU;AAEvD,SACE,qBAAC,WACC;AAAA,wBAAC,kBAAe,SAAO,MACrB;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM,SAAS,SAAS;AAAA,QACjC,WAAW;AAAA,QACX,cAAY,OAAO;AAAA,QAElB,4BAAkB,SAAS,oBAAC,OAAI,WAAU,UAAS,IAAK,oBAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IACpF,GACF;AAAA,IACA,oBAAC,kBAAe,MAAK,UAAU,wBAAc,SAAS,OAAO,YAAY,OAAO,YAAW;AAAA,KAC7F;AAEJ;","names":[]}
@@ -0,0 +1,168 @@
1
+ import { LucideIcon } from 'lucide-react';
2
+ import { ComponentType, AnchorHTMLAttributes } from 'react';
3
+
4
+ /**
5
+ * Hợp đồng dữ liệu của Tier 3 (shell).
6
+ *
7
+ * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /
8
+ * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**
9
+ * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.
10
+ *
11
+ * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace
12
+ * 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
13
+ * dùng, và ngược lại) nên không gộp được vào một menu phẳng:
14
+ * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)
15
+ * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)
16
+ */
17
+
18
+ /**
19
+ * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể
20
+ * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).
21
+ */
22
+ type ShellIcon = LucideIcon | ComponentType<{
23
+ className?: string;
24
+ }>;
25
+ /** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */
26
+ interface Workspace {
27
+ id: string;
28
+ name: string;
29
+ /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */
30
+ slug: string;
31
+ logoUrl?: string;
32
+ /** "Free" | "Pro" | "Business"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */
33
+ plan?: string;
34
+ /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */
35
+ role?: string;
36
+ memberCount?: number;
37
+ }
38
+ /** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */
39
+ interface AppDescriptor {
40
+ /** "tasks" | "chat" | "crm" | "hr" | … */
41
+ id: string;
42
+ /** Tên hiển thị (mặc định tiếng Việt). */
43
+ name: string;
44
+ description?: string;
45
+ icon: ShellIcon;
46
+ href: string;
47
+ /**
48
+ * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).
49
+ * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.
50
+ */
51
+ accent?: string;
52
+ /** Màu chữ trên nền accent; mặc định giữ `--app-accent-foreground` của theme. */
53
+ accentForeground?: string;
54
+ /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */
55
+ entitled?: boolean;
56
+ badge?: number | string;
57
+ }
58
+ /** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */
59
+ type NavBadgeVariant = "default" | "destructive" | "warning" | "success";
60
+ /** Một mục điều hướng trong sidebar. */
61
+ interface NavItem {
62
+ id: string;
63
+ label: string;
64
+ href: string;
65
+ icon?: ShellIcon;
66
+ badge?: number | string;
67
+ badgeVariant?: NavBadgeVariant;
68
+ /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */
69
+ children?: NavItem[];
70
+ /**
71
+ * Ép trạng thái active thay vì để shell tự suy từ `pathname`
72
+ * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).
73
+ */
74
+ isActive?: boolean;
75
+ /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */
76
+ external?: boolean;
77
+ disabled?: boolean;
78
+ }
79
+ /** Nhóm mục menu (Linear-style: nhãn nhỏ + thu gọn được). */
80
+ interface NavGroup {
81
+ id: string;
82
+ /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */
83
+ label?: string;
84
+ items: NavItem[];
85
+ /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */
86
+ collapsible?: boolean;
87
+ /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */
88
+ defaultCollapsed?: boolean;
89
+ }
90
+ /**
91
+ * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.
92
+ * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.
93
+ */
94
+ type ShellNav = NavItem[] | NavGroup[];
95
+ /** Người dùng đang đăng nhập. */
96
+ interface ShellUser {
97
+ id: string;
98
+ name: string;
99
+ email?: string;
100
+ avatarUrl?: string;
101
+ /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */
102
+ role?: string;
103
+ }
104
+ /** Mục bổ sung do app chèn vào `UserMenu`. */
105
+ interface UserMenuItem {
106
+ id: string;
107
+ label: string;
108
+ icon?: ShellIcon;
109
+ /** Có `href` thì render link; không thì render nút gọi `onSelect`. */
110
+ href?: string;
111
+ onSelect?: () => void;
112
+ /** 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). */
113
+ shortcut?: string;
114
+ variant?: "default" | "destructive";
115
+ }
116
+ /**
117
+ * Props của component link do app cung cấp (`LinkComponent`).
118
+ * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền
119
+ * component router của mình miễn khớp hình dạng props này.
120
+ */
121
+ interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
122
+ /** 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. */
123
+ href: string;
124
+ }
125
+ /** Component link thay thế `next/link`. */
126
+ type ShellLinkComponent = ComponentType<ShellLinkProps>;
127
+ /**
128
+ * Toàn bộ chuỗi hiển thị của shell — app tiếng Anh (hoặc ngôn ngữ khác) override
129
+ * từng khoá qua prop `labels`. Mặc định là tiếng Việt.
130
+ */
131
+ interface ShellLabels {
132
+ collapseSidebar: string;
133
+ expandSidebar: string;
134
+ openMenu: string;
135
+ closeMenu: string;
136
+ search: string;
137
+ searchPlaceholder: string;
138
+ notifications: string;
139
+ navigationLabel: string;
140
+ workspace: string;
141
+ workspaceSwitcherLabel: string;
142
+ workspaceSearchPlaceholder: string;
143
+ workspaceEmpty: string;
144
+ createWorkspace: string;
145
+ workspaceSettings: string;
146
+ inviteMembers: string;
147
+ members: string;
148
+ apps: string;
149
+ appLauncherLabel: string;
150
+ myApps: string;
151
+ discoverApps: string;
152
+ locked: string;
153
+ lockedHint: string;
154
+ account: string;
155
+ profile: string;
156
+ settings: string;
157
+ appearance: string;
158
+ help: string;
159
+ logout: string;
160
+ themeLight: string;
161
+ themeDark: string;
162
+ themeSystem: string;
163
+ toggleTheme: string;
164
+ }
165
+ /** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
166
+ declare const DEFAULT_SHELL_LABELS: ShellLabels;
167
+
168
+ 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 };
@@ -0,0 +1,38 @@
1
+ const DEFAULT_SHELL_LABELS = {
2
+ collapseSidebar: "Thu g\u1ECDn thanh b\xEAn",
3
+ expandSidebar: "M\u1EDF r\u1ED9ng thanh b\xEAn",
4
+ openMenu: "M\u1EDF menu",
5
+ closeMenu: "\u0110\xF3ng menu",
6
+ search: "T\xECm ki\u1EBFm",
7
+ searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
8
+ notifications: "Th\xF4ng b\xE1o",
9
+ navigationLabel: "\u0110i\u1EC1u h\u01B0\u1EDBng ch\xEDnh",
10
+ workspace: "Kh\xF4ng gian l\xE0m vi\u1EC7c",
11
+ workspaceSwitcherLabel: "\u0110\u1ED5i kh\xF4ng gian l\xE0m vi\u1EC7c",
12
+ workspaceSearchPlaceholder: "T\xECm kh\xF4ng gian l\xE0m vi\u1EC7c\u2026",
13
+ workspaceEmpty: "Kh\xF4ng t\xECm th\u1EA5y kh\xF4ng gian l\xE0m vi\u1EC7c",
14
+ createWorkspace: "T\u1EA1o kh\xF4ng gian l\xE0m vi\u1EC7c",
15
+ workspaceSettings: "C\xE0i \u0111\u1EB7t kh\xF4ng gian l\xE0m vi\u1EC7c",
16
+ inviteMembers: "M\u1EDDi th\xE0nh vi\xEAn",
17
+ members: "th\xE0nh vi\xEAn",
18
+ apps: "\u1EE8ng d\u1EE5ng",
19
+ appLauncherLabel: "M\u1EDF danh s\xE1ch \u1EE9ng d\u1EE5ng",
20
+ myApps: "\u1EE8ng d\u1EE5ng c\u1EE7a b\u1EA1n",
21
+ discoverApps: "Kh\xE1m ph\xE1 th\xEAm",
22
+ locked: "Ch\u01B0a m\u1EDF kho\xE1",
23
+ lockedHint: "\u1EE8ng d\u1EE5ng ch\u01B0a c\xF3 trong g\xF3i hi\u1EC7n t\u1EA1i",
24
+ account: "T\xE0i kho\u1EA3n",
25
+ profile: "H\u1ED3 s\u01A1",
26
+ settings: "C\xE0i \u0111\u1EB7t",
27
+ appearance: "Giao di\u1EC7n",
28
+ help: "Tr\u1EE3 gi\xFAp",
29
+ logout: "\u0110\u0103ng xu\u1EA5t",
30
+ themeLight: "S\xE1ng",
31
+ themeDark: "T\u1ED1i",
32
+ themeSystem: "H\u1EC7 th\u1ED1ng",
33
+ toggleTheme: "\u0110\u1ED5i giao di\u1EC7n"
34
+ };
35
+ export {
36
+ DEFAULT_SHELL_LABELS
37
+ };
38
+ //# sourceMappingURL=types.js.map
@@ -0,0 +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 * 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 accent?: string;\n /** Màu chữ trên nền accent; mặc định giữ `--app-accent-foreground` của theme. */\n accentForeground?: 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 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 members: 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 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: \"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":"AAoLO,MAAM,uBAAoC;AAAA,EAC/C,iBAAiB;AAAA,EACjB,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;AAAA,EACT,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":[]}
@@ -0,0 +1,42 @@
1
+ import * as React from 'react';
2
+ import { UserMenuItem, ShellUser } from './types.js';
3
+ import 'lucide-react';
4
+
5
+ interface UserAvatarProps {
6
+ user: ShellUser;
7
+ className?: string;
8
+ }
9
+ /**
10
+ * 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.
11
+ *
12
+ * Tô màu theo `user.name` chứ KHÔNG theo email: `LetterAvatar`/`AvatarGroup` ở Tier 2 seed
13
+ * 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.
14
+ */
15
+ declare function UserAvatar({ user, className }: UserAvatarProps): React.JSX.Element;
16
+ interface UserMenuProps {
17
+ className?: string;
18
+ /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */
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õ). */
21
+ profileHref?: string;
22
+ /** Link "Cài đặt". */
23
+ settingsHref?: string;
24
+ /** Link "Trợ giúp". */
25
+ helpHref?: string;
26
+ /** Mục bổ sung của app — chèn ngay trên "Đăng xuất". */
27
+ items?: UserMenuItem[];
28
+ /** Ẩn submenu đổi giao diện (khi header đã có `ThemeToggle` riêng và không muốn lặp). */
29
+ showThemeSubmenu?: boolean;
30
+ /** Ghi đè handler đăng xuất của context. */
31
+ onLogout?: () => void;
32
+ align?: "start" | "center" | "end";
33
+ }
34
+ /**
35
+ * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,
36
+ * submenu đổi giao diện, và "Đăng xuất" (tô màu destructive).
37
+ *
38
+ * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.
39
+ */
40
+ declare function UserMenu({ className, showName, profileHref, settingsHref, helpHref, items, showThemeSubmenu, onLogout, align }: UserMenuProps): React.JSX.Element | null;
41
+
42
+ export { UserAvatar, type UserAvatarProps, UserMenu, type UserMenuProps };
@@ -0,0 +1,132 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { ChevronDown, CircleUser, HelpCircle, LogOut, Monitor, Moon, Palette, Settings, Sun } from "lucide-react";
4
+ import { useTheme } from "next-themes";
5
+ import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar.js";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuLabel,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuShortcut,
13
+ DropdownMenuSub,
14
+ DropdownMenuSubContent,
15
+ DropdownMenuSubTrigger,
16
+ DropdownMenuTrigger
17
+ } from "../components/ui/dropdown-menu.js";
18
+ import { cn } from "../lib/cn.js";
19
+ import { useShell } from "./shell-context.js";
20
+ import { ShellNavLink } from "./shell-link.js";
21
+ import { getAvatarToneClass, getVietnameseInitials } from "./utils.js";
22
+ function UserAvatar({ user, className }) {
23
+ return /* @__PURE__ */ jsxs(Avatar, { className: cn("size-8", className), children: [
24
+ user.avatarUrl ? /* @__PURE__ */ jsx(AvatarImage, { src: user.avatarUrl, alt: "" }) : null,
25
+ /* @__PURE__ */ jsx(AvatarFallback, { className: cn("text-xs font-semibold", getAvatarToneClass(user.name)), children: getVietnameseInitials(user.name) })
26
+ ] });
27
+ }
28
+ function UserMenu({
29
+ className,
30
+ showName = false,
31
+ profileHref,
32
+ settingsHref,
33
+ helpHref,
34
+ items = [],
35
+ showThemeSubmenu = true,
36
+ onLogout,
37
+ align = "end"
38
+ }) {
39
+ const shell = useShell();
40
+ const { theme, setTheme } = useTheme();
41
+ const { user, labels } = shell;
42
+ if (!user) return null;
43
+ const handleLogout = onLogout ?? shell.onLogout;
44
+ const renderItem = (item) => {
45
+ const Icon = item.icon;
46
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
47
+ Icon ? /* @__PURE__ */ jsx(Icon, { className: "size-4" }) : null,
48
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: item.label }),
49
+ item.shortcut ? /* @__PURE__ */ jsx(DropdownMenuShortcut, { children: item.shortcut }) : null
50
+ ] });
51
+ if (item.href) {
52
+ return /* @__PURE__ */ jsx(DropdownMenuItem, { variant: item.variant, asChild: true, children: /* @__PURE__ */ jsx(ShellNavLink, { href: item.href, onClick: () => item.onSelect?.(), children: content }) }, item.id);
53
+ }
54
+ return /* @__PURE__ */ jsx(DropdownMenuItem, { variant: item.variant, onClick: () => item.onSelect?.(), children: content }, item.id);
55
+ };
56
+ const builtInItems = [
57
+ profileHref ? { id: "profile", label: labels.profile, icon: CircleUser, href: profileHref } : null,
58
+ settingsHref ? { id: "settings", label: labels.settings, icon: Settings, href: settingsHref } : null,
59
+ helpHref ? { id: "help", label: labels.help, icon: HelpCircle, href: helpHref } : null
60
+ ];
61
+ const visibleBuiltIns = builtInItems.filter((item) => item !== null);
62
+ const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;
63
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
64
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
65
+ "button",
66
+ {
67
+ type: "button",
68
+ "aria-label": labels.account,
69
+ className: cn(
70
+ "flex items-center gap-2 rounded-md p-1 text-left",
71
+ "transition-colors duration-150 ease-out hover:bg-muted",
72
+ className
73
+ ),
74
+ children: [
75
+ /* @__PURE__ */ jsx(UserAvatar, { user }),
76
+ showName ? /* @__PURE__ */ jsxs(Fragment, { children: [
77
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
78
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium leading-tight text-foreground", children: user.name }),
79
+ user.role ? /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] leading-tight text-muted-foreground", children: user.role }) : null
80
+ ] }),
81
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 shrink-0 text-muted-foreground" })
82
+ ] }) : null
83
+ ]
84
+ }
85
+ ) }),
86
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align, className: "w-60", children: [
87
+ /* @__PURE__ */ jsxs(DropdownMenuLabel, { className: "flex items-center gap-2 font-normal", children: [
88
+ /* @__PURE__ */ jsx(UserAvatar, { user }),
89
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
90
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium", children: user.name }),
91
+ user.email ? /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: user.email }) : null,
92
+ user.role ? /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: user.role }) : null
93
+ ] })
94
+ ] }),
95
+ hasEntries ? /* @__PURE__ */ jsx(DropdownMenuSeparator, {}) : null,
96
+ visibleBuiltIns.map(renderItem),
97
+ items.map(renderItem),
98
+ showThemeSubmenu ? /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [
99
+ /* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { children: [
100
+ /* @__PURE__ */ jsx(Palette, { className: "size-4" }),
101
+ /* @__PURE__ */ jsx("span", { children: labels.appearance })
102
+ ] }),
103
+ /* @__PURE__ */ jsxs(DropdownMenuSubContent, { children: [
104
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setTheme("light"), className: cn(theme === "light" && "bg-accent"), children: [
105
+ /* @__PURE__ */ jsx(Sun, { className: "size-4" }),
106
+ labels.themeLight
107
+ ] }),
108
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setTheme("dark"), className: cn(theme === "dark" && "bg-accent"), children: [
109
+ /* @__PURE__ */ jsx(Moon, { className: "size-4" }),
110
+ labels.themeDark
111
+ ] }),
112
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setTheme("system"), className: cn(theme === "system" && "bg-accent"), children: [
113
+ /* @__PURE__ */ jsx(Monitor, { className: "size-4" }),
114
+ labels.themeSystem
115
+ ] })
116
+ ] })
117
+ ] }) : null,
118
+ handleLogout ? /* @__PURE__ */ jsxs(Fragment, { children: [
119
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
120
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { variant: "destructive", onClick: handleLogout, children: [
121
+ /* @__PURE__ */ jsx(LogOut, { className: "size-4" }),
122
+ /* @__PURE__ */ jsx("span", { children: labels.logout })
123
+ ] })
124
+ ] }) : null
125
+ ] })
126
+ ] });
127
+ }
128
+ export {
129
+ UserAvatar,
130
+ UserMenu
131
+ };
132
+ //# sourceMappingURL=user-menu.js.map
@@ -0,0 +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 { 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 { getAvatarToneClass, 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\", getAvatarToneClass(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 đổi giao diện (khi header đã có `ThemeToggle` riêng và không muốn lặp). */\n showThemeSubmenu?: boolean;\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 = true,\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 ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger>\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":[]}
@@ -0,0 +1,62 @@
1
+ import { CSSProperties } from 'react';
2
+ import { AppDescriptor, NavGroup, NavItem, ShellNav } from './types.js';
3
+ export { getVietnameseInitials } from '../components/composite/text-utils.js';
4
+ import 'lucide-react';
5
+
6
+ /**
7
+ * Hàm thuần dùng chung cho tầng shell — KHÔNG hook, KHÔNG directive
8
+ * (dùng được ở cả server lẫn client).
9
+ */
10
+
11
+ /**
12
+ * Chuẩn hoá menu app truyền vào về `NavGroup[]`.
13
+ * Mảng phẳng `NavItem[]` → một nhóm không nhãn (id "default").
14
+ */
15
+ declare function toNavGroups(nav: ShellNav | undefined): NavGroup[];
16
+ /** Gộp mọi item của mọi nhóm thành một mảng phẳng (kể cả mục con). */
17
+ declare function flattenNavItems(groups: NavGroup[]): NavItem[];
18
+ /**
19
+ * Active matching "thông minh": chỉ dùng `startsWith` khi KHÔNG có mục nào khác
20
+ * khớp CHÍNH XÁC `pathname`. Nếu không, `/tasks` vẫn sáng khi đang đứng ở
21
+ * `/tasks/inbox` (có mục riêng cho nó).
22
+ *
23
+ * @param siblings Danh sách mục để đối chiếu khớp chính xác — nên truyền toàn bộ
24
+ * mục của app hiện tại (`flattenNavItems`), không chỉ mục cùng cấp.
25
+ */
26
+ declare function isNavItemActive(item: NavItem, pathname: string, siblings?: NavItem[]): boolean;
27
+ /** Mục cha được coi là active khi chính nó hoặc bất kỳ mục con nào active. */
28
+ declare function isNavBranchActive(item: NavItem, pathname: string, siblings?: NavItem[]): boolean;
29
+ /** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */
30
+ declare function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[];
31
+ /** App chưa mở khoá — hiển thị ở nhóm "Khám phá thêm" của AppLauncher. */
32
+ declare function getLockedApps(apps: AppDescriptor[]): AppDescriptor[];
33
+ /**
34
+ * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để
35
+ * "/crm/deals" không rơi nhầm vào app có href "/".
36
+ */
37
+ declare function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null;
38
+ /**
39
+ * Bỏ dấu tiếng Việt + hạ chữ thường để so khớp khi tìm kiếm.
40
+ * ("Đà Nẵng" → "da nang"). `đ/Đ` phải xử lý riêng vì NFD không tách được — phần bỏ dấu
41
+ * dùng lại `removeVietnameseTones` của Tier 2, ở đây chỉ thêm hạ chữ thường + trim.
42
+ */
43
+ declare function normalizeVi(value: string): string;
44
+ /**
45
+ * Class nền/chữ của avatar chữ cái theo TÊN.
46
+ *
47
+ * Chỉ là lối vào tầng shell của `getLetterAvatarTone` (`composite/letter-avatar.tsx`) —
48
+ * NGUỒN SỰ THẬT DUY NHẤT về màu avatar. 0.1.0 từng có bảng tông + hàm băm riêng ở đây,
49
+ * lệch cả bảng màu lẫn thuật toán băm với `LetterAvatar`, nên UserMenu/WorkspaceSwitcher
50
+ * tô một màu còn `AvatarGroup` của CÙNG một người tô màu khác trên CÙNG một trang.
51
+ *
52
+ * @param seed Phải là **tên hiển thị** (đúng thứ truyền vào prop `name` của `LetterAvatar`).
53
+ * Truyền email/slug sẽ lại lệch màu với avatar cùng người ở chỗ khác.
54
+ */
55
+ declare function getAvatarToneClass(seed: string): string;
56
+ /**
57
+ * Inline style gán `--app-accent` cho một app cụ thể.
58
+ * Trả về `undefined` khi app không khai accent → giữ nguyên token mặc định của theme.
59
+ */
60
+ declare function appAccentStyle(app: Pick<AppDescriptor, "accent" | "accentForeground"> | null | undefined): CSSProperties | undefined;
61
+
62
+ export { appAccentStyle, flattenNavItems, getAvatarToneClass, getEntitledApps, getLockedApps, isNavBranchActive, isNavItemActive, normalizeVi, resolveCurrentApp, toNavGroups };
@@ -0,0 +1,73 @@
1
+ import { getLetterAvatarTone } from "../components/composite/letter-avatar.js";
2
+ import { removeVietnameseTones } from "../components/composite/text-utils.js";
3
+ import { getVietnameseInitials } from "../components/composite/text-utils.js";
4
+ function isNavGroup(value) {
5
+ return Array.isArray(value.items);
6
+ }
7
+ function toNavGroups(nav) {
8
+ if (!nav || nav.length === 0) return [];
9
+ const first = nav[0];
10
+ if (first && isNavGroup(first)) return nav;
11
+ return [{ id: "default", items: nav }];
12
+ }
13
+ function flattenNavItems(groups) {
14
+ const out = [];
15
+ const walk = (items) => {
16
+ for (const item of items) {
17
+ out.push(item);
18
+ if (item.children?.length) walk(item.children);
19
+ }
20
+ };
21
+ for (const group of groups) walk(group.items);
22
+ return out;
23
+ }
24
+ function isNavItemActive(item, pathname, siblings = []) {
25
+ if (item.isActive !== void 0) return item.isActive;
26
+ if (!pathname || !item.href) return false;
27
+ if (pathname === item.href) return true;
28
+ const hasExactMatchElsewhere = siblings.some((other) => other.href !== item.href && pathname === other.href);
29
+ if (hasExactMatchElsewhere) return false;
30
+ return item.href !== "/" && pathname.startsWith(`${item.href}/`);
31
+ }
32
+ function isNavBranchActive(item, pathname, siblings = []) {
33
+ if (isNavItemActive(item, pathname, siblings)) return true;
34
+ return (item.children ?? []).some((child) => isNavBranchActive(child, pathname, siblings));
35
+ }
36
+ function getEntitledApps(apps) {
37
+ return apps.filter((app) => app.entitled !== false);
38
+ }
39
+ function getLockedApps(apps) {
40
+ return apps.filter((app) => app.entitled === false);
41
+ }
42
+ function resolveCurrentApp(apps, pathname) {
43
+ if (apps.length === 0) return null;
44
+ const matched = [...apps].sort((a, b) => b.href.length - a.href.length).find((app) => pathname === app.href || app.href !== "/" && pathname.startsWith(`${app.href}/`));
45
+ return matched ?? apps[0] ?? null;
46
+ }
47
+ function normalizeVi(value) {
48
+ return removeVietnameseTones(value).toLowerCase().trim();
49
+ }
50
+ function getAvatarToneClass(seed) {
51
+ return getLetterAvatarTone(seed);
52
+ }
53
+ function appAccentStyle(app) {
54
+ if (!app?.accent && !app?.accentForeground) return void 0;
55
+ const style = {};
56
+ if (app.accent) style["--app-accent"] = app.accent;
57
+ if (app.accentForeground) style["--app-accent-foreground"] = app.accentForeground;
58
+ return style;
59
+ }
60
+ export {
61
+ appAccentStyle,
62
+ flattenNavItems,
63
+ getAvatarToneClass,
64
+ getEntitledApps,
65
+ getLockedApps,
66
+ getVietnameseInitials,
67
+ isNavBranchActive,
68
+ isNavItemActive,
69
+ normalizeVi,
70
+ resolveCurrentApp,
71
+ toNavGroups
72
+ };
73
+ //# sourceMappingURL=utils.js.map