"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. */}