/ 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 }