@comitor/ui 3.3.1 → 3.3.3
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/dist/components/composite/command-palette.d.ts +1 -1
- package/dist/components/composite/command-palette.js.map +1 -1
- package/dist/components/composite/data-table.d.ts +10 -5
- package/dist/components/composite/data-table.js +2 -2
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/filter-chips.d.ts +1 -1
- package/dist/components/composite/filter-chips.js.map +1 -1
- package/dist/components/composite/inline-edit.d.ts +2 -2
- package/dist/components/composite/inline-edit.js.map +1 -1
- package/dist/components/composite/route-tabs.d.ts +8 -6
- package/dist/components/composite/route-tabs.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +1 -1
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/ui/progress.d.ts +14 -2
- package/dist/components/ui/progress.js +12 -3
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/resizable.d.ts +1 -1
- package/dist/components/ui/resizable.js.map +1 -1
- package/dist/data-grid/data-grid-context-menu.js +3 -2
- package/dist/data-grid/data-grid-context-menu.js.map +1 -1
- package/dist/data-grid/data-grid-row-height-menu.js +2 -2
- package/dist/data-grid/data-grid-row-height-menu.js.map +1 -1
- package/dist/data-grid/types.d.ts +7 -2
- package/dist/data-grid/use-data-grid.d.ts +14 -4
- package/dist/data-grid/use-data-grid.js +5 -3
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/data-grid/utils.js +0 -1
- package/dist/data-grid/utils.js.map +1 -1
- package/dist/i18n/namespaces/combobox.js +2 -2
- package/dist/i18n/namespaces/combobox.js.map +1 -1
- package/dist/i18n/namespaces/confirm-dialog.js +1 -1
- package/dist/i18n/namespaces/confirm-dialog.js.map +1 -1
- package/dist/i18n/namespaces/date-picker.js +2 -2
- package/dist/i18n/namespaces/date-picker.js.map +1 -1
- package/dist/i18n/namespaces/inline-edit.js +1 -1
- package/dist/i18n/namespaces/inline-edit.js.map +1 -1
- package/dist/i18n/namespaces/search-filter-bar.js +2 -2
- package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
- package/dist/i18n/namespaces/time-picker.js +1 -1
- package/dist/i18n/namespaces/time-picker.js.map +1 -1
- package/dist/shell/app-launcher.d.ts +2 -2
- package/dist/shell/app-launcher.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/comitor-logo.d.ts +1 -1
- package/dist/shell/comitor-logo.js.map +1 -1
- package/dist/shell/shell-context.d.ts +3 -2
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar-group.d.ts +1 -1
- package/dist/shell/sidebar-group.js.map +1 -1
- package/dist/shell/types.d.ts +1 -1
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/utils.d.ts +2 -2
- package/dist/shell/utils.js.map +1 -1
- package/dist/tokens.d.ts +23 -9
- package/dist/tokens.js +17 -9
- package/dist/tokens.js.map +1 -1
- package/dist/uploader/image-upload-field.d.ts +2 -2
- package/dist/uploader/image-upload-field.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/shell-context.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * ĐUÔI `.js` LÀ CHỦ ĐÍCH, ĐỪNG BỎ.\n *\n * Gói `next` KHÔNG khai `exports` map, nên `next/link` chỉ là đường dẫn file. Bundler\n * (webpack/Turbopack) tự thêm đuôi rồi trỏ đúng `node_modules/next/link.js`; Node ESM thuần thì\n * KHÔNG — nó ném `ERR_MODULE_NOT_FOUND: Cannot find module .../next/link` kèm gợi ý\n * \"Did you mean next/link.js?\". Hậu quả: `node -e \"await import('@comitor/ui/shell')\"` chết ngay\n * lúc nạp, tức là mọi smoke-test entry của CI đều mù với riêng entry shell.\n *\n * Ghi thẳng `next/link.js` / `next/navigation.js` là CÙNG MỘT file mà bundler vẫn resolve tới\n * (đã đối chiếu: `next` không có `resolve.alias` cho hai specifier này trong\n * `dist/build/create-compiler-aliases.js`), nhưng Node cũng nạp được.\n */\nimport NextLinkModule from \"next/link.js\";\nimport { usePathname } from \"next/navigation.js\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { layout } from \"../tokens.js\";\nimport type {\n AppDescriptor,\n NavGroup,\n ShellLabels,\n ShellLinkComponent,\n ShellLinkProps,\n ShellNav,\n ShellUser,\n Workspace\n} from \"./types.js\";\nimport { getEntitledApps, type NavSearchParams, resolveCurrentApp, toNavGroups, toSearchParams } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Chuẩn hoá interop của `next/link.js`.\n *\n * Hai môi trường trả về hai hình dạng khác nhau cho cùng một file CJS:\n * - bundler → đã interop sẵn, biến này CHÍNH LÀ component `Link`\n * - Node ESM → biến này là namespace `{ default: Link, useLinkStatus }`\n * `.default ?? chính nó` đúng ở cả hai (component `Link` không có thuộc tính `default`).\n */\nconst NextLink = ((NextLinkModule as unknown as { default?: ShellLinkComponent }).default ??\n NextLinkModule) as ShellLinkComponent;\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 /**\n * Query hiện tại đã chuẩn hoá, hoặc `null` khi app không truyền.\n * `null` KHÁC rỗng: rỗng nghĩa là \"URL không có query\", `null` nghĩa là \"shell không biết\"\n * — và active matching xử lý hai ca đó khác nhau (xem `isNavItemActive`).\n */\n searchParams: URLSearchParams | null;\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ị (bảng `shell` của `ComitorLocaleProvider`, đã trộn 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\n/**\n * Đang ở dải TABLET? — từ `breakpoints.mobile` (768px, mốc thanh bên thôi off-canvas) tới ngay\n * dưới `breakpoints.tablet` (1024px).\n *\n * Hai mốc lấy từ `layout.breakpoints` của `@comitor/ui/tokens` chứ không gõ số ở đây: đổi mốc mà\n * quên chỗ này thì thanh bên tự sập ở một dải mà CSS không còn coi là tablet nữa.\n */\nfunction isTabletWidth(): boolean {\n if (typeof window === \"undefined\") return false;\n const { mobile, tablet } = layout.breakpoints;\n return window.matchMedia(`(min-width: ${mobile}px) and (max-width: ${tablet - 1}px)`).matches;\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 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 /**\n * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query\n * (`/cai-dat?tab=chung` ⇄ `/cai-dat?tab=thanh-vien`). Bỏ trống thì mọi mục cùng đường dẫn\n * cùng sáng.\n *\n * Nhận `useSearchParams()` của Next, chuỗi `\"?tab=x\"`, hoặc object `searchParams` của\n * Server Component.\n *\n * ⚠ Shell KHÔNG tự gọi `useSearchParams()`: hook đó ép trang bọc `<Suspense>` và đẩy trang\n * sang render phía client khi build tĩnh — cái giá đó phải do app tự chọn, không phải do\n * việc lỡ import shell. Truyền vào từ một client component đã có sẵn boundary:\n *\n * ```tsx\n * \"use client\";\n * const searchParams = useSearchParams();\n * return <AppShell searchParams={searchParams} …>{children}</AppShell>;\n * ```\n */\n searchParams?: NavSearchParams | null;\n /**\n * Ở dải TABLET (768–1023px), khi người dùng CHƯA từng gạt thanh bên, mở app lần đầu thì thanh\n * bên thu gọn sẵn. Mặc định `true`.\n *\n * Đặt `false` khi app muốn thanh bên luôn mở rộng ở tablet — vd điều hướng là thứ chính của\n * màn hình đó. Lựa chọn ĐÃ LƯU của người dùng luôn thắng cờ này.\n */\n collapseSidebarOnTablet?: boolean;\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 /**\n * Override từng chuỗi hiển thị. Không truyền thì theo namespace `shell` của\n * `ComitorLocaleProvider` gần nhất (không có provider → tiếng Anh).\n */\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 searchParams: searchParamsProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n collapseSidebarOnTablet = true,\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 // Chuẩn hoá MỘT lần ở đây thay vì mỗi lần `isNavItemActive` chạy: prop có thể là chuỗi\n // hoặc object, mà mỗi lần dựng `URLSearchParams` mới là một identity mới → mọi memo bên\n // dưới hỏng. Khoá memo là chính prop app truyền vào.\n const searchParams = useMemo(() => toSearchParams(searchParamsProp), [searchParamsProp]);\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 = useComitorLabels(\"shell\", 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 * Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n *\n * Nếu CHƯA có lựa chọn nào của người dùng thì ở dải TABLET thanh bên mặc định thu gọn: từ 768px\n * thanh bên đã chiếm chỗ thật trong bố cục (dưới mức đó nó là sheet off-canvas), mà 768–1023px\n * thì 256px của nó ăn tới một phần ba bề ngang — vùng nội dung còn ~512px cho một bảng nhiều cột.\n *\n * ⚠ KHÔNG ghi lựa chọn này vào localStorage. Đây là GIÁ TRỊ MẶC ĐỊNH, không phải quyết định của\n * người dùng: ghi xuống là lần sau mở ở màn hình rộng thanh bên vẫn thu gọn vì \"đã có lựa chọn\n * lưu\", mà người dùng thì chưa bấm gì cả. Chỉ khi họ tự gạt thì `applySidebarCollapsed` mới lưu.\n *\n * ⚠ Chỉ chạy MỘT LẦN lúc mở, không theo dõi resize. \"Mặc định khi mới mở\" khác với \"ép theo bề\n * ngang\": kéo cửa sổ hẹp lại giữa phiên mà thanh bên tự sập là giật thứ người dùng đang cầm.\n *\n * Đọc bề ngang trong effect nên không đụng tới hydration — server không có `window`, và lần\n * render đầu ở client vẫn dùng đúng `defaultSidebarCollapsed` mà server đã render.\n */\n useEffect(() => {\n const stored = persistSidebarState ? readStoredCollapsed(currentWorkspace?.id) : null;\n if (stored !== null) {\n setSidebarCollapsed(stored);\n return;\n }\n if (collapseSidebarOnTablet && isTabletWidth()) setSidebarCollapsed(true);\n }, [persistSidebarState, currentWorkspace?.id, collapseSidebarOnTablet]);\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 searchParams,\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 searchParams,\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":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/shell-context.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * ĐUÔI `.js` LÀ CHỦ ĐÍCH, ĐỪNG BỎ.\n *\n * Gói `next` KHÔNG khai `exports` map, nên `next/link` chỉ là đường dẫn file. Bundler\n * (webpack/Turbopack) tự thêm đuôi rồi trỏ đúng `node_modules/next/link.js`; Node ESM thuần thì\n * KHÔNG — nó ném `ERR_MODULE_NOT_FOUND: Cannot find module .../next/link` kèm gợi ý\n * \"Did you mean next/link.js?\". Hậu quả: `node -e \"await import('@comitor/ui/shell')\"` chết ngay\n * lúc nạp, tức là mọi smoke-test entry của CI đều mù với riêng entry shell.\n *\n * Ghi thẳng `next/link.js` / `next/navigation.js` là CÙNG MỘT file mà bundler vẫn resolve tới\n * (đã đối chiếu: `next` không có `resolve.alias` cho hai specifier này trong\n * `dist/build/create-compiler-aliases.js`), nhưng Node cũng nạp được.\n */\nimport NextLinkModule from \"next/link.js\";\nimport { usePathname } from \"next/navigation.js\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { layout } from \"../tokens.js\";\nimport type {\n AppDescriptor,\n NavGroup,\n ShellLabels,\n ShellLinkComponent,\n ShellLinkProps,\n ShellNav,\n ShellUser,\n Workspace\n} from \"./types.js\";\nimport { getEntitledApps, type NavSearchParams, resolveCurrentApp, toNavGroups, toSearchParams } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Chuẩn hoá interop của `next/link.js`.\n *\n * Hai môi trường trả về hai hình dạng khác nhau cho cùng một file CJS:\n * - bundler → đã interop sẵn, biến này CHÍNH LÀ component `Link`\n * - Node ESM → biến này là namespace `{ default: Link, useLinkStatus }`\n * `.default ?? chính nó` đúng ở cả hai (component `Link` không có thuộc tính `default`).\n */\nconst NextLink = ((NextLinkModule as unknown as { default?: ShellLinkComponent }).default ??\n NextLinkModule) as ShellLinkComponent;\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 /**\n * Query hiện tại đã chuẩn hoá, hoặc `null` khi app không truyền.\n * `null` KHÁC rỗng: rỗng nghĩa là \"URL không có query\", `null` nghĩa là \"shell không biết\"\n * — và active matching xử lý hai ca đó khác nhau (xem `isNavItemActive`).\n */\n searchParams: URLSearchParams | null;\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ị (bảng `shell` của `ComitorLocaleProvider`, đã trộn 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\n/**\n * Đang ở dải TABLET? — từ `breakpoints.mobile` (768px, mốc thanh bên thôi off-canvas) tới ngay\n * dưới `breakpoints.tablet` (1024px).\n *\n * Hai mốc lấy từ `layout.breakpoints` của `@comitor/ui/tokens` chứ không gõ số ở đây: đổi mốc mà\n * quên chỗ này thì thanh bên tự sập ở một dải mà CSS không còn coi là tablet nữa.\n */\nfunction isTabletWidth(): boolean {\n if (typeof window === \"undefined\") return false;\n const { mobile, tablet } = layout.breakpoints;\n return window.matchMedia(`(min-width: ${mobile}px) and (max-width: ${tablet - 1}px)`).matches;\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 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 /**\n * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query\n * (`/settings?tab=general` ⇄ `/settings?tab=notifications`). Bỏ trống thì CHỈ mục KHÔNG mang\n * query mới sáng (`/settings` sáng, `/settings?tab=…` thì không) — luật của `isNavItemActive`\n * từ `0.3.3`; trước đó mọi mục cùng đường dẫn cùng sáng, và đó là lỗi.\n *\n * Nhận `useSearchParams()` của Next, chuỗi `\"?tab=x\"`, hoặc object `searchParams` của\n * Server Component.\n *\n * ⚠ Shell KHÔNG tự gọi `useSearchParams()`: hook đó ép trang bọc `<Suspense>` và đẩy trang\n * sang render phía client khi build tĩnh — cái giá đó phải do app tự chọn, không phải do\n * việc lỡ import shell. Truyền vào từ một client component đã có sẵn boundary:\n *\n * ```tsx\n * \"use client\";\n * const searchParams = useSearchParams();\n * return <AppShell searchParams={searchParams} …>{children}</AppShell>;\n * ```\n */\n searchParams?: NavSearchParams | null;\n /**\n * Ở dải TABLET (768–1023px), khi người dùng CHƯA từng gạt thanh bên, mở app lần đầu thì thanh\n * bên thu gọn sẵn. Mặc định `true`.\n *\n * Đặt `false` khi app muốn thanh bên luôn mở rộng ở tablet — vd điều hướng là thứ chính của\n * màn hình đó. Lựa chọn ĐÃ LƯU của người dùng luôn thắng cờ này.\n */\n collapseSidebarOnTablet?: boolean;\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 /**\n * Override từng chuỗi hiển thị. Không truyền thì theo namespace `shell` của\n * `ComitorLocaleProvider` gần nhất (không có provider → tiếng Anh).\n */\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 searchParams: searchParamsProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n collapseSidebarOnTablet = true,\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 // Chuẩn hoá MỘT lần ở đây thay vì mỗi lần `isNavItemActive` chạy: prop có thể là chuỗi\n // hoặc object, mà mỗi lần dựng `URLSearchParams` mới là một identity mới → mọi memo bên\n // dưới hỏng. Khoá memo là chính prop app truyền vào.\n const searchParams = useMemo(() => toSearchParams(searchParamsProp), [searchParamsProp]);\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 = useComitorLabels(\"shell\", 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 * Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n *\n * Nếu CHƯA có lựa chọn nào của người dùng thì ở dải TABLET thanh bên mặc định thu gọn: từ 768px\n * thanh bên đã chiếm chỗ thật trong bố cục (dưới mức đó nó là sheet off-canvas), mà 768–1023px\n * thì 256px của nó ăn tới một phần ba bề ngang — vùng nội dung còn ~512px cho một bảng nhiều cột.\n *\n * ⚠ KHÔNG ghi lựa chọn này vào localStorage. Đây là GIÁ TRỊ MẶC ĐỊNH, không phải quyết định của\n * người dùng: ghi xuống là lần sau mở ở màn hình rộng thanh bên vẫn thu gọn vì \"đã có lựa chọn\n * lưu\", mà người dùng thì chưa bấm gì cả. Chỉ khi họ tự gạt thì `applySidebarCollapsed` mới lưu.\n *\n * ⚠ Chỉ chạy MỘT LẦN lúc mở, không theo dõi resize. \"Mặc định khi mới mở\" khác với \"ép theo bề\n * ngang\": kéo cửa sổ hẹp lại giữa phiên mà thanh bên tự sập là giật thứ người dùng đang cầm.\n *\n * Đọc bề ngang trong effect nên không đụng tới hydration — server không có `window`, và lần\n * render đầu ở client vẫn dùng đúng `defaultSidebarCollapsed` mà server đã render.\n */\n useEffect(() => {\n const stored = persistSidebarState ? readStoredCollapsed(currentWorkspace?.id) : null;\n if (stored !== null) {\n setSidebarCollapsed(stored);\n return;\n }\n if (collapseSidebarOnTablet && isTabletWidth()) setSidebarCollapsed(true);\n }, [persistSidebarState, currentWorkspace?.id, collapseSidebarOnTablet]);\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 searchParams,\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 searchParams,\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":[]}
|
|
@@ -13,7 +13,7 @@ interface SidebarGroupProps {
|
|
|
13
13
|
className?: string;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Nhóm mục sidebar
|
|
16
|
+
* Nhóm mục sidebar: nhãn nhỏ in hoa, bấm nhãn để thu gọn cả nhóm.
|
|
17
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
18
|
*/
|
|
19
19
|
declare function SidebarGroup({ group, collapsed, siblings, onNavigate, className }: SidebarGroupProps): React.JSX.Element;
|
|
@@ -1 +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\n/**\n * Màu nhãn nhóm — `--muted-foreground`, KHÔNG phải `text-sidebar-foreground/45`.\n *\n * Nhãn nhóm là CHỮ (11px in hoa) nên chịu ngưỡng AA **4,5:1**, mà `/45` chỉ được **3,02:1**\n * ở light (`#909193` trên `--sidebar` #FAFAF9) và **4,26:1** ở dark — hỏng ở CẢ HAI theme,\n * light thì hỏng nặng. `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò này:\n * **5,01:1** (light) · **5,35:1** (dark) trên `--sidebar` ✓.\n *\n * Hover thì lên thẳng `--sidebar-foreground` (18,11:1 light · 16,59:1 dark) thay cho `/80`.\n */\nconst GROUP_LABEL_CLASS = \"text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\";\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
|
|
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\n/**\n * Màu nhãn nhóm — `--muted-foreground`, KHÔNG phải `text-sidebar-foreground/45`.\n *\n * Nhãn nhóm là CHỮ (11px in hoa) nên chịu ngưỡng AA **4,5:1**, mà `/45` chỉ được **3,02:1**\n * ở light (`#909193` trên `--sidebar` #FAFAF9) và **4,26:1** ở dark — hỏng ở CẢ HAI theme,\n * light thì hỏng nặng. `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò này:\n * **5,01:1** (light) · **5,35:1** (dark) trên `--sidebar` ✓.\n *\n * Hover thì lên thẳng `--sidebar-foreground` (18,11:1 light · 16,59:1 dark) thay cho `/80`.\n */\nconst GROUP_LABEL_CLASS = \"text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\";\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: 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 ? <p className={cn(\"px-2 pb-1\", GROUP_LABEL_CLASS)}>{group.label}</p> : 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\",\n GROUP_LABEL_CLASS,\n \"transition-colors duration-150 ease-out hover:text-sidebar-foreground\"\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":";AA2CQ,cAkBF,YAlBE;AAzCR,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,aAAa,oBAAoB,0BAA0B;AACpE,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAahC,MAAM,oBAAoB;AAiBnB,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,QAAQ,oBAAC,OAAE,WAAW,GAAG,aAAa,iBAAiB,GAAI,gBAAM,OAAM,IAAO;AAAA,MACpF;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,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":[]}
|
package/dist/shell/types.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ interface NavItem {
|
|
|
105
105
|
external?: boolean;
|
|
106
106
|
disabled?: boolean;
|
|
107
107
|
}
|
|
108
|
-
/** Nhóm mục menu (
|
|
108
|
+
/** Nhóm mục menu (nhãn nhỏ + thu gọn được). */
|
|
109
109
|
interface NavGroup {
|
|
110
110
|
id: string;
|
|
111
111
|
/** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */
|
package/dist/shell/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\nimport { shellEn } from \"../i18n/namespaces/shell.js\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (
|
|
1
|
+
{"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\nimport { shellEn } from \"../i18n/namespaces/shell.js\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định\n * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của\n * `ShellProvider`/`AppShell`.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.\n *\n * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại\n * \"bấm Collapse\" sẽ không khớp tên truy cập. \"Collapse\" ⊂ \"Collapse sidebar\" ✓, \"Thu gọn\" ⊂\n * \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và\n * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước (\"12 thành viên\"), tiếng Anh cũng vậy\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * là khoá cứng trật tự của một ngôn ngữ vào mọi bản dịch. Hàm còn chọn được dạng số ít/nhiều\n * (\"1 member\" ⇄ \"2 members\"), thứ tiếng Việt không có mà tiếng Anh thì bắt buộc. Cùng khuôn\n * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AA4NjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
|
package/dist/shell/utils.d.ts
CHANGED
|
@@ -31,11 +31,11 @@ declare function toSearchParams(input: NavSearchParams | null | undefined): URLS
|
|
|
31
31
|
*
|
|
32
32
|
* ─ QUERY TRONG `href` ─
|
|
33
33
|
* `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`
|
|
34
|
-
* làm mục kiểu `/
|
|
34
|
+
* làm mục kiểu `/settings?tab=notifications` vĩnh viễn không sáng. Query bị tách khỏi `href`
|
|
35
35
|
* trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.
|
|
36
36
|
*
|
|
37
37
|
* Không truyền `searchParams` → shell không biết đang ở tab nào, và khi đó **chỉ mục KHÔNG mang
|
|
38
|
-
* query mới sáng** (`/
|
|
38
|
+
* query mới sáng** (`/settings` sáng, `/settings?tab=…` thì không).
|
|
39
39
|
*
|
|
40
40
|
* ⚠ Bản trước cho MỌI mục cùng đường dẫn cùng sáng, với lý lẽ "đúng mức thông tin đang có, và tốt
|
|
41
41
|
* hơn không mục nào sáng". Lý lẽ đó dựng một lựa chọn nhị phân giả: còn khả năng thứ ba, và nó tốt
|
package/dist/shell/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/utils.ts"],"sourcesContent":["/**\n * Hàm thuần dùng chung cho tầng shell — KHÔNG hook, KHÔNG directive\n * (dùng được ở cả server lẫn client).\n */\n\nimport type { CSSProperties } from \"react\";\nimport type { AppDescriptor, NavGroup, NavItem, ShellNav } from \"./types.js\";\n\n/**\n * Tiện ích chuỗi tiếng Việt — **cùng một binding** với entry chính (`@comitor/ui`).\n *\n * Re-export chứ không viết lại. Hai lần repo này đã trả giá cho việc khai lại:\n * `getVietnameseInitials` từng có hai thân hàm (`toUpperCase()` ở đây,\n * `toLocaleUpperCase(\"vi-VN\")` ở Tier 2) nên cùng một tên cho ra hai kết quả tuỳ chỗ import;\n * `normalizeVi` thì có hai khai báo giống nhau chữ-cho-chữ, chưa sai kết quả nhưng là hai chỗ\n * để sửa lệch nhau lần sau. Tên vẫn xuất ở cả hai entry (không phá API của tầng shell), nhưng\n * chỉ còn MỘT thân hàm — xem docs/quyet-dinh-tich-hop.md §28 (\"một tên, một cửa\").\n */\nexport { getVietnameseInitials, normalizeVi } from \"../components/composite/text-utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Điều hướng\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** Phân biệt `NavGroup` với `NavItem` khi app truyền mảng phẳng. */\nfunction isNavGroup(value: NavItem | NavGroup): value is NavGroup {\n return Array.isArray((value as NavGroup).items);\n}\n\n/**\n * Chuẩn hoá menu app truyền vào về `NavGroup[]`.\n * Mảng phẳng `NavItem[]` → một nhóm không nhãn (id \"default\").\n */\nexport function toNavGroups(nav: ShellNav | undefined): NavGroup[] {\n if (!nav || nav.length === 0) return [];\n const first = nav[0];\n if (first && isNavGroup(first)) return nav as NavGroup[];\n return [{ id: \"default\", items: nav as NavItem[] }];\n}\n\n/** Gộp mọi item của mọi nhóm thành một mảng phẳng (kể cả mục con). */\nexport function flattenNavItems(groups: NavGroup[]): NavItem[] {\n const out: NavItem[] = [];\n const walk = (items: NavItem[]) => {\n for (const item of items) {\n out.push(item);\n if (item.children?.length) walk(item.children);\n }\n };\n for (const group of groups) walk(group.items);\n return out;\n}\n\n/**\n * Query string hiện tại, theo mọi hình dạng app thường có sẵn trong tay:\n * `useSearchParams()` của Next (một `URLSearchParams`), chuỗi `\"?tab=x\"` / `\"tab=x\"`,\n * hoặc object `searchParams` mà Server Component nhận được.\n */\nexport type NavSearchParams = string | URLSearchParams | Record<string, string | string[] | undefined>;\n\n/** Tách `href` thành đường dẫn + query. Hash bị bỏ: `pathname` không bao giờ chứa nó. */\nfunction splitHref(href: string): { path: string; query: string } {\n const hashAt = href.indexOf(\"#\");\n const noHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const queryAt = noHash.indexOf(\"?\");\n if (queryAt === -1) return { path: noHash, query: \"\" };\n return { path: noHash.slice(0, queryAt), query: noHash.slice(queryAt + 1) };\n}\n\n/** Quy mọi hình dạng `NavSearchParams` về `URLSearchParams`. `null`/`undefined` → `null`. */\nexport function toSearchParams(input: NavSearchParams | null | undefined): URLSearchParams | null {\n if (input === null || input === undefined) return null;\n if (typeof input === \"string\") return new URLSearchParams(input.startsWith(\"?\") ? input.slice(1) : input);\n // Duck-typing thay vì `instanceof`: `ReadonlyURLSearchParams` của Next kế thừa\n // `URLSearchParams`, nhưng `instanceof` còn hỏng khi hai realm khác nhau (test, iframe).\n if (typeof (input as URLSearchParams).getAll === \"function\") return input as URLSearchParams;\n const out = new URLSearchParams();\n for (const [key, value] of Object.entries(input as Record<string, string | string[] | undefined>)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const one of value) out.append(key, one);\n else out.append(key, value);\n }\n return out;\n}\n\n/** Mọi cặp key=value trong `query` đều có mặt ở `current` (khớp TẬP CON, không đòi bằng hệt). */\nfunction queryIsSubsetOf(query: string, current: URLSearchParams): boolean {\n for (const [key, value] of new URLSearchParams(query)) {\n if (!current.getAll(key).includes(value)) return false;\n }\n return true;\n}\n\n/** Đường dẫn hiệu dụng của một href. `\"?tab=x\"` (chỉ có query) = chính trang đang mở. */\nfunction navItemPath(href: string, pathname: string): string {\n const { path } = splitHref(href);\n return path === \"\" ? pathname : path;\n}\n\n/**\n * Active matching \"thông minh\": chỉ dùng `startsWith` khi KHÔNG có mục nào khác\n * khớp CHÍNH XÁC `pathname`. Nếu không, `/tasks` vẫn sáng khi đang đứng ở\n * `/tasks/inbox` (có mục riêng cho nó).\n *\n * ─ QUERY TRONG `href` ─\n * `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`\n * làm mục kiểu `/cai-dat?tab=thanh-vien` vĩnh viễn không sáng. Query bị tách khỏi `href`\n * trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.\n *\n * Không truyền `searchParams` → shell không biết đang ở tab nào, và khi đó **chỉ mục KHÔNG mang\n * query mới sáng** (`/cai-dat` sáng, `/cai-dat?tab=…` thì không).\n *\n * ⚠ Bản trước cho MỌI mục cùng đường dẫn cùng sáng, với lý lẽ \"đúng mức thông tin đang có, và tốt\n * hơn không mục nào sáng\". Lý lẽ đó dựng một lựa chọn nhị phân giả: còn khả năng thứ ba, và nó tốt\n * hơn cả hai. Bốn hàng cùng sáng không phải là \"chưa đủ thông tin\" — nó là một câu KHẲNG ĐỊNH SAI\n * với người dùng (\"bạn đang ở cả bốn chỗ\"), trong khi tô đúng một hàng canonical chỉ là chưa đủ\n * chi tiết. Ở `comitor-starter` lỗi này làm sáng cùng lúc \"Thành viên\", \"Chung\", \"Thông báo\" và\n * \"Nâng cao\".\n *\n * Ngoại lệ giữ lại: nếu KHÔNG có mục nào cùng đường dẫn mà không mang query (tức mọi lối vào trang\n * đó đều là một tab), thì các mục có query lại cùng sáng — vì lúc đó \"không hàng nào sáng\" mới là\n * kết quả tệ nhất.\n *\n * Muốn đúng từng tab thì truyền `searchParams` — xem prop cùng tên của `ShellProvider`.\n *\n * @param siblings Danh sách mục để đối chiếu khớp chính xác — nên truyền toàn bộ\n * mục của app hiện tại (`flattenNavItems`), không chỉ mục cùng cấp.\n * @param searchParams Query hiện tại (TUỲ CHỌN). Có nó thì mục có query phải khớp TẬP CON\n * (`?tab=a` sáng ở `?tab=a&q=x`), và mục KHÔNG query nhường cho mục cùng\n * đường dẫn có query khớp — tab mặc định không sáng cùng lúc với tab đang mở.\n */\nexport function isNavItemActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (item.isActive !== undefined) return item.isActive;\n if (!pathname || !item.href) return false;\n\n const { query } = splitHref(item.href);\n const itemPath = navItemPath(item.href, pathname);\n const current = toSearchParams(searchParams);\n\n if (pathname === itemPath) {\n if (!current) {\n // Query chưa biết → mục canonical (không query) thắng; xem chú thích ở đầu hàm.\n if (query === \"\") return true;\n return !siblings.some(\n (other) =>\n other.href !== undefined && splitHref(other.href).query === \"\" && splitHref(other.href).path === pathname\n );\n }\n if (query !== \"\") return queryIsSubsetOf(query, current);\n return !siblings.some((other) => {\n if (other.href === item.href) return false;\n const otherQuery = splitHref(other.href).query;\n return (\n otherQuery !== \"\" && navItemPath(other.href, pathname) === pathname && queryIsSubsetOf(otherQuery, current)\n );\n });\n }\n\n // So khớp chính xác của mục khác cũng phải bỏ query, nếu không `/cai-dat?tab=x` sẽ không\n // bao giờ được tính là \"đã có mục khớp đúng\" và mục cha `/cai-dat` sáng nhầm theo startsWith.\n const hasExactMatchElsewhere = siblings.some(\n (other) => other.href !== item.href && splitHref(other.href).path === pathname\n );\n if (hasExactMatchElsewhere) return false;\n return itemPath !== \"/\" && pathname.startsWith(`${itemPath}/`);\n}\n\n/** Mục cha được coi là active khi chính nó hoặc bất kỳ mục con nào active. */\nexport function isNavBranchActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (isNavItemActive(item, pathname, siblings, searchParams)) return true;\n return (item.children ?? []).some((child) => isNavBranchActive(child, pathname, siblings, searchParams));\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n App (trục 2)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */\nexport function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled !== false);\n}\n\n/** App chưa mở khoá — hiển thị ở nhóm `discoverApps` của AppLauncher. */\nexport function getLockedApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled === false);\n}\n\n/**\n * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để\n * \"/crm/deals\" không rơi nhầm vào app có href \"/\".\n *\n * Query trong `href` bị tách ra trước khi so, cùng lý do với `isNavItemActive`: `pathname`\n * không mang query, nên app khai `href: \"/crm?view=board\"` mà so nguyên chuỗi thì không bao\n * giờ được nhận ra. Sắp xếp cũng theo độ dài ĐƯỜNG DẪN (query dài không được đẩy app lên trước).\n */\nexport function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null {\n if (apps.length === 0) return null;\n const matched = apps\n .map((app) => ({ app, path: splitHref(app.href).path }))\n .sort((a, b) => b.path.length - a.path.length)\n .find(({ path }) => pathname === path || (path !== \"/\" && path !== \"\" && pathname.startsWith(`${path}/`)))?.app;\n return matched ?? apps[0] ?? null;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Biến CSS theo app\n\n (Màu avatar KHÔNG còn cửa riêng ở tầng shell: `getAvatarToneClass` — alias một dòng của\n `getAvatarToneClasses`, lệch tên tham số `seed` ⇄ `name` và phải kèm chú thích dặn \"seed\n phải là tên hiển thị\" — đã bị xoá ở 0.2.0. `UserMenu`/`WorkspaceSwitcher` nhập thẳng\n `getAvatarToneClasses` từ `composite/letter-avatar.js`; app dùng `@comitor/ui`.)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Inline style gán bộ biến accent cho một app cụ thể.\n * Trả về `undefined` khi app không khai gì → giữ nguyên token mặc định của theme.\n *\n * Cả BA vai trò đều phải đi qua đây, không chỉ `--app-accent`: theo `docs/hop-dong-mau.md` §5\n * thì `--app-accent` chỉ để TÔ NỀN, `--app-accent-foreground` là chữ trên nền đặc đó, còn\n * `--app-accent-ink` là chính màu app khi đi lên nền trang. Thiếu cửa cho `--app-accent-ink`\n * thì app đổi accent sang màu khác vẫn để lại bậc mực gold mặc định (#7A5800 ở light /\n * #F5D060 ở dark) ở mọi chỗ dùng `text-app-accent-ink` — sai màu mà không sai chuẩn.\n */\nexport function appAccentStyle(\n app: Pick<AppDescriptor, \"accent\" | \"accentForeground\" | \"accentInk\"> | null | undefined\n) {\n if (!app?.accent && !app?.accentForeground && !app?.accentInk) return undefined;\n const style: Record<string, string> = {};\n if (app.accent) style[\"--app-accent\"] = app.accent;\n if (app.accentForeground) style[\"--app-accent-foreground\"] = app.accentForeground;\n if (app.accentInk) style[\"--app-accent-ink\"] = app.accentInk;\n return style as CSSProperties;\n}\n"],"mappings":"AAkBA,SAAS,uBAAuB,mBAAmB;AAOnD,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;AAUA,SAAS,UAAU,MAA+C;AAChE,QAAM,SAAS,KAAK,QAAQ,GAAG;AAC/B,QAAM,SAAS,WAAW,KAAK,OAAO,KAAK,MAAM,GAAG,MAAM;AAC1D,QAAM,UAAU,OAAO,QAAQ,GAAG;AAClC,MAAI,YAAY,GAAI,QAAO,EAAE,MAAM,QAAQ,OAAO,GAAG;AACrD,SAAO,EAAE,MAAM,OAAO,MAAM,GAAG,OAAO,GAAG,OAAO,OAAO,MAAM,UAAU,CAAC,EAAE;AAC5E;AAGO,SAAS,eAAe,OAAmE;AAChG,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,IAAI,gBAAgB,MAAM,WAAW,GAAG,IAAI,MAAM,MAAM,CAAC,IAAI,KAAK;AAGxG,MAAI,OAAQ,MAA0B,WAAW,WAAY,QAAO;AACpE,QAAM,MAAM,IAAI,gBAAgB;AAChC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAsD,GAAG;AACjG,QAAI,UAAU,OAAW;AACzB,QAAI,MAAM,QAAQ,KAAK,EAAG,YAAW,OAAO,MAAO,KAAI,OAAO,KAAK,GAAG;AAAA,QACjE,KAAI,OAAO,KAAK,KAAK;AAAA,EAC5B;AACA,SAAO;AACT;AAGA,SAAS,gBAAgB,OAAe,SAAmC;AACzE,aAAW,CAAC,KAAK,KAAK,KAAK,IAAI,gBAAgB,KAAK,GAAG;AACrD,QAAI,CAAC,QAAQ,OAAO,GAAG,EAAE,SAAS,KAAK,EAAG,QAAO;AAAA,EACnD;AACA,SAAO;AACT;AAGA,SAAS,YAAY,MAAc,UAA0B;AAC3D,QAAM,EAAE,KAAK,IAAI,UAAU,IAAI;AAC/B,SAAO,SAAS,KAAK,WAAW;AAClC;AAkCO,SAAS,gBACd,MACA,UACA,WAAsB,CAAC,GACvB,cACS;AACT,MAAI,KAAK,aAAa,OAAW,QAAO,KAAK;AAC7C,MAAI,CAAC,YAAY,CAAC,KAAK,KAAM,QAAO;AAEpC,QAAM,EAAE,MAAM,IAAI,UAAU,KAAK,IAAI;AACrC,QAAM,WAAW,YAAY,KAAK,MAAM,QAAQ;AAChD,QAAM,UAAU,eAAe,YAAY;AAE3C,MAAI,aAAa,UAAU;AACzB,QAAI,CAAC,SAAS;AAEZ,UAAI,UAAU,GAAI,QAAO;AACzB,aAAO,CAAC,SAAS;AAAA,QACf,CAAC,UACC,MAAM,SAAS,UAAa,UAAU,MAAM,IAAI,EAAE,UAAU,MAAM,UAAU,MAAM,IAAI,EAAE,SAAS;AAAA,MACrG;AAAA,IACF;AACA,QAAI,UAAU,GAAI,QAAO,gBAAgB,OAAO,OAAO;AACvD,WAAO,CAAC,SAAS,KAAK,CAAC,UAAU;AAC/B,UAAI,MAAM,SAAS,KAAK,KAAM,QAAO;AACrC,YAAM,aAAa,UAAU,MAAM,IAAI,EAAE;AACzC,aACE,eAAe,MAAM,YAAY,MAAM,MAAM,QAAQ,MAAM,YAAY,gBAAgB,YAAY,OAAO;AAAA,IAE9G,CAAC;AAAA,EACH;AAIA,QAAM,yBAAyB,SAAS;AAAA,IACtC,CAAC,UAAU,MAAM,SAAS,KAAK,QAAQ,UAAU,MAAM,IAAI,EAAE,SAAS;AAAA,EACxE;AACA,MAAI,uBAAwB,QAAO;AACnC,SAAO,aAAa,OAAO,SAAS,WAAW,GAAG,QAAQ,GAAG;AAC/D;AAGO,SAAS,kBACd,MACA,UACA,WAAsB,CAAC,GACvB,cACS;AACT,MAAI,gBAAgB,MAAM,UAAU,UAAU,YAAY,EAAG,QAAO;AACpE,UAAQ,KAAK,YAAY,CAAC,GAAG,KAAK,CAAC,UAAU,kBAAkB,OAAO,UAAU,UAAU,YAAY,CAAC;AACzG;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;AAUO,SAAS,kBAAkB,MAAuB,UAAwC;AAC/F,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,UAAU,KACb,IAAI,CAAC,SAAS,EAAE,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,KAAK,EAAE,EACtD,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAC5C,KAAK,CAAC,EAAE,KAAK,MAAM,aAAa,QAAS,SAAS,OAAO,SAAS,MAAM,SAAS,WAAW,GAAG,IAAI,GAAG,CAAE,GAAG;AAC9G,SAAO,WAAW,KAAK,CAAC,KAAK;AAC/B;AAqBO,SAAS,eACd,KACA;AACA,MAAI,CAAC,KAAK,UAAU,CAAC,KAAK,oBAAoB,CAAC,KAAK,UAAW,QAAO;AACtE,QAAM,QAAgC,CAAC;AACvC,MAAI,IAAI,OAAQ,OAAM,cAAc,IAAI,IAAI;AAC5C,MAAI,IAAI,iBAAkB,OAAM,yBAAyB,IAAI,IAAI;AACjE,MAAI,IAAI,UAAW,OAAM,kBAAkB,IAAI,IAAI;AACnD,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/utils.ts"],"sourcesContent":["/**\n * Hàm thuần dùng chung cho tầng shell — KHÔNG hook, KHÔNG directive\n * (dùng được ở cả server lẫn client).\n */\n\nimport type { CSSProperties } from \"react\";\nimport type { AppDescriptor, NavGroup, NavItem, ShellNav } from \"./types.js\";\n\n/**\n * Tiện ích chuỗi tiếng Việt — **cùng một binding** với entry chính (`@comitor/ui`).\n *\n * Re-export chứ không viết lại. Hai lần repo này đã trả giá cho việc khai lại:\n * `getVietnameseInitials` từng có hai thân hàm (`toUpperCase()` ở đây,\n * `toLocaleUpperCase(\"vi-VN\")` ở Tier 2) nên cùng một tên cho ra hai kết quả tuỳ chỗ import;\n * `normalizeVi` thì có hai khai báo giống nhau chữ-cho-chữ, chưa sai kết quả nhưng là hai chỗ\n * để sửa lệch nhau lần sau. Tên vẫn xuất ở cả hai entry (không phá API của tầng shell), nhưng\n * chỉ còn MỘT thân hàm — xem docs/quyet-dinh-tich-hop.md §28 (\"một tên, một cửa\").\n */\nexport { getVietnameseInitials, normalizeVi } from \"../components/composite/text-utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Điều hướng\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** Phân biệt `NavGroup` với `NavItem` khi app truyền mảng phẳng. */\nfunction isNavGroup(value: NavItem | NavGroup): value is NavGroup {\n return Array.isArray((value as NavGroup).items);\n}\n\n/**\n * Chuẩn hoá menu app truyền vào về `NavGroup[]`.\n * Mảng phẳng `NavItem[]` → một nhóm không nhãn (id \"default\").\n */\nexport function toNavGroups(nav: ShellNav | undefined): NavGroup[] {\n if (!nav || nav.length === 0) return [];\n const first = nav[0];\n if (first && isNavGroup(first)) return nav as NavGroup[];\n return [{ id: \"default\", items: nav as NavItem[] }];\n}\n\n/** Gộp mọi item của mọi nhóm thành một mảng phẳng (kể cả mục con). */\nexport function flattenNavItems(groups: NavGroup[]): NavItem[] {\n const out: NavItem[] = [];\n const walk = (items: NavItem[]) => {\n for (const item of items) {\n out.push(item);\n if (item.children?.length) walk(item.children);\n }\n };\n for (const group of groups) walk(group.items);\n return out;\n}\n\n/**\n * Query string hiện tại, theo mọi hình dạng app thường có sẵn trong tay:\n * `useSearchParams()` của Next (một `URLSearchParams`), chuỗi `\"?tab=x\"` / `\"tab=x\"`,\n * hoặc object `searchParams` mà Server Component nhận được.\n */\nexport type NavSearchParams = string | URLSearchParams | Record<string, string | string[] | undefined>;\n\n/** Tách `href` thành đường dẫn + query. Hash bị bỏ: `pathname` không bao giờ chứa nó. */\nfunction splitHref(href: string): { path: string; query: string } {\n const hashAt = href.indexOf(\"#\");\n const noHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const queryAt = noHash.indexOf(\"?\");\n if (queryAt === -1) return { path: noHash, query: \"\" };\n return { path: noHash.slice(0, queryAt), query: noHash.slice(queryAt + 1) };\n}\n\n/** Quy mọi hình dạng `NavSearchParams` về `URLSearchParams`. `null`/`undefined` → `null`. */\nexport function toSearchParams(input: NavSearchParams | null | undefined): URLSearchParams | null {\n if (input === null || input === undefined) return null;\n if (typeof input === \"string\") return new URLSearchParams(input.startsWith(\"?\") ? input.slice(1) : input);\n // Duck-typing thay vì `instanceof`: `ReadonlyURLSearchParams` của Next kế thừa\n // `URLSearchParams`, nhưng `instanceof` còn hỏng khi hai realm khác nhau (test, iframe).\n if (typeof (input as URLSearchParams).getAll === \"function\") return input as URLSearchParams;\n const out = new URLSearchParams();\n for (const [key, value] of Object.entries(input as Record<string, string | string[] | undefined>)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const one of value) out.append(key, one);\n else out.append(key, value);\n }\n return out;\n}\n\n/** Mọi cặp key=value trong `query` đều có mặt ở `current` (khớp TẬP CON, không đòi bằng hệt). */\nfunction queryIsSubsetOf(query: string, current: URLSearchParams): boolean {\n for (const [key, value] of new URLSearchParams(query)) {\n if (!current.getAll(key).includes(value)) return false;\n }\n return true;\n}\n\n/** Đường dẫn hiệu dụng của một href. `\"?tab=x\"` (chỉ có query) = chính trang đang mở. */\nfunction navItemPath(href: string, pathname: string): string {\n const { path } = splitHref(href);\n return path === \"\" ? pathname : path;\n}\n\n/**\n * Active matching \"thông minh\": chỉ dùng `startsWith` khi KHÔNG có mục nào khác\n * khớp CHÍNH XÁC `pathname`. Nếu không, `/tasks` vẫn sáng khi đang đứng ở\n * `/tasks/inbox` (có mục riêng cho nó).\n *\n * ─ QUERY TRONG `href` ─\n * `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`\n * làm mục kiểu `/settings?tab=notifications` vĩnh viễn không sáng. Query bị tách khỏi `href`\n * trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.\n *\n * Không truyền `searchParams` → shell không biết đang ở tab nào, và khi đó **chỉ mục KHÔNG mang\n * query mới sáng** (`/settings` sáng, `/settings?tab=…` thì không).\n *\n * ⚠ Bản trước cho MỌI mục cùng đường dẫn cùng sáng, với lý lẽ \"đúng mức thông tin đang có, và tốt\n * hơn không mục nào sáng\". Lý lẽ đó dựng một lựa chọn nhị phân giả: còn khả năng thứ ba, và nó tốt\n * hơn cả hai. Bốn hàng cùng sáng không phải là \"chưa đủ thông tin\" — nó là một câu KHẲNG ĐỊNH SAI\n * với người dùng (\"bạn đang ở cả bốn chỗ\"), trong khi tô đúng một hàng canonical chỉ là chưa đủ\n * chi tiết. Ở `comitor-starter` lỗi này làm sáng cùng lúc \"Thành viên\", \"Chung\", \"Thông báo\" và\n * \"Nâng cao\".\n *\n * Ngoại lệ giữ lại: nếu KHÔNG có mục nào cùng đường dẫn mà không mang query (tức mọi lối vào trang\n * đó đều là một tab), thì các mục có query lại cùng sáng — vì lúc đó \"không hàng nào sáng\" mới là\n * kết quả tệ nhất.\n *\n * Muốn đúng từng tab thì truyền `searchParams` — xem prop cùng tên của `ShellProvider`.\n *\n * @param siblings Danh sách mục để đối chiếu khớp chính xác — nên truyền toàn bộ\n * mục của app hiện tại (`flattenNavItems`), không chỉ mục cùng cấp.\n * @param searchParams Query hiện tại (TUỲ CHỌN). Có nó thì mục có query phải khớp TẬP CON\n * (`?tab=a` sáng ở `?tab=a&q=x`), và mục KHÔNG query nhường cho mục cùng\n * đường dẫn có query khớp — tab mặc định không sáng cùng lúc với tab đang mở.\n */\nexport function isNavItemActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (item.isActive !== undefined) return item.isActive;\n if (!pathname || !item.href) return false;\n\n const { query } = splitHref(item.href);\n const itemPath = navItemPath(item.href, pathname);\n const current = toSearchParams(searchParams);\n\n if (pathname === itemPath) {\n if (!current) {\n // Query chưa biết → mục canonical (không query) thắng; xem chú thích ở đầu hàm.\n if (query === \"\") return true;\n return !siblings.some(\n (other) =>\n other.href !== undefined && splitHref(other.href).query === \"\" && splitHref(other.href).path === pathname\n );\n }\n if (query !== \"\") return queryIsSubsetOf(query, current);\n return !siblings.some((other) => {\n if (other.href === item.href) return false;\n const otherQuery = splitHref(other.href).query;\n return (\n otherQuery !== \"\" && navItemPath(other.href, pathname) === pathname && queryIsSubsetOf(otherQuery, current)\n );\n });\n }\n\n // So khớp chính xác của mục khác cũng phải bỏ query, nếu không `/settings?tab=x` sẽ không\n // bao giờ được tính là \"đã có mục khớp đúng\" và mục cha `/settings` sáng nhầm theo startsWith.\n const hasExactMatchElsewhere = siblings.some(\n (other) => other.href !== item.href && splitHref(other.href).path === pathname\n );\n if (hasExactMatchElsewhere) return false;\n return itemPath !== \"/\" && pathname.startsWith(`${itemPath}/`);\n}\n\n/** Mục cha được coi là active khi chính nó hoặc bất kỳ mục con nào active. */\nexport function isNavBranchActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (isNavItemActive(item, pathname, siblings, searchParams)) return true;\n return (item.children ?? []).some((child) => isNavBranchActive(child, pathname, siblings, searchParams));\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n App (trục 2)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */\nexport function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled !== false);\n}\n\n/** App chưa mở khoá — hiển thị ở nhóm `discoverApps` của AppLauncher. */\nexport function getLockedApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled === false);\n}\n\n/**\n * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để\n * \"/crm/deals\" không rơi nhầm vào app có href \"/\".\n *\n * Query trong `href` bị tách ra trước khi so, cùng lý do với `isNavItemActive`: `pathname`\n * không mang query, nên app khai `href: \"/crm?view=board\"` mà so nguyên chuỗi thì không bao\n * giờ được nhận ra. Sắp xếp cũng theo độ dài ĐƯỜNG DẪN (query dài không được đẩy app lên trước).\n */\nexport function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null {\n if (apps.length === 0) return null;\n const matched = apps\n .map((app) => ({ app, path: splitHref(app.href).path }))\n .sort((a, b) => b.path.length - a.path.length)\n .find(({ path }) => pathname === path || (path !== \"/\" && path !== \"\" && pathname.startsWith(`${path}/`)))?.app;\n return matched ?? apps[0] ?? null;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Biến CSS theo app\n\n (Màu avatar KHÔNG còn cửa riêng ở tầng shell: `getAvatarToneClass` — alias một dòng của\n `getAvatarToneClasses`, lệch tên tham số `seed` ⇄ `name` và phải kèm chú thích dặn \"seed\n phải là tên hiển thị\" — đã bị xoá ở 0.2.0. `UserMenu`/`WorkspaceSwitcher` nhập thẳng\n `getAvatarToneClasses` từ `composite/letter-avatar.js`; app dùng `@comitor/ui`.)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Inline style gán bộ biến accent cho một app cụ thể.\n * Trả về `undefined` khi app không khai gì → giữ nguyên token mặc định của theme.\n *\n * Cả BA vai trò đều phải đi qua đây, không chỉ `--app-accent`: theo `docs/hop-dong-mau.md` §5\n * thì `--app-accent` chỉ để TÔ NỀN, `--app-accent-foreground` là chữ trên nền đặc đó, còn\n * `--app-accent-ink` là chính màu app khi đi lên nền trang. Thiếu cửa cho `--app-accent-ink`\n * thì app đổi accent sang màu khác vẫn để lại bậc mực gold mặc định (#7A5800 ở light /\n * #F5D060 ở dark) ở mọi chỗ dùng `text-app-accent-ink` — sai màu mà không sai chuẩn.\n */\nexport function appAccentStyle(\n app: Pick<AppDescriptor, \"accent\" | \"accentForeground\" | \"accentInk\"> | null | undefined\n) {\n if (!app?.accent && !app?.accentForeground && !app?.accentInk) return undefined;\n const style: Record<string, string> = {};\n if (app.accent) style[\"--app-accent\"] = app.accent;\n if (app.accentForeground) style[\"--app-accent-foreground\"] = app.accentForeground;\n if (app.accentInk) style[\"--app-accent-ink\"] = app.accentInk;\n return style as CSSProperties;\n}\n"],"mappings":"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","names":[]}
|
package/dist/tokens.d.ts
CHANGED
|
@@ -245,10 +245,13 @@ declare const semantic: {
|
|
|
245
245
|
readonly destructiveInk: "#D64545";
|
|
246
246
|
/** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */
|
|
247
247
|
readonly border: "#1E2128";
|
|
248
|
-
/** Viền ô nhập bản dark của comitor-ds
|
|
249
|
-
|
|
248
|
+
/** Viền ô nhập bản dark — KHÔNG phải #1E2128 của comitor-ds (chỉ 1,10:1 trên `card`, ô nhập tan
|
|
249
|
+
* vào nền). #434A5A = 2,00:1, mirror `.dark { --input }` từ `0.5.0`; trước `3.3.3` mirror này còn
|
|
250
|
+
* ghi #1E2128. */
|
|
251
|
+
readonly input: "#434A5A";
|
|
250
252
|
readonly ring: "#F5D060";
|
|
251
|
-
|
|
253
|
+
/** = `input` (`--control-edge: var(--input)`). */
|
|
254
|
+
readonly controlEdge: "#434A5A";
|
|
252
255
|
/** comitor-ds dùng `dark:hover:border-gray-500` = `ash[500]` (lật chiều so với light). */
|
|
253
256
|
readonly controlEdgeStrong: "#7E8290";
|
|
254
257
|
/** comitor-ds tô `dark:data-[state=checked]:bg-gold-300`, nhưng token `primary` của chính nó
|
|
@@ -297,11 +300,14 @@ declare const highContrast: {
|
|
|
297
300
|
readonly input: "#7E8290";
|
|
298
301
|
readonly controlEdge: "#7E8290";
|
|
299
302
|
readonly controlEdgeStrong: "#3A3F4A";
|
|
300
|
-
/**
|
|
301
|
-
|
|
303
|
+
/** #B48700 = gold-400 tối đi 2%: 3,28:1 trên `background`/`card` · 3,06:1 trên `muted` · ⇄ núm
|
|
304
|
+
* trắng 3,28:1 (bảng mặc định: 1,86 / 1,86). Trước `3.3.3` mirror này còn ghi gold-500 — giá trị
|
|
305
|
+
* `styles.css` đã bỏ. */
|
|
306
|
+
readonly controlOn: "#B48700";
|
|
302
307
|
/** = `background`, nên luôn tương phản đúng chiều với cả rãnh BẬT lẫn rãnh TẮT. */
|
|
303
308
|
readonly controlKnob: "#FFFFFF";
|
|
304
|
-
|
|
309
|
+
/** MỰC trên nền gold (5,77:1) — cùng cách vẽ với bảng mặc định; trước `3.3.3` mirror này ghi trắng. */
|
|
310
|
+
readonly controlCheck: "#0E1116";
|
|
305
311
|
readonly switchTrack: "#7E8290";
|
|
306
312
|
readonly sliderTrack: "#7E8290";
|
|
307
313
|
readonly primaryInk: "#7A5800";
|
|
@@ -391,7 +397,9 @@ declare const status: {
|
|
|
391
397
|
readonly warningInk: "#7A5800";
|
|
392
398
|
readonly severeInk: "#AC440F";
|
|
393
399
|
readonly infoInk: "#1A6668";
|
|
394
|
-
|
|
400
|
+
/** = `semantic.light.destructiveInk` (#D64545 — `--destructive-ink: var(--destructive)` ở bảng mặc
|
|
401
|
+
* định). Trước `3.3.3` trỏ nhầm `accentInk.light.red` (#9A3537 — bậc của bảng TƯƠNG PHẢN CAO). */
|
|
402
|
+
readonly destructiveInk: "#D64545";
|
|
395
403
|
};
|
|
396
404
|
readonly dark: {
|
|
397
405
|
readonly success: "#2EA56A";
|
|
@@ -411,10 +419,16 @@ declare const status: {
|
|
|
411
419
|
readonly destructiveInk: "#D64545";
|
|
412
420
|
};
|
|
413
421
|
};
|
|
414
|
-
/**
|
|
422
|
+
/**
|
|
423
|
+
* Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định.
|
|
424
|
+
*
|
|
425
|
+
* ⚠ `chart-1` ở dark là `gold[300]` như light: `styles.css` KHÔNG khai lại `--chart-1` trong `.dark`
|
|
426
|
+
* (chỉ `--chart-3` tự sáng lên qua `--navy-ink`). Trước `3.3.3` mirror này ghi `gold[200]` — biểu
|
|
427
|
+
* đồ canvas/email/PDF dựng từ `tokens` ra màu khác biểu đồ trên trang.
|
|
428
|
+
*/
|
|
415
429
|
declare const chart: {
|
|
416
430
|
readonly light: readonly ["#E8B824", "#1F8A8B", "#0F1E3D", "#2EA56A", "#D64545"];
|
|
417
|
-
readonly dark: readonly ["#
|
|
431
|
+
readonly dark: readonly ["#E8B824", "#1F8A8B", "#7EA2E8", "#2EA56A", "#D64545"];
|
|
418
432
|
};
|
|
419
433
|
/**
|
|
420
434
|
* Cặp nền/chữ (hex) của avatar cho một tên, theo theme.
|
package/dist/tokens.js
CHANGED
|
@@ -176,10 +176,13 @@ const semantic = {
|
|
|
176
176
|
destructiveInk: "#D64545",
|
|
177
177
|
/** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */
|
|
178
178
|
border: "#1E2128",
|
|
179
|
-
/** Viền ô nhập bản dark của comitor-ds
|
|
180
|
-
|
|
179
|
+
/** Viền ô nhập bản dark — KHÔNG phải #1E2128 của comitor-ds (chỉ 1,10:1 trên `card`, ô nhập tan
|
|
180
|
+
* vào nền). #434A5A = 2,00:1, mirror `.dark { --input }` từ `0.5.0`; trước `3.3.3` mirror này còn
|
|
181
|
+
* ghi #1E2128. */
|
|
182
|
+
input: "#434A5A",
|
|
181
183
|
ring: "#F5D060",
|
|
182
|
-
|
|
184
|
+
/** = `input` (`--control-edge: var(--input)`). */
|
|
185
|
+
controlEdge: "#434A5A",
|
|
183
186
|
/** comitor-ds dùng `dark:hover:border-gray-500` = `ash[500]` (lật chiều so với light). */
|
|
184
187
|
controlEdgeStrong: ash[500],
|
|
185
188
|
/** comitor-ds tô `dark:data-[state=checked]:bg-gold-300`, nhưng token `primary` của chính nó
|
|
@@ -214,11 +217,14 @@ const highContrast = {
|
|
|
214
217
|
input: "#7E8290",
|
|
215
218
|
controlEdge: "#7E8290",
|
|
216
219
|
controlEdgeStrong: ash[700],
|
|
217
|
-
/**
|
|
218
|
-
|
|
220
|
+
/** #B48700 = gold-400 tối đi 2%: 3,28:1 trên `background`/`card` · 3,06:1 trên `muted` · ⇄ núm
|
|
221
|
+
* trắng 3,28:1 (bảng mặc định: 1,86 / 1,86). Trước `3.3.3` mirror này còn ghi gold-500 — giá trị
|
|
222
|
+
* `styles.css` đã bỏ. */
|
|
223
|
+
controlOn: "#B48700",
|
|
219
224
|
/** = `background`, nên luôn tương phản đúng chiều với cả rãnh BẬT lẫn rãnh TẮT. */
|
|
220
225
|
controlKnob: "#FFFFFF",
|
|
221
|
-
|
|
226
|
+
/** MỰC trên nền gold (5,77:1) — cùng cách vẽ với bảng mặc định; trước `3.3.3` mirror này ghi trắng. */
|
|
227
|
+
controlCheck: brandNeutral.ink,
|
|
222
228
|
switchTrack: "#7E8290",
|
|
223
229
|
sliderTrack: "#7E8290",
|
|
224
230
|
primaryInk: gold[500],
|
|
@@ -280,7 +286,9 @@ const status = {
|
|
|
280
286
|
warningInk: accentInk.light.yellow,
|
|
281
287
|
severeInk: accentInk.light.orange,
|
|
282
288
|
infoInk: accentInk.light.teal,
|
|
283
|
-
|
|
289
|
+
/** = `semantic.light.destructiveInk` (#D64545 — `--destructive-ink: var(--destructive)` ở bảng mặc
|
|
290
|
+
* định). Trước `3.3.3` trỏ nhầm `accentInk.light.red` (#9A3537 — bậc của bảng TƯƠNG PHẢN CAO). */
|
|
291
|
+
destructiveInk: semantic.light.destructiveInk
|
|
284
292
|
},
|
|
285
293
|
dark: {
|
|
286
294
|
success: accent.dark.green,
|
|
@@ -297,12 +305,12 @@ const status = {
|
|
|
297
305
|
warningInk: accentInk.dark.yellow,
|
|
298
306
|
severeInk: accentInk.dark.orange,
|
|
299
307
|
infoInk: accentInk.dark.teal,
|
|
300
|
-
destructiveInk:
|
|
308
|
+
destructiveInk: semantic.dark.destructiveInk
|
|
301
309
|
}
|
|
302
310
|
};
|
|
303
311
|
const chart = {
|
|
304
312
|
light: [gold[300], accent.light.teal, accent.light.navy, accent.light.green, accent.light.red],
|
|
305
|
-
dark: [gold[
|
|
313
|
+
dark: [gold[300], accent.dark.teal, accentInk.dark.navy, accent.dark.green, accent.dark.red]
|
|
306
314
|
};
|
|
307
315
|
const avatarTonePalette = {
|
|
308
316
|
light: {
|