@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/shell-context.tsx"],"sourcesContent":["\"use client\";\n\nimport NextLink from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore\n} from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport {\n type AppDescriptor,\n DEFAULT_SHELL_LABELS,\n type NavGroup,\n type ShellLabels,\n type ShellLinkComponent,\n type ShellLinkProps,\n type ShellNav,\n type ShellUser,\n type Workspace\n} from \"./types.js\";\nimport { getEntitledApps, resolveCurrentApp, toNavGroups } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Bọc `next/link` thay vì truyền thẳng: hình dạng props của NextLink rộng hơn\n * `ShellLinkProps` (href nhận cả UrlObject), bọc lại cho khớp type và để chỗ\n * thay adapter router khác về sau.\n */\nfunction DefaultShellLink({ href, children, ...props }: ShellLinkProps) {\n return (\n <NextLink href={href} {...props}>\n {children}\n </NextLink>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Context\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellContextValue {\n /* Trục 1 — workspace */\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /* Trục 2 — app */\n apps: AppDescriptor[];\n currentApp: AppDescriptor | null;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /* Điều hướng trong app hiện tại */\n nav: NavGroup[];\n pathname: string;\n LinkComponent: ShellLinkComponent;\n onNavigate?: (href: string) => void;\n\n /* Người dùng */\n user: ShellUser | null;\n onLogout?: () => void;\n\n /* Trạng thái khung */\n sidebarCollapsed: boolean;\n setSidebarCollapsed: (collapsed: boolean) => void;\n toggleSidebar: () => void;\n mobileMenuOpen: boolean;\n setMobileMenuOpen: (open: boolean) => void;\n commandPaletteOpen: boolean;\n setCommandPaletteOpen: (open: boolean) => void;\n appLauncherOpen: boolean;\n setAppLauncherOpen: (open: boolean) => void;\n\n /* Chuỗi hiển thị (đã trộn mặc định tiếng Việt với override của app) */\n labels: ShellLabels;\n}\n\nconst ShellContext = createContext<ShellContextValue | undefined>(undefined);\n\n/** Truy cập state của shell — phải gọi bên trong `<ShellProvider>`. */\nexport function useShell(): ShellContextValue {\n const context = useContext(ShellContext);\n if (!context) {\n throw new Error(\"useShell phải được dùng bên trong <ShellProvider> (hoặc <AppShell>).\");\n }\n return context;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Ghi nhớ trạng thái thu gọn sidebar\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Khoá localStorage theo workspace — mỗi workspace nhớ riêng trạng thái sidebar\n * (spec §7.6: KHÔNG dùng cookie `sidebar_state` của shadcn để tránh hai nguồn sự thật).\n */\nfunction sidebarStorageKey(workspaceId: string | undefined): string {\n return `comitor-sidebar:${workspaceId ?? \"default\"}`;\n}\n\nfunction readStoredCollapsed(workspaceId: string | undefined): boolean | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(sidebarStorageKey(workspaceId));\n if (raw === \"1\") return true;\n if (raw === \"0\") return false;\n return null;\n } catch {\n // Safari private mode / storage bị chặn — coi như chưa có giá trị lưu.\n return null;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Hook phụ trợ\n ──────────────────────────────────────────────────────────────────────────── */\n\nconst emptySubscribe = () => () => {};\n\n/**\n * `true` khi đang chạy trên macOS — để hiện `⌘K` thay vì `Ctrl+K`.\n * Dùng `useSyncExternalStore` với snapshot server = false: React tự khớp\n * server/client, không cần cờ `mounted` thủ công và không gây hydration mismatch.\n */\nexport function useIsMac(): boolean {\n return useSyncExternalStore(\n emptySubscribe,\n () => /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent),\n () => false\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Provider\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellProviderProps {\n /** Trục 1 — danh sách workspace của user. Rỗng/1 phần tử thì WorkspaceSwitcher tự ẩn danh sách. */\n workspaces?: Workspace[];\n /** Workspace đang mở; bỏ trống → lấy phần tử đầu. */\n currentWorkspaceId?: string;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /** Trục 2 — danh sách app của hệ sinh thái (kể cả app chưa mở khoá). */\n apps?: AppDescriptor[];\n /** App đang mở; bỏ trống → suy từ `pathname` (khớp prefix dài nhất). */\n currentAppId?: string;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /** Menu của app hiện tại — `NavItem[]` hoặc `NavGroup[]`. */\n nav?: ShellNav;\n /**\n * Ghi đè pathname (Storybook, app không-Next). Bỏ trống → dùng `usePathname()`\n * của next/navigation (trả `null` khi không có App Router → coi như \"\").\n */\n pathname?: string;\n /** Component link thay `next/link` (app không-Next). */\n LinkComponent?: ShellLinkComponent;\n /** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */\n onNavigate?: (href: string) => void;\n\n user?: ShellUser | null;\n onLogout?: () => void;\n\n /** Trạng thái thu gọn ban đầu (SSR). Sau khi mount, localStorage của workspace thắng. */\n defaultSidebarCollapsed?: boolean;\n /** Tắt ghi nhớ localStorage (Storybook, chế độ kiosk). */\n persistSidebarState?: boolean;\n /** Tắt bộ phím tắt toàn cục. */\n enableShortcuts?: boolean;\n\n /** Override chuỗi hiển thị (mặc định tiếng Việt). */\n labels?: Partial<ShellLabels>;\n\n children: ReactNode;\n}\n\n/**\n * Provider của app shell Comitor: giữ hai trục điều hướng (workspace × app),\n * trạng thái khung (sidebar/mobile/command palette/app launcher), phím tắt toàn cục,\n * và bọc sẵn `TooltipProvider`.\n *\n * KHÔNG dùng `useRouter()` của next/navigation: hook đó *ném lỗi* khi thiếu App Router\n * (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback\n * `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.\n */\nexport function ShellProvider({\n workspaces = [],\n currentWorkspaceId,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps = [],\n currentAppId,\n onAppSelect,\n onUpsell,\n nav,\n pathname: pathnameProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n persistSidebarState = true,\n enableShortcuts = true,\n labels: labelsProp,\n children\n}: ShellProviderProps) {\n // `usePathname()` trả null khi không có App Router → fallback chuỗi rỗng thay vì crash.\n const nextPathname = usePathname();\n const pathname = pathnameProp ?? nextPathname ?? \"\";\n\n const currentWorkspace = useMemo(\n () => workspaces.find((workspace) => workspace.id === currentWorkspaceId) ?? workspaces[0] ?? null,\n [workspaces, currentWorkspaceId]\n );\n\n const currentApp = useMemo(() => {\n if (currentAppId) return apps.find((app) => app.id === currentAppId) ?? null;\n return resolveCurrentApp(apps, pathname);\n }, [apps, currentAppId, pathname]);\n\n const navGroups = useMemo(() => toNavGroups(nav), [nav]);\n const labels = useMemo<ShellLabels>(() => ({ ...DEFAULT_SHELL_LABELS, ...labelsProp }), [labelsProp]);\n\n const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);\n const [appLauncherOpen, setAppLauncherOpen] = useState(false);\n\n // Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n useEffect(() => {\n if (!persistSidebarState) return;\n const stored = readStoredCollapsed(currentWorkspace?.id);\n if (stored !== null) setSidebarCollapsed(stored);\n }, [persistSidebarState, currentWorkspace?.id]);\n\n const persistCollapsed = useCallback(\n (collapsed: boolean) => {\n if (!persistSidebarState || typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(sidebarStorageKey(currentWorkspace?.id), collapsed ? \"1\" : \"0\");\n } catch {\n // Bỏ qua: không ghi được localStorage thì trạng thái chỉ sống trong phiên.\n }\n },\n [persistSidebarState, currentWorkspace?.id]\n );\n\n const applySidebarCollapsed = useCallback(\n (collapsed: boolean) => {\n setSidebarCollapsed(collapsed);\n persistCollapsed(collapsed);\n },\n [persistCollapsed]\n );\n\n const toggleSidebar = useCallback(() => {\n setSidebarCollapsed((prev) => {\n const next = !prev;\n persistCollapsed(next);\n return next;\n });\n }, [persistCollapsed]);\n\n // Phím tắt toàn cục (§7.4)\n useEffect(() => {\n if (!enableShortcuts) return;\n\n const entitledApps = getEntitledApps(apps);\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const target = event.target;\n // Đang gõ trong ô nhập → nhường phím cho ô nhập.\n const isTyping =\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable);\n\n if (event.key === \"Escape\") {\n setCommandPaletteOpen(false);\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n return;\n }\n\n if (isTyping) return;\n\n const mod = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n // Cmd/Ctrl + Shift + K — AppLauncher (phải xét TRƯỚC Cmd+K vì cùng phím \"k\")\n if (mod && event.shiftKey && key === \"k\") {\n event.preventDefault();\n setAppLauncherOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + K — Command Palette\n if (mod && !event.shiftKey && key === \"k\") {\n event.preventDefault();\n setCommandPaletteOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + B — thu gọn/mở sidebar\n if (mod && !event.shiftKey && key === \"b\") {\n event.preventDefault();\n toggleSidebar();\n return;\n }\n\n // Alt + 1…9 — nhảy tới app thứ n đã được cấp quyền.\n // Dùng `event.code` chứ không `event.key`: trên macOS Alt+1 cho ra \"¡\".\n if (event.altKey && !event.ctrlKey && !event.metaKey) {\n const match = /^Digit([1-9])$/.exec(event.code);\n if (match?.[1]) {\n const index = Number.parseInt(match[1], 10) - 1;\n const app = entitledApps[index];\n if (app) {\n event.preventDefault();\n onAppSelect?.(app);\n }\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [enableShortcuts, apps, onAppSelect, toggleSidebar]);\n\n // Đổi route → đóng mọi overlay đang mở.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chủ đích chạy theo MỌI lần đổi pathname\n useEffect(() => {\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n setCommandPaletteOpen(false);\n }, [pathname]);\n\n const value = useMemo<ShellContextValue>(\n () => ({\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n nav: navGroups,\n pathname,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n setSidebarCollapsed: applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n setMobileMenuOpen,\n commandPaletteOpen,\n setCommandPaletteOpen,\n appLauncherOpen,\n setAppLauncherOpen,\n labels\n }),\n [\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n navGroups,\n pathname,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n commandPaletteOpen,\n appLauncherOpen,\n labels\n ]\n );\n\n return (\n <ShellContext.Provider value={value}>\n <TooltipProvider delayDuration={200}>{children}</TooltipProvider>\n </ShellContext.Provider>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ShellLinkProps } from './types.js';
|
|
3
|
+
import 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface ShellNavLinkProps extends ShellLinkProps {
|
|
6
|
+
/** Link ra ngoài miền app — mở tab mới bằng thẻ `<a>` thường. */
|
|
7
|
+
external?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Link nội bộ của shell: lấy `LinkComponent` từ context (mặc định `next/link`;
|
|
11
|
+
* app không-Next truyền component router riêng) và bắn `onNavigate(href)` sau mỗi
|
|
12
|
+
* lần bấm — nhờ vậy panel/menu tự đóng mà không cần mỗi chỗ tự lo.
|
|
13
|
+
*
|
|
14
|
+
* Link ngoài đi thẳng bằng `<a target="_blank">`: router của app không nên
|
|
15
|
+
* (và thường không thể) xử lý URL ngoài miền.
|
|
16
|
+
*/
|
|
17
|
+
declare function ShellNavLink({ href, external, onClick, children, ...props }: ShellNavLinkProps): React.JSX.Element;
|
|
18
|
+
|
|
19
|
+
export { ShellNavLink, type ShellNavLinkProps };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useShell } from "./shell-context.js";
|
|
4
|
+
function ShellNavLink({ href, external, onClick, children, ...props }) {
|
|
5
|
+
const { LinkComponent, onNavigate } = useShell();
|
|
6
|
+
const handleClick = (event) => {
|
|
7
|
+
onClick?.(event);
|
|
8
|
+
if (!event.defaultPrevented) onNavigate?.(href);
|
|
9
|
+
};
|
|
10
|
+
if (external) {
|
|
11
|
+
return /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noreferrer noopener", onClick: handleClick, ...props, children });
|
|
12
|
+
}
|
|
13
|
+
return /* @__PURE__ */ jsx(LinkComponent, { href, onClick: handleClick, ...props, children });
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
ShellNavLink
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=shell-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/shell-link.tsx"],"sourcesContent":["\"use client\";\n\nimport type { MouseEvent } from \"react\";\nimport { useShell } from \"./shell-context.js\";\nimport type { ShellLinkProps } from \"./types.js\";\n\nexport interface ShellNavLinkProps extends ShellLinkProps {\n /** Link ra ngoài miền app — mở tab mới bằng thẻ `<a>` thường. */\n external?: boolean;\n}\n\n/**\n * Link nội bộ của shell: lấy `LinkComponent` từ context (mặc định `next/link`;\n * app không-Next truyền component router riêng) và bắn `onNavigate(href)` sau mỗi\n * lần bấm — nhờ vậy panel/menu tự đóng mà không cần mỗi chỗ tự lo.\n *\n * Link ngoài đi thẳng bằng `<a target=\"_blank\">`: router của app không nên\n * (và thường không thể) xử lý URL ngoài miền.\n */\nexport function ShellNavLink({ href, external, onClick, children, ...props }: ShellNavLinkProps) {\n const { LinkComponent, onNavigate } = useShell();\n\n const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {\n onClick?.(event);\n if (!event.defaultPrevented) onNavigate?.(href);\n };\n\n if (external) {\n return (\n <a href={href} target=\"_blank\" rel=\"noreferrer noopener\" onClick={handleClick} {...props}>\n {children}\n </a>\n );\n }\n\n return (\n <LinkComponent href={href} onClick={handleClick} {...props}>\n {children}\n </LinkComponent>\n );\n}\n"],"mappings":";AA6BM;AA1BN,SAAS,gBAAgB;AAgBlB,SAAS,aAAa,EAAE,MAAM,UAAU,SAAS,UAAU,GAAG,MAAM,GAAsB;AAC/F,QAAM,EAAE,eAAe,WAAW,IAAI,SAAS;AAE/C,QAAM,cAAc,CAAC,UAAyC;AAC5D,cAAU,KAAK;AACf,QAAI,CAAC,MAAM,iBAAkB,cAAa,IAAI;AAAA,EAChD;AAEA,MAAI,UAAU;AACZ,WACE,oBAAC,OAAE,MAAY,QAAO,UAAS,KAAI,uBAAsB,SAAS,aAAc,GAAG,OAChF,UACH;AAAA,EAEJ;AAEA,SACE,oBAAC,iBAAc,MAAY,SAAS,aAAc,GAAG,OAClD,UACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { NavGroup, NavItem } from './types.js';
|
|
3
|
+
import 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface SidebarGroupProps {
|
|
6
|
+
group: NavGroup;
|
|
7
|
+
/** Sidebar đang thu gọn — ẩn nhãn nhóm, chỉ còn dải icon. */
|
|
8
|
+
collapsed?: boolean;
|
|
9
|
+
/** Toàn bộ mục của app hiện tại — dùng cho active matching chính xác. */
|
|
10
|
+
siblings?: NavItem[];
|
|
11
|
+
/** Chuyển tiếp xuống từng mục — xem `SidebarMenuItemProps.onNavigate`. */
|
|
12
|
+
onNavigate?: () => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Nhóm mục sidebar kiểu Linear: nhãn nhỏ in hoa, bấm nhãn để thu gọn cả nhóm.
|
|
17
|
+
* Nhóm không có `label` (vd nhóm đầu tiên) render thẳng danh sách, không có phần đầu.
|
|
18
|
+
*/
|
|
19
|
+
declare function SidebarGroup({ group, collapsed, siblings, onNavigate, className }: SidebarGroupProps): React.JSX.Element;
|
|
20
|
+
|
|
21
|
+
export { SidebarGroup, type SidebarGroupProps };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronDown } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../components/ui/collapsible.js";
|
|
6
|
+
import { cn } from "../lib/cn.js";
|
|
7
|
+
import { SidebarMenuItem } from "./sidebar-menu-item.js";
|
|
8
|
+
function SidebarGroup({ group, collapsed = false, siblings = [], onNavigate, className }) {
|
|
9
|
+
const collapsible = group.collapsible ?? Boolean(group.label);
|
|
10
|
+
const [open, setOpen] = useState(!group.defaultCollapsed);
|
|
11
|
+
const items = /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: group.items.map((item) => /* @__PURE__ */ jsx(SidebarMenuItem, { item, collapsed, siblings, onNavigate }, item.id)) });
|
|
12
|
+
if (collapsed) {
|
|
13
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col items-center gap-0.5", className), role: "group", "aria-label": group.label, children: group.items.map((item) => /* @__PURE__ */ jsx(SidebarMenuItem, { item, collapsed: true, siblings, onNavigate }, item.id)) });
|
|
14
|
+
}
|
|
15
|
+
if (!group.label || !collapsible) {
|
|
16
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col", className), role: "group", "aria-label": group.label, children: [
|
|
17
|
+
group.label ? /* @__PURE__ */ jsx("p", { className: "px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-sidebar-foreground/45", children: group.label }) : null,
|
|
18
|
+
items
|
|
19
|
+
] });
|
|
20
|
+
}
|
|
21
|
+
return /* @__PURE__ */ jsxs(Collapsible, { open, onOpenChange: setOpen, className: cn("flex flex-col", className), children: [
|
|
22
|
+
/* @__PURE__ */ jsx(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
23
|
+
"button",
|
|
24
|
+
{
|
|
25
|
+
type: "button",
|
|
26
|
+
"aria-expanded": open,
|
|
27
|
+
className: cn(
|
|
28
|
+
"group flex w-full items-center gap-1 rounded-md px-2 py-1 text-[11px] font-semibold uppercase tracking-wide",
|
|
29
|
+
"text-sidebar-foreground/45 transition-colors duration-150 ease-out hover:text-sidebar-foreground/80"
|
|
30
|
+
),
|
|
31
|
+
children: [
|
|
32
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: cn("size-3 shrink-0 transition-transform duration-150", !open && "-rotate-90") }),
|
|
33
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: group.label })
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
) }),
|
|
37
|
+
/* @__PURE__ */ jsx(CollapsibleContent, { className: "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", children: /* @__PURE__ */ jsx("div", { className: "pt-0.5", children: items }) })
|
|
38
|
+
] });
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
SidebarGroup
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=sidebar-group.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/sidebar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../components/ui/collapsible.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { SidebarMenuItem } from \"./sidebar-menu-item.js\";\nimport type { NavGroup, NavItem } from \"./types.js\";\n\nexport interface SidebarGroupProps {\n group: NavGroup;\n /** Sidebar đang thu gọn — ẩn nhãn nhóm, chỉ còn dải icon. */\n collapsed?: boolean;\n /** Toàn bộ mục của app hiện tại — dùng cho active matching chính xác. */\n siblings?: NavItem[];\n /** Chuyển tiếp xuống từng mục — xem `SidebarMenuItemProps.onNavigate`. */\n onNavigate?: () => void;\n className?: string;\n}\n\n/**\n * Nhóm mục sidebar kiểu Linear: nhãn nhỏ in hoa, bấm nhãn để thu gọn cả nhóm.\n * Nhóm không có `label` (vd nhóm đầu tiên) render thẳng danh sách, không có phần đầu.\n */\nexport function SidebarGroup({ group, collapsed = false, siblings = [], onNavigate, className }: SidebarGroupProps) {\n const collapsible = group.collapsible ?? Boolean(group.label);\n const [open, setOpen] = useState(!group.defaultCollapsed);\n\n const items = (\n <div className=\"flex flex-col gap-0.5\">\n {group.items.map((item) => (\n <SidebarMenuItem key={item.id} item={item} collapsed={collapsed} siblings={siblings} onNavigate={onNavigate} />\n ))}\n </div>\n );\n\n // Thu gọn: nhãn không còn chỗ, chỉ giữ khoảng cách giữa các nhóm.\n if (collapsed) {\n return (\n <div className={cn(\"flex flex-col items-center gap-0.5\", className)} role=\"group\" aria-label={group.label}>\n {group.items.map((item) => (\n <SidebarMenuItem key={item.id} item={item} collapsed siblings={siblings} onNavigate={onNavigate} />\n ))}\n </div>\n );\n }\n\n if (!group.label || !collapsible) {\n return (\n <div className={cn(\"flex flex-col\", className)} role=\"group\" aria-label={group.label}>\n {group.label ? (\n <p className=\"px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-sidebar-foreground/45\">\n {group.label}\n </p>\n ) : null}\n {items}\n </div>\n );\n }\n\n return (\n <Collapsible open={open} onOpenChange={setOpen} className={cn(\"flex flex-col\", className)}>\n <CollapsibleTrigger asChild>\n <button\n type=\"button\"\n aria-expanded={open}\n className={cn(\n \"group flex w-full items-center gap-1 rounded-md px-2 py-1 text-[11px] font-semibold uppercase tracking-wide\",\n \"text-sidebar-foreground/45 transition-colors duration-150 ease-out hover:text-sidebar-foreground/80\"\n )}\n >\n <ChevronDown className={cn(\"size-3 shrink-0 transition-transform duration-150\", !open && \"-rotate-90\")} />\n <span className=\"truncate\">{group.label}</span>\n </button>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n <div className=\"pt-0.5\">{items}</div>\n </CollapsibleContent>\n </Collapsible>\n );\n}\n"],"mappings":";AA+BQ,cAkBF,YAlBE;AA7BR,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,aAAa,oBAAoB,0BAA0B;AACpE,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAkBzB,SAAS,aAAa,EAAE,OAAO,YAAY,OAAO,WAAW,CAAC,GAAG,YAAY,UAAU,GAAsB;AAClH,QAAM,cAAc,MAAM,eAAe,QAAQ,MAAM,KAAK;AAC5D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC,MAAM,gBAAgB;AAExD,QAAM,QACJ,oBAAC,SAAI,WAAU,yBACZ,gBAAM,MAAM,IAAI,CAAC,SAChB,oBAAC,mBAA8B,MAAY,WAAsB,UAAoB,cAA/D,KAAK,EAAkF,CAC9G,GACH;AAIF,MAAI,WAAW;AACb,WACE,oBAAC,SAAI,WAAW,GAAG,sCAAsC,SAAS,GAAG,MAAK,SAAQ,cAAY,MAAM,OACjG,gBAAM,MAAM,IAAI,CAAC,SAChB,oBAAC,mBAA8B,MAAY,WAAS,MAAC,UAAoB,cAAnD,KAAK,EAAsE,CAClG,GACH;AAAA,EAEJ;AAEA,MAAI,CAAC,MAAM,SAAS,CAAC,aAAa;AAChC,WACE,qBAAC,SAAI,WAAW,GAAG,iBAAiB,SAAS,GAAG,MAAK,SAAQ,cAAY,MAAM,OAC5E;AAAA,YAAM,QACL,oBAAC,OAAE,WAAU,0FACV,gBAAM,OACT,IACE;AAAA,MACH;AAAA,OACH;AAAA,EAEJ;AAEA,SACE,qBAAC,eAAY,MAAY,cAAc,SAAS,WAAW,GAAG,iBAAiB,SAAS,GACtF;AAAA,wBAAC,sBAAmB,SAAO,MACzB;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,eAAY,WAAW,GAAG,qDAAqD,CAAC,QAAQ,YAAY,GAAG;AAAA,UACxG,oBAAC,UAAK,WAAU,YAAY,gBAAM,OAAM;AAAA;AAAA;AAAA,IAC1C,GACF;AAAA,IACA,oBAAC,sBAAmB,WAAU,yGAC5B,8BAAC,SAAI,WAAU,UAAU,iBAAM,GACjC;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { NavItem } from './types.js';
|
|
3
|
+
import 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface SidebarMenuItemProps {
|
|
6
|
+
item: NavItem;
|
|
7
|
+
/** Sidebar đang thu gọn (chỉ icon) — chỉ áp dụng cho mục cấp 0. */
|
|
8
|
+
collapsed?: boolean;
|
|
9
|
+
/** Cấp lồng (0 = gốc). Cấp > 0 thụt lề và không có icon vuông. */
|
|
10
|
+
depth?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Tập mục để đối chiếu khớp CHÍNH XÁC pathname — nên truyền toàn bộ mục của app
|
|
13
|
+
* hiện tại. Thiếu nó, `/tasks` sẽ vẫn sáng khi đang ở `/tasks/inbox`.
|
|
14
|
+
*/
|
|
15
|
+
siblings?: NavItem[];
|
|
16
|
+
/** Gọi khi người dùng bấm vào một LINK thật (không gọi khi chỉ mở/đóng nhánh con) —
|
|
17
|
+
* `MobileMenu` dùng để tự đóng. */
|
|
18
|
+
onNavigate?: () => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Một mục menu sidebar: active matching thông minh, badge số, mục con `Collapsible`,
|
|
23
|
+
* khi sidebar thu gọn thì hiện `Tooltip` + chấm badge.
|
|
24
|
+
*/
|
|
25
|
+
declare function SidebarMenuItem({ item, collapsed, depth, siblings, onNavigate, className }: SidebarMenuItemProps): React.JSX.Element;
|
|
26
|
+
|
|
27
|
+
export { SidebarMenuItem, type SidebarMenuItemProps };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronRight } from "lucide-react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { Badge } from "../components/ui/badge.js";
|
|
6
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../components/ui/collapsible.js";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
|
|
8
|
+
import { cn } from "../lib/cn.js";
|
|
9
|
+
import { useShell } from "./shell-context.js";
|
|
10
|
+
import { ShellNavLink } from "./shell-link.js";
|
|
11
|
+
import { isNavBranchActive, isNavItemActive } from "./utils.js";
|
|
12
|
+
const BADGE_VARIANT = {
|
|
13
|
+
default: "default",
|
|
14
|
+
destructive: "destructive",
|
|
15
|
+
warning: "warning",
|
|
16
|
+
success: "success"
|
|
17
|
+
};
|
|
18
|
+
const FOCUS_RING = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring";
|
|
19
|
+
function SidebarMenuItem({
|
|
20
|
+
item,
|
|
21
|
+
collapsed = false,
|
|
22
|
+
depth = 0,
|
|
23
|
+
siblings = [],
|
|
24
|
+
onNavigate,
|
|
25
|
+
className
|
|
26
|
+
}) {
|
|
27
|
+
const { pathname } = useShell();
|
|
28
|
+
const active = isNavItemActive(item, pathname, siblings);
|
|
29
|
+
const branchActive = isNavBranchActive(item, pathname, siblings);
|
|
30
|
+
const hasChildren = Boolean(item.children?.length);
|
|
31
|
+
const [open, setOpen] = useState(branchActive);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (branchActive) setOpen(true);
|
|
34
|
+
}, [branchActive]);
|
|
35
|
+
const Icon = item.icon;
|
|
36
|
+
const disabled = Boolean(item.disabled);
|
|
37
|
+
const rowActive = active || branchActive && !hasChildren;
|
|
38
|
+
const rowClass = cn(
|
|
39
|
+
"group relative flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm",
|
|
40
|
+
"transition-colors duration-150 ease-out",
|
|
41
|
+
depth > 0 && "pl-8 text-[13px]",
|
|
42
|
+
rowActive ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "text-sidebar-foreground/70",
|
|
43
|
+
// Mục đã khoá thì KHÔNG gắn hover — đổi màu khi rê chuột là hứa hẹn bấm được.
|
|
44
|
+
!disabled && !rowActive && "hover:bg-sidebar-accent/60 hover:text-sidebar-foreground",
|
|
45
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
46
|
+
className
|
|
47
|
+
);
|
|
48
|
+
const iconClass = cn(
|
|
49
|
+
"size-4 shrink-0",
|
|
50
|
+
active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/55 group-hover:text-sidebar-foreground"
|
|
51
|
+
);
|
|
52
|
+
const activeMark = /* @__PURE__ */ jsx("span", { className: "absolute inset-y-1 left-0 w-0.5 rounded-r-full bg-sidebar-accent-foreground" });
|
|
53
|
+
const badge = item.badge === void 0 || item.badge === null || item.badge === "" ? null : /* @__PURE__ */ jsx(Badge, { variant: BADGE_VARIANT[item.badgeVariant ?? "default"], size: "sm", className: "ml-auto shrink-0", children: item.badge });
|
|
54
|
+
if (collapsed && depth === 0) {
|
|
55
|
+
const tileClass = cn(
|
|
56
|
+
"relative flex size-9 items-center justify-center rounded-md",
|
|
57
|
+
"transition-colors duration-150 ease-out",
|
|
58
|
+
active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/70",
|
|
59
|
+
!disabled && !active && "hover:bg-sidebar-accent/60 hover:text-sidebar-foreground",
|
|
60
|
+
disabled && "cursor-not-allowed opacity-50"
|
|
61
|
+
);
|
|
62
|
+
const tileBody = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
63
|
+
Icon ? /* @__PURE__ */ jsx(Icon, { className: "size-4" }) : null,
|
|
64
|
+
item.badge ? /* @__PURE__ */ jsx("span", { className: "absolute right-1.5 top-1.5 size-1.5 rounded-full bg-destructive" }) : null
|
|
65
|
+
] });
|
|
66
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
67
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: disabled ? (
|
|
68
|
+
// Mục khoá: KHÔNG render <a> — xem chú thích ở nhánh mục lá bên dưới.
|
|
69
|
+
/* @__PURE__ */ jsx(
|
|
70
|
+
"span",
|
|
71
|
+
{
|
|
72
|
+
role: "link",
|
|
73
|
+
"aria-disabled": "true",
|
|
74
|
+
"aria-label": item.label,
|
|
75
|
+
tabIndex: 0,
|
|
76
|
+
className: cn(tileClass, FOCUS_RING),
|
|
77
|
+
children: tileBody
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
) : /* @__PURE__ */ jsx(
|
|
81
|
+
ShellNavLink,
|
|
82
|
+
{
|
|
83
|
+
href: item.href,
|
|
84
|
+
external: item.external,
|
|
85
|
+
onClick: () => onNavigate?.(),
|
|
86
|
+
"aria-current": active ? "page" : void 0,
|
|
87
|
+
"aria-label": item.label,
|
|
88
|
+
className: tileClass,
|
|
89
|
+
children: tileBody
|
|
90
|
+
}
|
|
91
|
+
) }),
|
|
92
|
+
/* @__PURE__ */ jsxs(TooltipContent, { side: "right", className: "flex items-center gap-2", children: [
|
|
93
|
+
/* @__PURE__ */ jsx("span", { children: item.label }),
|
|
94
|
+
item.badge ? /* @__PURE__ */ jsx("span", { className: "text-xs opacity-70", children: item.badge }) : null
|
|
95
|
+
] })
|
|
96
|
+
] });
|
|
97
|
+
}
|
|
98
|
+
if (!hasChildren) {
|
|
99
|
+
const leafBody = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
100
|
+
active ? activeMark : null,
|
|
101
|
+
Icon ? /* @__PURE__ */ jsx(Icon, { className: iconClass }) : null,
|
|
102
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: item.label }),
|
|
103
|
+
badge
|
|
104
|
+
] });
|
|
105
|
+
if (disabled) {
|
|
106
|
+
return /* @__PURE__ */ jsx("span", { role: "link", "aria-disabled": "true", tabIndex: 0, className: cn(rowClass, FOCUS_RING), children: leafBody });
|
|
107
|
+
}
|
|
108
|
+
return /* @__PURE__ */ jsx(
|
|
109
|
+
ShellNavLink,
|
|
110
|
+
{
|
|
111
|
+
href: item.href,
|
|
112
|
+
external: item.external,
|
|
113
|
+
onClick: () => onNavigate?.(),
|
|
114
|
+
"aria-current": active ? "page" : void 0,
|
|
115
|
+
className: rowClass,
|
|
116
|
+
children: leafBody
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return /* @__PURE__ */ jsxs(Collapsible, { open, onOpenChange: setOpen, disabled, children: [
|
|
121
|
+
/* @__PURE__ */ jsx(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsxs("button", { type: "button", disabled, className: rowClass, "aria-expanded": open, children: [
|
|
122
|
+
branchActive ? activeMark : null,
|
|
123
|
+
Icon ? /* @__PURE__ */ jsx(Icon, { className: iconClass }) : null,
|
|
124
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: item.label }),
|
|
125
|
+
badge,
|
|
126
|
+
/* @__PURE__ */ jsx(
|
|
127
|
+
ChevronRight,
|
|
128
|
+
{
|
|
129
|
+
className: cn(
|
|
130
|
+
"ml-auto size-3.5 shrink-0 text-sidebar-foreground/40 transition-transform duration-150",
|
|
131
|
+
badge && "ml-1",
|
|
132
|
+
open && "rotate-90"
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
] }) }),
|
|
137
|
+
/* @__PURE__ */ jsx(CollapsibleContent, { className: "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", children: /* @__PURE__ */ jsx("div", { className: "mt-0.5 flex flex-col gap-0.5", children: item.children?.map((child) => /* @__PURE__ */ jsx(
|
|
138
|
+
SidebarMenuItem,
|
|
139
|
+
{
|
|
140
|
+
item: child,
|
|
141
|
+
depth: depth + 1,
|
|
142
|
+
siblings,
|
|
143
|
+
onNavigate
|
|
144
|
+
},
|
|
145
|
+
child.id
|
|
146
|
+
)) }) })
|
|
147
|
+
] });
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
SidebarMenuItem
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=sidebar-menu-item.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/sidebar-menu-item.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronRight } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../components/ui/collapsible.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { NavBadgeVariant, NavItem } from \"./types.js\";\nimport { isNavBranchActive, isNavItemActive } from \"./utils.js\";\n\n/** Variant badge của NavItem → variant của primitive Badge (tên trùng nhau, map tường minh cho an toàn). */\nconst BADGE_VARIANT: Record<NavBadgeVariant, \"default\" | \"destructive\" | \"warning\" | \"success\"> = {\n default: \"default\",\n destructive: \"destructive\",\n warning: \"warning\",\n success: \"success\"\n};\n\n/**\n * Vòng focus cho mục đã khoá.\n *\n * Mục khoá vẫn nằm trong tab order (`tabIndex={0}` + `aria-disabled`) theo đúng khuyến nghị\n * của ARIA APG: người dùng bàn phím phải *biết* mục đó tồn tại và đang bị khoá, thay vì mục\n * lặng lẽ biến mất. Không có href và không có handler nên Enter/click không đi đâu cả.\n */\nconst FOCUS_RING = \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\";\n\nexport interface SidebarMenuItemProps {\n item: NavItem;\n /** Sidebar đang thu gọn (chỉ icon) — chỉ áp dụng cho mục cấp 0. */\n collapsed?: boolean;\n /** Cấp lồng (0 = gốc). Cấp > 0 thụt lề và không có icon vuông. */\n depth?: number;\n /**\n * Tập mục để đối chiếu khớp CHÍNH XÁC pathname — nên truyền toàn bộ mục của app\n * hiện tại. Thiếu nó, `/tasks` sẽ vẫn sáng khi đang ở `/tasks/inbox`.\n */\n siblings?: NavItem[];\n /** Gọi khi người dùng bấm vào một LINK thật (không gọi khi chỉ mở/đóng nhánh con) —\n * `MobileMenu` dùng để tự đóng. */\n onNavigate?: () => void;\n className?: string;\n}\n\n/**\n * Một mục menu sidebar: active matching thông minh, badge số, mục con `Collapsible`,\n * khi sidebar thu gọn thì hiện `Tooltip` + chấm badge.\n */\nexport function SidebarMenuItem({\n item,\n collapsed = false,\n depth = 0,\n siblings = [],\n onNavigate,\n className\n}: SidebarMenuItemProps) {\n const { pathname } = useShell();\n\n const active = isNavItemActive(item, pathname, siblings);\n const branchActive = isNavBranchActive(item, pathname, siblings);\n const hasChildren = Boolean(item.children?.length);\n\n const [open, setOpen] = useState(branchActive);\n\n // Điều hướng tới một mục con → tự mở nhánh chứa nó.\n useEffect(() => {\n if (branchActive) setOpen(true);\n }, [branchActive]);\n\n const Icon = item.icon;\n const disabled = Boolean(item.disabled);\n const rowActive = active || (branchActive && !hasChildren);\n\n const rowClass = cn(\n \"group relative flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n depth > 0 && \"pl-8 text-[13px]\",\n rowActive ? \"bg-sidebar-accent font-medium text-sidebar-accent-foreground\" : \"text-sidebar-foreground/70\",\n // Mục đã khoá thì KHÔNG gắn hover — đổi màu khi rê chuột là hứa hẹn bấm được.\n !disabled && !rowActive && \"hover:bg-sidebar-accent/60 hover:text-sidebar-foreground\",\n disabled && \"cursor-not-allowed opacity-50\",\n className\n );\n\n /**\n * Màu icon/vạch active dùng `--sidebar-accent-foreground`, KHÔNG dùng `--app-accent`.\n *\n * Accent mặc định (= `--primary` = gold #E8B824) chỉ đạt **1,70:1** trên nền hàng active\n * (`--sidebar-accent` #FBF5DC) ở light — dưới ngưỡng 3:1 của WCAG 1.4.11 cho thành phần\n * phi-văn-bản, nên vạch chỉ mục active gần như vô hình đúng lúc nó phải nói lên trạng thái.\n * (Ở dark thì gold #F5D060 trên #1E2128 đạt 10,8:1 — chỉ light hỏng.)\n * `--sidebar-accent-foreground` đạt 17,3:1 (light) và 15,1:1 (dark), lại là token semantic\n * nên app đổi theme vẫn đúng. Cùng cách xử lý mà `stat-card.tsx` đã chọn (`text-foreground`).\n *\n * Muốn vạch active mang màu riêng của app trở lại thì phải sửa ở tầng token: cho\n * `--app-accent` một bậc đủ tối ở light (vd gold-500 #7A5800 = 6,0:1), rồi mới quay lại\n * `bg-app-accent` ở đây.\n */\n const iconClass = cn(\n \"size-4 shrink-0\",\n active ? \"text-sidebar-accent-foreground\" : \"text-sidebar-foreground/55 group-hover:text-sidebar-foreground\"\n );\n\n /** Vạch dọc chỉ mục đang mở, nằm sát mép trái hàng. */\n const activeMark = <span className=\"absolute inset-y-1 left-0 w-0.5 rounded-r-full bg-sidebar-accent-foreground\" />;\n\n const badge =\n item.badge === undefined || item.badge === null || item.badge === \"\" ? null : (\n <Badge variant={BADGE_VARIANT[item.badgeVariant ?? \"default\"]} size=\"sm\" className=\"ml-auto shrink-0\">\n {item.badge}\n </Badge>\n );\n\n /* ── Sidebar thu gọn: icon vuông + tooltip ─────────────────────────────── */\n if (collapsed && depth === 0) {\n const tileClass = cn(\n \"relative flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out\",\n active ? \"bg-sidebar-accent text-sidebar-accent-foreground\" : \"text-sidebar-foreground/70\",\n !disabled && !active && \"hover:bg-sidebar-accent/60 hover:text-sidebar-foreground\",\n disabled && \"cursor-not-allowed opacity-50\"\n );\n\n const tileBody = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n {/* Thu gọn không đủ chỗ cho số → chỉ chấm báo có nội dung mới */}\n {item.badge ? <span className=\"absolute right-1.5 top-1.5 size-1.5 rounded-full bg-destructive\" /> : null}\n </>\n );\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n {disabled ? (\n // Mục khoá: KHÔNG render <a> — xem chú thích ở nhánh mục lá bên dưới.\n <span\n role=\"link\"\n aria-disabled=\"true\"\n aria-label={item.label}\n tabIndex={0}\n className={cn(tileClass, FOCUS_RING)}\n >\n {tileBody}\n </span>\n ) : (\n <ShellNavLink\n href={item.href}\n external={item.external}\n onClick={() => onNavigate?.()}\n aria-current={active ? \"page\" : undefined}\n aria-label={item.label}\n className={tileClass}\n >\n {tileBody}\n </ShellNavLink>\n )}\n </TooltipTrigger>\n <TooltipContent side=\"right\" className=\"flex items-center gap-2\">\n <span>{item.label}</span>\n {item.badge ? <span className=\"text-xs opacity-70\">{item.badge}</span> : null}\n </TooltipContent>\n </Tooltip>\n );\n }\n\n /* ── Mục lá ────────────────────────────────────────────────────────────── */\n if (!hasChildren) {\n const leafBody = (\n <>\n {active ? activeMark : null}\n {Icon ? <Icon className={iconClass} /> : null}\n <span className=\"truncate\">{item.label}</span>\n {badge}\n </>\n );\n\n /*\n * Mục khoá phải dựng bằng <span role=\"link\" aria-disabled>, KHÔNG phải <a> tô mờ:\n * `pointer-events-none` chỉ chặn CHUỘT — người dùng bàn phím vẫn Tab tới thẻ <a> và\n * Enter vẫn điều hướng, lại không có tín hiệu nào cho trình đọc màn hình. Bỏ hẳn href\n * là cách duy nhất chặn được cả hai đường.\n */\n if (disabled) {\n return (\n <span role=\"link\" aria-disabled=\"true\" tabIndex={0} className={cn(rowClass, FOCUS_RING)}>\n {leafBody}\n </span>\n );\n }\n\n return (\n <ShellNavLink\n href={item.href}\n external={item.external}\n // Phải gọi `onNavigate` ở ĐÂY nữa, không chỉ ở nhánh thu gọn: `MobileMenu` truyền\n // `onNavigate={closeMenu}` cho mọi mục, thiếu nó thì bấm một mục điều hướng thường\n // trong menu mobile mà menu không đóng.\n onClick={() => onNavigate?.()}\n aria-current={active ? \"page\" : undefined}\n className={rowClass}\n >\n {leafBody}\n </ShellNavLink>\n );\n }\n\n /* ── Mục có con ────────────────────────────────────────────────────────── */\n return (\n <Collapsible open={open} onOpenChange={setOpen} disabled={disabled}>\n <CollapsibleTrigger asChild>\n {/* `disabled` thật trên <button>: bỏ khỏi tab order và chặn cả Enter/Space,\n khác hẳn `pointer-events-none` vốn chỉ chặn chuột. */}\n <button type=\"button\" disabled={disabled} className={rowClass} aria-expanded={open}>\n {branchActive ? activeMark : null}\n {Icon ? <Icon className={iconClass} /> : null}\n <span className=\"truncate\">{item.label}</span>\n {badge}\n <ChevronRight\n className={cn(\n \"ml-auto size-3.5 shrink-0 text-sidebar-foreground/40 transition-transform duration-150\",\n badge && \"ml-1\",\n open && \"rotate-90\"\n )}\n />\n </button>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n <div className=\"mt-0.5 flex flex-col gap-0.5\">\n {item.children?.map((child) => (\n <SidebarMenuItem\n key={child.id}\n item={child}\n depth={depth + 1}\n siblings={siblings}\n onNavigate={onNavigate}\n />\n ))}\n </div>\n </CollapsibleContent>\n </Collapsible>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface SidebarProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Slot ngay dưới WorkspaceSwitcher (vd nút "Tạo mới", ô tìm nhanh). */
|
|
7
|
+
header?: ReactNode;
|
|
8
|
+
/** Slot đáy sidebar (dung lượng gói, "Mời thành viên", phiên bản app…). */
|
|
9
|
+
footer?: ReactNode;
|
|
10
|
+
/** Ẩn WorkspaceSwitcher ở đỉnh — dùng khi app đặt bộ chuyển workspace ở header. */
|
|
11
|
+
showWorkspaceSwitcher?: boolean;
|
|
12
|
+
/** Ẩn nút thu gọn ở đáy (khi header đã có nút này). */
|
|
13
|
+
showCollapseButton?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Sidebar desktop: 256px (`--sidebar-width`) / 56px khi thu gọn (`--sidebar-width-icon`).
|
|
17
|
+
* Đỉnh: `WorkspaceSwitcher` (trục 1). Giữa: menu của app hiện tại. Đáy: slot footer.
|
|
18
|
+
*
|
|
19
|
+
* Ẩn hẳn dưới `md` — màn hình hẹp dùng `MobileMenu`.
|
|
20
|
+
*
|
|
21
|
+
* KHÔNG dùng `ui/sidebar.tsx` của shadcn (spec §7.6): primitive đó quản state bằng
|
|
22
|
+
* cookie + provider riêng, sẽ thành nguồn sự thật thứ hai bên cạnh `ShellProvider`.
|
|
23
|
+
*/
|
|
24
|
+
declare function Sidebar({ className, header, footer, showWorkspaceSwitcher, showCollapseButton }: SidebarProps): React.JSX.Element;
|
|
25
|
+
|
|
26
|
+
export { Sidebar, type SidebarProps };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { useShell } from "./shell-context.js";
|
|
7
|
+
import { SidebarGroup } from "./sidebar-group.js";
|
|
8
|
+
import { appAccentStyle, flattenNavItems } from "./utils.js";
|
|
9
|
+
import { WorkspaceSwitcher } from "./workspace-switcher.js";
|
|
10
|
+
function Sidebar({
|
|
11
|
+
className,
|
|
12
|
+
header,
|
|
13
|
+
footer,
|
|
14
|
+
showWorkspaceSwitcher = true,
|
|
15
|
+
showCollapseButton = true
|
|
16
|
+
}) {
|
|
17
|
+
const { nav, currentApp, workspaces, sidebarCollapsed, toggleSidebar, labels } = useShell();
|
|
18
|
+
const allItems = flattenNavItems(nav);
|
|
19
|
+
return /* @__PURE__ */ jsxs(
|
|
20
|
+
"aside",
|
|
21
|
+
{
|
|
22
|
+
"data-collapsed": sidebarCollapsed || void 0,
|
|
23
|
+
style: appAccentStyle(currentApp),
|
|
24
|
+
className: cn(
|
|
25
|
+
"hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex",
|
|
26
|
+
"transition-[width] duration-200 ease-out",
|
|
27
|
+
sidebarCollapsed ? "w-sidebar-icon" : "w-sidebar",
|
|
28
|
+
className
|
|
29
|
+
),
|
|
30
|
+
children: [
|
|
31
|
+
showWorkspaceSwitcher && workspaces.length > 0 ? /* @__PURE__ */ jsx(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
className: cn(
|
|
35
|
+
"flex h-header shrink-0 items-center border-b border-sidebar-border",
|
|
36
|
+
sidebarCollapsed ? "justify-center px-1" : "px-2"
|
|
37
|
+
),
|
|
38
|
+
children: /* @__PURE__ */ jsx(WorkspaceSwitcher, { collapsed: sidebarCollapsed })
|
|
39
|
+
}
|
|
40
|
+
) : null,
|
|
41
|
+
header ? /* @__PURE__ */ jsx("div", { className: cn("shrink-0", sidebarCollapsed ? "px-1.5 pt-2" : "px-2 pt-2"), children: header }) : null,
|
|
42
|
+
/* @__PURE__ */ jsx(
|
|
43
|
+
"nav",
|
|
44
|
+
{
|
|
45
|
+
"aria-label": labels.navigationLabel,
|
|
46
|
+
className: cn("flex-1 overflow-y-auto scrollbar-thin py-3", sidebarCollapsed ? "px-1.5" : "px-2"),
|
|
47
|
+
children: /* @__PURE__ */ jsx("div", { className: cn("flex flex-col", sidebarCollapsed ? "gap-2" : "gap-4"), children: nav.map((group) => /* @__PURE__ */ jsx(SidebarGroup, { group, collapsed: sidebarCollapsed, siblings: allItems }, group.id)) })
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
footer || showCollapseButton ? /* @__PURE__ */ jsxs(
|
|
51
|
+
"div",
|
|
52
|
+
{
|
|
53
|
+
className: cn(
|
|
54
|
+
"shrink-0 border-t border-sidebar-border",
|
|
55
|
+
sidebarCollapsed ? "flex flex-col items-center gap-1 p-1.5" : "p-2"
|
|
56
|
+
),
|
|
57
|
+
children: [
|
|
58
|
+
!sidebarCollapsed && footer ? /* @__PURE__ */ jsx("div", { className: "mb-1", children: footer }) : null,
|
|
59
|
+
showCollapseButton ? /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
60
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
61
|
+
"button",
|
|
62
|
+
{
|
|
63
|
+
type: "button",
|
|
64
|
+
onClick: toggleSidebar,
|
|
65
|
+
"aria-label": sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar,
|
|
66
|
+
className: cn(
|
|
67
|
+
"flex items-center gap-2 rounded-md text-sidebar-foreground/60",
|
|
68
|
+
"transition-colors duration-150 ease-out hover:bg-sidebar-accent hover:text-sidebar-foreground",
|
|
69
|
+
sidebarCollapsed ? "size-9 justify-center" : "h-8 w-full px-2 text-xs"
|
|
70
|
+
),
|
|
71
|
+
children: sidebarCollapsed ? /* @__PURE__ */ jsx(PanelLeftOpen, { className: "size-4" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
72
|
+
/* @__PURE__ */ jsx(PanelLeftClose, { className: "size-4" }),
|
|
73
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: labels.collapseSidebar })
|
|
74
|
+
] })
|
|
75
|
+
}
|
|
76
|
+
) }),
|
|
77
|
+
/* @__PURE__ */ jsxs(TooltipContent, { side: "right", children: [
|
|
78
|
+
sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar,
|
|
79
|
+
" \xB7 Ctrl/\u2318 + B"
|
|
80
|
+
] })
|
|
81
|
+
] }) : null
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
) : null
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
Sidebar
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=sidebar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/sidebar.tsx"],"sourcesContent":["\"use client\";\n\nimport { PanelLeftClose, PanelLeftOpen } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface SidebarProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher (vd nút \"Tạo mới\", ô tìm nhanh). */\n header?: ReactNode;\n /** Slot đáy sidebar (dung lượng gói, \"Mời thành viên\", phiên bản app…). */\n footer?: ReactNode;\n /** Ẩn WorkspaceSwitcher ở đỉnh — dùng khi app đặt bộ chuyển workspace ở header. */\n showWorkspaceSwitcher?: boolean;\n /** Ẩn nút thu gọn ở đáy (khi header đã có nút này). */\n showCollapseButton?: boolean;\n}\n\n/**\n * Sidebar desktop: 256px (`--sidebar-width`) / 56px khi thu gọn (`--sidebar-width-icon`).\n * Đỉnh: `WorkspaceSwitcher` (trục 1). Giữa: menu của app hiện tại. Đáy: slot footer.\n *\n * Ẩn hẳn dưới `md` — màn hình hẹp dùng `MobileMenu`.\n *\n * KHÔNG dùng `ui/sidebar.tsx` của shadcn (spec §7.6): primitive đó quản state bằng\n * cookie + provider riêng, sẽ thành nguồn sự thật thứ hai bên cạnh `ShellProvider`.\n */\nexport function Sidebar({\n className,\n header,\n footer,\n showWorkspaceSwitcher = true,\n showCollapseButton = true\n}: SidebarProps) {\n const { nav, currentApp, workspaces, sidebarCollapsed, toggleSidebar, labels } = useShell();\n\n // Đối chiếu khớp chính xác trên TOÀN BỘ mục của app, không chỉ mục cùng nhóm:\n // \"/tasks\" nằm nhóm A và \"/tasks/inbox\" nhóm B thì vẫn phải sáng đúng một mục.\n const allItems = flattenNavItems(nav);\n\n return (\n <aside\n data-collapsed={sidebarCollapsed || undefined}\n style={appAccentStyle(currentApp)}\n className={cn(\n \"hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex\",\n \"transition-[width] duration-200 ease-out\",\n sidebarCollapsed ? \"w-sidebar-icon\" : \"w-sidebar\",\n className\n )}\n >\n {/* Không có workspace nào (app đơn tổ chức) thì bỏ hẳn dải đỉnh, không để lại ô trống. */}\n {showWorkspaceSwitcher && workspaces.length > 0 ? (\n <div\n className={cn(\n \"flex h-header shrink-0 items-center border-b border-sidebar-border\",\n sidebarCollapsed ? \"justify-center px-1\" : \"px-2\"\n )}\n >\n <WorkspaceSwitcher collapsed={sidebarCollapsed} />\n </div>\n ) : null}\n\n {header ? <div className={cn(\"shrink-0\", sidebarCollapsed ? \"px-1.5 pt-2\" : \"px-2 pt-2\")}>{header}</div> : null}\n\n <nav\n aria-label={labels.navigationLabel}\n className={cn(\"flex-1 overflow-y-auto scrollbar-thin py-3\", sidebarCollapsed ? \"px-1.5\" : \"px-2\")}\n >\n <div className={cn(\"flex flex-col\", sidebarCollapsed ? \"gap-2\" : \"gap-4\")}>\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} collapsed={sidebarCollapsed} siblings={allItems} />\n ))}\n </div>\n </nav>\n\n {footer || showCollapseButton ? (\n <div\n className={cn(\n \"shrink-0 border-t border-sidebar-border\",\n sidebarCollapsed ? \"flex flex-col items-center gap-1 p-1.5\" : \"p-2\"\n )}\n >\n {!sidebarCollapsed && footer ? <div className=\"mb-1\">{footer}</div> : null}\n\n {showCollapseButton ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n className={cn(\n \"flex items-center gap-2 rounded-md text-sidebar-foreground/60\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent hover:text-sidebar-foreground\",\n sidebarCollapsed ? \"size-9 justify-center\" : \"h-8 w-full px-2 text-xs\"\n )}\n >\n {sidebarCollapsed ? (\n <PanelLeftOpen className=\"size-4\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4\" />\n <span className=\"truncate\">{labels.collapseSidebar}</span>\n </>\n )}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"right\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · Ctrl/⌘ + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n ) : null}\n </aside>\n );\n}\n"],"mappings":";AAgEU,SA0CU,UA1CV,KA0CU,YA1CV;AA9DV,SAAS,gBAAgB,qBAAqB;AAE9C,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAuB3B,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,wBAAwB;AAAA,EACxB,qBAAqB;AACvB,GAAiB;AACf,QAAM,EAAE,KAAK,YAAY,YAAY,kBAAkB,eAAe,OAAO,IAAI,SAAS;AAI1F,QAAM,WAAW,gBAAgB,GAAG;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,kBAAgB,oBAAoB;AAAA,MACpC,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,mBAAmB,mBAAmB;AAAA,QACtC;AAAA,MACF;AAAA,MAGC;AAAA,iCAAyB,WAAW,SAAS,IAC5C;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,wBAAwB;AAAA,YAC7C;AAAA,YAEA,8BAAC,qBAAkB,WAAW,kBAAkB;AAAA;AAAA,QAClD,IACE;AAAA,QAEH,SAAS,oBAAC,SAAI,WAAW,GAAG,YAAY,mBAAmB,gBAAgB,WAAW,GAAI,kBAAO,IAAS;AAAA,QAE3G;AAAA,UAAC;AAAA;AAAA,YACC,cAAY,OAAO;AAAA,YACnB,WAAW,GAAG,8CAA8C,mBAAmB,WAAW,MAAM;AAAA,YAEhG,8BAAC,SAAI,WAAW,GAAG,iBAAiB,mBAAmB,UAAU,OAAO,GACrE,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,WAAW,kBAAkB,UAAU,YAA/D,MAAM,EAAmE,CAC7F,GACH;AAAA;AAAA,QACF;AAAA,QAEC,UAAU,qBACT;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,2CAA2C;AAAA,YAChE;AAAA,YAEC;AAAA,eAAC,oBAAoB,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,cAErE,qBACC,qBAAC,WACC;AAAA,oCAAC,kBAAe,SAAO,MACrB;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS;AAAA,oBACT,cAAY,mBAAmB,OAAO,gBAAgB,OAAO;AAAA,oBAC7D,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA,mBAAmB,0BAA0B;AAAA,oBAC/C;AAAA,oBAEC,6BACC,oBAAC,iBAAc,WAAU,UAAS,IAElC,iCACE;AAAA,0CAAC,kBAAe,WAAU,UAAS;AAAA,sBACnC,oBAAC,UAAK,WAAU,YAAY,iBAAO,iBAAgB;AAAA,uBACrD;AAAA;AAAA,gBAEJ,GACF;AAAA,gBACA,qBAAC,kBAAe,MAAK,SAClB;AAAA,qCAAmB,OAAO,gBAAgB,OAAO;AAAA,kBAAgB;AAAA,mBACpE;AAAA,iBACF,IACE;AAAA;AAAA;AAAA,QACN,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ThemeProviderProps as ThemeProviderProps$1 } from 'next-themes';
|
|
3
|
+
|
|
4
|
+
type ThemeProviderProps = ThemeProviderProps$1;
|
|
5
|
+
/**
|
|
6
|
+
* Bọc `next-themes` với cấu hình chuẩn Comitor:
|
|
7
|
+
* `attribute="class"` (khớp `@custom-variant dark (&:is(.dark *))` trong styles.css),
|
|
8
|
+
* mặc định theo hệ điều hành, khoá lưu `comitor-theme`.
|
|
9
|
+
*
|
|
10
|
+
* Đặt ở **root layout** của app, NGOÀI `<AppShell>`. Mọi tham số đều override được.
|
|
11
|
+
*
|
|
12
|
+
* ⚠ `next-themes` là **peer optional có context singleton**: app phải tự cài, và chỉ
|
|
13
|
+
* được có MỘT bản trong node_modules — hai bản khác nhau thì `useTheme()` của app đọc
|
|
14
|
+
* provider khác với của package, đổi giao diện sẽ không ăn.
|
|
15
|
+
*/
|
|
16
|
+
declare function ThemeProvider({ attribute, defaultTheme, enableSystem, disableTransitionOnChange, storageKey, children, ...props }: ThemeProviderProps): React.JSX.Element;
|
|
17
|
+
|
|
18
|
+
export { ThemeProvider, type ThemeProviderProps };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
4
|
+
function ThemeProvider({
|
|
5
|
+
attribute = "class",
|
|
6
|
+
defaultTheme = "system",
|
|
7
|
+
enableSystem = true,
|
|
8
|
+
disableTransitionOnChange = true,
|
|
9
|
+
storageKey = "comitor-theme",
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}) {
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
NextThemesProvider,
|
|
15
|
+
{
|
|
16
|
+
attribute,
|
|
17
|
+
defaultTheme,
|
|
18
|
+
enableSystem,
|
|
19
|
+
disableTransitionOnChange,
|
|
20
|
+
storageKey,
|
|
21
|
+
...props,
|
|
22
|
+
children
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
ThemeProvider
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=theme-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/theme-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { ThemeProvider as NextThemesProvider, type ThemeProviderProps as NextThemesProviderProps } from \"next-themes\";\n\nexport type ThemeProviderProps = NextThemesProviderProps;\n\n/**\n * Bọc `next-themes` với cấu hình chuẩn Comitor:\n * `attribute=\"class\"` (khớp `@custom-variant dark (&:is(.dark *))` trong styles.css),\n * mặc định theo hệ điều hành, khoá lưu `comitor-theme`.\n *\n * Đặt ở **root layout** của app, NGOÀI `<AppShell>`. Mọi tham số đều override được.\n *\n * ⚠ `next-themes` là **peer optional có context singleton**: app phải tự cài, và chỉ\n * được có MỘT bản trong node_modules — hai bản khác nhau thì `useTheme()` của app đọc\n * provider khác với của package, đổi giao diện sẽ không ăn.\n */\nexport function ThemeProvider({\n attribute = \"class\",\n defaultTheme = \"system\",\n enableSystem = true,\n disableTransitionOnChange = true,\n storageKey = \"comitor-theme\",\n children,\n ...props\n}: ThemeProviderProps) {\n return (\n <NextThemesProvider\n attribute={attribute}\n defaultTheme={defaultTheme}\n enableSystem={enableSystem}\n disableTransitionOnChange={disableTransitionOnChange}\n storageKey={storageKey}\n {...props}\n >\n {children}\n </NextThemesProvider>\n );\n}\n"],"mappings":";AA2BI;AAzBJ,SAAS,iBAAiB,0BAA8E;AAejG,SAAS,cAAc;AAAA,EAC5B,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,eAAe;AAAA,EACf,4BAA4B;AAAA,EAC5B,aAAa;AAAA,EACb;AAAA,EACA,GAAG;AACL,GAAuB;AACrB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ShellLabels } from './types.js';
|
|
3
|
+
import 'lucide-react';
|
|
4
|
+
|
|
5
|
+
/** Chỉ những nhãn ThemeToggle thực sự dùng — để nút này KHÔNG buộc phải nằm trong ShellProvider. */
|
|
6
|
+
type ThemeToggleLabels = Pick<ShellLabels, "themeLight" | "themeDark" | "themeSystem" | "toggleTheme">;
|
|
7
|
+
interface ThemeToggleProps {
|
|
8
|
+
className?: string;
|
|
9
|
+
/**
|
|
10
|
+
* "icon" — một nút, bấm là lật Sáng ⇄ Tối (mặc định, dùng ở header).
|
|
11
|
+
* "menu" — dropdown 3 lựa chọn Sáng / Tối / Hệ thống.
|
|
12
|
+
*/
|
|
13
|
+
variant?: "icon" | "menu";
|
|
14
|
+
/** Override chuỗi hiển thị (mặc định tiếng Việt). `AppHeader` truyền labels của shell xuống. */
|
|
15
|
+
labels?: Partial<ThemeToggleLabels>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Nút đổi giao diện — dùng được ĐỘC LẬP (trang đăng nhập, trang marketing),
|
|
19
|
+
* không bắt buộc nằm trong `ShellProvider`.
|
|
20
|
+
*
|
|
21
|
+
* Chống lệch hydration bằng cờ `mounted`: theme thật chỉ biết được ở client
|
|
22
|
+
* (đọc localStorage/`prefers-color-scheme`), render thẳng icon theo theme ngay
|
|
23
|
+
* lần đầu sẽ lệch với HTML do server sinh ra. Trước khi mount hiện ô rỗng cùng
|
|
24
|
+
* kích thước để layout không nhảy.
|
|
25
|
+
*/
|
|
26
|
+
declare function ThemeToggle({ className, variant, labels: labelsProp }: ThemeToggleProps): React.JSX.Element;
|
|
27
|
+
|
|
28
|
+
export { ThemeToggle, type ThemeToggleLabels, type ThemeToggleProps };
|