feat(ui): complete structural rewrite of content components to strict engineering blueprint aesthetic
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 27s
Build & Deploy / 🧪 QA (push) Failing after 1m15s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-22 02:35:06 +01:00
parent 3eccff42e4
commit 75c61f1436
6 changed files with 574 additions and 62 deletions

View File

@@ -21,13 +21,39 @@ interface MermaidProps {
legendFontSize?: string;
}
export const Mermaid: React.FC<MermaidProps> = ({
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 (
<div className="p-4 bg-red-50 border border-red-100 rounded text-red-600 text-sm font-mono text-center my-8">
Diagram rendering crashed.
</div>
);
}
return this.props.children;
}
}
const MermaidInternal: React.FC<MermaidProps> = ({
graph,
children,
id: providedId,
title,
showShare = false,
fontSize = "16px",
fontSize = "20px",
nodeFontSize,
labelFontSize,
actorFontSize,
@@ -38,8 +64,11 @@ export const Mermaid: React.FC<MermaidProps> = ({
legendFontSize,
}) => {
const [id, setId] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const [svgContent, setSvgContent] = useState<string>("");
const [isVisible, setIsVisible] = useState(true); // Forced true specifically to bypass lazy loading bug
const [isRendered, setIsRendered] = useState(false);
const [error, setError] = useState<string | null>(null);
// Extract text from React children nodes (MDX parses multi-line content as React nodes)
const extractTextFromChildren = (node: React.ReactNode): string => {
@@ -66,29 +95,86 @@ export const Mermaid: React.FC<MermaidProps> = ({
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
useEffect(() => {
setId(
providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`,
// Early return for empty graph — show fallback instead of blank space
if (!sanitizedGraph || sanitizedGraph.length < 5) {
return (
<div className="not-prose my-8 p-6 bg-slate-50 border border-slate-200 rounded-xl text-center">
<p className="text-sm text-slate-400 font-medium">Diagramm konnte nicht geladen werden</p>
</div>
);
}, [providedId]);
const [isRendered, setIsRendered] = useState(false);
const [error, setError] = useState<string | null>(null);
}
useEffect(() => {
console.log(`[Mermaid DEBUG] id=${providedId}, graph prop length=${graph?.length ?? 'undefined'}, rawGraph length=${rawGraph.length}, sanitizedGraph length=${sanitizedGraph.length}`);
if (graph?.length === 0) {
console.log(`[Mermaid DEBUG] id=${providedId} EMPTY graph prop! children:`, children);
}
}, []);
useEffect(() => {
const generatedId = providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`;
setId(generatedId);
console.log(`[Mermaid DEBUG] id=${generatedId}, provided=${providedId}, graph length=${graph?.length ?? 'undefined'}`);
}, [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(() => {
console.log(`[Mermaid DEBUG] Main effect triggered. id=${id}, isVisible=${isVisible}, isRendered=${isRendered}`);
if (!isVisible || !id || isRendered) {
console.log(`[Mermaid DEBUG] Main effect early return (will not render). isVisible=${isVisible}, id=${id}, isRendered=${isRendered}`);
return;
}
console.log(`[Mermaid DEBUG] Initializing mermaid for ${id}...`);
mermaid.initialize({
startOnLoad: false,
theme: "base",
darkMode: false,
htmlLabels: false, // Added this line as per instruction
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: {
// Base colors - industrial slate/white palette
primaryColor: "#ffffff",
primaryTextColor: "#1e293b",
primaryBorderColor: "#cbd5e1",
lineColor: "#94a3b8",
secondaryColor: "#f8fafc",
tertiaryColor: "#f1f5f9",
// Background colors
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 colors
background: "#ffffff",
mainBkg: "#ffffff",
secondBkg: "#f8fafc",
@@ -107,16 +193,16 @@ export const Mermaid: React.FC<MermaidProps> = ({
edgeLabelBackground: "#ffffff",
// Font
fontFamily: "Inter, system-ui, sans-serif",
fontSize: fontSize,
nodeFontSize: nodeFontSize || fontSize,
labelFontSize: labelFontSize || fontSize,
actorFontSize: actorFontSize || fontSize,
messageFontSize: messageFontSize || fontSize,
noteFontSize: noteFontSize || fontSize,
titleFontSize: titleFontSize || "20px",
sectionFontSize: sectionFontSize || fontSize,
legendFontSize: legendFontSize || fontSize,
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",
// Pie Chart Colors - High Contrast Industrial Palette
pie1: "#0f172a", // Deep Navy
@@ -135,64 +221,161 @@ export const Mermaid: React.FC<MermaidProps> = ({
securityLevel: "loose",
});
const render = async () => {
if (containerRef.current && id) {
if (!sanitizedGraph || sanitizedGraph.trim() === "") {
console.warn("Mermaid: Empty or invalid graph provided, skipping render.");
return;
}
const renderGraph = async () => {
if (!wrapperRef.current) return;
// CRITICAL: Ensure invalid dimensions don't crash d3
if (wrapperRef.current.clientWidth === 0) {
console.warn("Mermaid: Container width is 0, deferring render", id);
return;
}
const maxRetries = 3;
let attempt = 0;
let success = false;
while (attempt < maxRetries && !success) {
attempt++;
try {
const { svg } = await mermaid.render(`${id}-svg`, sanitizedGraph);
containerRef.current.innerHTML = svg;
setSvgContent(svg);
if (!sanitizedGraph) {
console.warn("Mermaid: Empty graph definition received, skipping render");
return;
}
if (!mermaid.render) {
console.warn("Mermaid not ready");
return;
}
// Generate a unique ID for the SVG to prevent collisions during retries
const uniqueSvgId = `${id}-svg-${Date.now()}`;
// Render into a detached container to avoid React DOM conflicts
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 {
console.log(`[Mermaid DEBUG] Calling mermaid.render for ${id}...`);
const result = await mermaid.render(uniqueSvgId, sanitizedGraph, tempDiv);
rawSvg = result.svg;
console.log(`[Mermaid DEBUG] Render success for ${id}!`);
} 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;
}
// Store SVG in React state — React renders it via dangerouslySetInnerHTML
setSvgContent(scaledSvg);
setIsRendered(true);
setError(null);
success = true;
} catch (err) {
console.error("Mermaid rendering failed:", err);
console.error("Graph that failed:", sanitizedGraph);
const errorMessage = err instanceof Error ? err.message : String(err);
setError(`Failed to render diagram: ${errorMessage}`);
setIsRendered(true);
console.warn(`Mermaid render attempt ${attempt} failed:`, err);
if (attempt >= maxRetries) {
console.error("Mermaid Render Error Final:", err);
setError("Diagramm konnte nicht geladen werden (Render-Fehler).");
setIsRendered(true);
} else {
// Wait before retrying (exponential backoff)
await new Promise(resolve => setTimeout(resolve, attempt * 200));
}
}
}
};
if (id) {
render();
// Use ResizeObserver to trigger render ONLY when we have dimensions
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentRect.width > 0 && !isRendered) {
// Debounce slightly to ensure stable layout
requestAnimationFrame(() => renderGraph());
resizeObserver.disconnect();
}
}
});
resizeObserver.observe(wrapperRef.current);
// Fallback: Try immediately if we already have size
if (wrapperRef.current && wrapperRef.current.clientWidth > 0) {
renderGraph();
resizeObserver.disconnect();
}
}, [sanitizedGraph, id]);
return () => resizeObserver.disconnect();
}, [isVisible, id, sanitizedGraph, fontSize, nodeFontSize, labelFontSize, actorFontSize, messageFontSize, noteFontSize, titleFontSize, sectionFontSize, legendFontSize]);
if (!id) return null;
return (
<div className="mermaid-wrapper not-prose relative left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] w-screen px-5 md:px-16 py-12 bg-slate-50/50 border-y border-slate-100">
<div className="max-w-7xl mx-auto">
<figure ref={wrapperRef} className="mermaid-wrapper not-prose my-20 w-full max-w-full border-y-2 border-slate-900 py-12 relative overflow-visible">
{/* Blueprint Grid Background Pattern */}
<div className="absolute inset-0 z-0 pointer-events-none opacity-[0.03]" style={{ backgroundImage: 'linear-gradient(to right, #0f172a 1px, transparent 1px), linear-gradient(to bottom, #0f172a 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
<div className="w-full flex flex-col items-center justify-center relative z-10 px-4 md:px-8">
{title && (
<h4 className="text-center text-sm font-bold text-slate-700 mb-8 uppercase tracking-[0.2em]">
{title}
</h4>
<div className="w-full flex justify-between items-baseline mb-12 border-b border-slate-200 pb-4">
<h4 className="text-left text-lg md:text-xl font-bold text-slate-900 tracking-tight m-0">
{title}
</h4>
<span className="text-[10px] font-mono text-slate-400 uppercase tracking-[0.3em] font-bold">System_Architecture</span>
</div>
)}
<div className="flex justify-center w-full">
<div className="flex justify-center w-full overflow-x-auto">
<div
ref={containerRef}
className={`mermaid transition-opacity duration-500 w-full flex justify-center ${isRendered ? "opacity-100" : "opacity-0"}`}
className={`mermaid
w-full flex justify-center
/* Safely scale the SVG container wide without corrupting internal label calculations */
[&>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]
/* Premium Industrial Styling */
[&_.node_rect]:!rx-[0px] [&_.node_rect]:!ry-[0px] /* Sharp corners for notebook look */
[&_.node_rect]:!fill-white
[&_.node_rect]:!stroke-slate-900 [&_.node_rect]:!stroke-[2px]
[&_.node_rect]:!filter-none
[&_.edgePath_path]:!stroke-slate-900 [&_.edgePath_path]:!stroke-[2px]
[&_.marker]:!fill-slate-900 [&_.marker]:!stroke-slate-900
/* Labels */
[&_.nodeLabel]:!font-mono [&_.nodeLabel]:!font-bold [&_.nodeLabel]:!text-slate-900
`}
id={id}
style={{
maxWidth: "100%",
overflow: "visible",
overflow: "visible"
}}
>
{error ? (
<div className="text-red-500 p-4 border border-red-200 rounded bg-red-50 text-sm">
<div className="text-red-500 p-4 border border-red-200 bg-red-50 text-sm font-mono uppercase tracking-widest text-center w-full h-32 flex items-center justify-center">
{error}
</div>
) : svgContent ? (
<div dangerouslySetInnerHTML={{ __html: svgContent }} />
) : (
sanitizedGraph
// Hide raw graph until rendered
<div style={{ display: 'none' }}>{sanitizedGraph}</div>
)}
</div>
</div>
{showShare && id && (
<div className="flex justify-center mt-8">
{showShare && id && isRendered && (
<div className="flex justify-end w-full mt-10">
<DiagramShareButton
diagramId={id}
title={title}
@@ -201,6 +384,12 @@ export const Mermaid: React.FC<MermaidProps> = ({
</div>
)}
</div>
</div>
</figure>
);
};
export const Mermaid = (props: MermaidProps) => (
<MermaidErrorBoundary>
<MermaidInternal {...props} />
</MermaidErrorBoundary>
);