1- import { useEffect , useState } from 'react'
1+ import { useContext , useEffect , useState } from 'react'
22import {
33 chipVariants ,
44 cn ,
@@ -11,6 +11,9 @@ import {
1111import { Check , ChevronDown , Code , Duplicate , Eye , Wrap } from '@sim/emcn/icons'
1212import type { ReactNodeViewProps } from '@tiptap/react'
1313import { NodeViewContent , NodeViewWrapper , ReactNodeViewRenderer } from '@tiptap/react'
14+ import { DashboardEmbed } from '@/components/dashboards/dashboard-embed'
15+ import { DASHBOARD_EMBED_LANGUAGE } from '@/lib/dashboards/spec'
16+ import { MarkdownStreamingContext } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-streaming-context'
1417import { looksLikeMermaid , MermaidDiagram } from '../mermaid-diagram'
1518import { MarkdownCodeBlock } from './code-block-schema'
1619import { detectLanguage } from './detect-language'
@@ -51,7 +54,8 @@ const CONTROL_CLASS =
5154 * whenever the cursor is outside it (and always in read-only), and as editable source while the
5255 * cursor is inside, re-rendering on blur (the Linear/GitHub model). The source `<pre>` stays mounted
5356 * (hidden behind the diagram) so ProseMirror keeps managing its contentDOM, and the node remains an
54- * ordinary code block, so markdown round-trips unchanged.
57+ * ordinary code block, so markdown round-trips unchanged. A ```dashboard fence renders live
58+ * dashboard panels the same way.
5559 */
5660function CodeBlockView ( { node, updateAttributes, editor, getPos } : ReactNodeViewProps ) {
5761 const [ wrap , setWrap ] = useState ( false )
@@ -60,16 +64,19 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
6064 const [ peekSource , setPeekSource ] = useState ( false )
6165 const { copied, copy } = useCopyToClipboard ( { resetMs : 1500 } )
6266 const editable = useEditorEditable ( editor )
67+ const isStreaming = useContext ( MarkdownStreamingContext )
6368
6469 const explicitLanguage = node . attrs . language as string | null
6570 const text = node . textContent
6671 const isMermaid = explicitLanguage === MERMAID || ( ! explicitLanguage && looksLikeMermaid ( text ) )
72+ const isDashboard = explicitLanguage === DASHBOARD_EMBED_LANGUAGE
73+ const isRendered = isMermaid || isDashboard
6774
6875 // Editable Mermaid shows source while the caret is focused inside the block and re-renders the
6976 // diagram on blur (the Linear/GitHub model). The Show source / Show diagram control drives this by
7077 // focusing into / blurring the block; read-only uses {@link peekSource} since there is no caret.
7178 useEffect ( ( ) => {
72- if ( ! isMermaid || ! editable ) {
79+ if ( ! isRendered || ! editable ) {
7380 setEditingInline ( false )
7481 return
7582 }
@@ -92,13 +99,13 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
9299 editor . off ( 'focus' , sync )
93100 editor . off ( 'blur' , sync )
94101 }
95- } , [ editor , getPos , isMermaid , editable ] )
102+ } , [ editor , getPos , isRendered , editable ] )
96103
97104 const showSource = editable ? editingInline : peekSource
98- const showDiagram = isMermaid && text . trim ( ) . length > 0 && ! showSource
105+ const showRendered = isRendered && text . trim ( ) . length > 0 && ! showSource
99106
100- // Skip language detection on the mermaid path — the picker/label never render there.
101- const language = explicitLanguage ?? ( isMermaid ? null : detectLanguage ( text ) ) ?? PLAIN
107+ // Skip language detection on rendered blocks — the picker/label never render there.
108+ const language = explicitLanguage ?? ( isRendered ? null : detectLanguage ( text ) ) ?? PLAIN
102109 const label =
103110 LANGUAGE_OPTIONS . find ( ( option ) => option . value === language ) ?. label ??
104111 explicitLanguage ??
@@ -133,18 +140,20 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
133140 ) }
134141 contentEditable = { false }
135142 >
136- { isMermaid && (
143+ { isRendered && (
137144 < button
138145 type = 'button'
139- aria-label = { showSource ? 'Show diagram' : 'Show source' }
146+ aria-label = {
147+ showSource ? ( isDashboard ? 'Show dashboard' : 'Show diagram' ) : 'Show source'
148+ }
140149 onMouseDown = { ( event ) => event . preventDefault ( ) }
141150 onClick = { toggleSource }
142151 className = { CONTROL_CLASS }
143152 >
144153 { showSource ? < Eye /> : < Code /> }
145154 </ button >
146155 ) }
147- { ! isMermaid &&
156+ { ! isRendered &&
148157 ( editable ? (
149158 // Editable: a language picker. Read-only: a static label — selecting a language calls
150159 // updateAttributes, which would mutate a doc that must not change.
@@ -180,7 +189,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
180189 { label }
181190 </ span >
182191 ) ) }
183- { ! isMermaid && editable && (
192+ { ! isRendered && editable && (
184193 < button
185194 type = 'button'
186195 aria-label = 'Toggle line wrap'
@@ -205,22 +214,28 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView
205214 { copied ? < Check /> : < Duplicate /> }
206215 </ button >
207216 </ div >
208- < pre className = { cn ( 'code-editor-theme pr-20' , showDiagram && 'hidden' ) } data-wrap = { wrap } >
217+ < pre className = { cn ( 'code-editor-theme pr-20' , showRendered && 'hidden' ) } data-wrap = { wrap } >
209218 < NodeViewContent < 'code' > as = 'code' />
210219 </ pre >
211- { showDiagram && (
212- // Clicking the diagram selects the whole node (same selection ring as an image/code block)
213- // instead of dropping a caret inside — preventDefault stops ProseMirror placing the caret,
214- // which would otherwise flip to source. Editing is an explicit Show source / blur action.
220+ { showRendered && (
221+ // Select the whole node instead of placing a caret, which would flip the block to source.
215222 < div
216223 contentEditable = { false }
224+ className = { cn ( isDashboard && 'dashboard-embed' ) }
217225 onMouseDown = { ( event ) => {
226+ const target = event . target
227+ if ( ! ( target instanceof Element ) || ! event . currentTarget . contains ( target ) ) return
228+ if ( target . closest ( 'button, input' ) ) return
218229 event . preventDefault ( )
219230 const pos = typeof getPos === 'function' ? getPos ( ) : null
220231 if ( typeof pos === 'number' ) editor . commands . setNodeSelection ( pos )
221232 } }
222233 >
223- < MermaidDiagram definition = { text } className = 'mermaid-diagram-frame' />
234+ { isDashboard ? (
235+ < DashboardEmbed source = { text } isStreaming = { isStreaming } />
236+ ) : (
237+ < MermaidDiagram definition = { text } className = 'mermaid-diagram-frame' />
238+ ) }
224239 </ div >
225240 ) }
226241 </ NodeViewWrapper >
0 commit comments