diff --git a/components/toc-client.tsx b/components/toc-client.tsx new file mode 100644 index 0000000..a050a69 --- /dev/null +++ b/components/toc-client.tsx @@ -0,0 +1,85 @@ +'use client'; + +import * as React from 'react'; +import {useEffect, useState, useRef} from 'react'; +import clsx from 'clsx'; +import {ActiveHashLink} from './ui/ActiveHashLink'; +import {ScrollArea} from '@/components/ui/scroll-area'; + +interface TocEntry { + href: string; + level: number; + text: string; +} + +export default function TocClient({tocs}: {tocs: TocEntry[]}) { + const [activeId, setActiveId] = useState(null); + + useEffect(() => { + if (typeof window === 'undefined' || tocs.length === 0) return; + + const headingElements: HTMLElement[] = tocs + .map(entry => { + const id = entry.href.replace('#', ''); + return document.getElementById(id); + }) + .filter((el): el is HTMLElement => el !== null); + + if (headingElements.length === 0) return; + + const callback: IntersectionObserverCallback = entries => { + const visible = entries + .filter(e => e.isIntersecting) + .sort((a, b) => b.intersectionRatio - a.intersectionRatio); + + if (visible.length > 0) { + const id = visible[0].target.getAttribute('id'); + if (id) { + setActiveId(`#${id}`); + } + } + }; + + const observer = new IntersectionObserver(callback, { + root: null, // viewport + rootMargin: '0px 0px -80% 0px', + threshold: [0, 0.25, 0.5, 0.75, 1.0], + }); + + headingElements.forEach(elem => observer.observe(elem)); + + return () => observer.disconnect(); + }, [tocs]); + + return ( +
+
+ {tocs.length > 0 && ( + <> +

On this page

+ +
+ {tocs.map(({href, level, text}) => ( + + {text} + + ))} +
+
+ + )} +
+
+ ); +} diff --git a/components/toc.tsx b/components/toc.tsx index 14da7f8..0d833f8 100644 --- a/components/toc.tsx +++ b/components/toc.tsx @@ -1,38 +1,8 @@ -import {ScrollArea} from '@/components/ui/scroll-area'; +import TocClient from './toc-client'; import {getDocsTocs} from '@/lib/markdown'; -import clsx from 'clsx'; -import {ActiveHashLink} from './ui/ActiveHashLink'; export default async function Toc({path}: {path: string}) { const tocs = await getDocsTocs(path); - return ( -
-
- {tocs.length > 0 && ( - <> -

On this page

- -
- {tocs.map(({href, level, text}) => ( - - {text} - - ))} -
-
- - )} -
-
- ); + return ; }