diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx index 10019822f72..9e8c3c634c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx @@ -4,6 +4,10 @@ import { memo, useEffect, useState } from 'react' import { toError } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' import { useTheme } from 'next-themes' +import { + mermaidWorkflowCss, + readMermaidThemeVariables, +} from '@/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-theme' import { PreviewLoadingFrame } from './preview-shared' import { ZoomablePreview } from './zoomable-preview' @@ -124,10 +128,23 @@ export const MermaidDiagram = memo(function MermaidDiagram({ const { default: mermaid } = await import('mermaid') if (cancelled) return + const themeVariables = readMermaidThemeVariables( + document.documentElement, + mermaidTheme === 'dark' + ) mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', - theme: mermaidTheme, + theme: 'base', + themeCSS: mermaidWorkflowCss(String(themeVariables.fontFamily)), + flowchart: { + curve: 'basis', + padding: 12, + nodeSpacing: 36, + rankSpacing: 56, + wrappingWidth: 180, + }, + themeVariables, }) mermaid.setParseErrorHandler?.(() => undefined) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-theme.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-theme.ts new file mode 100644 index 00000000000..e5f43e0802b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-theme.ts @@ -0,0 +1,88 @@ +/** + * Mermaid `base` theme variables drawn from the app's tokens, so diagrams match the editor and + * charts instead of Mermaid's stock palette. Mermaid derives shades from these with its own colour + * library, so every token read here must resolve to a plain hex or rgb value. + */ +export function readMermaidThemeVariables( + element: Element, + darkMode: boolean +): Record { + const styles = getComputedStyle(element) + const token = (name: string) => { + const value = styles.getPropertyValue(name).trim() + if (!value) throw new Error(`Missing diagram theme token ${name}`) + return value + } + const background = token('--bg') + const node = token('--surface-2') + const muted = token('--surface-4') + const subtle = token('--surface-5') + const border = token('--border') + const text = token('--text-primary') + const body = token('--text-body') + const line = token('--text-icon') + return { + darkMode, + fontFamily: getComputedStyle(document.body).fontFamily, + fontSize: '14px', + background, + primaryColor: node, + primaryTextColor: text, + primaryBorderColor: border, + secondaryColor: muted, + secondaryTextColor: body, + secondaryBorderColor: border, + tertiaryColor: subtle, + tertiaryTextColor: body, + tertiaryBorderColor: border, + mainBkg: node, + nodeBorder: border, + textColor: body, + lineColor: token('--workflow-edge'), + clusterBkg: token('--surface-3'), + clusterBorder: border, + edgeLabelBackground: background, + noteBkgColor: muted, + noteTextColor: body, + noteBorderColor: border, + actorBkg: node, + actorBorder: border, + actorTextColor: text, + actorLineColor: line, + signalColor: line, + signalTextColor: body, + labelBoxBkgColor: subtle, + labelBoxBorderColor: border, + labelTextColor: body, + loopTextColor: body, + sequenceNumberColor: background, + pie1: body, + pie2: line, + pie3: token('--text-subtle'), + pie4: token('--text-muted'), + pieStrokeColor: background, + pieTitleTextColor: text, + pieSectionTextColor: background, + pieLegendTextColor: body, + } +} + +/** + * Flowcharts drawn like workflow canvas blocks and edges: rounded cards with a 1.5px outline, + * 1.5px edges without arrowheads, and subgraphs as rounded subflow containers. Labels are pinned + * to the font Mermaid measured them with; otherwise they inherit the surrounding document's font + * and line height and overflow their boxes. Edge rules are scoped to flowchart classes so sequence + * and other diagrams keep their arrows. + */ +export function mermaidWorkflowCss(fontFamily: string): string { + return ` + .label, .nodeLabel, .edgeLabel, .label p, .nodeLabel p, .edgeLabel p { + font-family: ${fontFamily}; font-size: 14px; line-height: 1.5; letter-spacing: normal; + } + .edgeLabel, .edgeLabel p { font-size: 12px; } + .node rect, .node polygon, .node circle, .node path { stroke-width: 1.5px; } + .node rect { rx: 10px; ry: 10px; } + .cluster rect { rx: 14px; ry: 14px; stroke-width: 1.5px; } + .flowchart-link { stroke-width: 1.5px; marker-end: none !important; } +` +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx index 227331565a1..80b86ad0d98 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx @@ -12,6 +12,7 @@ import { Check, ChevronDown, Code, Duplicate, Eye, Wrap } from '@sim/emcn/icons' import type { ReactNodeViewProps } from '@tiptap/react' import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' import { DASHBOARD_EMBED_LANGUAGE } from '@/lib/dashboards/embed-language' +import { DIFF_EMBED_LANGUAGE } from '@/lib/diff/embed-language' import { MarkdownStreamingContext } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-streaming-context' import { looksLikeMermaid, MermaidDiagram } from '../mermaid-diagram' import { MarkdownCodeBlock } from './code-block-schema' @@ -22,6 +23,9 @@ import { useEditorEditable } from './use-editor-editable' const DashboardEmbed = lazy(() => import('@/components/dashboards/dashboard-embed').then((m) => ({ default: m.DashboardEmbed })) ) +const DiffEmbed = lazy(() => + import('@/components/diff/diff-embed').then((m) => ({ default: m.DiffEmbed })) +) const PLAIN = 'plain' const MERMAID = 'mermaid' @@ -74,7 +78,8 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView const text = node.textContent const isMermaid = explicitLanguage === MERMAID || (!explicitLanguage && looksLikeMermaid(text)) const isDashboard = explicitLanguage === DASHBOARD_EMBED_LANGUAGE - const isRendered = isMermaid || isDashboard + const isDiff = explicitLanguage === DIFF_EMBED_LANGUAGE + const isRendered = isMermaid || isDashboard || isDiff // Editable Mermaid shows source while the caret is focused inside the block and re-renders the // diagram on blur (the Linear/GitHub model). The Show source / Show diagram control drives this by @@ -148,7 +153,13 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView