/ components / MermaidDiagram.tsx
MermaidDiagram.tsx
 1  import { useEffect, useRef, useState } from 'react';
 2  import mermaid from 'mermaid';
 3  import { Maximize2 } from 'lucide-react';
 4  import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
 5  
 6  mermaid.initialize({
 7    startOnLoad: false,
 8    theme: 'dark',
 9    securityLevel: 'loose',
10    themeVariables: {
11      fontSize: '10px',
12      fontFamily: 'ui-sans-serif, system-ui, -apple-system, sans-serif',
13    },
14  });
15  
16  let idCounter = 0;
17  
18  interface Props {
19    chart: string;
20  }
21  
22  export function MermaidDiagram({ chart }: Props) {
23    const ref = useRef<HTMLDivElement>(null);
24    const [svgHtml, setSvgHtml] = useState<string | null>(null);
25    const [error, setError] = useState<string | null>(null);
26    const [open, setOpen] = useState(false);
27  
28    useEffect(() => {
29      if (!ref.current) return;
30  
31      const id = `mermaid-${++idCounter}`;
32      setError(null);
33  
34      mermaid
35        .render(id, chart)
36        .then(({ svg }) => {
37          setSvgHtml(svg);
38          if (ref.current) {
39            ref.current.innerHTML = svg;
40            const svgEl = ref.current.querySelector('svg');
41            if (svgEl) {
42              svgEl.style.width = '100%';
43              svgEl.style.maxWidth = '800px';
44              svgEl.style.height = 'auto';
45            }
46          }
47        })
48        .catch((err: unknown) => {
49          setError(err instanceof Error ? err.message : String(err));
50        });
51    }, [chart]);
52  
53    if (error) return null;
54  
55    return (
56      <>
57        <button
58          type="button"
59          onClick={() => setOpen(true)}
60          className="group relative w-full rounded-md bg-muted/30 p-3 cursor-pointer transition-colors hover:bg-muted/50 text-left max-h-[600px] overflow-y-auto"
61        >
62          <div ref={ref} />
63          <span className="absolute top-2 right-2 rounded-md bg-background/60 p-1 opacity-0 group-hover:opacity-100 transition-opacity">
64            <Maximize2 className="h-3.5 w-3.5 text-muted-foreground" />
65          </span>
66        </button>
67  
68        <Dialog open={open} onOpenChange={setOpen}>
69          <DialogContent className="sm:max-w-[90vw] max-h-[90vh] flex flex-col">
70            <DialogTitle className="sr-only">Diagram</DialogTitle>
71            <div
72              className="flex-1 overflow-auto flex items-center justify-center p-4 mermaid-fullscreen"
73              dangerouslySetInnerHTML={svgHtml ? { __html: svgHtml } : undefined}
74            />
75          </DialogContent>
76        </Dialog>
77      </>
78    );
79  }