Files
Webserver/components/mdx/TableOfContents.tsx

61 lines
2.0 KiB
TypeScript
Raw Normal View History

2026-02-07 20:17:46 -05:00
'use client';
import { useEffect, useState } from 'react';
type Heading = {
id: string;
text: string;
level: number;
};
export function TableOfContents({ headings }: { headings: Heading[] }) {
2026-05-25 09:49:40 -04:00
const [activeId, setActiveId] = useState<string>(() => headings[0]?.id ?? '');
2026-02-07 20:17:46 -05:00
useEffect(() => {
2026-05-25 09:49:40 -04:00
const elements = headings
.map((heading) => document.getElementById(heading.id))
.filter((element): element is HTMLElement => Boolean(element));
2026-02-07 20:17:46 -05:00
2026-05-25 09:49:40 -04:00
if (elements.length === 0) return;
2026-02-07 20:17:46 -05:00
2026-05-25 09:49:40 -04:00
const observer = new IntersectionObserver((entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
2026-02-07 20:17:46 -05:00
2026-05-25 09:49:40 -04:00
if (visible[0]?.target.id) {
setActiveId(visible[0].target.id);
2026-02-07 20:17:46 -05:00
}
2026-05-25 09:49:40 -04:00
}, {
rootMargin: '-20% 0px -65% 0px',
threshold: 0,
});
2026-02-07 20:17:46 -05:00
2026-05-25 09:49:40 -04:00
elements.forEach((element) => observer.observe(element));
return () => observer.disconnect();
2026-02-07 20:17:46 -05:00
}, [headings]);
if (headings.length === 0) return null;
return (
2026-04-11 23:27:29 -04:00
<nav aria-label="Table of contents" className="text-left">
<h4 className="eyebrow mb-4">Contents</h4>
<ul className="space-y-2">
2026-02-07 20:17:46 -05:00
{Array.isArray(headings) && headings.map((heading) => (
2026-05-25 09:49:40 -04:00
<li key={heading.id} className={heading.level > 2 ? 'pl-3' : undefined}>
2026-02-07 20:17:46 -05:00
<a
href={`#${heading.id}`}
2026-04-11 23:27:29 -04:00
className={`block text-[0.82rem] leading-6 transition-colors ${activeId === heading.id
? 'text-ink'
: 'text-muted hover:text-ink'
2026-02-07 20:17:46 -05:00
}`}
>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
);
}