TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { usePathname } from "next/navigation";4import { cn } from "@/lib/utils";56interface TocItem {7 id: string;8 text: string;9 level: 2 | 3;10}1112/**13 * "On this page" table of contents. Reads h2/h3 headings from the rendered14 * `[data-docs-article]` element and highlights the section currently in view.15 */16export function DocsToc({ className }: { className?: string }) {17 const pathname = usePathname();18 const [items, setItems] = React.useState<TocItem[]>([]);19 const [active, setActive] = React.useState<string | null>(null);2021 React.useEffect(() => {22 // The article is rendered by a sibling server component; scan it once the frame has painted23 // so the DOM is complete, and subscribe to scroll position through an IntersectionObserver.24 let observer: IntersectionObserver | null = null;25 const frame = window.requestAnimationFrame(() => {26 const article = document.querySelector<HTMLElement>("[data-docs-article]");27 const headings = article ? Array.from(article.querySelectorAll<HTMLHeadingElement>("h2[id], h3[id]")) : [];28 const next: TocItem[] = headings.map((h) => ({29 id: h.id,30 text: (h.textContent ?? "").replace(/#\s*$/, "").trim(),31 level: h.tagName === "H2" ? 2 : 3,32 }));33 setItems(next);34 setActive(next[0]?.id ?? null);35 if (!headings.length) return;3637 const visible = new Map<string, number>();38 observer = new IntersectionObserver(39 (entries) => {40 for (const e of entries) {41 if (e.isIntersecting) visible.set(e.target.id, e.boundingClientRect.top);42 else visible.delete(e.target.id);43 }44 if (visible.size) {45 const top = [...visible.entries()].sort((a, b) => a[1] - b[1])[0]![0];46 setActive(top);47 } else {48 // Nothing intersecting: pick the last heading above the viewport.49 const above = headings.filter((h) => h.getBoundingClientRect().top < 120);50 if (above.length) setActive(above[above.length - 1]!.id);51 }52 },53 { rootMargin: "-80px 0px -60% 0px", threshold: [0, 1] },54 );55 headings.forEach((h) => observer!.observe(h));56 });57 return () => {58 window.cancelAnimationFrame(frame);59 observer?.disconnect();60 };61 }, [pathname]);6263 if (items.length < 2) return null;6465 return (66 <nav aria-label="On this page" className={cn("text-[12.5px]", className)}>67 <div className="mb-2 text-[11.5px] font-semibold uppercase tracking-wide text-fg-subtle">On this page</div>68 <ul className="space-y-px border-l border-border">69 {items.map((it) => (70 <li key={it.id}>71 <a72 href={`#${it.id}`}73 onClick={() => setActive(it.id)}74 className={cn(75 "-ml-px block border-l py-1 pr-2 leading-snug transition-colors",76 it.level === 3 ? "pl-6" : "pl-3",77 active === it.id ? "border-accent font-medium text-fg" : "border-transparent text-fg-muted hover:border-border-strong hover:text-fg",78 )}79 >80 {it.text}81 </a>82 </li>83 ))}84 </ul>85 </nav>86 );87}88