Files
kandev/apps/web/components/app-sidebar/app-sidebar-header.tsx
T

131 lines
5.1 KiB
TypeScript

"use client";
import Link from "@/components/routing/app-link";
import { useTranslation } from "react-i18next";
import { IconLayoutSidebarLeftCollapse, IconLayoutSidebarLeftExpand } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useAppStore } from "@/components/state-provider";
import { useOfficeModeState } from "@/hooks/use-in-office";
import { cn } from "@/lib/utils";
import { workspaceHomeHref } from "./app-sidebar-workspace-navigation";
import { AppSidebarWorkspacePicker } from "./app-sidebar-workspace-picker";
type AppSidebarHeaderProps = {
collapsed: boolean;
hoverRevealed?: boolean;
onToggleCollapse: () => void;
};
const COLLAPSE_BUTTON_CLASS = "h-7 w-7 shrink-0 cursor-pointer";
export function AppSidebarHeader({
collapsed,
hoverRevealed = false,
onToggleCollapse,
}: AppSidebarHeaderProps) {
const { t } = useTranslation();
const toggleLabel = t(hoverRevealed ? "sidebar:expandSidebar" : "sidebar:collapseSidebar");
const ToggleIcon = hoverRevealed ? IconLayoutSidebarLeftExpand : IconLayoutSidebarLeftCollapse;
const workspaces = useAppStore((s) => s.workspaces);
const startupPage = useAppStore((s) => s.userSettings.startupPage);
// The global WORKSPACE_PICKER shortcut opens this instance (and only this
// one) through the store; the mobile sheet keeps its own local open state.
const pickerOpen = useAppStore((s) => s.appSidebar.workspacePickerOpen);
const setPickerOpen = useAppStore((s) => s.setWorkspacePickerOpen);
const pickerProps = hoverRevealed ? {} : { open: pickerOpen, onOpenChange: setPickerOpen };
const mode = useOfficeModeState();
const activeWorkspace = workspaces.items.find(
(workspace) => workspace.id === workspaces.activeId,
);
const homeDisabled = mode === "unknown";
const homeHref = homeDisabled ? "#" : workspaceHomeHref(activeWorkspace, startupPage);
if (collapsed) {
// Minimal rail: brand home + expand. The workspace switcher lives only in
// the expanded header — a lone workspace glyph here read as noise.
return (
<div
data-testid="app-sidebar-header"
data-window-controls-overlay-region="sidebar"
className="flex flex-col items-center gap-1 px-1 py-1.5 border-b border-border shrink-0"
>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={homeHref}
aria-disabled={homeDisabled || undefined}
onClick={homeDisabled ? (event) => event.preventDefault() : undefined}
aria-label={t("sidebar:kandevHome")}
className="flex h-7 w-7 items-center justify-center rounded-md text-foreground/80 hover:bg-muted/60 cursor-pointer"
>
<span className="text-base font-bold tracking-tight">K</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">Kandev</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={COLLAPSE_BUTTON_CLASS}
data-sidebar-toggle
onClick={onToggleCollapse}
aria-label={t("sidebar:expandSidebar")}
>
<IconLayoutSidebarLeftExpand className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">{t("sidebar:expandSidebar")}</TooltipContent>
</Tooltip>
</div>
);
}
// Single h-10 row — brand · workspace picker · collapse — so the sidebar's
// top section lines up with the page/dockview top bar (also h-10). Brand and
// workspace share the same text size so they sit on a common baseline; the
// brand carries weight/colour, the workspace stays muted and secondary.
return (
<div
data-testid="app-sidebar-header"
data-window-controls-overlay-region="sidebar"
className="flex items-center gap-1.5 h-10 px-3 shrink-0 border-b border-border"
>
<Link
href={homeHref}
aria-disabled={homeDisabled || undefined}
onClick={homeDisabled ? (event) => event.preventDefault() : undefined}
aria-label={t("sidebar:kandevHome")}
className={cn(
"shrink-0 cursor-pointer text-sm font-semibold tracking-tight",
"text-foreground hover:text-foreground/80 transition-colors",
)}
>
Kandev
</Link>
<span aria-hidden className="shrink-0 select-none text-muted-foreground/30">
/
</span>
{/* Hover already provides an anchor; the global picker action persistently expands the rail. */}
<AppSidebarWorkspacePicker {...pickerProps} />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={COLLAPSE_BUTTON_CLASS}
data-sidebar-toggle
onClick={onToggleCollapse}
aria-label={toggleLabel}
>
<ToggleIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">{toggleLabel}</TooltipContent>
</Tooltip>
</div>
);
}