e7738de6d2
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
141 lines
5.2 KiB
TypeScript
141 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { ChevronDownIcon, HamburgerIcon } from "@/components/icons";
|
|
import { Sheet, SheetTrigger, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
|
import type { DocsGroup } from "@/lib/types";
|
|
|
|
const COLLAPSE_STORAGE_KEY = "docs-sidebar-collapsed";
|
|
|
|
type SidebarNavProps = {
|
|
groups: DocsGroup[];
|
|
activeSlug: string;
|
|
onNavigate?: () => void;
|
|
};
|
|
|
|
function SidebarNav({ groups, activeSlug, onNavigate }: SidebarNavProps) {
|
|
const initialCollapsed = new Set(
|
|
groups
|
|
.filter((g) => g.section !== "Start Here" && !g.items.some((i) => i.slug === activeSlug))
|
|
.map((g) => g.section),
|
|
);
|
|
const [collapsed, setCollapsed] = useState<Set<string>>(initialCollapsed);
|
|
const restored = useRef(false);
|
|
|
|
// Restore the user's expand/collapse choices so navigating between pages
|
|
// does not reset sections the user opened. (The active section is always
|
|
// shown expanded via the `hasActive` check below, regardless of this set.)
|
|
useEffect(() => {
|
|
if (restored.current) return;
|
|
restored.current = true;
|
|
try {
|
|
const stored = sessionStorage.getItem(COLLAPSE_STORAGE_KEY);
|
|
if (stored) setCollapsed(new Set(JSON.parse(stored) as string[]));
|
|
} catch {}
|
|
}, []);
|
|
|
|
function toggle(section: string) {
|
|
setCollapsed((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(section)) next.delete(section);
|
|
else next.add(section);
|
|
try {
|
|
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify([...next]));
|
|
} catch {}
|
|
return next;
|
|
});
|
|
}
|
|
|
|
return (
|
|
<nav className="px-3 py-4">
|
|
{groups.map((group) => {
|
|
const hasActive = group.items.some((item) => item.slug === activeSlug);
|
|
const isCollapsed = collapsed.has(group.section) && !hasActive;
|
|
return (
|
|
<div key={group.section} className="mb-4">
|
|
<button
|
|
type="button"
|
|
aria-expanded={!isCollapsed}
|
|
onClick={() => toggle(group.section)}
|
|
className="flex w-full cursor-pointer items-center justify-between rounded bg-transparent px-3 py-2 text-[0.6875rem] font-semibold uppercase tracking-[0.06em] text-muted transition hover:text-fg"
|
|
>
|
|
<span>{group.section}</span>
|
|
<ChevronDownIcon className={`shrink-0 transition-transform ${isCollapsed ? "-rotate-90" : ""}`} />
|
|
</button>
|
|
<div
|
|
className={`grid transition-[grid-template-rows] duration-200 ${
|
|
isCollapsed ? "grid-rows-[0fr]" : "grid-rows-[1fr]"
|
|
}`}
|
|
>
|
|
<div className="overflow-hidden">
|
|
{group.items.map((item) => {
|
|
const active = item.slug === activeSlug;
|
|
return (
|
|
<Link
|
|
key={item.slug}
|
|
href={item.path}
|
|
aria-current={active ? "page" : undefined}
|
|
onClick={onNavigate}
|
|
className={`block rounded px-3 py-[0.1875rem] text-[0.8125rem] leading-[1.8] no-underline transition hover:text-fg ${
|
|
active ? "font-medium text-fg" : "text-muted"
|
|
}`}
|
|
>
|
|
{item.title}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
type DocsSidebarShellProps = {
|
|
groups: DocsGroup[];
|
|
};
|
|
|
|
function activeDocForPath(groups: DocsGroup[], pathname: string) {
|
|
const normalized = pathname.replace(/\/$/, "") || "/";
|
|
return groups.flatMap((group) => group.items).find((item) => item.path === normalized);
|
|
}
|
|
|
|
export function DocsSidebarShell({ groups }: DocsSidebarShellProps) {
|
|
const pathname = usePathname();
|
|
const activeDoc = activeDocForPath(groups, pathname);
|
|
const activeSlug = activeDoc?.slug ?? "";
|
|
const currentTitle = activeDoc?.title;
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<>
|
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
<SheetTrigger
|
|
aria-label="Open table of contents"
|
|
className="sticky top-14 z-40 flex w-full items-center justify-between border-b border-border bg-bg/80 px-6 py-3 backdrop-blur-sm focus:outline-none md:hidden"
|
|
>
|
|
<span className="text-sm font-medium text-fg">{currentTitle ?? "Documentation"}</span>
|
|
<span className="flex h-8 w-8 items-center justify-center text-muted">
|
|
<HamburgerIcon className="h-4 w-4" />
|
|
</span>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="overflow-y-auto p-0" showCloseButton={false}>
|
|
<SheetTitle className="px-6 pt-6">Table of Contents</SheetTitle>
|
|
<SidebarNav groups={groups} activeSlug={activeSlug} onNavigate={() => setOpen(false)} />
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
<aside
|
|
aria-label="Documentation"
|
|
className="hidden md:sticky md:top-14 md:block md:h-[calc(100vh-3.5rem)] md:w-60 md:overflow-y-auto"
|
|
>
|
|
<SidebarNav groups={groups} activeSlug={activeSlug} />
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|