@markdy/renderer-dom 1.0.10 → 1.0.11

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/README.md CHANGED
@@ -77,8 +77,9 @@ diagram.destroy(); // clean up DOM + cancel animations
77
77
  | `sceneBoundaryProgress` | `boolean \| string` | `true` | Show boundary progress bar, or pass a custom color/gradient string |
78
78
  | `progressColor` | `string` | `plan.meta.progressColor ?? "rainbow"` | Custom progress bar color (e.g. `"#3b82f6"`) or gradient (e.g. `"#ec4899, #8b5cf6"`) |
79
79
  | `playbackRate` | `number` | `plan.meta.playbackRate ?? 1` | Normalized timeline speed multiplier; `1` is Markdy's normal pace |
80
- | `interactiveViewport` | `boolean` | `controls \|\| plan.meta.interactiveViewport` | Enable wheel zoom and drag pan on the rendered viewport |
81
- | `controls` | `boolean` | `plan.meta.controls ?? false` | Show left-aligned footer controls toolbar (play/pause, restart, speed, reset view); also enables viewport interaction |
80
+ | `interactiveViewport` | `boolean` | `controls \|\| player.interaction` | Enable wheel zoom and drag pan on the rendered viewport |
81
+ | `controls` | `boolean` | `player.controls` | Show the footer toolbar (play, prev/next beat, restart, seek, speed, fit, reset view, SVG, share); also enables viewport interaction |
82
+ | `shareUrl` | `string` | Markdy playground | Base URL used by the Share control when building `#code=` links |
82
83
  | `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
83
84
  | `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
84
85
  | `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
@@ -0,0 +1,99 @@
1
+ // src/export/svg-exporter.ts
2
+ function copyRenderedStyles(source, clone) {
3
+ if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
4
+ const sourceElements = [source, ...Array.from(source.querySelectorAll("*"))];
5
+ const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
6
+ for (let index = 0; index < Math.min(sourceElements.length, cloneElements.length); index++) {
7
+ const computed = window.getComputedStyle(sourceElements[index]);
8
+ const target = cloneElements[index].style;
9
+ for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
10
+ const property = computed.item(propertyIndex);
11
+ target.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
12
+ }
13
+ }
14
+ }
15
+ function normalizeExportViewport(scene) {
16
+ scene.querySelectorAll(".markdy-viewport-transform").forEach((viewportTransform) => {
17
+ viewportTransform.style.transform = "translate(0px, 0px) scale(1)";
18
+ viewportTransform.style.transformOrigin = "0 0";
19
+ viewportTransform.style.willChange = "auto";
20
+ });
21
+ }
22
+ function getDiagramSceneElement(containerEl) {
23
+ const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
24
+ if (!sceneEl) throw new Error("No Markdy scene element found in container");
25
+ return sceneEl;
26
+ }
27
+ function prepareHtmlSceneForExport(sceneEl, options = {}) {
28
+ const clonedScene = sceneEl.cloneNode(true);
29
+ copyRenderedStyles(sceneEl, clonedScene);
30
+ normalizeExportViewport(clonedScene);
31
+ const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
32
+ const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
33
+ let width = parseFloat(widthStr);
34
+ let height = parseFloat(heightStr);
35
+ if (isNaN(width)) width = 800;
36
+ if (isNaN(height)) height = 400;
37
+ const scale = options.scale || 1;
38
+ const scaledWidth = width * scale;
39
+ const scaledHeight = height * scale;
40
+ clonedScene.style.transform = `scale(${scale})`;
41
+ clonedScene.style.transformOrigin = "0 0";
42
+ clonedScene.style.position = "relative";
43
+ clonedScene.style.left = "0px";
44
+ clonedScene.style.top = "0px";
45
+ clonedScene.style.margin = "0";
46
+ clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
47
+ if (options.transparentBackground) {
48
+ clonedScene.style.background = "transparent";
49
+ }
50
+ return { sceneEl, clonedScene, width, height, scaledWidth, scaledHeight };
51
+ }
52
+ function exportDiagramAsVectorSvg(containerEl, options = {}) {
53
+ const sceneEl = getDiagramSceneElement(containerEl);
54
+ if (sceneEl.tagName?.toLowerCase() === "svg") {
55
+ const clonedSvg = sceneEl.cloneNode(true);
56
+ if (!clonedSvg.getAttribute("xmlns")) {
57
+ clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
58
+ }
59
+ const serializer2 = new XMLSerializer();
60
+ return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
61
+ ${serializer2.serializeToString(clonedSvg)}`;
62
+ }
63
+ const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
64
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
65
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
66
+ svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
67
+ svg.setAttribute("width", String(scaledWidth));
68
+ svg.setAttribute("height", String(scaledHeight));
69
+ svg.setAttribute("viewBox", `0 0 ${scaledWidth} ${scaledHeight}`);
70
+ if (options.includeThemeStyles !== false) {
71
+ const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
72
+ let combinedStyles = `
73
+ foreignObject { width: 100%; height: 100%; }
74
+ .markdy-node { transition: opacity 0.3s ease; }
75
+ `;
76
+ if (typeof document !== "undefined") {
77
+ const styles = document.querySelectorAll("style[id^='markdy-']");
78
+ for (let i = 0; i < styles.length; i++) {
79
+ combinedStyles += styles[i].textContent + "\n";
80
+ }
81
+ }
82
+ styleEl.textContent = combinedStyles;
83
+ svg.appendChild(styleEl);
84
+ }
85
+ const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
86
+ foreignObject.setAttribute("width", "100%");
87
+ foreignObject.setAttribute("height", "100%");
88
+ foreignObject.appendChild(clonedScene);
89
+ svg.appendChild(foreignObject);
90
+ const serializer = new XMLSerializer();
91
+ return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
92
+ ` + serializer.serializeToString(svg);
93
+ }
94
+
95
+ export {
96
+ getDiagramSceneElement,
97
+ prepareHtmlSceneForExport,
98
+ exportDiagramAsVectorSvg
99
+ };
package/dist/index.d.ts CHANGED
@@ -28,6 +28,8 @@ interface DiagramOptions {
28
28
  playbackRate?: number;
29
29
  /** Enable wheel zoom and drag pan on the rendered viewport. Defaults to false. */
30
30
  interactiveViewport?: boolean;
31
+ /** Base URL for the Share control. Defaults to the Markdy playground. */
32
+ shareUrl?: string;
31
33
  /** Show built-in playback and viewport controls. Also enables viewport interaction. Defaults to false. */
32
34
  controls?: boolean;
33
35
  onWarning?: (warning: Diagnostic) => void;
@@ -46,6 +48,8 @@ interface Diagram {
46
48
  isPlaying(): boolean;
47
49
  beats(): BeatRange[];
48
50
  seekToBeat(name: string): void;
51
+ nextBeat(): void;
52
+ prevBeat(): void;
49
53
  destroy(): void;
50
54
  }
51
55
  declare function createDiagram(opts: DiagramOptions): Diagram;
package/dist/index.js CHANGED
@@ -1,5 +1,15 @@
1
+ import {
2
+ exportDiagramAsVectorSvg,
3
+ getDiagramSceneElement,
4
+ prepareHtmlSceneForExport
5
+ } from "./chunk-VIZHA7GI.js";
6
+
1
7
  // src/diagram.ts
2
- import { parseAndCompile } from "@markdy/core";
8
+ import {
9
+ compressMarkdyToUrlHash,
10
+ parseAndCompile,
11
+ resolvePlayer
12
+ } from "@markdy/core";
3
13
 
4
14
  // src/geometry/rect.ts
5
15
  function boxRect(box) {
@@ -2571,11 +2581,11 @@ function applyThemeToScene(scene, theme) {
2571
2581
 
2572
2582
  // src/diagram.ts
2573
2583
  var NORMAL_PLAYBACK_RATE = 4 / 5;
2574
- var DEFAULT_PLAYBACK_RATE = 1;
2575
2584
  var MIN_VIEWPORT_ZOOM = 0.5;
2576
2585
  var MAX_VIEWPORT_ZOOM = 3;
2577
2586
  var VIEWPORT_ZOOM_STEP = 15e-4;
2578
2587
  var DRAG_CLICK_THRESHOLD_PX = 4;
2588
+ var BEAT_NAV_EPSILON_S = 0.05;
2579
2589
  var MARKDY_PLAYGROUND_URL = "https://markdy.com/playground/";
2580
2590
  function encodeCodeForPlaygroundHash(code) {
2581
2591
  return encodeURIComponent(btoa(encodeURIComponent(code)));
@@ -2628,6 +2638,7 @@ function createDiagram(opts) {
2628
2638
  playbackRate: initialPlaybackRate,
2629
2639
  controls: explicitControls,
2630
2640
  interactiveViewport: explicitInteractiveViewport,
2641
+ shareUrl,
2631
2642
  autoplay: explicitAutoplay,
2632
2643
  loop: explicitLoop,
2633
2644
  copyright: explicitCopyright,
@@ -2640,15 +2651,46 @@ function createDiagram(opts) {
2640
2651
  for (const w of ast.diagnostics) {
2641
2652
  if (w.severity === "warning") onWarning(w);
2642
2653
  }
2643
- const controls = explicitControls ?? plan.meta.controls ?? false;
2644
- const interactiveViewport = explicitInteractiveViewport ?? (explicitControls !== void 0 ? explicitControls : plan.meta.interactiveViewport ?? plan.meta.controls ?? false);
2645
- const autoplay = explicitAutoplay ?? plan.meta.autoplay ?? true;
2646
- const loop = explicitLoop ?? plan.meta.loop ?? true;
2647
- const copyright = explicitCopyright ?? plan.meta.copyright ?? true;
2648
- const rawPlaybackRate = initialPlaybackRate ?? plan.meta.playbackRate ?? DEFAULT_PLAYBACK_RATE;
2649
- let playbackRate = Number.isFinite(rawPlaybackRate) && rawPlaybackRate > 0 ? rawPlaybackRate : DEFAULT_PLAYBACK_RATE;
2650
- const showSceneBoundaryProgress = sceneBoundaryProgress === false || sceneBoundaryProgress === void 0 && progressBar === false ? false : plan.meta.progressColor === "none" ? false : true;
2651
- const rawColor = progressColor ?? progressBarColor ?? (typeof sceneBoundaryProgress === "string" && sceneBoundaryProgress !== "true" && sceneBoundaryProgress !== "false" ? sceneBoundaryProgress : typeof progressBar === "string" && progressBar !== "true" && progressBar !== "false" ? progressBar : void 0) ?? (plan.meta.progressColor && plan.meta.progressColor !== "none" ? plan.meta.progressColor : void 0);
2654
+ const hostProgress = sceneBoundaryProgress === false || sceneBoundaryProgress === void 0 && progressBar === false ? "none" : void 0;
2655
+ const hostProgressColor = progressColor ?? progressBarColor ?? (typeof sceneBoundaryProgress === "string" && sceneBoundaryProgress !== "true" && sceneBoundaryProgress !== "false" ? sceneBoundaryProgress : typeof progressBar === "string" && progressBar !== "true" && progressBar !== "false" ? progressBar : void 0);
2656
+ const player = resolvePlayer(plan.meta.player, {
2657
+ autoplay: explicitAutoplay,
2658
+ loop: explicitLoop,
2659
+ playbackRate: initialPlaybackRate,
2660
+ copyright: explicitCopyright,
2661
+ controls: explicitControls,
2662
+ interactiveViewport: explicitInteractiveViewport,
2663
+ progress: hostProgress,
2664
+ progressColor: hostProgressColor
2665
+ });
2666
+ const { autoplay, loop } = player.playback;
2667
+ const {
2668
+ enabled: interactiveViewport,
2669
+ zoom: allowZoom,
2670
+ pan: allowPan,
2671
+ clickToPlay,
2672
+ doubleClickToReset,
2673
+ keyboard: keyboardShortcuts
2674
+ } = player.interaction;
2675
+ const {
2676
+ enabled: showControls,
2677
+ play: playButton,
2678
+ restart: restartButton,
2679
+ prevBeat: prevBeatButton,
2680
+ nextBeat: nextBeatButton,
2681
+ seek: seekBar,
2682
+ speed: speedControls,
2683
+ speeds: speedOptions,
2684
+ fit: fitViewButton,
2685
+ resetView: resetViewButton,
2686
+ svg: svgButton,
2687
+ share: shareButton
2688
+ } = player.controls;
2689
+ const copyright = player.chrome.badge;
2690
+ const progressMode = player.chrome.progress;
2691
+ const showProgress = progressMode !== "none";
2692
+ let playbackRate = player.playback.rate;
2693
+ const rawColor = player.chrome.progressColor;
2652
2694
  const customColor = rawColor && rawColor.trim() !== "rainbow" ? rawColor.trim() : null;
2653
2695
  const DEFAULT_RAINBOW = "hsl(0,90%,60%), hsl(45,90%,55%), hsl(90,80%,50%), hsl(180,80%,50%), hsl(270,80%,55%), hsl(330,90%,60%)";
2654
2696
  const totalDurationMs = plan.duration * 1e3;
@@ -2667,11 +2709,11 @@ function createDiagram(opts) {
2667
2709
  });
2668
2710
  container.appendChild(viewport);
2669
2711
  let progressEl = null;
2670
- if (showSceneBoundaryProgress) {
2712
+ if (showProgress) {
2671
2713
  progressEl = document.createElement("div");
2672
2714
  Object.assign(progressEl.style, {
2673
2715
  position: "absolute",
2674
- inset: "0",
2716
+ ...progressMode === "bar" ? { left: "0", right: "0", bottom: "0", height: "3px" } : { inset: "0" },
2675
2717
  zIndex: "9999",
2676
2718
  pointerEvents: "none",
2677
2719
  borderRadius: "inherit"
@@ -2682,6 +2724,12 @@ function createDiagram(opts) {
2682
2724
  const tlAngleNorm = (tlAngle % 360 + 360) % 360;
2683
2725
  function updateProgressBar(pct) {
2684
2726
  if (!progressEl) return;
2727
+ if (progressMode === "bar") {
2728
+ progressEl.style.background = customColor ?? "#2563eb";
2729
+ progressEl.style.transformOrigin = "left center";
2730
+ progressEl.style.transform = `scaleX(${pct})`;
2731
+ return;
2732
+ }
2685
2733
  const deg = pct * 360;
2686
2734
  const colorStops = customColor ? customColor.includes(",") ? customColor : `${customColor} 0deg, ${customColor}` : DEFAULT_RAINBOW;
2687
2735
  progressEl.style.background = `conic-gradient(from ${tlAngleNorm}deg, ${colorStops} ${deg}deg, transparent ${deg}deg)`;
@@ -2955,15 +3003,40 @@ function createDiagram(opts) {
2955
3003
  let suppressNextClick = false;
2956
3004
  let controlsPlayButton = null;
2957
3005
  let controlsRateButtons = [];
3006
+ let controlsSeekBar = null;
3007
+ let controlsFitButton = null;
3008
+ let fitViewActive = false;
2958
3009
  function applyViewportTransform() {
2959
3010
  viewportTransform.style.transform = `translate(${viewportPanX}px, ${viewportPanY}px) scale(${viewportScale})`;
2960
3011
  }
2961
3012
  function resetViewportTransform() {
3013
+ releaseFitView();
2962
3014
  viewportScale = 1;
2963
3015
  viewportPanX = 0;
2964
3016
  viewportPanY = 0;
2965
3017
  applyViewportTransform();
2966
3018
  }
3019
+ function releaseFitView() {
3020
+ if (!fitViewActive) return;
3021
+ fitViewActive = false;
3022
+ cameraLayer.style.removeProperty("transform");
3023
+ }
3024
+ function toggleFitView() {
3025
+ if (fitViewActive) {
3026
+ resetViewportTransform();
3027
+ syncControls();
3028
+ return;
3029
+ }
3030
+ const bounds = computeContentBounds();
3031
+ const scale = Math.min(plan.meta.width / bounds.width, plan.meta.height / bounds.height);
3032
+ viewportScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
3033
+ viewportPanX = -bounds.minX * viewportScale + (plan.meta.width - bounds.width * viewportScale) / 2;
3034
+ viewportPanY = -bounds.minY * viewportScale + (plan.meta.height - bounds.height * viewportScale) / 2;
3035
+ applyViewportTransform();
3036
+ fitViewActive = true;
3037
+ cameraLayer.style.setProperty("transform", "none", "important");
3038
+ syncControls();
3039
+ }
2967
3040
  function handleViewportWheel(event) {
2968
3041
  event.preventDefault();
2969
3042
  const rect = viewport.getBoundingClientRect();
@@ -2979,6 +3052,7 @@ function createDiagram(opts) {
2979
3052
  applyViewportTransform();
2980
3053
  }
2981
3054
  function handleViewportPointerDown(event) {
3055
+ if (!allowPan) return;
2982
3056
  if (event.button !== 0 || activePointerId !== null) return;
2983
3057
  activePointerId = event.pointerId;
2984
3058
  dragStartX = event.clientX;
@@ -3031,6 +3105,13 @@ function createDiagram(opts) {
3031
3105
  button.style.color = active ? "#ffffff" : "#475569";
3032
3106
  button.style.borderColor = active ? "#0f172a" : "rgba(148, 163, 184, 0.55)";
3033
3107
  }
3108
+ if (controlsSeekBar) controlsSeekBar.value = String(sceneMs / 1e3);
3109
+ if (controlsFitButton) {
3110
+ controlsFitButton.setAttribute("aria-pressed", fitViewActive ? "true" : "false");
3111
+ controlsFitButton.style.background = fitViewActive ? "#1e293b" : "rgba(248, 250, 252, 0.92)";
3112
+ controlsFitButton.style.color = fitViewActive ? "#ffffff" : "#475569";
3113
+ controlsFitButton.style.borderColor = fitViewActive ? "#0f172a" : "rgba(148, 163, 184, 0.55)";
3114
+ }
3034
3115
  }
3035
3116
  function emitPlayStateChange(playing) {
3036
3117
  onPlayStateChange?.(playing);
@@ -3105,8 +3186,23 @@ function createDiagram(opts) {
3105
3186
  const beat = plan.beats.find((b) => b.name === name);
3106
3187
  if (beat) diagram.seek(beat.start);
3107
3188
  },
3189
+ nextBeat() {
3190
+ const next = plan.beats.find((beat) => beat.start > sceneMs / 1e3 + BEAT_NAV_EPSILON_S);
3191
+ if (next) diagram.seek(next.start);
3192
+ },
3193
+ prevBeat() {
3194
+ const current = sceneMs / 1e3;
3195
+ let target = 0;
3196
+ for (const beat of plan.beats) {
3197
+ if (beat.start < current - BEAT_NAV_EPSILON_S) target = beat.start;
3198
+ }
3199
+ diagram.seek(target);
3200
+ },
3108
3201
  destroy() {
3109
3202
  diagram.pause();
3203
+ if (keyboardShortcuts && typeof window !== "undefined") {
3204
+ window.removeEventListener("keydown", handleKeyDown);
3205
+ }
3110
3206
  for (const anim of allAnims) anim.cancel();
3111
3207
  resizeObserver?.disconnect();
3112
3208
  if (progressEl?.parentNode === viewport) viewport.removeChild(progressEl);
@@ -3143,6 +3239,159 @@ function createDiagram(opts) {
3143
3239
  });
3144
3240
  return button;
3145
3241
  }
3242
+ function mountPlayControl(toolbar) {
3243
+ if (!playButton) return;
3244
+ controlsPlayButton = makeControlButton("Play", "Play diagram");
3245
+ controlsPlayButton.className = "markdy-control-play";
3246
+ controlsPlayButton.addEventListener("click", togglePlayback);
3247
+ toolbar.appendChild(controlsPlayButton);
3248
+ }
3249
+ function mountBeatNavControls(toolbar, position) {
3250
+ const wanted = position === "prev" ? prevBeatButton : nextBeatButton;
3251
+ if (!wanted || plan.beats.length < 2) return;
3252
+ const label = position === "prev" ? "Prev" : "Next";
3253
+ const button = makeControlButton(label, `${label === "Prev" ? "Previous" : "Next"} beat`);
3254
+ button.className = `markdy-control-${position}-beat`;
3255
+ button.addEventListener("click", () => position === "prev" ? diagram.prevBeat() : diagram.nextBeat());
3256
+ toolbar.appendChild(button);
3257
+ }
3258
+ function mountRestartControl(toolbar) {
3259
+ if (!restartButton) return;
3260
+ const button = makeControlButton("Restart", "Restart diagram");
3261
+ button.className = "markdy-control-restart";
3262
+ button.addEventListener("click", () => {
3263
+ diagram.seek(0);
3264
+ diagram.play();
3265
+ });
3266
+ toolbar.appendChild(button);
3267
+ }
3268
+ function mountSeekControl(toolbar) {
3269
+ if (!seekBar) return;
3270
+ controlsSeekBar = document.createElement("input");
3271
+ controlsSeekBar.className = "markdy-control-seek";
3272
+ controlsSeekBar.type = "range";
3273
+ controlsSeekBar.min = "0";
3274
+ controlsSeekBar.max = String(durationSeconds);
3275
+ controlsSeekBar.step = "0.01";
3276
+ controlsSeekBar.value = String(sceneMs / 1e3);
3277
+ controlsSeekBar.setAttribute("aria-label", "Seek diagram timeline");
3278
+ controlsSeekBar.addEventListener("input", () => diagram.seek(Number(controlsSeekBar?.value ?? 0)));
3279
+ toolbar.appendChild(controlsSeekBar);
3280
+ }
3281
+ function mountSpeedControls(toolbar) {
3282
+ if (!speedControls) return;
3283
+ controlsRateButtons = speedOptions.map((rate) => {
3284
+ const button = makeControlButton(`${rate}x`, `Set playback speed to ${rate}x`);
3285
+ button.className = "markdy-control-rate";
3286
+ button.dataset.rate = String(rate);
3287
+ button.setAttribute("aria-pressed", "false");
3288
+ button.addEventListener("click", () => diagram.setPlaybackRate(rate));
3289
+ toolbar.appendChild(button);
3290
+ return button;
3291
+ });
3292
+ }
3293
+ function flashControlLabel(button, message) {
3294
+ const original = button.textContent ?? "";
3295
+ button.textContent = message;
3296
+ setTimeout(() => {
3297
+ button.textContent = original;
3298
+ }, 1400);
3299
+ }
3300
+ function downloadFile(filename, contents, type) {
3301
+ const blob = new Blob([contents], { type });
3302
+ const url = URL.createObjectURL(blob);
3303
+ const link = document.createElement("a");
3304
+ link.href = url;
3305
+ link.download = filename;
3306
+ link.style.display = "none";
3307
+ document.body.appendChild(link);
3308
+ link.click();
3309
+ document.body.removeChild(link);
3310
+ URL.revokeObjectURL(url);
3311
+ }
3312
+ function mountSvgControl(toolbar) {
3313
+ if (!svgButton) return;
3314
+ const button = makeControlButton("SVG", "Export diagram as SVG");
3315
+ button.className = "markdy-control-svg";
3316
+ button.addEventListener("click", async () => {
3317
+ const resumeAt = sceneMs;
3318
+ const wasPlaying = isPlaying;
3319
+ try {
3320
+ diagram.pause();
3321
+ diagram.seek(durationSeconds);
3322
+ const { exportDiagramAsVectorSvg: exportDiagramAsVectorSvg2 } = await import("./svg-exporter-7FW6RIP6.js");
3323
+ const svg = exportDiagramAsVectorSvg2(container);
3324
+ const name = (plan.title || "markdy-diagram").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3325
+ downloadFile(`${name || "markdy-diagram"}.svg`, svg, "image/svg+xml");
3326
+ } catch (error) {
3327
+ onWarning({ severity: "warning", message: `SVG export failed: ${String(error)}`, line: 0 });
3328
+ flashControlLabel(button, "Failed");
3329
+ } finally {
3330
+ diagram.seek(resumeAt / 1e3);
3331
+ if (wasPlaying) diagram.play();
3332
+ }
3333
+ });
3334
+ toolbar.appendChild(button);
3335
+ }
3336
+ function mountShareControl(toolbar) {
3337
+ if (!shareButton) return;
3338
+ const button = makeControlButton("Share", "Copy a share link for this diagram");
3339
+ button.className = "markdy-control-share";
3340
+ button.addEventListener("click", async () => {
3341
+ try {
3342
+ const hash = await compressMarkdyToUrlHash(code);
3343
+ const base = shareUrl ?? MARKDY_PLAYGROUND_URL;
3344
+ await navigator.clipboard.writeText(`${base}#code=${hash}`);
3345
+ flashControlLabel(button, "Copied");
3346
+ } catch (error) {
3347
+ onWarning({ severity: "warning", message: `Share link failed: ${String(error)}`, line: 0 });
3348
+ flashControlLabel(button, "Failed");
3349
+ }
3350
+ });
3351
+ toolbar.appendChild(button);
3352
+ }
3353
+ function mountFitControl(toolbar) {
3354
+ if (!fitViewButton) return;
3355
+ controlsFitButton = makeControlButton("Fit", "Fit all items in view and ignore camera zoom");
3356
+ controlsFitButton.className = "markdy-control-fit";
3357
+ controlsFitButton.setAttribute("aria-pressed", "false");
3358
+ controlsFitButton.addEventListener("click", toggleFitView);
3359
+ toolbar.appendChild(controlsFitButton);
3360
+ }
3361
+ function mountResetViewControl(toolbar) {
3362
+ if (!resetViewButton) return;
3363
+ const button = makeControlButton("Reset", "Reset diagram view");
3364
+ button.className = "markdy-control-reset-view";
3365
+ button.addEventListener("click", resetViewportTransform);
3366
+ toolbar.appendChild(button);
3367
+ }
3368
+ function handleKeyDown(event) {
3369
+ const target = event.target;
3370
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target?.isContentEditable) {
3371
+ return;
3372
+ }
3373
+ switch (event.key) {
3374
+ case "ArrowRight":
3375
+ case "PageDown":
3376
+ event.preventDefault();
3377
+ diagram.nextBeat();
3378
+ break;
3379
+ case "ArrowLeft":
3380
+ case "PageUp":
3381
+ event.preventDefault();
3382
+ diagram.prevBeat();
3383
+ break;
3384
+ case " ":
3385
+ event.preventDefault();
3386
+ togglePlayback();
3387
+ break;
3388
+ case "Home":
3389
+ event.preventDefault();
3390
+ diagram.seek(0);
3391
+ break;
3392
+ default:
3393
+ }
3394
+ }
3146
3395
  function mountControls() {
3147
3396
  const toolbar = document.createElement("div");
3148
3397
  toolbar.className = "markdy-controls";
@@ -3168,53 +3417,44 @@ function createDiagram(opts) {
3168
3417
  for (const eventName of ["click", "dblclick", "pointerdown", "pointermove", "pointerup", "wheel"]) {
3169
3418
  toolbar.addEventListener(eventName, (event) => event.stopPropagation());
3170
3419
  }
3171
- controlsPlayButton = makeControlButton("Play", "Play diagram");
3172
- controlsPlayButton.className = "markdy-control-play";
3173
- controlsPlayButton.addEventListener("click", togglePlayback);
3174
- toolbar.appendChild(controlsPlayButton);
3175
- const restartButton = makeControlButton("Restart", "Restart diagram");
3176
- restartButton.className = "markdy-control-restart";
3177
- restartButton.addEventListener("click", () => {
3178
- diagram.seek(0);
3179
- diagram.play();
3180
- });
3181
- toolbar.appendChild(restartButton);
3182
- controlsRateButtons = [0.5, 1, 2].map((rate) => {
3183
- const button = makeControlButton(`${rate}x`, `Set playback speed to ${rate}x`);
3184
- button.className = "markdy-control-rate";
3185
- button.dataset.rate = String(rate);
3186
- button.setAttribute("aria-pressed", "false");
3187
- button.addEventListener("click", () => diagram.setPlaybackRate(rate));
3188
- toolbar.appendChild(button);
3189
- return button;
3190
- });
3191
- if (interactiveViewport) {
3192
- const resetButton = makeControlButton("Reset", "Reset diagram view");
3193
- resetButton.className = "markdy-control-reset-view";
3194
- resetButton.addEventListener("click", resetViewportTransform);
3195
- toolbar.appendChild(resetButton);
3196
- }
3420
+ mountPlayControl(toolbar);
3421
+ mountBeatNavControls(toolbar, "prev");
3422
+ mountBeatNavControls(toolbar, "next");
3423
+ mountRestartControl(toolbar);
3424
+ mountSeekControl(toolbar);
3425
+ mountSpeedControls(toolbar);
3426
+ mountFitControl(toolbar);
3427
+ mountResetViewControl(toolbar);
3428
+ mountSvgControl(toolbar);
3429
+ mountShareControl(toolbar);
3197
3430
  ensureFooter().insertBefore(toolbar, badge ?? null);
3198
3431
  syncControls();
3199
3432
  }
3200
3433
  viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
3201
3434
  if (interactiveViewport) {
3202
3435
  viewport.style.touchAction = "none";
3203
- viewport.addEventListener("wheel", handleViewportWheel, { passive: false });
3204
- viewport.addEventListener("pointerdown", handleViewportPointerDown);
3205
- viewport.addEventListener("pointermove", handleViewportPointerMove);
3206
- viewport.addEventListener("pointerup", handleViewportPointerEnd);
3207
- viewport.addEventListener("pointercancel", handleViewportPointerEnd);
3208
- viewport.addEventListener("dblclick", handleViewportDoubleClick);
3209
- }
3210
- if (controls) mountControls();
3211
- viewport.addEventListener("click", () => {
3212
- if (suppressNextClick) {
3213
- suppressNextClick = false;
3214
- return;
3215
- }
3216
- togglePlayback();
3217
- });
3436
+ if (allowZoom) viewport.addEventListener("wheel", handleViewportWheel, { passive: false });
3437
+ if (allowPan) {
3438
+ viewport.addEventListener("pointerdown", handleViewportPointerDown);
3439
+ viewport.addEventListener("pointermove", handleViewportPointerMove);
3440
+ viewport.addEventListener("pointerup", handleViewportPointerEnd);
3441
+ viewport.addEventListener("pointercancel", handleViewportPointerEnd);
3442
+ }
3443
+ if (doubleClickToReset) viewport.addEventListener("dblclick", handleViewportDoubleClick);
3444
+ }
3445
+ if (showControls) mountControls();
3446
+ if (clickToPlay) {
3447
+ viewport.addEventListener("click", () => {
3448
+ if (suppressNextClick) {
3449
+ suppressNextClick = false;
3450
+ return;
3451
+ }
3452
+ togglePlayback();
3453
+ });
3454
+ }
3455
+ if (keyboardShortcuts && typeof window !== "undefined") {
3456
+ window.addEventListener("keydown", handleKeyDown);
3457
+ }
3218
3458
  if (autoplay) diagram.play();
3219
3459
  return diagram;
3220
3460
  }
@@ -3410,100 +3650,6 @@ function encodeGifSequence(frames, options = {}) {
3410
3650
  return new Uint8Array(buffer);
3411
3651
  }
3412
3652
 
3413
- // src/export/svg-exporter.ts
3414
- function copyRenderedStyles(source, clone) {
3415
- if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
3416
- const sourceElements = [source, ...Array.from(source.querySelectorAll("*"))];
3417
- const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
3418
- for (let index = 0; index < Math.min(sourceElements.length, cloneElements.length); index++) {
3419
- const computed = window.getComputedStyle(sourceElements[index]);
3420
- const target = cloneElements[index].style;
3421
- for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
3422
- const property = computed.item(propertyIndex);
3423
- target.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
3424
- }
3425
- }
3426
- }
3427
- function normalizeExportViewport(scene) {
3428
- scene.querySelectorAll(".markdy-viewport-transform").forEach((viewportTransform) => {
3429
- viewportTransform.style.transform = "translate(0px, 0px) scale(1)";
3430
- viewportTransform.style.transformOrigin = "0 0";
3431
- viewportTransform.style.willChange = "auto";
3432
- });
3433
- }
3434
- function getDiagramSceneElement(containerEl) {
3435
- const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
3436
- if (!sceneEl) throw new Error("No Markdy scene element found in container");
3437
- return sceneEl;
3438
- }
3439
- function prepareHtmlSceneForExport(sceneEl, options = {}) {
3440
- const clonedScene = sceneEl.cloneNode(true);
3441
- copyRenderedStyles(sceneEl, clonedScene);
3442
- normalizeExportViewport(clonedScene);
3443
- const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
3444
- const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
3445
- let width = parseFloat(widthStr);
3446
- let height = parseFloat(heightStr);
3447
- if (isNaN(width)) width = 800;
3448
- if (isNaN(height)) height = 400;
3449
- const scale = options.scale || 1;
3450
- const scaledWidth = width * scale;
3451
- const scaledHeight = height * scale;
3452
- clonedScene.style.transform = `scale(${scale})`;
3453
- clonedScene.style.transformOrigin = "0 0";
3454
- clonedScene.style.position = "relative";
3455
- clonedScene.style.left = "0px";
3456
- clonedScene.style.top = "0px";
3457
- clonedScene.style.margin = "0";
3458
- clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
3459
- if (options.transparentBackground) {
3460
- clonedScene.style.background = "transparent";
3461
- }
3462
- return { sceneEl, clonedScene, width, height, scaledWidth, scaledHeight };
3463
- }
3464
- function exportDiagramAsVectorSvg(containerEl, options = {}) {
3465
- const sceneEl = getDiagramSceneElement(containerEl);
3466
- if (sceneEl.tagName?.toLowerCase() === "svg") {
3467
- const clonedSvg = sceneEl.cloneNode(true);
3468
- if (!clonedSvg.getAttribute("xmlns")) {
3469
- clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
3470
- }
3471
- const serializer2 = new XMLSerializer();
3472
- return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
3473
- ${serializer2.serializeToString(clonedSvg)}`;
3474
- }
3475
- const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
3476
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3477
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
3478
- svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
3479
- svg.setAttribute("width", String(scaledWidth));
3480
- svg.setAttribute("height", String(scaledHeight));
3481
- svg.setAttribute("viewBox", `0 0 ${scaledWidth} ${scaledHeight}`);
3482
- if (options.includeThemeStyles !== false) {
3483
- const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
3484
- let combinedStyles = `
3485
- foreignObject { width: 100%; height: 100%; }
3486
- .markdy-node { transition: opacity 0.3s ease; }
3487
- `;
3488
- if (typeof document !== "undefined") {
3489
- const styles = document.querySelectorAll("style[id^='markdy-']");
3490
- for (let i = 0; i < styles.length; i++) {
3491
- combinedStyles += styles[i].textContent + "\n";
3492
- }
3493
- }
3494
- styleEl.textContent = combinedStyles;
3495
- svg.appendChild(styleEl);
3496
- }
3497
- const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
3498
- foreignObject.setAttribute("width", "100%");
3499
- foreignObject.setAttribute("height", "100%");
3500
- foreignObject.appendChild(clonedScene);
3501
- svg.appendChild(foreignObject);
3502
- const serializer = new XMLSerializer();
3503
- return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
3504
- ` + serializer.serializeToString(svg);
3505
- }
3506
-
3507
3653
  // src/export/inline-resources.ts
3508
3654
  var TRANSPARENT_PIXEL_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
3509
3655
  var RESOURCE_FETCH_TIMEOUT_MS = 3e3;
@@ -0,0 +1,10 @@
1
+ import {
2
+ exportDiagramAsVectorSvg,
3
+ getDiagramSceneElement,
4
+ prepareHtmlSceneForExport
5
+ } from "./chunk-VIZHA7GI.js";
6
+ export {
7
+ exportDiagramAsVectorSvg,
8
+ getDiagramSceneElement,
9
+ prepareHtmlSceneForExport
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Browser renderer for diagram-native animated MarkdyScript architecture diagrams, built on the Web Animations API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,14 +45,14 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "html2canvas": "^1.4.1",
48
- "@markdy/core": "1.0.10"
48
+ "@markdy/core": "1.0.11"
49
49
  },
50
50
  "devDependencies": {
51
51
  "jsdom": "^29.1.1",
52
52
  "tsup": "^8.5.1",
53
53
  "typescript": "^5.9.3",
54
54
  "vitest": "^4.1.7",
55
- "@markdy/stdlib-systems": "1.0.10"
55
+ "@markdy/stdlib-systems": "1.0.11"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",