@@ -578,14 +578,16 @@ interface MermaidProps {
578578 zoomingEnabled ?: boolean ;
579579}
580580
581- // Full screen modal component for the diagram
581+ // Full screen modal component for the diagram with svg-pan-zoom
582582const FullScreenModal : React . FC < {
583583 isOpen : boolean ;
584584 onClose : ( ) => void ;
585- children : React . ReactNode ;
586- } > = ( { isOpen, onClose, children } ) => {
585+ svgHtml : string ;
586+ } > = ( { isOpen, onClose, svgHtml } ) => {
587587 const modalRef = useRef < HTMLDivElement > ( null ) ;
588- const [ zoom , setZoom ] = useState ( 1 ) ;
588+ const svgContainerRef = useRef < HTMLDivElement > ( null ) ;
589+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
590+ const panZoomRef = useRef < any > ( null ) ;
589591
590592 // Close on Escape key
591593 useEffect ( ( ) => {
@@ -621,61 +623,104 @@ const FullScreenModal: React.FC<{
621623 } ;
622624 } , [ isOpen , onClose ] ) ;
623625
624- // Reset zoom when modal opens
626+ // Initialize svg-pan- zoom in the modal
625627 useEffect ( ( ) => {
626- if ( isOpen ) {
627- setZoom ( 1 ) ;
628- }
629- } , [ isOpen ] ) ;
628+ if ( ! isOpen || ! svgContainerRef . current ) return ;
629+
630+ const initPanZoom = async ( ) => {
631+ const svgElement = svgContainerRef . current ?. querySelector ( 'svg' ) ;
632+ if ( ! svgElement ) return ;
633+
634+ svgElement . style . maxWidth = 'none' ;
635+ svgElement . style . width = '100%' ;
636+ svgElement . style . height = '100%' ;
637+
638+ try {
639+ const svgPanZoom = ( await import ( 'svg-pan-zoom' ) ) . default ;
640+ panZoomRef . current = svgPanZoom ( svgElement , {
641+ zoomEnabled : true ,
642+ controlIconsEnabled : true ,
643+ fit : true ,
644+ center : true ,
645+ minZoom : 0.1 ,
646+ maxZoom : 10 ,
647+ zoomScaleSensitivity : 0.3 ,
648+ } ) ;
649+ } catch ( error ) {
650+ console . error ( 'Failed to load svg-pan-zoom in modal:' , error ) ;
651+ }
652+ } ;
653+
654+ setTimeout ( ( ) => { void initPanZoom ( ) ; } , 200 ) ;
655+
656+ return ( ) => {
657+ if ( panZoomRef . current ) {
658+ try { panZoomRef . current . destroy ( ) ; } catch { /* ignore */ }
659+ panZoomRef . current = null ;
660+ }
661+ } ;
662+ } , [ isOpen , svgHtml ] ) ;
663+
664+ const handleZoomIn = ( ) => { panZoomRef . current ?. zoomIn ( ) ; } ;
665+ const handleZoomOut = ( ) => { panZoomRef . current ?. zoomOut ( ) ; } ;
666+ const handleReset = ( ) => { panZoomRef . current ?. resetZoom ( ) ; panZoomRef . current ?. resetPan ( ) ; panZoomRef . current ?. fit ( ) ; panZoomRef . current ?. center ( ) ; } ;
630667
631668 if ( ! isOpen ) return null ;
632669
633670 return (
634671 < div className = "fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4" >
635672 < div
636673 ref = { modalRef }
637- className = "bg-card border border-border rounded-lg shadow-lg max-w-5xl max-h-[90vh ] w-full overflow-hidden flex flex-col"
674+ className = "bg-card border border-border rounded-lg shadow-lg max-w-6xl max-h-[92vh ] w-full overflow-hidden flex flex-col"
638675 >
639676 { /* Modal header with controls */ }
640677 < div className = "flex items-center justify-between p-4 border-b border-border bg-muted/40" >
641- < div className = "font-semibold text-foreground" > Diagram View</ div >
642- < div className = "flex items-center gap-4" >
643- < div className = "flex items-center gap-2" >
644- < button
645- onClick = { ( ) => setZoom ( Math . max ( 0.5 , zoom - 0.1 ) ) }
646- className = "text-foreground hover:bg-accent hover:text-accent-foreground p-2 rounded-md border border-input transition-colors bg-background"
647- aria-label = "Zoom out"
648- >
649- < svg xmlns = "http://www.w3.org/2000/svg" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
650- < circle cx = "11" cy = "11" r = "8" > </ circle >
651- < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" > </ line >
652- < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" > </ line >
653- </ svg >
654- </ button >
655- < span className = "text-sm text-muted-foreground w-12 text-center" > { Math . round ( zoom * 100 ) } %</ span >
656- < button
657- onClick = { ( ) => setZoom ( Math . min ( 2 , zoom + 0.1 ) ) }
658- className = "text-foreground hover:bg-accent hover:text-accent-foreground p-2 rounded-md border border-input transition-colors bg-background"
659- aria-label = "Zoom in"
660- >
661- < svg xmlns = "http://www.w3.org/2000/svg" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
662- < circle cx = "11" cy = "11" r = "8" > </ circle >
663- < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" > </ line >
664- < line x1 = "11" y1 = "8" x2 = "11" y2 = "14" > </ line >
665- < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" > </ line >
666- </ svg >
667- </ button >
668- < button
669- onClick = { ( ) => setZoom ( 1 ) }
670- className = "text-foreground hover:bg-accent hover:text-accent-foreground p-2 rounded-md border border-input transition-colors bg-background"
671- aria-label = "Reset zoom"
672- >
673- < svg xmlns = "http://www.w3.org/2000/svg" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
674- < path d = "M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" > </ path >
675- < path d = "M21 3v5h-5" > </ path >
676- </ svg >
677- </ button >
678- </ div >
678+ < div className = "font-semibold text-foreground flex items-center gap-2" >
679+ < svg xmlns = "http://www.w3.org/2000/svg" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
680+ < rect x = "3" y = "3" width = "18" height = "18" rx = "2" ry = "2" > </ rect >
681+ < line x1 = "3" y1 = "9" x2 = "21" y2 = "9" > </ line >
682+ < line x1 = "9" y1 = "21" x2 = "9" y2 = "9" > </ line >
683+ </ svg >
684+ Diagram View
685+ </ div >
686+ < div className = "flex items-center gap-2" >
687+ < span className = "text-xs text-muted-foreground mr-2" > Scroll to zoom, drag to pan</ span >
688+ < button
689+ onClick = { handleZoomOut }
690+ className = "text-foreground hover:bg-accent hover:text-accent-foreground p-2 rounded-md border border-input transition-colors bg-background"
691+ aria-label = "Zoom out"
692+ >
693+ < svg xmlns = "http://www.w3.org/2000/svg" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
694+ < circle cx = "11" cy = "11" r = "8" > </ circle >
695+ < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" > </ line >
696+ < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" > </ line >
697+ </ svg >
698+ </ button >
699+ < button
700+ onClick = { handleZoomIn }
701+ className = "text-foreground hover:bg-accent hover:text-accent-foreground p-2 rounded-md border border-input transition-colors bg-background"
702+ aria-label = "Zoom in"
703+ >
704+ < svg xmlns = "http://www.w3.org/2000/svg" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
705+ < circle cx = "11" cy = "11" r = "8" > </ circle >
706+ < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" > </ line >
707+ < line x1 = "11" y1 = "8" x2 = "11" y2 = "14" > </ line >
708+ < line x1 = "8" y1 = "11" x2 = "14" y2 = "11" > </ line >
709+ </ svg >
710+ </ button >
711+ < button
712+ onClick = { handleReset }
713+ className = "text-foreground hover:bg-accent hover:text-accent-foreground p-2 rounded-md border border-input transition-colors bg-background"
714+ aria-label = "Fit to view"
715+ >
716+ < svg xmlns = "http://www.w3.org/2000/svg" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
717+ < path d = "M8 3H5a2 2 0 0 0-2 2v3" > </ path >
718+ < path d = "M21 8V5a2 2 0 0 0-2-2h-3" > </ path >
719+ < path d = "M3 16v3a2 2 0 0 0 2 2h3" > </ path >
720+ < path d = "M16 21h3a2 2 0 0 0 2-2v-3" > </ path >
721+ </ svg >
722+ </ button >
723+ < div className = "w-px h-6 bg-border mx-1" > </ div >
679724 < button
680725 onClick = { onClose }
681726 className = "text-muted-foreground hover:text-foreground hover:bg-accent p-2 rounded-md transition-colors"
@@ -689,18 +734,13 @@ const FullScreenModal: React.FC<{
689734 </ div >
690735 </ div >
691736
692- { /* Modal content with zoom */ }
693- < div className = "overflow-auto p-6 flex-1 flex items-center justify-center bg-background" >
694- < div
695- style = { {
696- transform : `scale(${ zoom } )` ,
697- transformOrigin : 'center center' ,
698- transition : 'transform 0.3s ease-out'
699- } }
700- >
701- { children }
702- </ div >
703- </ div >
737+ { /* Modal content with svg-pan-zoom */ }
738+ < div
739+ ref = { svgContainerRef }
740+ className = "flex-1 bg-background mermaid-diagram-container"
741+ style = { { minHeight : '500px' } }
742+ dangerouslySetInnerHTML = { { __html : svgHtml } }
743+ />
704744 </ div >
705745 </ div >
706746 ) ;
@@ -731,10 +771,19 @@ const Mermaid: React.FC<MermaidProps> = ({ chart, className = '', zoomingEnabled
731771 return ( ) => observer . disconnect ( ) ;
732772 } , [ ] ) ;
733773
734- // Initialize pan-zoom functionality when SVG is rendered
774+ // Initialize pan-zoom functionality when SVG is rendered or modal closes
775+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
776+ const inlinePanZoomRef = useRef < any > ( null ) ;
777+
735778 useEffect ( ( ) => {
736- if ( svg && zoomingEnabled && containerRef . current ) {
779+ if ( svg && zoomingEnabled && containerRef . current && ! isFullscreen ) {
737780 const initializePanZoom = async ( ) => {
781+ // Destroy previous instance if it exists
782+ if ( inlinePanZoomRef . current ) {
783+ try { inlinePanZoomRef . current . destroy ( ) ; } catch { /* ignore */ }
784+ inlinePanZoomRef . current = null ;
785+ }
786+
738787 const svgElement = containerRef . current ?. querySelector ( "svg" ) ;
739788 if ( svgElement ) {
740789 // Remove any max-width constraints but keep height responsive
@@ -747,7 +796,7 @@ const Mermaid: React.FC<MermaidProps> = ({ chart, className = '', zoomingEnabled
747796 // Dynamically import svg-pan-zoom only when needed in the browser
748797 const svgPanZoom = ( await import ( "svg-pan-zoom" ) ) . default ;
749798
750- svgPanZoom ( svgElement , {
799+ inlinePanZoomRef . current = svgPanZoom ( svgElement , {
751800 zoomEnabled : true ,
752801 controlIconsEnabled : true ,
753802 fit : true ,
@@ -767,7 +816,14 @@ const Mermaid: React.FC<MermaidProps> = ({ chart, className = '', zoomingEnabled
767816 void initializePanZoom ( ) ;
768817 } , 150 ) ;
769818 }
770- } , [ svg , zoomingEnabled ] ) ;
819+
820+ return ( ) => {
821+ if ( inlinePanZoomRef . current ) {
822+ try { inlinePanZoomRef . current . destroy ( ) ; } catch { /* ignore */ }
823+ inlinePanZoomRef . current = null ;
824+ }
825+ } ;
826+ } , [ svg , zoomingEnabled , isFullscreen ] ) ;
771827
772828 const renderChart = useCallback ( async ( isMountedRef : { current : boolean } ) => {
773829 if ( ! chart || ! isMountedRef . current ) return ;
@@ -885,9 +941,8 @@ const Mermaid: React.FC<MermaidProps> = ({ chart, className = '', zoomingEnabled
885941 < FullScreenModal
886942 isOpen = { isFullscreen }
887943 onClose = { ( ) => setIsFullscreen ( false ) }
888- >
889- < div className = "mermaid-diagram-container" dangerouslySetInnerHTML = { { __html : svg } } />
890- </ FullScreenModal >
944+ svgHtml = { svg }
945+ />
891946 </ >
892947 ) ;
893948} ;
0 commit comments