{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "mermaid", "title": "Mermaid", "description": "A customizable Mermaid.js diagram renderer for React.", "dependencies": [ "mermaid" ], "files": [ { "path": "components/mermaid.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { mermaidThemes, type MermaidCustomTheme } from \"@/lib/mermaid-themes\";\n\n/* -------------------------------------------------------------------------------------------------\n * Types\n * -----------------------------------------------------------------------------------------------*/\n\nexport type MermaidBuiltinTheme =\n | \"default\"\n | \"dark\"\n | \"forest\"\n | \"neutral\"\n | \"base\";\nexport type MermaidTheme = MermaidBuiltinTheme | MermaidCustomTheme;\n\nconst BUILTIN_THEMES = new Set([\n \"default\",\n \"dark\",\n \"forest\",\n \"neutral\",\n \"base\",\n]);\n\nexport interface MermaidConfig {\n theme?: MermaidTheme;\n darkMode?: boolean;\n look?: \"classic\" | \"handdrawn\";\n themeVariables?: Record;\n flowchart?: {\n curve?: \"linear\" | \"cardinal\";\n padding?: number;\n htmlLabels?: boolean;\n };\n sequence?: {\n diagramMarginX?: number;\n diagramMarginY?: number;\n actorMargin?: number;\n width?: number;\n height?: number;\n boxMargin?: number;\n useMaxWidth?: boolean;\n };\n fontFamily?: string;\n fontSize?: number;\n logLevel?: \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\" | \"fatal\";\n}\n\nexport interface MermaidProps {\n chart: string;\n config?: MermaidConfig;\n className?: string;\n onError?: (error: string) => void;\n onSuccess?: (svg: string) => void;\n /** Delay in ms before rendering triggers (useful for live editors) */\n debounceTime?: number;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Hook: useMermaid\n * Handles dynamic imports, configuration, and rendering logic.\n * -----------------------------------------------------------------------------------------------*/\n\nfunction useMermaid({\n chart,\n config,\n debounceTime = 300,\n}: {\n chart: string;\n config?: MermaidConfig;\n debounceTime?: number;\n}) {\n const [svg, setSvg] = React.useState(null);\n const [error, setError] = React.useState(null);\n const [status, setStatus] = React.useState<\n \"idle\" | \"loading\" | \"success\" | \"error\"\n >(\"idle\");\n\n // Unique ID for this diagram instance\n const id = React.useId().replace(/:/g, \"\");\n\n // Hidden container for Mermaid's size calculations\n const renderRef = React.useRef(null);\n\n // Debounce the input chart string to avoid thrashing\n const debouncedChart = useDebounce(chart, debounceTime);\n\n // Memoize config to prevent deep object comparison issues in effects\n const configString = React.useMemo(\n () => JSON.stringify(config ?? {}),\n [config],\n );\n\n React.useEffect(() => {\n if (!debouncedChart.trim()) {\n setStatus(\"idle\");\n setSvg(null);\n setError(null);\n return;\n }\n\n let isCancelled = false;\n\n const render = async () => {\n setStatus(\"loading\");\n setError(null);\n\n try {\n // Dynamic import to keep bundle size small\n const mermaid = (await import(\"mermaid\")).default;\n\n if (isCancelled) return;\n\n const parsedConfig: MermaidConfig = JSON.parse(configString);\n\n // Resolve Theme\n const isCustomTheme =\n parsedConfig.theme && !BUILTIN_THEMES.has(parsedConfig.theme);\n const resolvedThemeVars = isCustomTheme\n ? {\n ...mermaidThemes[parsedConfig.theme as MermaidCustomTheme],\n ...parsedConfig.themeVariables,\n }\n : parsedConfig.themeVariables;\n\n const explicitTheme = parsedConfig.theme as MermaidBuiltinTheme;\n const resolvedMermaidTheme = isCustomTheme\n ? \"base\"\n : (!explicitTheme || explicitTheme === \"default\") &&\n parsedConfig.darkMode\n ? \"dark\"\n : (explicitTheme ?? \"default\");\n\n // Initialize Mermaid\n // Note: startOnLoad must be false so we can manually render\n mermaid.initialize({\n startOnLoad: false,\n theme: resolvedMermaidTheme,\n themeVariables: resolvedThemeVars,\n look: parsedConfig.look === \"handdrawn\" ? \"handDrawn\" : \"classic\",\n flowchart: {\n htmlLabels: parsedConfig.flowchart?.htmlLabels ?? true,\n ...(parsedConfig.flowchart?.padding != null\n ? { padding: parsedConfig.flowchart.padding }\n : {}),\n },\n sequence: parsedConfig.sequence,\n fontFamily: parsedConfig.fontFamily ?? \"Inter, sans-serif\",\n fontSize: parsedConfig.fontSize ?? 14,\n logLevel: parsedConfig.logLevel ?? \"error\",\n securityLevel: \"loose\",\n });\n\n // Ensure we have a DOM node for calculation\n if (!renderRef.current) return;\n renderRef.current.innerHTML = \"\";\n\n // Generate unique ID for this specific render cycle\n const uniqueId = `mermaid-${id}-${Date.now()}`;\n\n // Render\n // We pass the ref as the container so Mermaid can calculate dimensions accurately\n const { svg: svgOutput } = await mermaid.render(\n uniqueId,\n debouncedChart.trim(),\n renderRef.current,\n );\n\n if (!isCancelled) {\n setSvg(svgOutput);\n setStatus(\"success\");\n // Clean up the calculation node to free memory\n renderRef.current.innerHTML = \"\";\n }\n } catch (err) {\n if (!isCancelled) {\n const message =\n err instanceof Error ? err.message : \"Failed to render diagram\";\n console.error(\"Mermaid Render Error:\", err);\n setError(message);\n setStatus(\"error\");\n setSvg(null);\n }\n }\n };\n\n render();\n\n return () => {\n isCancelled = true;\n };\n }, [debouncedChart, configString, id]);\n\n return { svg, error, status, renderRef };\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Helper: useDebounce\n * -----------------------------------------------------------------------------------------------*/\n\nfunction useDebounce(value: T, delay: number): T {\n const [debouncedValue, setDebouncedValue] = React.useState(value);\n\n React.useEffect(() => {\n const handler = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n return () => {\n clearTimeout(handler);\n };\n }, [value, delay]);\n\n return debouncedValue;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Component: Mermaid\n * -----------------------------------------------------------------------------------------------*/\n\nexport function Mermaid({\n chart,\n config,\n className,\n onError,\n onSuccess,\n debounceTime = 300,\n}: MermaidProps) {\n const { svg, error, status, renderRef } = useMermaid({\n chart,\n config,\n debounceTime,\n });\n\n // Propagate events to parent\n React.useEffect(() => {\n if (status === \"success\" && svg) onSuccess?.(svg);\n if (status === \"error\" && error) onError?.(error);\n }, [status, svg, error, onSuccess, onError]);\n\n return (\n
\n {/* 1. Visible Output Container */}\n {status === \"success\" && svg && (\n \n )}\n\n {/* 2. Hidden Calculation Container \n Mermaid needs this to calculate layout dimensions before we show it. \n */}\n \n\n {/* 3. Loading State */}\n {status === \"loading\" && (\n
\n
\n
\n \n Rendering...\n \n
\n
\n )}\n\n {/* 4. Error State */}\n {status === \"error\" && error && (\n
\n
\n \n Syntax Error\n \n \n {error.split(\"\\n\")[0]}{\" \"}\n {/* Show only first line of error for brevity */}\n \n
\n
\n )}\n\n {/* 5. Idle State */}\n {status === \"idle\" && (\n
\n

\n No diagram code provided\n

\n
\n )}\n
\n );\n}\n", "type": "registry:component", "target": "components/mermaidcn/mermaid.tsx" } ], "type": "registry:component" }