@eventcatalog/core 3.4.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,6 +33,9 @@ const diagramId = props.data.id;
33
33
  --ec-page-text-muted: 100 116 139;
34
34
  --ec-page-border: 226 232 240;
35
35
  --ec-content-hover: 241 245 249;
36
+ --ec-card-bg: 255 255 255;
37
+ --ec-icon-color: 100 116 139;
38
+ --ec-icon-hover: 15 23 42;
36
39
  }
37
40
 
38
41
  :root[data-theme='dark'] {
@@ -41,6 +44,9 @@ const diagramId = props.data.id;
41
44
  --ec-page-text-muted: 139 148 158;
42
45
  --ec-page-border: 33 38 45;
43
46
  --ec-content-hover: 33 38 45;
47
+ --ec-card-bg: 22 27 34;
48
+ --ec-icon-color: 139 148 158;
49
+ --ec-icon-hover: 240 246 252;
44
50
  }
45
51
 
46
52
  body {
@@ -66,6 +72,16 @@ const diagramId = props.data.id;
66
72
  display: block;
67
73
  }
68
74
 
75
+ /* Zoom controls styling - must stay as CSS since svg-pan-zoom injects them */
76
+ .svg-pan-zoom-control {
77
+ fill: rgb(var(--ec-page-text-muted)) !important;
78
+ cursor: pointer;
79
+ }
80
+
81
+ .svg-pan-zoom-control:hover {
82
+ fill: rgb(var(--ec-page-text)) !important;
83
+ }
84
+
69
85
  /* Prose styles */
70
86
  .prose {
71
87
  max-width: none;
@@ -172,6 +188,17 @@ const diagramId = props.data.id;
172
188
  </div>
173
189
 
174
190
  <script is:inline define:vars={{ config, baseUrl: import.meta.env.BASE_URL }}>
191
+ /**
192
+ * IMPORTANT: Diagram zoom functionality is intentionally duplicated here from mermaid-zoom.ts.
193
+ *
194
+ * This embed page is designed to be loaded in iframes and must be fully self-contained.
195
+ * It uses CDN imports (e.g., https://cdn.jsdelivr.net/npm/mermaid@10) instead of npm
196
+ * package imports to ensure isolation from the parent application's build process.
197
+ *
198
+ * If you need to update the zoom controls or diagram rendering logic, please update
199
+ * both this file AND eventcatalog/src/utils/mermaid-zoom.ts to keep them in sync.
200
+ */
201
+
175
202
  window.eventcatalog = { mermaid: config?.mermaid };
176
203
 
177
204
  // Set the go-to-diagram link by parsing URL: /diagrams/{id}/{version}/embed
@@ -185,6 +212,356 @@ const diagramId = props.data.id;
185
212
  goToBtn.href = `${base}/diagrams/${diagramId}/${version}`;
186
213
  }
187
214
 
215
+ // Store zoom instances for cleanup
216
+ const zoomInstances = new Map();
217
+
218
+ function createZoomControls(onZoomIn, onZoomOut, onFitView) {
219
+ // Detect dark mode
220
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
221
+
222
+ // Use explicit colors based on theme
223
+ const bgColor = isDark ? '#161b22' : '#ffffff';
224
+ const borderColor = isDark ? '#30363d' : '#e2e8f0';
225
+ const iconColor = isDark ? '#8b949e' : '#64748b';
226
+ const iconHoverColor = isDark ? '#f0f6fc' : '#0f172a';
227
+ const hoverBgColor = isDark ? '#21262d' : '#f1f5f9';
228
+
229
+ const controls = document.createElement('div');
230
+ controls.className = 'mermaid-zoom-controls';
231
+ controls.style.cssText = `
232
+ position: absolute;
233
+ bottom: 12px;
234
+ left: 12px;
235
+ display: flex;
236
+ flex-direction: column;
237
+ background: ${bgColor};
238
+ border-radius: 6px;
239
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
240
+ border: 1px solid ${borderColor};
241
+ overflow: hidden;
242
+ z-index: 10;
243
+ `;
244
+
245
+ const createButton = (svg, title, onClick, isLast = false) => {
246
+ const btn = document.createElement('button');
247
+ btn.type = 'button';
248
+ btn.title = title;
249
+ btn.innerHTML = svg;
250
+ btn.onclick = onClick;
251
+ btn.style.cssText = `
252
+ display: flex;
253
+ justify-content: center;
254
+ align-items: center;
255
+ width: 26px;
256
+ height: 26px;
257
+ padding: 0;
258
+ margin: 0;
259
+ border: none;
260
+ background: ${bgColor};
261
+ color: ${iconColor};
262
+ cursor: pointer;
263
+ transition: background-color 0.15s, color 0.15s;
264
+ line-height: 1;
265
+ ${!isLast ? `border-bottom: 1px solid ${borderColor};` : ''}
266
+ `;
267
+ // Ensure SVG is properly centered
268
+ const svgEl = btn.querySelector('svg');
269
+ if (svgEl) {
270
+ svgEl.style.display = 'block';
271
+ }
272
+ btn.onmouseenter = () => {
273
+ btn.style.backgroundColor = hoverBgColor;
274
+ btn.style.color = iconHoverColor;
275
+ };
276
+ btn.onmouseleave = () => {
277
+ btn.style.backgroundColor = bgColor;
278
+ btn.style.color = iconColor;
279
+ };
280
+ return btn;
281
+ };
282
+
283
+ const plusSvg = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`;
284
+ const minusSvg = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`;
285
+ const fitSvg = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>`;
286
+
287
+ controls.appendChild(createButton(plusSvg, 'Zoom in', onZoomIn));
288
+ controls.appendChild(createButton(minusSvg, 'Zoom out', onZoomOut));
289
+ controls.appendChild(createButton(fitSvg, 'Fit view', onFitView, true));
290
+
291
+ return controls;
292
+ }
293
+
294
+ // Icons for toolbar
295
+ const ICONS = {
296
+ presentation: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5m.75-9l3-3 2.148 2.148A12.061 12.061 0 0116.5 7.605"></path></svg>`,
297
+ copy: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z"></path></svg>`,
298
+ check: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
299
+ };
300
+
301
+ function createToolbarButton(icon, tooltipText, onClick, tooltipPosition = 'bottom') {
302
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
303
+ const bgColor = isDark ? '#161b22' : '#ffffff';
304
+ const iconColor = isDark ? '#8b949e' : '#64748b';
305
+ const iconHoverColor = isDark ? '#f0f6fc' : '#0f172a';
306
+ const hoverBgColor = isDark ? '#21262d' : '#f1f5f9';
307
+
308
+ const wrapper = document.createElement('div');
309
+ wrapper.style.cssText = 'position: relative;';
310
+
311
+ const btn = document.createElement('button');
312
+ btn.type = 'button';
313
+ btn.innerHTML = icon;
314
+ btn.style.cssText = `
315
+ all: unset;
316
+ box-sizing: border-box;
317
+ display: flex;
318
+ justify-content: center;
319
+ align-items: center;
320
+ width: 40px;
321
+ height: 40px;
322
+ min-width: 40px;
323
+ min-height: 40px;
324
+ padding: 0;
325
+ margin: 0;
326
+ border: none;
327
+ border-radius: 6px;
328
+ background: ${bgColor};
329
+ color: ${iconColor};
330
+ cursor: pointer;
331
+ transition: background-color 0.15s, color 0.15s;
332
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
333
+ `;
334
+
335
+ const svgEl = btn.querySelector('svg');
336
+ if (svgEl) svgEl.style.cssText = 'display: block; width: 20px; height: 20px;';
337
+
338
+ btn.onmouseenter = () => {
339
+ btn.style.backgroundColor = hoverBgColor;
340
+ btn.style.color = iconHoverColor;
341
+ };
342
+ btn.onmouseleave = () => {
343
+ btn.style.backgroundColor = bgColor;
344
+ btn.style.color = iconColor;
345
+ };
346
+ btn.onclick = onClick;
347
+
348
+ const tooltip = document.createElement('div');
349
+ tooltip.textContent = tooltipText;
350
+
351
+ // Position tooltip based on tooltipPosition parameter
352
+ let tooltipStyles = `
353
+ position: absolute;
354
+ padding: 4px 8px;
355
+ background: #1f2937;
356
+ color: white;
357
+ font-size: 12px;
358
+ border-radius: 4px;
359
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
360
+ white-space: nowrap;
361
+ pointer-events: none;
362
+ opacity: 0;
363
+ transition: opacity 0.15s;
364
+ z-index: 50;
365
+ `;
366
+
367
+ if (tooltipPosition === 'right') {
368
+ tooltipStyles += `
369
+ top: 50%;
370
+ left: 100%;
371
+ transform: translateY(-50%);
372
+ margin-left: 8px;
373
+ `;
374
+ } else if (tooltipPosition === 'left') {
375
+ tooltipStyles += `
376
+ top: 50%;
377
+ right: 100%;
378
+ transform: translateY(-50%);
379
+ margin-right: 8px;
380
+ `;
381
+ } else {
382
+ // Default: bottom
383
+ tooltipStyles += `
384
+ top: 100%;
385
+ left: 50%;
386
+ transform: translateX(-50%);
387
+ margin-top: 8px;
388
+ `;
389
+ }
390
+
391
+ tooltip.style.cssText = tooltipStyles;
392
+
393
+ wrapper.onmouseenter = () => (tooltip.style.opacity = '1');
394
+ wrapper.onmouseleave = () => (tooltip.style.opacity = '0');
395
+
396
+ wrapper.appendChild(btn);
397
+ wrapper.appendChild(tooltip);
398
+ return { wrapper, btn, tooltip, iconColor };
399
+ }
400
+
401
+ function createFullscreenButton(onClick) {
402
+ const { wrapper } = createToolbarButton(ICONS.presentation, 'Presentation Mode', onClick, 'right');
403
+ wrapper.style.cssText = 'position: absolute; top: 12px; left: 12px; z-index: 10;';
404
+ return wrapper;
405
+ }
406
+
407
+ function createCopyButton(onCopy) {
408
+ const copy = createToolbarButton(
409
+ ICONS.copy,
410
+ 'Copy diagram code',
411
+ () => {
412
+ onCopy();
413
+ copy.btn.innerHTML = ICONS.check;
414
+ copy.btn.style.color = '#10b981';
415
+ copy.tooltip.textContent = 'Copied!';
416
+ const svgEl = copy.btn.querySelector('svg');
417
+ if (svgEl) svgEl.style.cssText = 'display: block; width: 20px; height: 20px;';
418
+
419
+ setTimeout(() => {
420
+ copy.btn.innerHTML = ICONS.copy;
421
+ copy.btn.style.color = copy.iconColor;
422
+ copy.tooltip.textContent = 'Copy diagram code';
423
+ const svgEl = copy.btn.querySelector('svg');
424
+ if (svgEl) svgEl.style.cssText = 'display: block; width: 20px; height: 20px;';
425
+ }, 2000);
426
+ },
427
+ 'left'
428
+ );
429
+ copy.wrapper.style.cssText = 'position: absolute; top: 12px; right: 12px; z-index: 10;';
430
+ return copy.wrapper;
431
+ }
432
+
433
+ function toggleFullscreen(container) {
434
+ if (!document.fullscreenElement) {
435
+ container.requestFullscreen().catch((err) => console.warn('Fullscreen error:', err));
436
+ } else {
437
+ document.exitFullscreen();
438
+ }
439
+ }
440
+
441
+ async function initMermaidZoom(svgElement, container, id, diagramContent) {
442
+ const { default: svgPanZoom } = await import('https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.2/+esm');
443
+
444
+ // Get the natural dimensions from viewBox or getBBox
445
+ let width, height;
446
+ const viewBox = svgElement.getAttribute('viewBox');
447
+ if (viewBox) {
448
+ const parts = viewBox.split(/[\s,]+/).map(Number);
449
+ width = parts[2];
450
+ height = parts[3];
451
+ } else {
452
+ const bbox = svgElement.getBBox();
453
+ width = bbox.width;
454
+ height = bbox.height;
455
+ if (width > 0 && height > 0) {
456
+ svgElement.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${width} ${height}`);
457
+ }
458
+ }
459
+
460
+ // Set container height based on SVG aspect ratio, capped for usability
461
+ if (width > 0 && height > 0) {
462
+ const containerWidth = container.clientWidth || 800;
463
+ const aspectRatio = height / width;
464
+ const calculatedHeight = Math.min(Math.max(containerWidth * aspectRatio, 200), 500);
465
+ container.style.height = `${calculatedHeight}px`;
466
+ }
467
+
468
+ // SVG needs to fill the container for svg-pan-zoom
469
+ svgElement.style.width = '100%';
470
+ svgElement.style.height = '100%';
471
+ svgElement.removeAttribute('height');
472
+ svgElement.removeAttribute('width');
473
+
474
+ try {
475
+ const instance = svgPanZoom(svgElement, {
476
+ zoomEnabled: true,
477
+ controlIconsEnabled: false,
478
+ fit: true,
479
+ center: true,
480
+ minZoom: 0.5,
481
+ maxZoom: 10,
482
+ zoomScaleSensitivity: 0.15,
483
+ dblClickZoomEnabled: true,
484
+ mouseWheelZoomEnabled: false, // Disabled to avoid hijacking page scroll
485
+ preventMouseEventsDefault: true,
486
+ panEnabled: true,
487
+ });
488
+
489
+ zoomInstances.set(id, instance);
490
+
491
+ // Update cursor during pan
492
+ container.addEventListener('mousedown', () => {
493
+ container.style.cursor = 'grabbing';
494
+ });
495
+ container.addEventListener('mouseup', () => {
496
+ container.style.cursor = 'grab';
497
+ });
498
+ container.addEventListener('mouseleave', () => {
499
+ container.style.cursor = 'grab';
500
+ });
501
+
502
+ // Add custom controls
503
+ const controls = createZoomControls(
504
+ () => instance.zoomIn(),
505
+ () => instance.zoomOut(),
506
+ () => {
507
+ instance.fit();
508
+ instance.center();
509
+ }
510
+ );
511
+ container.appendChild(controls);
512
+
513
+ // Add fullscreen button (top-left)
514
+ const fullscreenBtn = createFullscreenButton(() => toggleFullscreen(container));
515
+ container.appendChild(fullscreenBtn);
516
+
517
+ // Add copy button (top-right) if diagram content is available
518
+ if (diagramContent) {
519
+ const copyBtn = createCopyButton(() => {
520
+ navigator.clipboard.writeText(diagramContent).catch((err) => {
521
+ console.warn('Failed to copy:', err);
522
+ });
523
+ });
524
+ container.appendChild(copyBtn);
525
+ }
526
+
527
+ // Handle fullscreen changes
528
+ document.addEventListener('fullscreenchange', () => {
529
+ const isFullscreen = document.fullscreenElement === container;
530
+ if (isFullscreen) {
531
+ instance.enableMouseWheelZoom();
532
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
533
+ container.style.background = isDark ? '#0d1117' : '#ffffff';
534
+ } else {
535
+ instance.disableMouseWheelZoom();
536
+ container.style.background = '';
537
+ }
538
+ setTimeout(() => {
539
+ if (container.clientWidth > 0 && container.clientHeight > 0) {
540
+ try {
541
+ instance.resize();
542
+ instance.fit();
543
+ instance.center();
544
+ } catch (e) {}
545
+ }
546
+ }, 100);
547
+ });
548
+
549
+ // Resize handler for responsiveness
550
+ const resizeObserver = new ResizeObserver(() => {
551
+ if (container.clientWidth > 0 && container.clientHeight > 0) {
552
+ try {
553
+ instance.resize();
554
+ instance.fit();
555
+ instance.center();
556
+ } catch (e) {}
557
+ }
558
+ });
559
+ resizeObserver.observe(container);
560
+ } catch (e) {
561
+ console.warn('Failed to initialize zoom on mermaid diagram:', e);
562
+ }
563
+ }
564
+
188
565
  async function renderMermaid() {
189
566
  const graphs = document.getElementsByClassName('mermaid');
190
567
  if (graphs.length === 0) return;
@@ -204,7 +581,24 @@ const diagramId = props.data.id;
204
581
  const id = 'mermaid-' + Math.round(Math.random() * 100000);
205
582
  try {
206
583
  const result = await mermaid.render(id, content);
207
- graph.innerHTML = result.svg;
584
+
585
+ // Create zoom container with inline styles (no Tailwind in embed)
586
+ const container = document.createElement('div');
587
+ container.style.cssText =
588
+ 'position: relative; width: 100%; min-height: 200px; overflow: hidden; margin: 1em 0; cursor: grab;';
589
+
590
+ // Insert the rendered SVG
591
+ container.innerHTML = result.svg;
592
+
593
+ // Replace the graph content with the container
594
+ graph.innerHTML = '';
595
+ graph.appendChild(container);
596
+
597
+ // Initialize zoom on the SVG
598
+ const svgElement = container.querySelector('svg');
599
+ if (svgElement) {
600
+ await initMermaidZoom(svgElement, container, id, content);
601
+ }
208
602
  } catch (e) {
209
603
  console.error('Mermaid render error:', e);
210
604
  }
@@ -213,12 +607,13 @@ const diagramId = props.data.id;
213
607
 
214
608
  renderMermaid();
215
609
 
216
- // PlantUML rendering
610
+ // PlantUML rendering with zoom
217
611
  async function renderPlantUML() {
218
612
  const blocks = document.getElementsByClassName('plantuml');
219
613
  if (blocks.length === 0) return;
220
614
 
221
615
  const { deflate } = await import('https://cdn.jsdelivr.net/npm/pako@2.1.0/+esm');
616
+ const { default: svgPanZoom } = await import('https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.2/+esm');
222
617
 
223
618
  function encode64(data) {
224
619
  const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_';
@@ -250,14 +645,42 @@ const diagramId = props.data.id;
250
645
  if (!content) continue;
251
646
 
252
647
  const encoded = encodePlantUML(content);
253
- const img = document.createElement('img');
254
- img.src = `https://www.plantuml.com/plantuml/svg/~1${encoded}`;
255
- img.alt = 'PlantUML diagram';
256
- img.loading = 'lazy';
257
- img.style.margin = '0 auto';
258
- img.style.display = 'block';
259
- block.innerHTML = '';
260
- block.appendChild(img);
648
+ const svgUrl = `https://www.plantuml.com/plantuml/svg/~1${encoded}`;
649
+ const id = 'plantuml-' + Math.round(Math.random() * 100000);
650
+
651
+ try {
652
+ // Fetch SVG content for zoom support
653
+ const response = await fetch(svgUrl);
654
+ if (!response.ok) throw new Error('Fetch failed');
655
+
656
+ const svgText = await response.text();
657
+
658
+ // Create zoom container
659
+ const container = document.createElement('div');
660
+ container.style.cssText =
661
+ 'position: relative; width: 100%; min-height: 200px; overflow: hidden; margin: 1em 0; cursor: grab;';
662
+ container.innerHTML = svgText;
663
+
664
+ block.innerHTML = '';
665
+ block.appendChild(container);
666
+
667
+ // Initialize zoom
668
+ const svgElement = container.querySelector('svg');
669
+ if (svgElement) {
670
+ await initMermaidZoom(svgElement, container, id, content);
671
+ }
672
+ } catch (e) {
673
+ // Fallback to img
674
+ console.warn('PlantUML fetch failed:', e);
675
+ const img = document.createElement('img');
676
+ img.src = svgUrl;
677
+ img.alt = 'PlantUML diagram';
678
+ img.loading = 'lazy';
679
+ img.style.margin = '0 auto';
680
+ img.style.display = 'block';
681
+ block.innerHTML = '';
682
+ block.appendChild(img);
683
+ }
261
684
  }
262
685
  }
263
686
 
@@ -210,6 +210,8 @@ const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${pro
210
210
  </script>
211
211
 
212
212
  <script>
213
+ import { destroyZoomInstances, renderMermaidWithZoom } from '@utils/mermaid-zoom';
214
+
213
215
  function initDiagramPage() {
214
216
  // Version selector
215
217
  const versionSelect = document.getElementById('version-select') as HTMLSelectElement;
@@ -231,7 +233,6 @@ const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${pro
231
233
  const rightFrame = document.getElementById('compare-right-frame') as HTMLIFrameElement;
232
234
 
233
235
  function buildCompareUrl(version: string) {
234
- // Parse diagramId from current URL: /diagrams/{id}/{version}
235
236
  const pathParts = window.location.pathname.split('/');
236
237
  const diagramsIndex = pathParts.indexOf('diagrams');
237
238
  const diagramId = diagramsIndex !== -1 ? pathParts[diagramsIndex + 1] : '';
@@ -244,12 +245,8 @@ const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${pro
244
245
  if (!compareModal) return;
245
246
  compareModal.classList.remove('hidden');
246
247
  document.body.style.overflow = 'hidden';
247
- if (leftFrame && leftVersionSelect) {
248
- leftFrame.src = buildCompareUrl(leftVersionSelect.value);
249
- }
250
- if (rightFrame && rightVersionSelect) {
251
- rightFrame.src = buildCompareUrl(rightVersionSelect.value);
252
- }
248
+ if (leftFrame && leftVersionSelect) leftFrame.src = buildCompareUrl(leftVersionSelect.value);
249
+ if (rightFrame && rightVersionSelect) rightFrame.src = buildCompareUrl(rightVersionSelect.value);
253
250
  }
254
251
 
255
252
  function closeCompareModal() {
@@ -272,27 +269,19 @@ const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${pro
272
269
  document.body.style.overflow = '';
273
270
  }
274
271
 
275
- // Handle compare button click - check if Scale is enabled
276
272
  if (compareBtn) {
277
273
  compareBtn.onclick = () => {
278
274
  const scaleEnabled = compareBtn.dataset.scaleEnabled === 'true';
279
- if (scaleEnabled) {
280
- openCompareModal();
281
- } else {
282
- openUpgradeModal();
283
- }
275
+ scaleEnabled ? openCompareModal() : openUpgradeModal();
284
276
  };
285
277
  }
286
278
 
287
279
  if (compareCloseBtn) compareCloseBtn.onclick = closeCompareModal;
288
280
  if (upgradeCloseBtn) upgradeCloseBtn.onclick = closeUpgradeModal;
289
281
 
290
- // Close upgrade modal when clicking outside the card
291
282
  if (upgradeModal) {
292
283
  upgradeModal.onclick = (e) => {
293
- if (e.target === upgradeModal) {
294
- closeUpgradeModal();
295
- }
284
+ if (e.target === upgradeModal) closeUpgradeModal();
296
285
  };
297
286
  }
298
287
 
@@ -309,42 +298,16 @@ const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${pro
309
298
 
310
299
  document.onkeydown = (e) => {
311
300
  if (e.key === 'Escape') {
312
- if (compareModal && !compareModal.classList.contains('hidden')) {
313
- closeCompareModal();
314
- }
315
- if (upgradeModal && !upgradeModal.classList.contains('hidden')) {
316
- closeUpgradeModal();
317
- }
301
+ if (compareModal && !compareModal.classList.contains('hidden')) closeCompareModal();
302
+ if (upgradeModal && !upgradeModal.classList.contains('hidden')) closeUpgradeModal();
318
303
  }
319
304
  };
320
305
 
321
- // Mermaid
322
- renderMermaidDiagrams();
323
- }
324
-
325
- async function renderMermaidDiagrams() {
306
+ // Render Mermaid diagrams with zoom
307
+ destroyZoomInstances();
326
308
  const graphs = document.getElementsByClassName('mermaid');
327
- if (graphs.length === 0) return;
328
-
329
- const { default: mermaid } = await import('mermaid');
330
- const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
331
-
332
- mermaid.initialize({
333
- startOnLoad: false,
334
- theme: isDarkMode ? 'dark' : 'default',
335
- fontFamily: 'var(--sans-font)',
336
- });
337
-
338
- for (const graph of graphs) {
339
- const content = graph.getAttribute('data-content');
340
- if (!content) continue;
341
- const id = 'mermaid-' + Math.round(Math.random() * 100000);
342
- try {
343
- const result = await mermaid.render(id, content);
344
- graph.innerHTML = result.svg;
345
- } catch (e) {
346
- console.error('Mermaid error:', e);
347
- }
309
+ if (graphs.length > 0) {
310
+ renderMermaidWithZoom(graphs, window.eventcatalog?.mermaid);
348
311
  }
349
312
  }
350
313
 
@@ -354,58 +317,16 @@ const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${pro
354
317
  </script>
355
318
 
356
319
  <script>
357
- import('pako').then(({ deflate }: any) => {
358
- document.addEventListener('astro:page-load', () => {
359
- const blocks = document.getElementsByClassName('plantuml');
360
- if (blocks.length > 0) {
361
- renderPlantUML(blocks, deflate);
362
- }
363
- });
364
-
365
- function renderPlantUML(blocks: any, deflate: any) {
366
- for (const block of blocks) {
367
- const content = block.getAttribute('data-content');
368
- if (!content) continue;
320
+ import { renderPlantUMLWithZoom } from '@utils/mermaid-zoom';
369
321
 
370
- const encoded = encodePlantUML(content, deflate);
371
- const img = document.createElement('img');
372
- img.src = `https://www.plantuml.com/plantuml/svg/~1${encoded}`;
373
- img.alt = 'PlantUML diagram';
374
- img.loading = 'lazy';
375
- block.innerHTML = '';
376
- img.classList.add('mx-auto');
377
- block.appendChild(img);
378
- }
379
- }
380
-
381
- function encodePlantUML(text: any, deflate: any) {
382
- const data = new TextEncoder().encode(text);
383
- const compressed = deflate(data, { level: 9, to: 'Uint8Array' });
384
- return encode64(compressed);
385
- }
386
-
387
- function encode64(data: any) {
388
- const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_';
389
- let str = '';
390
- const len = data.length;
391
- for (let i = 0; i < len; i += 3) {
392
- const b1 = data[i];
393
- const b2 = i + 1 < len ? data[i + 1] : 0;
394
- const b3 = i + 2 < len ? data[i + 2] : 0;
395
-
396
- let c1 = b1 >> 2;
397
- let c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
398
- let c3 = ((b2 & 0xf) << 2) | (b3 >> 6);
399
- let c4 = b3 & 0x3f;
400
-
401
- str += chars[c1] + chars[c2] + chars[c3] + chars[c4];
402
- }
403
- return str;
322
+ function initPlantUML() {
323
+ const blocks = document.getElementsByClassName('plantuml');
324
+ if (blocks.length > 0) {
325
+ renderPlantUMLWithZoom(blocks);
404
326
  }
327
+ }
405
328
 
406
- const graphs = document.getElementsByClassName('plantuml');
407
- if (graphs.length > 0) {
408
- renderPlantUML(graphs, deflate);
409
- }
410
- });
329
+ // Run on initial load and page transitions
330
+ initPlantUML();
331
+ document.addEventListener('astro:page-load', initPlantUML);
411
332
  </script>