Scrum4Me/app/(app)/manual/_components/mermaid-block.tsx
Janpeter Visser bd7478861b
PBI-58: Developer manual + in-app /manual page (#148)
* docs(PBI-58): add developer manual chapters under docs/manual/

Adds a 7-file English-language manual targeted at new human contributors:
index, overview, statuses & transitions (with mermaid state diagrams),
git workflow, MCP integration, docker, and troubleshooting. The manual
is the *map* — it cross-references existing runbooks/ADRs/architecture
docs rather than duplicating their content.

Regenerates docs/INDEX.md and validates with check-doc-links.mjs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(PBI-58): add markdown rendering deps + manual:build script

Adds mermaid, rehype-slug, rehype-autolink-headings for the in-app
/manual page. Wires manual:build into prebuild so production builds
always regenerate the chapter TOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(PBI-58): codegen script for in-app manual TOC

scripts/build-manual.mjs walks docs/manual/, parses YAML front-matter,
strips it from the body, and emits lib/manual.generated.ts with a typed
ManualEntry[] containing slug, title, description, filePath, and the
embedded markdown body. Pure Node 20, mirrors generate-docs-index.mjs.

Inlining the markdown at build time keeps runtime serverless functions
free of filesystem reads, which avoids whole-project NFT tracing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 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>

* feat(PBI-58): add Manual link to main nav bar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:00:10 +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"
/>
)
}