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 } ${  
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