"use client" import clsx from "clsx" import Link from "next/link" import { useState, useRef, useEffect, useCallback } from "react" import { motion } from "framer-motion" 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] ) // Function to check if an item has children const hasChildren = (currentId: string, currentLevel: number) => { const currentIndex = data.findIndex((item) => item.href.slice(1) === currentId) if (currentIndex === -1 || currentIndex === data.length - 1) return false const nextItem = data[currentIndex + 1] return nextItem.level > currentLevel } // Calculate scroll progress for the active section const [scrollProgress, setScrollProgress] = useState(0) useEffect(() => { const handleScroll = () => { if (!activeId) return const activeElement = document.getElementById(activeId) if (!activeElement) return const rect = activeElement.getBoundingClientRect() const windowHeight = window.innerHeight const elementTop = rect.top const elementHeight = rect.height // Calculate how much of the element is visible let progress = 0 if (elementTop < windowHeight) { progress = Math.min(1, (windowHeight - elementTop) / (windowHeight + elementHeight)) } setScrollProgress(progress) } const container = document.getElementById("scroll-container") || window container.addEventListener("scroll", handleScroll, { passive: true }) // Initial calculation handleScroll() return () => container.removeEventListener("scroll", handleScroll) }, [activeId]) return (
{data.map(({ href, level, text }, index) => { const id = href.slice(1) const isActive = activeId === id const indent = level > 1 ? (level - 1) * 20 : 0 // Prefix with underscore to indicate intentionally unused const _isParent = hasChildren(id, level) const _isLastInLevel = index === data.length - 1 || data[index + 1].level <= level return (
{/* Simple L-shaped connector */} {level > 1 && (
{/* Vertical line */}
{isActive && ( )}
{/* Horizontal line */}
{isActive && ( )}
)} handleLinkClick(id)} className={clsx("relative flex items-center py-2 transition-colors", { "text-primary dark:text-primary font-medium": isActive, "text-muted-foreground hover:text-foreground dark:hover:text-foreground/90": !isActive, })} style={{ paddingLeft: `${indent}px`, marginLeft: level > 1 ? "12px" : "0", }} ref={(el) => { const map = itemRefs.current if (el) { map.set(id, el) } else { map.delete(id) } }} > {/* Circle indicator */}
{isActive && ( )}
{text}
) })}
{/* Add scroll to top link at the bottom of TOC */}
) }