Files
mcpedia/apps/web/app/components/TOC.tsx
T
asepharyana 1dd16ebcba feat(web): Phase 11 UI/UX — TOC, dark mode toggle, /docs index
- Install rehype-slug (proper heading anchors) + github-slugger (matching TOC)
- TOC component: auto-generated from h2/h3 headings, clickable anchors
- Dark mode toggle: ThemeToggle (localStorage + system default), class-based
- /docs index page (lists all docs by section)
- Markdown.tsx uses rehype-slug for stable heading ids
- globals.css: @custom-variant dark (.dark class) for class-based dark mode
- layout.tsx: nav includes ThemeToggle

Note: per user instruction, installed real deps (rehype-slug, github-slugger,
@types/hast) instead of hacky inline any-cast hacks.
2026-08-20 13:21:53 +07:00

56 lines
1.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import GithubSlugger from "github-slugger";
interface TOCEntry {
id: string;
text: string;
level: number;
}
/** Extract h2/h3 headings from markdown and render as a clickable TOC. The
* heading `id`s are generated by rehype-slug (via github-slugger) in Markdown.tsx,
* so the same slugger is used here to produce matching anchors. */
export default function TOC({ source }: { source: string }) {
const [toc, setToc] = useState<TOCEntry[]>([]);
useEffect(() => {
const slugger = new GithubSlugger();
const lines = source.split("\n");
const entries: TOCEntry[] = [];
for (const line of lines) {
const m = line.match(/^(#{2,3})\s+(.+?)\s*#*\s*$/);
if (m) {
const level = m[1].length;
const text = m[2].trim();
entries.push({ id: slugger.slug(text), text, level });
}
}
setToc(entries);
}, [source]);
if (toc.length === 0) return null;
return (
<nav className="mb-6 text-sm">
<h3 className="text-xs font-semibold text-zinc-500 uppercase mb-2">
Contents
</h3>
<ul className="space-y-1">
{toc.map((entry) => (
<li key={entry.id} className={entry.level === 3 ? "ml-3" : ""}>
<Link
href={`#${entry.id}`}
className="text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:underline"
>
{entry.text}
</Link>
</li>
))}
</ul>
</nav>
);
}