"use client"; import React, { useEffect, useRef, useState } from "react"; import mermaid from "mermaid"; import { ComponentShareButton } from "./ComponentShareButton"; import { Reveal } from "./Reveal"; interface MermaidProps { graph?: string; children?: React.ReactNode; id?: string; title?: string; showShare?: boolean; fontSize?: string; nodeFontSize?: string; labelFontSize?: string; actorFontSize?: string; messageFontSize?: string; noteFontSize?: string; titleFontSize?: string; sectionFontSize?: string; legendFontSize?: string; } class MermaidErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> { constructor(props: { children: React.ReactNode }) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(_error: any) { return { hasError: true }; } componentDidCatch(error: any, errorInfo: any) { console.error("MermaidErrorBoundary caught an error:", error, errorInfo); } render() { if (this.state.hasError) { return (
Diagram rendering crashed.
); } return this.props.children; } } const MermaidInternal: React.FC = ({ graph, children, id: providedId, title, showShare = false, fontSize = "20px", nodeFontSize, labelFontSize, actorFontSize, messageFontSize, noteFontSize, titleFontSize, sectionFontSize, legendFontSize, }) => { const [id, setId] = useState(null); const wrapperRef = useRef(null); const [svgContent, setSvgContent] = useState(""); const [isVisible, setIsVisible] = useState(true); // Forced true specifically to bypass lazy loading bug const [isRendered, setIsRendered] = useState(false); const [error, setError] = useState(null); // Extract text from React children nodes (MDX parses multi-line content as React nodes) const extractTextFromChildren = (node: React.ReactNode): string => { if (typeof node === 'string') return node; if (typeof node === 'number') return String(node); if (Array.isArray(node)) return node.map(extractTextFromChildren).join('\n'); if (React.isValidElement(node)) { const props = node.props as { children?: React.ReactNode }; if (props.children) { return extractTextFromChildren(props.children); } } return ''; }; const rawGraph = graph || extractTextFromChildren(children) || ""; // MDXRemote double-escapes \n in plain string props (e.g., "graph TD\\nA-->B" becomes "graph TD\\\\nA-->B") // We need to unescape these back to real newlines for Mermaid to parse const sanitizedGraph = rawGraph .trim() .replace(/^`+|`+$/g, '') .replace(/\\n/g, '\n') .replace(/\\"/g, '"') .replace(/\\'/g, "'"); // Early return for empty graph — show fallback instead of blank space if (!sanitizedGraph || sanitizedGraph.length < 5) { return (

Diagramm konnte nicht geladen werden

); } useEffect(() => { const generatedId = providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`; setId(generatedId); }, [providedId]); // Observer to detect when the component is actually in view and layout is ready useEffect(() => { if (!wrapperRef.current) return; const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); // Render once per diagram } }); }, { rootMargin: "200px" } // Start rendering 200px before it scrolls into view ); observer.observe(wrapperRef.current); return () => observer.disconnect(); }, [id]); useEffect(() => { if (!isVisible || !id || isRendered) { return; } mermaid.initialize({ startOnLoad: false, theme: "base", darkMode: false, htmlLabels: false, flowchart: { useMaxWidth: true, htmlLabels: false, curve: 'basis', nodeSpacing: 50, rankSpacing: 60, padding: 15 }, sequence: { useMaxWidth: true, showSequenceNumbers: false, height: 40, actorMargin: 30, messageMargin: 30 }, gantt: { useMaxWidth: true }, pie: { useMaxWidth: true }, state: { useMaxWidth: true }, themeVariables: { primaryColor: "#f8fafc", // slate-50 primaryTextColor: "#334155", // slate-700 primaryBorderColor: "#cbd5e1", // slate-300 lineColor: "#64748b", // slate-500 secondaryColor: "#f1f5f9", // slate-100 tertiaryColor: "#e2e8f0", // slate-200 background: "#ffffff", mainBkg: "#ffffff", secondBkg: "#f8fafc", tertiaryBkg: "#f1f5f9", textColor: "#1e293b", labelTextColor: "#475569", nodeBorder: "#cbd5e1", clusterBkg: "#f8fafc", clusterBorder: "#cbd5e1", edgeLabelBackground: "#ffffff", fontFamily: "var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", fontSize: fontSize || "18px", nodeFontSize: nodeFontSize || fontSize || "18px", labelFontSize: labelFontSize || fontSize || "16px", actorFontSize: actorFontSize || fontSize || "18px", messageFontSize: messageFontSize || fontSize || "16px", noteFontSize: noteFontSize || fontSize || "16px", titleFontSize: titleFontSize || "24px", sectionFontSize: sectionFontSize || fontSize || "18px", legendFontSize: legendFontSize || fontSize || "18px", pie1: "#0f172a", pie2: "#334155", pie3: "#64748b", pie4: "#94a3b8", pie5: "#cbd5e1", pie6: "#1e293b", pie7: "#475569", pie8: "#000000", pie9: "#e2e8f0", pie10: "#020617", pie11: "#525252", pie12: "#262626", }, securityLevel: "loose", }); const renderGraph = async () => { if (!wrapperRef.current) return; if (wrapperRef.current.clientWidth === 0) return; const maxRetries = 3; let attempt = 0; let success = false; while (attempt < maxRetries && !success) { attempt++; try { if (!sanitizedGraph) return; if (!mermaid.render) return; const uniqueSvgId = `${id}-svg-${Date.now()}`; const tempDiv = document.createElement('div'); document.body.appendChild(tempDiv); tempDiv.style.position = 'absolute'; tempDiv.style.left = '-9999px'; tempDiv.style.visibility = 'hidden'; let rawSvg: string; try { const result = await mermaid.render(uniqueSvgId, sanitizedGraph, tempDiv); rawSvg = result.svg; } finally { if (document.body.contains(tempDiv)) { document.body.removeChild(tempDiv); } } let scaledSvg = rawSvg; const svgStartEnd = rawSvg.indexOf('>'); if (svgStartEnd > -1) { const svgTag = rawSvg.substring(0, svgStartEnd + 1); const rest = rawSvg.substring(svgStartEnd + 1); const newSvgTag = svgTag.replace(/width="[^"]*"/, 'width="100%"').replace(/height="[^"]*"/, 'height="auto"'); scaledSvg = newSvgTag + rest; } setSvgContent(scaledSvg); setIsRendered(true); setError(null); success = true; } catch (err) { if (attempt >= maxRetries) { setError("Diagramm konnte nicht geladen werden (Render-Fehler)."); setIsRendered(true); } else { await new Promise(resolve => setTimeout(resolve, attempt * 200)); } } } }; const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { if (entry.contentRect.width > 0 && !isRendered) { requestAnimationFrame(() => renderGraph()); resizeObserver.disconnect(); } } }); resizeObserver.observe(wrapperRef.current); if (wrapperRef.current && wrapperRef.current.clientWidth > 0) { renderGraph(); resizeObserver.disconnect(); } return () => resizeObserver.disconnect(); }, [isVisible, id, sanitizedGraph, fontSize, nodeFontSize, labelFontSize, actorFontSize, messageFontSize, noteFontSize, titleFontSize, sectionFontSize, legendFontSize]); if (!id) return null; return (
{showShare && (
)} {title && (

{title}

)}
div]:!w-full [&>div]:!flex [&>div]:!justify-center [&_svg]:!max-w-full [&_svg]:max-w-4xl [&_svg]:!h-auto [&_svg]:!max-h-[60vh] md:[&_svg]:!max-h-[600px] [&_.node_rect]:!rx-[8px] [&_.node_rect]:!ry-[8px] [&_.node_rect]:!fill-white [&_.node_rect]:!stroke-slate-200 [&_.node_rect]:!stroke-[2px] [&_.edgePath_path]:!stroke-slate-400 [&_.edgePath_path]:!stroke-[1.5px] [&_.marker]:!fill-slate-400 [&_.marker]:!stroke-slate-400 [&_.nodeLabel]:!font-sans [&_.nodeLabel]:!font-bold [&_.nodeLabel]:!text-slate-700 `} id={id} style={{ maxWidth: "100%", overflow: "visible" }} > {error ? (
{error}
) : svgContent ? (
) : (
{sanitizedGraph}
)}
); }; export const Mermaid = (props: MermaidProps) => ( );