@@ -45,8 +45,207 @@ function renderMermaidDiagrams() {
4545 if (parent ) {
4646 try {
4747 const id = ` mermaid-${Math .random ().toString (36 ).substr (2 , 9 )} ` ;
48- mermaid .render (id , code ).then ((value ) => (parent .innerHTML = value .svg ));
49- parent .classList .add (" not-prose" );
48+ mermaid .render (id , code ).then ((value ) => {
49+ // Create container with zoom controls
50+ const container = document .createElement (" div" );
51+ container .className = " mermaid-zoom-container" ;
52+
53+ const controls = document .createElement (" div" );
54+ controls .className = " mermaid-zoom-controls" ;
55+
56+ // Create buttons
57+ const zoomInBtn = document .createElement (" button" );
58+ zoomInBtn .textContent = " +" ;
59+ zoomInBtn .className = " mermaid-zoom-btn" ;
60+ zoomInBtn .setAttribute (" aria-label" , " Zoom in" );
61+
62+ const zoomOutBtn = document .createElement (" button" );
63+ zoomOutBtn .textContent = " −" ;
64+ zoomOutBtn .className = " mermaid-zoom-btn" ;
65+ zoomOutBtn .setAttribute (" aria-label" , " Zoom out" );
66+
67+ const resetBtn = document .createElement (" button" );
68+ resetBtn .textContent = " ⌂" ;
69+ resetBtn .className = " mermaid-zoom-btn" ;
70+ resetBtn .setAttribute (" aria-label" , " Reset zoom" );
71+
72+ const fullscreenBtn = document .createElement (" button" );
73+ fullscreenBtn .textContent = " ⛶" ;
74+ fullscreenBtn .className = " mermaid-zoom-btn" ;
75+ fullscreenBtn .setAttribute (" aria-label" , " Toggle fullscreen" );
76+
77+ controls .appendChild (zoomInBtn );
78+ controls .appendChild (zoomOutBtn );
79+ controls .appendChild (resetBtn );
80+ controls .appendChild (fullscreenBtn );
81+
82+ const content = document .createElement (" div" );
83+ content .className = " mermaid-zoom-content" ;
84+ content .innerHTML = value .svg ;
85+
86+ container .appendChild (controls );
87+ container .appendChild (content );
88+ parent .innerHTML = " " ;
89+ parent .appendChild (container );
90+ parent .classList .add (" not-prose" );
91+
92+ // Zoom functionality
93+ let scale = 1 ;
94+ let originX = 0 ;
95+ let originY = 0 ;
96+ let isDragging = false ;
97+ let startX = 0 ;
98+ let startY = 0 ;
99+
100+ const svg = content .querySelector (" svg" );
101+ if (svg ) {
102+ svg .style .cursor = " grab" ;
103+ svg .style .transformOrigin = " 0 0" ;
104+
105+ function updateTransform() {
106+ svg .style .transform = ` translate(${originX }px, ${originY }px) scale(${scale }) ` ;
107+ }
108+
109+ let lastMouseX = content .offsetWidth / 2 ;
110+ let lastMouseY = content .offsetHeight / 2 ;
111+
112+ content .addEventListener (" mousemove" , (e ) => {
113+ if (! isDragging ) {
114+ const rect = content .getBoundingClientRect ();
115+ lastMouseX = e .clientX - rect .left ;
116+ lastMouseY = e .clientY - rect .top ;
117+ }
118+ });
119+
120+ function zoomToPoint(newScale , mouseX , mouseY ) {
121+ const oldScale = scale ;
122+ const scaleDiff = newScale / oldScale ;
123+
124+ const newOriginX = mouseX - (mouseX - originX ) * scaleDiff ;
125+ const newOriginY = mouseY - (mouseY - originY ) * scaleDiff ;
126+
127+ scale = newScale ;
128+ originX = newOriginX ;
129+ originY = newOriginY ;
130+ updateTransform ();
131+ }
132+
133+ zoomInBtn .addEventListener (" click" , () => {
134+ svg .style .transition = " transform 0.2s ease" ;
135+ const newScale = Math .min (scale * 1.2 , 5 );
136+ zoomToPoint (newScale , lastMouseX , lastMouseY );
137+ setTimeout (() => (svg .style .transition = " none" ), 200 );
138+ });
139+
140+ zoomOutBtn .addEventListener (" click" , () => {
141+ svg .style .transition = " transform 0.2s ease" ;
142+ const newScale = Math .max (scale / 1.2 , 0.1 );
143+ zoomToPoint (newScale , lastMouseX , lastMouseY );
144+ setTimeout (() => (svg .style .transition = " none" ), 200 );
145+ });
146+
147+ resetBtn .addEventListener (" click" , () => {
148+ svg .style .transition = " transform 0.3s ease" ;
149+ scale = 1 ;
150+ originX = 0 ;
151+ originY = 0 ;
152+ updateTransform ();
153+ setTimeout (() => (svg .style .transition = " none" ), 300 );
154+ });
155+
156+ fullscreenBtn .addEventListener (" click" , () => {
157+ if (container .classList .contains (" mermaid-fullscreen" )) {
158+ container .classList .remove (" mermaid-fullscreen" );
159+ document .body .style .overflow = " " ;
160+ } else {
161+ container .classList .add (" mermaid-fullscreen" );
162+ document .body .style .overflow = " hidden" ;
163+ }
164+ });
165+
166+ // Mouse wheel zoom
167+ content .addEventListener (" wheel" , (e ) => {
168+ e .preventDefault ();
169+ const rect = content .getBoundingClientRect ();
170+ const mouseX = e .clientX - rect .left ;
171+ const mouseY = e .clientY - rect .top ;
172+
173+ const zoomFactor = 1.02 ;
174+ const newScale =
175+ e .deltaY > 0 ? Math .max (scale / zoomFactor , 0.1 ) : Math .min (scale * zoomFactor , 5 );
176+
177+ zoomToPoint (newScale , mouseX , mouseY );
178+ });
179+
180+ svg .addEventListener (" mousedown" , (e ) => {
181+ isDragging = true ;
182+ startX = e .clientX - originX ;
183+ startY = e .clientY - originY ;
184+ svg .style .cursor = " grabbing" ;
185+ e .preventDefault ();
186+ });
187+
188+ document .addEventListener (" mousemove" , (e ) => {
189+ if (isDragging ) {
190+ originX = e .clientX - startX ;
191+ originY = e .clientY - startY ;
192+ updateTransform ();
193+ }
194+ });
195+
196+ document .addEventListener (" mouseup" , () => {
197+ if (isDragging ) {
198+ isDragging = false ;
199+ svg .style .cursor = " grab" ;
200+ }
201+ });
202+
203+ // Touch support
204+ let initialDistance = 0 ;
205+ let initialScale = 1 ;
206+
207+ svg .addEventListener (" touchstart" , (e ) => {
208+ if (e .touches .length === 2 ) {
209+ const touch1 = e .touches [0 ];
210+ const touch2 = e .touches [1 ];
211+ initialDistance = Math .hypot (
212+ touch2 .clientX - touch1 .clientX ,
213+ touch2 .clientY - touch1 .clientY ,
214+ );
215+ initialScale = scale ;
216+ } else if (e .touches .length === 1 ) {
217+ isDragging = true ;
218+ const touch = e .touches [0 ];
219+ startX = touch .clientX - originX ;
220+ startY = touch .clientY - originY ;
221+ }
222+ e .preventDefault ();
223+ });
224+
225+ svg .addEventListener (" touchmove" , (e ) => {
226+ if (e .touches .length === 2 ) {
227+ const touch1 = e .touches [0 ];
228+ const touch2 = e .touches [1 ];
229+ const distance = Math .hypot (
230+ touch2 .clientX - touch1 .clientX ,
231+ touch2 .clientY - touch1 .clientY ,
232+ );
233+ scale = Math .max (0.1 , Math .min (5 , initialScale * (distance / initialDistance )));
234+ updateTransform ();
235+ } else if (e .touches .length === 1 && isDragging ) {
236+ const touch = e .touches [0 ];
237+ originX = touch .clientX - startX ;
238+ originY = touch .clientY - startY ;
239+ updateTransform ();
240+ }
241+ e .preventDefault ();
242+ });
243+
244+ svg .addEventListener (" touchend" , () => {
245+ isDragging = false ;
246+ });
247+ }
248+ });
50249 } catch (e ) {
51250 console .error (" Mermaid rendering error:" , e );
52251 }
@@ -58,3 +257,140 @@ function renderMermaidDiagrams() {
58257<template >
59258 <div v-html =" renderedHtml" ></div >
60259</template >
260+
261+ <style scoped>
262+ :deep(.mermaid-zoom-container ) {
263+ position : relative ;
264+ border : 1px solid #e5e7eb ;
265+ border-radius : 8px ;
266+ background : #fff ;
267+ margin : 1rem 0 ;
268+ overflow : hidden ;
269+ }
270+
271+ :deep(.mermaid-zoom-container.mermaid-fullscreen ) {
272+ position : fixed ;
273+ top : 0 ;
274+ left : 0 ;
275+ width : 100vw ;
276+ height : 100vh ;
277+ z-index : 1000 ;
278+ border-radius : 0 ;
279+ margin : 0 ;
280+ }
281+
282+ :deep(.mermaid-zoom-controls ) {
283+ position : absolute ;
284+ top : 8px ;
285+ right : 8px ;
286+ display : flex ;
287+ gap : 4px ;
288+ z-index : 10 ;
289+ background : rgba (255 , 255 , 255 , 0.9 );
290+ backdrop-filter : blur (4px );
291+ border-radius : 6px ;
292+ padding : 4px ;
293+ box-shadow : 0 2px 8px rgba (0 , 0 , 0 , 0.1 );
294+ }
295+
296+ :deep(.mermaid-zoom-btn ) {
297+ width : 32px ;
298+ height : 32px ;
299+ border : 1px solid #d1d5db ;
300+ background : #f9fafb ;
301+ color : #374151 ;
302+ border-radius : 4px ;
303+ display : flex ;
304+ align-items : center ;
305+ justify-content : center ;
306+ cursor : pointer ;
307+ font-size : 16px ;
308+ font-weight : bold ;
309+ transition : all 0.2s ease ;
310+ user-select : none ;
311+ }
312+
313+ :deep(.mermaid-zoom-btn :hover ) {
314+ background : #e5e7eb ;
315+ border-color : #9ca3af ;
316+ }
317+
318+ :deep(.mermaid-zoom-btn :active ) {
319+ transform : scale (0.95 );
320+ }
321+
322+ :deep(.mermaid-zoom-content ) {
323+ width : 100% ;
324+ height : 400px ;
325+ overflow : hidden ;
326+ position : relative ;
327+ display : flex ;
328+ align-items : center ;
329+ justify-content : center ;
330+ }
331+
332+ :deep(.mermaid-fullscreen .mermaid-zoom-content ) {
333+ height : 100vh ;
334+ }
335+
336+ :deep(.mermaid-zoom-content svg ) {
337+ transform-origin : center center ;
338+ max-width : none ;
339+ max-height : none ;
340+ user-select : none ;
341+ }
342+
343+ /* Dark mode support */
344+ :deep(.dark .mermaid-zoom-container ) {
345+ border-color : #374151 ;
346+ background : #1f2937 ;
347+ }
348+
349+ :deep(.dark .mermaid-zoom-controls ) {
350+ background : rgba (31 , 41 , 55 , 0.9 );
351+ }
352+
353+ :deep(.dark .mermaid-zoom-btn ) {
354+ border-color : #4b5563 ;
355+ background : #374151 ;
356+ color : #d1d5db ;
357+ }
358+
359+ :deep(.dark .mermaid-zoom-btn :hover ) {
360+ background : #4b5563 ;
361+ border-color : #6b7280 ;
362+ }
363+
364+ /* Instructions overlay for first time users */
365+ :deep(.mermaid-zoom-container ::after ) {
366+ content : " 💡 Use mouse wheel to zoom, drag to pan, or use controls in top-right" ;
367+ position : absolute ;
368+ bottom : 8px ;
369+ left : 8px ;
370+ font-size : 12px ;
371+ color : #6b7280 ;
372+ background : rgba (255 , 255 , 255 , 0.9 );
373+ backdrop-filter : blur (4px );
374+ padding : 4px 8px ;
375+ border-radius : 4px ;
376+ opacity : 0 ;
377+ animation : fade-in-out 4s ease-in-out ;
378+ pointer-events : none ;
379+ }
380+
381+ :deep(.dark .mermaid-zoom-container ::after ) {
382+ background : rgba (31 , 41 , 55 , 0.9 );
383+ color : #d1d5db ;
384+ }
385+
386+ @keyframes fade-in-out {
387+ 0% ,
388+ 100% {
389+ opacity : 0 ;
390+ }
391+ 20% ,
392+ 80% {
393+ opacity : 1 ;
394+ }
395+ }
396+ </style >
0 commit comments