Skip to content

Commit 39067d0

Browse files
feat(files): render diff fences and theme mermaid diagrams (#8543)
* improvement(files): theme mermaid diagrams with app tokens * feat(files): render diff fences with source-checked state * improvement(files): frame diffs as cards with tinted change rows * improvement(files): render prose diffs as source excerpt cards and compare two documents * improvement(files): put the open link beside the excerpt title * improvement(files): render inline bold and code in prose diffs * improvement(files): render edits as line diffs with softer change colors * improvement(files): draw mermaid flowcharts like workflow blocks and edges * fix(files): keep mermaid labels inside their nodes and fork edges cleanly * improvement(files): drop diff source checks and split view, size tables to content * improvement(files): keep the diff parser out of the editor bundle * fix(files): parse multi-file and copy git diffs, cap word diffs, readable pie labels * fix(files): title renames by their new path and expand collapsed lines one run at a time
1 parent 428a945 commit 39067d0

11 files changed

Lines changed: 877 additions & 20 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { memo, useEffect, useState } from 'react'
44
import { toError } from '@sim/utils/errors'
55
import { generateShortId } from '@sim/utils/id'
66
import { useTheme } from 'next-themes'
7+
import {
8+
mermaidWorkflowCss,
9+
readMermaidThemeVariables,
10+
} from '@/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-theme'
711
import { PreviewLoadingFrame } from './preview-shared'
812
import { ZoomablePreview } from './zoomable-preview'
913

@@ -124,10 +128,23 @@ export const MermaidDiagram = memo(function MermaidDiagram({
124128
const { default: mermaid } = await import('mermaid')
125129
if (cancelled) return
126130

131+
const themeVariables = readMermaidThemeVariables(
132+
document.documentElement,
133+
mermaidTheme === 'dark'
134+
)
127135
mermaid.initialize({
128136
startOnLoad: false,
129137
securityLevel: 'strict',
130-
theme: mermaidTheme,
138+
theme: 'base',
139+
themeCSS: mermaidWorkflowCss(String(themeVariables.fontFamily)),
140+
flowchart: {
141+
curve: 'basis',
142+
padding: 12,
143+
nodeSpacing: 36,
144+
rankSpacing: 56,
145+
wrappingWidth: 180,
146+
},
147+
themeVariables,
131148
})
132149
mermaid.setParseErrorHandler?.(() => undefined)
133150

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Mermaid `base` theme variables drawn from the app's tokens, so diagrams match the editor and
3+
* charts instead of Mermaid's stock palette. Mermaid derives shades from these with its own colour
4+
* library, so every token read here must resolve to a plain hex or rgb value.
5+
*/
6+
export function readMermaidThemeVariables(
7+
element: Element,
8+
darkMode: boolean
9+
): Record<string, string | boolean> {
10+
const styles = getComputedStyle(element)
11+
const token = (name: string) => {
12+
const value = styles.getPropertyValue(name).trim()
13+
if (!value) throw new Error(`Missing diagram theme token ${name}`)
14+
return value
15+
}
16+
const background = token('--bg')
17+
const node = token('--surface-2')
18+
const muted = token('--surface-4')
19+
const subtle = token('--surface-5')
20+
const border = token('--border')
21+
const text = token('--text-primary')
22+
const body = token('--text-body')
23+
const line = token('--text-icon')
24+
return {
25+
darkMode,
26+
fontFamily: getComputedStyle(document.body).fontFamily,
27+
fontSize: '14px',
28+
background,
29+
primaryColor: node,
30+
primaryTextColor: text,
31+
primaryBorderColor: border,
32+
secondaryColor: muted,
33+
secondaryTextColor: body,
34+
secondaryBorderColor: border,
35+
tertiaryColor: subtle,
36+
tertiaryTextColor: body,
37+
tertiaryBorderColor: border,
38+
mainBkg: node,
39+
nodeBorder: border,
40+
textColor: body,
41+
lineColor: token('--workflow-edge'),
42+
clusterBkg: token('--surface-3'),
43+
clusterBorder: border,
44+
edgeLabelBackground: background,
45+
noteBkgColor: muted,
46+
noteTextColor: body,
47+
noteBorderColor: border,
48+
actorBkg: node,
49+
actorBorder: border,
50+
actorTextColor: text,
51+
actorLineColor: line,
52+
signalColor: line,
53+
signalTextColor: body,
54+
labelBoxBkgColor: subtle,
55+
labelBoxBorderColor: border,
56+
labelTextColor: body,
57+
loopTextColor: body,
58+
sequenceNumberColor: background,
59+
pie1: body,
60+
pie2: line,
61+
pie3: token('--text-subtle'),
62+
pie4: token('--text-muted'),
63+
pieStrokeColor: background,
64+
pieTitleTextColor: text,
65+
pieSectionTextColor: background,
66+
pieLegendTextColor: body,
67+
}
68+
}
69+
70+
/**
71+
* Flowcharts drawn like workflow canvas blocks and edges: rounded cards with a 1.5px outline,
72+
* 1.5px edges without arrowheads, and subgraphs as rounded subflow containers. Labels are pinned
73+
* to the font Mermaid measured them with; otherwise they inherit the surrounding document's font
74+
* and line height and overflow their boxes. Edge rules are scoped to flowchart classes so sequence
75+
* and other diagrams keep their arrows.
76+
*/
77+
export function mermaidWorkflowCss(fontFamily: string): string {
78+
return `
79+
.label, .nodeLabel, .edgeLabel, .label p, .nodeLabel p, .edgeLabel p {
80+
font-family: ${fontFamily}; font-size: 14px; line-height: 1.5; letter-spacing: normal;
81+
}
82+
.edgeLabel, .edgeLabel p { font-size: 12px; }
83+
.node rect, .node polygon, .node circle, .node path { stroke-width: 1.5px; }
84+
.node rect { rx: 10px; ry: 10px; }
85+
.cluster rect { rx: 14px; ry: 14px; stroke-width: 1.5px; }
86+
.flowchart-link { stroke-width: 1.5px; marker-end: none !important; }
87+
`
88+
}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Check, ChevronDown, Code, Duplicate, Eye, Wrap } from '@sim/emcn/icons'
1212
import type { ReactNodeViewProps } from '@tiptap/react'
1313
import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
1414
import { DASHBOARD_EMBED_LANGUAGE } from '@/lib/dashboards/embed-language'
15+
import { DIFF_EMBED_LANGUAGE } from '@/lib/diff/embed-language'
1516
import { MarkdownStreamingContext } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-streaming-context'
1617
import { looksLikeMermaid, MermaidDiagram } from '../mermaid-diagram'
1718
import { MarkdownCodeBlock } from './code-block-schema'
@@ -22,6 +23,9 @@ import { useEditorEditable } from './use-editor-editable'
2223
const DashboardEmbed = lazy(() =>
2324
import('@/components/dashboards/dashboard-embed').then((m) => ({ default: m.DashboardEmbed }))
2425
)
26+
const DiffEmbed = lazy(() =>
27+
import('@/components/diff/diff-embed').then((m) => ({ default: m.DiffEmbed }))
28+
)
2529

2630
const PLAIN = 'plain'
2731
const MERMAID = 'mermaid'
@@ -74,7 +78,8 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
7478
const text = node.textContent
7579
const isMermaid = explicitLanguage === MERMAID || (!explicitLanguage && looksLikeMermaid(text))
7680
const isDashboard = explicitLanguage === DASHBOARD_EMBED_LANGUAGE
77-
const isRendered = isMermaid || isDashboard
81+
const isDiff = explicitLanguage === DIFF_EMBED_LANGUAGE
82+
const isRendered = isMermaid || isDashboard || isDiff
7883

7984
// Editable Mermaid shows source while the caret is focused inside the block and re-renders the
8085
// 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
148153
<button
149154
type='button'
150155
aria-label={
151-
showSource ? (isDashboard ? 'Show dashboard' : 'Show diagram') : 'Show source'
156+
showSource
157+
? isDashboard
158+
? 'Show dashboard'
159+
: isDiff
160+
? 'Show diff'
161+
: 'Show diagram'
162+
: 'Show source'
152163
}
153164
onMouseDown={(event) => event.preventDefault()}
154165
onClick={toggleSource}
@@ -225,7 +236,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
225236
// Select the whole node instead of placing a caret, which would flip the block to source.
226237
<div
227238
contentEditable={false}
228-
className={cn(isDashboard && 'dashboard-embed')}
239+
className={cn(isDashboard && 'dashboard-embed', isDiff && 'diff-embed')}
229240
onMouseDown={(event) => {
230241
const target = event.target
231242
if (!(target instanceof Element) || !event.currentTarget.contains(target)) return
@@ -239,6 +250,10 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
239250
<Suspense fallback={null}>
240251
<DashboardEmbed source={text} isStreaming={isStreaming} />
241252
</Suspense>
253+
) : isDiff ? (
254+
<Suspense fallback={null}>
255+
<DiffEmbed source={text} isStreaming={isStreaming} />
256+
</Suspense>
242257
) : (
243258
<MermaidDiagram definition={text} className='mermaid-diagram-frame' />
244259
)}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,19 +276,22 @@
276276
color: var(--brand-secondary);
277277
}
278278

279-
.rich-markdown-prose pre,
280-
.rich-markdown-prose .mermaid-diagram-frame {
279+
.rich-markdown-prose pre {
281280
background: var(--surface-5);
282281
border-radius: 8px;
283282
padding: 1rem;
284283
overflow-x: auto;
285284
}
286285

287-
/* A rendered Mermaid diagram wears the same frame as a code block (single source of truth above),
288-
centered within it. */
286+
/* A rendered Mermaid diagram matches the workflow canvas surface. */
289287
.rich-markdown-prose .mermaid-diagram-frame {
290288
display: flex;
291289
justify-content: center;
290+
background: var(--bg);
291+
border: 1px solid var(--border);
292+
border-radius: 8px;
293+
padding: 1rem;
294+
overflow-x: auto;
292295
}
293296

294297
/* Override ProseMirror's built-in `.ProseMirror pre { white-space: pre-wrap }` (the
@@ -309,8 +312,7 @@
309312
overflow-wrap: anywhere;
310313
}
311314

312-
.dark .rich-markdown-prose pre,
313-
.dark .rich-markdown-prose .mermaid-diagram-frame {
315+
.dark .rich-markdown-prose pre {
314316
background: var(--code-bg);
315317
}
316318

@@ -391,6 +393,13 @@
391393
margin: 1rem 0;
392394
}
393395

396+
/* A diff's links are chips, so they keep their own colour instead of the prose link style. */
397+
.rich-markdown-prose .diff-embed a,
398+
.rich-markdown-prose .diff-embed a:hover {
399+
color: revert-layer;
400+
text-decoration: revert-layer;
401+
}
402+
394403
/* A dashboard results table sizes its own columns and sits flush in its panel. */
395404
.rich-markdown-nodes .dashboard-embed table {
396405
table-layout: auto;

‎apps/sim/components/dashboards/dashboard-panel.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ interface ResultsTableProps {
5757
}
5858
function ResultsTable({ data, timeField, timeZone }: ResultsTableProps) {
5959
return (
60-
<div className='h-full overflow-auto'>
60+
<div className='max-h-[400px] overflow-auto'>
6161
<Table>
6262
<TableHeader>
6363
<TableRow>
@@ -195,7 +195,7 @@ export function DashboardPanel({
195195
: barChartHeight !== null
196196
? 'relative'
197197
: 'relative h-[240px]'
198-
: 'relative h-[400px]')
198+
: 'relative grid min-h-[120px]')
199199
)}
200200
style={!timeSeries && barChartHeight !== null ? { height: barChartHeight } : undefined}
201201
>

0 commit comments

Comments
 (0)