@comitor/ui 3.3.2 → 3.3.4

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 (56) hide show
  1. package/dist/components/composite/command-palette.d.ts +1 -1
  2. package/dist/components/composite/command-palette.js.map +1 -1
  3. package/dist/components/composite/data-table.d.ts +10 -5
  4. package/dist/components/composite/data-table.js +2 -2
  5. package/dist/components/composite/data-table.js.map +1 -1
  6. package/dist/components/composite/filter-chips.d.ts +1 -1
  7. package/dist/components/composite/filter-chips.js.map +1 -1
  8. package/dist/components/composite/inline-edit.d.ts +2 -2
  9. package/dist/components/composite/inline-edit.js.map +1 -1
  10. package/dist/components/composite/route-tabs.d.ts +8 -6
  11. package/dist/components/composite/route-tabs.js.map +1 -1
  12. package/dist/components/composite/status-config.d.ts +1 -1
  13. package/dist/components/composite/status-config.js.map +1 -1
  14. package/dist/components/ui/progress.d.ts +14 -2
  15. package/dist/components/ui/progress.js +12 -3
  16. package/dist/components/ui/progress.js.map +1 -1
  17. package/dist/components/ui/resizable.d.ts +1 -1
  18. package/dist/components/ui/resizable.js.map +1 -1
  19. package/dist/data-grid/data-grid-context-menu.js +3 -2
  20. package/dist/data-grid/data-grid-context-menu.js.map +1 -1
  21. package/dist/data-grid/types.d.ts +7 -2
  22. package/dist/data-grid/use-data-grid.d.ts +19 -4
  23. package/dist/data-grid/use-data-grid.js +7 -5
  24. package/dist/data-grid/use-data-grid.js.map +1 -1
  25. package/dist/i18n/namespaces/combobox.js +2 -2
  26. package/dist/i18n/namespaces/combobox.js.map +1 -1
  27. package/dist/i18n/namespaces/confirm-dialog.js +1 -1
  28. package/dist/i18n/namespaces/confirm-dialog.js.map +1 -1
  29. package/dist/i18n/namespaces/date-picker.js +2 -2
  30. package/dist/i18n/namespaces/date-picker.js.map +1 -1
  31. package/dist/i18n/namespaces/inline-edit.js +1 -1
  32. package/dist/i18n/namespaces/inline-edit.js.map +1 -1
  33. package/dist/i18n/namespaces/search-filter-bar.js +2 -2
  34. package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
  35. package/dist/i18n/namespaces/time-picker.js +1 -1
  36. package/dist/i18n/namespaces/time-picker.js.map +1 -1
  37. package/dist/shell/app-launcher.d.ts +2 -2
  38. package/dist/shell/app-launcher.js.map +1 -1
  39. package/dist/shell/app-shell.d.ts +1 -1
  40. package/dist/shell/app-shell.js.map +1 -1
  41. package/dist/shell/comitor-logo.d.ts +1 -1
  42. package/dist/shell/comitor-logo.js.map +1 -1
  43. package/dist/shell/shell-context.d.ts +3 -2
  44. package/dist/shell/shell-context.js.map +1 -1
  45. package/dist/shell/sidebar-group.d.ts +1 -1
  46. package/dist/shell/sidebar-group.js.map +1 -1
  47. package/dist/shell/types.d.ts +1 -1
  48. package/dist/shell/types.js.map +1 -1
  49. package/dist/shell/utils.d.ts +2 -2
  50. package/dist/shell/utils.js.map +1 -1
  51. package/dist/tokens.d.ts +23 -9
  52. package/dist/tokens.js +17 -9
  53. package/dist/tokens.js.map +1 -1
  54. package/dist/uploader/image-upload-field.d.ts +2 -2
  55. package/dist/uploader/image-upload-field.js.map +1 -1
  56. package/package.json +1 -1
@@ -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 (Linear-style: nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định\n * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của\n * `ShellProvider`/`AppShell`.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.\n *\n * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại\n * \"bấm Collapse\" sẽ không khớp tên truy cập. \"Collapse\" ⊂ \"Collapse sidebar\" ✓, \"Thu gọn\" ⊂\n * \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và\n * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước (\"12 thành viên\"), tiếng Anh cũng vậy\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * là khoá cứng trật tự của một ngôn ngữ vào mọi bản dịch. Hàm còn chọn được dạng số ít/nhiều\n * (\"1 member\" ⇄ \"2 members\"), thứ tiếng Việt không có mà tiếng Anh thì bắt buộc. Cùng khuôn\n * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AA4NjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
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}\n\n/**\n * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AA4NjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
@@ -31,11 +31,11 @@ declare function toSearchParams(input: NavSearchParams | null | undefined): URLS
31
31
  *
32
32
  * ─ QUERY TRONG `href` ─
33
33
  * `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`
34
- * làm mục kiểu `/cai-dat?tab=thanh-vien` vĩnh viễn không sáng. Query bị tách khỏi `href`
34
+ * làm mục kiểu `/settings?tab=notifications` vĩnh viễn không sáng. Query bị tách khỏi `href`
35
35
  * trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.
36
36
  *
37
37
  * Không truyền `searchParams` → shell không biết đang ở tab nào, và khi đó **chỉ mục KHÔNG mang
38
- * query mới sáng** (`/cai-dat` sáng, `/cai-dat?tab=…` thì không).
38
+ * query mới sáng** (`/settings` sáng, `/settings?tab=…` thì không).
39
39
  *
40
40
  * ⚠ Bản trước cho MỌI mục cùng đường dẫn cùng sáng, với lý lẽ "đúng mức thông tin đang có, và tốt
41
41
  * hơn không mục nào sáng". Lý lẽ đó dựng một lựa chọn nhị phân giả: còn khả năng thứ ba, và nó tốt
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/utils.ts"],"sourcesContent":["/**\n * Hàm thuần dùng chung cho tầng shell — KHÔNG hook, KHÔNG directive\n * (dùng được ở cả server lẫn client).\n */\n\nimport type { CSSProperties } from \"react\";\nimport type { AppDescriptor, NavGroup, NavItem, ShellNav } from \"./types.js\";\n\n/**\n * Tiện ích chuỗi tiếng Việt — **cùng một binding** với entry chính (`@comitor/ui`).\n *\n * Re-export chứ không viết lại. Hai lần repo này đã trả giá cho việc khai lại:\n * `getVietnameseInitials` từng có hai thân hàm (`toUpperCase()` ở đây,\n * `toLocaleUpperCase(\"vi-VN\")` ở Tier 2) nên cùng một tên cho ra hai kết quả tuỳ chỗ import;\n * `normalizeVi` thì có hai khai báo giống nhau chữ-cho-chữ, chưa sai kết quả nhưng là hai chỗ\n * để sửa lệch nhau lần sau. Tên vẫn xuất ở cả hai entry (không phá API của tầng shell), nhưng\n * chỉ còn MỘT thân hàm — xem docs/quyet-dinh-tich-hop.md §28 (\"một tên, một cửa\").\n */\nexport { getVietnameseInitials, normalizeVi } from \"../components/composite/text-utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Điều hướng\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** Phân biệt `NavGroup` với `NavItem` khi app truyền mảng phẳng. */\nfunction isNavGroup(value: NavItem | NavGroup): value is NavGroup {\n return Array.isArray((value as NavGroup).items);\n}\n\n/**\n * Chuẩn hoá menu app truyền vào về `NavGroup[]`.\n * Mảng phẳng `NavItem[]` → một nhóm không nhãn (id \"default\").\n */\nexport function toNavGroups(nav: ShellNav | undefined): NavGroup[] {\n if (!nav || nav.length === 0) return [];\n const first = nav[0];\n if (first && isNavGroup(first)) return nav as NavGroup[];\n return [{ id: \"default\", items: nav as NavItem[] }];\n}\n\n/** Gộp mọi item của mọi nhóm thành một mảng phẳng (kể cả mục con). */\nexport function flattenNavItems(groups: NavGroup[]): NavItem[] {\n const out: NavItem[] = [];\n const walk = (items: NavItem[]) => {\n for (const item of items) {\n out.push(item);\n if (item.children?.length) walk(item.children);\n }\n };\n for (const group of groups) walk(group.items);\n return out;\n}\n\n/**\n * Query string hiện tại, theo mọi hình dạng app thường có sẵn trong tay:\n * `useSearchParams()` của Next (một `URLSearchParams`), chuỗi `\"?tab=x\"` / `\"tab=x\"`,\n * hoặc object `searchParams` mà Server Component nhận được.\n */\nexport type NavSearchParams = string | URLSearchParams | Record<string, string | string[] | undefined>;\n\n/** Tách `href` thành đường dẫn + query. Hash bị bỏ: `pathname` không bao giờ chứa nó. */\nfunction splitHref(href: string): { path: string; query: string } {\n const hashAt = href.indexOf(\"#\");\n const noHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const queryAt = noHash.indexOf(\"?\");\n if (queryAt === -1) return { path: noHash, query: \"\" };\n return { path: noHash.slice(0, queryAt), query: noHash.slice(queryAt + 1) };\n}\n\n/** Quy mọi hình dạng `NavSearchParams` về `URLSearchParams`. `null`/`undefined` → `null`. */\nexport function toSearchParams(input: NavSearchParams | null | undefined): URLSearchParams | null {\n if (input === null || input === undefined) return null;\n if (typeof input === \"string\") return new URLSearchParams(input.startsWith(\"?\") ? input.slice(1) : input);\n // Duck-typing thay vì `instanceof`: `ReadonlyURLSearchParams` của Next kế thừa\n // `URLSearchParams`, nhưng `instanceof` còn hỏng khi hai realm khác nhau (test, iframe).\n if (typeof (input as URLSearchParams).getAll === \"function\") return input as URLSearchParams;\n const out = new URLSearchParams();\n for (const [key, value] of Object.entries(input as Record<string, string | string[] | undefined>)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const one of value) out.append(key, one);\n else out.append(key, value);\n }\n return out;\n}\n\n/** Mọi cặp key=value trong `query` đều có mặt ở `current` (khớp TẬP CON, không đòi bằng hệt). */\nfunction queryIsSubsetOf(query: string, current: URLSearchParams): boolean {\n for (const [key, value] of new URLSearchParams(query)) {\n if (!current.getAll(key).includes(value)) return false;\n }\n return true;\n}\n\n/** Đường dẫn hiệu dụng của một href. `\"?tab=x\"` (chỉ có query) = chính trang đang mở. */\nfunction navItemPath(href: string, pathname: string): string {\n const { path } = splitHref(href);\n return path === \"\" ? pathname : path;\n}\n\n/**\n * Active matching \"thông minh\": chỉ dùng `startsWith` khi KHÔNG có mục nào khác\n * khớp CHÍNH XÁC `pathname`. Nếu không, `/tasks` vẫn sáng khi đang đứng ở\n * `/tasks/inbox` (có mục riêng cho nó).\n *\n * ─ QUERY TRONG `href` ─\n * `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`\n * làm mục kiểu `/cai-dat?tab=thanh-vien` vĩnh viễn không sáng. Query bị tách khỏi `href`\n * trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.\n *\n * Không truyền `searchParams` → shell không biết đang ở tab nào, và khi đó **chỉ mục KHÔNG mang\n * query mới sáng** (`/cai-dat` sáng, `/cai-dat?tab=…` thì không).\n *\n * ⚠ Bản trước cho MỌI mục cùng đường dẫn cùng sáng, với lý lẽ \"đúng mức thông tin đang có, và tốt\n * hơn không mục nào sáng\". Lý lẽ đó dựng một lựa chọn nhị phân giả: còn khả năng thứ ba, và nó tốt\n * hơn cả hai. Bốn hàng cùng sáng không phải là \"chưa đủ thông tin\" — nó là một câu KHẲNG ĐỊNH SAI\n * với người dùng (\"bạn đang ở cả bốn chỗ\"), trong khi tô đúng một hàng canonical chỉ là chưa đủ\n * chi tiết. Ở `comitor-starter` lỗi này làm sáng cùng lúc \"Thành viên\", \"Chung\", \"Thông báo\" và\n * \"Nâng cao\".\n *\n * Ngoại lệ giữ lại: nếu KHÔNG có mục nào cùng đường dẫn mà không mang query (tức mọi lối vào trang\n * đó đều là một tab), thì các mục có query lại cùng sáng — vì lúc đó \"không hàng nào sáng\" mới là\n * kết quả tệ nhất.\n *\n * Muốn đúng từng tab thì truyền `searchParams` — xem prop cùng tên của `ShellProvider`.\n *\n * @param siblings Danh sách mục để đối chiếu khớp chính xác — nên truyền toàn bộ\n * mục của app hiện tại (`flattenNavItems`), không chỉ mục cùng cấp.\n * @param searchParams Query hiện tại (TUỲ CHỌN). Có nó thì mục có query phải khớp TẬP CON\n * (`?tab=a` sáng ở `?tab=a&q=x`), và mục KHÔNG query nhường cho mục cùng\n * đường dẫn có query khớp — tab mặc định không sáng cùng lúc với tab đang mở.\n */\nexport function isNavItemActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (item.isActive !== undefined) return item.isActive;\n if (!pathname || !item.href) return false;\n\n const { query } = splitHref(item.href);\n const itemPath = navItemPath(item.href, pathname);\n const current = toSearchParams(searchParams);\n\n if (pathname === itemPath) {\n if (!current) {\n // Query chưa biết → mục canonical (không query) thắng; xem chú thích ở đầu hàm.\n if (query === \"\") return true;\n return !siblings.some(\n (other) =>\n other.href !== undefined && splitHref(other.href).query === \"\" && splitHref(other.href).path === pathname\n );\n }\n if (query !== \"\") return queryIsSubsetOf(query, current);\n return !siblings.some((other) => {\n if (other.href === item.href) return false;\n const otherQuery = splitHref(other.href).query;\n return (\n otherQuery !== \"\" && navItemPath(other.href, pathname) === pathname && queryIsSubsetOf(otherQuery, current)\n );\n });\n }\n\n // So khớp chính xác của mục khác cũng phải bỏ query, nếu không `/cai-dat?tab=x` sẽ không\n // bao giờ được tính là \"đã có mục khớp đúng\" và mục cha `/cai-dat` sáng nhầm theo startsWith.\n const hasExactMatchElsewhere = siblings.some(\n (other) => other.href !== item.href && splitHref(other.href).path === pathname\n );\n if (hasExactMatchElsewhere) return false;\n return itemPath !== \"/\" && pathname.startsWith(`${itemPath}/`);\n}\n\n/** Mục cha được coi là active khi chính nó hoặc bất kỳ mục con nào active. */\nexport function isNavBranchActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (isNavItemActive(item, pathname, siblings, searchParams)) return true;\n return (item.children ?? []).some((child) => isNavBranchActive(child, pathname, siblings, searchParams));\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n App (trục 2)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */\nexport function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled !== false);\n}\n\n/** App chưa mở khoá — hiển thị ở nhóm `discoverApps` của AppLauncher. */\nexport function getLockedApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled === false);\n}\n\n/**\n * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để\n * \"/crm/deals\" không rơi nhầm vào app có href \"/\".\n *\n * Query trong `href` bị tách ra trước khi so, cùng lý do với `isNavItemActive`: `pathname`\n * không mang query, nên app khai `href: \"/crm?view=board\"` mà so nguyên chuỗi thì không bao\n * giờ được nhận ra. Sắp xếp cũng theo độ dài ĐƯỜNG DẪN (query dài không được đẩy app lên trước).\n */\nexport function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null {\n if (apps.length === 0) return null;\n const matched = apps\n .map((app) => ({ app, path: splitHref(app.href).path }))\n .sort((a, b) => b.path.length - a.path.length)\n .find(({ path }) => pathname === path || (path !== \"/\" && path !== \"\" && pathname.startsWith(`${path}/`)))?.app;\n return matched ?? apps[0] ?? null;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Biến CSS theo app\n\n (Màu avatar KHÔNG còn cửa riêng ở tầng shell: `getAvatarToneClass` — alias một dòng của\n `getAvatarToneClasses`, lệch tên tham số `seed` ⇄ `name` và phải kèm chú thích dặn \"seed\n phải là tên hiển thị\" — đã bị xoá ở 0.2.0. `UserMenu`/`WorkspaceSwitcher` nhập thẳng\n `getAvatarToneClasses` từ `composite/letter-avatar.js`; app dùng `@comitor/ui`.)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Inline style gán bộ biến accent cho một app cụ thể.\n * Trả về `undefined` khi app không khai gì → giữ nguyên token mặc định của theme.\n *\n * Cả BA vai trò đều phải đi qua đây, không chỉ `--app-accent`: theo `docs/hop-dong-mau.md` §5\n * thì `--app-accent` chỉ để TÔ NỀN, `--app-accent-foreground` là chữ trên nền đặc đó, còn\n * `--app-accent-ink` là chính màu app khi đi lên nền trang. Thiếu cửa cho `--app-accent-ink`\n * thì app đổi accent sang màu khác vẫn để lại bậc mực gold mặc định (#7A5800 ở light /\n * #F5D060 ở dark) ở mọi chỗ dùng `text-app-accent-ink` — sai màu mà không sai chuẩn.\n */\nexport function appAccentStyle(\n app: Pick<AppDescriptor, \"accent\" | \"accentForeground\" | \"accentInk\"> | null | undefined\n) {\n if (!app?.accent && !app?.accentForeground && !app?.accentInk) return undefined;\n const style: Record<string, string> = {};\n if (app.accent) style[\"--app-accent\"] = app.accent;\n if (app.accentForeground) style[\"--app-accent-foreground\"] = app.accentForeground;\n if (app.accentInk) style[\"--app-accent-ink\"] = app.accentInk;\n return style as CSSProperties;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/utils.ts"],"sourcesContent":["/**\n * Hàm thuần dùng chung cho tầng shell — KHÔNG hook, KHÔNG directive\n * (dùng được ở cả server lẫn client).\n */\n\nimport type { CSSProperties } from \"react\";\nimport type { AppDescriptor, NavGroup, NavItem, ShellNav } from \"./types.js\";\n\n/**\n * Tiện ích chuỗi tiếng Việt — **cùng một binding** với entry chính (`@comitor/ui`).\n *\n * Re-export chứ không viết lại. Hai lần repo này đã trả giá cho việc khai lại:\n * `getVietnameseInitials` từng có hai thân hàm (`toUpperCase()` ở đây,\n * `toLocaleUpperCase(\"vi-VN\")` ở Tier 2) nên cùng một tên cho ra hai kết quả tuỳ chỗ import;\n * `normalizeVi` thì có hai khai báo giống nhau chữ-cho-chữ, chưa sai kết quả nhưng là hai chỗ\n * để sửa lệch nhau lần sau. Tên vẫn xuất ở cả hai entry (không phá API của tầng shell), nhưng\n * chỉ còn MỘT thân hàm — xem docs/quyet-dinh-tich-hop.md §28 (\"một tên, một cửa\").\n */\nexport { getVietnameseInitials, normalizeVi } from \"../components/composite/text-utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Điều hướng\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** Phân biệt `NavGroup` với `NavItem` khi app truyền mảng phẳng. */\nfunction isNavGroup(value: NavItem | NavGroup): value is NavGroup {\n return Array.isArray((value as NavGroup).items);\n}\n\n/**\n * Chuẩn hoá menu app truyền vào về `NavGroup[]`.\n * Mảng phẳng `NavItem[]` → một nhóm không nhãn (id \"default\").\n */\nexport function toNavGroups(nav: ShellNav | undefined): NavGroup[] {\n if (!nav || nav.length === 0) return [];\n const first = nav[0];\n if (first && isNavGroup(first)) return nav as NavGroup[];\n return [{ id: \"default\", items: nav as NavItem[] }];\n}\n\n/** Gộp mọi item của mọi nhóm thành một mảng phẳng (kể cả mục con). */\nexport function flattenNavItems(groups: NavGroup[]): NavItem[] {\n const out: NavItem[] = [];\n const walk = (items: NavItem[]) => {\n for (const item of items) {\n out.push(item);\n if (item.children?.length) walk(item.children);\n }\n };\n for (const group of groups) walk(group.items);\n return out;\n}\n\n/**\n * Query string hiện tại, theo mọi hình dạng app thường có sẵn trong tay:\n * `useSearchParams()` của Next (một `URLSearchParams`), chuỗi `\"?tab=x\"` / `\"tab=x\"`,\n * hoặc object `searchParams` mà Server Component nhận được.\n */\nexport type NavSearchParams = string | URLSearchParams | Record<string, string | string[] | undefined>;\n\n/** Tách `href` thành đường dẫn + query. Hash bị bỏ: `pathname` không bao giờ chứa nó. */\nfunction splitHref(href: string): { path: string; query: string } {\n const hashAt = href.indexOf(\"#\");\n const noHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const queryAt = noHash.indexOf(\"?\");\n if (queryAt === -1) return { path: noHash, query: \"\" };\n return { path: noHash.slice(0, queryAt), query: noHash.slice(queryAt + 1) };\n}\n\n/** Quy mọi hình dạng `NavSearchParams` về `URLSearchParams`. `null`/`undefined` → `null`. */\nexport function toSearchParams(input: NavSearchParams | null | undefined): URLSearchParams | null {\n if (input === null || input === undefined) return null;\n if (typeof input === \"string\") return new URLSearchParams(input.startsWith(\"?\") ? input.slice(1) : input);\n // Duck-typing thay vì `instanceof`: `ReadonlyURLSearchParams` của Next kế thừa\n // `URLSearchParams`, nhưng `instanceof` còn hỏng khi hai realm khác nhau (test, iframe).\n if (typeof (input as URLSearchParams).getAll === \"function\") return input as URLSearchParams;\n const out = new URLSearchParams();\n for (const [key, value] of Object.entries(input as Record<string, string | string[] | undefined>)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const one of value) out.append(key, one);\n else out.append(key, value);\n }\n return out;\n}\n\n/** Mọi cặp key=value trong `query` đều có mặt ở `current` (khớp TẬP CON, không đòi bằng hệt). */\nfunction queryIsSubsetOf(query: string, current: URLSearchParams): boolean {\n for (const [key, value] of new URLSearchParams(query)) {\n if (!current.getAll(key).includes(value)) return false;\n }\n return true;\n}\n\n/** Đường dẫn hiệu dụng của một href. `\"?tab=x\"` (chỉ có query) = chính trang đang mở. */\nfunction navItemPath(href: string, pathname: string): string {\n const { path } = splitHref(href);\n return path === \"\" ? pathname : path;\n}\n\n/**\n * Active matching \"thông minh\": chỉ dùng `startsWith` khi KHÔNG có mục nào khác\n * khớp CHÍNH XÁC `pathname`. Nếu không, `/tasks` vẫn sáng khi đang đứng ở\n * `/tasks/inbox` (có mục riêng cho nó).\n *\n * ─ QUERY TRONG `href` ─\n * `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`\n * làm mục kiểu `/settings?tab=notifications` vĩnh viễn không sáng. Query bị tách khỏi `href`\n * trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.\n *\n * Không truyền `searchParams` → shell không biết đang ở tab nào, và khi đó **chỉ mục KHÔNG mang\n * query mới sáng** (`/settings` sáng, `/settings?tab=…` thì không).\n *\n * ⚠ Bản trước cho MỌI mục cùng đường dẫn cùng sáng, với lý lẽ \"đúng mức thông tin đang có, và tốt\n * hơn không mục nào sáng\". Lý lẽ đó dựng một lựa chọn nhị phân giả: còn khả năng thứ ba, và nó tốt\n * hơn cả hai. Bốn hàng cùng sáng không phải là \"chưa đủ thông tin\" — nó là một câu KHẲNG ĐỊNH SAI\n * với người dùng (\"bạn đang ở cả bốn chỗ\"), trong khi tô đúng một hàng canonical chỉ là chưa đủ\n * chi tiết. Ở `comitor-starter` lỗi này làm sáng cùng lúc \"Thành viên\", \"Chung\", \"Thông báo\" và\n * \"Nâng cao\".\n *\n * Ngoại lệ giữ lại: nếu KHÔNG có mục nào cùng đường dẫn mà không mang query (tức mọi lối vào trang\n * đó đều là một tab), thì các mục có query lại cùng sáng — vì lúc đó \"không hàng nào sáng\" mới là\n * kết quả tệ nhất.\n *\n * Muốn đúng từng tab thì truyền `searchParams` — xem prop cùng tên của `ShellProvider`.\n *\n * @param siblings Danh sách mục để đối chiếu khớp chính xác — nên truyền toàn bộ\n * mục của app hiện tại (`flattenNavItems`), không chỉ mục cùng cấp.\n * @param searchParams Query hiện tại (TUỲ CHỌN). Có nó thì mục có query phải khớp TẬP CON\n * (`?tab=a` sáng ở `?tab=a&q=x`), và mục KHÔNG query nhường cho mục cùng\n * đường dẫn có query khớp — tab mặc định không sáng cùng lúc với tab đang mở.\n */\nexport function isNavItemActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (item.isActive !== undefined) return item.isActive;\n if (!pathname || !item.href) return false;\n\n const { query } = splitHref(item.href);\n const itemPath = navItemPath(item.href, pathname);\n const current = toSearchParams(searchParams);\n\n if (pathname === itemPath) {\n if (!current) {\n // Query chưa biết → mục canonical (không query) thắng; xem chú thích ở đầu hàm.\n if (query === \"\") return true;\n return !siblings.some(\n (other) =>\n other.href !== undefined && splitHref(other.href).query === \"\" && splitHref(other.href).path === pathname\n );\n }\n if (query !== \"\") return queryIsSubsetOf(query, current);\n return !siblings.some((other) => {\n if (other.href === item.href) return false;\n const otherQuery = splitHref(other.href).query;\n return (\n otherQuery !== \"\" && navItemPath(other.href, pathname) === pathname && queryIsSubsetOf(otherQuery, current)\n );\n });\n }\n\n // So khớp chính xác của mục khác cũng phải bỏ query, nếu không `/settings?tab=x` sẽ không\n // bao giờ được tính là \"đã có mục khớp đúng\" và mục cha `/settings` sáng nhầm theo startsWith.\n const hasExactMatchElsewhere = siblings.some(\n (other) => other.href !== item.href && splitHref(other.href).path === pathname\n );\n if (hasExactMatchElsewhere) return false;\n return itemPath !== \"/\" && pathname.startsWith(`${itemPath}/`);\n}\n\n/** Mục cha được coi là active khi chính nó hoặc bất kỳ mục con nào active. */\nexport function isNavBranchActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (isNavItemActive(item, pathname, siblings, searchParams)) return true;\n return (item.children ?? []).some((child) => isNavBranchActive(child, pathname, siblings, searchParams));\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n App (trục 2)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */\nexport function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled !== false);\n}\n\n/** App chưa mở khoá — hiển thị ở nhóm `discoverApps` của AppLauncher. */\nexport function getLockedApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled === false);\n}\n\n/**\n * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để\n * \"/crm/deals\" không rơi nhầm vào app có href \"/\".\n *\n * Query trong `href` bị tách ra trước khi so, cùng lý do với `isNavItemActive`: `pathname`\n * không mang query, nên app khai `href: \"/crm?view=board\"` mà so nguyên chuỗi thì không bao\n * giờ được nhận ra. Sắp xếp cũng theo độ dài ĐƯỜNG DẪN (query dài không được đẩy app lên trước).\n */\nexport function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null {\n if (apps.length === 0) return null;\n const matched = apps\n .map((app) => ({ app, path: splitHref(app.href).path }))\n .sort((a, b) => b.path.length - a.path.length)\n .find(({ path }) => pathname === path || (path !== \"/\" && path !== \"\" && pathname.startsWith(`${path}/`)))?.app;\n return matched ?? apps[0] ?? null;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Biến CSS theo app\n\n (Màu avatar KHÔNG còn cửa riêng ở tầng shell: `getAvatarToneClass` — alias một dòng của\n `getAvatarToneClasses`, lệch tên tham số `seed` ⇄ `name` và phải kèm chú thích dặn \"seed\n phải là tên hiển thị\" — đã bị xoá ở 0.2.0. `UserMenu`/`WorkspaceSwitcher` nhập thẳng\n `getAvatarToneClasses` từ `composite/letter-avatar.js`; app dùng `@comitor/ui`.)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Inline style gán bộ biến accent cho một app cụ thể.\n * Trả về `undefined` khi app không khai gì → giữ nguyên token mặc định của theme.\n *\n * Cả BA vai trò đều phải đi qua đây, không chỉ `--app-accent`: theo `docs/hop-dong-mau.md` §5\n * thì `--app-accent` chỉ để TÔ NỀN, `--app-accent-foreground` là chữ trên nền đặc đó, còn\n * `--app-accent-ink` là chính màu app khi đi lên nền trang. Thiếu cửa cho `--app-accent-ink`\n * thì app đổi accent sang màu khác vẫn để lại bậc mực gold mặc định (#7A5800 ở light /\n * #F5D060 ở dark) ở mọi chỗ dùng `text-app-accent-ink` — sai màu mà không sai chuẩn.\n */\nexport function appAccentStyle(\n app: Pick<AppDescriptor, \"accent\" | \"accentForeground\" | \"accentInk\"> | null | undefined\n) {\n if (!app?.accent && !app?.accentForeground && !app?.accentInk) return undefined;\n const style: Record<string, string> = {};\n if (app.accent) style[\"--app-accent\"] = app.accent;\n if (app.accentForeground) style[\"--app-accent-foreground\"] = app.accentForeground;\n if (app.accentInk) style[\"--app-accent-ink\"] = app.accentInk;\n return style as CSSProperties;\n}\n"],"mappings":"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","names":[]}
package/dist/tokens.d.ts CHANGED
@@ -245,10 +245,13 @@ declare const semantic: {
245
245
  readonly destructiveInk: "#D64545";
246
246
  /** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */
247
247
  readonly border: "#1E2128";
248
- /** Viền ô nhập bản dark của comitor-ds — cùng bậc với `border`. */
249
- readonly input: "#1E2128";
248
+ /** Viền ô nhập bản dark — KHÔNG phải #1E2128 của comitor-ds (chỉ 1,10:1 trên `card`, ô nhập tan
249
+ * vào nền). #434A5A = 2,00:1, mirror `.dark { --input }` từ `0.5.0`; trước `3.3.3` mirror này còn
250
+ * ghi #1E2128. */
251
+ readonly input: "#434A5A";
250
252
  readonly ring: "#F5D060";
251
- readonly controlEdge: "#1E2128";
253
+ /** = `input` (`--control-edge: var(--input)`). */
254
+ readonly controlEdge: "#434A5A";
252
255
  /** comitor-ds dùng `dark:hover:border-gray-500` = `ash[500]` (lật chiều so với light). */
253
256
  readonly controlEdgeStrong: "#7E8290";
254
257
  /** comitor-ds tô `dark:data-[state=checked]:bg-gold-300`, nhưng token `primary` của chính nó
@@ -297,11 +300,14 @@ declare const highContrast: {
297
300
  readonly input: "#7E8290";
298
301
  readonly controlEdge: "#7E8290";
299
302
  readonly controlEdgeStrong: "#3A3F4A";
300
- /** Rãnh ⇄ nền 6,51:1 và rãnh ⇄ núm 6,51:1 (bảng mặc định: 1,86 / 1,86) */
301
- readonly controlOn: "#7A5800";
303
+ /** #B48700 = gold-400 tối đi 2%: 3,28:1 trên `background`/`card` · 3,06:1 trên `muted` · ⇄ núm
304
+ * trắng 3,28:1 (bảng mặc định: 1,86 / 1,86). Trước `3.3.3` mirror này còn ghi gold-500 — giá trị
305
+ * `styles.css` đã bỏ. */
306
+ readonly controlOn: "#B48700";
302
307
  /** = `background`, nên luôn tương phản đúng chiều với cả rãnh BẬT lẫn rãnh TẮT. */
303
308
  readonly controlKnob: "#FFFFFF";
304
- readonly controlCheck: "#FFFFFF";
309
+ /** MỰC trên nền gold (5,77:1) — cùng cách vẽ với bảng mặc định; trước `3.3.3` mirror này ghi trắng. */
310
+ readonly controlCheck: "#0E1116";
305
311
  readonly switchTrack: "#7E8290";
306
312
  readonly sliderTrack: "#7E8290";
307
313
  readonly primaryInk: "#7A5800";
@@ -391,7 +397,9 @@ declare const status: {
391
397
  readonly warningInk: "#7A5800";
392
398
  readonly severeInk: "#AC440F";
393
399
  readonly infoInk: "#1A6668";
394
- readonly destructiveInk: "#9A3537";
400
+ /** = `semantic.light.destructiveInk` (#D64545 — `--destructive-ink: var(--destructive)` ở bảng mặc
401
+ * định). Trước `3.3.3` trỏ nhầm `accentInk.light.red` (#9A3537 — bậc của bảng TƯƠNG PHẢN CAO). */
402
+ readonly destructiveInk: "#D64545";
395
403
  };
396
404
  readonly dark: {
397
405
  readonly success: "#2EA56A";
@@ -411,10 +419,16 @@ declare const status: {
411
419
  readonly destructiveInk: "#D64545";
412
420
  };
413
421
  };
414
- /** Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định. */
422
+ /**
423
+ * Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định.
424
+ *
425
+ * ⚠ `chart-1` ở dark là `gold[300]` như light: `styles.css` KHÔNG khai lại `--chart-1` trong `.dark`
426
+ * (chỉ `--chart-3` tự sáng lên qua `--navy-ink`). Trước `3.3.3` mirror này ghi `gold[200]` — biểu
427
+ * đồ canvas/email/PDF dựng từ `tokens` ra màu khác biểu đồ trên trang.
428
+ */
415
429
  declare const chart: {
416
430
  readonly light: readonly ["#E8B824", "#1F8A8B", "#0F1E3D", "#2EA56A", "#D64545"];
417
- readonly dark: readonly ["#F5D060", "#1F8A8B", "#7EA2E8", "#2EA56A", "#D64545"];
431
+ readonly dark: readonly ["#E8B824", "#1F8A8B", "#7EA2E8", "#2EA56A", "#D64545"];
418
432
  };
419
433
  /**
420
434
  * Cặp nền/chữ (hex) của avatar cho một tên, theo theme.
package/dist/tokens.js CHANGED
@@ -176,10 +176,13 @@ const semantic = {
176
176
  destructiveInk: "#D64545",
177
177
  /** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */
178
178
  border: "#1E2128",
179
- /** Viền ô nhập bản dark của comitor-ds — cùng bậc với `border`. */
180
- input: "#1E2128",
179
+ /** Viền ô nhập bản dark — KHÔNG phải #1E2128 của comitor-ds (chỉ 1,10:1 trên `card`, ô nhập tan
180
+ * vào nền). #434A5A = 2,00:1, mirror `.dark { --input }` từ `0.5.0`; trước `3.3.3` mirror này còn
181
+ * ghi #1E2128. */
182
+ input: "#434A5A",
181
183
  ring: "#F5D060",
182
- controlEdge: "#1E2128",
184
+ /** = `input` (`--control-edge: var(--input)`). */
185
+ controlEdge: "#434A5A",
183
186
  /** comitor-ds dùng `dark:hover:border-gray-500` = `ash[500]` (lật chiều so với light). */
184
187
  controlEdgeStrong: ash[500],
185
188
  /** comitor-ds tô `dark:data-[state=checked]:bg-gold-300`, nhưng token `primary` của chính nó
@@ -214,11 +217,14 @@ const highContrast = {
214
217
  input: "#7E8290",
215
218
  controlEdge: "#7E8290",
216
219
  controlEdgeStrong: ash[700],
217
- /** Rãnh ⇄ nền 6,51:1 và rãnh ⇄ núm 6,51:1 (bảng mặc định: 1,86 / 1,86) */
218
- controlOn: gold[500],
220
+ /** #B48700 = gold-400 tối đi 2%: 3,28:1 trên `background`/`card` · 3,06:1 trên `muted` · ⇄ núm
221
+ * trắng 3,28:1 (bảng mặc định: 1,86 / 1,86). Trước `3.3.3` mirror này còn ghi gold-500 — giá trị
222
+ * `styles.css` đã bỏ. */
223
+ controlOn: "#B48700",
219
224
  /** = `background`, nên luôn tương phản đúng chiều với cả rãnh BẬT lẫn rãnh TẮT. */
220
225
  controlKnob: "#FFFFFF",
221
- controlCheck: "#FFFFFF",
226
+ /** MỰC trên nền gold (5,77:1) — cùng cách vẽ với bảng mặc định; trước `3.3.3` mirror này ghi trắng. */
227
+ controlCheck: brandNeutral.ink,
222
228
  switchTrack: "#7E8290",
223
229
  sliderTrack: "#7E8290",
224
230
  primaryInk: gold[500],
@@ -280,7 +286,9 @@ const status = {
280
286
  warningInk: accentInk.light.yellow,
281
287
  severeInk: accentInk.light.orange,
282
288
  infoInk: accentInk.light.teal,
283
- destructiveInk: accentInk.light.red
289
+ /** = `semantic.light.destructiveInk` (#D64545 — `--destructive-ink: var(--destructive)` ở bảng mặc
290
+ * định). Trước `3.3.3` trỏ nhầm `accentInk.light.red` (#9A3537 — bậc của bảng TƯƠNG PHẢN CAO). */
291
+ destructiveInk: semantic.light.destructiveInk
284
292
  },
285
293
  dark: {
286
294
  success: accent.dark.green,
@@ -297,12 +305,12 @@ const status = {
297
305
  warningInk: accentInk.dark.yellow,
298
306
  severeInk: accentInk.dark.orange,
299
307
  infoInk: accentInk.dark.teal,
300
- destructiveInk: accentInk.dark.red
308
+ destructiveInk: semantic.dark.destructiveInk
301
309
  }
302
310
  };
303
311
  const chart = {
304
312
  light: [gold[300], accent.light.teal, accent.light.navy, accent.light.green, accent.light.red],
305
- dark: [gold[200], accent.dark.teal, accentInk.dark.navy, accent.dark.green, accent.dark.red]
313
+ dark: [gold[300], accent.dark.teal, accentInk.dark.navy, accent.dark.green, accent.dark.red]
306
314
  };
307
315
  const avatarTonePalette = {
308
316
  light: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tokens.ts"],"sourcesContent":["/**\n * Design token dạng TS object — dành cho nơi KHÔNG có Tailwind: canvas, biểu đồ tuỳ biến,\n * email HTML, xuất PDF, (sau này) React Native.\n *\n * ⚠ Nguồn sự thật của giá trị là `styles.css` (CSS variable). File này chỉ MIRROR hằng số.\n * Sửa màu ở styles.css thì phải sửa kèm ở đây — và ngược lại.\n *\n * Trong môi trường có Tailwind, LUÔN dùng class token (`bg-primary`, `text-ash-500`),\n * đừng đọc hex từ đây rồi nhét vào style inline: làm vậy là mất dark mode.\n *\n * Hợp đồng ba vai trò của màu (giống hệt `styles.css`; bảng đầy đủ ở docs/hop-dong-mau.md),\n * đọc tên là biết dùng ở đâu:\n * `x` → tô NỀN (fill đặc hoặc tint /5–/20 — trần chung an toàn cho mọi tone)\n * `xForeground` → chữ/icon NẰM TRÊN nền `x` ĐẶC — đo với `x`, ngưỡng 4,5:1\n * `xInk` → chính màu đó khi ĐI LÊN nền trang hoặc lên tint của chính nó (chữ, icon\n * rời, viền, vạch active) — đo với nền đó\n *\n * Cố ý KHÔNG mirror (xem docs/quyet-dinh-tich-hop.md §21): `--elevation-*` (chuỗi box-shadow chỉ\n * có nghĩa trong CSS), `--app-accent*` (giá trị thuộc về APP, không phải gói), `--font-*`.\n */\n\nimport { AVATAR_TONES, type AvatarToneColors, type AvatarTonePalette, getAvatarToneIndex } from \"./lib/avatar-tone.js\";\n\n/** Thang GOLD 7 bậc — trục màu chính của brand Comitor. */\nexport const gold = {\n 50: \"#FBF5DC\",\n 100: \"#F8E89D\",\n 200: \"#F5D060\",\n /** primary ở light mode */\n 300: \"#E8B824\",\n 400: \"#B88A00\",\n 500: \"#7A5800\",\n 600: \"#5C4006\"\n} as const;\n\n/** Trung tính brand (khác thang ash — đây là màu \"giấy/mực\" của brand book). */\nexport const brandNeutral = {\n ink: \"#0E1116\",\n inkSoft: \"#3A3F4A\",\n paper: \"#FBF7EE\",\n warm: \"#EDE8D8\"\n} as const;\n\n/**\n * Thang xám Comitor — namespace `ash`, KHÔNG phải `gray`.\n * Chỉ có 6 bậc; các bậc khuyết (200/400/600/800) cố ý không khai để không lẫn với\n * thang gray dựng sẵn của Tailwind (xem chú thích trong styles.css).\n */\nexport const ash = {\n 50: \"#F7F7F8\",\n 100: \"#EDEEF1\",\n 300: \"#C8CBD3\",\n 500: \"#7E8290\",\n 700: \"#3A3F4A\",\n 900: \"#16181D\"\n} as const;\n\n/**\n * Màu phụ — vai trò TÔ NỀN (fill đặc hoặc tint). Bản `dark` được nâng sáng vì trên nền #0E1116\n * bản `light` chìm nghỉm.\n *\n * ⚠ ĐỪNG dùng bảng này làm màu CHỮ ở light: nó được chọn để tô nền nên quá nhạt — dùng\n * `accentInk` ngay dưới. `accent.dark.navy` giữ nguyên #0F1E3D chỉ để mirror đúng `styles.css`\n * (khối `.dark` không override `--navy`); vai trò navy ở dark do `accentInk.dark.navy` gánh.\n */\nexport const accent = {\n light: {\n navy: \"#0F1E3D\",\n teal: \"#1F8A8B\",\n red: \"#D64545\",\n green: \"#2EA56A\",\n yellow: \"#F2C94C\",\n /** Gốc của tone `severe`. Giữ 3:1 trên mọi nền trang (3,56:1 trắng · 3,32:1 `muted`) — vì nó\n * TÔ chấm của `StatusPill variant=\"dot\"`; bậc sáng hơn (#F97316) chỉ 2,80:1. */\n orange: \"#EA580C\"\n },\n /** Bảng MẶC ĐỊNH (comitor-ds) không khai lại màu phụ ở dark — cùng hex với light. Bản sáng\n * dành cho nền tối nằm ở `highContrast.dark`. Trừ `orange`: comitor-ds không có cam, và cùng\n * hex thì chữ cam trên tint /15 của chính nó ở dark chỉ 4,21:1 — nên bảng mặc định có bản\n * dark riêng. */\n dark: {\n navy: \"#0F1E3D\",\n teal: \"#1F8A8B\",\n red: \"#D64545\",\n green: \"#2EA56A\",\n yellow: \"#F2C94C\",\n orange: \"#FB923C\"\n }\n} as const;\n\n/**\n * Bậc INK của màu phụ — mirror `--teal-ink` / `--green-ink` / `--red-ink` / `--yellow-ink` /\n * `--orange-ink` / `--navy-ink`. Đây là màu phụ khi nó làm **CHỮ/ICON/VIỀN trên nền trang**\n * (hoặc trên tint /5–/20 của chính nó), tức vai trò NGƯỢC với `accent`.\n *\n * Hợp đồng đặt tên của cả gói: `--x` tô nền · `--x-foreground` là chữ NẰM TRÊN nền `--x` ·\n * `--x-ink` là chính màu đó ĐI LÊN nền trang.\n *\n * Bản `light` là bậc tối đã đóng băng (kết quả của `color-mix(in srgb, --<màu>, --ink 30/40%)`\n * mà trước đây mỗi component tự tính lấy); bản `dark` chính là `accent.dark`. Đo trên tint 15%\n * của chính nó — nền thật mà chữ initials avatar và badge tonal đứng lên: teal 5,54 · green 5,60\n * · red 5,83:1 ở light (bản chưa có ink: 3,44 / 2,69 / 3,57:1 ✗).\n *\n * ⚠ Ở bảng MẶC ĐỊNH, `accent.dark` bằng hex của light nên bậc ink ở dark cũng vậy — đúng thứ\n * comitor-ds render. Bậc sáng cho nền tối nằm ở `highContrast.dark`.\n */\nexport const accentInk = {\n light: {\n navy: accent.light.navy,\n teal: \"#1A6668\",\n red: \"#9A3537\",\n green: \"#216A48\",\n /** = gold[500]: vàng không có bậc tối riêng, mượn thang gold cùng sắc. */\n yellow: gold[500],\n /** = color-mix(srgb, orange 72%, ink 28%) — bậc SÁNG nhất còn giữ trần tint /20: 5,85:1 trắng ·\n * 4,87:1 trên orange/15 · 4,58:1 trên orange/20. Bảng tương phản cao tối hơn: `highContrast`. */\n orange: \"#AC440F\"\n },\n dark: {\n /** navy nguyên bản chìm vào nền dark (1,15:1) → bậc sáng riêng; cũng là `--chart-3` ở dark. */\n navy: \"#7EA2E8\",\n teal: accent.dark.teal,\n red: accent.dark.red,\n green: accent.dark.green,\n yellow: accent.dark.yellow,\n orange: accent.dark.orange\n }\n} as const;\n\n/**\n * Bậc FOREGROUND của màu phụ — mirror `--teal-foreground` / `--green-foreground` /\n * `--yellow-foreground` / `--orange-foreground` / `--navy-foreground`. Đây là chữ/icon nằm\n * **TRÊN nền `accent.*` ĐẶC**, tức vai trò thứ ba, khác hẳn `accentInk` (màu đó đi lên nền TRANG).\n *\n * Cả bốn sắc teal/green/yellow/orange đều sáng ở light và còn sáng hơn ở dark → chữ là MỰC ở cả\n * hai theme, một giá trị đủ dùng: teal 4,56:1 (light) / 10,16:1 (dark) · green 6,04 / 10,85 ·\n * yellow 11,92 / 12,35 · orange 5,31 / 8,36. Navy thì ngược lại — chữ TRẮNG, 16,49:1 ở cả hai theme.\n *\n * ⚠ CỐ Ý KHÔNG có `red`: nền đỏ đặc không mang được chữ tốt ở light — trắng trên #D64545 được\n * 4,38:1 và mực 4,32:1, cả hai đều dưới AA. Nền đỏ đặc có chữ phải dùng `status.*.destructive`;\n * ở bảng tương phản cao token đó là #C93B3B và đạt 5,03:1. `accent.*.red` chỉ để tô TINT và làm\n * màu chart. (Cam thì mực cứu được — 5,31:1 — nên `orange` có mặt.)\n */\nexport const accentForeground = {\n light: {\n navy: \"#FFFFFF\",\n teal: \"#0E1116\",\n green: \"#0E1116\",\n yellow: \"#0E1116\",\n orange: \"#0E1116\"\n },\n dark: {\n navy: \"#FFFFFF\",\n teal: \"#0E1116\",\n green: \"#0E1116\",\n yellow: \"#0E1116\",\n orange: \"#0E1116\"\n }\n} as const;\n\n/** Semantic token của shadcn — mirror khối `:root` / `.dark` trong styles.css. */\nexport const semantic = {\n light: {\n background: \"#FFFFFF\",\n foreground: \"#0E1116\",\n card: \"#FFFFFF\",\n cardForeground: \"#0E1116\",\n popover: \"#FFFFFF\",\n popoverForeground: \"#0E1116\",\n primary: \"#E8B824\",\n primaryForeground: \"#0E1116\",\n /** Bậc INK của brand. Ở bảng MẶC ĐỊNH nó BẰNG `primary`: comitor-ds tô gold cho cả những vai\n * trò mà 1.4.11 đòi 3:1 (rãnh Switch BẬT, gạch active của Tabs) — 1,86:1, đã biết và chấp\n * nhận. Bậc tối #7A5800 nằm ở `highContrast.light.primaryInk`. */\n primaryInk: \"#E8B824\",\n secondary: \"#EDE8D8\",\n secondaryForeground: \"#0E1116\",\n muted: \"#F7F7F8\",\n /** Bậc chữ phụ — giá trị của comitor-ds, bằng đúng `ash[500]`. 3,83:1 trên nền trắng và\n * 3,58:1 trên `muted`, tức dưới AA 4,5:1: đã biết và chấp nhận, bản đạt chuẩn nằm ở\n * `highContrast.light.mutedForeground`. */\n mutedForeground: ash[500],\n accent: \"#FBF5DC\",\n accentForeground: \"#0E1116\",\n /** Nền nút xoá. Chữ trắng trên nó được 4,38:1 — thiếu 0,12 so với AA, đã biết và chấp nhận. */\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n /** Nền nút xoá lúc HOVER = `destructive` ở 90% trên nền trắng, đúng thứ `hover:bg-red/90`\n * của comitor-ds render. Ghim hex vì `/90` phụ thuộc nền phía sau. */\n destructiveHover: \"#DA5A5A\",\n /** Chữ báo lỗi trên nền trang. comitor-ds viết `text-destructive` cho vai trò này, tức cùng\n * hex với nền — 4,38:1 trên nền trắng. */\n destructiveInk: \"#D64545\",\n /** RANH GIỚI TRANG TRÍ (vạch chia, viền thẻ nghỉ) — 1,16:1, cố ý: 1.4.11 không đòi tương\n * phản cho thứ không mang thông tin. Ranh giới của CONTROL đi qua `controlEdge`. */\n border: \"#EDEEF1\",\n /** Viền ô nhập — comitor-ds cho nó cùng bậc với `border`. 1,16:1, đã biết và chấp nhận. */\n input: \"#EDEEF1\",\n ring: \"#E8B824\",\n /** Ranh giới của control lúc NGHỈ — ở bảng mặc định bằng đúng `input`. */\n controlEdge: \"#EDEEF1\",\n /** Cùng vai trò nhưng ở HOVER/ACTIVE. comitor-ds dùng `hover:border-gray-300` = `ash[300]`. */\n controlEdgeStrong: ash[300],\n /** Nền control khi BẬT/ĐÃ CHỌN (rãnh Switch bật, ô Checkbox đã chọn, chấm Radio). comitor-ds\n * tô gold: 1,86:1 với nền trang và với núm — đã biết và chấp nhận. */\n controlOn: \"#E8B824\",\n /** Núm Switch / chấm Radio — TRẮNG ở cả hai theme, đúng `bg-white` của comitor-ds. */\n controlKnob: \"#FFFFFF\",\n /** DẤU CHECK của Checkbox và chữ trên ngày ĐANG CHỌN của Calendar — tách khỏi `controlKnob`\n * vì trên nền gold SÁNG thì dấu phải là MỰC (10,19:1), còn ở bảng tương phản cao nền là\n * gold-500 TỐI nên dấu lật thành màu nền trang. */\n controlCheck: brandNeutral.ink,\n /** Rãnh Switch khi TẮT — comitor-ds dùng `bg-gray-300` / `dark:bg-gray-700`. */\n switchTrack: ash[300],\n /** Rãnh Slider — comitor-ds dùng `bg-muted`, khác bậc với rãnh Switch. */\n sliderTrack: \"#F7F7F8\",\n /** Button variant `link`. comitor-ds dùng `gold[400]` — 3,15:1, dưới AA, đã biết và chấp nhận. */\n linkInk: gold[400],\n linkInkHover: gold[300],\n sidebar: \"#FAFAF9\",\n sidebarForeground: \"#0E1116\",\n sidebarBorder: \"#EDEEF1\",\n sidebarAccent: \"#FBF5DC\",\n sidebarAccentForeground: \"#0E1116\"\n },\n dark: {\n background: \"#0E1116\",\n foreground: \"#F7F7F8\",\n card: \"#16181D\",\n cardForeground: \"#F7F7F8\",\n popover: \"#16181D\",\n popoverForeground: \"#F7F7F8\",\n primary: \"#F5D060\",\n primaryForeground: \"#0E1116\",\n /** Ở dark `primary` vốn đã là bậc sáng (12,67:1 trên nền `background`) nên ink = chính nó. */\n primaryInk: \"#F5D060\",\n secondary: \"#1E2128\",\n secondaryForeground: \"#F7F7F8\",\n muted: \"#1E2128\",\n /** comitor-ds không khai lại token này ở dark — cùng hex với light. 4,21:1 trên `muted`,\n * dưới AA: đã biết và chấp nhận. */\n mutedForeground: ash[500],\n accent: \"#1E2128\",\n accentForeground: \"#F7F7F8\",\n /** Ghim đúng giá trị của light — comitor-ds dùng chung một hex cho cả hai theme. */\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n destructiveHover: \"#DA5A5A\",\n /** 4,06:1 trên `card` — cùng hex với nền, đúng `text-destructive` của comitor-ds. */\n destructiveInk: \"#D64545\",\n /** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */\n border: \"#1E2128\",\n /** Viền ô nhập bản dark của comitor-ds — cùng bậc với `border`. */\n input: \"#1E2128\",\n ring: \"#F5D060\",\n controlEdge: \"#1E2128\",\n /** comitor-ds dùng `dark:hover:border-gray-500` = `ash[500]` (lật chiều so với light). */\n controlEdgeStrong: ash[500],\n /** comitor-ds tô `dark:data-[state=checked]:bg-gold-300`, nhưng token `primary` của chính nó\n * ở dark là #F5D060; gói theo token, nên control BẬT ở dark là bậc sáng. */\n controlOn: \"#F5D060\",\n /** TRẮNG ở cả hai theme, đúng `bg-white` của comitor-ds. */\n controlKnob: \"#FFFFFF\",\n /** comitor-ds vẽ dấu check bằng `text-ink` ở CẢ hai theme. */\n controlCheck: brandNeutral.ink,\n switchTrack: ash[700],\n sliderTrack: \"#1E2128\",\n linkInk: gold[400],\n linkInkHover: gold[300],\n sidebar: \"#16181D\",\n sidebarForeground: \"#F7F7F8\",\n sidebarBorder: \"#1E2128\",\n sidebarAccent: \"#1E2128\",\n sidebarAccentForeground: \"#F7F7F8\"\n }\n} as const;\n\n/**\n * BẢNG MÀU THỨ HAI — TƯƠNG PHẢN CAO. Mirror khối `[data-contrast=\"high\"]` /\n * `.dark[data-contrast=\"high\"]` trong styles.css.\n *\n * Chỉ chứa những token KHÁC bảng mặc định, đúng như CSS: hợp lên trên để có bảng đầy đủ.\n *\n * ```ts\n * const mau = { ...semantic.light, ...highContrast.light };\n * ```\n *\n * Bảng mặc định là comitor-ds (bản duyệt, có những cặp dưới ngưỡng — đã liệt kê ở từng token).\n * Bảng này là bảng đã đo, do NGƯỜI DÙNG bật qua `<ContrastProvider>` + `<ContrastToggle>`.\n * `accent` là màu phụ bản sáng dành cho nền tối — bảng mặc định không có, xem `accent.dark`.\n */\nexport const highContrast = {\n light: {\n /** 5,23:1 trên `background` · 4,89:1 trên `muted` (bảng mặc định: 3,83 / 3,58) */\n mutedForeground: \"#686C7A\",\n /** Chữ trắng trên nền này được 5,03:1 (bảng mặc định #D64545 chỉ 4,38:1) */\n destructive: \"#C93B3B\",\n /** = color-mix(destructive, ink 15%) — ĐẬM thêm khi hover, ngược chiều bảng mặc định. */\n destructiveHover: \"#AD3535\",\n /** 7,15:1 trên nền trắng (bảng mặc định dùng chính #D64545, 4,38:1) */\n destructiveInk: accentInk.light.red,\n /** 3,83:1 trên nền trắng (bảng mặc định #EDEEF1 chỉ 1,16:1) */\n input: \"#7E8290\",\n controlEdge: \"#7E8290\",\n controlEdgeStrong: ash[700],\n /** Rãnh ⇄ nền 6,51:1 và rãnh ⇄ núm 6,51:1 (bảng mặc định: 1,86 / 1,86) */\n controlOn: gold[500],\n /** = `background`, nên luôn tương phản đúng chiều với cả rãnh BẬT lẫn rãnh TẮT. */\n controlKnob: \"#FFFFFF\",\n controlCheck: \"#FFFFFF\",\n switchTrack: \"#7E8290\",\n sliderTrack: \"#7E8290\",\n primaryInk: gold[500],\n linkInk: gold[500],\n linkInkHover: gold[600],\n /** = `--orange-ink` của khối này — bậc ink màu phụ DUY NHẤT mà bảng này đổi ở light:\n * color-mix(srgb, orange 65%, ink 35%), 6,66:1 trắng · 5,55:1 trên orange/15 (bảng mặc định\n * #AC440F: 5,85 / 4,87:1). */\n severeInk: \"#9D3F10\",\n accent: accent.light\n },\n dark: {\n /** 5,70:1 trên `background` · 5,35:1 trên `card` · 4,86:1 trên `muted` */\n mutedForeground: \"#888D9B\",\n destructive: \"#C93B3B\",\n destructiveHover: \"#AD3535\",\n destructiveInk: \"#F87171\",\n /** Ghim một giá trị cho cả hai theme — 4,94:1 trên `background` */\n input: \"#7E8290\",\n controlEdge: \"#7E8290\",\n controlEdgeStrong: ash[300],\n /** 12,67:1 với nền trang VÀ với `controlKnob` (#0E1116) */\n controlOn: \"#F5D060\",\n controlKnob: \"#0E1116\",\n controlCheck: \"#0E1116\",\n switchTrack: \"#7E8290\",\n sliderTrack: \"#7E8290\",\n primaryInk: \"#F5D060\",\n linkInk: gold[200],\n linkInkHover: gold[100],\n /** Bằng bảng mặc định (8,36:1 trên `background` · 6,09:1 trên orange/15) — khai cho đủ cặp\n * với `light`, vì CSS phải huỷ bậc tối của khối light ở đây. */\n severeInk: accent.dark.orange,\n /** Màu phụ bản SÁNG cho nền tối — bảng mặc định không có bản dark cho nhóm này (trừ `orange`,\n * vốn đã sáng ở `accent.dark`). */\n accent: {\n navy: \"#0F1E3D\",\n teal: \"#2DD4BF\",\n red: \"#F87171\",\n green: \"#4ADE80\",\n yellow: \"#FACC15\",\n orange: accent.dark.orange\n }\n }\n} as const;\n\n/**\n * Alias trạng thái — success/warning/severe/info trỏ vào màu phụ, destructive giữ nguyên 2 chế độ.\n * `severe` (cam) là nấc giữa warning và destructive: ưu tiên cao, SLA sắp trễ.\n *\n * Ba lớp KHÁC nhau, đừng gộp:\n * • `destructive` — màu NỀN (nút xoá): #D64545 ở bảng mặc định (chữ trắng 4,38:1, dưới AA và đã\n * chấp nhận), #C93B3B ở bảng tương phản cao (5,03:1 ✓);\n * • `destructiveForeground` (#FFFFFF) — chữ NẰM TRÊN nền đó;\n * • `destructiveInk` — màu CHỮ đỏ trên nền trang (dòng lỗi). Bảng mặc định cho nó cùng hex với\n * nền (đúng `text-destructive` của comitor-ds); bảng tương phản cao tách thành bậc tối riêng.\n *\n * `successForeground` / `warningForeground` / `infoForeground` là MỰC ở CẢ hai theme (không phải\n * trắng ở light rồi lật ở dark như bản 0.1.0): ba nền trạng thái đều là sắc sáng.\n * success mực 6,04:1 (light) · 10,85:1 (dark) [trắng chỉ 3,13 / 1,74:1 ✗]\n * info mực 4,56:1 (light) · 10,16:1 (dark) [trắng chỉ 4,14 / 1,86:1 ✗]\n * warning mực 11,92:1 (light) · 12,35:1 (dark) [trắng chỉ 1,59 / 1,53:1 ✗]\n * severe mực 5,31:1 (light) · 8,36:1 (dark) [trắng chỉ 3,56 / 2,26:1 ✗]\n * ⚠ `info` ở light là cặp chặt nhất gói (4,56:1, dư 0,06) — đổi `accent.light.teal` là phải đo lại.\n */\nexport const status = {\n light: {\n success: accent.light.green,\n successForeground: \"#0E1116\",\n warning: accent.light.yellow,\n warningForeground: \"#0E1116\",\n severe: accent.light.orange,\n severeForeground: \"#0E1116\",\n info: accent.light.teal,\n infoForeground: \"#0E1116\",\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n successInk: accentInk.light.green,\n warningInk: accentInk.light.yellow,\n severeInk: accentInk.light.orange,\n infoInk: accentInk.light.teal,\n destructiveInk: accentInk.light.red\n },\n dark: {\n success: accent.dark.green,\n successForeground: \"#0E1116\",\n warning: accent.dark.yellow,\n warningForeground: \"#0E1116\",\n severe: accent.dark.orange,\n severeForeground: \"#0E1116\",\n info: accent.dark.teal,\n infoForeground: \"#0E1116\",\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n successInk: accentInk.dark.green,\n warningInk: accentInk.dark.yellow,\n severeInk: accentInk.dark.orange,\n infoInk: accentInk.dark.teal,\n destructiveInk: accentInk.dark.red\n }\n} as const;\n\n/** Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định. */\nexport const chart = {\n light: [gold[300], accent.light.teal, accent.light.navy, accent.light.green, accent.light.red],\n dark: [gold[200], accent.dark.teal, accentInk.dark.navy, accent.dark.green, accent.dark.red]\n} as const;\n\n/**\n * Màu avatar chữ cái (hex đục) cho nơi KHÔNG có Tailwind: canvas, email HTML, PDF.\n *\n * ⚠ KHÔNG phải bảng màu thứ hai. Bảng tone DUY NHẤT của cả gói nằm ở `lib/avatar-tone.ts`, mỗi\n * tone mang cả chuỗi class (cho DOM) lẫn công thức hex tương ứng; ở đây chỉ nạp bảng màu vào công\n * thức đó. Web và email vì vậy dùng chung một thứ tự tone, một hàm băm, một cách chuẩn hoá tên —\n * không còn hai bảng để lệch nhau.\n */\nconst avatarTonePalette = {\n light: {\n gold,\n ash,\n brandNeutral,\n accent: accent.light,\n accentInk: accentInk.light,\n card: semantic.light.card\n },\n dark: {\n gold,\n ash,\n brandNeutral,\n accent: accent.dark,\n accentInk: accentInk.dark,\n card: semantic.dark.card\n }\n} satisfies Record<ThemeMode, AvatarTonePalette>;\n\n/**\n * Cặp nền/chữ (hex) của avatar cho một tên, theo theme.\n *\n * Tên hàm nói đúng thứ nó trả về: `...Colors` = hex (canvas/email/PDF), còn bản trả về chuỗi\n * class Tailwind cho DOM là `getAvatarToneClasses` ở `@comitor/ui`. Cùng một tên người luôn ra\n * cùng một tone ở cả hai đường vì chỉ số do MỘT hàm tính (`getAvatarToneIndex`).\n */\nexport function getAvatarToneColors(name: string, mode: ThemeMode = \"light\"): AvatarToneColors {\n // `noUncheckedIndexedAccess` bật → phải có nhánh lùi, dù chỉ số luôn nằm trong [0, 8).\n const tone = AVATAR_TONES[getAvatarToneIndex(name)] ?? AVATAR_TONES[0];\n return tone[mode](avatarTonePalette[mode]);\n}\n\nexport type { AvatarToneColors };\nexport { getAvatarToneIndex };\n\n/** Số đo khung ứng dụng — mirror --sidebar-width / --sidebar-width-icon / --header-height. */\nexport const layout = {\n /** px — --header-height 3.5rem */\n headerHeight: 56,\n /** px — --sidebar-width 16rem */\n sidebarWidth: 256,\n /** px — --sidebar-width-icon 3.5rem */\n sidebarCollapsedWidth: 56,\n /** px — --radius 0.375rem */\n radius: 6,\n breakpoints: { mobile: 768, tablet: 1024, desktop: 1280 },\n /** px — kích thước tối thiểu của vùng chạm trên mobile (WCAG 2.5.5) */\n minTapTarget: 44\n} as const;\n\n/** Font stack — app phải tự nạp Inter (subsets latin + vietnamese) và gán --font-inter. */\nexport const typography = {\n fontSans: '\"Inter\", system-ui, sans-serif',\n fontMono: '\"Geist Mono\", ui-monospace, monospace'\n} as const;\n\nexport type ThemeMode = \"light\" | \"dark\";\n"],"mappings":"AAqBA,SAAS,cAA6D,0BAA0B;AAGzF,MAAM,OAAO;AAAA,EAClB,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,KAAK;AAAA;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAGO,MAAM,eAAe;AAAA,EAC1B,KAAK;AAAA,EACL,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AACR;AAOO,MAAM,MAAM;AAAA,EACjB,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAUO,MAAM,SAAS;AAAA,EACpB,OAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO;AAAA,IACP,QAAQ;AAAA;AAAA;AAAA,IAGR,QAAQ;AAAA,EACV;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AACF;AAkBO,MAAM,YAAY;AAAA,EACvB,OAAO;AAAA,IACL,MAAM,OAAO,MAAM;AAAA,IACnB,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO;AAAA;AAAA,IAEP,QAAQ,KAAK,GAAG;AAAA;AAAA;AAAA,IAGhB,QAAQ;AAAA,EACV;AAAA,EACA,MAAM;AAAA;AAAA,IAEJ,MAAM;AAAA,IACN,MAAM,OAAO,KAAK;AAAA,IAClB,KAAK,OAAO,KAAK;AAAA,IACjB,OAAO,OAAO,KAAK;AAAA,IACnB,QAAQ,OAAO,KAAK;AAAA,IACpB,QAAQ,OAAO,KAAK;AAAA,EACtB;AACF;AAgBO,MAAM,mBAAmB;AAAA,EAC9B,OAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AACF;AAGO,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,IACL,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,SAAS;AAAA,IACT,mBAAmB;AAAA,IACnB,SAAS;AAAA,IACT,mBAAmB;AAAA;AAAA;AAAA;AAAA,IAInB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,qBAAqB;AAAA,IACrB,OAAO;AAAA;AAAA;AAAA;AAAA,IAIP,iBAAiB,IAAI,GAAG;AAAA,IACxB,QAAQ;AAAA,IACR,kBAAkB;AAAA;AAAA,IAElB,aAAa;AAAA,IACb,uBAAuB;AAAA;AAAA;AAAA,IAGvB,kBAAkB;AAAA;AAAA;AAAA,IAGlB,gBAAgB;AAAA;AAAA;AAAA,IAGhB,QAAQ;AAAA;AAAA,IAER,OAAO;AAAA,IACP,MAAM;AAAA;AAAA,IAEN,aAAa;AAAA;AAAA,IAEb,mBAAmB,IAAI,GAAG;AAAA;AAAA;AAAA,IAG1B,WAAW;AAAA;AAAA,IAEX,aAAa;AAAA;AAAA;AAAA;AAAA,IAIb,cAAc,aAAa;AAAA;AAAA,IAE3B,aAAa,IAAI,GAAG;AAAA;AAAA,IAEpB,aAAa;AAAA;AAAA,IAEb,SAAS,KAAK,GAAG;AAAA,IACjB,cAAc,KAAK,GAAG;AAAA,IACtB,SAAS;AAAA,IACT,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,eAAe;AAAA,IACf,yBAAyB;AAAA,EAC3B;AAAA,EACA,MAAM;AAAA,IACJ,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,SAAS;AAAA,IACT,mBAAmB;AAAA,IACnB,SAAS;AAAA,IACT,mBAAmB;AAAA;AAAA,IAEnB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,qBAAqB;AAAA,IACrB,OAAO;AAAA;AAAA;AAAA,IAGP,iBAAiB,IAAI,GAAG;AAAA,IACxB,QAAQ;AAAA,IACR,kBAAkB;AAAA;AAAA,IAElB,aAAa;AAAA,IACb,uBAAuB;AAAA,IACvB,kBAAkB;AAAA;AAAA,IAElB,gBAAgB;AAAA;AAAA,IAEhB,QAAQ;AAAA;AAAA,IAER,OAAO;AAAA,IACP,MAAM;AAAA,IACN,aAAa;AAAA;AAAA,IAEb,mBAAmB,IAAI,GAAG;AAAA;AAAA;AAAA,IAG1B,WAAW;AAAA;AAAA,IAEX,aAAa;AAAA;AAAA,IAEb,cAAc,aAAa;AAAA,IAC3B,aAAa,IAAI,GAAG;AAAA,IACpB,aAAa;AAAA,IACb,SAAS,KAAK,GAAG;AAAA,IACjB,cAAc,KAAK,GAAG;AAAA,IACtB,SAAS;AAAA,IACT,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,eAAe;AAAA,IACf,yBAAyB;AAAA,EAC3B;AACF;AAgBO,MAAM,eAAe;AAAA,EAC1B,OAAO;AAAA;AAAA,IAEL,iBAAiB;AAAA;AAAA,IAEjB,aAAa;AAAA;AAAA,IAEb,kBAAkB;AAAA;AAAA,IAElB,gBAAgB,UAAU,MAAM;AAAA;AAAA,IAEhC,OAAO;AAAA,IACP,aAAa;AAAA,IACb,mBAAmB,IAAI,GAAG;AAAA;AAAA,IAE1B,WAAW,KAAK,GAAG;AAAA;AAAA,IAEnB,aAAa;AAAA,IACb,cAAc;AAAA,IACd,aAAa;AAAA,IACb,aAAa;AAAA,IACb,YAAY,KAAK,GAAG;AAAA,IACpB,SAAS,KAAK,GAAG;AAAA,IACjB,cAAc,KAAK,GAAG;AAAA;AAAA;AAAA;AAAA,IAItB,WAAW;AAAA,IACX,QAAQ,OAAO;AAAA,EACjB;AAAA,EACA,MAAM;AAAA;AAAA,IAEJ,iBAAiB;AAAA,IACjB,aAAa;AAAA,IACb,kBAAkB;AAAA,IAClB,gBAAgB;AAAA;AAAA,IAEhB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,mBAAmB,IAAI,GAAG;AAAA;AAAA,IAE1B,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,aAAa;AAAA,IACb,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,SAAS,KAAK,GAAG;AAAA,IACjB,cAAc,KAAK,GAAG;AAAA;AAAA;AAAA,IAGtB,WAAW,OAAO,KAAK;AAAA;AAAA;AAAA,IAGvB,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,KAAK;AAAA,MACL,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,QAAQ,OAAO,KAAK;AAAA,IACtB;AAAA,EACF;AACF;AAqBO,MAAM,SAAS;AAAA,EACpB,OAAO;AAAA,IACL,SAAS,OAAO,MAAM;AAAA,IACtB,mBAAmB;AAAA,IACnB,SAAS,OAAO,MAAM;AAAA,IACtB,mBAAmB;AAAA,IACnB,QAAQ,OAAO,MAAM;AAAA,IACrB,kBAAkB;AAAA,IAClB,MAAM,OAAO,MAAM;AAAA,IACnB,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,uBAAuB;AAAA,IACvB,YAAY,UAAU,MAAM;AAAA,IAC5B,YAAY,UAAU,MAAM;AAAA,IAC5B,WAAW,UAAU,MAAM;AAAA,IAC3B,SAAS,UAAU,MAAM;AAAA,IACzB,gBAAgB,UAAU,MAAM;AAAA,EAClC;AAAA,EACA,MAAM;AAAA,IACJ,SAAS,OAAO,KAAK;AAAA,IACrB,mBAAmB;AAAA,IACnB,SAAS,OAAO,KAAK;AAAA,IACrB,mBAAmB;AAAA,IACnB,QAAQ,OAAO,KAAK;AAAA,IACpB,kBAAkB;AAAA,IAClB,MAAM,OAAO,KAAK;AAAA,IAClB,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,uBAAuB;AAAA,IACvB,YAAY,UAAU,KAAK;AAAA,IAC3B,YAAY,UAAU,KAAK;AAAA,IAC3B,WAAW,UAAU,KAAK;AAAA,IAC1B,SAAS,UAAU,KAAK;AAAA,IACxB,gBAAgB,UAAU,KAAK;AAAA,EACjC;AACF;AAGO,MAAM,QAAQ;AAAA,EACnB,OAAO,CAAC,KAAK,GAAG,GAAG,OAAO,MAAM,MAAM,OAAO,MAAM,MAAM,OAAO,MAAM,OAAO,OAAO,MAAM,GAAG;AAAA,EAC7F,MAAM,CAAC,KAAK,GAAG,GAAG,OAAO,KAAK,MAAM,UAAU,KAAK,MAAM,OAAO,KAAK,OAAO,OAAO,KAAK,GAAG;AAC7F;AAUA,MAAM,oBAAoB;AAAA,EACxB,OAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,WAAW,UAAU;AAAA,IACrB,MAAM,SAAS,MAAM;AAAA,EACvB;AAAA,EACA,MAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,WAAW,UAAU;AAAA,IACrB,MAAM,SAAS,KAAK;AAAA,EACtB;AACF;AASO,SAAS,oBAAoB,MAAc,OAAkB,SAA2B;AAE7F,QAAM,OAAO,aAAa,mBAAmB,IAAI,CAAC,KAAK,aAAa,CAAC;AACrE,SAAO,KAAK,IAAI,EAAE,kBAAkB,IAAI,CAAC;AAC3C;AAMO,MAAM,SAAS;AAAA;AAAA,EAEpB,cAAc;AAAA;AAAA,EAEd,cAAc;AAAA;AAAA,EAEd,uBAAuB;AAAA;AAAA,EAEvB,QAAQ;AAAA,EACR,aAAa,EAAE,QAAQ,KAAK,QAAQ,MAAM,SAAS,KAAK;AAAA;AAAA,EAExD,cAAc;AAChB;AAGO,MAAM,aAAa;AAAA,EACxB,UAAU;AAAA,EACV,UAAU;AACZ;","names":[]}
1
+ {"version":3,"sources":["../src/tokens.ts"],"sourcesContent":["/**\n * Design token dạng TS object — dành cho nơi KHÔNG có Tailwind: canvas, biểu đồ tuỳ biến,\n * email HTML, xuất PDF, (sau này) React Native.\n *\n * ⚠ Nguồn sự thật của giá trị là `styles.css` (CSS variable). File này chỉ MIRROR hằng số.\n * Sửa màu ở styles.css thì phải sửa kèm ở đây — và ngược lại.\n *\n * Trong môi trường có Tailwind, LUÔN dùng class token (`bg-primary`, `text-ash-500`),\n * đừng đọc hex từ đây rồi nhét vào style inline: làm vậy là mất dark mode.\n *\n * Hợp đồng ba vai trò của màu (giống hệt `styles.css`; bảng đầy đủ ở docs/hop-dong-mau.md),\n * đọc tên là biết dùng ở đâu:\n * `x` → tô NỀN (fill đặc hoặc tint /5–/20 — trần chung an toàn cho mọi tone)\n * `xForeground` → chữ/icon NẰM TRÊN nền `x` ĐẶC — đo với `x`, ngưỡng 4,5:1\n * `xInk` → chính màu đó khi ĐI LÊN nền trang hoặc lên tint của chính nó (chữ, icon\n * rời, viền, vạch active) — đo với nền đó\n *\n * Cố ý KHÔNG mirror (xem docs/quyet-dinh-tich-hop.md §21): `--elevation-*` (chuỗi box-shadow chỉ\n * có nghĩa trong CSS), `--app-accent*` (giá trị thuộc về APP, không phải gói), `--font-*`.\n */\n\nimport { AVATAR_TONES, type AvatarToneColors, type AvatarTonePalette, getAvatarToneIndex } from \"./lib/avatar-tone.js\";\n\n/** Thang GOLD 7 bậc — trục màu chính của brand Comitor. */\nexport const gold = {\n 50: \"#FBF5DC\",\n 100: \"#F8E89D\",\n 200: \"#F5D060\",\n /** primary ở light mode */\n 300: \"#E8B824\",\n 400: \"#B88A00\",\n 500: \"#7A5800\",\n 600: \"#5C4006\"\n} as const;\n\n/** Trung tính brand (khác thang ash — đây là màu \"giấy/mực\" của brand book). */\nexport const brandNeutral = {\n ink: \"#0E1116\",\n inkSoft: \"#3A3F4A\",\n paper: \"#FBF7EE\",\n warm: \"#EDE8D8\"\n} as const;\n\n/**\n * Thang xám Comitor — namespace `ash`, KHÔNG phải `gray`.\n * Chỉ có 6 bậc; các bậc khuyết (200/400/600/800) cố ý không khai để không lẫn với\n * thang gray dựng sẵn của Tailwind (xem chú thích trong styles.css).\n */\nexport const ash = {\n 50: \"#F7F7F8\",\n 100: \"#EDEEF1\",\n 300: \"#C8CBD3\",\n 500: \"#7E8290\",\n 700: \"#3A3F4A\",\n 900: \"#16181D\"\n} as const;\n\n/**\n * Màu phụ — vai trò TÔ NỀN (fill đặc hoặc tint). Bản `dark` được nâng sáng vì trên nền #0E1116\n * bản `light` chìm nghỉm.\n *\n * ⚠ ĐỪNG dùng bảng này làm màu CHỮ ở light: nó được chọn để tô nền nên quá nhạt — dùng\n * `accentInk` ngay dưới. `accent.dark.navy` giữ nguyên #0F1E3D chỉ để mirror đúng `styles.css`\n * (khối `.dark` không override `--navy`); vai trò navy ở dark do `accentInk.dark.navy` gánh.\n */\nexport const accent = {\n light: {\n navy: \"#0F1E3D\",\n teal: \"#1F8A8B\",\n red: \"#D64545\",\n green: \"#2EA56A\",\n yellow: \"#F2C94C\",\n /** Gốc của tone `severe`. Giữ 3:1 trên mọi nền trang (3,56:1 trắng · 3,32:1 `muted`) — vì nó\n * TÔ chấm của `StatusPill variant=\"dot\"`; bậc sáng hơn (#F97316) chỉ 2,80:1. */\n orange: \"#EA580C\"\n },\n /** Bảng MẶC ĐỊNH (comitor-ds) không khai lại màu phụ ở dark — cùng hex với light. Bản sáng\n * dành cho nền tối nằm ở `highContrast.dark`. Trừ `orange`: comitor-ds không có cam, và cùng\n * hex thì chữ cam trên tint /15 của chính nó ở dark chỉ 4,21:1 — nên bảng mặc định có bản\n * dark riêng. */\n dark: {\n navy: \"#0F1E3D\",\n teal: \"#1F8A8B\",\n red: \"#D64545\",\n green: \"#2EA56A\",\n yellow: \"#F2C94C\",\n orange: \"#FB923C\"\n }\n} as const;\n\n/**\n * Bậc INK của màu phụ — mirror `--teal-ink` / `--green-ink` / `--red-ink` / `--yellow-ink` /\n * `--orange-ink` / `--navy-ink`. Đây là màu phụ khi nó làm **CHỮ/ICON/VIỀN trên nền trang**\n * (hoặc trên tint /5–/20 của chính nó), tức vai trò NGƯỢC với `accent`.\n *\n * Hợp đồng đặt tên của cả gói: `--x` tô nền · `--x-foreground` là chữ NẰM TRÊN nền `--x` ·\n * `--x-ink` là chính màu đó ĐI LÊN nền trang.\n *\n * Bản `light` là bậc tối đã đóng băng (kết quả của `color-mix(in srgb, --<màu>, --ink 30/40%)`\n * mà trước đây mỗi component tự tính lấy); bản `dark` chính là `accent.dark`. Đo trên tint 15%\n * của chính nó — nền thật mà chữ initials avatar và badge tonal đứng lên: teal 5,54 · green 5,60\n * · red 5,83:1 ở light (bản chưa có ink: 3,44 / 2,69 / 3,57:1 ✗).\n *\n * ⚠ Ở bảng MẶC ĐỊNH, `accent.dark` bằng hex của light nên bậc ink ở dark cũng vậy — đúng thứ\n * comitor-ds render. Bậc sáng cho nền tối nằm ở `highContrast.dark`.\n */\nexport const accentInk = {\n light: {\n navy: accent.light.navy,\n teal: \"#1A6668\",\n red: \"#9A3537\",\n green: \"#216A48\",\n /** = gold[500]: vàng không có bậc tối riêng, mượn thang gold cùng sắc. */\n yellow: gold[500],\n /** = color-mix(srgb, orange 72%, ink 28%) — bậc SÁNG nhất còn giữ trần tint /20: 5,85:1 trắng ·\n * 4,87:1 trên orange/15 · 4,58:1 trên orange/20. Bảng tương phản cao tối hơn: `highContrast`. */\n orange: \"#AC440F\"\n },\n dark: {\n /** navy nguyên bản chìm vào nền dark (1,15:1) → bậc sáng riêng; cũng là `--chart-3` ở dark. */\n navy: \"#7EA2E8\",\n teal: accent.dark.teal,\n red: accent.dark.red,\n green: accent.dark.green,\n yellow: accent.dark.yellow,\n orange: accent.dark.orange\n }\n} as const;\n\n/**\n * Bậc FOREGROUND của màu phụ — mirror `--teal-foreground` / `--green-foreground` /\n * `--yellow-foreground` / `--orange-foreground` / `--navy-foreground`. Đây là chữ/icon nằm\n * **TRÊN nền `accent.*` ĐẶC**, tức vai trò thứ ba, khác hẳn `accentInk` (màu đó đi lên nền TRANG).\n *\n * Cả bốn sắc teal/green/yellow/orange đều sáng ở light và còn sáng hơn ở dark → chữ là MỰC ở cả\n * hai theme, một giá trị đủ dùng: teal 4,56:1 (light) / 10,16:1 (dark) · green 6,04 / 10,85 ·\n * yellow 11,92 / 12,35 · orange 5,31 / 8,36. Navy thì ngược lại — chữ TRẮNG, 16,49:1 ở cả hai theme.\n *\n * ⚠ CỐ Ý KHÔNG có `red`: nền đỏ đặc không mang được chữ tốt ở light — trắng trên #D64545 được\n * 4,38:1 và mực 4,32:1, cả hai đều dưới AA. Nền đỏ đặc có chữ phải dùng `status.*.destructive`;\n * ở bảng tương phản cao token đó là #C93B3B và đạt 5,03:1. `accent.*.red` chỉ để tô TINT và làm\n * màu chart. (Cam thì mực cứu được — 5,31:1 — nên `orange` có mặt.)\n */\nexport const accentForeground = {\n light: {\n navy: \"#FFFFFF\",\n teal: \"#0E1116\",\n green: \"#0E1116\",\n yellow: \"#0E1116\",\n orange: \"#0E1116\"\n },\n dark: {\n navy: \"#FFFFFF\",\n teal: \"#0E1116\",\n green: \"#0E1116\",\n yellow: \"#0E1116\",\n orange: \"#0E1116\"\n }\n} as const;\n\n/** Semantic token của shadcn — mirror khối `:root` / `.dark` trong styles.css. */\nexport const semantic = {\n light: {\n background: \"#FFFFFF\",\n foreground: \"#0E1116\",\n card: \"#FFFFFF\",\n cardForeground: \"#0E1116\",\n popover: \"#FFFFFF\",\n popoverForeground: \"#0E1116\",\n primary: \"#E8B824\",\n primaryForeground: \"#0E1116\",\n /** Bậc INK của brand. Ở bảng MẶC ĐỊNH nó BẰNG `primary`: comitor-ds tô gold cho cả những vai\n * trò mà 1.4.11 đòi 3:1 (rãnh Switch BẬT, gạch active của Tabs) — 1,86:1, đã biết và chấp\n * nhận. Bậc tối #7A5800 nằm ở `highContrast.light.primaryInk`. */\n primaryInk: \"#E8B824\",\n secondary: \"#EDE8D8\",\n secondaryForeground: \"#0E1116\",\n muted: \"#F7F7F8\",\n /** Bậc chữ phụ — giá trị của comitor-ds, bằng đúng `ash[500]`. 3,83:1 trên nền trắng và\n * 3,58:1 trên `muted`, tức dưới AA 4,5:1: đã biết và chấp nhận, bản đạt chuẩn nằm ở\n * `highContrast.light.mutedForeground`. */\n mutedForeground: ash[500],\n accent: \"#FBF5DC\",\n accentForeground: \"#0E1116\",\n /** Nền nút xoá. Chữ trắng trên nó được 4,38:1 — thiếu 0,12 so với AA, đã biết và chấp nhận. */\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n /** Nền nút xoá lúc HOVER = `destructive` ở 90% trên nền trắng, đúng thứ `hover:bg-red/90`\n * của comitor-ds render. Ghim hex vì `/90` phụ thuộc nền phía sau. */\n destructiveHover: \"#DA5A5A\",\n /** Chữ báo lỗi trên nền trang. comitor-ds viết `text-destructive` cho vai trò này, tức cùng\n * hex với nền — 4,38:1 trên nền trắng. */\n destructiveInk: \"#D64545\",\n /** RANH GIỚI TRANG TRÍ (vạch chia, viền thẻ nghỉ) — 1,16:1, cố ý: 1.4.11 không đòi tương\n * phản cho thứ không mang thông tin. Ranh giới của CONTROL đi qua `controlEdge`. */\n border: \"#EDEEF1\",\n /** Viền ô nhập — comitor-ds cho nó cùng bậc với `border`. 1,16:1, đã biết và chấp nhận. */\n input: \"#EDEEF1\",\n ring: \"#E8B824\",\n /** Ranh giới của control lúc NGHỈ — ở bảng mặc định bằng đúng `input`. */\n controlEdge: \"#EDEEF1\",\n /** Cùng vai trò nhưng ở HOVER/ACTIVE. comitor-ds dùng `hover:border-gray-300` = `ash[300]`. */\n controlEdgeStrong: ash[300],\n /** Nền control khi BẬT/ĐÃ CHỌN (rãnh Switch bật, ô Checkbox đã chọn, chấm Radio). comitor-ds\n * tô gold: 1,86:1 với nền trang và với núm — đã biết và chấp nhận. */\n controlOn: \"#E8B824\",\n /** Núm Switch / chấm Radio — TRẮNG ở cả hai theme, đúng `bg-white` của comitor-ds. */\n controlKnob: \"#FFFFFF\",\n /** DẤU CHECK của Checkbox và chữ trên ngày ĐANG CHỌN của Calendar — tách khỏi `controlKnob`\n * vì trên nền gold SÁNG thì dấu phải là MỰC (10,19:1), còn ở bảng tương phản cao nền là\n * gold-500 TỐI nên dấu lật thành màu nền trang. */\n controlCheck: brandNeutral.ink,\n /** Rãnh Switch khi TẮT — comitor-ds dùng `bg-gray-300` / `dark:bg-gray-700`. */\n switchTrack: ash[300],\n /** Rãnh Slider — comitor-ds dùng `bg-muted`, khác bậc với rãnh Switch. */\n sliderTrack: \"#F7F7F8\",\n /** Button variant `link`. comitor-ds dùng `gold[400]` — 3,15:1, dưới AA, đã biết và chấp nhận. */\n linkInk: gold[400],\n linkInkHover: gold[300],\n sidebar: \"#FAFAF9\",\n sidebarForeground: \"#0E1116\",\n sidebarBorder: \"#EDEEF1\",\n sidebarAccent: \"#FBF5DC\",\n sidebarAccentForeground: \"#0E1116\"\n },\n dark: {\n background: \"#0E1116\",\n foreground: \"#F7F7F8\",\n card: \"#16181D\",\n cardForeground: \"#F7F7F8\",\n popover: \"#16181D\",\n popoverForeground: \"#F7F7F8\",\n primary: \"#F5D060\",\n primaryForeground: \"#0E1116\",\n /** Ở dark `primary` vốn đã là bậc sáng (12,67:1 trên nền `background`) nên ink = chính nó. */\n primaryInk: \"#F5D060\",\n secondary: \"#1E2128\",\n secondaryForeground: \"#F7F7F8\",\n muted: \"#1E2128\",\n /** comitor-ds không khai lại token này ở dark — cùng hex với light. 4,21:1 trên `muted`,\n * dưới AA: đã biết và chấp nhận. */\n mutedForeground: ash[500],\n accent: \"#1E2128\",\n accentForeground: \"#F7F7F8\",\n /** Ghim đúng giá trị của light — comitor-ds dùng chung một hex cho cả hai theme. */\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n destructiveHover: \"#DA5A5A\",\n /** 4,06:1 trên `card` — cùng hex với nền, đúng `text-destructive` của comitor-ds. */\n destructiveInk: \"#D64545\",\n /** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */\n border: \"#1E2128\",\n /** Viền ô nhập bản dark — KHÔNG phải #1E2128 của comitor-ds (chỉ 1,10:1 trên `card`, ô nhập tan\n * vào nền). #434A5A = 2,00:1, mirror `.dark { --input }` từ `0.5.0`; trước `3.3.3` mirror này còn\n * ghi #1E2128. */\n input: \"#434A5A\",\n ring: \"#F5D060\",\n /** = `input` (`--control-edge: var(--input)`). */\n controlEdge: \"#434A5A\",\n /** comitor-ds dùng `dark:hover:border-gray-500` = `ash[500]` (lật chiều so với light). */\n controlEdgeStrong: ash[500],\n /** comitor-ds tô `dark:data-[state=checked]:bg-gold-300`, nhưng token `primary` của chính nó\n * ở dark là #F5D060; gói theo token, nên control BẬT ở dark là bậc sáng. */\n controlOn: \"#F5D060\",\n /** TRẮNG ở cả hai theme, đúng `bg-white` của comitor-ds. */\n controlKnob: \"#FFFFFF\",\n /** comitor-ds vẽ dấu check bằng `text-ink` ở CẢ hai theme. */\n controlCheck: brandNeutral.ink,\n switchTrack: ash[700],\n sliderTrack: \"#1E2128\",\n linkInk: gold[400],\n linkInkHover: gold[300],\n sidebar: \"#16181D\",\n sidebarForeground: \"#F7F7F8\",\n sidebarBorder: \"#1E2128\",\n sidebarAccent: \"#1E2128\",\n sidebarAccentForeground: \"#F7F7F8\"\n }\n} as const;\n\n/**\n * BẢNG MÀU THỨ HAI — TƯƠNG PHẢN CAO. Mirror khối `[data-contrast=\"high\"]` /\n * `.dark[data-contrast=\"high\"]` trong styles.css.\n *\n * Chỉ chứa những token KHÁC bảng mặc định, đúng như CSS: hợp lên trên để có bảng đầy đủ.\n *\n * ```ts\n * const mau = { ...semantic.light, ...highContrast.light };\n * ```\n *\n * Bảng mặc định là comitor-ds (bản duyệt, có những cặp dưới ngưỡng — đã liệt kê ở từng token).\n * Bảng này là bảng đã đo, do NGƯỜI DÙNG bật qua `<ContrastProvider>` + `<ContrastToggle>`.\n * `accent` là màu phụ bản sáng dành cho nền tối — bảng mặc định không có, xem `accent.dark`.\n */\nexport const highContrast = {\n light: {\n /** 5,23:1 trên `background` · 4,89:1 trên `muted` (bảng mặc định: 3,83 / 3,58) */\n mutedForeground: \"#686C7A\",\n /** Chữ trắng trên nền này được 5,03:1 (bảng mặc định #D64545 chỉ 4,38:1) */\n destructive: \"#C93B3B\",\n /** = color-mix(destructive, ink 15%) — ĐẬM thêm khi hover, ngược chiều bảng mặc định. */\n destructiveHover: \"#AD3535\",\n /** 7,15:1 trên nền trắng (bảng mặc định dùng chính #D64545, 4,38:1) */\n destructiveInk: accentInk.light.red,\n /** 3,83:1 trên nền trắng (bảng mặc định #EDEEF1 chỉ 1,16:1) */\n input: \"#7E8290\",\n controlEdge: \"#7E8290\",\n controlEdgeStrong: ash[700],\n /** #B48700 = gold-400 tối đi 2%: 3,28:1 trên `background`/`card` · 3,06:1 trên `muted` · ⇄ núm\n * trắng 3,28:1 (bảng mặc định: 1,86 / 1,86). Trước `3.3.3` mirror này còn ghi gold-500 — giá trị\n * `styles.css` đã bỏ. */\n controlOn: \"#B48700\",\n /** = `background`, nên luôn tương phản đúng chiều với cả rãnh BẬT lẫn rãnh TẮT. */\n controlKnob: \"#FFFFFF\",\n /** MỰC trên nền gold (5,77:1) — cùng cách vẽ với bảng mặc định; trước `3.3.3` mirror này ghi trắng. */\n controlCheck: brandNeutral.ink,\n switchTrack: \"#7E8290\",\n sliderTrack: \"#7E8290\",\n primaryInk: gold[500],\n linkInk: gold[500],\n linkInkHover: gold[600],\n /** = `--orange-ink` của khối này — bậc ink màu phụ DUY NHẤT mà bảng này đổi ở light:\n * color-mix(srgb, orange 65%, ink 35%), 6,66:1 trắng · 5,55:1 trên orange/15 (bảng mặc định\n * #AC440F: 5,85 / 4,87:1). */\n severeInk: \"#9D3F10\",\n accent: accent.light\n },\n dark: {\n /** 5,70:1 trên `background` · 5,35:1 trên `card` · 4,86:1 trên `muted` */\n mutedForeground: \"#888D9B\",\n destructive: \"#C93B3B\",\n destructiveHover: \"#AD3535\",\n destructiveInk: \"#F87171\",\n /** Ghim một giá trị cho cả hai theme — 4,94:1 trên `background` */\n input: \"#7E8290\",\n controlEdge: \"#7E8290\",\n controlEdgeStrong: ash[300],\n /** 12,67:1 với nền trang VÀ với `controlKnob` (#0E1116) */\n controlOn: \"#F5D060\",\n controlKnob: \"#0E1116\",\n controlCheck: \"#0E1116\",\n switchTrack: \"#7E8290\",\n sliderTrack: \"#7E8290\",\n primaryInk: \"#F5D060\",\n linkInk: gold[200],\n linkInkHover: gold[100],\n /** Bằng bảng mặc định (8,36:1 trên `background` · 6,09:1 trên orange/15) — khai cho đủ cặp\n * với `light`, vì CSS phải huỷ bậc tối của khối light ở đây. */\n severeInk: accent.dark.orange,\n /** Màu phụ bản SÁNG cho nền tối — bảng mặc định không có bản dark cho nhóm này (trừ `orange`,\n * vốn đã sáng ở `accent.dark`). */\n accent: {\n navy: \"#0F1E3D\",\n teal: \"#2DD4BF\",\n red: \"#F87171\",\n green: \"#4ADE80\",\n yellow: \"#FACC15\",\n orange: accent.dark.orange\n }\n }\n} as const;\n\n/**\n * Alias trạng thái — success/warning/severe/info trỏ vào màu phụ, destructive giữ nguyên 2 chế độ.\n * `severe` (cam) là nấc giữa warning và destructive: ưu tiên cao, SLA sắp trễ.\n *\n * Ba lớp KHÁC nhau, đừng gộp:\n * • `destructive` — màu NỀN (nút xoá): #D64545 ở bảng mặc định (chữ trắng 4,38:1, dưới AA và đã\n * chấp nhận), #C93B3B ở bảng tương phản cao (5,03:1 ✓);\n * • `destructiveForeground` (#FFFFFF) — chữ NẰM TRÊN nền đó;\n * • `destructiveInk` — màu CHỮ đỏ trên nền trang (dòng lỗi). Bảng mặc định cho nó cùng hex với\n * nền (đúng `text-destructive` của comitor-ds); bảng tương phản cao tách thành bậc tối riêng.\n *\n * `successForeground` / `warningForeground` / `infoForeground` là MỰC ở CẢ hai theme (không phải\n * trắng ở light rồi lật ở dark như bản 0.1.0): ba nền trạng thái đều là sắc sáng.\n * success mực 6,04:1 (light) · 10,85:1 (dark) [trắng chỉ 3,13 / 1,74:1 ✗]\n * info mực 4,56:1 (light) · 10,16:1 (dark) [trắng chỉ 4,14 / 1,86:1 ✗]\n * warning mực 11,92:1 (light) · 12,35:1 (dark) [trắng chỉ 1,59 / 1,53:1 ✗]\n * severe mực 5,31:1 (light) · 8,36:1 (dark) [trắng chỉ 3,56 / 2,26:1 ✗]\n * ⚠ `info` ở light là cặp chặt nhất gói (4,56:1, dư 0,06) — đổi `accent.light.teal` là phải đo lại.\n */\nexport const status = {\n light: {\n success: accent.light.green,\n successForeground: \"#0E1116\",\n warning: accent.light.yellow,\n warningForeground: \"#0E1116\",\n severe: accent.light.orange,\n severeForeground: \"#0E1116\",\n info: accent.light.teal,\n infoForeground: \"#0E1116\",\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n successInk: accentInk.light.green,\n warningInk: accentInk.light.yellow,\n severeInk: accentInk.light.orange,\n infoInk: accentInk.light.teal,\n /** = `semantic.light.destructiveInk` (#D64545 — `--destructive-ink: var(--destructive)` ở bảng mặc\n * định). Trước `3.3.3` trỏ nhầm `accentInk.light.red` (#9A3537 — bậc của bảng TƯƠNG PHẢN CAO). */\n destructiveInk: semantic.light.destructiveInk\n },\n dark: {\n success: accent.dark.green,\n successForeground: \"#0E1116\",\n warning: accent.dark.yellow,\n warningForeground: \"#0E1116\",\n severe: accent.dark.orange,\n severeForeground: \"#0E1116\",\n info: accent.dark.teal,\n infoForeground: \"#0E1116\",\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n successInk: accentInk.dark.green,\n warningInk: accentInk.dark.yellow,\n severeInk: accentInk.dark.orange,\n infoInk: accentInk.dark.teal,\n destructiveInk: semantic.dark.destructiveInk\n }\n} as const;\n\n/**\n * Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định.\n *\n * ⚠ `chart-1` ở dark là `gold[300]` như light: `styles.css` KHÔNG khai lại `--chart-1` trong `.dark`\n * (chỉ `--chart-3` tự sáng lên qua `--navy-ink`). Trước `3.3.3` mirror này ghi `gold[200]` — biểu\n * đồ canvas/email/PDF dựng từ `tokens` ra màu khác biểu đồ trên trang.\n */\nexport const chart = {\n light: [gold[300], accent.light.teal, accent.light.navy, accent.light.green, accent.light.red],\n dark: [gold[300], accent.dark.teal, accentInk.dark.navy, accent.dark.green, accent.dark.red]\n} as const;\n\n/**\n * Màu avatar chữ cái (hex đục) cho nơi KHÔNG có Tailwind: canvas, email HTML, PDF.\n *\n * ⚠ KHÔNG phải bảng màu thứ hai. Bảng tone DUY NHẤT của cả gói nằm ở `lib/avatar-tone.ts`, mỗi\n * tone mang cả chuỗi class (cho DOM) lẫn công thức hex tương ứng; ở đây chỉ nạp bảng màu vào công\n * thức đó. Web và email vì vậy dùng chung một thứ tự tone, một hàm băm, một cách chuẩn hoá tên —\n * không còn hai bảng để lệch nhau.\n */\nconst avatarTonePalette = {\n light: {\n gold,\n ash,\n brandNeutral,\n accent: accent.light,\n accentInk: accentInk.light,\n card: semantic.light.card\n },\n dark: {\n gold,\n ash,\n brandNeutral,\n accent: accent.dark,\n accentInk: accentInk.dark,\n card: semantic.dark.card\n }\n} satisfies Record<ThemeMode, AvatarTonePalette>;\n\n/**\n * Cặp nền/chữ (hex) của avatar cho một tên, theo theme.\n *\n * Tên hàm nói đúng thứ nó trả về: `...Colors` = hex (canvas/email/PDF), còn bản trả về chuỗi\n * class Tailwind cho DOM là `getAvatarToneClasses` ở `@comitor/ui`. Cùng một tên người luôn ra\n * cùng một tone ở cả hai đường vì chỉ số do MỘT hàm tính (`getAvatarToneIndex`).\n */\nexport function getAvatarToneColors(name: string, mode: ThemeMode = \"light\"): AvatarToneColors {\n // `noUncheckedIndexedAccess` bật → phải có nhánh lùi, dù chỉ số luôn nằm trong [0, 8).\n const tone = AVATAR_TONES[getAvatarToneIndex(name)] ?? AVATAR_TONES[0];\n return tone[mode](avatarTonePalette[mode]);\n}\n\nexport type { AvatarToneColors };\nexport { getAvatarToneIndex };\n\n/** Số đo khung ứng dụng — mirror --sidebar-width / --sidebar-width-icon / --header-height. */\nexport const layout = {\n /** px — --header-height 3.5rem */\n headerHeight: 56,\n /** px — --sidebar-width 16rem */\n sidebarWidth: 256,\n /** px — --sidebar-width-icon 3.5rem */\n sidebarCollapsedWidth: 56,\n /** px — --radius 0.375rem */\n radius: 6,\n breakpoints: { mobile: 768, tablet: 1024, desktop: 1280 },\n /** px — kích thước tối thiểu của vùng chạm trên mobile (WCAG 2.5.5) */\n minTapTarget: 44\n} as const;\n\n/** Font stack — app phải tự nạp Inter (subsets latin + vietnamese) và gán --font-inter. */\nexport const typography = {\n fontSans: '\"Inter\", system-ui, sans-serif',\n fontMono: '\"Geist Mono\", ui-monospace, monospace'\n} as const;\n\nexport type ThemeMode = \"light\" | \"dark\";\n"],"mappings":"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","names":[]}
@@ -12,8 +12,8 @@ import { FileUploadResult } from './use-file-upload.js';
12
12
  * hơn chính thứ nó đang sửa, và nó đứng tách khỏi tấm ảnh nên người dùng phải tự ghép "hộp bên
13
13
  * phải này là để đổi ảnh bên trái".
14
14
  *
15
- * Ở đây bố cục là: **ảnh · nút · nút · một dòng chú thích** — đúng khuôn mà GitHub, Google, Slack
16
- * và Linear đều dùng cho avatar.
15
+ * Ở đây bố cục là: **ảnh · nút · nút · một dòng chú thích** — khuôn quen thuộc của ô ảnh đại diện,
16
+ * người dùng nhận ra ngay mà không phải đọc.
17
17
  *
18
18
  * ── KÉO-THẢ VẪN CÒN, NHƯNG VÔ HÌNH CHO TỚI KHI CẦN ─────────────────────────────────────────
19
19
  * Cả hàng là vùng nhận thả. Viền đứt nét CÓ MẶT ở mọi trạng thái nhưng `border-transparent` lúc
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/uploader/image-upload-field.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { Progress } from \"../components/ui/progress.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport type { ImageUploadFieldLabels } from \"./file-upload-constants.js\";\nimport { type FileUploadResult, useFileUpload } from \"./use-file-upload.js\";\n\n/**\n * Ô tải MỘT tấm ảnh gắn với một thực thể — ảnh đại diện người dùng, logo workspace, ảnh nhân sự.\n *\n * ── VÌ SAO KHÔNG DÙNG `FileUpload` CHO CA NÀY ────────────────────────────────────────────────\n * `FileUpload` vẽ một khung kéo-thả viền đứt nét chiếm cả bề rộng thẻ. Đó là bố cục ĐÚNG khi người\n * dùng sắp thả vào đó nhiều tài liệu, và là bố cục SAI cho một tấm ảnh vuông 56px: cái khung to\n * hơn chính thứ nó đang sửa, và nó đứng tách khỏi tấm ảnh nên người dùng phải tự ghép \"hộp bên\n * phải này là để đổi ảnh bên trái\".\n *\n * Ở đây bố cục là: **ảnh · nút · nút · một dòng chú thích** — đúng khuôn mà GitHub, Google, Slack\n * và Linear đều dùng cho avatar.\n *\n * ── KÉO-THẢ VẪN CÒN, NHƯNG VÔ HÌNH CHO TỚI KHI CẦN ─────────────────────────────────────────\n * Cả hàng là vùng nhận thả. Viền đứt nét CÓ MẶT ở mọi trạng thái nhưng `border-transparent` lúc\n * nghỉ — cùng độ dày, nên nó không chiếm một pixel nào và không có cú nhảy bố cục khi tệp được rê\n * qua. Đây là lý do `border-2` nằm ở lớp ngoài cùng chứ không được thêm vào lúc `dragging`.\n *\n * ⚠ Vì không có khung thường trực, `labels.hint` là chỗ DUY NHẤT người dùng học được rằng kéo-thả\n * chạy được. Rút gọn nó thành mỗi \"tối đa 2 MB\" là bỏ đi tính năng trong mắt người dùng.\n */\nexport interface ImageUploadFieldProps<T = unknown> {\n /** Đường dẫn endpoint nhận multipart, ví dụ `/api/account/avatar`. */\n endpoint: string;\n fieldName?: string;\n maxFileSize: number;\n allowedFileTypes: string[];\n /**\n * Ảnh xem trước — một `ReactNode`, KHÔNG phải một chuỗi `src`.\n *\n * Gói không được biết `LetterAvatar` rơi về initials kiểu gì, tone màu nào, tròn hay vuông. Nhận\n * cả node còn cho một cái nữa: ảnh bìa 16:9 cắm vừa cùng component này mà không thêm prop nào.\n */\n preview: ReactNode;\n /** Có ảnh thật hay không — quyết định nút gỡ ảnh (`labels.remove`) có bấm được không. */\n hasImage?: boolean;\n /**\n * `false` = chỉ hiện ảnh, không nút, không vùng thả.\n *\n * Dành cho người xem không có quyền sửa. Hiện một khung tải bị vô hiệu với họ là mời họ bấm vào\n * một thứ sẽ không bao giờ chạy.\n */\n editable?: boolean;\n /** Đang chạy một thao tác khác (ví dụ gỡ ảnh) — khoá cả hai nút. */\n busy?: boolean;\n onRemove?: () => void;\n onUploaded: (result: FileUploadResult<T>) => void;\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /**\n * Nhãn hiển thị. Không truyền thì theo namespace `imageUploadField` của `ComitorLocaleProvider`\n * (không có provider → tiếng Anh); truyền `Partial<>` để đổi từng chuỗi.\n */\n labels?: Partial<ImageUploadFieldLabels>;\n className?: string;\n}\n\nexport function ImageUploadField<T = unknown>({\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n preview,\n hasImage = false,\n editable = true,\n busy = false,\n onRemove,\n onUploaded,\n onError,\n labels,\n className\n}: ImageUploadFieldProps<T>) {\n const text = useComitorLabels(\"imageUploadField\", labels);\n\n const upload = useFileUpload<T>({\n endpoint,\n ...(fieldName ? { fieldName } : {}),\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles: 1,\n onUploaded,\n ...(onError ? { onError } : {}),\n labels: { uploadFailed: text.uploadFailed, missingDependency: text.missingDependency },\n disabled: !editable || busy\n });\n\n const blocked = !editable || busy || upload.busy;\n\n /*\n * Không sửa được thì chỉ còn tấm ảnh. Không vùng thả, không nút, không `<input>` — không có gì\n * để người dùng bấm nhầm rồi tự hỏi vì sao không có chuyện gì xảy ra.\n */\n if (!editable) return <div className={cn(\"flex items-center gap-4\", className)}>{preview}</div>;\n\n return (\n <div className={cn(\"flex flex-col gap-2\", className)}>\n <input {...upload.inputProps} className=\"sr-only\" />\n\n {/*\n ⚠ `border-2 border-dashed` có mặt ở CẢ HAI trạng thái; chỉ MÀU đổi. Thêm viền lúc `dragging`\n sẽ đẩy nội dung 2px và làm cả hàng giật đúng lúc người dùng đang nhắm chỗ thả.\n */}\n <div\n {...upload.dropProps}\n className={cn(\n \"relative flex flex-wrap items-center gap-4 rounded-lg border-2 border-dashed p-2 transition-colors\",\n upload.dragging ? \"border-primary-ink\" : \"border-transparent\"\n )}\n >\n {preview}\n\n <div className={cn(\"flex flex-wrap items-center gap-2\", blocked && \"opacity-60\")}>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" loading={upload.busy} onClick={upload.open}>\n {upload.busy ? text.uploading : text.choose}\n </Button>\n {/* Luôn render, chỉ `disabled` khi chưa có ảnh — bố cục không nhảy một nút. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" disabled={!hasImage || blocked} onClick={onRemove}>\n {text.remove}\n </Button>\n </div>\n\n {/*\n Chữ `dropPrompt` phủ lên cả khối khi đang rê tệp qua. `pointer-events-none` là BẮT\n BUỘC: thiếu nó, lớp phủ tự nuốt sự kiện `drop` của khối bên dưới và việc thả im lặng\n không làm gì.\n */}\n {upload.dragging ? (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center rounded-lg bg-card/90\">\n <span className=\"font-medium text-sm\">{text.dropPrompt}</span>\n </div>\n ) : null}\n </div>\n\n <p className=\"text-muted-foreground text-xs\">{text.hint}</p>\n\n {/* Thanh tiến độ chỉ hiện khi đang tải — một thanh 0% đứng yên trông như hỏng. */}\n {upload.busy ? <Progress value={upload.percent} aria-label={text.uploading} /> : null}\n </div>\n );\n}\n"],"mappings":";AAoGwB,cAmBhB,YAnBgB;AAjGxB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,wBAAwB;AACjC,SAAS,UAAU;AAEnB,SAAgC,qBAAqB;AAyD9C,SAAS,iBAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,OAAO,iBAAiB,oBAAoB,MAAM;AAExD,QAAM,SAAS,cAAiB;AAAA,IAC9B;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA,kBAAkB;AAAA,IAClB;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,QAAQ,EAAE,cAAc,KAAK,cAAc,mBAAmB,KAAK,kBAAkB;AAAA,IACrF,UAAU,CAAC,YAAY;AAAA,EACzB,CAAC;AAED,QAAM,UAAU,CAAC,YAAY,QAAQ,OAAO;AAM5C,MAAI,CAAC,SAAU,QAAO,oBAAC,SAAI,WAAW,GAAG,2BAA2B,SAAS,GAAI,mBAAQ;AAEzF,SACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,SAAS,GACjD;AAAA,wBAAC,WAAO,GAAG,OAAO,YAAY,WAAU,WAAU;AAAA,IAMlD;AAAA,MAAC;AAAA;AAAA,QACE,GAAG,OAAO;AAAA,QACX,WAAW;AAAA,UACT;AAAA,UACA,OAAO,WAAW,uBAAuB;AAAA,QAC3C;AAAA,QAEC;AAAA;AAAA,UAED,qBAAC,SAAI,WAAW,GAAG,qCAAqC,WAAW,YAAY,GAC7E;AAAA,gCAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,OAAO,MAAM,SAAS,OAAO,MACrF,iBAAO,OAAO,KAAK,YAAY,KAAK,QACvC;AAAA,YAEA,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,UAAU,CAAC,YAAY,SAAS,SAAS,UACtF,eAAK,QACR;AAAA,aACF;AAAA,UAOC,OAAO,WACN,oBAAC,SAAI,WAAU,+FACb,8BAAC,UAAK,WAAU,uBAAuB,eAAK,YAAW,GACzD,IACE;AAAA;AAAA;AAAA,IACN;AAAA,IAEA,oBAAC,OAAE,WAAU,iCAAiC,eAAK,MAAK;AAAA,IAGvD,OAAO,OAAO,oBAAC,YAAS,OAAO,OAAO,SAAS,cAAY,KAAK,WAAW,IAAK;AAAA,KACnF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/uploader/image-upload-field.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { Progress } from \"../components/ui/progress.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport type { ImageUploadFieldLabels } from \"./file-upload-constants.js\";\nimport { type FileUploadResult, useFileUpload } from \"./use-file-upload.js\";\n\n/**\n * Ô tải MỘT tấm ảnh gắn với một thực thể — ảnh đại diện người dùng, logo workspace, ảnh nhân sự.\n *\n * ── VÌ SAO KHÔNG DÙNG `FileUpload` CHO CA NÀY ────────────────────────────────────────────────\n * `FileUpload` vẽ một khung kéo-thả viền đứt nét chiếm cả bề rộng thẻ. Đó là bố cục ĐÚNG khi người\n * dùng sắp thả vào đó nhiều tài liệu, và là bố cục SAI cho một tấm ảnh vuông 56px: cái khung to\n * hơn chính thứ nó đang sửa, và nó đứng tách khỏi tấm ảnh nên người dùng phải tự ghép \"hộp bên\n * phải này là để đổi ảnh bên trái\".\n *\n * Ở đây bố cục là: **ảnh · nút · nút · một dòng chú thích** — khuôn quen thuộc của ô ảnh đại diện,\n * người dùng nhận ra ngay mà không phải đọc.\n *\n * ── KÉO-THẢ VẪN CÒN, NHƯNG VÔ HÌNH CHO TỚI KHI CẦN ─────────────────────────────────────────\n * Cả hàng là vùng nhận thả. Viền đứt nét CÓ MẶT ở mọi trạng thái nhưng `border-transparent` lúc\n * nghỉ — cùng độ dày, nên nó không chiếm một pixel nào và không có cú nhảy bố cục khi tệp được rê\n * qua. Đây là lý do `border-2` nằm ở lớp ngoài cùng chứ không được thêm vào lúc `dragging`.\n *\n * ⚠ Vì không có khung thường trực, `labels.hint` là chỗ DUY NHẤT người dùng học được rằng kéo-thả\n * chạy được. Rút gọn nó thành mỗi \"tối đa 2 MB\" là bỏ đi tính năng trong mắt người dùng.\n */\nexport interface ImageUploadFieldProps<T = unknown> {\n /** Đường dẫn endpoint nhận multipart, ví dụ `/api/account/avatar`. */\n endpoint: string;\n fieldName?: string;\n maxFileSize: number;\n allowedFileTypes: string[];\n /**\n * Ảnh xem trước — một `ReactNode`, KHÔNG phải một chuỗi `src`.\n *\n * Gói không được biết `LetterAvatar` rơi về initials kiểu gì, tone màu nào, tròn hay vuông. Nhận\n * cả node còn cho một cái nữa: ảnh bìa 16:9 cắm vừa cùng component này mà không thêm prop nào.\n */\n preview: ReactNode;\n /** Có ảnh thật hay không — quyết định nút gỡ ảnh (`labels.remove`) có bấm được không. */\n hasImage?: boolean;\n /**\n * `false` = chỉ hiện ảnh, không nút, không vùng thả.\n *\n * Dành cho người xem không có quyền sửa. Hiện một khung tải bị vô hiệu với họ là mời họ bấm vào\n * một thứ sẽ không bao giờ chạy.\n */\n editable?: boolean;\n /** Đang chạy một thao tác khác (ví dụ gỡ ảnh) — khoá cả hai nút. */\n busy?: boolean;\n onRemove?: () => void;\n onUploaded: (result: FileUploadResult<T>) => void;\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /**\n * Nhãn hiển thị. Không truyền thì theo namespace `imageUploadField` của `ComitorLocaleProvider`\n * (không có provider → tiếng Anh); truyền `Partial<>` để đổi từng chuỗi.\n */\n labels?: Partial<ImageUploadFieldLabels>;\n className?: string;\n}\n\nexport function ImageUploadField<T = unknown>({\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n preview,\n hasImage = false,\n editable = true,\n busy = false,\n onRemove,\n onUploaded,\n onError,\n labels,\n className\n}: ImageUploadFieldProps<T>) {\n const text = useComitorLabels(\"imageUploadField\", labels);\n\n const upload = useFileUpload<T>({\n endpoint,\n ...(fieldName ? { fieldName } : {}),\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles: 1,\n onUploaded,\n ...(onError ? { onError } : {}),\n labels: { uploadFailed: text.uploadFailed, missingDependency: text.missingDependency },\n disabled: !editable || busy\n });\n\n const blocked = !editable || busy || upload.busy;\n\n /*\n * Không sửa được thì chỉ còn tấm ảnh. Không vùng thả, không nút, không `<input>` — không có gì\n * để người dùng bấm nhầm rồi tự hỏi vì sao không có chuyện gì xảy ra.\n */\n if (!editable) return <div className={cn(\"flex items-center gap-4\", className)}>{preview}</div>;\n\n return (\n <div className={cn(\"flex flex-col gap-2\", className)}>\n <input {...upload.inputProps} className=\"sr-only\" />\n\n {/*\n ⚠ `border-2 border-dashed` có mặt ở CẢ HAI trạng thái; chỉ MÀU đổi. Thêm viền lúc `dragging`\n sẽ đẩy nội dung 2px và làm cả hàng giật đúng lúc người dùng đang nhắm chỗ thả.\n */}\n <div\n {...upload.dropProps}\n className={cn(\n \"relative flex flex-wrap items-center gap-4 rounded-lg border-2 border-dashed p-2 transition-colors\",\n upload.dragging ? \"border-primary-ink\" : \"border-transparent\"\n )}\n >\n {preview}\n\n <div className={cn(\"flex flex-wrap items-center gap-2\", blocked && \"opacity-60\")}>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" loading={upload.busy} onClick={upload.open}>\n {upload.busy ? text.uploading : text.choose}\n </Button>\n {/* Luôn render, chỉ `disabled` khi chưa có ảnh — bố cục không nhảy một nút. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" disabled={!hasImage || blocked} onClick={onRemove}>\n {text.remove}\n </Button>\n </div>\n\n {/*\n Chữ `dropPrompt` phủ lên cả khối khi đang rê tệp qua. `pointer-events-none` là BẮT\n BUỘC: thiếu nó, lớp phủ tự nuốt sự kiện `drop` của khối bên dưới và việc thả im lặng\n không làm gì.\n */}\n {upload.dragging ? (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center rounded-lg bg-card/90\">\n <span className=\"font-medium text-sm\">{text.dropPrompt}</span>\n </div>\n ) : null}\n </div>\n\n <p className=\"text-muted-foreground text-xs\">{text.hint}</p>\n\n {/* Thanh tiến độ chỉ hiện khi đang tải — một thanh 0% đứng yên trông như hỏng. */}\n {upload.busy ? <Progress value={upload.percent} aria-label={text.uploading} /> : null}\n </div>\n );\n}\n"],"mappings":";AAoGwB,cAmBhB,YAnBgB;AAjGxB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,wBAAwB;AACjC,SAAS,UAAU;AAEnB,SAAgC,qBAAqB;AAyD9C,SAAS,iBAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,OAAO,iBAAiB,oBAAoB,MAAM;AAExD,QAAM,SAAS,cAAiB;AAAA,IAC9B;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA,kBAAkB;AAAA,IAClB;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,QAAQ,EAAE,cAAc,KAAK,cAAc,mBAAmB,KAAK,kBAAkB;AAAA,IACrF,UAAU,CAAC,YAAY;AAAA,EACzB,CAAC;AAED,QAAM,UAAU,CAAC,YAAY,QAAQ,OAAO;AAM5C,MAAI,CAAC,SAAU,QAAO,oBAAC,SAAI,WAAW,GAAG,2BAA2B,SAAS,GAAI,mBAAQ;AAEzF,SACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,SAAS,GACjD;AAAA,wBAAC,WAAO,GAAG,OAAO,YAAY,WAAU,WAAU;AAAA,IAMlD;AAAA,MAAC;AAAA;AAAA,QACE,GAAG,OAAO;AAAA,QACX,WAAW;AAAA,UACT;AAAA,UACA,OAAO,WAAW,uBAAuB;AAAA,QAC3C;AAAA,QAEC;AAAA;AAAA,UAED,qBAAC,SAAI,WAAW,GAAG,qCAAqC,WAAW,YAAY,GAC7E;AAAA,gCAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,OAAO,MAAM,SAAS,OAAO,MACrF,iBAAO,OAAO,KAAK,YAAY,KAAK,QACvC;AAAA,YAEA,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,UAAU,CAAC,YAAY,SAAS,SAAS,UACtF,eAAK,QACR;AAAA,aACF;AAAA,UAOC,OAAO,WACN,oBAAC,SAAI,WAAU,+FACb,8BAAC,UAAK,WAAU,uBAAuB,eAAK,YAAW,GACzD,IACE;AAAA;AAAA;AAAA,IACN;AAAA,IAEA,oBAAC,OAAE,WAAU,iCAAiC,eAAK,MAAK;AAAA,IAGvD,OAAO,OAAO,oBAAC,YAAS,OAAO,OAAO,SAAS,cAAY,KAAK,WAAW,IAAK;AAAA,KACnF;AAEJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "3.3.2",
3
+ "version": "3.3.4",
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",