"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import GithubSlugger from "github-slugger"; import { AlignLeft, ArrowUp, Hash } from "lucide-react"; interface TOCEntry { id: string; text: string; level: number; } export default function TOC({ source }: { source: string }) { const [toc, setToc] = useState([]); const [activeId, setActiveId] = useState(""); useEffect(() => { const slugger = new GithubSlugger(); const lines = source.split("\n"); const entries: TOCEntry[] = []; for (const line of lines) { const m = line.match(/^(#{2,3})\s+(.+?)\s*#*\s*$/); if (m) { const level = m[1].length; const text = m[2].trim(); entries.push({ id: slugger.slug(text), text, level }); } } setToc(entries); }, [source]); // Scroll spy using IntersectionObserver useEffect(() => { if (toc.length === 0) return; const headingElements = toc .map((entry) => document.getElementById(entry.id)) .filter((el): el is HTMLElement => el !== null); const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { setActiveId(entry.target.id); break; } } }, { rootMargin: "-80px 0% -60% 0%", threshold: 0.1, }, ); headingElements.forEach((el) => observer.observe(el)); return () => observer.disconnect(); }, [toc]); if (toc.length === 0) return null; function scrollToTop() { window.scrollTo({ top: 0, behavior: "smooth" }); } return ( ); }