1- import { Div , Flex } from 'honorable'
1+ import { Div } from 'honorable'
22import {
33 type ComponentProps ,
44 type PropsWithChildren ,
@@ -18,6 +18,7 @@ import useResizeObserver from '../hooks/useResizeObserver'
1818
1919import Card , { type CardProps } from './Card'
2020import Highlight from './Highlight'
21+ import { downloadMermaidSvg , Mermaid , MermaidRefHandle } from './Mermaid'
2122import { ListBoxItem } from './ListBoxItem'
2223import { Select } from './Select'
2324import SubTab from './SubTab'
@@ -34,6 +35,9 @@ import CopyIcon from './icons/CopyIcon'
3435import DropdownArrowIcon from './icons/DropdownArrowIcon'
3536import FileIcon from './icons/FileIcon'
3637import Button from './Button'
38+ import { DownloadIcon } from '../icons'
39+ import IconFrame from './IconFrame'
40+ import Flex from './Flex'
3741
3842type CodeProps = Omit < CardProps , 'children' > & {
3943 children ?: string
@@ -43,6 +47,7 @@ type CodeProps = Omit<CardProps, 'children'> & {
4347 tabs ?: CodeTabData [ ]
4448 title ?: ReactNode
4549 onSelectedTabChange ?: ( key : string ) => void
50+ isStreaming ?: boolean // currently just used to block mermaid from rendering mid-stream, but might have other uses later on
4651}
4752
4853type TabInterfaceT = 'tabs' | 'dropdown'
@@ -133,6 +138,15 @@ const CopyButton = styled(CopyButtonBase)<{ $verticallyCenter: boolean }>(
133138 } )
134139)
135140
141+ const MermaidButtonsSC = styled . div ( ( { theme } ) => ( {
142+ position : 'absolute' ,
143+ right : theme . spacing . medium ,
144+ top : theme . spacing . medium ,
145+ gap : theme . spacing . xsmall ,
146+ display : 'flex' ,
147+ alignItems : 'center' ,
148+ } ) )
149+
136150type CodeTabData = {
137151 key : string
138152 label ?: string
@@ -297,8 +311,16 @@ const CodeSelect = styled(CodeSelectUnstyled)<{ $isDisabled?: boolean }>(
297311function CodeContent ( {
298312 children,
299313 hasSetHeight,
314+ language,
315+ isStreaming = false ,
300316 ...props
301- } : ComponentProps < typeof Highlight > & { hasSetHeight : boolean } ) {
317+ } : ComponentProps < typeof Highlight > & {
318+ hasSetHeight : boolean
319+ isStreaming ?: boolean
320+ } ) {
321+ const { spacing, borderRadiuses } = useTheme ( )
322+ const mermaidRef = useRef < MermaidRefHandle > ( null )
323+ const [ mermaidError , setMermaidError ] = useState < Nullable < Error > > ( null )
302324 const [ copied , setCopied ] = useState ( false )
303325 const codeString = children ?. trim ( ) || ''
304326 const multiLine = ! ! codeString . match ( / \r ? \n / ) || hasSetHeight
@@ -313,38 +335,80 @@ function CodeContent({
313335 useEffect ( ( ) => {
314336 if ( copied ) {
315337 const timeout = setTimeout ( ( ) => setCopied ( false ) , 1000 )
316-
317338 return ( ) => clearTimeout ( timeout )
318339 }
319340 } , [ copied ] )
320341
321- if ( typeof children !== 'string' ) {
342+ if ( typeof children !== 'string' )
322343 throw new Error ( 'Code component expects a string as its children' )
323- }
344+
345+ const isMermaidDownloadable =
346+ language === 'mermaid' && ! isStreaming && ! mermaidError
324347
325348 return (
326- < Div
327- height = "100%"
328- overflow = "auto"
329- alignItems = "center"
349+ < div
350+ css = { {
351+ height : '100%' ,
352+ overflow : 'auto' ,
353+ alignItems : 'center' ,
354+ } }
330355 >
331- < CopyButton
332- copied = { copied }
333- handleCopy = { handleCopy }
334- $verticallyCenter = { ! multiLine }
335- />
336- < Div
337- paddingHorizontal = "medium"
338- paddingVertical = { multiLine ? 'medium' : 'small' }
356+ { isMermaidDownloadable ? (
357+ < MermaidButtonsSC >
358+ < IconFrame
359+ clickable
360+ onClick = { handleCopy }
361+ icon = { copied ? < CheckIcon /> : < CopyIcon /> }
362+ type = "floating"
363+ tooltip = "Copy Mermaid code"
364+ />
365+ < IconFrame
366+ clickable
367+ onClick = { ( ) => {
368+ const { svgStr } = mermaidRef . current
369+ if ( ! svgStr ) return
370+ downloadMermaidSvg ( svgStr )
371+ } }
372+ icon = { < DownloadIcon /> }
373+ type = "floating"
374+ tooltip = "Download as PNG"
375+ />
376+ </ MermaidButtonsSC >
377+ ) : (
378+ < CopyButton
379+ copied = { copied }
380+ handleCopy = { handleCopy }
381+ $verticallyCenter = { ! multiLine }
382+ />
383+ ) }
384+ < div
385+ css = { {
386+ ...( isMermaidDownloadable ? { backgroundColor : 'white' } : { } ) ,
387+ padding : `${ multiLine ? spacing . medium : spacing . small } px ${
388+ spacing . medium
389+ } px`,
390+ borderBottomLeftRadius : borderRadiuses . large ,
391+ borderBottomRightRadius : borderRadiuses . large ,
392+ } }
339393 >
340- < Highlight
341- key = { codeString }
342- { ...props }
343- >
344- { codeString }
345- </ Highlight >
346- </ Div >
347- </ Div >
394+ { isMermaidDownloadable ? (
395+ < Mermaid
396+ ref = { mermaidRef }
397+ setError = { setMermaidError }
398+ >
399+ { codeString }
400+ </ Mermaid >
401+ ) : (
402+ < Highlight
403+ key = { codeString }
404+ language = { language }
405+ { ...props }
406+ >
407+ { codeString }
408+ </ Highlight >
409+ ) }
410+ </ div >
411+ </ div >
348412 )
349413}
350414
@@ -357,6 +421,7 @@ function CodeUnstyled({
357421 tabs,
358422 title,
359423 onSelectedTabChange,
424+ isStreaming = false ,
360425 ...props
361426} : CodeProps ) {
362427 const parentFillLevel = useFillLevel ( )
@@ -381,9 +446,7 @@ function CodeUnstyled({
381446 selectedKey : selectedTabKey ,
382447 onSelectionChange : ( key : string ) => {
383448 setSelectedTabKey ( key )
384- if ( typeof onSelectedTabChange === 'function' ) {
385- onSelectedTabChange ( key )
386- }
449+ if ( typeof onSelectedTabChange === 'function' ) onSelectedTabChange ( key )
387450 } ,
388451 } ) ,
389452 [ onSelectedTabChange , selectedTabKey , tabInterface , setTabInterface , tabs ]
@@ -449,6 +512,7 @@ function CodeUnstyled({
449512 language = { tab . language }
450513 showLineNumbers = { showLineNumbers }
451514 hasSetHeight = { hasSetHeight }
515+ isStreaming = { isStreaming }
452516 >
453517 { tab . content }
454518 </ CodeContent >
@@ -464,6 +528,7 @@ function CodeUnstyled({
464528 language = { language }
465529 showLineNumbers = { showLineNumbers }
466530 hasSetHeight = { hasSetHeight }
531+ isStreaming = { isStreaming }
467532 >
468533 { children }
469534 </ CodeContent >
@@ -479,12 +544,12 @@ function CodeUnstyled({
479544}
480545
481546const Code = styled ( CodeUnstyled ) ( ( _ ) => ( {
482- [ `${ CopyButton } ` ] : {
547+ [ `${ CopyButton } , ${ MermaidButtonsSC } ` ] : {
483548 opacity : 0 ,
484549 pointerEvents : 'none' ,
485550 transition : 'opacity 0.2s ease' ,
486551 } ,
487- [ `&:hover ${ CopyButton } ` ] : {
552+ [ `&:hover ${ CopyButton } , &:hover ${ MermaidButtonsSC } ` ] : {
488553 opacity : 1 ,
489554 pointerEvents : 'auto' ,
490555 transition : 'opacity 0.2s ease' ,
0 commit comments