Scrum4Me/app/(app)/manual/_components/mermaid-block.tsx
Madhura68 797c7d32b0 feat(PBI-58): /manual route renders developer manual chapters in-app
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>
2026-05-07 17:43:47 +02:00

73 lines
1.8 KiB
TypeScript

'use client'
import { useEffect, useId, useRef, useState } from 'react'
type Props = {
source: string
}
let mermaidPromise: Promise<typeof import('mermaid').default> | null = null
function loadMermaid() {
if (!mermaidPromise) {
mermaidPromise = import('mermaid').then((mod) => {
const mermaid = mod.default
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'strict',
fontFamily: 'inherit',
})
return mermaid
})
}
return mermaidPromise
}
export function MermaidBlock({ source }: Props) {
const id = useId().replace(/[^a-zA-Z0-9]/g, '')
const containerRef = useRef<HTMLDivElement | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
loadMermaid()
.then(async (mermaid) => {
if (cancelled) return
try {
const { svg } = await mermaid.render(`mermaid-${id}`, source)
if (cancelled) return
if (containerRef.current) containerRef.current.innerHTML = svg
setError(null)
} catch (err) {
if (cancelled) return
setError(err instanceof Error ? err.message : String(err))
}
})
.catch((err) => {
if (cancelled) return
setError(err instanceof Error ? err.message : String(err))
})
return () => {
cancelled = true
}
}, [id, source])
if (error) {
return (
<pre className="overflow-x-auto rounded-md bg-muted p-3 text-xs text-destructive">
<code>
{`Mermaid render failed: ${error}\n\n${source}`}
</code>
</pre>
)
}
return (
<div
ref={containerRef}
className="my-4 flex justify-center overflow-x-auto rounded-md bg-muted p-4 [&_svg]:max-w-full"
aria-label="Diagram"
/>
)
}