@eventcatalog/core 4.10.4 → 4.10.6
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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/count-resources.cjs +32 -0
- package/dist/analytics/count-resources.d.cts +9 -1
- package/dist/analytics/count-resources.d.ts +9 -1
- package/dist/analytics/count-resources.js +3 -1
- package/dist/analytics/log-build.cjs +30 -3
- package/dist/analytics/log-build.d.cts +2 -1
- package/dist/analytics/log-build.d.ts +2 -1
- package/dist/analytics/log-build.js +4 -4
- package/dist/{chunk-2SLBVVUZ.js → chunk-EB43LSVE.js} +12 -4
- package/dist/{chunk-T5XTOY52.js → chunk-GMVJ4B2S.js} +1 -1
- package/dist/{chunk-6S2ZIVEV.js → chunk-NECJFWMI.js} +1 -1
- package/dist/{chunk-S3J4N5ZP.js → chunk-XFS3XVP2.js} +1 -1
- package/dist/{chunk-322TAIDG.js → chunk-YSWJHMAX.js} +1 -1
- package/dist/{chunk-K2XIENVT.js → chunk-ZAZHAVKB.js} +21 -0
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +277 -243
- package/dist/eventcatalog.js +14 -7
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/MermaidFileLoader/MermaidFileLoader.astro +10 -3
- package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +665 -325
- package/eventcatalog/src/remark-plugins/mermaid.ts +48 -1
- package/eventcatalog/src/utils/mermaid-zoom.ts +781 -404
- package/package.json +6 -7
|
@@ -73,16 +73,6 @@ const diagramId = props.data.id;
|
|
|
73
73
|
display: block;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/* Zoom controls styling - must stay as CSS since svg-pan-zoom injects them */
|
|
77
|
-
.svg-pan-zoom-control {
|
|
78
|
-
fill: rgb(var(--ec-page-text-muted)) !important;
|
|
79
|
-
cursor: pointer;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.svg-pan-zoom-control:hover {
|
|
83
|
-
fill: rgb(var(--ec-page-text)) !important;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
76
|
/* Prose styles */
|
|
87
77
|
.prose {
|
|
88
78
|
max-width: none;
|
|
@@ -216,351 +206,702 @@ const diagramId = props.data.id;
|
|
|
216
206
|
// Store zoom instances for cleanup
|
|
217
207
|
const zoomInstances = new Map();
|
|
218
208
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Diagram pan/zoom, controls and fullscreen modal
|
|
211
|
+
// (kept in sync with eventcatalog/src/utils/mermaid-zoom.ts)
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
const CONTROLS_PLACEMENTS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
214
|
+
const DEFAULT_PLACEMENT = 'top-right';
|
|
215
|
+
const CONTROLS_MIN_HEIGHT = 120;
|
|
216
|
+
const PAN_STEP = 60;
|
|
217
|
+
const ZOOM_STEP = 1.2;
|
|
218
|
+
const TRANSITION_MS = 150;
|
|
219
|
+
const FIT_PADDING = 16;
|
|
220
|
+
const STYLE_ID = 'ec-diagram-styles';
|
|
221
|
+
let closeOpenModal = null;
|
|
222
|
+
|
|
223
|
+
const DIAGRAM_STYLES = `
|
|
224
|
+
.mermaid-zoom-container {
|
|
225
|
+
position: relative;
|
|
226
|
+
width: 100%;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
}
|
|
229
|
+
.ec-diagram-viewport {
|
|
230
|
+
position: absolute;
|
|
231
|
+
inset: 0;
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
cursor: grab;
|
|
234
|
+
touch-action: pan-y pinch-zoom;
|
|
235
|
+
user-select: none;
|
|
236
|
+
-webkit-user-select: none;
|
|
237
|
+
outline: none;
|
|
238
|
+
}
|
|
239
|
+
.ec-diagram-viewport.is-panning {
|
|
240
|
+
cursor: grabbing;
|
|
241
|
+
}
|
|
242
|
+
.ec-diagram-viewport--modal {
|
|
243
|
+
touch-action: none;
|
|
244
|
+
background-image: radial-gradient(rgb(var(--ec-page-text, 15 23 42) / 0.08) 1px, transparent 1px);
|
|
245
|
+
background-size: 16px 16px;
|
|
246
|
+
}
|
|
247
|
+
.ec-diagram-content {
|
|
248
|
+
position: absolute;
|
|
249
|
+
left: 0;
|
|
250
|
+
top: 0;
|
|
251
|
+
transform-origin: 0 0;
|
|
252
|
+
transition: transform ${TRANSITION_MS}ms ease-out;
|
|
253
|
+
will-change: transform;
|
|
254
|
+
}
|
|
255
|
+
/* Element selectors bump specificity above the page's global ".mermaid svg" rules */
|
|
256
|
+
div.ec-diagram-content > svg {
|
|
257
|
+
display: block;
|
|
258
|
+
margin: 0;
|
|
259
|
+
max-width: none !important;
|
|
260
|
+
}
|
|
261
|
+
.ec-diagram-btn {
|
|
262
|
+
all: unset;
|
|
263
|
+
box-sizing: border-box;
|
|
264
|
+
display: inline-flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
justify-content: center;
|
|
267
|
+
width: 28px;
|
|
268
|
+
height: 28px;
|
|
269
|
+
border-radius: 6px;
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
background: rgb(var(--ec-card-bg, 255 255 255));
|
|
272
|
+
color: rgb(var(--ec-page-text, 15 23 42));
|
|
273
|
+
border: 1px solid rgb(var(--ec-page-border, 226 232 240));
|
|
274
|
+
transition: background-color 0.15s, transform 0.1s;
|
|
275
|
+
}
|
|
276
|
+
.ec-diagram-btn:hover {
|
|
277
|
+
background: rgb(var(--ec-content-hover, 241 245 249));
|
|
278
|
+
}
|
|
279
|
+
.ec-diagram-btn:active {
|
|
280
|
+
transform: scale(0.95);
|
|
281
|
+
}
|
|
282
|
+
.ec-diagram-btn:focus-visible {
|
|
283
|
+
outline: 2px solid rgb(var(--ec-accent, 59 130 246));
|
|
284
|
+
outline-offset: 1px;
|
|
285
|
+
}
|
|
286
|
+
button.ec-diagram-btn svg {
|
|
287
|
+
display: block;
|
|
288
|
+
width: 16px;
|
|
289
|
+
height: 16px;
|
|
290
|
+
margin: 0;
|
|
291
|
+
flex-shrink: 0;
|
|
292
|
+
}
|
|
293
|
+
.ec-diagram-btn.ec-diagram-btn--success {
|
|
294
|
+
color: #10b981;
|
|
295
|
+
}
|
|
296
|
+
.ec-diagram-controls {
|
|
297
|
+
position: absolute;
|
|
298
|
+
z-index: 10;
|
|
299
|
+
display: grid;
|
|
300
|
+
grid-template-columns: repeat(3, 28px);
|
|
301
|
+
gap: 4px;
|
|
302
|
+
opacity: 0;
|
|
303
|
+
pointer-events: none;
|
|
304
|
+
transition: opacity 0.15s ease;
|
|
305
|
+
}
|
|
306
|
+
.ec-diagram-controls[data-placement="top-left"] { top: 8px; left: 8px; }
|
|
307
|
+
.ec-diagram-controls[data-placement="top-right"] { top: 8px; right: 8px; }
|
|
308
|
+
.ec-diagram-controls[data-placement="bottom-left"] { bottom: 8px; left: 8px; }
|
|
309
|
+
.ec-diagram-controls[data-placement="bottom-right"] { bottom: 8px; right: 8px; }
|
|
310
|
+
.mermaid-zoom-container:hover .ec-diagram-controls,
|
|
311
|
+
.mermaid-zoom-container:focus-within .ec-diagram-controls {
|
|
312
|
+
opacity: 1;
|
|
313
|
+
pointer-events: auto;
|
|
314
|
+
}
|
|
315
|
+
@media (pointer: coarse) {
|
|
316
|
+
.ec-diagram-controls { opacity: 1; pointer-events: auto; }
|
|
317
|
+
}
|
|
318
|
+
@media print {
|
|
319
|
+
.ec-diagram-controls { display: none; }
|
|
320
|
+
}
|
|
321
|
+
.ec-diagram-modal-backdrop {
|
|
322
|
+
position: fixed;
|
|
323
|
+
inset: 0;
|
|
324
|
+
z-index: 9998;
|
|
325
|
+
background: rgb(0 0 0 / 0.4);
|
|
326
|
+
opacity: 0;
|
|
327
|
+
transition: opacity 250ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
328
|
+
}
|
|
329
|
+
.ec-diagram-modal-backdrop.is-open {
|
|
330
|
+
opacity: 1;
|
|
331
|
+
}
|
|
332
|
+
.ec-diagram-modal {
|
|
333
|
+
position: fixed;
|
|
334
|
+
inset: 0;
|
|
335
|
+
z-index: 9999;
|
|
336
|
+
padding: 16px;
|
|
337
|
+
}
|
|
338
|
+
@media (min-width: 640px) {
|
|
339
|
+
.ec-diagram-modal { padding: 24px; }
|
|
340
|
+
}
|
|
341
|
+
.ec-diagram-modal__dialog {
|
|
342
|
+
position: relative;
|
|
343
|
+
width: 100%;
|
|
344
|
+
height: 100%;
|
|
345
|
+
overflow: hidden;
|
|
346
|
+
border-radius: 16px;
|
|
347
|
+
background: rgb(var(--ec-page-bg, 255 255 255));
|
|
348
|
+
box-shadow:
|
|
349
|
+
0 0 0 1px rgb(var(--ec-page-border, 226 232 240)),
|
|
350
|
+
0 25px 50px -12px rgb(0 0 0 / 0.25);
|
|
351
|
+
transform: scale(0.96);
|
|
352
|
+
opacity: 0;
|
|
353
|
+
transition:
|
|
354
|
+
transform 250ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
355
|
+
opacity 250ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
356
|
+
}
|
|
357
|
+
.ec-diagram-modal.is-open .ec-diagram-modal__dialog {
|
|
358
|
+
transform: none;
|
|
359
|
+
opacity: 1;
|
|
360
|
+
}
|
|
361
|
+
.ec-diagram-modal.is-closing .ec-diagram-modal__dialog,
|
|
362
|
+
.ec-diagram-modal-backdrop.is-closing {
|
|
363
|
+
transition-duration: 150ms;
|
|
364
|
+
}
|
|
365
|
+
.ec-diagram-modal__toolbar {
|
|
366
|
+
position: absolute;
|
|
367
|
+
left: 12px;
|
|
368
|
+
top: 12px;
|
|
369
|
+
z-index: 10;
|
|
370
|
+
display: flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
gap: 4px;
|
|
373
|
+
}
|
|
374
|
+
.ec-diagram-modal__zoom {
|
|
375
|
+
box-sizing: border-box;
|
|
376
|
+
display: inline-flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
justify-content: center;
|
|
379
|
+
height: 28px;
|
|
380
|
+
min-width: 56px;
|
|
381
|
+
padding: 0 6px;
|
|
382
|
+
border-radius: 6px;
|
|
383
|
+
border: 1px solid rgb(var(--ec-page-border, 226 232 240));
|
|
384
|
+
background: rgb(var(--ec-card-bg, 255 255 255));
|
|
385
|
+
color: rgb(var(--ec-page-text, 15 23 42));
|
|
386
|
+
font-family: inherit;
|
|
387
|
+
font-size: 12px;
|
|
388
|
+
line-height: 1;
|
|
389
|
+
font-variant-numeric: tabular-nums;
|
|
390
|
+
}
|
|
391
|
+
.ec-diagram-modal__close {
|
|
392
|
+
position: absolute;
|
|
393
|
+
right: 12px;
|
|
394
|
+
top: 12px;
|
|
395
|
+
z-index: 10;
|
|
396
|
+
}
|
|
397
|
+
@media (prefers-reduced-motion: reduce) {
|
|
398
|
+
.ec-diagram-content,
|
|
399
|
+
.ec-diagram-controls,
|
|
400
|
+
.ec-diagram-modal-backdrop,
|
|
401
|
+
.ec-diagram-modal__dialog {
|
|
402
|
+
transition: none;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
`;
|
|
406
|
+
|
|
407
|
+
function ensureDiagramStyles() {
|
|
408
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
409
|
+
const style = document.createElement('style');
|
|
410
|
+
style.id = STYLE_ID;
|
|
411
|
+
style.textContent = DIAGRAM_STYLES;
|
|
412
|
+
document.head.appendChild(style);
|
|
413
|
+
}
|
|
222
414
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const borderColor = isDark ? '#30363d' : '#e2e8f0';
|
|
226
|
-
const iconColor = isDark ? '#8b949e' : '#64748b';
|
|
227
|
-
const iconHoverColor = isDark ? '#f0f6fc' : '#0f172a';
|
|
228
|
-
const hoverBgColor = isDark ? '#21262d' : '#f1f5f9';
|
|
415
|
+
const icon = (paths, strokeWidth = 2) =>
|
|
416
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
229
417
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
418
|
+
const ICONS = {
|
|
419
|
+
fullscreen: icon('<path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/>'),
|
|
420
|
+
close: icon('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
|
|
421
|
+
panUp: icon('<path d="m18 15-6-6-6 6"/>', 2.5),
|
|
422
|
+
panDown: icon('<path d="m6 9 6 6 6-6"/>', 2.5),
|
|
423
|
+
panLeft: icon('<path d="m15 18-6-6 6-6"/>', 2.5),
|
|
424
|
+
panRight: icon('<path d="m9 18 6-6-6-6"/>', 2.5),
|
|
425
|
+
zoomIn: icon('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M11 8v6"/><path d="M8 11h6"/>'),
|
|
426
|
+
zoomOut: icon('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M8 11h6"/>'),
|
|
427
|
+
reset: icon('<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>', 2.5),
|
|
428
|
+
copy: icon(
|
|
429
|
+
'<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>'
|
|
430
|
+
),
|
|
431
|
+
check: icon('<path d="M20 6 9 17l-5-5"/>', 2.5),
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
function createControlButton(svg, label, onClick) {
|
|
435
|
+
const btn = document.createElement('button');
|
|
436
|
+
btn.type = 'button';
|
|
437
|
+
btn.className = 'ec-diagram-btn';
|
|
438
|
+
btn.title = label;
|
|
439
|
+
btn.setAttribute('aria-label', label);
|
|
440
|
+
btn.innerHTML = svg;
|
|
441
|
+
btn.onclick = onClick;
|
|
442
|
+
return btn;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function createCopyButton(diagramContent) {
|
|
446
|
+
let copyTimeout;
|
|
447
|
+
const button = createControlButton(ICONS.copy, 'Copy diagram code', () => {
|
|
448
|
+
navigator.clipboard.writeText(diagramContent).catch((err) => {
|
|
449
|
+
console.warn('Failed to copy diagram code:', err);
|
|
450
|
+
});
|
|
451
|
+
button.innerHTML = ICONS.check;
|
|
452
|
+
button.classList.add('ec-diagram-btn--success');
|
|
453
|
+
button.title = 'Copied!';
|
|
454
|
+
if (copyTimeout) clearTimeout(copyTimeout);
|
|
455
|
+
copyTimeout = setTimeout(() => {
|
|
456
|
+
button.innerHTML = ICONS.copy;
|
|
457
|
+
button.classList.remove('ec-diagram-btn--success');
|
|
458
|
+
button.title = 'Copy diagram code';
|
|
459
|
+
}, 2000);
|
|
460
|
+
});
|
|
461
|
+
return button;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function resolvePlacement(value) {
|
|
465
|
+
return CONTROLS_PLACEMENTS.includes(value) ? value : DEFAULT_PLACEMENT;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function resolveActions(value) {
|
|
469
|
+
if (value === 'true') return true;
|
|
470
|
+
if (value === 'false') return false;
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function getControlOptionsFromElement(element) {
|
|
475
|
+
return {
|
|
476
|
+
placement: resolvePlacement(element.getAttribute('data-placement')),
|
|
477
|
+
actions: resolveActions(element.getAttribute('data-actions')),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Pan/zoom engine: applies translate/scale to the content element. Button/keyboard changes
|
|
483
|
+
* are animated with a CSS transition; drag, wheel and pinch changes are instant.
|
|
484
|
+
*/
|
|
485
|
+
function createPanZoom(viewport, content, contentWidth, contentHeight, options = {}) {
|
|
486
|
+
const { minScale = 0.1, maxScale = 8, wheelZoom = false, onChange } = options;
|
|
487
|
+
|
|
488
|
+
let scale = 1;
|
|
489
|
+
let x = 0;
|
|
490
|
+
let y = 0;
|
|
491
|
+
|
|
492
|
+
const clamp = (value) => Math.min(maxScale, Math.max(minScale, value));
|
|
493
|
+
|
|
494
|
+
const apply = (animate) => {
|
|
495
|
+
content.style.transitionDuration = animate ? `${TRANSITION_MS}ms` : '0ms';
|
|
496
|
+
content.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
|
|
497
|
+
if (onChange) onChange(scale);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const fit = (animate = false) => {
|
|
501
|
+
const vw = viewport.clientWidth;
|
|
502
|
+
const vh = viewport.clientHeight;
|
|
503
|
+
if (vw <= 0 || vh <= 0) return;
|
|
504
|
+
scale = clamp(Math.min((vw - FIT_PADDING * 2) / contentWidth, (vh - FIT_PADDING * 2) / contentHeight, 1));
|
|
505
|
+
x = (vw - contentWidth * scale) / 2;
|
|
506
|
+
y = (vh - contentHeight * scale) / 2;
|
|
507
|
+
apply(animate);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const zoomTo = (next, ox, oy, animate) => {
|
|
511
|
+
const target = clamp(next);
|
|
512
|
+
const ratio = target / scale;
|
|
513
|
+
x = ox - (ox - x) * ratio;
|
|
514
|
+
y = oy - (oy - y) * ratio;
|
|
515
|
+
scale = target;
|
|
516
|
+
apply(animate);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const zoomAtCenter = (factor) => zoomTo(scale * factor, viewport.clientWidth / 2, viewport.clientHeight / 2, true);
|
|
520
|
+
|
|
521
|
+
const panBy = (dx, dy) => {
|
|
522
|
+
x += dx;
|
|
523
|
+
y += dy;
|
|
524
|
+
apply(true);
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const pointers = new Map();
|
|
528
|
+
let dragStart = null;
|
|
529
|
+
let pinchStart = null;
|
|
530
|
+
|
|
531
|
+
const pointerDistance = () => {
|
|
532
|
+
const [a, b] = Array.from(pointers.values());
|
|
533
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const pointerMidpoint = () => {
|
|
537
|
+
const [a, b] = Array.from(pointers.values());
|
|
538
|
+
const rect = viewport.getBoundingClientRect();
|
|
539
|
+
return { x: (a.x + b.x) / 2 - rect.left, y: (a.y + b.y) / 2 - rect.top };
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const onPointerDown = (event) => {
|
|
543
|
+
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
|
544
|
+
if (event.target.closest('button')) return;
|
|
545
|
+
viewport.setPointerCapture(event.pointerId);
|
|
546
|
+
pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
547
|
+
|
|
548
|
+
if (pointers.size === 1) {
|
|
549
|
+
dragStart = { px: event.clientX, py: event.clientY, x, y };
|
|
550
|
+
viewport.classList.add('is-panning');
|
|
551
|
+
} else if (pointers.size === 2) {
|
|
552
|
+
dragStart = null;
|
|
553
|
+
pinchStart = { distance: pointerDistance(), scale };
|
|
272
554
|
}
|
|
273
|
-
btn.onmouseenter = () => {
|
|
274
|
-
btn.style.backgroundColor = hoverBgColor;
|
|
275
|
-
btn.style.color = iconHoverColor;
|
|
276
|
-
};
|
|
277
|
-
btn.onmouseleave = () => {
|
|
278
|
-
btn.style.backgroundColor = bgColor;
|
|
279
|
-
btn.style.color = iconColor;
|
|
280
|
-
};
|
|
281
|
-
return btn;
|
|
282
555
|
};
|
|
283
556
|
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
557
|
+
const onPointerMove = (event) => {
|
|
558
|
+
if (!pointers.has(event.pointerId)) return;
|
|
559
|
+
pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
560
|
+
|
|
561
|
+
if (pointers.size === 2 && pinchStart) {
|
|
562
|
+
const mid = pointerMidpoint();
|
|
563
|
+
zoomTo((pinchStart.scale * pointerDistance()) / pinchStart.distance, mid.x, mid.y, false);
|
|
564
|
+
} else if (pointers.size === 1 && dragStart) {
|
|
565
|
+
x = dragStart.x + (event.clientX - dragStart.px);
|
|
566
|
+
y = dragStart.y + (event.clientY - dragStart.py);
|
|
567
|
+
apply(false);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
287
570
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
571
|
+
const onPointerUp = (event) => {
|
|
572
|
+
if (!pointers.has(event.pointerId)) return;
|
|
573
|
+
pointers.delete(event.pointerId);
|
|
574
|
+
try {
|
|
575
|
+
viewport.releasePointerCapture(event.pointerId);
|
|
576
|
+
} catch (e) {}
|
|
577
|
+
|
|
578
|
+
if (pointers.size === 0) {
|
|
579
|
+
dragStart = null;
|
|
580
|
+
pinchStart = null;
|
|
581
|
+
viewport.classList.remove('is-panning');
|
|
582
|
+
} else if (pointers.size === 1) {
|
|
583
|
+
const [remaining] = Array.from(pointers.values());
|
|
584
|
+
pinchStart = null;
|
|
585
|
+
dragStart = { px: remaining.x, py: remaining.y, x, y };
|
|
586
|
+
}
|
|
587
|
+
};
|
|
291
588
|
|
|
292
|
-
|
|
589
|
+
const onWheel = (event) => {
|
|
590
|
+
if (!wheelZoom) return;
|
|
591
|
+
event.preventDefault();
|
|
592
|
+
const rect = viewport.getBoundingClientRect();
|
|
593
|
+
const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
|
|
594
|
+
zoomTo(scale * Math.exp(-delta * 0.0015), event.clientX - rect.left, event.clientY - rect.top, false);
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const onDoubleClick = (event) => {
|
|
598
|
+
if (event.target.closest('button')) return;
|
|
599
|
+
const rect = viewport.getBoundingClientRect();
|
|
600
|
+
zoomTo(scale * ZOOM_STEP * ZOOM_STEP, event.clientX - rect.left, event.clientY - rect.top, true);
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
viewport.addEventListener('pointerdown', onPointerDown);
|
|
604
|
+
viewport.addEventListener('pointermove', onPointerMove);
|
|
605
|
+
viewport.addEventListener('pointerup', onPointerUp);
|
|
606
|
+
viewport.addEventListener('pointercancel', onPointerUp);
|
|
607
|
+
viewport.addEventListener('wheel', onWheel, { passive: false });
|
|
608
|
+
viewport.addEventListener('dblclick', onDoubleClick);
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
fit,
|
|
612
|
+
zoomIn: () => zoomAtCenter(ZOOM_STEP),
|
|
613
|
+
zoomOut: () => zoomAtCenter(1 / ZOOM_STEP),
|
|
614
|
+
panBy,
|
|
615
|
+
getScale: () => scale,
|
|
616
|
+
destroy: () => {
|
|
617
|
+
viewport.removeEventListener('pointerdown', onPointerDown);
|
|
618
|
+
viewport.removeEventListener('pointermove', onPointerMove);
|
|
619
|
+
viewport.removeEventListener('pointerup', onPointerUp);
|
|
620
|
+
viewport.removeEventListener('pointercancel', onPointerUp);
|
|
621
|
+
viewport.removeEventListener('wheel', onWheel);
|
|
622
|
+
viewport.removeEventListener('dblclick', onDoubleClick);
|
|
623
|
+
pointers.clear();
|
|
624
|
+
},
|
|
625
|
+
};
|
|
293
626
|
}
|
|
294
627
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
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>`,
|
|
299
|
-
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>`,
|
|
300
|
-
};
|
|
628
|
+
function createViewport(svgElement, width, height, modal = false) {
|
|
629
|
+
const viewport = document.createElement('div');
|
|
630
|
+
viewport.className = modal ? 'ec-diagram-viewport ec-diagram-viewport--modal' : 'ec-diagram-viewport';
|
|
301
631
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const iconHoverColor = isDark ? '#f0f6fc' : '#0f172a';
|
|
307
|
-
const hoverBgColor = isDark ? '#21262d' : '#f1f5f9';
|
|
632
|
+
const content = document.createElement('div');
|
|
633
|
+
content.className = 'ec-diagram-content';
|
|
634
|
+
content.style.width = `${width}px`;
|
|
635
|
+
content.style.height = `${height}px`;
|
|
308
636
|
|
|
309
|
-
|
|
310
|
-
|
|
637
|
+
svgElement.setAttribute('width', String(width));
|
|
638
|
+
svgElement.setAttribute('height', String(height));
|
|
639
|
+
svgElement.style.width = `${width}px`;
|
|
640
|
+
svgElement.style.height = `${height}px`;
|
|
641
|
+
svgElement.style.maxWidth = 'none';
|
|
311
642
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
643
|
+
content.appendChild(svgElement);
|
|
644
|
+
viewport.appendChild(content);
|
|
645
|
+
return { viewport, content };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Opens the diagram in a fullscreen modal viewer with its own zoom toolbar.
|
|
650
|
+
*/
|
|
651
|
+
function openDiagramModal(source) {
|
|
652
|
+
ensureDiagramStyles();
|
|
653
|
+
if (closeOpenModal) closeOpenModal();
|
|
654
|
+
|
|
655
|
+
const previouslyFocused = document.activeElement;
|
|
656
|
+
const previousBodyOverflow = document.body.style.overflow;
|
|
657
|
+
|
|
658
|
+
const backdrop = document.createElement('div');
|
|
659
|
+
backdrop.className = 'ec-diagram-modal-backdrop';
|
|
660
|
+
|
|
661
|
+
const root = document.createElement('div');
|
|
662
|
+
root.className = 'ec-diagram-modal';
|
|
663
|
+
|
|
664
|
+
const dialog = document.createElement('div');
|
|
665
|
+
dialog.className = 'ec-diagram-modal__dialog';
|
|
666
|
+
dialog.setAttribute('role', 'dialog');
|
|
667
|
+
dialog.setAttribute('aria-modal', 'true');
|
|
668
|
+
dialog.setAttribute('aria-label', 'Diagram');
|
|
669
|
+
|
|
670
|
+
const { viewport, content } = createViewport(source.svg.cloneNode(true), source.width, source.height, true);
|
|
671
|
+
viewport.tabIndex = 0;
|
|
672
|
+
viewport.setAttribute('role', 'application');
|
|
673
|
+
viewport.setAttribute(
|
|
674
|
+
'aria-label',
|
|
675
|
+
'Diagram viewer. Use the arrow keys to pan, plus and minus to zoom, and zero to reset.'
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
const zoomStatus = document.createElement('span');
|
|
679
|
+
zoomStatus.className = 'ec-diagram-modal__zoom';
|
|
680
|
+
zoomStatus.setAttribute('role', 'status');
|
|
681
|
+
zoomStatus.textContent = '100%';
|
|
682
|
+
|
|
683
|
+
const panZoom = createPanZoom(viewport, content, source.width, source.height, {
|
|
684
|
+
wheelZoom: true,
|
|
685
|
+
onChange: (scale) => {
|
|
686
|
+
zoomStatus.textContent = `${Math.round(scale * 100)}%`;
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
let closed = false;
|
|
691
|
+
const close = () => {
|
|
692
|
+
if (closed) return;
|
|
693
|
+
closed = true;
|
|
694
|
+
closeOpenModal = null;
|
|
695
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
696
|
+
panZoom.destroy();
|
|
697
|
+
root.classList.add('is-closing');
|
|
698
|
+
backdrop.classList.add('is-closing');
|
|
699
|
+
root.classList.remove('is-open');
|
|
700
|
+
backdrop.classList.remove('is-open');
|
|
701
|
+
document.body.style.overflow = previousBodyOverflow;
|
|
702
|
+
setTimeout(() => {
|
|
703
|
+
root.remove();
|
|
704
|
+
backdrop.remove();
|
|
705
|
+
}, 150);
|
|
706
|
+
if (previouslyFocused && previouslyFocused.focus) previouslyFocused.focus({ preventScroll: true });
|
|
342
707
|
};
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
708
|
+
closeOpenModal = close;
|
|
709
|
+
|
|
710
|
+
const onKeyDown = (event) => {
|
|
711
|
+
switch (event.key) {
|
|
712
|
+
case 'Escape':
|
|
713
|
+
close();
|
|
714
|
+
break;
|
|
715
|
+
case 'ArrowUp':
|
|
716
|
+
panZoom.panBy(0, PAN_STEP);
|
|
717
|
+
break;
|
|
718
|
+
case 'ArrowDown':
|
|
719
|
+
panZoom.panBy(0, -PAN_STEP);
|
|
720
|
+
break;
|
|
721
|
+
case 'ArrowLeft':
|
|
722
|
+
panZoom.panBy(PAN_STEP, 0);
|
|
723
|
+
break;
|
|
724
|
+
case 'ArrowRight':
|
|
725
|
+
panZoom.panBy(-PAN_STEP, 0);
|
|
726
|
+
break;
|
|
727
|
+
case '+':
|
|
728
|
+
case '=':
|
|
729
|
+
panZoom.zoomIn();
|
|
730
|
+
break;
|
|
731
|
+
case '-':
|
|
732
|
+
case '_':
|
|
733
|
+
panZoom.zoomOut();
|
|
734
|
+
break;
|
|
735
|
+
case '0':
|
|
736
|
+
panZoom.fit(true);
|
|
737
|
+
break;
|
|
738
|
+
default:
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
event.preventDefault();
|
|
346
742
|
};
|
|
347
|
-
btn.onclick = onClick;
|
|
348
743
|
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
color: white;
|
|
358
|
-
font-size: 12px;
|
|
359
|
-
border-radius: 4px;
|
|
360
|
-
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
361
|
-
white-space: nowrap;
|
|
362
|
-
pointer-events: none;
|
|
363
|
-
opacity: 0;
|
|
364
|
-
transition: opacity 0.15s;
|
|
365
|
-
z-index: 50;
|
|
366
|
-
`;
|
|
367
|
-
|
|
368
|
-
if (tooltipPosition === 'right') {
|
|
369
|
-
tooltipStyles += `
|
|
370
|
-
top: 50%;
|
|
371
|
-
left: 100%;
|
|
372
|
-
transform: translateY(-50%);
|
|
373
|
-
margin-left: 8px;
|
|
374
|
-
`;
|
|
375
|
-
} else if (tooltipPosition === 'left') {
|
|
376
|
-
tooltipStyles += `
|
|
377
|
-
top: 50%;
|
|
378
|
-
right: 100%;
|
|
379
|
-
transform: translateY(-50%);
|
|
380
|
-
margin-right: 8px;
|
|
381
|
-
`;
|
|
382
|
-
} else {
|
|
383
|
-
// Default: bottom
|
|
384
|
-
tooltipStyles += `
|
|
385
|
-
top: 100%;
|
|
386
|
-
left: 50%;
|
|
387
|
-
transform: translateX(-50%);
|
|
388
|
-
margin-top: 8px;
|
|
389
|
-
`;
|
|
744
|
+
const toolbar = document.createElement('div');
|
|
745
|
+
toolbar.className = 'ec-diagram-modal__toolbar';
|
|
746
|
+
toolbar.appendChild(createControlButton(ICONS.zoomOut, 'Zoom out', () => panZoom.zoomOut()));
|
|
747
|
+
toolbar.appendChild(zoomStatus);
|
|
748
|
+
toolbar.appendChild(createControlButton(ICONS.zoomIn, 'Zoom in', () => panZoom.zoomIn()));
|
|
749
|
+
toolbar.appendChild(createControlButton(ICONS.reset, 'Reset view', () => panZoom.fit(true)));
|
|
750
|
+
if (source.diagramContent) {
|
|
751
|
+
toolbar.appendChild(createCopyButton(source.diagramContent));
|
|
390
752
|
}
|
|
391
753
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
setTimeout(() => {
|
|
421
|
-
copy.btn.innerHTML = ICONS.copy;
|
|
422
|
-
copy.btn.style.color = copy.iconColor;
|
|
423
|
-
copy.tooltip.textContent = 'Copy diagram code';
|
|
424
|
-
const svgEl = copy.btn.querySelector('svg');
|
|
425
|
-
if (svgEl) svgEl.style.cssText = 'display: block; width: 20px; height: 20px;';
|
|
426
|
-
}, 2000);
|
|
427
|
-
},
|
|
428
|
-
'left'
|
|
429
|
-
);
|
|
430
|
-
copy.wrapper.style.cssText = 'position: absolute; top: 12px; right: 12px; z-index: 10;';
|
|
431
|
-
return copy.wrapper;
|
|
754
|
+
const closeBtn = createControlButton(ICONS.close, 'Close fullscreen', close);
|
|
755
|
+
closeBtn.classList.add('ec-diagram-modal__close');
|
|
756
|
+
|
|
757
|
+
dialog.appendChild(viewport);
|
|
758
|
+
dialog.appendChild(toolbar);
|
|
759
|
+
dialog.appendChild(closeBtn);
|
|
760
|
+
root.appendChild(dialog);
|
|
761
|
+
|
|
762
|
+
backdrop.addEventListener('click', close);
|
|
763
|
+
root.addEventListener('click', (event) => {
|
|
764
|
+
if (event.target === root) close();
|
|
765
|
+
});
|
|
766
|
+
document.addEventListener('keydown', onKeyDown);
|
|
767
|
+
|
|
768
|
+
document.body.appendChild(backdrop);
|
|
769
|
+
document.body.appendChild(root);
|
|
770
|
+
document.body.style.overflow = 'hidden';
|
|
771
|
+
|
|
772
|
+
panZoom.fit(false);
|
|
773
|
+
viewport.focus({ preventScroll: true });
|
|
774
|
+
|
|
775
|
+
requestAnimationFrame(() => {
|
|
776
|
+
requestAnimationFrame(() => {
|
|
777
|
+
backdrop.classList.add('is-open');
|
|
778
|
+
root.classList.add('is-open');
|
|
779
|
+
});
|
|
780
|
+
});
|
|
432
781
|
}
|
|
433
782
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
783
|
+
/**
|
|
784
|
+
* 3x3 grid of inline controls:
|
|
785
|
+
* [fullscreen] [pan up] [zoom in]
|
|
786
|
+
* [pan left] [reset] [pan right]
|
|
787
|
+
* [copy] [pan down] [zoom out]
|
|
788
|
+
*/
|
|
789
|
+
function createDiagramControls(panZoom, { placement, source }) {
|
|
790
|
+
const controls = document.createElement('div');
|
|
791
|
+
controls.className = 'ec-diagram-controls';
|
|
792
|
+
controls.setAttribute('data-placement', placement);
|
|
793
|
+
controls.setAttribute('role', 'toolbar');
|
|
794
|
+
controls.setAttribute('aria-label', 'Diagram controls');
|
|
795
|
+
|
|
796
|
+
let copyBtn;
|
|
797
|
+
if (source.diagramContent) {
|
|
798
|
+
copyBtn = createCopyButton(source.diagramContent);
|
|
437
799
|
} else {
|
|
438
|
-
document.
|
|
800
|
+
copyBtn = document.createElement('div');
|
|
801
|
+
copyBtn.setAttribute('aria-hidden', 'true');
|
|
439
802
|
}
|
|
803
|
+
|
|
804
|
+
[
|
|
805
|
+
createControlButton(ICONS.fullscreen, 'Open fullscreen', () => openDiagramModal(source)),
|
|
806
|
+
createControlButton(ICONS.panUp, 'Pan up', () => panZoom.panBy(0, PAN_STEP)),
|
|
807
|
+
createControlButton(ICONS.zoomIn, 'Zoom in', () => panZoom.zoomIn()),
|
|
808
|
+
createControlButton(ICONS.panLeft, 'Pan left', () => panZoom.panBy(PAN_STEP, 0)),
|
|
809
|
+
createControlButton(ICONS.reset, 'Reset view', () => panZoom.fit(true)),
|
|
810
|
+
createControlButton(ICONS.panRight, 'Pan right', () => panZoom.panBy(-PAN_STEP, 0)),
|
|
811
|
+
copyBtn,
|
|
812
|
+
createControlButton(ICONS.panDown, 'Pan down', () => panZoom.panBy(0, -PAN_STEP)),
|
|
813
|
+
createControlButton(ICONS.zoomOut, 'Zoom out', () => panZoom.zoomOut()),
|
|
814
|
+
].forEach((btn) => controls.appendChild(btn));
|
|
815
|
+
|
|
816
|
+
return controls;
|
|
440
817
|
}
|
|
441
818
|
|
|
442
|
-
|
|
443
|
-
|
|
819
|
+
const TRIM_PADDING = 8;
|
|
820
|
+
const TRIM_THRESHOLD = 16;
|
|
444
821
|
|
|
445
|
-
|
|
446
|
-
|
|
822
|
+
// Tighten over-allocated viewBoxes (e.g. mermaid C4 diagrams) around the drawn content
|
|
823
|
+
function trimSvgViewBox(svgElement) {
|
|
824
|
+
const viewBox = svgElement.getAttribute('viewBox');
|
|
825
|
+
if (!viewBox) return;
|
|
826
|
+
const [vx, vy, vw, vh] = viewBox.split(/[\s,]+/).map(Number);
|
|
827
|
+
if (!(vw > 0 && vh > 0)) return;
|
|
828
|
+
|
|
829
|
+
let bbox;
|
|
830
|
+
try {
|
|
831
|
+
bbox = svgElement.getBBox();
|
|
832
|
+
} catch (e) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (!(bbox.width > 0 && bbox.height > 0)) return;
|
|
836
|
+
|
|
837
|
+
const slack = Math.max(bbox.x - vx, bbox.y - vy, vx + vw - (bbox.x + bbox.width), vy + vh - (bbox.y + bbox.height));
|
|
838
|
+
if (slack <= TRIM_THRESHOLD) return;
|
|
839
|
+
|
|
840
|
+
const x = Math.max(vx, bbox.x - TRIM_PADDING);
|
|
841
|
+
const y = Math.max(vy, bbox.y - TRIM_PADDING);
|
|
842
|
+
const width = Math.min(vx + vw, bbox.x + bbox.width + TRIM_PADDING) - x;
|
|
843
|
+
const height = Math.min(vy + vh, bbox.y + bbox.height + TRIM_PADDING) - y;
|
|
844
|
+
svgElement.setAttribute('viewBox', `${x} ${y} ${width} ${height}`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function getSvgDimensions(svgElement) {
|
|
848
|
+
let width = 0;
|
|
849
|
+
let height = 0;
|
|
850
|
+
trimSvgViewBox(svgElement);
|
|
447
851
|
const viewBox = svgElement.getAttribute('viewBox');
|
|
448
852
|
if (viewBox) {
|
|
449
853
|
const parts = viewBox.split(/[\s,]+/).map(Number);
|
|
450
854
|
width = parts[2];
|
|
451
855
|
height = parts[3];
|
|
452
|
-
} else {
|
|
453
|
-
const bbox = svgElement.getBBox();
|
|
454
|
-
width = bbox.width;
|
|
455
|
-
height = bbox.height;
|
|
456
|
-
if (width > 0 && height > 0) {
|
|
457
|
-
svgElement.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${width} ${height}`);
|
|
458
|
-
}
|
|
459
856
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
857
|
+
if (!(width > 0 && height > 0)) {
|
|
858
|
+
try {
|
|
859
|
+
const bbox = svgElement.getBBox();
|
|
860
|
+
width = bbox.width;
|
|
861
|
+
height = bbox.height;
|
|
862
|
+
if (width > 0 && height > 0 && !viewBox) {
|
|
863
|
+
svgElement.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${width} ${height}`);
|
|
864
|
+
}
|
|
865
|
+
} catch (e) {}
|
|
467
866
|
}
|
|
867
|
+
if (!(width > 0 && height > 0)) {
|
|
868
|
+
width = svgElement.clientWidth || parseFloat(svgElement.getAttribute('width') || '0') || 800;
|
|
869
|
+
height = svgElement.clientHeight || parseFloat(svgElement.getAttribute('height') || '0') || 400;
|
|
870
|
+
}
|
|
871
|
+
return { width, height };
|
|
872
|
+
}
|
|
468
873
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
svgElement.style.height = '100%';
|
|
472
|
-
svgElement.removeAttribute('height');
|
|
473
|
-
svgElement.removeAttribute('width');
|
|
474
|
-
|
|
475
|
-
try {
|
|
476
|
-
const instance = svgPanZoom(svgElement, {
|
|
477
|
-
zoomEnabled: true,
|
|
478
|
-
controlIconsEnabled: false,
|
|
479
|
-
fit: true,
|
|
480
|
-
center: true,
|
|
481
|
-
minZoom: 0.5,
|
|
482
|
-
maxZoom: 10,
|
|
483
|
-
zoomScaleSensitivity: 0.15,
|
|
484
|
-
dblClickZoomEnabled: true,
|
|
485
|
-
mouseWheelZoomEnabled: false, // Disabled to avoid hijacking page scroll
|
|
486
|
-
preventMouseEventsDefault: true,
|
|
487
|
-
panEnabled: true,
|
|
488
|
-
});
|
|
874
|
+
async function initMermaidZoom(svgElement, container, id, diagramContent, controlOptions = {}) {
|
|
875
|
+
ensureDiagramStyles();
|
|
489
876
|
|
|
490
|
-
|
|
877
|
+
const { placement = DEFAULT_PLACEMENT, actions } = controlOptions;
|
|
878
|
+
const { width, height } = getSvgDimensions(svgElement);
|
|
491
879
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
880
|
+
// Set container height based on SVG aspect ratio, capped for usability
|
|
881
|
+
const containerWidth = container.clientWidth || 800;
|
|
882
|
+
container.style.height = `${Math.min(Math.max(containerWidth * (height / width), 200), 500)}px`;
|
|
883
|
+
|
|
884
|
+
const { viewport, content } = createViewport(svgElement, width, height);
|
|
885
|
+
container.innerHTML = '';
|
|
886
|
+
container.appendChild(viewport);
|
|
887
|
+
|
|
888
|
+
const panZoom = createPanZoom(viewport, content, width, height);
|
|
889
|
+
zoomInstances.set(id, panZoom);
|
|
890
|
+
panZoom.fit(false);
|
|
891
|
+
|
|
892
|
+
// Add interactive controls. By default they are only shown for diagrams taller than 120px.
|
|
893
|
+
const showControls = actions ?? height > CONTROLS_MIN_HEIGHT;
|
|
894
|
+
if (showControls) {
|
|
895
|
+
const source = { svg: svgElement, width, height, diagramContent };
|
|
896
|
+
container.appendChild(createDiagramControls(panZoom, { placement, source }));
|
|
897
|
+
}
|
|
502
898
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
() => instance.zoomOut(),
|
|
507
|
-
() => {
|
|
508
|
-
instance.fit();
|
|
509
|
-
instance.center();
|
|
510
|
-
}
|
|
511
|
-
);
|
|
512
|
-
container.appendChild(controls);
|
|
513
|
-
|
|
514
|
-
// Add fullscreen button (top-left)
|
|
515
|
-
const fullscreenBtn = createFullscreenButton(() => toggleFullscreen(container));
|
|
516
|
-
container.appendChild(fullscreenBtn);
|
|
517
|
-
|
|
518
|
-
// Add copy button (top-right) if diagram content is available
|
|
519
|
-
if (diagramContent) {
|
|
520
|
-
const copyBtn = createCopyButton(() => {
|
|
521
|
-
navigator.clipboard.writeText(diagramContent).catch((err) => {
|
|
522
|
-
console.warn('Failed to copy:', err);
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
container.appendChild(copyBtn);
|
|
899
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
900
|
+
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
|
901
|
+
panZoom.fit(false);
|
|
526
902
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
document.addEventListener('fullscreenchange', () => {
|
|
530
|
-
const isFullscreen = document.fullscreenElement === container;
|
|
531
|
-
if (isFullscreen) {
|
|
532
|
-
instance.enableMouseWheelZoom();
|
|
533
|
-
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
534
|
-
container.style.background = isDark ? '#0d1117' : '#ffffff';
|
|
535
|
-
} else {
|
|
536
|
-
instance.disableMouseWheelZoom();
|
|
537
|
-
container.style.background = '';
|
|
538
|
-
}
|
|
539
|
-
setTimeout(() => {
|
|
540
|
-
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
|
541
|
-
try {
|
|
542
|
-
instance.resize();
|
|
543
|
-
instance.fit();
|
|
544
|
-
instance.center();
|
|
545
|
-
} catch (e) {}
|
|
546
|
-
}
|
|
547
|
-
}, 100);
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// Resize handler for responsiveness
|
|
551
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
552
|
-
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
|
553
|
-
try {
|
|
554
|
-
instance.resize();
|
|
555
|
-
instance.fit();
|
|
556
|
-
instance.center();
|
|
557
|
-
} catch (e) {}
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
resizeObserver.observe(container);
|
|
561
|
-
} catch (e) {
|
|
562
|
-
console.warn('Failed to initialize zoom on mermaid diagram:', e);
|
|
563
|
-
}
|
|
903
|
+
});
|
|
904
|
+
resizeObserver.observe(container);
|
|
564
905
|
}
|
|
565
906
|
|
|
566
907
|
async function renderMermaid() {
|
|
@@ -608,8 +949,8 @@ const diagramId = props.data.id;
|
|
|
608
949
|
|
|
609
950
|
// Create zoom container with inline styles (no Tailwind in embed)
|
|
610
951
|
const container = document.createElement('div');
|
|
611
|
-
container.
|
|
612
|
-
|
|
952
|
+
container.className = 'mermaid-zoom-container';
|
|
953
|
+
container.style.cssText = 'min-height: 200px; margin: 1em 0;';
|
|
613
954
|
|
|
614
955
|
// Insert the rendered SVG
|
|
615
956
|
container.innerHTML = result.svg;
|
|
@@ -621,7 +962,7 @@ const diagramId = props.data.id;
|
|
|
621
962
|
// Initialize zoom on the SVG
|
|
622
963
|
const svgElement = container.querySelector('svg');
|
|
623
964
|
if (svgElement) {
|
|
624
|
-
await initMermaidZoom(svgElement, container, id, content);
|
|
965
|
+
await initMermaidZoom(svgElement, container, id, content, getControlOptionsFromElement(graph));
|
|
625
966
|
}
|
|
626
967
|
} catch (e) {
|
|
627
968
|
console.error('Mermaid render error:', e);
|
|
@@ -637,7 +978,6 @@ const diagramId = props.data.id;
|
|
|
637
978
|
if (blocks.length === 0) return;
|
|
638
979
|
|
|
639
980
|
const { deflate } = await import('https://cdn.jsdelivr.net/npm/pako@2.1.0/+esm');
|
|
640
|
-
const { default: svgPanZoom } = await import('https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.2/+esm');
|
|
641
981
|
|
|
642
982
|
function encode64(data) {
|
|
643
983
|
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_';
|
|
@@ -681,8 +1021,8 @@ const diagramId = props.data.id;
|
|
|
681
1021
|
|
|
682
1022
|
// Create zoom container
|
|
683
1023
|
const container = document.createElement('div');
|
|
684
|
-
container.
|
|
685
|
-
|
|
1024
|
+
container.className = 'mermaid-zoom-container';
|
|
1025
|
+
container.style.cssText = 'min-height: 200px; margin: 1em 0;';
|
|
686
1026
|
container.innerHTML = svgText;
|
|
687
1027
|
|
|
688
1028
|
block.innerHTML = '';
|