import { Link, useLocation } from "@tanstack/react-router"; import { ChevronDown, ChevronRight } from "lucide-react"; import { type MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type DocsNavNode } from "~/docs"; interface DocsNavProps { nodes: DocsNavNode[]; mobile?: boolean; onNavigate?: () => void; } const ACTIVE_OPTIONS_EXACT = { exact: true }; function nodeContainsHref(node: DocsNavNode, href: string): boolean { if (node.type === "page") return node.href === href; if (node.type === "group" && node.href === href) return true; return node.children.some((child) => nodeContainsHref(child, href)); } function clsx(...classes: (string | false | null | undefined)[]): string { return classes.filter(Boolean).join(" "); } /** * The sidebar scrolls independently of the page, so the active entry has to * bring itself into view. The entry owns this rather than the layout, because * a link inside a collapsed group only mounts once that group opens, and * anything watching from above would have to query before that happens. */ function useScrollIntoViewWhenActive(isActive: boolean) { const ref = useRef(null); useEffect(() => { if (isActive) ref.current?.scrollIntoView({ block: "nearest" }); }, [isActive]); return ref; } function PageLink({ node, mobile, onNavigate, }: { node: Extract; mobile?: boolean; onNavigate?: () => void; }) { const location = useLocation(); const isActive = location.pathname === node.href; const ref = useScrollIntoViewWhenActive(isActive); return ( {node.label} ); } function GroupNode({ node, mobile, onNavigate, }: { node: Extract; mobile?: boolean; onNavigate?: () => void; }) { const location = useLocation(); const currentHref = location.pathname; const containsActive = useMemo(() => nodeContainsHref(node, currentHref), [node, currentHref]); const isActive = node.href === currentHref; const ref = useScrollIntoViewWhenActive(isActive); const [isOpen, setIsOpen] = useState(containsActive); const toggle = useCallback(() => setIsOpen((open) => !open), []); // Navigating into a group opens it. Navigating away must not close it, or a // group you opened to browse collapses the moment you click something else. useEffect(() => { if (containsActive) setIsOpen(true); }, [containsActive]); // The row is one target: it takes you to the group's page and opens it. // Clicking it again once you are already there collapses it. const handleClick = useCallback( (event: MouseEvent) => { if (isActive) { event.preventDefault(); toggle(); return; } setIsOpen(true); onNavigate?.(); }, [isActive, onNavigate, toggle], ); const hasChildren = node.children.length > 0; const chevron = isOpen ? : ; const rowClassName = clsx( "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm rounded-md transition-colors text-left", mobile ? "text-muted-foreground hover:text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-muted", containsActive && "text-foreground", isActive && !mobile && "bg-muted", ); return (
{node.href === undefined ? ( ) : ( {node.label} {hasChildren && chevron} )} {isOpen && hasChildren && (
)}
); } function CategoryNode({ node, mobile, onNavigate, }: { node: Extract; mobile?: boolean; onNavigate?: () => void; }) { return (
{node.label}
); } function NavTree({ nodes, mobile, onNavigate }: DocsNavProps) { return (
{nodes.map((node) => { if (node.type === "category") { return ( ); } if (node.type === "group") { return ( ); } return ( ); })}
); } export function DocsNav({ nodes, mobile, onNavigate }: DocsNavProps) { return (
); }