@comitor/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/dist/chart/chart.d.ts +88 -0
- package/dist/chart/chart.js +199 -0
- package/dist/chart/chart.js.map +1 -0
- package/dist/chart/comitor-chart.d.ts +109 -0
- package/dist/chart/comitor-chart.js +189 -0
- package/dist/chart/comitor-chart.js.map +1 -0
- package/dist/chart/index.d.ts +4 -0
- package/dist/chart/index.js +3 -0
- package/dist/chart/index.js.map +1 -0
- package/dist/components/composite/avatar-group.d.ts +33 -0
- package/dist/components/composite/avatar-group.js +86 -0
- package/dist/components/composite/avatar-group.js.map +1 -0
- package/dist/components/composite/combobox.d.ts +70 -0
- package/dist/components/composite/combobox.js +291 -0
- package/dist/components/composite/combobox.js.map +1 -0
- package/dist/components/composite/command-palette.d.ts +56 -0
- package/dist/components/composite/command-palette.js +131 -0
- package/dist/components/composite/command-palette.js.map +1 -0
- package/dist/components/composite/confirm-dialog.d.ts +40 -0
- package/dist/components/composite/confirm-dialog.js +96 -0
- package/dist/components/composite/confirm-dialog.js.map +1 -0
- package/dist/components/composite/copy-button.d.ts +27 -0
- package/dist/components/composite/copy-button.js +80 -0
- package/dist/components/composite/copy-button.js.map +1 -0
- package/dist/components/composite/data-table.d.ts +136 -0
- package/dist/components/composite/data-table.js +300 -0
- package/dist/components/composite/data-table.js.map +1 -0
- package/dist/components/composite/date-picker.d.ts +77 -0
- package/dist/components/composite/date-picker.js +268 -0
- package/dist/components/composite/date-picker.js.map +1 -0
- package/dist/components/composite/empty-state.d.ts +38 -0
- package/dist/components/composite/empty-state.js +35 -0
- package/dist/components/composite/empty-state.js.map +1 -0
- package/dist/components/composite/filter-chips.d.ts +39 -0
- package/dist/components/composite/filter-chips.js +79 -0
- package/dist/components/composite/filter-chips.js.map +1 -0
- package/dist/components/composite/form-field.d.ts +58 -0
- package/dist/components/composite/form-field.js +81 -0
- package/dist/components/composite/form-field.js.map +1 -0
- package/dist/components/composite/form-section.d.ts +33 -0
- package/dist/components/composite/form-section.js +85 -0
- package/dist/components/composite/form-section.js.map +1 -0
- package/dist/components/composite/inline-edit.d.ts +38 -0
- package/dist/components/composite/inline-edit.js +165 -0
- package/dist/components/composite/inline-edit.js.map +1 -0
- package/dist/components/composite/keyboard-hint.d.ts +31 -0
- package/dist/components/composite/keyboard-hint.js +72 -0
- package/dist/components/composite/keyboard-hint.js.map +1 -0
- package/dist/components/composite/letter-avatar.d.ts +47 -0
- package/dist/components/composite/letter-avatar.js +66 -0
- package/dist/components/composite/letter-avatar.js.map +1 -0
- package/dist/components/composite/page-container.d.ts +25 -0
- package/dist/components/composite/page-container.js +23 -0
- package/dist/components/composite/page-container.js.map +1 -0
- package/dist/components/composite/page-header.d.ts +49 -0
- package/dist/components/composite/page-header.js +75 -0
- package/dist/components/composite/page-header.js.map +1 -0
- package/dist/components/composite/relative-time.d.ts +28 -0
- package/dist/components/composite/relative-time.js +44 -0
- package/dist/components/composite/relative-time.js.map +1 -0
- package/dist/components/composite/search-filter-bar.d.ts +38 -0
- package/dist/components/composite/search-filter-bar.js +135 -0
- package/dist/components/composite/search-filter-bar.js.map +1 -0
- package/dist/components/composite/stat-card.d.ts +48 -0
- package/dist/components/composite/stat-card.js +76 -0
- package/dist/components/composite/stat-card.js.map +1 -0
- package/dist/components/composite/status-config.d.ts +85 -0
- package/dist/components/composite/status-config.js +41 -0
- package/dist/components/composite/status-config.js.map +1 -0
- package/dist/components/composite/status-pill.d.ts +26 -0
- package/dist/components/composite/status-pill.js +59 -0
- package/dist/components/composite/status-pill.js.map +1 -0
- package/dist/components/composite/table-pagination.d.ts +31 -0
- package/dist/components/composite/table-pagination.js +103 -0
- package/dist/components/composite/table-pagination.js.map +1 -0
- package/dist/components/composite/text-utils.d.ts +38 -0
- package/dist/components/composite/text-utils.js +40 -0
- package/dist/components/composite/text-utils.js.map +1 -0
- package/dist/components/composite/toast.d.ts +56 -0
- package/dist/components/composite/toast.js +63 -0
- package/dist/components/composite/toast.js.map +1 -0
- package/dist/components/ui/accordion.d.ts +17 -0
- package/dist/components/ui/accordion.js +53 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +34 -0
- package/dist/components/ui/alert-dialog.js +103 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/alert.d.ts +16 -0
- package/dist/components/ui/alert.js +50 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/aspect-ratio.d.ts +7 -0
- package/dist/components/ui/aspect-ratio.js +10 -0
- package/dist/components/ui/aspect-ratio.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +33 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +15 -0
- package/dist/components/ui/badge.js +37 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +30 -0
- package/dist/components/ui/breadcrumb.js +89 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button-group.d.ts +21 -0
- package/dist/components/ui/button-group.js +62 -0
- package/dist/components/ui/button-group.js.map +1 -0
- package/dist/components/ui/button.d.ts +21 -0
- package/dist/components/ui/button.js +70 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/calendar.d.ts +26 -0
- package/dist/components/ui/calendar.js +148 -0
- package/dist/components/ui/calendar.js.map +1 -0
- package/dist/components/ui/card.d.ts +33 -0
- package/dist/components/ui/card.js +70 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/carousel.d.ts +46 -0
- package/dist/components/ui/carousel.js +173 -0
- package/dist/components/ui/carousel.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +17 -0
- package/dist/components/ui/checkbox.js +67 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +17 -0
- package/dist/components/ui/collapsible.js +18 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/command.d.ts +41 -0
- package/dist/components/ui/command.js +122 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +51 -0
- package/dist/components/ui/context-menu.js +166 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +48 -0
- package/dist/components/ui/dialog.js +131 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +32 -0
- package/dist/components/ui/drawer.js +102 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +52 -0
- package/dist/components/ui/dropdown-menu.js +167 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty.d.ts +22 -0
- package/dist/components/ui/empty.js +87 -0
- package/dist/components/ui/empty.js.map +1 -0
- package/dist/components/ui/field.d.ts +46 -0
- package/dist/components/ui/field.js +197 -0
- package/dist/components/ui/field.js.map +1 -0
- package/dist/components/ui/form.d.ts +50 -0
- package/dist/components/ui/form.js +100 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/hover-card.d.ts +17 -0
- package/dist/components/ui/hover-card.js +31 -0
- package/dist/components/ui/hover-card.js.map +1 -0
- package/dist/components/ui/input-group.d.ts +26 -0
- package/dist/components/ui/input-group.js +148 -0
- package/dist/components/ui/input-group.js.map +1 -0
- package/dist/components/ui/input-otp.d.ts +23 -0
- package/dist/components/ui/input-otp.js +54 -0
- package/dist/components/ui/input-otp.js.map +1 -0
- package/dist/components/ui/input.d.ts +21 -0
- package/dist/components/ui/input.js +62 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/item.d.ts +39 -0
- package/dist/components/ui/item.js +142 -0
- package/dist/components/ui/item.js.map +1 -0
- package/dist/components/ui/kbd.d.ts +8 -0
- package/dist/components/ui/kbd.js +25 -0
- package/dist/components/ui/kbd.js.map +1 -0
- package/dist/components/ui/label.d.ts +7 -0
- package/dist/components/ui/label.js +21 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/menubar.d.ts +51 -0
- package/dist/components/ui/menubar.js +196 -0
- package/dist/components/ui/menubar.js.map +1 -0
- package/dist/components/ui/navigation-menu.d.ts +39 -0
- package/dist/components/ui/navigation-menu.js +123 -0
- package/dist/components/ui/navigation-menu.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +40 -0
- package/dist/components/ui/pagination.js +95 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +19 -0
- package/dist/components/ui/popover.js +35 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/progress.d.ts +12 -0
- package/dist/components/ui/progress.js +27 -0
- package/dist/components/ui/progress.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +9 -0
- package/dist/components/ui/radio-group.js +34 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/resizable.d.ts +19 -0
- package/dist/components/ui/resizable.js +38 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +13 -0
- package/dist/components/ui/scroll-area.js +46 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.d.ts +33 -0
- package/dist/components/ui/select.js +133 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/separator.d.ts +7 -0
- package/dist/components/ui/separator.js +23 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +38 -0
- package/dist/components/ui/sheet.js +96 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +6 -0
- package/dist/components/ui/skeleton.js +9 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.d.ts +12 -0
- package/dist/components/ui/slider.js +54 -0
- package/dist/components/ui/slider.js.map +1 -0
- package/dist/components/ui/sonner.d.ts +9 -0
- package/dist/components/ui/sonner.js +25 -0
- package/dist/components/ui/sonner.js.map +1 -0
- package/dist/components/ui/spinner.d.ts +6 -0
- package/dist/components/ui/spinner.js +10 -0
- package/dist/components/ui/spinner.js.map +1 -0
- package/dist/components/ui/switch.d.ts +14 -0
- package/dist/components/ui/switch.js +65 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +30 -0
- package/dist/components/ui/table.js +87 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +35 -0
- package/dist/components/ui/tabs.js +54 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +6 -0
- package/dist/components/ui/textarea.js +19 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toggle-group.d.ts +18 -0
- package/dist/components/ui/toggle-group.js +52 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +15 -0
- package/dist/components/ui/toggle.js +33 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +21 -0
- package/dist/components/ui/tooltip.js +38 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/form.d.ts +5 -0
- package/dist/form.js +2 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +16 -0
- package/dist/lib/cn.js +9 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/shell/app-header.d.ts +41 -0
- package/dist/shell/app-header.js +136 -0
- package/dist/shell/app-header.js.map +1 -0
- package/dist/shell/app-launcher.d.ts +33 -0
- package/dist/shell/app-launcher.js +189 -0
- package/dist/shell/app-launcher.js.map +1 -0
- package/dist/shell/app-shell.d.ts +44 -0
- package/dist/shell/app-shell.js +99 -0
- package/dist/shell/app-shell.js.map +1 -0
- package/dist/shell/comitor-lockup.d.ts +33 -0
- package/dist/shell/comitor-lockup.js +112 -0
- package/dist/shell/comitor-lockup.js.map +1 -0
- package/dist/shell/comitor-logo.d.ts +28 -0
- package/dist/shell/comitor-logo.js +92 -0
- package/dist/shell/comitor-logo.js.map +1 -0
- package/dist/shell/index.d.ts +24 -0
- package/dist/shell/index.js +23 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/shell/mobile-menu.d.ts +26 -0
- package/dist/shell/mobile-menu.js +56 -0
- package/dist/shell/mobile-menu.js.map +1 -0
- package/dist/shell/notifications-button.d.ts +22 -0
- package/dist/shell/notifications-button.js +39 -0
- package/dist/shell/notifications-button.js.map +1 -0
- package/dist/shell/shell-context.d.ts +91 -0
- package/dist/shell/shell-context.js +233 -0
- package/dist/shell/shell-context.js.map +1 -0
- package/dist/shell/shell-link.d.ts +19 -0
- package/dist/shell/shell-link.js +18 -0
- package/dist/shell/shell-link.js.map +1 -0
- package/dist/shell/sidebar-group.d.ts +21 -0
- package/dist/shell/sidebar-group.js +43 -0
- package/dist/shell/sidebar-group.js.map +1 -0
- package/dist/shell/sidebar-menu-item.d.ts +27 -0
- package/dist/shell/sidebar-menu-item.js +152 -0
- package/dist/shell/sidebar-menu-item.js.map +1 -0
- package/dist/shell/sidebar.d.ts +26 -0
- package/dist/shell/sidebar.js +92 -0
- package/dist/shell/sidebar.js.map +1 -0
- package/dist/shell/theme-provider.d.ts +18 -0
- package/dist/shell/theme-provider.js +29 -0
- package/dist/shell/theme-provider.js.map +1 -0
- package/dist/shell/theme-toggle.d.ts +28 -0
- package/dist/shell/theme-toggle.js +67 -0
- package/dist/shell/theme-toggle.js.map +1 -0
- package/dist/shell/types.d.ts +168 -0
- package/dist/shell/types.js +38 -0
- package/dist/shell/types.js.map +1 -0
- package/dist/shell/user-menu.d.ts +42 -0
- package/dist/shell/user-menu.js +132 -0
- package/dist/shell/user-menu.js.map +1 -0
- package/dist/shell/utils.d.ts +62 -0
- package/dist/shell/utils.js +73 -0
- package/dist/shell/utils.js.map +1 -0
- package/dist/shell/workspace-switcher.d.ts +24 -0
- package/dist/shell/workspace-switcher.js +219 -0
- package/dist/shell/workspace-switcher.js.map +1 -0
- package/dist/tokens.d.ts +173 -0
- package/dist/tokens.js +161 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +151 -0
- package/styles.css +357 -0
|
@@ -0,0 +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 { getLetterAvatarTone } from \"../components/composite/letter-avatar.js\";\nimport { removeVietnameseTones } from \"../components/composite/text-utils.js\";\nimport type { AppDescriptor, NavGroup, NavItem, ShellNav } from \"./types.js\";\n\n/**\n * Initials kiểu Việt Nam — **cùng một cài đặt** với entry chính (`@comitor/ui`).\n *\n * Re-export chứ không viết lại: 0.1.0 từng có hai bản (`toUpperCase()` ở đây,\n * `toLocaleUpperCase(\"vi-VN\")` ở `composite/text-utils.ts`) nên cùng một cái tên\n * cho ra hai kết quả tuỳ chỗ import. Tên vẫn xuất ở cả hai entry để không phá API,\n * nhưng chỉ còn MỘT thân hàm.\n */\nexport { getVietnameseInitials } 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 * 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 * @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 */\nexport function isNavItemActive(item: NavItem, pathname: string, siblings: NavItem[] = []): boolean {\n if (item.isActive !== undefined) return item.isActive;\n if (!pathname || !item.href) return false;\n if (pathname === item.href) return true;\n const hasExactMatchElsewhere = siblings.some((other) => other.href !== item.href && pathname === other.href);\n if (hasExactMatchElsewhere) return false;\n return item.href !== \"/\" && pathname.startsWith(`${item.href}/`);\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(item: NavItem, pathname: string, siblings: NavItem[] = []): boolean {\n if (isNavItemActive(item, pathname, siblings)) return true;\n return (item.children ?? []).some((child) => isNavBranchActive(child, pathname, siblings));\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 \"Khám phá thêm\" 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 */\nexport function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null {\n if (apps.length === 0) return null;\n const matched = [...apps]\n .sort((a, b) => b.href.length - a.href.length)\n .find((app) => pathname === app.href || (app.href !== \"/\" && pathname.startsWith(`${app.href}/`)));\n return matched ?? apps[0] ?? null;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Chuỗi tiếng Việt\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Bỏ dấu tiếng Việt + hạ chữ thường để so khớp khi tìm kiếm.\n * (\"Đà Nẵng\" → \"da nang\"). `đ/Đ` phải xử lý riêng vì NFD không tách được — phần bỏ dấu\n * dùng lại `removeVietnameseTones` của Tier 2, ở đây chỉ thêm hạ chữ thường + trim.\n */\nexport function normalizeVi(value: string): string {\n return removeVietnameseTones(value).toLowerCase().trim();\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Avatar chữ cái\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Class nền/chữ của avatar chữ cái theo TÊN.\n *\n * Chỉ là lối vào tầng shell của `getLetterAvatarTone` (`composite/letter-avatar.tsx`) —\n * NGUỒN SỰ THẬT DUY NHẤT về màu avatar. 0.1.0 từng có bảng tông + hàm băm riêng ở đây,\n * lệch cả bảng màu lẫn thuật toán băm với `LetterAvatar`, nên UserMenu/WorkspaceSwitcher\n * tô một màu còn `AvatarGroup` của CÙNG một người tô màu khác trên CÙNG một trang.\n *\n * @param seed Phải là **tên hiển thị** (đúng thứ truyền vào prop `name` của `LetterAvatar`).\n * Truyền email/slug sẽ lại lệch màu với avatar cùng người ở chỗ khác.\n */\nexport function getAvatarToneClass(seed: string): string {\n return getLetterAvatarTone(seed);\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Biến CSS theo app\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Inline style gán `--app-accent` cho một app cụ thể.\n * Trả về `undefined` khi app không khai accent → giữ nguyên token mặc định của theme.\n */\nexport function appAccentStyle(app: Pick<AppDescriptor, \"accent\" | \"accentForeground\"> | null | undefined) {\n if (!app?.accent && !app?.accentForeground) 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 return style as CSSProperties;\n}\n"],"mappings":"AAMA,SAAS,2BAA2B;AACpC,SAAS,6BAA6B;AAWtC,SAAS,6BAA6B;AAOtC,SAAS,WAAW,OAA8C;AAChE,SAAO,MAAM,QAAS,MAAmB,KAAK;AAChD;AAMO,SAAS,YAAY,KAAuC;AACjE,MAAI,CAAC,OAAO,IAAI,WAAW,EAAG,QAAO,CAAC;AACtC,QAAM,QAAQ,IAAI,CAAC;AACnB,MAAI,SAAS,WAAW,KAAK,EAAG,QAAO;AACvC,SAAO,CAAC,EAAE,IAAI,WAAW,OAAO,IAAiB,CAAC;AACpD;AAGO,SAAS,gBAAgB,QAA+B;AAC7D,QAAM,MAAiB,CAAC;AACxB,QAAM,OAAO,CAAC,UAAqB;AACjC,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,IAAI;AACb,UAAI,KAAK,UAAU,OAAQ,MAAK,KAAK,QAAQ;AAAA,IAC/C;AAAA,EACF;AACA,aAAW,SAAS,OAAQ,MAAK,MAAM,KAAK;AAC5C,SAAO;AACT;AAUO,SAAS,gBAAgB,MAAe,UAAkB,WAAsB,CAAC,GAAY;AAClG,MAAI,KAAK,aAAa,OAAW,QAAO,KAAK;AAC7C,MAAI,CAAC,YAAY,CAAC,KAAK,KAAM,QAAO;AACpC,MAAI,aAAa,KAAK,KAAM,QAAO;AACnC,QAAM,yBAAyB,SAAS,KAAK,CAAC,UAAU,MAAM,SAAS,KAAK,QAAQ,aAAa,MAAM,IAAI;AAC3G,MAAI,uBAAwB,QAAO;AACnC,SAAO,KAAK,SAAS,OAAO,SAAS,WAAW,GAAG,KAAK,IAAI,GAAG;AACjE;AAGO,SAAS,kBAAkB,MAAe,UAAkB,WAAsB,CAAC,GAAY;AACpG,MAAI,gBAAgB,MAAM,UAAU,QAAQ,EAAG,QAAO;AACtD,UAAQ,KAAK,YAAY,CAAC,GAAG,KAAK,CAAC,UAAU,kBAAkB,OAAO,UAAU,QAAQ,CAAC;AAC3F;AAOO,SAAS,gBAAgB,MAAwC;AACtE,SAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,aAAa,KAAK;AACpD;AAGO,SAAS,cAAc,MAAwC;AACpE,SAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,aAAa,KAAK;AACpD;AAMO,SAAS,kBAAkB,MAAuB,UAAwC;AAC/F,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,UAAU,CAAC,GAAG,IAAI,EACrB,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAC5C,KAAK,CAAC,QAAQ,aAAa,IAAI,QAAS,IAAI,SAAS,OAAO,SAAS,WAAW,GAAG,IAAI,IAAI,GAAG,CAAE;AACnG,SAAO,WAAW,KAAK,CAAC,KAAK;AAC/B;AAWO,SAAS,YAAY,OAAuB;AACjD,SAAO,sBAAsB,KAAK,EAAE,YAAY,EAAE,KAAK;AACzD;AAiBO,SAAS,mBAAmB,MAAsB;AACvD,SAAO,oBAAoB,IAAI;AACjC;AAUO,SAAS,eAAe,KAA4E;AACzG,MAAI,CAAC,KAAK,UAAU,CAAC,KAAK,iBAAkB,QAAO;AACnD,QAAM,QAAgC,CAAC;AACvC,MAAI,IAAI,OAAQ,OAAM,cAAc,IAAI,IAAI;AAC5C,MAAI,IAAI,iBAAkB,OAAM,yBAAyB,IAAI,IAAI;AACjE,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface WorkspaceSwitcherProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
/** Sidebar thu gọn → chỉ hiện avatar + tooltip. */
|
|
6
|
+
collapsed?: boolean;
|
|
7
|
+
/** Ẩn khu "Hành động" (tạo workspace, cài đặt, mời thành viên, đăng xuất). */
|
|
8
|
+
showActions?: boolean;
|
|
9
|
+
/** Căn panel — mặc định "start" (mở xuôi theo mép trái sidebar). */
|
|
10
|
+
align?: "start" | "center" | "end";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* **Trục 1 — đổi không gian làm việc.**
|
|
14
|
+
*
|
|
15
|
+
* Chỉ gọi `onWorkspaceChange(id)`; điều hướng/đổi subdomain là việc của app
|
|
16
|
+
* (đổi workspace kéo theo đổi toàn bộ dữ liệu + quyền nên shell không tự quyết).
|
|
17
|
+
* Có ≤1 workspace thì phần danh sách tự ẩn, chỉ còn nhãn workspace hiện tại.
|
|
18
|
+
*
|
|
19
|
+
* Dùng `Popover` chứ không `DropdownMenu`: menu của Radix có typeahead, gõ vào
|
|
20
|
+
* ô tìm kiếm bên trong sẽ bị nó cướp phím.
|
|
21
|
+
*/
|
|
22
|
+
declare function WorkspaceSwitcher({ className, collapsed, showActions, align }: WorkspaceSwitcherProps): React.JSX.Element | null;
|
|
23
|
+
|
|
24
|
+
export { WorkspaceSwitcher, type WorkspaceSwitcherProps };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Building2, Check, ChevronsUpDown, LogOut, Plus, Search, Settings, UserPlus } from "lucide-react";
|
|
4
|
+
import { useMemo, useRef, useState } from "react";
|
|
5
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar.js";
|
|
6
|
+
import { Badge } from "../components/ui/badge.js";
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../components/ui/popover.js";
|
|
8
|
+
import { Separator } from "../components/ui/separator.js";
|
|
9
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
|
|
10
|
+
import { cn } from "../lib/cn.js";
|
|
11
|
+
import { useShell } from "./shell-context.js";
|
|
12
|
+
import { getAvatarToneClass, getVietnameseInitials, normalizeVi } from "./utils.js";
|
|
13
|
+
const SEARCH_THRESHOLD = 8;
|
|
14
|
+
function WorkspaceAvatar({ workspace, className }) {
|
|
15
|
+
return /* @__PURE__ */ jsxs(Avatar, { className: cn("size-6 rounded-md", className), children: [
|
|
16
|
+
workspace.logoUrl ? /* @__PURE__ */ jsx(AvatarImage, { src: workspace.logoUrl, alt: "", className: "rounded-md" }) : null,
|
|
17
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: cn("rounded-md text-[10px] font-semibold", getAvatarToneClass(workspace.name)), children: getVietnameseInitials(workspace.name) })
|
|
18
|
+
] });
|
|
19
|
+
}
|
|
20
|
+
function WorkspaceSwitcher({
|
|
21
|
+
className,
|
|
22
|
+
collapsed = false,
|
|
23
|
+
showActions = true,
|
|
24
|
+
align = "start"
|
|
25
|
+
}) {
|
|
26
|
+
const {
|
|
27
|
+
workspaces,
|
|
28
|
+
currentWorkspace,
|
|
29
|
+
onWorkspaceChange,
|
|
30
|
+
onCreateWorkspace,
|
|
31
|
+
onWorkspaceSettings,
|
|
32
|
+
onInviteMembers,
|
|
33
|
+
onLogout,
|
|
34
|
+
labels
|
|
35
|
+
} = useShell();
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const [query, setQuery] = useState("");
|
|
38
|
+
const searchRef = useRef(null);
|
|
39
|
+
const showList = workspaces.length > 1;
|
|
40
|
+
const showSearch = workspaces.length >= SEARCH_THRESHOLD;
|
|
41
|
+
const filtered = useMemo(() => {
|
|
42
|
+
if (!query.trim()) return workspaces;
|
|
43
|
+
const needle = normalizeVi(query);
|
|
44
|
+
return workspaces.filter(
|
|
45
|
+
(workspace) => normalizeVi(workspace.name).includes(needle) || normalizeVi(workspace.slug).includes(needle)
|
|
46
|
+
);
|
|
47
|
+
}, [workspaces, query]);
|
|
48
|
+
const hasActions = showActions && Boolean(onCreateWorkspace || onWorkspaceSettings || onInviteMembers || onLogout);
|
|
49
|
+
if (!currentWorkspace) return null;
|
|
50
|
+
const handleSelect = (workspace) => {
|
|
51
|
+
setOpen(false);
|
|
52
|
+
setQuery("");
|
|
53
|
+
if (workspace.id !== currentWorkspace.id) onWorkspaceChange?.(workspace.id);
|
|
54
|
+
};
|
|
55
|
+
const closeThen = (action) => () => {
|
|
56
|
+
setOpen(false);
|
|
57
|
+
action();
|
|
58
|
+
};
|
|
59
|
+
const trigger = collapsed ? /* @__PURE__ */ jsx(
|
|
60
|
+
"button",
|
|
61
|
+
{
|
|
62
|
+
type: "button",
|
|
63
|
+
"aria-label": labels.workspaceSwitcherLabel,
|
|
64
|
+
className: cn(
|
|
65
|
+
"flex size-9 items-center justify-center rounded-md",
|
|
66
|
+
"transition-colors duration-150 ease-out hover:bg-sidebar-accent",
|
|
67
|
+
className
|
|
68
|
+
),
|
|
69
|
+
children: /* @__PURE__ */ jsx(WorkspaceAvatar, { workspace: currentWorkspace })
|
|
70
|
+
}
|
|
71
|
+
) : /* @__PURE__ */ jsxs(
|
|
72
|
+
"button",
|
|
73
|
+
{
|
|
74
|
+
type: "button",
|
|
75
|
+
"aria-label": labels.workspaceSwitcherLabel,
|
|
76
|
+
className: cn(
|
|
77
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left",
|
|
78
|
+
"transition-colors duration-150 ease-out hover:bg-sidebar-accent",
|
|
79
|
+
className
|
|
80
|
+
),
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx(WorkspaceAvatar, { workspace: currentWorkspace }),
|
|
83
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
84
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-sidebar-foreground", children: currentWorkspace.name }),
|
|
85
|
+
currentWorkspace.plan ? /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-sidebar-foreground/55", children: currentWorkspace.plan }) : null
|
|
86
|
+
] }),
|
|
87
|
+
/* @__PURE__ */ jsx(ChevronsUpDown, { className: "size-3.5 shrink-0 text-sidebar-foreground/45" })
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
92
|
+
collapsed ? /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
93
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: trigger }) }),
|
|
94
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "right", children: currentWorkspace.name })
|
|
95
|
+
] }) : /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: trigger }),
|
|
96
|
+
/* @__PURE__ */ jsxs(
|
|
97
|
+
PopoverContent,
|
|
98
|
+
{
|
|
99
|
+
align,
|
|
100
|
+
sideOffset: 6,
|
|
101
|
+
className: "w-72 p-0",
|
|
102
|
+
onOpenAutoFocus: (event) => {
|
|
103
|
+
if (!searchRef.current) return;
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
searchRef.current.focus();
|
|
106
|
+
},
|
|
107
|
+
children: [
|
|
108
|
+
showList ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
109
|
+
showSearch ? /* @__PURE__ */ jsxs("div", { className: "relative border-b border-border p-2", children: [
|
|
110
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" }),
|
|
111
|
+
/* @__PURE__ */ jsx(
|
|
112
|
+
"input",
|
|
113
|
+
{
|
|
114
|
+
ref: searchRef,
|
|
115
|
+
type: "search",
|
|
116
|
+
value: query,
|
|
117
|
+
onChange: (event) => setQuery(event.target.value),
|
|
118
|
+
placeholder: labels.workspaceSearchPlaceholder,
|
|
119
|
+
"aria-label": labels.workspaceSearchPlaceholder,
|
|
120
|
+
className: cn(
|
|
121
|
+
"h-8 w-full rounded-md border border-input bg-background pl-7 pr-2 text-sm",
|
|
122
|
+
// `border-ring` chứ không `border-gold-300`: token semantic đổi theo theme
|
|
123
|
+
// (gold-300 giữ nguyên ở dark), và đây là quy tắc màu của package.
|
|
124
|
+
"placeholder:text-muted-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/50"
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
] }) : null,
|
|
129
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto scrollbar-thin p-1", role: "group", "aria-label": labels.workspace, children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-2 py-6 text-center text-sm text-muted-foreground", children: labels.workspaceEmpty }) : filtered.map((workspace) => {
|
|
130
|
+
const isCurrent = workspace.id === currentWorkspace.id;
|
|
131
|
+
return /* @__PURE__ */ jsxs(
|
|
132
|
+
"button",
|
|
133
|
+
{
|
|
134
|
+
type: "button",
|
|
135
|
+
"aria-current": isCurrent ? "true" : void 0,
|
|
136
|
+
onClick: () => handleSelect(workspace),
|
|
137
|
+
className: cn(
|
|
138
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left",
|
|
139
|
+
"transition-colors duration-150 ease-out hover:bg-accent",
|
|
140
|
+
isCurrent && "bg-accent/60"
|
|
141
|
+
),
|
|
142
|
+
children: [
|
|
143
|
+
/* @__PURE__ */ jsx(WorkspaceAvatar, { workspace }),
|
|
144
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
145
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-foreground", children: workspace.name }),
|
|
146
|
+
workspace.role || workspace.memberCount !== void 0 ? /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: [
|
|
147
|
+
workspace.role,
|
|
148
|
+
workspace.memberCount === void 0 ? void 0 : `${workspace.memberCount} ${labels.members}`
|
|
149
|
+
].filter(Boolean).join(" \xB7 ") }) : null
|
|
150
|
+
] }),
|
|
151
|
+
workspace.plan ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", size: "sm", className: "shrink-0", children: workspace.plan }) : null,
|
|
152
|
+
isCurrent ? /* @__PURE__ */ jsx(Check, { className: "size-4 shrink-0 text-popover-foreground" }) : null
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
workspace.id
|
|
156
|
+
);
|
|
157
|
+
}) })
|
|
158
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3", children: [
|
|
159
|
+
/* @__PURE__ */ jsx(WorkspaceAvatar, { workspace: currentWorkspace, className: "size-8" }),
|
|
160
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
|
|
161
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-foreground", children: currentWorkspace.name }),
|
|
162
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: currentWorkspace.plan ?? currentWorkspace.slug })
|
|
163
|
+
] })
|
|
164
|
+
] }),
|
|
165
|
+
hasActions ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
166
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
167
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col p-1", children: [
|
|
168
|
+
onCreateWorkspace ? /* @__PURE__ */ jsx(ActionRow, { icon: Plus, label: labels.createWorkspace, onClick: closeThen(onCreateWorkspace) }) : null,
|
|
169
|
+
onWorkspaceSettings ? /* @__PURE__ */ jsx(
|
|
170
|
+
ActionRow,
|
|
171
|
+
{
|
|
172
|
+
icon: Settings,
|
|
173
|
+
label: labels.workspaceSettings,
|
|
174
|
+
onClick: closeThen(() => onWorkspaceSettings(currentWorkspace.id))
|
|
175
|
+
}
|
|
176
|
+
) : null,
|
|
177
|
+
onInviteMembers ? /* @__PURE__ */ jsx(
|
|
178
|
+
ActionRow,
|
|
179
|
+
{
|
|
180
|
+
icon: UserPlus,
|
|
181
|
+
label: labels.inviteMembers,
|
|
182
|
+
onClick: closeThen(() => onInviteMembers(currentWorkspace.id))
|
|
183
|
+
}
|
|
184
|
+
) : null,
|
|
185
|
+
onLogout ? /* @__PURE__ */ jsx(ActionRow, { icon: LogOut, label: labels.logout, destructive: true, onClick: closeThen(onLogout) }) : null
|
|
186
|
+
] })
|
|
187
|
+
] }) : null
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
] });
|
|
192
|
+
}
|
|
193
|
+
function ActionRow({
|
|
194
|
+
icon: Icon = Building2,
|
|
195
|
+
label,
|
|
196
|
+
onClick,
|
|
197
|
+
destructive = false
|
|
198
|
+
}) {
|
|
199
|
+
return /* @__PURE__ */ jsxs(
|
|
200
|
+
"button",
|
|
201
|
+
{
|
|
202
|
+
type: "button",
|
|
203
|
+
onClick,
|
|
204
|
+
className: cn(
|
|
205
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
206
|
+
"transition-colors duration-150 ease-out",
|
|
207
|
+
destructive ? "text-destructive hover:bg-destructive/10" : "text-foreground hover:bg-accent"
|
|
208
|
+
),
|
|
209
|
+
children: [
|
|
210
|
+
/* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0 opacity-70" }),
|
|
211
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: label })
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
WorkspaceSwitcher
|
|
218
|
+
};
|
|
219
|
+
//# sourceMappingURL=workspace-switcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/workspace-switcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { Building2, Check, ChevronsUpDown, LogOut, Plus, Search, Settings, UserPlus } from \"lucide-react\";\nimport { useMemo, useRef, useState } from \"react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport type { Workspace } from \"./types.js\";\nimport { getAvatarToneClass, getVietnameseInitials, normalizeVi } from \"./utils.js\";\n\n/** Từ số workspace này trở lên thì hiện ô tìm kiếm trong danh sách. */\nconst SEARCH_THRESHOLD = 8;\n\n/**\n * Ảnh đại diện workspace: logo nếu có, không thì ô chữ cái tô màu deterministic.\n *\n * Seed là `workspace.name` chứ KHÔNG phải slug: app hiển thị workspace bằng\n * `<LetterAvatar name={workspace.name} square />` ở chỗ khác, seed lệch là màu lệch.\n */\nfunction WorkspaceAvatar({ workspace, className }: { workspace: Workspace; className?: string }) {\n return (\n <Avatar className={cn(\"size-6 rounded-md\", className)}>\n {workspace.logoUrl ? <AvatarImage src={workspace.logoUrl} alt=\"\" className=\"rounded-md\" /> : null}\n <AvatarFallback className={cn(\"rounded-md text-[10px] font-semibold\", getAvatarToneClass(workspace.name))}>\n {getVietnameseInitials(workspace.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface WorkspaceSwitcherProps {\n className?: string;\n /** Sidebar thu gọn → chỉ hiện avatar + tooltip. */\n collapsed?: boolean;\n /** Ẩn khu \"Hành động\" (tạo workspace, cài đặt, mời thành viên, đăng xuất). */\n showActions?: boolean;\n /** Căn panel — mặc định \"start\" (mở xuôi theo mép trái sidebar). */\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 1 — đổi không gian làm việc.**\n *\n * Chỉ gọi `onWorkspaceChange(id)`; điều hướng/đổi subdomain là việc của app\n * (đổi workspace kéo theo đổi toàn bộ dữ liệu + quyền nên shell không tự quyết).\n * Có ≤1 workspace thì phần danh sách tự ẩn, chỉ còn nhãn workspace hiện tại.\n *\n * Dùng `Popover` chứ không `DropdownMenu`: menu của Radix có typeahead, gõ vào\n * ô tìm kiếm bên trong sẽ bị nó cướp phím.\n */\nexport function WorkspaceSwitcher({\n className,\n collapsed = false,\n showActions = true,\n align = \"start\"\n}: WorkspaceSwitcherProps) {\n const {\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n onLogout,\n labels\n } = useShell();\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const searchRef = useRef<HTMLInputElement>(null);\n\n const showList = workspaces.length > 1;\n const showSearch = workspaces.length >= SEARCH_THRESHOLD;\n\n const filtered = useMemo(() => {\n if (!query.trim()) return workspaces;\n const needle = normalizeVi(query);\n return workspaces.filter(\n (workspace) => normalizeVi(workspace.name).includes(needle) || normalizeVi(workspace.slug).includes(needle)\n );\n }, [workspaces, query]);\n\n const hasActions = showActions && Boolean(onCreateWorkspace || onWorkspaceSettings || onInviteMembers || onLogout);\n\n if (!currentWorkspace) return null;\n\n const handleSelect = (workspace: Workspace) => {\n setOpen(false);\n setQuery(\"\");\n if (workspace.id !== currentWorkspace.id) onWorkspaceChange?.(workspace.id);\n };\n\n /** Đóng panel rồi mới chạy hành động — tránh panel còn mở đè lên dialog app mở ra sau đó. */\n const closeThen = (action: () => void) => () => {\n setOpen(false);\n action();\n };\n\n const trigger = collapsed ? (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n </button>\n ) : (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-semibold text-sidebar-foreground\">{currentWorkspace.name}</span>\n {currentWorkspace.plan ? (\n <span className=\"truncate text-[11px] text-sidebar-foreground/55\">{currentWorkspace.plan}</span>\n ) : null}\n </span>\n <ChevronsUpDown className=\"size-3.5 shrink-0 text-sidebar-foreground/45\" />\n </button>\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {collapsed ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{currentWorkspace.name}</TooltipContent>\n </Tooltip>\n ) : (\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n )}\n\n <PopoverContent\n align={align}\n sideOffset={6}\n className=\"w-72 p-0\"\n onOpenAutoFocus={(event) => {\n // Mặc định Radix focus vào khung panel; có ô tìm thì đưa con trỏ vào đó luôn.\n if (!searchRef.current) return;\n event.preventDefault();\n searchRef.current.focus();\n }}\n >\n {showList ? (\n <>\n {showSearch ? (\n <div className=\"relative border-b border-border p-2\">\n <Search className=\"pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={labels.workspaceSearchPlaceholder}\n aria-label={labels.workspaceSearchPlaceholder}\n className={cn(\n \"h-8 w-full rounded-md border border-input bg-background pl-7 pr-2 text-sm\",\n // `border-ring` chứ không `border-gold-300`: token semantic đổi theo theme\n // (gold-300 giữ nguyên ở dark), và đây là quy tắc màu của package.\n \"placeholder:text-muted-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/50\"\n )}\n />\n </div>\n ) : null}\n\n {/*\n CỐ Ý không dùng role=\"listbox\"/\"option\": bộ ARIA đó hứa với trình đọc màn hình\n một widget điều khiển bằng ↑/↓/Home/End + roving tabindex + aria-activedescendant,\n mà hành vi thật ở đây chỉ là Tab qua từng nút trong Popover (Popover được chọn\n thay DropdownMenu vì menu của Radix cướp phím gõ của ô tìm kiếm — xem chú thích\n component). Khai đúng cái mình làm còn hơn khai sang trọng rồi lừa người dùng:\n danh sách nút thường + `aria-current` cho mục đang chọn.\n */}\n <div className=\"max-h-64 overflow-y-auto scrollbar-thin p-1\" role=\"group\" aria-label={labels.workspace}>\n {filtered.length === 0 ? (\n <p className=\"px-2 py-6 text-center text-sm text-muted-foreground\">{labels.workspaceEmpty}</p>\n ) : (\n filtered.map((workspace) => {\n const isCurrent = workspace.id === currentWorkspace.id;\n return (\n <button\n key={workspace.id}\n type=\"button\"\n aria-current={isCurrent ? \"true\" : undefined}\n onClick={() => handleSelect(workspace)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-accent\",\n isCurrent && \"bg-accent/60\"\n )}\n >\n <WorkspaceAvatar workspace={workspace} />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium text-foreground\">{workspace.name}</span>\n {workspace.role || workspace.memberCount !== undefined ? (\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {[\n workspace.role,\n workspace.memberCount === undefined\n ? undefined\n : `${workspace.memberCount} ${labels.members}`\n ]\n .filter(Boolean)\n .join(\" · \")}\n </span>\n ) : null}\n </span>\n {workspace.plan ? (\n <Badge variant=\"secondary\" size=\"sm\" className=\"shrink-0\">\n {workspace.plan}\n </Badge>\n ) : null}\n {/* KHÔNG dùng `text-app-accent`: accent mặc định (gold #E8B824) chỉ đạt\n 1,86:1 trên nền popover trắng — dưới ngưỡng 3:1 của WCAG 1.4.11 cho\n thành phần phi-văn-bản. `text-popover-foreground` đạt 18,9:1 (light)\n và 16,6:1 (dark). */}\n {isCurrent ? <Check className=\"size-4 shrink-0 text-popover-foreground\" /> : null}\n </button>\n );\n })\n )}\n </div>\n </>\n ) : (\n <div className=\"flex items-center gap-2 p-3\">\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{currentWorkspace.name}</span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {currentWorkspace.plan ?? currentWorkspace.slug}\n </span>\n </span>\n </div>\n )}\n\n {hasActions ? (\n <>\n <Separator />\n <div className=\"flex flex-col p-1\">\n {onCreateWorkspace ? (\n <ActionRow icon={Plus} label={labels.createWorkspace} onClick={closeThen(onCreateWorkspace)} />\n ) : null}\n {onWorkspaceSettings ? (\n <ActionRow\n icon={Settings}\n label={labels.workspaceSettings}\n onClick={closeThen(() => onWorkspaceSettings(currentWorkspace.id))}\n />\n ) : null}\n {onInviteMembers ? (\n <ActionRow\n icon={UserPlus}\n label={labels.inviteMembers}\n onClick={closeThen(() => onInviteMembers(currentWorkspace.id))}\n />\n ) : null}\n {onLogout ? (\n <ActionRow icon={LogOut} label={labels.logout} destructive onClick={closeThen(onLogout)} />\n ) : null}\n </div>\n </>\n ) : null}\n </PopoverContent>\n </Popover>\n );\n}\n\n/** Một dòng hành động trong panel (không phải mục chọn workspace). */\nfunction ActionRow({\n icon: Icon = Building2,\n label,\n onClick,\n destructive = false\n}: {\n icon?: typeof Building2;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n destructive ? \"text-destructive hover:bg-destructive/10\" : \"text-foreground hover:bg-accent\"\n )}\n >\n <Icon className=\"size-4 shrink-0 opacity-70\" />\n <span className=\"truncate\">{label}</span>\n </button>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design token dạng TS object — dành cho nơi KHÔNG có Tailwind: canvas, biểu đồ tuỳ biến,
|
|
3
|
+
* email HTML, xuất PDF, (sau này) React Native.
|
|
4
|
+
*
|
|
5
|
+
* ⚠ Nguồn sự thật của giá trị là `styles.css` (CSS variable). File này chỉ MIRROR hằng số.
|
|
6
|
+
* Sửa màu ở styles.css thì phải sửa kèm ở đây — và ngược lại.
|
|
7
|
+
*
|
|
8
|
+
* Trong môi trường có Tailwind, LUÔN dùng class token (`bg-primary`, `text-ash-500`),
|
|
9
|
+
* đừng đọc hex từ đây rồi nhét vào style inline: làm vậy là mất dark mode.
|
|
10
|
+
*/
|
|
11
|
+
/** Thang GOLD 7 bậc — trục màu chính của brand Comitor. */
|
|
12
|
+
declare const gold: {
|
|
13
|
+
readonly 50: "#FBF5DC";
|
|
14
|
+
readonly 100: "#F8E89D";
|
|
15
|
+
readonly 200: "#F5D060";
|
|
16
|
+
/** primary ở light mode */
|
|
17
|
+
readonly 300: "#E8B824";
|
|
18
|
+
readonly 400: "#B88A00";
|
|
19
|
+
readonly 500: "#7A5800";
|
|
20
|
+
readonly 600: "#5C4006";
|
|
21
|
+
};
|
|
22
|
+
/** Trung tính brand (khác thang ash — đây là màu "giấy/mực" của brand book). */
|
|
23
|
+
declare const brandNeutral: {
|
|
24
|
+
readonly ink: "#0E1116";
|
|
25
|
+
readonly inkSoft: "#3A3F4A";
|
|
26
|
+
readonly paper: "#FBF7EE";
|
|
27
|
+
readonly warm: "#EDE8D8";
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Thang xám Comitor — namespace `ash`, KHÔNG phải `gray`.
|
|
31
|
+
* 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
|
|
32
|
+
* thang gray dựng sẵn của Tailwind (xem chú thích trong styles.css).
|
|
33
|
+
*/
|
|
34
|
+
declare const ash: {
|
|
35
|
+
readonly 50: "#F7F7F8";
|
|
36
|
+
readonly 100: "#EDEEF1";
|
|
37
|
+
readonly 300: "#C8CBD3";
|
|
38
|
+
readonly 500: "#7E8290";
|
|
39
|
+
readonly 700: "#3A3F4A";
|
|
40
|
+
readonly 900: "#16181D";
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Màu phụ. Bản `dark` được nâng sáng để đạt tương phản AA khi làm CHỮ trên nền #0E1116 —
|
|
44
|
+
* bản `light` quá tối cho việc đó.
|
|
45
|
+
*/
|
|
46
|
+
declare const accent: {
|
|
47
|
+
readonly light: {
|
|
48
|
+
readonly navy: "#0F1E3D";
|
|
49
|
+
readonly teal: "#1F8A8B";
|
|
50
|
+
readonly red: "#D64545";
|
|
51
|
+
readonly green: "#2EA56A";
|
|
52
|
+
readonly yellow: "#F2C94C";
|
|
53
|
+
};
|
|
54
|
+
readonly dark: {
|
|
55
|
+
readonly navy: "#0F1E3D";
|
|
56
|
+
readonly teal: "#2DD4BF";
|
|
57
|
+
readonly red: "#F87171";
|
|
58
|
+
readonly green: "#4ADE80";
|
|
59
|
+
readonly yellow: "#FACC15";
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
/** Semantic token của shadcn — mirror khối `:root` / `.dark` trong styles.css. */
|
|
63
|
+
declare const semantic: {
|
|
64
|
+
readonly light: {
|
|
65
|
+
readonly background: "#FFFFFF";
|
|
66
|
+
readonly foreground: "#0E1116";
|
|
67
|
+
readonly card: "#FFFFFF";
|
|
68
|
+
readonly cardForeground: "#0E1116";
|
|
69
|
+
readonly popover: "#FFFFFF";
|
|
70
|
+
readonly popoverForeground: "#0E1116";
|
|
71
|
+
readonly primary: "#E8B824";
|
|
72
|
+
readonly primaryForeground: "#0E1116";
|
|
73
|
+
readonly secondary: "#EDE8D8";
|
|
74
|
+
readonly secondaryForeground: "#0E1116";
|
|
75
|
+
readonly muted: "#F7F7F8";
|
|
76
|
+
readonly mutedForeground: "#7E8290";
|
|
77
|
+
readonly accent: "#FBF5DC";
|
|
78
|
+
readonly accentForeground: "#0E1116";
|
|
79
|
+
readonly destructive: "#D64545";
|
|
80
|
+
readonly destructiveForeground: "#FFFFFF";
|
|
81
|
+
readonly border: "#EDEEF1";
|
|
82
|
+
readonly input: "#EDEEF1";
|
|
83
|
+
readonly ring: "#E8B824";
|
|
84
|
+
readonly sidebar: "#FAFAF9";
|
|
85
|
+
readonly sidebarForeground: "#0E1116";
|
|
86
|
+
readonly sidebarBorder: "#EDEEF1";
|
|
87
|
+
readonly sidebarAccent: "#FBF5DC";
|
|
88
|
+
readonly sidebarAccentForeground: "#0E1116";
|
|
89
|
+
};
|
|
90
|
+
readonly dark: {
|
|
91
|
+
readonly background: "#0E1116";
|
|
92
|
+
readonly foreground: "#F7F7F8";
|
|
93
|
+
readonly card: "#16181D";
|
|
94
|
+
readonly cardForeground: "#F7F7F8";
|
|
95
|
+
readonly popover: "#16181D";
|
|
96
|
+
readonly popoverForeground: "#F7F7F8";
|
|
97
|
+
readonly primary: "#F5D060";
|
|
98
|
+
readonly primaryForeground: "#0E1116";
|
|
99
|
+
readonly secondary: "#1E2128";
|
|
100
|
+
readonly secondaryForeground: "#F7F7F8";
|
|
101
|
+
readonly muted: "#1E2128";
|
|
102
|
+
readonly mutedForeground: "#7E8290";
|
|
103
|
+
readonly accent: "#1E2128";
|
|
104
|
+
readonly accentForeground: "#F7F7F8";
|
|
105
|
+
readonly destructive: "#D64545";
|
|
106
|
+
readonly destructiveForeground: "#FFFFFF";
|
|
107
|
+
readonly border: "#1E2128";
|
|
108
|
+
readonly input: "#1E2128";
|
|
109
|
+
readonly ring: "#F5D060";
|
|
110
|
+
readonly sidebar: "#16181D";
|
|
111
|
+
readonly sidebarForeground: "#F7F7F8";
|
|
112
|
+
readonly sidebarBorder: "#1E2128";
|
|
113
|
+
readonly sidebarAccent: "#1E2128";
|
|
114
|
+
readonly sidebarAccentForeground: "#F7F7F8";
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
/** Alias trạng thái — success/warning/info trỏ vào màu phụ, destructive giữ nguyên 2 chế độ. */
|
|
118
|
+
declare const status: {
|
|
119
|
+
readonly light: {
|
|
120
|
+
readonly success: "#2EA56A";
|
|
121
|
+
readonly successForeground: "#FFFFFF";
|
|
122
|
+
readonly warning: "#F2C94C";
|
|
123
|
+
readonly warningForeground: "#0E1116";
|
|
124
|
+
readonly info: "#1F8A8B";
|
|
125
|
+
readonly infoForeground: "#FFFFFF";
|
|
126
|
+
readonly destructive: "#D64545";
|
|
127
|
+
readonly destructiveForeground: "#FFFFFF";
|
|
128
|
+
};
|
|
129
|
+
readonly dark: {
|
|
130
|
+
readonly success: "#4ADE80";
|
|
131
|
+
readonly successForeground: "#0E1116";
|
|
132
|
+
readonly warning: "#FACC15";
|
|
133
|
+
readonly warningForeground: "#0E1116";
|
|
134
|
+
readonly info: "#2DD4BF";
|
|
135
|
+
readonly infoForeground: "#0E1116";
|
|
136
|
+
readonly destructive: "#D64545";
|
|
137
|
+
readonly destructiveForeground: "#FFFFFF";
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
/** Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định. */
|
|
141
|
+
declare const chart: {
|
|
142
|
+
readonly light: readonly ["#E8B824", "#1F8A8B", "#0F1E3D", "#2EA56A", "#D64545"];
|
|
143
|
+
readonly dark: readonly ["#F5D060", "#2DD4BF", "#7EA2E8", "#4ADE80", "#F87171"];
|
|
144
|
+
};
|
|
145
|
+
/** Màu avatar chữ cái — chọn deterministic theo tên, lấy từ token nên luôn hợp tông brand. */
|
|
146
|
+
declare const avatarPalette: readonly ["#B88A00", "#5C4006", "#1F8A8B", "#0F1E3D", "#2EA56A", "#D64545", "#7E8290", "#3A3F4A"];
|
|
147
|
+
/** Số đo khung ứng dụng — mirror --sidebar-width / --sidebar-width-icon / --header-height. */
|
|
148
|
+
declare const layout: {
|
|
149
|
+
/** px — --header-height 3.5rem */
|
|
150
|
+
readonly headerHeight: 56;
|
|
151
|
+
/** px — --sidebar-width 16rem */
|
|
152
|
+
readonly sidebarWidth: 256;
|
|
153
|
+
/** px — --sidebar-width-icon 3.5rem */
|
|
154
|
+
readonly sidebarCollapsedWidth: 56;
|
|
155
|
+
/** px — --radius 0.375rem */
|
|
156
|
+
readonly radius: 6;
|
|
157
|
+
readonly breakpoints: {
|
|
158
|
+
readonly mobile: 768;
|
|
159
|
+
readonly tablet: 1024;
|
|
160
|
+
readonly desktop: 1280;
|
|
161
|
+
};
|
|
162
|
+
readonly pageSizes: readonly [20, 50, 100];
|
|
163
|
+
/** px — kích thước tối thiểu của vùng chạm trên mobile (WCAG 2.5.5) */
|
|
164
|
+
readonly minTapTarget: 44;
|
|
165
|
+
};
|
|
166
|
+
/** Font stack — app phải tự nạp Inter (subsets latin + vietnamese) và gán --font-inter. */
|
|
167
|
+
declare const typography: {
|
|
168
|
+
readonly fontSans: "\"Inter\", system-ui, sans-serif";
|
|
169
|
+
readonly fontMono: "\"Geist Mono\", ui-monospace, monospace";
|
|
170
|
+
};
|
|
171
|
+
type ThemeMode = "light" | "dark";
|
|
172
|
+
|
|
173
|
+
export { type ThemeMode, accent, ash, avatarPalette, brandNeutral, chart, gold, layout, semantic, status, typography };
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const gold = {
|
|
2
|
+
50: "#FBF5DC",
|
|
3
|
+
100: "#F8E89D",
|
|
4
|
+
200: "#F5D060",
|
|
5
|
+
/** primary ở light mode */
|
|
6
|
+
300: "#E8B824",
|
|
7
|
+
400: "#B88A00",
|
|
8
|
+
500: "#7A5800",
|
|
9
|
+
600: "#5C4006"
|
|
10
|
+
};
|
|
11
|
+
const brandNeutral = {
|
|
12
|
+
ink: "#0E1116",
|
|
13
|
+
inkSoft: "#3A3F4A",
|
|
14
|
+
paper: "#FBF7EE",
|
|
15
|
+
warm: "#EDE8D8"
|
|
16
|
+
};
|
|
17
|
+
const ash = {
|
|
18
|
+
50: "#F7F7F8",
|
|
19
|
+
100: "#EDEEF1",
|
|
20
|
+
300: "#C8CBD3",
|
|
21
|
+
500: "#7E8290",
|
|
22
|
+
700: "#3A3F4A",
|
|
23
|
+
900: "#16181D"
|
|
24
|
+
};
|
|
25
|
+
const accent = {
|
|
26
|
+
light: {
|
|
27
|
+
navy: "#0F1E3D",
|
|
28
|
+
teal: "#1F8A8B",
|
|
29
|
+
red: "#D64545",
|
|
30
|
+
green: "#2EA56A",
|
|
31
|
+
yellow: "#F2C94C"
|
|
32
|
+
},
|
|
33
|
+
dark: {
|
|
34
|
+
navy: "#0F1E3D",
|
|
35
|
+
teal: "#2DD4BF",
|
|
36
|
+
red: "#F87171",
|
|
37
|
+
green: "#4ADE80",
|
|
38
|
+
yellow: "#FACC15"
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const semantic = {
|
|
42
|
+
light: {
|
|
43
|
+
background: "#FFFFFF",
|
|
44
|
+
foreground: "#0E1116",
|
|
45
|
+
card: "#FFFFFF",
|
|
46
|
+
cardForeground: "#0E1116",
|
|
47
|
+
popover: "#FFFFFF",
|
|
48
|
+
popoverForeground: "#0E1116",
|
|
49
|
+
primary: "#E8B824",
|
|
50
|
+
primaryForeground: "#0E1116",
|
|
51
|
+
secondary: "#EDE8D8",
|
|
52
|
+
secondaryForeground: "#0E1116",
|
|
53
|
+
muted: "#F7F7F8",
|
|
54
|
+
mutedForeground: "#7E8290",
|
|
55
|
+
accent: "#FBF5DC",
|
|
56
|
+
accentForeground: "#0E1116",
|
|
57
|
+
destructive: "#D64545",
|
|
58
|
+
destructiveForeground: "#FFFFFF",
|
|
59
|
+
border: "#EDEEF1",
|
|
60
|
+
input: "#EDEEF1",
|
|
61
|
+
ring: "#E8B824",
|
|
62
|
+
sidebar: "#FAFAF9",
|
|
63
|
+
sidebarForeground: "#0E1116",
|
|
64
|
+
sidebarBorder: "#EDEEF1",
|
|
65
|
+
sidebarAccent: "#FBF5DC",
|
|
66
|
+
sidebarAccentForeground: "#0E1116"
|
|
67
|
+
},
|
|
68
|
+
dark: {
|
|
69
|
+
background: "#0E1116",
|
|
70
|
+
foreground: "#F7F7F8",
|
|
71
|
+
card: "#16181D",
|
|
72
|
+
cardForeground: "#F7F7F8",
|
|
73
|
+
popover: "#16181D",
|
|
74
|
+
popoverForeground: "#F7F7F8",
|
|
75
|
+
primary: "#F5D060",
|
|
76
|
+
primaryForeground: "#0E1116",
|
|
77
|
+
secondary: "#1E2128",
|
|
78
|
+
secondaryForeground: "#F7F7F8",
|
|
79
|
+
muted: "#1E2128",
|
|
80
|
+
mutedForeground: "#7E8290",
|
|
81
|
+
accent: "#1E2128",
|
|
82
|
+
accentForeground: "#F7F7F8",
|
|
83
|
+
destructive: "#D64545",
|
|
84
|
+
destructiveForeground: "#FFFFFF",
|
|
85
|
+
border: "#1E2128",
|
|
86
|
+
input: "#1E2128",
|
|
87
|
+
ring: "#F5D060",
|
|
88
|
+
sidebar: "#16181D",
|
|
89
|
+
sidebarForeground: "#F7F7F8",
|
|
90
|
+
sidebarBorder: "#1E2128",
|
|
91
|
+
sidebarAccent: "#1E2128",
|
|
92
|
+
sidebarAccentForeground: "#F7F7F8"
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const status = {
|
|
96
|
+
light: {
|
|
97
|
+
success: accent.light.green,
|
|
98
|
+
successForeground: "#FFFFFF",
|
|
99
|
+
warning: accent.light.yellow,
|
|
100
|
+
warningForeground: "#0E1116",
|
|
101
|
+
info: accent.light.teal,
|
|
102
|
+
infoForeground: "#FFFFFF",
|
|
103
|
+
destructive: "#D64545",
|
|
104
|
+
destructiveForeground: "#FFFFFF"
|
|
105
|
+
},
|
|
106
|
+
dark: {
|
|
107
|
+
success: accent.dark.green,
|
|
108
|
+
successForeground: "#0E1116",
|
|
109
|
+
warning: accent.dark.yellow,
|
|
110
|
+
warningForeground: "#0E1116",
|
|
111
|
+
info: accent.dark.teal,
|
|
112
|
+
infoForeground: "#0E1116",
|
|
113
|
+
destructive: "#D64545",
|
|
114
|
+
destructiveForeground: "#FFFFFF"
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const chart = {
|
|
118
|
+
light: [gold[300], accent.light.teal, accent.light.navy, accent.light.green, accent.light.red],
|
|
119
|
+
dark: [gold[200], accent.dark.teal, "#7EA2E8", accent.dark.green, accent.dark.red]
|
|
120
|
+
};
|
|
121
|
+
const avatarPalette = [
|
|
122
|
+
gold[400],
|
|
123
|
+
gold[600],
|
|
124
|
+
accent.light.teal,
|
|
125
|
+
accent.light.navy,
|
|
126
|
+
accent.light.green,
|
|
127
|
+
accent.light.red,
|
|
128
|
+
ash[500],
|
|
129
|
+
brandNeutral.inkSoft
|
|
130
|
+
];
|
|
131
|
+
const layout = {
|
|
132
|
+
/** px — --header-height 3.5rem */
|
|
133
|
+
headerHeight: 56,
|
|
134
|
+
/** px — --sidebar-width 16rem */
|
|
135
|
+
sidebarWidth: 256,
|
|
136
|
+
/** px — --sidebar-width-icon 3.5rem */
|
|
137
|
+
sidebarCollapsedWidth: 56,
|
|
138
|
+
/** px — --radius 0.375rem */
|
|
139
|
+
radius: 6,
|
|
140
|
+
breakpoints: { mobile: 768, tablet: 1024, desktop: 1280 },
|
|
141
|
+
pageSizes: [20, 50, 100],
|
|
142
|
+
/** px — kích thước tối thiểu của vùng chạm trên mobile (WCAG 2.5.5) */
|
|
143
|
+
minTapTarget: 44
|
|
144
|
+
};
|
|
145
|
+
const typography = {
|
|
146
|
+
fontSans: '"Inter", system-ui, sans-serif',
|
|
147
|
+
fontMono: '"Geist Mono", ui-monospace, monospace'
|
|
148
|
+
};
|
|
149
|
+
export {
|
|
150
|
+
accent,
|
|
151
|
+
ash,
|
|
152
|
+
avatarPalette,
|
|
153
|
+
brandNeutral,
|
|
154
|
+
chart,
|
|
155
|
+
gold,
|
|
156
|
+
layout,
|
|
157
|
+
semantic,
|
|
158
|
+
status,
|
|
159
|
+
typography
|
|
160
|
+
};
|
|
161
|
+
//# sourceMappingURL=tokens.js.map
|