sketchmark 1.1.2 → 1.1.4

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.
@@ -8808,6 +8808,7 @@ var AIDiagram = (function (exports) {
8808
8808
  }, delayMs);
8809
8809
  }
8810
8810
  const NODE_DRAW_GUIDE_ATTR = "data-node-draw-guide";
8811
+ const TEXT_REVEAL_CLIP_ATTR = "data-text-reveal-clip-id";
8811
8812
  const GUIDED_NODE_SHAPES = new Set([
8812
8813
  "box",
8813
8814
  "circle",
@@ -8936,6 +8937,7 @@ var AIDiagram = (function (exports) {
8936
8937
  });
8937
8938
  const text = nodeText(el);
8938
8939
  if (text) {
8940
+ clearTextReveal(text);
8939
8941
  text.style.opacity = text.style.transition = "";
8940
8942
  }
8941
8943
  }
@@ -8981,55 +8983,74 @@ var AIDiagram = (function (exports) {
8981
8983
  clearNodeDrawStyles(el);
8982
8984
  }
8983
8985
  // ── Text writing reveal (clipPath) ───────────────────────
8986
+ function clearTextReveal(textEl, clipId) {
8987
+ const activeClipId = textEl.getAttribute(TEXT_REVEAL_CLIP_ATTR);
8988
+ const shouldClearCurrentClip = !clipId || activeClipId === clipId;
8989
+ if (shouldClearCurrentClip) {
8990
+ textEl.removeAttribute("clip-path");
8991
+ textEl.removeAttribute(TEXT_REVEAL_CLIP_ATTR);
8992
+ }
8993
+ const clipIdToRemove = clipId ?? activeClipId;
8994
+ if (clipIdToRemove) {
8995
+ textEl.ownerSVGElement?.querySelector(`#${clipIdToRemove}`)?.remove();
8996
+ }
8997
+ }
8984
8998
  function animateTextReveal(textEl, delayMs, durationMs = ANIMATION.textRevealMs) {
8985
8999
  const ownerSvg = textEl.ownerSVGElement;
9000
+ clearTextReveal(textEl);
8986
9001
  if (!ownerSvg) {
8987
9002
  // fallback: just fade
8988
9003
  textEl.style.transition = `opacity ${ANIMATION.textFade}ms ease ${delayMs}ms`;
8989
9004
  textEl.style.opacity = "1";
8990
9005
  return;
8991
9006
  }
8992
- // Make text visible but clipped to zero width
9007
+ const bbox = textEl.getBBox?.();
9008
+ if (!bbox || bbox.width === 0) {
9009
+ // fallback if can't measure
9010
+ textEl.style.transition = `opacity ${ANIMATION.textFade}ms ease ${delayMs}ms`;
9011
+ textEl.style.opacity = "1";
9012
+ return;
9013
+ }
9014
+ let defs = ownerSvg.querySelector("defs");
9015
+ if (!defs) {
9016
+ defs = document.createElementNS(SVG_NS$1, "defs");
9017
+ ownerSvg.insertBefore(defs, ownerSvg.firstChild);
9018
+ }
9019
+ const clipId = `skm-clip-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
9020
+ const clipPath = document.createElementNS(SVG_NS$1, "clipPath");
9021
+ clipPath.setAttribute("id", clipId);
9022
+ const rect = document.createElementNS(SVG_NS$1, "rect");
9023
+ rect.setAttribute("x", String(bbox.x - 2));
9024
+ rect.setAttribute("y", String(bbox.y - 2));
9025
+ rect.setAttribute("width", "0");
9026
+ rect.setAttribute("height", String(bbox.height + 4));
9027
+ clipPath.appendChild(rect);
9028
+ defs.appendChild(clipPath);
9029
+ textEl.setAttribute("clip-path", `url(#${clipId})`);
9030
+ textEl.setAttribute(TEXT_REVEAL_CLIP_ATTR, clipId);
8993
9031
  textEl.style.opacity = "1";
8994
- // We need to wait for text to be visible before we can measure it
9032
+ requestAnimationFrame(() => requestAnimationFrame(() => {
9033
+ rect.style.transition = `width ${durationMs}ms cubic-bezier(.4,0,.2,1) ${delayMs}ms`;
9034
+ rect.setAttribute("width", String(bbox.width + 4));
9035
+ }));
9036
+ // Cleanup after animation
8995
9037
  setTimeout(() => {
8996
- const bbox = textEl.getBBox?.();
8997
- if (!bbox || bbox.width === 0) {
8998
- // fallback if can't measure
8999
- return;
9000
- }
9001
- let defs = ownerSvg.querySelector("defs");
9002
- if (!defs) {
9003
- defs = document.createElementNS(SVG_NS$1, "defs");
9004
- ownerSvg.insertBefore(defs, ownerSvg.firstChild);
9005
- }
9006
- const clipId = `skm-clip-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
9007
- const clipPath = document.createElementNS(SVG_NS$1, "clipPath");
9008
- clipPath.setAttribute("id", clipId);
9009
- const rect = document.createElementNS(SVG_NS$1, "rect");
9010
- rect.setAttribute("x", String(bbox.x - 2));
9011
- rect.setAttribute("y", String(bbox.y - 2));
9012
- rect.setAttribute("width", "0");
9013
- rect.setAttribute("height", String(bbox.height + 4));
9014
- clipPath.appendChild(rect);
9015
- defs.appendChild(clipPath);
9016
- textEl.setAttribute("clip-path", `url(#${clipId})`);
9017
- requestAnimationFrame(() => requestAnimationFrame(() => {
9018
- rect.style.transition = `width ${durationMs}ms cubic-bezier(.4,0,.2,1)`;
9019
- rect.setAttribute("width", String(bbox.width + 4));
9020
- }));
9021
- // Cleanup after animation
9022
- setTimeout(() => {
9023
- textEl.removeAttribute("clip-path");
9024
- clipPath.remove();
9025
- }, durationMs + 50);
9026
- }, delayMs);
9038
+ clearTextReveal(textEl, clipId);
9039
+ }, delayMs + durationMs + 50);
9027
9040
  }
9028
- function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
9041
+ function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur, textOnlyDur = ANIMATION.textRevealMs) {
9029
9042
  showDrawEl(el);
9030
9043
  const guide = nodeGuidePathEl(el);
9031
9044
  if (!guide) {
9032
9045
  const firstPath = el.querySelector("path");
9046
+ const text = nodeText(el);
9047
+ if (!firstPath && el.dataset.nodeShape === "text" && text) {
9048
+ animateTextReveal(text, 0, textOnlyDur);
9049
+ setTimeout(() => {
9050
+ clearNodeDrawStyles(el);
9051
+ }, textOnlyDur + 80);
9052
+ return;
9053
+ }
9033
9054
  if (!firstPath?.style.strokeDasharray)
9034
9055
  prepareForDraw(el);
9035
9056
  animateShapeDraw(el, strokeDur, ANIMATION.nodeStagger);
@@ -9249,11 +9270,13 @@ var AIDiagram = (function (exports) {
9249
9270
  this._config = _config;
9250
9271
  this._step = -1;
9251
9272
  this._pendingStepTimers = new Set();
9273
+ this._pendingNarrationTimers = new Set();
9252
9274
  this._transforms = new Map();
9253
9275
  this._listeners = [];
9254
9276
  // ── Narration caption ──
9255
9277
  this._captionEl = null;
9256
9278
  this._captionTextEl = null;
9279
+ this._narrationRunId = 0;
9257
9280
  // ── Annotations ──
9258
9281
  this._annotationLayer = null;
9259
9282
  this._annotations = [];
@@ -9516,20 +9539,30 @@ var AIDiagram = (function (exports) {
9516
9539
  }
9517
9540
  this.emit("step-change");
9518
9541
  }
9542
+ _clearTimerBucket(bucket) {
9543
+ bucket.forEach((id) => window.clearTimeout(id));
9544
+ bucket.clear();
9545
+ }
9519
9546
  _clearPendingStepTimers() {
9520
- this._pendingStepTimers.forEach((id) => window.clearTimeout(id));
9521
- this._pendingStepTimers.clear();
9547
+ this._clearTimerBucket(this._pendingStepTimers);
9522
9548
  }
9523
- _scheduleStep(fn, delayMs) {
9549
+ _cancelNarrationTyping() {
9550
+ this._narrationRunId += 1;
9551
+ this._clearTimerBucket(this._pendingNarrationTimers);
9552
+ }
9553
+ _scheduleTimer(fn, delayMs, bucket = this._pendingStepTimers) {
9524
9554
  if (delayMs <= 0) {
9525
9555
  fn();
9526
9556
  return;
9527
9557
  }
9528
9558
  const id = window.setTimeout(() => {
9529
- this._pendingStepTimers.delete(id);
9559
+ bucket.delete(id);
9530
9560
  fn();
9531
9561
  }, delayMs);
9532
- this._pendingStepTimers.add(id);
9562
+ bucket.add(id);
9563
+ }
9564
+ _scheduleStep(fn, delayMs) {
9565
+ this._scheduleTimer(fn, delayMs, this._pendingStepTimers);
9533
9566
  }
9534
9567
  _stepWaitMs(step, fallbackMs) {
9535
9568
  const delay = Math.max(0, step.delay ?? 0);
@@ -9573,6 +9606,7 @@ var AIDiagram = (function (exports) {
9573
9606
  }
9574
9607
  _clearAll() {
9575
9608
  this._clearPendingStepTimers();
9609
+ this._cancelNarrationTyping();
9576
9610
  this._cancelSpeech();
9577
9611
  this._transforms.clear();
9578
9612
  // Nodes
@@ -10037,7 +10071,7 @@ var AIDiagram = (function (exports) {
10037
10071
  if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
10038
10072
  prepareNodeForDraw(nodeEl);
10039
10073
  }
10040
- animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur);
10074
+ animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
10041
10075
  }
10042
10076
  }
10043
10077
  // ── erase ─────────────────────────────────────────────────
@@ -10127,6 +10161,7 @@ var AIDiagram = (function (exports) {
10127
10161
  _doNarrate(text, silent) {
10128
10162
  if (!this._captionEl || !this._captionTextEl)
10129
10163
  return;
10164
+ this._cancelNarrationTyping();
10130
10165
  this._captionEl.style.opacity = "1";
10131
10166
  if (silent || !text) {
10132
10167
  this._captionTextEl.textContent = text;
@@ -10137,12 +10172,16 @@ var AIDiagram = (function (exports) {
10137
10172
  this._speak(text);
10138
10173
  // Typing effect
10139
10174
  this._captionTextEl.textContent = "";
10175
+ const narrationRunId = this._narrationRunId;
10140
10176
  let charIdx = 0;
10141
10177
  const typeNext = () => {
10178
+ if (this._narrationRunId !== narrationRunId || !this._captionTextEl)
10179
+ return;
10142
10180
  if (charIdx < text.length) {
10143
10181
  this._captionTextEl.textContent += text[charIdx++];
10144
- const id = window.setTimeout(typeNext, ANIMATION.narrationTypeMs);
10145
- this._pendingStepTimers.add(id);
10182
+ if (charIdx < text.length) {
10183
+ this._scheduleTimer(typeNext, ANIMATION.narrationTypeMs, this._pendingNarrationTimers);
10184
+ }
10146
10185
  }
10147
10186
  };
10148
10187
  typeNext();
@@ -10252,12 +10291,11 @@ var AIDiagram = (function (exports) {
10252
10291
  requestAnimationFrame(animate);
10253
10292
  }
10254
10293
  // After guide finishes: reveal rough.js element, remove guide
10255
- const id = window.setTimeout(() => {
10294
+ this._scheduleTimer(() => {
10256
10295
  roughEl.style.transition = `opacity 120ms ease`;
10257
10296
  roughEl.style.opacity = "1";
10258
10297
  guide.remove();
10259
10298
  }, dur + 30);
10260
- this._pendingStepTimers.add(id);
10261
10299
  }));
10262
10300
  }
10263
10301
  _doAnnotationCircle(target, silent) {
@@ -11834,6 +11872,7 @@ var AIDiagram = (function (exports) {
11834
11872
 
11835
11873
  .skm-embed__controls {
11836
11874
  display: flex;
11875
+ flex-wrap: wrap;
11837
11876
  align-items: center;
11838
11877
  gap: 8px;
11839
11878
  padding: 10px 12px;
@@ -11851,6 +11890,13 @@ var AIDiagram = (function (exports) {
11851
11890
  display: none;
11852
11891
  }
11853
11892
 
11893
+ .skm-embed__controls-group {
11894
+ display: flex;
11895
+ align-items: center;
11896
+ gap: 8px;
11897
+ flex-wrap: wrap;
11898
+ }
11899
+
11854
11900
  .skm-embed__button {
11855
11901
  border: 1px solid #caba98;
11856
11902
  background: #f5eedd;
@@ -11886,6 +11932,17 @@ var AIDiagram = (function (exports) {
11886
11932
  cursor: default;
11887
11933
  }
11888
11934
 
11935
+ .skm-embed__zoom {
11936
+ min-width: 48px;
11937
+ text-align: center;
11938
+ color: #8a6040;
11939
+ font-size: 11px;
11940
+ }
11941
+
11942
+ .skm-embed--dark .skm-embed__zoom {
11943
+ color: #d0b176;
11944
+ }
11945
+
11889
11946
  .skm-embed__step {
11890
11947
  margin-left: auto;
11891
11948
  min-width: 96px;
@@ -11904,8 +11961,10 @@ var AIDiagram = (function (exports) {
11904
11961
  this.emitter = new EventEmitter();
11905
11962
  this.animUnsub = null;
11906
11963
  this.playInFlight = false;
11964
+ this.zoom = 1;
11907
11965
  this.offsetX = 0;
11908
11966
  this.offsetY = 0;
11967
+ this.autoFitEnabled = true;
11909
11968
  this.motionFrame = null;
11910
11969
  this.resizeObserver = null;
11911
11970
  this.options = options;
@@ -11926,10 +11985,18 @@ var AIDiagram = (function (exports) {
11926
11985
  </div>
11927
11986
  <div class="skm-embed__error"></div>
11928
11987
  <div class="skm-embed__controls">
11929
- <button type="button" class="skm-embed__button" data-action="reset">Reset</button>
11930
- <button type="button" class="skm-embed__button" data-action="prev">Prev</button>
11931
- <button type="button" class="skm-embed__button" data-action="next">Next</button>
11932
- <button type="button" class="skm-embed__button" data-action="play">Play</button>
11988
+ <div class="skm-embed__controls-group">
11989
+ <button type="button" class="skm-embed__button" data-action="zoom-out">-</button>
11990
+ <span class="skm-embed__zoom">100%</span>
11991
+ <button type="button" class="skm-embed__button" data-action="zoom-in">+</button>
11992
+ <button type="button" class="skm-embed__button" data-action="fit">Reset</button>
11993
+ </div>
11994
+ <div class="skm-embed__controls-group">
11995
+ <button type="button" class="skm-embed__button" data-action="restart">Restart</button>
11996
+ <button type="button" class="skm-embed__button" data-action="prev">Prev</button>
11997
+ <button type="button" class="skm-embed__button" data-action="next">Next</button>
11998
+ <button type="button" class="skm-embed__button" data-action="play">Play</button>
11999
+ </div>
11933
12000
  <span class="skm-embed__step">No steps</span>
11934
12001
  </div>
11935
12002
  `;
@@ -11939,12 +12006,19 @@ var AIDiagram = (function (exports) {
11939
12006
  this.errorElement = this.root.querySelector(".skm-embed__error");
11940
12007
  this.controlsElement = this.root.querySelector(".skm-embed__controls");
11941
12008
  this.stepInfoElement = this.root.querySelector(".skm-embed__step");
11942
- this.btnReset = this.root.querySelector('[data-action="reset"]');
12009
+ this.zoomInfoElement = this.root.querySelector(".skm-embed__zoom");
12010
+ this.btnFit = this.root.querySelector('[data-action="fit"]');
12011
+ this.btnZoomIn = this.root.querySelector('[data-action="zoom-in"]');
12012
+ this.btnZoomOut = this.root.querySelector('[data-action="zoom-out"]');
12013
+ this.btnRestart = this.root.querySelector('[data-action="restart"]');
11943
12014
  this.btnPrev = this.root.querySelector('[data-action="prev"]');
11944
12015
  this.btnNext = this.root.querySelector('[data-action="next"]');
11945
12016
  this.btnPlay = this.root.querySelector('[data-action="play"]');
11946
12017
  this.controlsElement.classList.toggle("is-hidden", options.showControls === false);
11947
- this.btnReset.addEventListener("click", () => this.resetAnimation());
12018
+ this.btnFit.addEventListener("click", () => this.resetView());
12019
+ this.btnZoomIn.addEventListener("click", () => this.zoomIn());
12020
+ this.btnZoomOut.addEventListener("click", () => this.zoomOut());
12021
+ this.btnRestart.addEventListener("click", () => this.resetAnimation());
11948
12022
  this.btnPrev.addEventListener("click", () => this.prevStep());
11949
12023
  this.btnNext.addEventListener("click", () => this.nextStep());
11950
12024
  this.btnPlay.addEventListener("click", () => {
@@ -11990,7 +12064,12 @@ var AIDiagram = (function (exports) {
11990
12064
  this.animUnsub = null;
11991
12065
  this.instance?.anim?.destroy();
11992
12066
  this.instance = null;
12067
+ this.autoFitEnabled = true;
12068
+ this.zoom = 1;
12069
+ this.offsetX = 0;
12070
+ this.offsetY = 0;
11993
12071
  this.diagramWrap.innerHTML = "";
12072
+ this.applyTransform();
11994
12073
  try {
11995
12074
  const instance = render({
11996
12075
  container: this.diagramWrap,
@@ -12071,6 +12150,21 @@ var AIDiagram = (function (exports) {
12071
12150
  this.syncControls();
12072
12151
  this.positionViewport(true);
12073
12152
  }
12153
+ fitToViewport(animated = false) {
12154
+ if (!this.instance?.svg)
12155
+ return;
12156
+ this.autoFitEnabled = true;
12157
+ this.positionViewport(animated);
12158
+ }
12159
+ resetView(animated = false) {
12160
+ this.fitToViewport(animated);
12161
+ }
12162
+ zoomIn() {
12163
+ this.zoomAroundViewportCenter(1.2);
12164
+ }
12165
+ zoomOut() {
12166
+ this.zoomAroundViewportCenter(0.8);
12167
+ }
12074
12168
  exportSVG(filename) {
12075
12169
  this.instance?.exportSVG(filename);
12076
12170
  }
@@ -12093,10 +12187,14 @@ var AIDiagram = (function (exports) {
12093
12187
  return typeof value === "number" ? `${value}px` : value;
12094
12188
  }
12095
12189
  syncControls() {
12190
+ this.syncAnimationControls();
12191
+ this.syncViewControls();
12192
+ }
12193
+ syncAnimationControls() {
12096
12194
  const anim = this.instance?.anim;
12097
12195
  if (!anim || !anim.total) {
12098
12196
  this.stepInfoElement.textContent = "No steps";
12099
- this.btnReset.disabled = true;
12197
+ this.btnRestart.disabled = true;
12100
12198
  this.btnPrev.disabled = true;
12101
12199
  this.btnNext.disabled = true;
12102
12200
  this.btnPlay.disabled = true;
@@ -12104,56 +12202,69 @@ var AIDiagram = (function (exports) {
12104
12202
  }
12105
12203
  this.stepInfoElement.textContent =
12106
12204
  anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
12107
- this.btnReset.disabled = false;
12205
+ this.btnRestart.disabled = false;
12108
12206
  this.btnPrev.disabled = !anim.canPrev;
12109
12207
  this.btnNext.disabled = !anim.canNext;
12110
12208
  this.btnPlay.disabled = this.playInFlight || !anim.canNext;
12111
12209
  }
12210
+ syncViewControls() {
12211
+ const hasView = !!this.instance?.svg;
12212
+ const zoomMin = this.getZoomMin();
12213
+ const zoomMax = this.getZoomMax();
12214
+ this.zoomInfoElement.textContent = `${Math.round(this.zoom * 100)}%`;
12215
+ this.btnFit.disabled = !hasView;
12216
+ this.btnZoomOut.disabled = !hasView || this.zoom <= zoomMin + 0.001;
12217
+ this.btnZoomIn.disabled = !hasView || this.zoom >= zoomMax - 0.001;
12218
+ }
12112
12219
  positionViewport(animated) {
12113
- if (!this.instance?.svg)
12114
- return;
12115
- const svg = this.instance.svg;
12116
- const svgWidth = parseFloat(svg.getAttribute("width") || "0");
12117
- const svgHeight = parseFloat(svg.getAttribute("height") || "0");
12118
- if (!svgWidth || !svgHeight)
12220
+ const size = this.getContentSize();
12221
+ if (!size)
12119
12222
  return;
12223
+ const { width: svgWidth, height: svgHeight } = size;
12120
12224
  const viewportRect = this.viewport.getBoundingClientRect();
12121
12225
  const viewWidth = viewportRect.width || this.viewport.clientWidth;
12122
12226
  const viewHeight = viewportRect.height || this.viewport.clientHeight;
12123
12227
  if (!viewWidth || !viewHeight)
12124
12228
  return;
12125
- const sceneIsLarge = svgWidth > viewWidth || svgHeight > viewHeight;
12229
+ if (this.autoFitEnabled) {
12230
+ this.zoom = this.getFitZoom(svgWidth, svgHeight, viewWidth, viewHeight);
12231
+ }
12232
+ this.syncViewControls();
12233
+ const scaledWidth = svgWidth * this.zoom;
12234
+ const scaledHeight = svgHeight * this.zoom;
12235
+ const focusTarget = this.getFocusTarget();
12236
+ const sceneIsLarge = scaledWidth > viewWidth || scaledHeight > viewHeight;
12126
12237
  const shouldFocus = sceneIsLarge &&
12127
12238
  this.options.autoFocus !== false &&
12128
- !!this.getFocusTarget();
12239
+ !!focusTarget;
12129
12240
  if (!shouldFocus) {
12130
- this.animateTo(svgWidth <= viewWidth ? (viewWidth - svgWidth) / 2 : 0, svgHeight <= viewHeight ? (viewHeight - svgHeight) / 2 : 0, animated);
12241
+ this.animateTo(scaledWidth <= viewWidth ? (viewWidth - scaledWidth) / 2 : 0, scaledHeight <= viewHeight ? (viewHeight - scaledHeight) / 2 : 0, animated);
12131
12242
  return;
12132
12243
  }
12133
- const target = this.findTargetElement(this.getFocusTarget());
12244
+ const target = this.findTargetElement(focusTarget);
12134
12245
  if (!target) {
12135
12246
  this.animateTo(0, 0, animated);
12136
12247
  return;
12137
12248
  }
12138
- const currentRect = target.getBoundingClientRect();
12139
- const sceneX = currentRect.left - viewportRect.left - this.offsetX;
12140
- const sceneY = currentRect.top - viewportRect.top - this.offsetY;
12141
- const targetCenterX = sceneX + currentRect.width / 2;
12142
- const targetCenterY = sceneY + currentRect.height / 2;
12143
- let nextX = viewWidth / 2 - targetCenterX;
12144
- let nextY = viewHeight / 2 - targetCenterY;
12249
+ const targetBox = this.getTargetBox(target, viewportRect);
12250
+ if (!targetBox) {
12251
+ this.animateTo(0, 0, animated);
12252
+ return;
12253
+ }
12254
+ let nextX = viewWidth / 2 - targetBox.centerX;
12255
+ let nextY = viewHeight / 2 - targetBox.centerY;
12145
12256
  const padding = this.options.focusPadding ?? 24;
12146
- if (svgWidth <= viewWidth) {
12147
- nextX = (viewWidth - svgWidth) / 2;
12257
+ if (scaledWidth <= viewWidth) {
12258
+ nextX = (viewWidth - scaledWidth) / 2;
12148
12259
  }
12149
12260
  else {
12150
- nextX = clamp(nextX, viewWidth - svgWidth - padding, padding);
12261
+ nextX = clamp(nextX, viewWidth - scaledWidth - padding, padding);
12151
12262
  }
12152
- if (svgHeight <= viewHeight) {
12153
- nextY = (viewHeight - svgHeight) / 2;
12263
+ if (scaledHeight <= viewHeight) {
12264
+ nextY = (viewHeight - scaledHeight) / 2;
12154
12265
  }
12155
12266
  else {
12156
- nextY = clamp(nextY, viewHeight - svgHeight - padding, padding);
12267
+ nextY = clamp(nextY, viewHeight - scaledHeight - padding, padding);
12157
12268
  }
12158
12269
  this.animateTo(nextX, nextY, animated);
12159
12270
  }
@@ -12185,7 +12296,78 @@ var AIDiagram = (function (exports) {
12185
12296
  this.motionFrame = requestAnimationFrame(frame);
12186
12297
  }
12187
12298
  applyTransform() {
12188
- this.world.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px)`;
12299
+ this.world.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.zoom})`;
12300
+ this.zoomInfoElement.textContent = `${Math.round(this.zoom * 100)}%`;
12301
+ }
12302
+ getContentSize() {
12303
+ if (!this.instance?.svg)
12304
+ return null;
12305
+ const svg = this.instance.svg;
12306
+ const width = parseFloat(svg.getAttribute("width") || "0");
12307
+ const height = parseFloat(svg.getAttribute("height") || "0");
12308
+ if (!width || !height)
12309
+ return null;
12310
+ return { width, height };
12311
+ }
12312
+ getFitZoom(svgWidth, svgHeight, viewWidth, viewHeight) {
12313
+ const padding = this.getFitPadding(viewWidth, viewHeight);
12314
+ const availableWidth = Math.max(viewWidth - padding * 2, 1);
12315
+ const availableHeight = Math.max(viewHeight - padding * 2, 1);
12316
+ const nextZoom = Math.min(availableWidth / svgWidth, availableHeight / svgHeight, 1);
12317
+ return clamp(nextZoom || 1, this.getZoomMin(), this.getZoomMax());
12318
+ }
12319
+ getFitPadding(viewWidth, viewHeight) {
12320
+ if (typeof this.options.fitPadding === "number") {
12321
+ return Math.max(0, this.options.fitPadding);
12322
+ }
12323
+ return Math.max(16, Math.min(40, Math.round(Math.min(viewWidth, viewHeight) * 0.08)));
12324
+ }
12325
+ getZoomMin() {
12326
+ return this.options.zoomMin ?? 0.08;
12327
+ }
12328
+ getZoomMax() {
12329
+ return this.options.zoomMax ?? 4;
12330
+ }
12331
+ zoomAroundViewportCenter(factor) {
12332
+ if (!this.instance?.svg)
12333
+ return;
12334
+ const pivotX = this.viewport.clientWidth / 2;
12335
+ const pivotY = this.viewport.clientHeight / 2;
12336
+ this.zoomTo(this.zoom * factor, pivotX, pivotY);
12337
+ }
12338
+ zoomTo(nextZoom, pivotX, pivotY) {
12339
+ const clampedZoom = clamp(nextZoom, this.getZoomMin(), this.getZoomMax());
12340
+ const ratio = clampedZoom / this.zoom;
12341
+ if (!Number.isFinite(ratio) || ratio === 1) {
12342
+ this.syncViewControls();
12343
+ return;
12344
+ }
12345
+ this.stopMotion();
12346
+ this.autoFitEnabled = false;
12347
+ this.offsetX = pivotX - (pivotX - this.offsetX) * ratio;
12348
+ this.offsetY = pivotY - (pivotY - this.offsetY) * ratio;
12349
+ this.zoom = clampedZoom;
12350
+ this.applyTransform();
12351
+ this.syncViewControls();
12352
+ }
12353
+ getTargetBox(target, viewportRect) {
12354
+ if (target instanceof SVGGraphicsElement) {
12355
+ try {
12356
+ const bounds = target.getBBox();
12357
+ return {
12358
+ centerX: (bounds.x + bounds.width / 2) * this.zoom,
12359
+ centerY: (bounds.y + bounds.height / 2) * this.zoom,
12360
+ };
12361
+ }
12362
+ catch {
12363
+ // Ignore and fall back to layout-based bounds below.
12364
+ }
12365
+ }
12366
+ const currentRect = target.getBoundingClientRect();
12367
+ return {
12368
+ centerX: currentRect.left - viewportRect.left - this.offsetX + currentRect.width / 2,
12369
+ centerY: currentRect.top - viewportRect.top - this.offsetY + currentRect.height / 2,
12370
+ };
12189
12371
  }
12190
12372
  getFocusTarget() {
12191
12373
  const anim = this.instance?.anim;
@@ -12,6 +12,9 @@ export interface SketchmarkEmbedOptions {
12
12
  theme?: EmbedTheme;
13
13
  showControls?: boolean;
14
14
  playStepDelay?: number;
15
+ fitPadding?: number;
16
+ zoomMin?: number;
17
+ zoomMax?: number;
15
18
  focusPadding?: number;
16
19
  focusDuration?: number;
17
20
  autoFocus?: boolean;
@@ -43,19 +46,25 @@ export declare class SketchmarkEmbed {
43
46
  readonly errorElement: HTMLDivElement;
44
47
  readonly controlsElement: HTMLDivElement;
45
48
  readonly stepInfoElement: HTMLSpanElement;
49
+ readonly zoomInfoElement: HTMLSpanElement;
46
50
  instance: DiagramInstance | null;
47
51
  private readonly emitter;
48
52
  private readonly options;
49
- private readonly btnReset;
53
+ private readonly btnRestart;
50
54
  private readonly btnPrev;
51
55
  private readonly btnNext;
52
56
  private readonly btnPlay;
57
+ private readonly btnFit;
58
+ private readonly btnZoomIn;
59
+ private readonly btnZoomOut;
53
60
  private animUnsub;
54
61
  private playInFlight;
55
62
  private dsl;
56
63
  private theme;
64
+ private zoom;
57
65
  private offsetX;
58
66
  private offsetY;
67
+ private autoFitEnabled;
59
68
  private motionFrame;
60
69
  private resizeObserver;
61
70
  constructor(options: SketchmarkEmbedOptions);
@@ -69,15 +78,29 @@ export declare class SketchmarkEmbed {
69
78
  nextStep(): void;
70
79
  prevStep(): void;
71
80
  resetAnimation(): void;
81
+ fitToViewport(animated?: boolean): void;
82
+ resetView(animated?: boolean): void;
83
+ zoomIn(): void;
84
+ zoomOut(): void;
72
85
  exportSVG(filename?: string): void;
73
86
  exportPNG(filename?: string): Promise<void>;
74
87
  destroy(): void;
75
88
  private applySize;
76
89
  private formatSize;
77
90
  private syncControls;
91
+ private syncAnimationControls;
92
+ private syncViewControls;
78
93
  private positionViewport;
79
94
  private animateTo;
80
95
  private applyTransform;
96
+ private getContentSize;
97
+ private getFitZoom;
98
+ private getFitPadding;
99
+ private getZoomMin;
100
+ private getZoomMax;
101
+ private zoomAroundViewportCenter;
102
+ private zoomTo;
103
+ private getTargetBox;
81
104
  private getFocusTarget;
82
105
  private findTargetElement;
83
106
  private getStepTarget;
@@ -1 +1 @@
1
- {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/ui/embed.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/ui/embed.ts"],"names":[],"mappings":"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"}