Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string | boolean> {
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,
Comment thread
TheodoreSpeaks marked this conversation as resolved.
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; }
`
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -148,7 +153,13 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
<button
type='button'
aria-label={
showSource ? (isDashboard ? 'Show dashboard' : 'Show diagram') : 'Show source'
showSource
? isDashboard
? 'Show dashboard'
: isDiff
? 'Show diff'
: 'Show diagram'
: 'Show source'
}
onMouseDown={(event) => event.preventDefault()}
onClick={toggleSource}
Expand Down Expand Up @@ -225,7 +236,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
// Select the whole node instead of placing a caret, which would flip the block to source.
<div
contentEditable={false}
className={cn(isDashboard && 'dashboard-embed')}
className={cn(isDashboard && 'dashboard-embed', isDiff && 'diff-embed')}
onMouseDown={(event) => {
const target = event.target
if (!(target instanceof Element) || !event.currentTarget.contains(target)) return
Expand All @@ -239,6 +250,10 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
<Suspense fallback={null}>
<DashboardEmbed source={text} isStreaming={isStreaming} />
</Suspense>
) : isDiff ? (
<Suspense fallback={null}>
<DiffEmbed source={text} isStreaming={isStreaming} />
</Suspense>
) : (
<MermaidDiagram definition={text} className='mermaid-diagram-frame' />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,19 +276,22 @@
color: var(--brand-secondary);
}

.rich-markdown-prose pre,
.rich-markdown-prose .mermaid-diagram-frame {
.rich-markdown-prose pre {
background: var(--surface-5);
border-radius: 8px;
padding: 1rem;
overflow-x: auto;
}

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

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

.dark .rich-markdown-prose pre,
.dark .rich-markdown-prose .mermaid-diagram-frame {
.dark .rich-markdown-prose pre {
background: var(--code-bg);
}

Expand Down Expand Up @@ -391,6 +393,13 @@
margin: 1rem 0;
}

/* A diff's links are chips, so they keep their own colour instead of the prose link style. */
.rich-markdown-prose .diff-embed a,
.rich-markdown-prose .diff-embed a:hover {
color: revert-layer;
text-decoration: revert-layer;
}

/* A dashboard results table sizes its own columns and sits flush in its panel. */
.rich-markdown-nodes .dashboard-embed table {
table-layout: auto;
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/components/dashboards/dashboard-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ interface ResultsTableProps {
}
function ResultsTable({ data, timeField, timeZone }: ResultsTableProps) {
return (
<div className='h-full overflow-auto'>
<div className='max-h-[400px] overflow-auto'>
<Table>
<TableHeader>
<TableRow>
Expand Down Expand Up @@ -195,7 +195,7 @@ export function DashboardPanel({
: barChartHeight !== null
? 'relative'
: 'relative h-[240px]'
: 'relative h-[400px]')
: 'relative grid min-h-[120px]')
)}
style={!timeSeries && barChartHeight !== null ? { height: barChartHeight } : undefined}
>
Expand Down
Loading
Loading