@comitor/ui 0.3.3 → 0.5.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 (74) hide show
  1. package/README.md +12 -8
  2. package/dist/components/composite/avatar-group.d.ts +13 -1
  3. package/dist/components/composite/avatar-group.js +19 -4
  4. package/dist/components/composite/avatar-group.js.map +1 -1
  5. package/dist/components/composite/combobox.js +169 -146
  6. package/dist/components/composite/combobox.js.map +1 -1
  7. package/dist/components/composite/data-table.d.ts +24 -1
  8. package/dist/components/composite/data-table.js +7 -1
  9. package/dist/components/composite/data-table.js.map +1 -1
  10. package/dist/components/composite/date-picker.js +46 -17
  11. package/dist/components/composite/date-picker.js.map +1 -1
  12. package/dist/components/composite/form-section.js +1 -1
  13. package/dist/components/composite/form-section.js.map +1 -1
  14. package/dist/components/composite/route-tabs.d.ts +75 -0
  15. package/dist/components/composite/route-tabs.js +50 -0
  16. package/dist/components/composite/route-tabs.js.map +1 -0
  17. package/dist/components/composite/stat-card.js +14 -10
  18. package/dist/components/composite/stat-card.js.map +1 -1
  19. package/dist/components/ui/calendar.d.ts +29 -24
  20. package/dist/components/ui/calendar.js +1 -1
  21. package/dist/components/ui/calendar.js.map +1 -1
  22. package/dist/components/ui/checkbox.js +2 -2
  23. package/dist/components/ui/checkbox.js.map +1 -1
  24. package/dist/components/ui/command.js +1 -1
  25. package/dist/components/ui/command.js.map +1 -1
  26. package/dist/components/ui/context-menu.js +4 -4
  27. package/dist/components/ui/context-menu.js.map +1 -1
  28. package/dist/components/ui/dropdown-menu.js +4 -4
  29. package/dist/components/ui/dropdown-menu.js.map +1 -1
  30. package/dist/components/ui/empty.js +1 -1
  31. package/dist/components/ui/empty.js.map +1 -1
  32. package/dist/components/ui/field.js +1 -1
  33. package/dist/components/ui/field.js.map +1 -1
  34. package/dist/components/ui/item.js +1 -1
  35. package/dist/components/ui/item.js.map +1 -1
  36. package/dist/components/ui/menubar.js +4 -4
  37. package/dist/components/ui/menubar.js.map +1 -1
  38. package/dist/components/ui/radio-group.js +1 -1
  39. package/dist/components/ui/radio-group.js.map +1 -1
  40. package/dist/components/ui/select.d.ts +35 -12
  41. package/dist/components/ui/select.js +1 -1
  42. package/dist/components/ui/select.js.map +1 -1
  43. package/dist/components/ui/switch.js +2 -2
  44. package/dist/components/ui/switch.js.map +1 -1
  45. package/dist/components/ui/table.js +2 -2
  46. package/dist/components/ui/table.js.map +1 -1
  47. package/dist/components/ui/tabs-variants.d.ts +21 -0
  48. package/dist/components/ui/tabs-variants.js +15 -0
  49. package/dist/components/ui/tabs-variants.js.map +1 -0
  50. package/dist/components/ui/tabs.d.ts +2 -10
  51. package/dist/components/ui/tabs.js +3 -14
  52. package/dist/components/ui/tabs.js.map +1 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/shell/app-header.js +47 -41
  57. package/dist/shell/app-header.js.map +1 -1
  58. package/dist/shell/mobile-menu.d.ts +6 -1
  59. package/dist/shell/mobile-menu.js +5 -5
  60. package/dist/shell/mobile-menu.js.map +1 -1
  61. package/dist/shell/notifications-button.js +31 -9
  62. package/dist/shell/notifications-button.js.map +1 -1
  63. package/dist/shell/sidebar.js +1 -1
  64. package/dist/shell/sidebar.js.map +1 -1
  65. package/dist/shell/types.d.ts +13 -0
  66. package/dist/shell/types.js +1 -0
  67. package/dist/shell/types.js.map +1 -1
  68. package/dist/shell/user-menu.d.ts +14 -2
  69. package/dist/shell/user-menu.js +3 -3
  70. package/dist/shell/user-menu.js.map +1 -1
  71. package/dist/shell/workspace-switcher.js +48 -34
  72. package/dist/shell/workspace-switcher.js.map +1 -1
  73. package/package.json +1 -1
  74. package/styles.css +95 -16
@@ -17,7 +17,12 @@ interface MobileMenuProps {
17
17
  }
18
18
  /**
19
19
  * Menu off-canvas cho màn hình hẹp (`Sheet` trượt từ trái):
20
- * WorkspaceSwitcher (trục 1) → lưới app dạng danh sách (trục 2) → menu app hiện tại → user.
20
+ * WorkspaceSwitcher (trục 1) → menu app HIỆN TẠI → danh sách app (trục 2) → user.
21
+ *
22
+ * ⚠ Thứ tự đó cố ý KHÁC thanh bên desktop. Trên desktop danh sách app nằm ở header nên nó không
23
+ * chiếm chỗ của menu; ở đây cả hai nằm chung một vùng cuộn, và menu của app đang mở là thứ người
24
+ * dùng mở menu để tìm. Đặt danh sách app lên trước thì một hệ sinh thái 8–10 sản phẩm đẩy toàn bộ
25
+ * menu chính xuống dưới nếp gấp — phải cuộn mới thấy thứ mình cần.
21
26
  *
22
27
  * Tự đóng khi đổi route (`ShellProvider` lo) và khi bấm vào một link thật trong menu.
23
28
  */
@@ -29,17 +29,17 @@ function MobileMenu({
29
29
  style: appAccentStyle(currentApp),
30
30
  className: cn("flex w-80 max-w-[85vw] flex-col gap-0 bg-sidebar p-0 md:hidden", className),
31
31
  children: [
32
- /* @__PURE__ */ jsxs(SheetHeader, { className: "shrink-0 border-b border-sidebar-border p-2", children: [
32
+ /* @__PURE__ */ jsxs(SheetHeader, { className: "shrink-0 border-b border-sidebar-border p-2 pr-12", children: [
33
33
  /* @__PURE__ */ jsx(SheetTitle, { className: "sr-only", children: labels.navigationLabel }),
34
34
  /* @__PURE__ */ jsx(WorkspaceSwitcher, { align: "start" })
35
35
  ] }),
36
36
  header ? /* @__PURE__ */ jsx("div", { className: "shrink-0 p-2", children: header }) : null,
37
37
  /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto scrollbar-thin", children: [
38
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4 p-2", children: nav.map((group) => /* @__PURE__ */ jsx(SidebarGroup, { group, siblings: allItems, onNavigate: closeMenu }, group.id)) }),
38
39
  showAppLauncher ? /* @__PURE__ */ jsxs(Fragment, { children: [
39
- /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(AppLauncher, { inline: true, variant: "list", onSelected: closeMenu }) }),
40
- /* @__PURE__ */ jsx(Separator, { className: "bg-sidebar-border" })
41
- ] }) : null,
42
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4 p-2", children: nav.map((group) => /* @__PURE__ */ jsx(SidebarGroup, { group, siblings: allItems, onNavigate: closeMenu }, group.id)) })
40
+ /* @__PURE__ */ jsx(Separator, { className: "bg-sidebar-border" }),
41
+ /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(AppLauncher, { inline: true, variant: "list", onSelected: closeMenu }) })
42
+ ] }) : null
43
43
  ] }),
44
44
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-sidebar-border p-2", children: [
45
45
  user ? /* @__PURE__ */ jsx(UserMenu, { showName: true, ...userMenuProps, className: "min-w-0 flex-1" }) : null,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/mobile-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Sheet, SheetContent, SheetHeader, SheetTitle } from \"../components/ui/sheet.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppLauncher } from \"./app-launcher.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { ThemeToggle } from \"./theme-toggle.js\";\nimport { UserMenu, type UserMenuProps } from \"./user-menu.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface MobileMenuProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher. */\n header?: ReactNode;\n /** Slot đáy (phiên bản app, liên kết hỗ trợ…). */\n footer?: ReactNode;\n showAppLauncher?: boolean;\n showThemeToggle?: boolean;\n /** Chuyển tiếp props cho `UserMenu` ở đáy. */\n userMenuProps?: UserMenuProps;\n}\n\n/**\n * Menu off-canvas cho màn hình hẹp (`Sheet` trượt từ trái):\n * WorkspaceSwitcher (trục 1) → lưới app dạng danh sách (trục 2) → menu app hiện tại → user.\n *\n * Tự đóng khi đổi route (`ShellProvider` lo) và khi bấm vào một link thật trong menu.\n */\nexport function MobileMenu({\n className,\n header,\n footer,\n showAppLauncher = true,\n showThemeToggle = true,\n userMenuProps\n}: MobileMenuProps) {\n const { mobileMenuOpen, setMobileMenuOpen, nav, currentApp, user, labels } = useShell();\n\n const allItems = flattenNavItems(nav);\n\n // Chỉ đóng khi bấm trúng LINK thật — `SidebarMenuItem` không gọi callback này\n // khi người dùng chỉ mở/đóng nhánh con, nên menu giữ nguyên cho họ đi tiếp.\n const closeMenu = () => setMobileMenuOpen(false);\n\n return (\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\n <SheetContent\n side=\"left\"\n aria-describedby={undefined}\n style={appAccentStyle(currentApp)}\n className={cn(\"flex w-80 max-w-[85vw] flex-col gap-0 bg-sidebar p-0 md:hidden\", className)}\n >\n <SheetHeader className=\"shrink-0 border-b border-sidebar-border p-2\">\n <SheetTitle className=\"sr-only\">{labels.navigationLabel}</SheetTitle>\n <WorkspaceSwitcher align=\"start\" />\n </SheetHeader>\n\n {header ? <div className=\"shrink-0 p-2\">{header}</div> : null}\n\n <div className=\"flex-1 overflow-y-auto scrollbar-thin\">\n {showAppLauncher ? (\n <>\n <div className=\"p-2\">\n <AppLauncher inline variant=\"list\" onSelected={closeMenu} />\n </div>\n <Separator className=\"bg-sidebar-border\" />\n </>\n ) : null}\n\n <div className=\"flex flex-col gap-4 p-2\">\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} siblings={allItems} onNavigate={closeMenu} />\n ))}\n </div>\n </div>\n\n <div className=\"flex shrink-0 items-center gap-2 border-t border-sidebar-border p-2\">\n {user ? <UserMenu showName {...userMenuProps} className=\"min-w-0 flex-1\" /> : null}\n {showThemeToggle ? <ThemeToggle labels={labels} /> : null}\n </div>\n\n {footer ? <div className=\"shrink-0 border-t border-sidebar-border p-2\">{footer}</div> : null}\n </SheetContent>\n </Sheet>\n );\n}\n"],"mappings":";AAwDQ,SASI,UARF,KADF;AArDR,SAAS,iBAAiB;AAC1B,SAAS,OAAO,cAAc,aAAa,kBAAkB;AAC7D,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,gBAAoC;AAC7C,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAoB3B,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB;AACF,GAAoB;AAClB,QAAM,EAAE,gBAAgB,mBAAmB,KAAK,YAAY,MAAM,OAAO,IAAI,SAAS;AAEtF,QAAM,WAAW,gBAAgB,GAAG;AAIpC,QAAM,YAAY,MAAM,kBAAkB,KAAK;AAE/C,SACE,oBAAC,SAAM,MAAM,gBAAgB,cAAc,mBACzC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,oBAAkB;AAAA,MAClB,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW,GAAG,kEAAkE,SAAS;AAAA,MAEzF;AAAA,6BAAC,eAAY,WAAU,+CACrB;AAAA,8BAAC,cAAW,WAAU,WAAW,iBAAO,iBAAgB;AAAA,UACxD,oBAAC,qBAAkB,OAAM,SAAQ;AAAA,WACnC;AAAA,QAEC,SAAS,oBAAC,SAAI,WAAU,gBAAgB,kBAAO,IAAS;AAAA,QAEzD,qBAAC,SAAI,WAAU,yCACZ;AAAA,4BACC,iCACE;AAAA,gCAAC,SAAI,WAAU,OACb,8BAAC,eAAY,QAAM,MAAC,SAAQ,QAAO,YAAY,WAAW,GAC5D;AAAA,YACA,oBAAC,aAAU,WAAU,qBAAoB;AAAA,aAC3C,IACE;AAAA,UAEJ,oBAAC,SAAI,WAAU,2BACZ,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,UAAU,UAAU,YAAY,aAAxD,MAAM,EAA6D,CACvF,GACH;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,uEACZ;AAAA,iBAAO,oBAAC,YAAS,UAAQ,MAAE,GAAG,eAAe,WAAU,kBAAiB,IAAK;AAAA,UAC7E,kBAAkB,oBAAC,eAAY,QAAgB,IAAK;AAAA,WACvD;AAAA,QAEC,SAAS,oBAAC,SAAI,WAAU,+CAA+C,kBAAO,IAAS;AAAA;AAAA;AAAA,EAC1F,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/mobile-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Sheet, SheetContent, SheetHeader, SheetTitle } from \"../components/ui/sheet.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppLauncher } from \"./app-launcher.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { ThemeToggle } from \"./theme-toggle.js\";\nimport { UserMenu, type UserMenuProps } from \"./user-menu.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface MobileMenuProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher. */\n header?: ReactNode;\n /** Slot đáy (phiên bản app, liên kết hỗ trợ…). */\n footer?: ReactNode;\n showAppLauncher?: boolean;\n showThemeToggle?: boolean;\n /** Chuyển tiếp props cho `UserMenu` ở đáy. */\n userMenuProps?: UserMenuProps;\n}\n\n/**\n * Menu off-canvas cho màn hình hẹp (`Sheet` trượt từ trái):\n * WorkspaceSwitcher (trục 1) → menu app HIỆN TẠI → danh sách app (trục 2) → user.\n *\n * ⚠ Thứ tự đó cố ý KHÁC thanh bên desktop. Trên desktop danh sách app nằm ở header nên nó không\n * chiếm chỗ của menu; ở đây cả hai nằm chung một vùng cuộn, và menu của app đang mở là thứ người\n * dùng mở menu để tìm. Đặt danh sách app lên trước thì một hệ sinh thái 8–10 sản phẩm đẩy toàn bộ\n * menu chính xuống dưới nếp gấp — phải cuộn mới thấy thứ mình cần.\n *\n * Tự đóng khi đổi route (`ShellProvider` lo) và khi bấm vào một link thật trong menu.\n */\nexport function MobileMenu({\n className,\n header,\n footer,\n showAppLauncher = true,\n showThemeToggle = true,\n userMenuProps\n}: MobileMenuProps) {\n const { mobileMenuOpen, setMobileMenuOpen, nav, currentApp, user, labels } = useShell();\n\n const allItems = flattenNavItems(nav);\n\n // Chỉ đóng khi bấm trúng LINK thật — `SidebarMenuItem` không gọi callback này\n // khi người dùng chỉ mở/đóng nhánh con, nên menu giữ nguyên cho họ đi tiếp.\n const closeMenu = () => setMobileMenuOpen(false);\n\n return (\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\n <SheetContent\n side=\"left\"\n aria-describedby={undefined}\n style={appAccentStyle(currentApp)}\n className={cn(\"flex w-80 max-w-[85vw] flex-col gap-0 bg-sidebar p-0 md:hidden\", className)}\n >\n {/*\n `pr-12`: `SheetContent` đặt nút đóng ở `absolute top-4 right-4`, tức nó nằm ĐÈ lên nội\n dung header. Không chừa chỗ thì nó phủ lên mũi tên của `WorkspaceSwitcher` — bấm vào\n tưởng đổi workspace mà hoá ra đóng menu.\n */}\n <SheetHeader className=\"shrink-0 border-b border-sidebar-border p-2 pr-12\">\n <SheetTitle className=\"sr-only\">{labels.navigationLabel}</SheetTitle>\n <WorkspaceSwitcher align=\"start\" />\n </SheetHeader>\n\n {header ? <div className=\"shrink-0 p-2\">{header}</div> : null}\n\n <div className=\"flex-1 overflow-y-auto scrollbar-thin\">\n {/* Menu của app HIỆN TẠI đứng trước — xem lý do thứ tự ở JSDoc đầu file. */}\n <div className=\"flex flex-col gap-4 p-2\">\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} siblings={allItems} onNavigate={closeMenu} />\n ))}\n </div>\n\n {showAppLauncher ? (\n <>\n <Separator className=\"bg-sidebar-border\" />\n <div className=\"p-2\">\n <AppLauncher inline variant=\"list\" onSelected={closeMenu} />\n </div>\n </>\n ) : null}\n </div>\n\n <div className=\"flex shrink-0 items-center gap-2 border-t border-sidebar-border p-2\">\n {user ? <UserMenu showName {...userMenuProps} className=\"min-w-0 flex-1\" /> : null}\n {showThemeToggle ? <ThemeToggle labels={labels} /> : null}\n </div>\n\n {footer ? <div className=\"shrink-0 border-t border-sidebar-border p-2\">{footer}</div> : null}\n </SheetContent>\n </Sheet>\n );\n}\n"],"mappings":";AAkEQ,SAgBI,UAfF,KADF;AA/DR,SAAS,iBAAiB;AAC1B,SAAS,OAAO,cAAc,aAAa,kBAAkB;AAC7D,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,gBAAoC;AAC7C,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAyB3B,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB;AACF,GAAoB;AAClB,QAAM,EAAE,gBAAgB,mBAAmB,KAAK,YAAY,MAAM,OAAO,IAAI,SAAS;AAEtF,QAAM,WAAW,gBAAgB,GAAG;AAIpC,QAAM,YAAY,MAAM,kBAAkB,KAAK;AAE/C,SACE,oBAAC,SAAM,MAAM,gBAAgB,cAAc,mBACzC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,oBAAkB;AAAA,MAClB,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW,GAAG,kEAAkE,SAAS;AAAA,MAOzF;AAAA,6BAAC,eAAY,WAAU,qDACrB;AAAA,8BAAC,cAAW,WAAU,WAAW,iBAAO,iBAAgB;AAAA,UACxD,oBAAC,qBAAkB,OAAM,SAAQ;AAAA,WACnC;AAAA,QAEC,SAAS,oBAAC,SAAI,WAAU,gBAAgB,kBAAO,IAAS;AAAA,QAEzD,qBAAC,SAAI,WAAU,yCAEb;AAAA,8BAAC,SAAI,WAAU,2BACZ,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,UAAU,UAAU,YAAY,aAAxD,MAAM,EAA6D,CACvF,GACH;AAAA,UAEC,kBACC,iCACE;AAAA,gCAAC,aAAU,WAAU,qBAAoB;AAAA,YACzC,oBAAC,SAAI,WAAU,OACb,8BAAC,eAAY,QAAM,MAAC,SAAQ,QAAO,YAAY,WAAW,GAC5D;AAAA,aACF,IACE;AAAA,WACN;AAAA,QAEA,qBAAC,SAAI,WAAU,uEACZ;AAAA,iBAAO,oBAAC,YAAS,UAAQ,MAAE,GAAG,eAAe,WAAU,kBAAiB,IAAK;AAAA,UAC7E,kBAAkB,oBAAC,eAAY,QAAgB,IAAK;AAAA,WACvD;AAAA,QAEC,SAAS,oBAAC,SAAI,WAAU,+CAA+C,kBAAO,IAAS;AAAA;AAAA;AAAA,EAC1F,GACF;AAEJ;","names":[]}
@@ -19,15 +19,37 @@ function NotificationsButton({ count = 0, label, max = 9, className, ...props })
19
19
  ...props,
20
20
  children: [
21
21
  /* @__PURE__ */ jsx(Bell, { className: "size-4" }),
22
- hasUnread ? /* @__PURE__ */ jsx(
23
- "span",
24
- {
25
- className: cn(
26
- "absolute right-1 top-1 flex h-4 min-w-4 items-center justify-center rounded-full px-1",
27
- "bg-destructive text-[10px] font-semibold leading-none text-destructive-foreground"
28
- ),
29
- children: count > max ? `${max}+` : count
30
- }
22
+ hasUnread ? (
23
+ /*
24
+ * Huy hiệu ĐẨY RA GÓC, không nằm trong lòng nút.
25
+ *
26
+ * `top-0 right-0` — huy hiệu nằm SÁT GÓC nút, không lấn vào trong và cũng không tràn ra
27
+ * ngoài. Hai bản trước đều lệch, đo trên DOM thật (nút 36px, icon 16px căn giữa):
28
+ *
29
+ * right-1 top-1 che 39% cái chuông — thấy khối đỏ, không thấy chuông
30
+ * -right-0.5 -top-0.5 che 6% — hết che, nhưng huy hiệu trôi ra ngoài nút, đọc như
31
+ * một chấm rời không thuộc về cái chuông
32
+ * top-0 right-0 che 14% — dính vào góc icon, vẫn thấy rõ hình chuông ← đang dùng
33
+ *
34
+ * KHÔNG dùng giá trị âm: các nút header cách nhau `gap-1` (4px), tràn ra ngoài là huy
35
+ * hiệu tiến sát nút bên cạnh.
36
+ *
37
+ * `ring-2 ring-background` là nửa còn lại: viền cùng màu nền header cắt một đường sạch
38
+ * giữa huy hiệu và icon, nên 14% chồng lấn kia đọc thành hai hình tách bạch chứ không
39
+ * nhoè vào nhau. Cùng thủ pháp mà mẫu AppHeader ở mục Patterns dùng cho chấm thông báo
40
+ * (`ring-2 ring-card`).
41
+ */
42
+ /* @__PURE__ */ jsx(
43
+ "span",
44
+ {
45
+ className: cn(
46
+ "absolute right-0 top-0 flex h-4 min-w-4 items-center justify-center rounded-full px-1",
47
+ "ring-2 ring-background",
48
+ "bg-destructive text-[10px] font-semibold leading-none text-destructive-foreground"
49
+ ),
50
+ children: count > max ? `${max}+` : count
51
+ }
52
+ )
31
53
  ) : null
32
54
  ]
33
55
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/notifications-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Bell } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport { DEFAULT_SHELL_LABELS } from \"./types.js\";\n\nexport interface NotificationsButtonProps extends Omit<ComponentProps<\"button\">, \"children\"> {\n /** Số thông báo chưa đọc; 0 hoặc bỏ trống thì không hiện chấm số. */\n count?: number;\n /** Nhãn a11y (mặc định \"Thông báo\"). */\n label?: string;\n /** Vượt ngưỡng này thì hiển thị \"n+\" (mặc định 9). */\n max?: number;\n}\n\n/**\n * Nút chuông + số chưa đọc. **Chỉ có cái nút** — nội dung panel thông báo là\n * app-specific: đóng sẵn một NotificationPanel vào package thì app nào cũng phải\n * uốn dữ liệu của mình cho vừa cái khuôn đó.\n *\n * App tự bọc trong Popover/Sheet của mình:\n * `<PopoverTrigger asChild><NotificationsButton count={3} /></PopoverTrigger>`\n */\nexport function NotificationsButton({ count = 0, label, max = 9, className, ...props }: NotificationsButtonProps) {\n const text = label ?? DEFAULT_SHELL_LABELS.notifications;\n const hasUnread = count > 0;\n\n return (\n <button\n type=\"button\"\n aria-label={hasUnread ? `${text} (${count})` : text}\n className={cn(\n \"relative flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n className\n )}\n {...props}\n >\n <Bell className=\"size-4\" />\n {hasUnread ? (\n <span\n className={cn(\n \"absolute right-1 top-1 flex h-4 min-w-4 items-center justify-center rounded-full px-1\",\n \"bg-destructive text-[10px] font-semibold leading-none text-destructive-foreground\"\n )}\n >\n {count > max ? `${max}+` : count}\n </span>\n ) : null}\n </button>\n );\n}\n"],"mappings":";AA6BI,SAUE,KAVF;AA3BJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,4BAA4B;AAmB9B,SAAS,oBAAoB,EAAE,QAAQ,GAAG,OAAO,MAAM,GAAG,WAAW,GAAG,MAAM,GAA6B;AAChH,QAAM,OAAO,SAAS,qBAAqB;AAC3C,QAAM,YAAY,QAAQ;AAE1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,YAAY,GAAG,IAAI,KAAK,KAAK,MAAM;AAAA,MAC/C,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,4BAAC,QAAK,WAAU,UAAS;AAAA,QACxB,YACC;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,YACF;AAAA,YAEC,kBAAQ,MAAM,GAAG,GAAG,MAAM;AAAA;AAAA,QAC7B,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/notifications-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Bell } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport { DEFAULT_SHELL_LABELS } from \"./types.js\";\n\nexport interface NotificationsButtonProps extends Omit<ComponentProps<\"button\">, \"children\"> {\n /** Số thông báo chưa đọc; 0 hoặc bỏ trống thì không hiện chấm số. */\n count?: number;\n /** Nhãn a11y (mặc định \"Thông báo\"). */\n label?: string;\n /** Vượt ngưỡng này thì hiển thị \"n+\" (mặc định 9). */\n max?: number;\n}\n\n/**\n * Nút chuông + số chưa đọc. **Chỉ có cái nút** — nội dung panel thông báo là\n * app-specific: đóng sẵn một NotificationPanel vào package thì app nào cũng phải\n * uốn dữ liệu của mình cho vừa cái khuôn đó.\n *\n * App tự bọc trong Popover/Sheet của mình:\n * `<PopoverTrigger asChild><NotificationsButton count={3} /></PopoverTrigger>`\n */\nexport function NotificationsButton({ count = 0, label, max = 9, className, ...props }: NotificationsButtonProps) {\n const text = label ?? DEFAULT_SHELL_LABELS.notifications;\n const hasUnread = count > 0;\n\n return (\n <button\n type=\"button\"\n aria-label={hasUnread ? `${text} (${count})` : text}\n className={cn(\n \"relative flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n className\n )}\n {...props}\n >\n <Bell className=\"size-4\" />\n {hasUnread ? (\n /*\n * Huy hiệu ĐẨY RA GÓC, không nằm trong lòng nút.\n *\n * `top-0 right-0` — huy hiệu nằm SÁT GÓC nút, không lấn vào trong và cũng không tràn ra\n * ngoài. Hai bản trước đều lệch, đo trên DOM thật (nút 36px, icon 16px căn giữa):\n *\n * right-1 top-1 che 39% cái chuông — thấy khối đỏ, không thấy chuông\n * -right-0.5 -top-0.5 che 6% — hết che, nhưng huy hiệu trôi ra ngoài nút, đọc như\n * một chấm rời không thuộc về cái chuông\n * top-0 right-0 che 14% — dính vào góc icon, vẫn thấy rõ hình chuông ← đang dùng\n *\n * KHÔNG dùng giá trị âm: các nút header cách nhau `gap-1` (4px), tràn ra ngoài là huy\n * hiệu tiến sát nút bên cạnh.\n *\n * `ring-2 ring-background` là nửa còn lại: viền cùng màu nền header cắt một đường sạch\n * giữa huy hiệu và icon, nên 14% chồng lấn kia đọc thành hai hình tách bạch chứ không\n * nhoè vào nhau. Cùng thủ pháp mà mẫu AppHeader ở mục Patterns dùng cho chấm thông báo\n * (`ring-2 ring-card`).\n */\n <span\n className={cn(\n \"absolute right-0 top-0 flex h-4 min-w-4 items-center justify-center rounded-full px-1\",\n \"ring-2 ring-background\",\n \"bg-destructive text-[10px] font-semibold leading-none text-destructive-foreground\"\n )}\n >\n {count > max ? `${max}+` : count}\n </span>\n ) : null}\n </button>\n );\n}\n"],"mappings":";AA6BI,SAUE,KAVF;AA3BJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,4BAA4B;AAmB9B,SAAS,oBAAoB,EAAE,QAAQ,GAAG,OAAO,MAAM,GAAG,WAAW,GAAG,MAAM,GAA6B;AAChH,QAAM,OAAO,SAAS,qBAAqB;AAC3C,QAAM,YAAY,QAAQ;AAE1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,YAAY,GAAG,IAAI,KAAK,KAAK,MAAM;AAAA,MAC/C,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,4BAAC,QAAK,WAAU,UAAS;AAAA,QACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAoBC;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cAEC,kBAAQ,MAAM,GAAG,GAAG,MAAM;AAAA;AAAA,UAC7B;AAAA,YACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -70,7 +70,7 @@ function Sidebar({
70
70
  ),
71
71
  children: sidebarCollapsed ? /* @__PURE__ */ jsx(PanelLeftOpen, { className: "size-4" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
72
72
  /* @__PURE__ */ jsx(PanelLeftClose, { className: "size-4" }),
73
- /* @__PURE__ */ jsx("span", { className: "truncate", children: labels.collapseSidebar })
73
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: labels.collapseSidebarShort })
74
74
  ] })
75
75
  }
76
76
  ) }),
@@ -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 className={cn(\n \"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\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4\" />\n <span className=\"truncate\">{labels.collapseSidebar}</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,SA0CU,UA1CV,KA0CU,YA1CV;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,oBAC7D,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA,mBAAmB,0BAA0B;AAAA,oBAC/C;AAAA,oBAEC,6BACC,oBAAC,iBAAc,WAAU,UAAS,IAElC,iCACE;AAAA,0CAAC,kBAAe,WAAU,UAAS;AAAA,sBACnC,oBAAC,UAAK,WAAU,YAAY,iBAAO,iBAAgB;AAAA,uBACrD;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, \"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 className={cn(\n \"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\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4\" />\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,SA0CU,UA1CV,KA0CU,YA1CV;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,oBAC7D,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA,mBAAmB,0BAA0B;AAAA,oBAC/C;AAAA,oBAEC,6BACC,oBAAC,iBAAc,WAAU,UAAS,IAElC,iCACE;AAAA,0CAAC,kBAAe,WAAU,UAAS;AAAA,sBAGnC,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":[]}
@@ -146,6 +146,19 @@ type ShellLinkComponent = ComponentType<ShellLinkProps>;
146
146
  */
147
147
  interface ShellLabels {
148
148
  collapseSidebar: string;
149
+ /**
150
+ * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).
151
+ *
152
+ * 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
153
+ * (`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
154
+ * 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
155
+ * 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.
156
+ *
157
+ * ⚠ 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
158
+ * "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ệ đó
159
+ * khi dịch sang ngôn ngữ khác.
160
+ */
161
+ collapseSidebarShort: string;
149
162
  expandSidebar: string;
150
163
  openMenu: string;
151
164
  closeMenu: string;
@@ -1,5 +1,6 @@
1
1
  const DEFAULT_SHELL_LABELS = {
2
2
  collapseSidebar: "Thu g\u1ECDn thanh b\xEAn",
3
+ collapseSidebarShort: "Thu g\u1ECDn",
3
4
  expandSidebar: "M\u1EDF r\u1ED9ng thanh b\xEAn",
4
5
  openMenu: "M\u1EDF menu",
5
6
  closeMenu: "\u0110\xF3ng menu",
@@ -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 * 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 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 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 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 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":"AAoMO,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":[]}
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 * ⚠ `--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 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 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 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 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 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: \"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":"AAiNO,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;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":[]}
@@ -25,8 +25,20 @@ interface UserMenuProps {
25
25
  helpHref?: string;
26
26
  /** Mục bổ sung của app — chèn ngay trên "Đăng xuất". */
27
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;
28
+ /**
29
+ * Submenu "Giao diện" trong menu người dùng.
30
+ *
31
+ * `"mobile"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có
32
+ * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.
33
+ * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn
34
+ * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.
35
+ * `false` — không bao giờ hiện.
36
+ *
37
+ * ⚠ 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
38
+ * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại
39
+ * 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.
40
+ */
41
+ showThemeSubmenu?: boolean | "mobile";
30
42
  /** Ghi đè handler đăng xuất của context. */
31
43
  onLogout?: () => void;
32
44
  align?: "start" | "center" | "end";
@@ -33,7 +33,7 @@ function UserMenu({
33
33
  settingsHref,
34
34
  helpHref,
35
35
  items = [],
36
- showThemeSubmenu = true,
36
+ showThemeSubmenu = "mobile",
37
37
  onLogout,
38
38
  align = "end"
39
39
  }) {
@@ -96,8 +96,8 @@ function UserMenu({
96
96
  hasEntries ? /* @__PURE__ */ jsx(DropdownMenuSeparator, {}) : null,
97
97
  visibleBuiltIns.map(renderItem),
98
98
  items.map(renderItem),
99
- showThemeSubmenu ? /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [
100
- /* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { children: [
99
+ showThemeSubmenu !== false ? /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [
100
+ /* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { className: cn(showThemeSubmenu === "mobile" && "md:hidden"), children: [
101
101
  /* @__PURE__ */ jsx(Palette, { className: "size-4" }),
102
102
  /* @__PURE__ */ jsx("span", { children: labels.appearance })
103
103
  ] }),
@@ -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 đổ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":[]}
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":[]}
@@ -80,15 +80,15 @@ function WorkspaceSwitcher({
80
80
  className
81
81
  ),
82
82
  children: [
83
- /* @__PURE__ */ jsx(WorkspaceAvatar, { workspace: currentWorkspace }),
83
+ /* @__PURE__ */ jsx(WorkspaceAvatar, { workspace: currentWorkspace, className: "size-8" }),
84
84
  /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
85
- /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-sidebar-foreground", children: currentWorkspace.name }),
85
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold leading-tight text-sidebar-foreground", children: currentWorkspace.name }),
86
86
  currentWorkspace.plan ? (
87
87
  // `text-muted-foreground` chứ không `text-sidebar-foreground/55`: đây là CHỮ (11px) nên
88
88
  // ngưỡng là AA 4,5:1, mà /55 trên `--sidebar` #FAFAF9 chỉ được 4,13:1 ✗ (dark 5,78:1 —
89
89
  // mỗi theme hỏng một nửa). `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò:
90
90
  // 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓.
91
- /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: currentWorkspace.plan })
91
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] leading-tight text-muted-foreground", children: currentWorkspace.plan })
92
92
  ) : null
93
93
  ] }),
94
94
  /* @__PURE__ */ jsx(ChevronsUpDown, { className: "size-3.5 shrink-0 text-muted-foreground" })
@@ -105,7 +105,7 @@ function WorkspaceSwitcher({
105
105
  {
106
106
  align,
107
107
  sideOffset: 6,
108
- className: "w-72 p-0",
108
+ className: "w-80 p-0",
109
109
  onOpenAutoFocus: (event) => {
110
110
  if (!searchRef.current) return;
111
111
  event.preventDefault();
@@ -136,35 +136,49 @@ function WorkspaceSwitcher({
136
136
  }
137
137
  )
138
138
  ] }) : null,
139
- /* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto scrollbar-thin p-1", role: "group", "aria-label": labels.workspace, children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-2 py-6 text-center text-sm text-muted-foreground", children: labels.workspaceEmpty }) : filtered.map((workspace) => {
140
- const isCurrent = workspace.id === currentWorkspace.id;
141
- return /* @__PURE__ */ jsxs(
142
- "button",
143
- {
144
- type: "button",
145
- "aria-current": isCurrent ? "true" : void 0,
146
- onClick: () => handleSelect(workspace),
147
- className: cn(
148
- "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left",
149
- "transition-colors duration-150 ease-out hover:bg-accent",
150
- isCurrent && "bg-accent/60"
151
- ),
152
- children: [
153
- /* @__PURE__ */ jsx(WorkspaceAvatar, { workspace }),
154
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
155
- /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-foreground", children: workspace.name }),
156
- workspace.role || workspace.memberCount !== void 0 ? /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: [
157
- workspace.role,
158
- workspace.memberCount === void 0 ? void 0 : `${workspace.memberCount} ${labels.members}`
159
- ].filter(Boolean).join(" \xB7 ") }) : null
160
- ] }),
161
- workspace.plan ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", size: "sm", className: "shrink-0", children: workspace.plan }) : null,
162
- isCurrent ? /* @__PURE__ */ jsx(Check, { className: "size-4 shrink-0 text-popover-foreground" }) : null
163
- ]
164
- },
165
- workspace.id
166
- );
167
- }) })
139
+ /* @__PURE__ */ jsx(
140
+ "div",
141
+ {
142
+ className: "flex max-h-64 flex-col gap-1 overflow-y-auto scrollbar-thin p-1",
143
+ role: "group",
144
+ "aria-label": labels.workspace,
145
+ children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-2 py-6 text-center text-sm text-muted-foreground", children: labels.workspaceEmpty }) : filtered.map((workspace) => {
146
+ const isCurrent = workspace.id === currentWorkspace.id;
147
+ return /* @__PURE__ */ jsxs(
148
+ "button",
149
+ {
150
+ type: "button",
151
+ "aria-current": isCurrent ? "true" : void 0,
152
+ onClick: () => handleSelect(workspace),
153
+ className: cn(
154
+ "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left",
155
+ "transition-colors duration-150 ease-out hover:bg-accent",
156
+ isCurrent && "bg-accent/60"
157
+ ),
158
+ children: [
159
+ /* @__PURE__ */ jsx(WorkspaceAvatar, { workspace, className: "size-8" }),
160
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
161
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium leading-tight text-foreground", children: workspace.name }),
162
+ workspace.role || workspace.memberCount !== void 0 ? /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] leading-tight text-muted-foreground", children: [
163
+ workspace.role,
164
+ workspace.memberCount === void 0 ? void 0 : `${workspace.memberCount} ${labels.members}`
165
+ ].filter(Boolean).join(" \xB7 ") }) : null
166
+ ] }),
167
+ workspace.plan ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", size: "sm", className: "shrink-0", children: workspace.plan }) : null,
168
+ /* @__PURE__ */ jsx(
169
+ Check,
170
+ {
171
+ "aria-hidden": "true",
172
+ className: cn("size-4 shrink-0 text-popover-foreground", !isCurrent && "invisible")
173
+ }
174
+ )
175
+ ]
176
+ },
177
+ workspace.id
178
+ );
179
+ })
180
+ }
181
+ )
168
182
  ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3", children: [
169
183
  /* @__PURE__ */ jsx(WorkspaceAvatar, { workspace: currentWorkspace, className: "size-8" }),
170
184
  /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
@@ -174,7 +188,7 @@ function WorkspaceSwitcher({
174
188
  ] }),
175
189
  hasActions ? /* @__PURE__ */ jsxs(Fragment, { children: [
176
190
  /* @__PURE__ */ jsx(Separator, {}),
177
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col p-1", children: [
191
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 p-1", children: [
178
192
  onCreateWorkspace ? /* @__PURE__ */ jsx(ActionRow, { icon: Plus, label: labels.createWorkspace, onClick: closeThen(onCreateWorkspace) }) : null,
179
193
  onWorkspaceSettings ? /* @__PURE__ */ jsx(
180
194
  ActionRow,