import { useCallback, useEffect, useState } from 'react' export type TocItem = { href: string text: string } type UseDocTocOptions = { appDetail: Record | null locale: string } const HEADER_OFFSET = 80 const SCROLL_CONTAINER_SELECTOR = '.overflow-auto' const getTargetId = (href: string) => href.replace('#', '') /** * Extract heading anchors from the rendered
as TOC items. */ const extractTocFromArticle = (): TocItem[] => { const article = document.querySelector('article') if (!article) return [] return Array.from(article.querySelectorAll('h2')) .map((heading) => { const anchor = heading.querySelector('a') if (!anchor) return null return { href: anchor.getAttribute('href') || '', text: anchor.textContent || '', } }) .filter((item): item is TocItem => item !== null) } /** * Custom hook that manages table-of-contents state: * - Extracts TOC items from rendered headings * - Tracks the active section on scroll * - Auto-expands the panel on wide viewports */ export const useDocToc = ({ appDetail, locale }: UseDocTocOptions) => { const [toc, setToc] = useState([]) const [isTocExpanded, setIsTocExpanded] = useState(() => { if (typeof window === 'undefined') return false return window.matchMedia('(min-width: 1280px)').matches }) const [activeSection, setActiveSection] = useState('') // Re-extract TOC items whenever the doc content changes useEffect(() => { const timer = setTimeout(() => { const tocItems = extractTocFromArticle() setToc(tocItems) if (tocItems.length > 0) setActiveSection(getTargetId(tocItems[0].href)) }, 0) return () => clearTimeout(timer) }, [appDetail, locale]) // Track active section based on scroll position useEffect(() => { const scrollContainer = document.querySelector(SCROLL_CONTAINER_SELECTOR) if (!scrollContainer || toc.length === 0) return const handleScroll = () => { let currentSection = '' for (const item of toc) { const targetId = getTargetId(item.href) const element = document.getElementById(targetId) if (element) { const rect = element.getBoundingClientRect() if (rect.top <= window.innerHeight / 2) currentSection = targetId } } if (currentSection && currentSection !== activeSection) setActiveSection(currentSection) } scrollContainer.addEventListener('scroll', handleScroll) return () => scrollContainer.removeEventListener('scroll', handleScroll) }, [toc, activeSection]) // Smooth-scroll to a TOC target on click const handleTocClick = useCallback((e: React.MouseEvent, item: TocItem) => { e.preventDefault() const targetId = getTargetId(item.href) const element = document.getElementById(targetId) if (!element) return const scrollContainer = document.querySelector(SCROLL_CONTAINER_SELECTOR) if (scrollContainer) { scrollContainer.scrollTo({ top: element.offsetTop - HEADER_OFFSET, behavior: 'smooth', }) } }, []) return { toc, isTocExpanded, setIsTocExpanded, activeSection, handleTocClick, } }