Catch-all route at app/(app)/manual/[[...slug]]/page.tsx with generateStaticParams covering every TOC entry. Server-side MarkdownView uses react-markdown with remark-gfm, rehype-slug, and rehype-autolink-headings; mermaid code blocks are routed to a client-only MermaidBlock that dynamic-imports mermaid on mount. ManualSidebar (client) reads the typed TOC and highlights the active chapter via usePathname. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
51 lines
1.4 KiB
TypeScript
51 lines
1.4 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import { cn } from '@/lib/utils'
|
|
import type { ManualEntry } from '@/lib/manual.generated'
|
|
|
|
type Props = {
|
|
toc: readonly ManualEntry[]
|
|
}
|
|
|
|
function entryHref(entry: ManualEntry): string {
|
|
if (entry.slug.length === 0) return '/manual'
|
|
return '/manual/' + entry.slug.join('/')
|
|
}
|
|
|
|
export function ManualSidebar({ toc }: Props) {
|
|
const pathname = usePathname()
|
|
|
|
return (
|
|
<nav
|
|
aria-label="Manual chapters"
|
|
className="sticky top-20 hidden h-[calc(100vh-6rem)] w-64 shrink-0 overflow-y-auto border-r border-border px-4 py-6 lg:block"
|
|
>
|
|
<p className="mb-2 px-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
Manual
|
|
</p>
|
|
<ul className="space-y-1">
|
|
{toc.map((entry) => {
|
|
const href = entryHref(entry)
|
|
const isActive = pathname === href
|
|
return (
|
|
<li key={href}>
|
|
<Link
|
|
href={href}
|
|
className={cn(
|
|
'block rounded-md px-3 py-2 text-sm transition-colors',
|
|
isActive
|
|
? 'bg-primary/10 font-medium text-primary'
|
|
: 'text-foreground hover:bg-muted hover:text-foreground'
|
|
)}
|
|
>
|
|
{entry.title}
|
|
</Link>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
)
|
|
}
|