Mermaid.tsx193 lines · main
1'use client'
2
3import mermaid from 'mermaid'
4import { useTheme } from 'next-themes'
5import { useEffect, useState } from 'react'
6import { cn } from 'ui'
7
8// @mildtomato - 28/11/2025
9// Colors are hardcoded because Mermaid's parser doesn't support CSS variables
10const darkThemeVariables = {
11 background: 'transparent',
12 mainBkg: '#171717',
13 primaryTextColor: '#ededed',
14 secondaryTextColor: '#a0a0a0',
15 tertiaryTextColor: '#ededed',
16 textColor: '#ededed',
17 primaryColor: '#3ecf8e',
18 primaryBorderColor: '#3ecf8e',
19 secondaryColor: '#9333ea',
20 secondaryBorderColor: '#a855f7',
21 tertiaryColor: '#262626',
22 tertiaryBorderColor: '#404040',
23 lineColor: '#525252',
24 border1: '#404040',
25 border2: '#525252',
26 noteBkgColor: '#1a3a2a',
27 noteTextColor: '#ededed',
28 noteBorderColor: '#3ecf8e',
29 actorBkg: '#171717',
30 actorBorder: '#525252',
31 actorTextColor: '#ededed',
32 actorLineColor: '#525252',
33 activationBkgColor: '#9333ea',
34 activationBorderColor: '#a855f7',
35 signalColor: '#ededed',
36 signalTextColor: '#ededed',
37 sequenceNumberColor: '#171717',
38 nodeBkg: '#262626',
39 nodeBorder: '#404040',
40 clusterBkg: '#1a1a1a',
41 clusterBorder: '#404040',
42 defaultLinkColor: '#3ecf8e',
43 edgeLabelBackground: '#171717',
44 attributeBackgroundColorOdd: '#262626',
45 attributeBackgroundColorEven: '#171717',
46 rowOdd: '#262626',
47 rowEven: '#171717',
48 fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
49 fontSize: '14px',
50}
51
52const lightThemeVariables = {
53 background: 'transparent',
54 mainBkg: '#ffffff',
55 primaryTextColor: '#1c1c1c',
56 secondaryTextColor: '#6b7280',
57 tertiaryTextColor: '#1c1c1c',
58 textColor: '#1c1c1c',
59 primaryColor: '#3ecf8e',
60 primaryBorderColor: '#3ecf8e',
61 secondaryColor: '#9333ea',
62 secondaryBorderColor: '#a855f7',
63 tertiaryColor: '#f5f5f5',
64 tertiaryBorderColor: '#e5e5e5',
65 lineColor: '#d4d4d4',
66 border1: '#e5e5e5',
67 border2: '#d4d4d4',
68 noteBkgColor: '#ecfdf5',
69 noteTextColor: '#1c1c1c',
70 noteBorderColor: '#3ecf8e',
71 actorBkg: '#ffffff',
72 actorBorder: '#d4d4d4',
73 actorTextColor: '#1c1c1c',
74 actorLineColor: '#d4d4d4',
75 activationBkgColor: '#9333ea',
76 activationBorderColor: '#a855f7',
77 signalColor: '#1c1c1c',
78 signalTextColor: '#1c1c1c',
79 sequenceNumberColor: '#ffffff',
80 nodeBkg: '#f5f5f5',
81 nodeBorder: '#e5e5e5',
82 clusterBkg: '#fafafa',
83 clusterBorder: '#e5e5e5',
84 defaultLinkColor: '#3ecf8e',
85 edgeLabelBackground: '#ffffff',
86 attributeBackgroundColorOdd: '#f5f5f5',
87 attributeBackgroundColorEven: '#ffffff',
88 rowOdd: '#f5f5f5',
89 rowEven: '#ffffff',
90 fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
91 fontSize: '13px',
92}
93
94export interface MermaidProps {
95 /** Mermaid diagram definition (e.g. flowchart, sequence, erDiagram) */
96 chart: string
97 /** Additional CSS classes for the container */
98 className?: string
99}
100
101/**
102 * Renders a Mermaid diagram from text.
103 *
104 * Supports flowcharts, sequence diagrams, ER diagrams, and other Mermaid syntax.
105 * Automatically adapts to light/dark theme.
106 *
107 * @example
108 * ```tsx
109 * <Mermaid chart={`
110 * flowchart LR
111 * A[Start] --> B[End]
112 * `} />
113 * ```
114 *
115 * @see https://mermaid.js.org/intro/
116 */
117export function Mermaid({ chart, className }: MermaidProps) {
118 const [svg, setSvg] = useState<string>('')
119 const [error, setError] = useState<string | null>(null)
120 const { resolvedTheme } = useTheme()
121
122 const isDark = resolvedTheme === 'dark'
123
124 useEffect(() => {
125 if (!resolvedTheme) return
126
127 // Re-initialize mermaid with current theme
128 mermaid.initialize({
129 startOnLoad: false,
130 theme: 'base',
131 themeVariables: isDark ? darkThemeVariables : lightThemeVariables,
132 sequence: {
133 useMaxWidth: false,
134 actorMargin: 150,
135 messageMargin: 60,
136 noteMargin: 20,
137 },
138 flowchart: {
139 useMaxWidth: false,
140 },
141 er: {
142 useMaxWidth: false,
143 },
144 })
145
146 const renderChart = async () => {
147 try {
148 // Mermaid requires a unique ID per render to avoid DOM conflicts
149 const id = `mermaid-${Math.random().toString(36).substring(2, 11)}`
150 const { svg } = await mermaid.render(id, chart.trim())
151 // Mermaid outputs <br> which isn't valid XML - fix for browser compatibility
152 setSvg(svg.replace(/<br\s*>/gi, '<br/>'))
153 setError(null)
154 } catch (err) {
155 // Invalid chart syntax throws - display error instead of crashing
156 console.error('Mermaid rendering error:', err)
157 setError(err instanceof Error ? err.message : 'Failed to render diagram')
158 }
159 }
160
161 renderChart()
162 }, [chart, resolvedTheme])
163
164 if (!resolvedTheme) {
165 return <div className={cn('my-6 rounded-lg bg-muted p-6 animate-pulse h-64', className)} />
166 }
167
168 if (error) {
169 return (
170 <div
171 className={cn(
172 'my-4 p-4 bg-destructive-200 border border-destructive-400 rounded-md',
173 className
174 )}
175 >
176 <p className="text-destructive-600 text-sm font-mono">Mermaid Error: {error}</p>
177 <pre className="mt-2 text-xs text-foreground-lighter overflow-auto">{chart}</pre>
178 </div>
179 )
180 }
181
182 return (
183 <figure
184 className={cn(
185 'my-6 w-full flex justify-center rounded-lg border p-6',
186 'bg-white border-[#e5e5e5] dark:bg-[#171717] dark:border-[#333]',
187 '[&_svg]:h-auto [&_svg]:max-w-full',
188 className
189 )}
190 dangerouslySetInnerHTML={{ __html: svg }}
191 />
192 )
193}