chrysopedia/frontend/src/components/TableOfContents.tsx
jlightner 1a7c11cac1 feat: Moved Table of Contents from main prose column to sidebar top; re…
- "frontend/src/pages/TechniquePage.tsx"
- "frontend/src/components/TableOfContents.tsx"
- "frontend/src/App.css"

GSD-Task: S04/T01
2026-04-03 05:52:47 +00:00

58 lines
1.8 KiB
TypeScript

/**
* Table of Contents for v2 technique pages with nested sections.
*
* Renders a nested list of anchor links matching the H2/H3 section structure.
* Uses slugified headings as IDs for scroll targeting.
*/
import type { BodySectionV2 } from "../api/public-client";
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
interface TableOfContentsProps {
sections: BodySectionV2[];
}
export default function TableOfContents({ sections }: TableOfContentsProps) {
if (sections.length === 0) return null;
return (
<nav className="technique-toc" aria-label="Table of contents">
<h3 className="technique-toc__title">On this page</h3>
<ul className="technique-toc__list">
{sections.map((section) => {
const sectionSlug = slugify(section.heading);
return (
<li key={sectionSlug} className="technique-toc__item">
<a href={`#${sectionSlug}`} className="technique-toc__link">
{section.heading}
</a>
{section.subsections.length > 0 && (
<ul className="technique-toc__sublist">
{section.subsections.map((sub) => {
const subSlug = `${sectionSlug}--${slugify(sub.heading)}`;
return (
<li key={subSlug} className="technique-toc__subitem">
<a
href={`#${subSlug}`}
className="technique-toc__sublink"
>
{sub.heading}
</a>
</li>
);
})}
</ul>
)}
</li>
);
})}
</ul>
</nav>
);
}