"use client" import * as React from "react" import Link from "next/link" import { usePathname } from "next/navigation" import { PAGES_NEW } from "@/lib/docs" import { DOCS_SIDEBAR_SCROLL_STORAGE_KEY } from "@/lib/docs-sidebar-scroll" import { showMcpDocs } from "@/lib/flags" import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree" import type { source } from "@/lib/source" import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, } from "@/registry/new-york-v4/ui/sidebar" const TOP_LEVEL_SECTIONS = [ { name: "Introduction", href: "/docs" }, { name: "Components", href: "/docs/components", }, { name: "Installation", href: "/docs/installation", }, { name: "Theming", href: "/docs/theming", }, { name: "CLI", href: "/docs/cli", }, { name: "Typeset", href: "/docs/typeset", }, { name: "Skills", href: "/docs/skills", }, { name: "Registry", href: "/docs/registry", }, { name: "Changelog", href: "/docs/changelog", }, ] const EXCLUDED_SECTIONS = ["installation", "dark-mode", "changelog", "rtl"] const EXCLUDED_PAGES = ["/docs", "/docs/changelog", "/docs/rtl", "/docs/new"] function readScrollState() { try { return JSON.parse( sessionStorage.getItem(DOCS_SIDEBAR_SCROLL_STORAGE_KEY) ?? "" ) as { pathname: string scrollTop: number } } catch { return null } } function saveScrollState(container: HTMLElement) { try { sessionStorage.setItem( DOCS_SIDEBAR_SCROLL_STORAGE_KEY, JSON.stringify({ pathname: location.pathname, scrollTop: container.scrollTop, }) ) } catch {} } function getActiveItem(container: HTMLElement) { const items = container.querySelectorAll('[data-active="true"]') let active: HTMLElement | null = null let activePathLength = -1 let activeDistance = Infinity const containerRect = container.getBoundingClientRect() const containerCenter = containerRect.top + container.clientHeight / 2 for (const item of items) { const link = item.querySelector("a[href]") const href = item.getAttribute("href") ?? link?.getAttribute("href") const pathLength = href?.length ?? 0 const itemRect = item.getBoundingClientRect() const distance = Math.abs( itemRect.top + itemRect.height / 2 - containerCenter ) if ( pathLength > activePathLength || (pathLength === activePathLength && distance < activeDistance) ) { active = item activePathLength = pathLength activeDistance = distance } } return active } export function DocsSidebar({ tree, ...props }: React.ComponentProps & { tree: typeof source.pageTree }) { const pathname = usePathname() const currentBase = getCurrentBase(pathname) const contentRef = React.useRef(null) React.useLayoutEffect(() => { const container = contentRef.current if (!container) { return } const scrollState = readScrollState() if (scrollState?.pathname === pathname) { container.scrollTop = scrollState.scrollTop } else { // Prefer the longest route because section links also match by prefix. // Equal routes keep the item closest to the current viewport. const active = getActiveItem(container) if (active) { const containerRect = container.getBoundingClientRect() const activeRect = active.getBoundingClientRect() if ( activeRect.top < containerRect.top || activeRect.bottom > containerRect.bottom ) { container.scrollTop += activeRect.top - containerRect.top - (container.clientHeight - activeRect.height) / 2 } } } saveScrollState(container) }, [pathname]) React.useEffect(() => { const container = contentRef.current if (!container) { return } const onScroll = () => saveScrollState(container) container.addEventListener("scroll", onScroll, { passive: true }) return () => container.removeEventListener("scroll", onScroll) }, []) return (
Sections {TOP_LEVEL_SECTIONS.map(({ name, href }) => { if (!showMcpDocs && href.includes("/mcp")) { return null } return ( {name} {PAGES_NEW.includes(href) && ( )} ) })} {tree.children.map((item) => { if (EXCLUDED_SECTIONS.includes(item.$id ?? "")) { return null } return ( {item.name} {item.type === "folder" && ( {getPagesFromFolder(item, currentBase).map((page) => { if (!showMcpDocs && page.url.includes("/mcp")) { return null } if (EXCLUDED_PAGES.includes(page.url)) { return null } return ( {page.name} {PAGES_NEW.includes(page.url) && ( )} ) })} )} ) })} ) }