"use client"; import { AnimatePresence, MotionConfig, motion } from "framer-motion"; import { useSearchContext } from "fumadocs-ui/contexts/search"; import { ChevronDownIcon, GitBranch, Search } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import type { ReactNode } from "react"; import { useEffect, useRef, useState } from "react"; import type { ListItem } from "@/components/sidebar-content"; import { contents } from "@/components/sidebar-content"; import { ThemeToggle } from "@/components/theme-toggle"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; type Section = (typeof contents)[number]; export function DocsSidebar() { const pathname = usePathname(); const searchParams = useSearchParams(); const branch = searchParams.get("branch"); const { setOpenSearch } = useSearchContext(); const [currentOpen, setCurrentOpen] = useState(0); const navRef = useRef(null); const branchQuery = branch === "canary" ? "?branch=canary" : ""; const getDefaultOpen = (sections: Section[]) => { const defaultValue = sections.findIndex((item) => item.list.some( (listItem) => listItem.href === pathname || (listItem.subpages && listItem.subpages.length > 0 && pathname.startsWith(`${listItem.href}/`)) || listItem.subpages?.some((sp) => sp.href && pathname === sp.href), ), ); return defaultValue === -1 ? 0 : defaultValue; }; useEffect(() => { setCurrentOpen(getDefaultOpen(contents)); }, [pathname]); // Scroll the active item into view after section expands useEffect(() => { const timer = setTimeout(() => { const nav = navRef.current; if (!nav) return; const activeEl = nav.querySelector("[data-active='true']"); if (!activeEl) return; const navRect = nav.getBoundingClientRect(); const elRect = activeEl.getBoundingClientRect(); // Only scroll if the active item is outside the visible area const isAbove = elRect.top < navRect.top; const isBelow = elRect.bottom > navRect.bottom; if (isAbove || isBelow) { activeEl.scrollIntoView({ block: "center", behavior: "smooth" }); } }, 380); // wait for expand animation to finish return () => clearTimeout(timer); }, [pathname, currentOpen]); return ( {/* Branch switcher */} {/* Scrollable navigation area */} {/* Footer: GitHub + Theme Toggle */}
); } // ─── Branch Switcher ────────────────────────────────────────────────────────── function BranchSwitcher() { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const branch = searchParams.get("branch") === "canary" ? "canary" : "main"; const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { function handleClickOutside(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const switchBranch = (target: "main" | "canary") => { setOpen(false); if (target === branch) return; const params = new URLSearchParams(searchParams.toString()); if (target === "canary") { params.set("branch", "canary"); } else { params.delete("branch"); } const qs = params.toString(); router.push(qs ? `${pathname}?${qs}` : pathname); }; return (
{open && ( )}
); } // ─── Collapsible Section ────────────────────────────────────────────────────── function SidebarSection({ section, pathname, branchQuery, }: { section: Section; pathname: string; branchQuery: string; }) { return (
{section.href && ( Overview )} {section.list.map((item, i) => { if (item.separator) { return (

{item.title}

); } if (item.group) { return (

{item.title}

); } if (!item.href) return null; const hasSubpages = !!(item.subpages && item.subpages.length > 0); const subpageMatch = hasSubpages && item.subpages?.some((sp) => sp.href && pathname === sp.href); const active = pathname === item.href || subpageMatch || (!!(item.subpages && item.subpages.length > 0) && pathname.startsWith(`${item.href}/`)); return ( ); })}
); } // ─── Sidebar Item with Subpages ─────────────────────────────────────────────── function SidebarItemWithSubpages({ item, active, pathname, branchQuery, hasSubpages, }: { item: ListItem; active: boolean; pathname: string; branchQuery: string; hasSubpages: boolean | undefined; }) { const showSubpages = hasSubpages && active; return (
} isNew={item.isNew} > {item.title} {showSubpages && item.subpages && (
{item.subpages.map((subpage, i) => { if (subpage.group) { return (

{subpage.title}

); } if (!subpage.href) return null; return ( ) : undefined } > {subpage.title} ); })}
)}
); } // ─── Subpage Link ───────────────────────────────────────────────────────────── function SubpageLink({ href, active, icon, children, }: { href: string; active: boolean; icon?: ReactNode; children: ReactNode; }) { return ( {icon && ( svg]:size-[12px] transition-colors duration-150", active ? "text-foreground" : "text-foreground/55", )} > {icon} )} {children} ); } // ─── Sidebar Link ───────────────────────────────────────────────────────────── function SidebarLink({ href, active, icon, isNew, children, }: { href: string; active: boolean; icon?: ReactNode; isNew?: boolean; children: ReactNode; }) { return ( {icon && ( {icon} )} {children} {isNew && } ); } function NewBadge({ isSelected }: { isSelected?: boolean }) { return ( New ); }