"use client"; import clsx from "clsx"; import Link from "next/link"; import { useRef, useCallback } from "react"; import { ScrollToTop } from "./ScrollToTop"; import { TocItem } from "@/lib/toc"; interface TocObserverProps { data: TocItem[]; activeId?: string | null; onActiveIdChange?: (id: string | null) => void; } export default function TocObserver({ data, activeId: externalActiveId, onActiveIdChange, }: TocObserverProps) { const itemRefs = useRef>(new Map()); const activeId = externalActiveId ?? null; const handleLinkClick = useCallback( (id: string) => { onActiveIdChange?.(id); }, [onActiveIdChange] ); return (
{/* Single vertical line on the left */}
{data.map(({ href, level, text }) => { const id = href.slice(1); const isActive = activeId === id; // Calculate padding based on level for indentation const levelPadding = (level - 2) * 16; // 0px for level 2, 16px for level 3, 32px for level 4, etc return (
{/* Horizontal line connected to vertical line + Dot */}
{/* Horizontal line from vertical line to dot */}
{/* Dot */}
{/* Text link with indentation padding */} handleLinkClick(id)} aria-current={isActive ? "location" : undefined} className={clsx("flex flex-1 items-center py-2 transition-all duration-200", { "text-primary dark:text-primary font-medium": isActive, "text-muted-foreground hover:text-foreground dark:hover:text-foreground/90": !isActive, })} style={{ paddingLeft: `${levelPadding + 6}px` }} ref={(el: HTMLAnchorElement | null) => { const map = itemRefs.current; if (el) { map.set(id, el); } else { map.delete(id); } }} > {text}
); })}
{/* Add scroll to top link at the bottom of TOC */}
); }