@comitor/ui 3.7.1 → 3.9.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.
@@ -122,7 +122,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
122
122
  pasteDialog: PasteDialogState;
123
123
  onRowAdd: ((event?: React.MouseEvent<HTMLDivElement>) => Promise<void>) | undefined;
124
124
  adjustLayout: boolean;
125
- locale: {
125
+ locale: "en" | "vi" | {
126
126
  grid: {
127
127
  label: string;
128
128
  addRow: string;
@@ -397,7 +397,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
397
397
  shortcuts: string;
398
398
  };
399
399
  };
400
- } | "en" | "vi" | undefined;
400
+ } | undefined;
401
401
  };
402
402
 
403
403
  export { type UseDataGridProps, useDataGrid };
@@ -24,6 +24,8 @@ const shellEn = {
24
24
  lockedHint: "Not included in your current plan",
25
25
  account: "Account",
26
26
  profile: "Profile",
27
+ preferences: "Preferences",
28
+ security: "Security",
27
29
  settings: "Settings",
28
30
  appearance: "Appearance",
29
31
  help: "Help",
@@ -60,6 +62,8 @@ const shellVi = {
60
62
  lockedHint: "\u1EE8ng d\u1EE5ng ch\u01B0a c\xF3 trong g\xF3i hi\u1EC7n t\u1EA1i",
61
63
  account: "T\xE0i kho\u1EA3n",
62
64
  profile: "H\u1ED3 s\u01A1",
65
+ preferences: "Tu\u1EF3 ch\u1ECDn hi\u1EC3n th\u1ECB",
66
+ security: "B\u1EA3o m\u1EADt",
63
67
  settings: "C\xE0i \u0111\u1EB7t",
64
68
  appearance: "Giao di\u1EC7n",
65
69
  help: "Tr\u1EE3 gi\xFAp",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/shell.ts"],"sourcesContent":["import type { ShellLabels } from \"../../shell/types.js\";\n\n/**\n * Chữ của cả tầng shell — `ShellProvider`/`AppShell` và mọi thứ đọc `useShell().labels`\n * (`Sidebar`, `AppHeader`, `MobileMenu`, `WorkspaceSwitcher`, `AppLauncher`, `UserMenu`), cộng\n * `ThemeToggle` và `NotificationsButton` khi đứng ngoài shell. Hình dạng + chú thích từng khoá:\n * `ShellLabels`.\n *\n * ⚠ `collapseSidebarShort` PHẢI là một phần của `collapseSidebar` ở MỌI ngôn ngữ (WCAG 2.5.3):\n * \"Collapse\" ⊂ \"Collapse sidebar\", \"Thu gọn\" ⊂ \"Thu gọn thanh bên\".\n * ⚠ Cùng luật cho cặp `apps` (tooltip HIỆN RA của nút mở launcher) ⊂ `appLauncherLabel` (tên cho\n * trình đọc màn hình): \"Ứng dụng\" ⊂ \"Mở danh sách ứng dụng\". Bảng tiếng Anh của `3.0.0` hỏng đúng\n * chỗ này — \"Apps\" không nằm trong \"Open app launcher\", nên người điều khiển bằng giọng nói đọc\n * chữ họ THẤY (\"click Apps\") mà không khớp nút nào. Sửa ở `3.0.1`.\n */\nexport const shellEn: ShellLabels = {\n collapseSidebar: \"Collapse sidebar\",\n collapseSidebarShort: \"Collapse\",\n expandSidebar: \"Expand sidebar\",\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n search: \"Search\",\n searchPlaceholder: \"Search…\",\n notifications: \"Notifications\",\n navigationLabel: \"Main navigation\",\n workspace: \"Workspace\",\n workspaceSwitcherLabel: \"Switch workspace\",\n workspaceSearchPlaceholder: \"Search workspaces…\",\n workspaceEmpty: \"No workspaces found\",\n createWorkspace: \"Create workspace\",\n workspaceSettings: \"Workspace settings\",\n inviteMembers: \"Invite members\",\n members: (count) => `${count} ${count === 1 ? \"member\" : \"members\"}`,\n apps: \"Apps\",\n appLauncherLabel: \"Open apps list\",\n myApps: \"Your apps\",\n discoverApps: \"Discover more\",\n locked: \"Locked\",\n lockedHint: \"Not included in your current plan\",\n account: \"Account\",\n profile: \"Profile\",\n settings: \"Settings\",\n appearance: \"Appearance\",\n help: \"Help\",\n logout: \"Log out\",\n themeLight: \"Light\",\n themeDark: \"Dark\",\n themeSystem: \"System\",\n toggleTheme: \"Toggle theme\",\n skipToContent: \"Skip to main content\"\n};\n\nexport const shellVi: ShellLabels = {\n collapseSidebar: \"Thu gọn thanh bên\",\n collapseSidebarShort: \"Thu gọn\",\n expandSidebar: \"Mở rộng thanh bên\",\n openMenu: \"Mở menu\",\n closeMenu: \"Đóng menu\",\n search: \"Tìm kiếm\",\n searchPlaceholder: \"Tìm kiếm…\",\n notifications: \"Thông báo\",\n navigationLabel: \"Điều hướng chính\",\n workspace: \"Không gian làm việc\",\n workspaceSwitcherLabel: \"Đổi không gian làm việc\",\n workspaceSearchPlaceholder: \"Tìm không gian làm việc…\",\n workspaceEmpty: \"Không tìm thấy không gian làm việc\",\n createWorkspace: \"Tạo không gian làm việc\",\n workspaceSettings: \"Cài đặt không gian làm việc\",\n inviteMembers: \"Mời thành viên\",\n members: (count) => `${count} thành viên`,\n apps: \"Ứng dụng\",\n appLauncherLabel: \"Mở danh sách ứng dụng\",\n myApps: \"Ứng dụng của bạn\",\n discoverApps: \"Khám phá thêm\",\n locked: \"Chưa mở khoá\",\n lockedHint: \"Ứng dụng chưa có trong gói hiện tại\",\n account: \"Tài khoản\",\n profile: \"Hồ sơ\",\n settings: \"Cài đặt\",\n appearance: \"Giao diện\",\n help: \"Trợ giúp\",\n logout: \"Đăng xuất\",\n themeLight: \"Sáng\",\n themeDark: \"Tối\",\n themeSystem: \"Hệ thống\",\n toggleTheme: \"Đổi giao diện\",\n skipToContent: \"Bỏ qua điều hướng, sang nội dung chính\"\n};\n"],"mappings":"AAeO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EAClE,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;AAAA,EACb,eAAe;AACjB;AAEO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AACjB;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/shell.ts"],"sourcesContent":["import type { ShellLabels } from \"../../shell/types.js\";\n\n/**\n * Chữ của cả tầng shell — `ShellProvider`/`AppShell` và mọi thứ đọc `useShell().labels`\n * (`Sidebar`, `AppHeader`, `MobileMenu`, `WorkspaceSwitcher`, `AppLauncher`, `UserMenu`), cộng\n * `ThemeToggle` và `NotificationsButton` khi đứng ngoài shell. Hình dạng + chú thích từng khoá:\n * `ShellLabels`.\n *\n * ⚠ `collapseSidebarShort` PHẢI là một phần của `collapseSidebar` ở MỌI ngôn ngữ (WCAG 2.5.3):\n * \"Collapse\" ⊂ \"Collapse sidebar\", \"Thu gọn\" ⊂ \"Thu gọn thanh bên\".\n * ⚠ Cùng luật cho cặp `apps` (tooltip HIỆN RA của nút mở launcher) ⊂ `appLauncherLabel` (tên cho\n * trình đọc màn hình): \"Ứng dụng\" ⊂ \"Mở danh sách ứng dụng\". Bảng tiếng Anh của `3.0.0` hỏng đúng\n * chỗ này — \"Apps\" không nằm trong \"Open app launcher\", nên người điều khiển bằng giọng nói đọc\n * chữ họ THẤY (\"click Apps\") mà không khớp nút nào. Sửa ở `3.0.1`.\n */\nexport const shellEn: ShellLabels = {\n collapseSidebar: \"Collapse sidebar\",\n collapseSidebarShort: \"Collapse\",\n expandSidebar: \"Expand sidebar\",\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n search: \"Search\",\n searchPlaceholder: \"Search…\",\n notifications: \"Notifications\",\n navigationLabel: \"Main navigation\",\n workspace: \"Workspace\",\n workspaceSwitcherLabel: \"Switch workspace\",\n workspaceSearchPlaceholder: \"Search workspaces…\",\n workspaceEmpty: \"No workspaces found\",\n createWorkspace: \"Create workspace\",\n workspaceSettings: \"Workspace settings\",\n inviteMembers: \"Invite members\",\n members: (count) => `${count} ${count === 1 ? \"member\" : \"members\"}`,\n apps: \"Apps\",\n appLauncherLabel: \"Open apps list\",\n myApps: \"Your apps\",\n discoverApps: \"Discover more\",\n locked: \"Locked\",\n lockedHint: \"Not included in your current plan\",\n account: \"Account\",\n profile: \"Profile\",\n preferences: \"Preferences\",\n security: \"Security\",\n settings: \"Settings\",\n appearance: \"Appearance\",\n help: \"Help\",\n logout: \"Log out\",\n themeLight: \"Light\",\n themeDark: \"Dark\",\n themeSystem: \"System\",\n toggleTheme: \"Toggle theme\",\n skipToContent: \"Skip to main content\"\n};\n\nexport const shellVi: ShellLabels = {\n collapseSidebar: \"Thu gọn thanh bên\",\n collapseSidebarShort: \"Thu gọn\",\n expandSidebar: \"Mở rộng thanh bên\",\n openMenu: \"Mở menu\",\n closeMenu: \"Đóng menu\",\n search: \"Tìm kiếm\",\n searchPlaceholder: \"Tìm kiếm…\",\n notifications: \"Thông báo\",\n navigationLabel: \"Điều hướng chính\",\n workspace: \"Không gian làm việc\",\n workspaceSwitcherLabel: \"Đổi không gian làm việc\",\n workspaceSearchPlaceholder: \"Tìm không gian làm việc…\",\n workspaceEmpty: \"Không tìm thấy không gian làm việc\",\n createWorkspace: \"Tạo không gian làm việc\",\n workspaceSettings: \"Cài đặt không gian làm việc\",\n inviteMembers: \"Mời thành viên\",\n members: (count) => `${count} thành viên`,\n apps: \"Ứng dụng\",\n appLauncherLabel: \"Mở danh sách ứng dụng\",\n myApps: \"Ứng dụng của bạn\",\n discoverApps: \"Khám phá thêm\",\n locked: \"Chưa mở khoá\",\n lockedHint: \"Ứng dụng chưa có trong gói hiện tại\",\n account: \"Tài khoản\",\n profile: \"Hồ sơ\",\n preferences: \"Tuỳ chọn hiển thị\",\n security: \"Bảo mật\",\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 skipToContent: \"Bỏ qua điều hướng, sang nội dung chính\"\n};\n"],"mappings":"AAeO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EAClE,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AACjB;AAEO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AACjB;","names":[]}
@@ -140,6 +140,7 @@ interface UserMenuItem {
140
140
  onSelect?: () => void;
141
141
  /** 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). */
142
142
  shortcut?: string;
143
+ /** `"destructive"`: tô đỏ VÀ chuyển mục xuống nhóm thoát (dưới vạch phân cách, trên mục đăng xuất). */
143
144
  variant?: "default" | "destructive";
144
145
  }
145
146
  /**
@@ -204,6 +205,10 @@ interface ShellLabels {
204
205
  lockedHint: string;
205
206
  account: string;
206
207
  profile: string;
208
+ /** Mục dựng sẵn khi `UserMenu` có `accountUrl` — trang `/preferences` của Comitor.Account. */
209
+ preferences: string;
210
+ /** Mục dựng sẵn khi `UserMenu` có `accountUrl` — trang `/security` của Comitor.Account. */
211
+ security: string;
207
212
  settings: string;
208
213
  appearance: string;
209
214
  help: string;
@@ -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\";\nimport { shellEn } from \"../i18n/namespaces/shell.js\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định\n * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của\n * `ShellProvider`/`AppShell`.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.\n *\n * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại\n * \"bấm Collapse\" sẽ không khớp tên truy cập. \"Collapse\" ⊂ \"Collapse sidebar\" ✓, \"Thu gọn\" ⊂\n * \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và\n * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước (\"12 thành viên\"), tiếng Anh cũng vậy\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * là khoá cứng trật tự của một ngôn ngữ vào mọi bản dịch. Hàm còn chọn được dạng số ít/nhiều\n * (\"1 member\" ⇄ \"2 members\"), thứ tiếng Việt không có mà tiếng Anh thì bắt buộc. Cùng khuôn\n * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n /* Trợ năng */\n /** Chữ của skip link đầu khung (`AppShell`, WCAG 2.4.1) — chỉ hiện khi nhận focus bàn phím */\n skipToContent: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AA+NjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\nimport { shellEn } from \"../i18n/namespaces/shell.js\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (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 /** `\"destructive\"`: tô đỏ VÀ chuyển mục xuống nhóm thoát (dưới vạch phân cách, trên mục đăng xuất). */\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định\n * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của\n * `ShellProvider`/`AppShell`.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.\n *\n * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại\n * \"bấm Collapse\" sẽ không khớp tên truy cập. \"Collapse\" ⊂ \"Collapse sidebar\" ✓, \"Thu gọn\" ⊂\n * \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và\n * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước (\"12 thành viên\"), tiếng Anh cũng vậy\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * là khoá cứng trật tự của một ngôn ngữ vào mọi bản dịch. Hàm còn chọn được dạng số ít/nhiều\n * (\"1 member\" ⇄ \"2 members\"), thứ tiếng Việt không có mà tiếng Anh thì bắt buộc. Cùng khuôn\n * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n /** Mục dựng sẵn khi `UserMenu` có `accountUrl` — trang `/preferences` của Comitor.Account. */\n preferences: string;\n /** Mục dựng sẵn khi `UserMenu` có `accountUrl` — trang `/security` của Comitor.Account. */\n security: 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 /* Trợ năng */\n /** Chữ của skip link đầu khung (`AppShell`, WCAG 2.4.1) — chỉ hiện khi nhận focus bàn phím */\n skipToContent: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AAoOjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
@@ -17,13 +17,27 @@ interface UserMenuProps {
17
17
  className?: string;
18
18
  /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */
19
19
  showName?: boolean;
20
+ /**
21
+ * Gốc của Comitor.Account (vd `https://account.comitor.ai`, không dấu `/` cuối). Có giá trị thì menu
22
+ * dựng sẵn BA mục trỏ sang Account — Hồ sơ (`/profile`), Tuỳ chọn hiển thị (`/preferences`), Bảo mật
23
+ * (`/security`) — đứng đầu menu, trước mục của app. Chính app Account truyền chuỗi rỗng `""` để ra
24
+ * đường dẫn tương đối. `profileHref` vẫn thắng nếu app khai cả hai.
25
+ *
26
+ * Ba trang ấy là của DANH TÍNH dùng chung cho cả hệ sinh thái, nên mọi app hiện cùng ba mục, cùng
27
+ * chữ, cùng thứ tự — gói giữ nhãn để không app nào phải dịch lại.
28
+ */
29
+ accountUrl?: string;
20
30
  /** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */
21
31
  profileHref?: string;
22
32
  /** Link cài đặt (nhãn `settings`). */
23
33
  settingsHref?: string;
24
34
  /** Link trợ giúp (nhãn `help`). */
25
35
  helpHref?: string;
26
- /** Mục bổ sung của app — chèn ngay trên mục đăng xuất. */
36
+ /**
37
+ * Mục bổ sung của app. Mục thường đứng sau các mục dựng sẵn; mục `variant: "destructive"` (vd "Thoát
38
+ * khỏi mọi sản phẩm") vào NHÓM THOÁT — dưới vạch phân cách, ngay trên mục đăng xuất — để mọi lối ra
39
+ * nằm chung một nhóm, tách khỏi các mục điều hướng.
40
+ */
27
41
  items?: UserMenuItem[];
28
42
  /**
29
43
  * Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.
@@ -49,6 +63,6 @@ interface UserMenuProps {
49
63
  *
50
64
  * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.
51
65
  */
52
- declare function UserMenu({ className, showName, profileHref, settingsHref, helpHref, items, showThemeSubmenu, onLogout, align }: UserMenuProps): React.JSX.Element | null;
66
+ declare function UserMenu({ className, showName, accountUrl, profileHref, settingsHref, helpHref, items, showThemeSubmenu, onLogout, align }: UserMenuProps): React.JSX.Element | null;
53
67
 
54
68
  export { UserAvatar, type UserAvatarProps, UserMenu, type UserMenuProps };
@@ -1,6 +1,18 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { ChevronDown, CircleUser, HelpCircle, LogOut, Monitor, Moon, Palette, Settings, Sun } from "lucide-react";
3
+ import {
4
+ ChevronDown,
5
+ CircleUser,
6
+ HelpCircle,
7
+ LogOut,
8
+ Monitor,
9
+ Moon,
10
+ Palette,
11
+ Settings,
12
+ ShieldCheck,
13
+ SlidersHorizontal,
14
+ Sun
15
+ } from "lucide-react";
4
16
  import { useTheme } from "next-themes";
5
17
  import { getAvatarToneClasses } from "../components/composite/letter-avatar.js";
6
18
  import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar.js";
@@ -29,6 +41,7 @@ function UserAvatar({ user, className }) {
29
41
  function UserMenu({
30
42
  className,
31
43
  showName = false,
44
+ accountUrl,
32
45
  profileHref,
33
46
  settingsHref,
34
47
  helpHref,
@@ -55,12 +68,17 @@ function UserMenu({
55
68
  return /* @__PURE__ */ jsx(DropdownMenuItem, { variant: item.variant, onClick: () => item.onSelect?.(), children: content }, item.id);
56
69
  };
57
70
  const builtInItems = [
58
- profileHref ? { id: "profile", label: labels.profile, icon: CircleUser, href: profileHref } : null,
71
+ profileHref || accountUrl !== void 0 ? { id: "profile", label: labels.profile, icon: CircleUser, href: profileHref ?? `${accountUrl}/profile` } : null,
72
+ accountUrl !== void 0 ? { id: "preferences", label: labels.preferences, icon: SlidersHorizontal, href: `${accountUrl}/preferences` } : null,
73
+ accountUrl !== void 0 ? { id: "security", label: labels.security, icon: ShieldCheck, href: `${accountUrl}/security` } : null,
59
74
  settingsHref ? { id: "settings", label: labels.settings, icon: Settings, href: settingsHref } : null,
60
75
  helpHref ? { id: "help", label: labels.help, icon: HelpCircle, href: helpHref } : null
61
76
  ];
62
77
  const visibleBuiltIns = builtInItems.filter((item) => item !== null);
63
- const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;
78
+ const mainItems = items.filter((item) => item.variant !== "destructive");
79
+ const exitItems = items.filter((item) => item.variant === "destructive");
80
+ const hasEntries = visibleBuiltIns.length > 0 || mainItems.length > 0 || showThemeSubmenu;
81
+ const hasExitGroup = exitItems.length > 0 || Boolean(handleLogout);
64
82
  return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
65
83
  /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
66
84
  "button",
@@ -95,7 +113,7 @@ function UserMenu({
95
113
  ] }),
96
114
  hasEntries ? /* @__PURE__ */ jsx(DropdownMenuSeparator, {}) : null,
97
115
  visibleBuiltIns.map(renderItem),
98
- items.map(renderItem),
116
+ mainItems.map(renderItem),
99
117
  showThemeSubmenu !== false ? /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [
100
118
  /* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { className: cn(showThemeSubmenu === "mobile" && "md:hidden"), children: [
101
119
  /* @__PURE__ */ jsx(Palette, { className: "size-4" }),
@@ -116,12 +134,13 @@ function UserMenu({
116
134
  ] })
117
135
  ] })
118
136
  ] }) : null,
119
- handleLogout ? /* @__PURE__ */ jsxs(Fragment, { children: [
137
+ hasExitGroup ? /* @__PURE__ */ jsxs(Fragment, { children: [
120
138
  /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
121
- /* @__PURE__ */ jsxs(DropdownMenuItem, { variant: "destructive", onClick: handleLogout, children: [
139
+ exitItems.map(renderItem),
140
+ handleLogout ? /* @__PURE__ */ jsxs(DropdownMenuItem, { variant: "destructive", onClick: handleLogout, children: [
122
141
  /* @__PURE__ */ jsx(LogOut, { className: "size-4" }),
123
142
  /* @__PURE__ */ jsx("span", { children: labels.logout })
124
- ] })
143
+ ] }) : null
125
144
  ] }) : null
126
145
  ] })
127
146
  ] });
@@ -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ơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */\n profileHref?: string;\n /** Link cài đặt (nhãn `settings`). */\n settingsHref?: string;\n /** Link trợ giúp (nhãn `help`). */\n helpHref?: string;\n /** Mục bổ sung của app — chèn ngay trên mục đăng xuất. */\n items?: UserMenuItem[];\n /**\n * Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.\n *\n * `\"mobile\"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có\n * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.\n * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn\n * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.\n * `false` — không bao giờ hiện.\n *\n * ⚠ Nhánh `\"mobile\"` ẩn bằng CSS (`md:hidden`), không phải bằng điều kiện render: nút gạt theo\n * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại\n * phần tử khỏi CẢ cây trợ năng nên trình đọc màn hình ở desktop cũng không thấy mục thừa.\n */\n showThemeSubmenu?: boolean | \"mobile\";\n /** Ghi đè handler đăng xuất của context. */\n onLogout?: () => void;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,\n * submenu đổi giao diện, và mục đăng xuất (tô màu destructive).\n *\n * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.\n */\nexport function UserMenu({\n className,\n showName = false,\n profileHref,\n settingsHref,\n helpHref,\n items = [],\n showThemeSubmenu = \"mobile\",\n onLogout,\n align = \"end\"\n}: UserMenuProps) {\n const shell = useShell();\n const { theme, setTheme } = useTheme();\n const { user, labels } = shell;\n\n if (!user) return null;\n\n const handleLogout = onLogout ?? shell.onLogout;\n\n const renderItem = (item: UserMenuItem) => {\n const Icon = item.icon;\n const content = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n <span className=\"truncate\">{item.label}</span>\n {item.shortcut ? <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut> : null}\n </>\n );\n\n if (item.href) {\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} asChild>\n <ShellNavLink href={item.href} onClick={() => item.onSelect?.()}>\n {content}\n </ShellNavLink>\n </DropdownMenuItem>\n );\n }\n\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} onClick={() => item.onSelect?.()}>\n {content}\n </DropdownMenuItem>\n );\n };\n\n const builtInItems: (UserMenuItem | null)[] = [\n profileHref ? { id: \"profile\", label: labels.profile, icon: CircleUser, href: profileHref } : null,\n settingsHref ? { id: \"settings\", label: labels.settings, icon: Settings, href: settingsHref } : null,\n helpHref ? { id: \"help\", label: labels.help, icon: HelpCircle, href: helpHref } : null\n ];\n const visibleBuiltIns = builtInItems.filter((item): item is UserMenuItem => item !== null);\n\n const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.account}\n className={cn(\n \"flex items-center gap-2 rounded-md p-1 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-muted\",\n className\n )}\n >\n <UserAvatar user={user} />\n {showName ? (\n <>\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">{user.name}</span>\n {user.role ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{user.role}</span>\n ) : null}\n </span>\n <ChevronDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n </button>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align={align} className=\"w-60\">\n <DropdownMenuLabel className=\"flex items-center gap-2 font-normal\">\n <UserAvatar user={user} />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email ? <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span> : null}\n {user.role ? <span className=\"truncate text-[11px] text-muted-foreground\">{user.role}</span> : null}\n </span>\n </DropdownMenuLabel>\n\n {hasEntries ? <DropdownMenuSeparator /> : null}\n\n {visibleBuiltIns.map(renderItem)}\n {items.map(renderItem)}\n\n {showThemeSubmenu !== false ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className={cn(showThemeSubmenu === \"mobile\" && \"md:hidden\")}>\n <Palette className=\"size-4\" />\n <span>{labels.appearance}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n <DropdownMenuItem onClick={() => setTheme(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n\n {handleLogout ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n <span>{labels.logout}</span>\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/user-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n ChevronDown,\n CircleUser,\n HelpCircle,\n LogOut,\n Monitor,\n Moon,\n Palette,\n Settings,\n ShieldCheck,\n SlidersHorizontal,\n Sun\n} 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 /**\n * Gốc của Comitor.Account (vd `https://account.comitor.ai`, không dấu `/` cuối). Có giá trị thì menu\n * dựng sẵn BA mục trỏ sang Account — Hồ sơ (`/profile`), Tuỳ chọn hiển thị (`/preferences`), Bảo mật\n * (`/security`) — đứng đầu menu, trước mục của app. Chính app Account truyền chuỗi rỗng `\"\"` để ra\n * đường dẫn tương đối. `profileHref` vẫn thắng nếu app khai cả hai.\n *\n * Ba trang ấy là của DANH TÍNH dùng chung cho cả hệ sinh thái, nên mọi app hiện cùng ba mục, cùng\n * chữ, cùng thứ tự — gói giữ nhãn để không app nào phải dịch lại.\n */\n accountUrl?: string;\n /** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */\n profileHref?: string;\n /** Link cài đặt (nhãn `settings`). */\n settingsHref?: string;\n /** Link trợ giúp (nhãn `help`). */\n helpHref?: string;\n /**\n * Mục bổ sung của app. Mục thường đứng sau các mục dựng sẵn; mục `variant: \"destructive\"` (vd \"Thoát\n * khỏi mọi sản phẩm\") vào NHÓM THOÁT — dưới vạch phân cách, ngay trên mục đăng xuất — để mọi lối ra\n * nằm chung một nhóm, tách khỏi các mục điều hướng.\n */\n items?: UserMenuItem[];\n /**\n * Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.\n *\n * `\"mobile\"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có\n * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.\n * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn\n * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.\n * `false` — không bao giờ hiện.\n *\n * ⚠ Nhánh `\"mobile\"` ẩn bằng CSS (`md:hidden`), không phải bằng điều kiện render: nút gạt theo\n * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại\n * phần tử khỏi CẢ cây trợ năng nên trình đọc màn hình ở desktop cũng không thấy mục thừa.\n */\n showThemeSubmenu?: boolean | \"mobile\";\n /** Ghi đè handler đăng xuất của context. */\n onLogout?: () => void;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,\n * submenu đổi giao diện, và mục đăng xuất (tô màu destructive).\n *\n * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.\n */\nexport function UserMenu({\n className,\n showName = false,\n accountUrl,\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 || accountUrl !== undefined\n ? { id: \"profile\", label: labels.profile, icon: CircleUser, href: profileHref ?? `${accountUrl}/profile` }\n : null,\n accountUrl !== undefined\n ? { id: \"preferences\", label: labels.preferences, icon: SlidersHorizontal, href: `${accountUrl}/preferences` }\n : null,\n accountUrl !== undefined\n ? { id: \"security\", label: labels.security, icon: ShieldCheck, href: `${accountUrl}/security` }\n : 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 mainItems = items.filter((item) => item.variant !== \"destructive\");\n const exitItems = items.filter((item) => item.variant === \"destructive\");\n\n const hasEntries = visibleBuiltIns.length > 0 || mainItems.length > 0 || showThemeSubmenu;\n const hasExitGroup = exitItems.length > 0 || Boolean(handleLogout);\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 {mainItems.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 {hasExitGroup ? (\n <>\n <DropdownMenuSeparator />\n {exitItems.map(renderItem)}\n {handleLogout ? (\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n <span>{labels.logout}</span>\n </DropdownMenuItem>\n ) : null}\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "3.7.1",
3
+ "version": "3.9.0",
4
4
  "description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
5
5
  "license": "MIT",
6
6
  "type": "module",