Files
Webserver/components/mdx/TableOfContents.tsx

80 lines
2.7 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[] }) {
const [activeId, setActiveId] = useState<string>('');
useEffect(() => {
const handleScroll = () => {
const headingElements = headings.map((heading) => ({
id: heading.id,
element: document.getElementById(heading.id),
}));
// Find the first heading that is currently visible or just above the fold
// We look for headings that are above the 150px mark
let currentActiveId = '';
for (const { id, element } of headingElements) {
if (!element) continue;
const rect = element.getBoundingClientRect();
// If the heading is within the top portion of the screen
// OR if we haven't found a better one yet, this one 'might' be it
// We basically want the *last* heading that has a 'top' value <= some threshold
if (rect.top <= 150) {
currentActiveId = id;
}
}
// If we are at the very bottom, it's likely the last item
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 50) {
if (headings.length > 0) currentActiveId = headings[headings.length - 1].id;
}
if (currentActiveId) {
setActiveId(currentActiveId);
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
// Trigger once on mount
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, [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) => (
<li
key={heading.id}
2026-04-11 23:27:29 -04:00
style={{ paddingLeft: `${(heading.level - 2) * 10}px` }}
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>
);
}