"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 && (
)}
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) => (
);