"use client"; import { useCallback, useLayoutEffect, useRef, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { IconFolder } from "@tabler/icons-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@kandev/ui/dropdown-menu"; import Link from "@/components/routing/app-link"; import { useAppStore } from "@/components/state-provider"; import { useFeature } from "@/hooks/domains/features/use-feature"; import { useRouter } from "@/lib/routing/client-router"; import { getWorkspaceSettingsTabs, workspaceSettingsHref, type WorkspaceSettingsTab, } from "@/lib/settings/workspace-settings-tabs"; import { WorkspacePickerContent, WorkspaceTrigger, type WorkspaceItem, } from "@/components/workspaces/workspace-picker-content"; import { cn } from "@kandev/ui/lib/utils"; // The tab table and href builder are data — see `workspace-settings-tabs.ts`, // which the settings menu's Workspaces branch reads too. Re-exported here so // existing callers keep one import. // The badge is shared with the settings menu and the workspace list — see // `record-badges.tsx`. Re-exported so existing callers keep one import. import { ActiveWorkspaceBadge } from "@/components/settings/record-badges"; export { ActiveWorkspaceBadge }; export { workspaceSettingsHref, workspaceSettingsTabSpec, type WorkspaceSettingsTab, } from "@/lib/settings/workspace-settings-tabs"; /** * The workspace switcher this page is headed by — the same control the sidebar * header draws, so the two read as one design (see * `workspace-picker-content.tsx`). It is a page-to-page navigator, not an * activation control: picking a workspace opens that workspace's copy of the * tab you are on and leaves the active workspace alone. The "Active" pill in * the list is what tells the two apart. */ function WorkspaceSettingsSwitcher({ workspaceName, workspaceId, activeTab, }: { workspaceName: string; workspaceId: string; activeTab: WorkspaceSettingsTab; }) { const router = useRouter(); const officeEnabled = useFeature("office"); const workspaces = useAppStore((s) => s.workspaces.items); const activeId = useAppStore((s) => s.workspaces.activeId); const [open, setOpen] = useState(false); const handleSelect = useCallback( (next: WorkspaceItem) => { if (next.id !== workspaceId) { router.push(workspaceSettingsHref(next.id, activeTab)); } setOpen(false); }, [router, workspaceId, activeTab], ); const handleNavigate = useCallback( (href: string) => { router.push(href); setOpen(false); }, [router], ); return ( <> {/* The page-level mark: the closed picker names the workspace being edited, so when that is also the active one, say so without opening the menu. */} {workspaceId === activeId && ( )} ); } /** * The tabbed shell every workspace settings page renders through: the * workspace name doubles as a switcher (move between workspaces without going * back to the list — the breadcrumb's Workspaces crumb owns the way back), * plus one tab per section. The menu holds no workspace rows, so this is the * only workspace-level navigation surface. */ export function WorkspaceSettingsShell({ workspaceId, activeTab, children, }: { workspaceId: string; activeTab: WorkspaceSettingsTab; children: ReactNode; }) { const { t } = useTranslation(); const workspaces = useAppStore((s) => s.workspaces.items); const canvasesEnabled = useFeature("canvases"); const workspace = workspaces.find((item) => item.id === workspaceId); const tabs = getWorkspaceSettingsTabs(canvasesEnabled); const tabsRef = useRef(null); // Each tab is its own route, so navigating remounts this shell and the // strip snaps back to its start — on a phone the pill you just tapped // scrolls out of view. Centre the active pill before paint instead; when // the strip fits (desktop rail), there is nothing to scroll and this is a // no-op. useLayoutEffect(() => { const nav = tabsRef.current; if (!nav || nav.scrollWidth <= nav.clientWidth) return; const pill = nav.querySelector('[aria-current="page"]'); if (!pill) return; const navRect = nav.getBoundingClientRect(); const pillRect = pill.getBoundingClientRect(); nav.scrollLeft += pillRect.left - navRect.left - (nav.clientWidth - pillRect.width) / 2; }, [activeTab]); return (
{workspace ? ( ) : (

{t("common:workspace")}

)}
{/* Pills on a phone, an underline rail from `md` up — the same boundary the sidebar uses, so the strip changes shape exactly when the sidebar that would otherwise carry this navigation disappears. Scrolls horizontally at both sizes; six sections outrun a phone's width, and wrapping them would push the page content below the fold. */}
{children}
); }