sketchmark 1.1.0 → 1.1.2

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.

Potentially problematic release.


This version of sketchmark might be problematic. Click here for more details.

package/dist/index.cjs CHANGED
@@ -10567,13 +10567,13 @@ function exportHTML(svg, dslSource, opts = {}) {
10567
10567
  </head>
10568
10568
  <body>
10569
10569
  <div class="diagram">${svgStr}</div>
10570
- <details class="dsl"><summary style="cursor:pointer;color:#f0c96a">DSL source</summary><pre>${escapeHtml(dslSource)}</pre></details>
10570
+ <details class="dsl"><summary style="cursor:pointer;color:#f0c96a">DSL source</summary><pre>${escapeHtml$1(dslSource)}</pre></details>
10571
10571
  </body>
10572
10572
  </html>`;
10573
10573
  const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
10574
10574
  download(blob, opts.filename ?? 'diagram.html');
10575
10575
  }
10576
- function escapeHtml(s) {
10576
+ function escapeHtml$1(s) {
10577
10577
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
10578
10578
  }
10579
10579
  // ── GIF stub (requires gifshot or gif.js at runtime) ──────
@@ -10684,22 +10684,16 @@ class EventEmitter {
10684
10684
  }
10685
10685
  }
10686
10686
 
10687
- // ============================================================
10688
- // sketchmark — Public API
10689
- // ============================================================
10690
- // ── Core Pipeline ─────────────────────────────────────────
10691
10687
  function render(options) {
10692
- const { container: rawContainer, dsl, renderer = 'svg', injectCSS = true, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
10693
- // Inject animation CSS once
10694
- if (injectCSS && !document.getElementById('ai-diagram-css')) {
10695
- const style = document.createElement('style');
10696
- style.id = 'ai-diagram-css';
10688
+ const { container: rawContainer, dsl, renderer = "svg", injectCSS = true, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
10689
+ if (injectCSS && !document.getElementById("ai-diagram-css")) {
10690
+ const style = document.createElement("style");
10691
+ style.id = "ai-diagram-css";
10697
10692
  style.textContent = ANIMATION_CSS;
10698
10693
  document.head.appendChild(style);
10699
10694
  }
10700
- // Resolve container
10701
10695
  let el;
10702
- if (typeof rawContainer === 'string') {
10696
+ if (typeof rawContainer === "string") {
10703
10697
  el = document.querySelector(rawContainer);
10704
10698
  if (!el)
10705
10699
  throw new Error(`Container "${rawContainer}" not found`);
@@ -10707,19 +10701,22 @@ function render(options) {
10707
10701
  else {
10708
10702
  el = rawContainer;
10709
10703
  }
10710
- // Pipeline: DSL → AST → Scene → Layout → Render
10711
10704
  const ast = parse(dsl);
10712
10705
  const scene = buildSceneGraph(ast);
10713
10706
  layout(scene);
10714
10707
  let svg;
10715
10708
  let canvas;
10716
10709
  let anim;
10717
- if (renderer === 'canvas') {
10710
+ if (renderer === "canvas") {
10718
10711
  canvas = el instanceof HTMLCanvasElement
10719
10712
  ? el
10720
- : (() => { const c = document.createElement('canvas'); el.appendChild(c); return c; })();
10713
+ : (() => {
10714
+ const nextCanvas = document.createElement("canvas");
10715
+ el.appendChild(nextCanvas);
10716
+ return nextCanvas;
10717
+ })();
10721
10718
  renderToCanvas(scene, canvas, canvasOptions);
10722
- anim = new AnimationController(document.createElementNS('http://www.w3.org/2000/svg', 'svg'), ast.steps);
10719
+ anim = new AnimationController(document.createElementNS("http://www.w3.org/2000/svg", "svg"), ast.steps);
10723
10720
  }
10724
10721
  else {
10725
10722
  svg = renderToSVG(scene, el, {
@@ -10727,32 +10724,1553 @@ function render(options) {
10727
10724
  interactive: true,
10728
10725
  onNodeClick,
10729
10726
  });
10730
- // Create rough.js instance for annotations (same import as SVG renderer)
10731
10727
  let rc = null;
10732
10728
  try {
10733
10729
  rc = rough.svg(svg);
10734
10730
  }
10735
- catch { /* rough.js not available — annotations disabled */ }
10731
+ catch {
10732
+ rc = null;
10733
+ }
10736
10734
  const containerEl = el instanceof SVGSVGElement ? undefined : el;
10737
10735
  anim = new AnimationController(svg, ast.steps, containerEl, rc, ast.config);
10738
10736
  }
10739
10737
  onReady?.(anim, svg);
10740
- const instance = {
10741
- scene, anim, svg, canvas,
10742
- update: (newDsl) => { anim?.destroy(); return render({ ...options, dsl: newDsl }); },
10743
- exportSVG: (filename = 'diagram.svg') => {
10738
+ return {
10739
+ scene,
10740
+ anim,
10741
+ svg,
10742
+ canvas,
10743
+ update: (newDsl) => {
10744
+ anim?.destroy();
10745
+ return render({ ...options, dsl: newDsl });
10746
+ },
10747
+ exportSVG: (filename = "diagram.svg") => {
10744
10748
  if (svg) {
10745
- Promise.resolve().then(function () { return index; }).then(m => m.exportSVG(svg, { filename }));
10749
+ Promise.resolve().then(function () { return index; }).then((mod) => mod.exportSVG(svg, { filename }));
10746
10750
  }
10747
10751
  },
10748
- exportPNG: async (filename = 'diagram.png') => {
10752
+ exportPNG: async (filename = "diagram.png") => {
10749
10753
  if (svg) {
10750
- const m = await Promise.resolve().then(function () { return index; });
10751
- await m.exportPNG(svg, { filename });
10754
+ const mod = await Promise.resolve().then(function () { return index; });
10755
+ await mod.exportPNG(svg, { filename });
10752
10756
  }
10753
10757
  },
10754
10758
  };
10755
- return instance;
10759
+ }
10760
+
10761
+ function resolveContainer(target) {
10762
+ if (typeof target === "string") {
10763
+ const el = document.querySelector(target);
10764
+ if (!el)
10765
+ throw new Error(`Container "${target}" not found`);
10766
+ return el;
10767
+ }
10768
+ return target;
10769
+ }
10770
+ function injectStyleOnce(id, cssText) {
10771
+ if (document.getElementById(id))
10772
+ return;
10773
+ const style = document.createElement("style");
10774
+ style.id = id;
10775
+ style.textContent = cssText;
10776
+ document.head.appendChild(style);
10777
+ }
10778
+ function normalizeNewlines(value) {
10779
+ return value.replace(/\r\n?/g, "\n");
10780
+ }
10781
+ function toError(error) {
10782
+ return error instanceof Error ? error : new Error(String(error));
10783
+ }
10784
+
10785
+ const CANVAS_STYLE_ID = "sketchmark-canvas-ui";
10786
+ const CANVAS_CSS = `
10787
+ .skm-canvas{display:flex;flex-direction:column;width:100%;height:100%;min-height:320px;overflow:hidden;border:1px solid #caba98;border-radius:10px;background:#f8f4ea;color:#3a2010;font-family:"Courier New",monospace}
10788
+ .skm-canvas__animbar{display:flex;align-items:center;gap:6px;padding:6px 10px;background:#eee7d8;border-bottom:1px solid #caba98;flex-shrink:0}
10789
+ .skm-canvas__status{min-width:96px;text-align:center;color:#6a4820;font-size:11px}
10790
+ .skm-canvas__label{color:#8a6040;font-size:11px;font-style:italic}
10791
+ .skm-canvas__spacer{flex:1}
10792
+ .skm-canvas__stats{color:#9a7848;font-size:10px}
10793
+ .skm-canvas__button{border:1px solid #caba98;background:#f5eedd;color:#3a2010;border-radius:6px;padding:4px 9px;font:inherit;font-size:11px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease}
10794
+ .skm-canvas__button:hover:not(:disabled){background:#c8a060;border-color:#c8a060;color:#fff}
10795
+ .skm-canvas__button:disabled{opacity:.45;cursor:default}
10796
+ .skm-canvas__error{display:none;padding:8px 12px;background:#280a0a;border-bottom:1px solid #5a1818;color:#f07070;font-size:11px;line-height:1.4;white-space:pre-wrap;flex-shrink:0}
10797
+ .skm-canvas__error.is-visible{display:block}
10798
+ .skm-canvas__viewport{position:relative;flex:1;overflow:hidden;background:#f8f4ea;cursor:grab;touch-action:none}
10799
+ .skm-canvas__viewport.is-panning{cursor:grabbing}
10800
+ .skm-canvas--dark .skm-canvas__viewport{background:#12100a}
10801
+ .skm-canvas__grid{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
10802
+ .skm-canvas__world{position:absolute;top:0;left:0;transform-origin:0 0;}
10803
+ .skm-canvas__controls{position:absolute;right:14px;bottom:14px;display:flex;flex-direction:column;align-items:center;gap:4px;z-index:2}
10804
+ .skm-canvas__zoom{min-width:40px;text-align:center;color:#8a6040;font-size:10px}
10805
+ .skm-canvas__minimap{position:absolute;left:14px;bottom:14px;width:120px;height:80px;background:rgba(255,248,234,.94);border:1px solid #caba98;border-radius:6px;overflow:hidden;z-index:2}
10806
+ .skm-canvas__minimap canvas{width:100%;height:100%;display:block}
10807
+ .skm-canvas__minimap-viewport{position:absolute;border:1.5px solid #c85428;background:rgba(200,84,40,.08);pointer-events:none}
10808
+ .skm-canvas--hide-anim .skm-canvas__animbar,.skm-canvas--hide-controls .skm-canvas__controls,.skm-canvas--hide-minimap .skm-canvas__minimap{display:none}
10809
+ `;
10810
+ let canvasUid = 0;
10811
+ class SketchmarkCanvas {
10812
+ constructor(options) {
10813
+ this.instance = null;
10814
+ this.emitter = new EventEmitter();
10815
+ this.dsl = "";
10816
+ this.panX = 60;
10817
+ this.panY = 60;
10818
+ this.zoom = 1;
10819
+ this.isPanning = false;
10820
+ this.panMoved = false;
10821
+ this.activePointerId = null;
10822
+ this.lastPX = 0;
10823
+ this.lastPY = 0;
10824
+ this.suppressClickUntil = 0;
10825
+ this.hasRenderedOnce = false;
10826
+ this.playInFlight = false;
10827
+ this.minimapToken = 0;
10828
+ this.animUnsub = null;
10829
+ this.editorCleanup = null;
10830
+ this.mirroredEditor = null;
10831
+ this.onPointerDown = (event) => {
10832
+ if (event.button !== 0 && event.button !== 1)
10833
+ return;
10834
+ const target = event.target;
10835
+ if (target instanceof Element && target.closest(".skm-canvas__controls, .skm-canvas__minimap"))
10836
+ return;
10837
+ this.isPanning = true;
10838
+ this.panMoved = false;
10839
+ this.activePointerId = event.pointerId;
10840
+ this.lastPX = event.clientX;
10841
+ this.lastPY = event.clientY;
10842
+ try {
10843
+ this.viewport.setPointerCapture(event.pointerId);
10844
+ }
10845
+ catch {
10846
+ // ignore pointer capture failures
10847
+ }
10848
+ };
10849
+ this.onPointerMove = (event) => {
10850
+ if (!this.isPanning)
10851
+ return;
10852
+ if (this.activePointerId !== null && event.pointerId !== this.activePointerId)
10853
+ return;
10854
+ const dx = event.clientX - this.lastPX;
10855
+ const dy = event.clientY - this.lastPY;
10856
+ if (!this.panMoved && Math.abs(dx) + Math.abs(dy) > 4) {
10857
+ this.panMoved = true;
10858
+ this.viewport.classList.add("is-panning");
10859
+ }
10860
+ if (this.panMoved) {
10861
+ this.panX += dx;
10862
+ this.panY += dy;
10863
+ this.applyTransform();
10864
+ }
10865
+ this.lastPX = event.clientX;
10866
+ this.lastPY = event.clientY;
10867
+ };
10868
+ this.onStopPanning = (event) => {
10869
+ if (this.activePointerId !== null && event?.pointerId != null && event.pointerId !== this.activePointerId)
10870
+ return;
10871
+ if (this.panMoved)
10872
+ this.suppressClickUntil = performance.now() + 180;
10873
+ if (this.activePointerId !== null && this.viewport.hasPointerCapture?.(this.activePointerId)) {
10874
+ try {
10875
+ this.viewport.releasePointerCapture(this.activePointerId);
10876
+ }
10877
+ catch {
10878
+ // ignore pointer capture release failures
10879
+ }
10880
+ }
10881
+ this.activePointerId = null;
10882
+ this.isPanning = false;
10883
+ this.panMoved = false;
10884
+ this.viewport.classList.remove("is-panning");
10885
+ };
10886
+ this.onViewportClick = (event) => {
10887
+ if (performance.now() <= this.suppressClickUntil) {
10888
+ event.preventDefault();
10889
+ event.stopPropagation();
10890
+ }
10891
+ };
10892
+ this.onWheel = (event) => {
10893
+ event.preventDefault();
10894
+ const rect = this.viewport.getBoundingClientRect();
10895
+ const pivotX = event.clientX - rect.left;
10896
+ const pivotY = event.clientY - rect.top;
10897
+ const factor = event.deltaY > 0 ? 0.9 : 1.1;
10898
+ this.zoomTo(this.zoom * factor, pivotX, pivotY);
10899
+ };
10900
+ this.options = options;
10901
+ this.renderer = options.renderer ?? "svg";
10902
+ this.theme = options.theme ?? "light";
10903
+ this.dsl = normalizeNewlines(options.dsl ?? "");
10904
+ injectStyleOnce(CANVAS_STYLE_ID, CANVAS_CSS);
10905
+ const host = resolveContainer(options.container);
10906
+ host.innerHTML = "";
10907
+ this.root = document.createElement("div");
10908
+ this.root.className = "skm-canvas";
10909
+ this.root.classList.toggle("skm-canvas--dark", this.theme === "dark");
10910
+ this.root.classList.toggle("skm-canvas--hide-anim", options.showAnimationBar === false);
10911
+ this.root.classList.toggle("skm-canvas--hide-controls", options.showControls === false);
10912
+ this.root.classList.toggle("skm-canvas--hide-minimap", options.showMinimap === false);
10913
+ const patternId = `skm-grid-${++canvasUid}`;
10914
+ this.root.innerHTML = `
10915
+ <div class="skm-canvas__animbar">
10916
+ <button type="button" class="skm-canvas__button" data-action="reset">Reset</button>
10917
+ <button type="button" class="skm-canvas__button" data-action="prev">Prev</button>
10918
+ <span class="skm-canvas__status">No steps</span>
10919
+ <button type="button" class="skm-canvas__button" data-action="next">Next</button>
10920
+ <button type="button" class="skm-canvas__button" data-action="play">Play</button>
10921
+ <span class="skm-canvas__label"></span>
10922
+ <span class="skm-canvas__spacer"></span>
10923
+ <span class="skm-canvas__stats"></span>
10924
+ </div>
10925
+ <div class="skm-canvas__error"></div>
10926
+ <div class="skm-canvas__viewport">
10927
+ <svg class="skm-canvas__grid" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
10928
+ <defs><pattern id="${patternId}" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse"><circle cx="12" cy="12" r="0.9" fill="rgba(170,145,100,0.38)"></circle></pattern></defs>
10929
+ <rect width="100%" height="100%" fill="url(#${patternId})"></rect>
10930
+ </svg>
10931
+ <div class="skm-canvas__world"><div class="skm-canvas__diagram"></div></div>
10932
+ <div class="skm-canvas__controls">
10933
+ <button type="button" class="skm-canvas__button" data-action="fit">Fit</button>
10934
+ <button type="button" class="skm-canvas__button" data-action="reset-view">Reset</button>
10935
+ <button type="button" class="skm-canvas__button" data-action="zoom-in">+</button>
10936
+ <span class="skm-canvas__zoom">100%</span>
10937
+ <button type="button" class="skm-canvas__button" data-action="zoom-out">-</button>
10938
+ </div>
10939
+ <div class="skm-canvas__minimap"><canvas width="120" height="80"></canvas><div class="skm-canvas__minimap-viewport"></div></div>
10940
+ </div>`;
10941
+ this.errorElement = this.root.querySelector(".skm-canvas__error");
10942
+ this.viewport = this.root.querySelector(".skm-canvas__viewport");
10943
+ this.world = this.root.querySelector(".skm-canvas__world");
10944
+ this.diagramWrap = this.root.querySelector(".skm-canvas__diagram");
10945
+ this.zoomLabel = this.root.querySelector(".skm-canvas__zoom");
10946
+ this.stepDisplay = this.root.querySelector(".skm-canvas__status");
10947
+ this.stepLabel = this.root.querySelector(".skm-canvas__label");
10948
+ this.statsLabel = this.root.querySelector(".skm-canvas__stats");
10949
+ this.minimapCanvas = this.root.querySelector(".skm-canvas__minimap canvas");
10950
+ this.minimapIndicator = this.root.querySelector(".skm-canvas__minimap-viewport");
10951
+ this.playButton = this.root.querySelector('[data-action="play"]');
10952
+ this.prevButton = this.root.querySelector('[data-action="prev"]');
10953
+ this.nextButton = this.root.querySelector('[data-action="next"]');
10954
+ this.resetButton = this.root.querySelector('[data-action="reset"]');
10955
+ this.gridPattern = this.root.querySelector(`#${patternId}`);
10956
+ this.gridDot = this.gridPattern.querySelector("circle");
10957
+ this.root.querySelector('[data-action="fit"]')?.addEventListener("click", () => this.fitContent());
10958
+ this.root.querySelector('[data-action="reset-view"]')?.addEventListener("click", () => this.resetView());
10959
+ this.root.querySelector('[data-action="zoom-in"]')?.addEventListener("click", () => this.zoomTo(this.zoom * 1.2, this.viewport.clientWidth / 2, this.viewport.clientHeight / 2));
10960
+ this.root.querySelector('[data-action="zoom-out"]')?.addEventListener("click", () => this.zoomTo(this.zoom * 0.8, this.viewport.clientWidth / 2, this.viewport.clientHeight / 2));
10961
+ this.resetButton.addEventListener("click", () => this.resetAnimation());
10962
+ this.prevButton.addEventListener("click", () => this.prevStep());
10963
+ this.nextButton.addEventListener("click", () => this.nextStep());
10964
+ this.playButton.addEventListener("click", () => void this.play());
10965
+ this.viewport.addEventListener("pointerdown", this.onPointerDown);
10966
+ this.viewport.addEventListener("pointermove", this.onPointerMove);
10967
+ this.viewport.addEventListener("pointerup", this.onStopPanning);
10968
+ this.viewport.addEventListener("pointercancel", this.onStopPanning);
10969
+ this.viewport.addEventListener("lostpointercapture", this.onStopPanning);
10970
+ this.viewport.addEventListener("click", this.onViewportClick, true);
10971
+ this.viewport.addEventListener("wheel", this.onWheel, { passive: false });
10972
+ host.appendChild(this.root);
10973
+ this.applyTransform();
10974
+ this.syncAnimationUi();
10975
+ if (this.dsl.trim())
10976
+ this.render();
10977
+ }
10978
+ getDsl() {
10979
+ return this.dsl;
10980
+ }
10981
+ setDsl(dsl, renderNow = false) {
10982
+ this.dsl = normalizeNewlines(dsl);
10983
+ if (renderNow)
10984
+ this.render();
10985
+ }
10986
+ bindEditor(editor, options = {}) {
10987
+ this.editorCleanup?.();
10988
+ const renderOnRun = options.renderOnRun !== false;
10989
+ const renderOnChange = options.renderOnChange === true;
10990
+ const mirrorErrors = options.mirrorErrors !== false;
10991
+ const initialRender = options.initialRender !== false;
10992
+ this.mirroredEditor = mirrorErrors ? editor : null;
10993
+ const unsubs = [];
10994
+ if (renderOnRun)
10995
+ unsubs.push(editor.on("run", ({ value }) => this.render(value)));
10996
+ if (renderOnChange)
10997
+ unsubs.push(editor.on("change", ({ value }) => this.render(value)));
10998
+ if (initialRender)
10999
+ this.render(editor.getValue());
11000
+ this.editorCleanup = () => {
11001
+ unsubs.forEach((unsub) => unsub());
11002
+ this.mirroredEditor = null;
11003
+ this.editorCleanup = null;
11004
+ };
11005
+ return this.editorCleanup;
11006
+ }
11007
+ on(event, listener) {
11008
+ this.emitter.on(event, listener);
11009
+ return () => this.emitter.off(event, listener);
11010
+ }
11011
+ render(nextDsl) {
11012
+ if (typeof nextDsl === "string")
11013
+ this.dsl = normalizeNewlines(nextDsl);
11014
+ this.clearError();
11015
+ this.mirroredEditor?.clearError();
11016
+ this.animUnsub?.();
11017
+ this.animUnsub = null;
11018
+ this.instance?.anim?.destroy();
11019
+ this.diagramWrap.innerHTML = "";
11020
+ try {
11021
+ const instance = render({
11022
+ container: this.diagramWrap,
11023
+ dsl: this.dsl,
11024
+ renderer: this.renderer,
11025
+ svgOptions: { interactive: true, showTitle: true, theme: this.options.svgOptions?.theme ?? this.theme, ...this.options.svgOptions },
11026
+ canvasOptions: this.options.canvasOptions,
11027
+ onNodeClick: this.options.onNodeClick,
11028
+ });
11029
+ this.instance = instance;
11030
+ this.statsLabel.textContent = `${instance.scene.nodes.length}n / ${instance.scene.edges.length}e / ${instance.scene.groups.length}g`;
11031
+ if (this.renderer === "svg") {
11032
+ this.animUnsub = instance.anim.on((event) => {
11033
+ this.syncAnimationUi();
11034
+ if (event.type === "step-change") {
11035
+ const targetId = this.getStepTarget(event.step);
11036
+ if (targetId)
11037
+ requestAnimationFrame(() => window.setTimeout(() => this.focusAnimatedElement(targetId), 40));
11038
+ this.emitter.emit("stepchange", { stepIndex: event.stepIndex, step: event.step, canvas: this });
11039
+ }
11040
+ });
11041
+ }
11042
+ this.syncAnimationUi();
11043
+ this.renderMinimapPreview();
11044
+ if (!this.hasRenderedOnce || this.options.preserveViewOnRender === false) {
11045
+ this.hasRenderedOnce = true;
11046
+ if (this.options.autoFit !== false)
11047
+ requestAnimationFrame(() => this.fitContent());
11048
+ else
11049
+ this.applyTransform();
11050
+ }
11051
+ else {
11052
+ this.applyTransform();
11053
+ }
11054
+ this.options.onRender?.(instance, this);
11055
+ this.emitter.emit("render", { instance, canvas: this });
11056
+ return instance;
11057
+ }
11058
+ catch (error) {
11059
+ const normalized = toError(error);
11060
+ this.instance = null;
11061
+ this.statsLabel.textContent = "";
11062
+ this.showError(normalized.message);
11063
+ this.mirroredEditor?.showError(normalized.message);
11064
+ this.syncAnimationUi();
11065
+ this.renderMinimapPreview();
11066
+ this.emitter.emit("error", { error: normalized, canvas: this });
11067
+ return null;
11068
+ }
11069
+ }
11070
+ async play() {
11071
+ if (!this.instance || this.playInFlight || this.renderer !== "svg" || !this.instance.anim.total)
11072
+ return;
11073
+ this.playInFlight = true;
11074
+ this.syncAnimationUi();
11075
+ try {
11076
+ await this.instance.anim.play(this.options.playStepDelay ?? 800);
11077
+ }
11078
+ finally {
11079
+ this.playInFlight = false;
11080
+ this.syncAnimationUi();
11081
+ }
11082
+ }
11083
+ nextStep() {
11084
+ if (!this.instance || this.renderer !== "svg")
11085
+ return;
11086
+ this.instance.anim.next();
11087
+ this.syncAnimationUi();
11088
+ this.focusCurrentStep();
11089
+ }
11090
+ prevStep() {
11091
+ if (!this.instance || this.renderer !== "svg")
11092
+ return;
11093
+ this.instance.anim.prev();
11094
+ this.syncAnimationUi();
11095
+ this.focusCurrentStep();
11096
+ }
11097
+ resetAnimation() {
11098
+ if (!this.instance || this.renderer !== "svg")
11099
+ return;
11100
+ this.instance.anim.reset();
11101
+ this.syncAnimationUi();
11102
+ }
11103
+ fitContent() {
11104
+ const size = this.getContentSize();
11105
+ if (!size)
11106
+ return;
11107
+ const vpW = this.viewport.clientWidth || size.width;
11108
+ const vpH = this.viewport.clientHeight || size.height;
11109
+ const padding = this.options.fitPadding ?? 80;
11110
+ const nextZoom = Math.min((vpW - padding) / size.width, (vpH - padding) / size.height, 1);
11111
+ this.zoom = clamp(nextZoom || 1, this.options.zoomMin ?? 0.08, this.options.zoomMax ?? 4);
11112
+ this.panX = (vpW - size.width * this.zoom) / 2;
11113
+ this.panY = (vpH - size.height * this.zoom) / 2;
11114
+ this.applyTransform();
11115
+ }
11116
+ resetView() {
11117
+ this.panX = 60;
11118
+ this.panY = 60;
11119
+ this.zoom = 1;
11120
+ this.applyTransform();
11121
+ }
11122
+ setTheme(theme) {
11123
+ this.theme = theme;
11124
+ this.root.classList.toggle("skm-canvas--dark", theme === "dark");
11125
+ this.render();
11126
+ }
11127
+ destroy() {
11128
+ this.editorCleanup?.();
11129
+ this.animUnsub?.();
11130
+ this.instance?.anim?.destroy();
11131
+ this.viewport.removeEventListener("pointerdown", this.onPointerDown);
11132
+ this.viewport.removeEventListener("pointermove", this.onPointerMove);
11133
+ this.viewport.removeEventListener("pointerup", this.onStopPanning);
11134
+ this.viewport.removeEventListener("pointercancel", this.onStopPanning);
11135
+ this.viewport.removeEventListener("lostpointercapture", this.onStopPanning);
11136
+ this.viewport.removeEventListener("click", this.onViewportClick, true);
11137
+ this.viewport.removeEventListener("wheel", this.onWheel);
11138
+ this.root.remove();
11139
+ }
11140
+ applyTransform() {
11141
+ this.world.style.transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoom})`;
11142
+ this.zoomLabel.textContent = `${Math.round(this.zoom * 100)}%`;
11143
+ const gridWidth = 24 * this.zoom;
11144
+ this.gridPattern.setAttribute("x", String(this.panX % gridWidth));
11145
+ this.gridPattern.setAttribute("y", String(this.panY % gridWidth));
11146
+ this.gridPattern.setAttribute("width", String(gridWidth));
11147
+ this.gridPattern.setAttribute("height", String(gridWidth));
11148
+ this.gridDot.setAttribute("cx", String(gridWidth / 2));
11149
+ this.gridDot.setAttribute("cy", String(gridWidth / 2));
11150
+ this.gridDot.setAttribute("r", String(Math.min(1.1, this.zoom * 0.85)));
11151
+ this.updateMinimapIndicator();
11152
+ this.emitter.emit("viewchange", { panX: this.panX, panY: this.panY, zoom: this.zoom, canvas: this });
11153
+ }
11154
+ zoomTo(nextZoom, pivotX, pivotY) {
11155
+ const clampedZoom = clamp(nextZoom, this.options.zoomMin ?? 0.08, this.options.zoomMax ?? 4);
11156
+ const ratio = clampedZoom / this.zoom;
11157
+ this.panX = pivotX - (pivotX - this.panX) * ratio;
11158
+ this.panY = pivotY - (pivotY - this.panY) * ratio;
11159
+ this.zoom = clampedZoom;
11160
+ this.applyTransform();
11161
+ }
11162
+ syncAnimationUi() {
11163
+ const anim = this.instance?.anim;
11164
+ const canAnimate = this.renderer === "svg" && !!anim && anim.total > 0;
11165
+ if (!anim || !canAnimate) {
11166
+ this.stepDisplay.textContent = this.renderer === "canvas" ? "Static view" : "No steps";
11167
+ this.stepLabel.textContent = "";
11168
+ this.prevButton.disabled = true;
11169
+ this.nextButton.disabled = true;
11170
+ this.resetButton.disabled = true;
11171
+ this.playButton.disabled = true;
11172
+ return;
11173
+ }
11174
+ this.stepDisplay.textContent = anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
11175
+ this.stepLabel.textContent = anim.currentStep >= 0 ? this.getStepLabel(anim.steps[anim.currentStep]) : "";
11176
+ this.prevButton.disabled = !anim.canPrev;
11177
+ this.nextButton.disabled = !anim.canNext;
11178
+ this.resetButton.disabled = false;
11179
+ this.playButton.disabled = this.playInFlight || !anim.canNext;
11180
+ }
11181
+ getStepTarget(stepItem) {
11182
+ if (!stepItem)
11183
+ return null;
11184
+ return stepItem.kind === "beat" ? stepItem.children?.[0]?.target ?? null : stepItem.target ?? null;
11185
+ }
11186
+ getStepLabel(stepItem) {
11187
+ if (!stepItem)
11188
+ return "";
11189
+ if (stepItem.kind === "beat") {
11190
+ const first = stepItem.children?.[0];
11191
+ return first ? `beat ${first.action} ${first.target ?? ""}`.trim() : "beat";
11192
+ }
11193
+ return `${stepItem.action} ${stepItem.target ?? ""}`.trim();
11194
+ }
11195
+ focusCurrentStep() {
11196
+ const anim = this.instance?.anim;
11197
+ if (!anim || anim.currentStep < 0 || anim.currentStep >= anim.total)
11198
+ return;
11199
+ const targetId = this.getStepTarget(anim.steps[anim.currentStep]);
11200
+ if (targetId)
11201
+ window.setTimeout(() => this.focusAnimatedElement(targetId), 40);
11202
+ }
11203
+ findSvgElement(svg, id) {
11204
+ const prefixes = ["group-", "node-", "edge-", "table-", "chart-", "markdown-", "note-", ""];
11205
+ const esc = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape : (value) => value;
11206
+ for (const prefix of prefixes) {
11207
+ const found = svg.querySelector(`#${esc(prefix + id)}`);
11208
+ if (found)
11209
+ return found;
11210
+ }
11211
+ for (const attr of ["data-id", "data-node", "data-group", "sketchmark-id"]) {
11212
+ const found = svg.querySelector(`[${attr}="${id}"]`);
11213
+ if (found)
11214
+ return found;
11215
+ }
11216
+ return null;
11217
+ }
11218
+ focusAnimatedElement(targetId) {
11219
+ const svg = this.instance?.svg;
11220
+ if (!svg)
11221
+ return;
11222
+ const searchIds = this.splitEdgeTarget(targetId);
11223
+ let target = null;
11224
+ for (const id of searchIds) {
11225
+ target = this.findSvgElement(svg, id);
11226
+ if (target)
11227
+ break;
11228
+ }
11229
+ if (!target)
11230
+ return;
11231
+ const box = target.getBoundingClientRect();
11232
+ if (!box.width && !box.height)
11233
+ return;
11234
+ const vpBox = this.viewport.getBoundingClientRect();
11235
+ const centerX = box.left + box.width / 2 - vpBox.left;
11236
+ const centerY = box.top + box.height / 2 - vpBox.top;
11237
+ const margin = 100;
11238
+ if (centerX >= margin && centerX <= vpBox.width - margin && centerY >= margin && centerY <= vpBox.height - margin)
11239
+ return;
11240
+ const targetPanX = this.panX + (vpBox.width / 2 - centerX);
11241
+ const targetPanY = this.panY + (vpBox.height / 2 - centerY);
11242
+ const startPanX = this.panX;
11243
+ const startPanY = this.panY;
11244
+ const startTs = performance.now();
11245
+ const duration = 350;
11246
+ const frame = (now) => {
11247
+ const t = Math.min((now - startTs) / duration, 1);
11248
+ const eased = 1 - Math.pow(1 - t, 3);
11249
+ this.panX = startPanX + (targetPanX - startPanX) * eased;
11250
+ this.panY = startPanY + (targetPanY - startPanY) * eased;
11251
+ this.applyTransform();
11252
+ if (t < 1)
11253
+ requestAnimationFrame(frame);
11254
+ };
11255
+ requestAnimationFrame(frame);
11256
+ }
11257
+ splitEdgeTarget(targetId) {
11258
+ const connectors = ["<-->", "<->", "-->", "<--", "---", "--", "->", "<-"];
11259
+ for (const connector of connectors) {
11260
+ if (targetId.includes(connector)) {
11261
+ return targetId.split(connector).map((part) => part.trim()).filter(Boolean);
11262
+ }
11263
+ }
11264
+ return [targetId.trim()];
11265
+ }
11266
+ getContentSize() {
11267
+ if (this.instance?.svg) {
11268
+ return { width: parseFloat(this.instance.svg.getAttribute("width") || "400"), height: parseFloat(this.instance.svg.getAttribute("height") || "300") };
11269
+ }
11270
+ if (this.instance?.canvas) {
11271
+ return { width: this.instance.canvas.width || 400, height: this.instance.canvas.height || 300 };
11272
+ }
11273
+ return null;
11274
+ }
11275
+ updateMinimapIndicator() {
11276
+ if (this.options.showMinimap === false)
11277
+ return;
11278
+ const size = this.getContentSize();
11279
+ if (!size) {
11280
+ this.minimapIndicator.style.width = "0px";
11281
+ this.minimapIndicator.style.height = "0px";
11282
+ return;
11283
+ }
11284
+ const mW = this.minimapCanvas.width;
11285
+ const mH = this.minimapCanvas.height;
11286
+ const scale = Math.min(mW / size.width, mH / size.height) * 0.9;
11287
+ const offX = (mW - size.width * scale) / 2;
11288
+ const offY = (mH - size.height * scale) / 2;
11289
+ const vpW = this.viewport.clientWidth || size.width;
11290
+ const vpH = this.viewport.clientHeight || size.height;
11291
+ const ix = offX + (-this.panX / this.zoom) * scale;
11292
+ const iy = offY + (-this.panY / this.zoom) * scale;
11293
+ const iw = (vpW / this.zoom) * scale;
11294
+ const ih = (vpH / this.zoom) * scale;
11295
+ this.minimapIndicator.style.left = `${Math.max(0, ix)}px`;
11296
+ this.minimapIndicator.style.top = `${Math.max(0, iy)}px`;
11297
+ this.minimapIndicator.style.width = `${Math.min(mW - Math.max(0, ix), iw)}px`;
11298
+ this.minimapIndicator.style.height = `${Math.min(mH - Math.max(0, iy), ih)}px`;
11299
+ }
11300
+ renderMinimapPreview() {
11301
+ if (this.options.showMinimap === false)
11302
+ return;
11303
+ const ctx = this.minimapCanvas.getContext("2d");
11304
+ const size = this.getContentSize();
11305
+ if (!ctx)
11306
+ return;
11307
+ const width = this.minimapCanvas.width;
11308
+ const height = this.minimapCanvas.height;
11309
+ ctx.clearRect(0, 0, width, height);
11310
+ ctx.fillStyle = this.theme === "dark" ? "#1a140b" : "#fff8ea";
11311
+ ctx.fillRect(0, 0, width, height);
11312
+ if (!size) {
11313
+ this.updateMinimapIndicator();
11314
+ return;
11315
+ }
11316
+ const scale = Math.min(width / size.width, height / size.height) * 0.9;
11317
+ const drawW = size.width * scale;
11318
+ const drawH = size.height * scale;
11319
+ const offX = (width - drawW) / 2;
11320
+ const offY = (height - drawH) / 2;
11321
+ const token = ++this.minimapToken;
11322
+ const drawFallback = () => {
11323
+ if (token !== this.minimapToken)
11324
+ return;
11325
+ ctx.fillStyle = this.theme === "dark" ? "#20180e" : "#f7f1e2";
11326
+ ctx.fillRect(offX, offY, drawW, drawH);
11327
+ ctx.strokeStyle = this.theme === "dark" ? "#5a4525" : "#caba98";
11328
+ ctx.strokeRect(offX, offY, drawW, drawH);
11329
+ this.updateMinimapIndicator();
11330
+ };
11331
+ if (this.instance?.canvas) {
11332
+ try {
11333
+ ctx.drawImage(this.instance.canvas, offX, offY, drawW, drawH);
11334
+ ctx.strokeStyle = this.theme === "dark" ? "#5a4525" : "#caba98";
11335
+ ctx.strokeRect(offX, offY, drawW, drawH);
11336
+ }
11337
+ catch {
11338
+ drawFallback();
11339
+ }
11340
+ this.updateMinimapIndicator();
11341
+ return;
11342
+ }
11343
+ if (!this.instance?.svg || typeof XMLSerializer === "undefined") {
11344
+ drawFallback();
11345
+ return;
11346
+ }
11347
+ try {
11348
+ const serialized = new XMLSerializer().serializeToString(this.instance.svg);
11349
+ const blob = new Blob([serialized], { type: "image/svg+xml;charset=utf-8" });
11350
+ const url = URL.createObjectURL(blob);
11351
+ const image = new Image();
11352
+ image.onload = () => {
11353
+ if (token !== this.minimapToken) {
11354
+ URL.revokeObjectURL(url);
11355
+ return;
11356
+ }
11357
+ try {
11358
+ ctx.drawImage(image, offX, offY, drawW, drawH);
11359
+ ctx.strokeStyle = this.theme === "dark" ? "#5a4525" : "#caba98";
11360
+ ctx.strokeRect(offX, offY, drawW, drawH);
11361
+ }
11362
+ catch {
11363
+ drawFallback();
11364
+ }
11365
+ finally {
11366
+ URL.revokeObjectURL(url);
11367
+ this.updateMinimapIndicator();
11368
+ }
11369
+ };
11370
+ image.onerror = () => {
11371
+ URL.revokeObjectURL(url);
11372
+ drawFallback();
11373
+ };
11374
+ image.src = url;
11375
+ }
11376
+ catch {
11377
+ drawFallback();
11378
+ }
11379
+ }
11380
+ showError(message) {
11381
+ this.errorElement.textContent = message;
11382
+ this.errorElement.classList.add("is-visible");
11383
+ }
11384
+ clearError() {
11385
+ this.errorElement.textContent = "";
11386
+ this.errorElement.classList.remove("is-visible");
11387
+ }
11388
+ }
11389
+
11390
+ const EDITOR_STYLE_ID = "sketchmark-editor-ui";
11391
+ const DEFAULT_CLEAR_VALUE = "diagram\n\nend";
11392
+ const EDITOR_CSS = `
11393
+ .skm-editor {
11394
+ display: flex;
11395
+ flex-direction: column;
11396
+ width: 100%;
11397
+ height: 100%;
11398
+ min-height: 240px;
11399
+ background: #1c1608;
11400
+ color: #e0c898;
11401
+ border: 1px solid #3a2a12;
11402
+ border-radius: 10px;
11403
+ overflow: hidden;
11404
+ font-family: "Courier New", monospace;
11405
+ }
11406
+
11407
+ .skm-editor__toolbar {
11408
+ display: flex;
11409
+ align-items: center;
11410
+ gap: 8px;
11411
+ padding: 8px 10px;
11412
+ background: #12100a;
11413
+ border-bottom: 1px solid #3a2a12;
11414
+ flex-shrink: 0;
11415
+ }
11416
+
11417
+ .skm-editor__hint {
11418
+ margin-left: auto;
11419
+ color: #9a7848;
11420
+ font-size: 11px;
11421
+ }
11422
+
11423
+ .skm-editor__button {
11424
+ border: 1px solid #4a3520;
11425
+ background: #22190e;
11426
+ color: #dcc48a;
11427
+ border-radius: 6px;
11428
+ padding: 4px 10px;
11429
+ font: inherit;
11430
+ font-size: 11px;
11431
+ cursor: pointer;
11432
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
11433
+ }
11434
+
11435
+ .skm-editor__button:hover {
11436
+ border-color: #f0c96a;
11437
+ color: #f0c96a;
11438
+ }
11439
+
11440
+ .skm-editor__button--primary {
11441
+ background: #c85428;
11442
+ border-color: #c85428;
11443
+ color: #fff9ef;
11444
+ }
11445
+
11446
+ .skm-editor__button--primary:hover {
11447
+ background: #db6437;
11448
+ border-color: #db6437;
11449
+ color: #fff;
11450
+ }
11451
+
11452
+ .skm-editor__surface {
11453
+ position: relative;
11454
+ flex: 1;
11455
+ min-height: 0;
11456
+ background: #1c1608;
11457
+ overflow: hidden;
11458
+ }
11459
+
11460
+ .skm-editor__highlight,
11461
+ .skm-editor__input {
11462
+ position: absolute;
11463
+ inset: 0;
11464
+ width: 100%;
11465
+ height: 100%;
11466
+ padding: 12px 14px;
11467
+ font: inherit;
11468
+ font-size: 12px;
11469
+ line-height: 1.7;
11470
+ tab-size: 2;
11471
+ white-space: pre-wrap;
11472
+ overflow: auto;
11473
+ }
11474
+
11475
+ .skm-editor__highlight {
11476
+ margin: 0;
11477
+ border: 0;
11478
+ background: #1c1608;
11479
+ color: #e0c898;
11480
+ pointer-events: none;
11481
+ word-break: break-word;
11482
+ }
11483
+
11484
+ .skm-editor__input {
11485
+ border: 0;
11486
+ outline: 0;
11487
+ resize: none;
11488
+ background: transparent;
11489
+ color: transparent;
11490
+ caret-color: #f0c96a;
11491
+ }
11492
+
11493
+ .skm-editor__input::placeholder {
11494
+ color: #80633b;
11495
+ }
11496
+
11497
+ .skm-editor__input::selection {
11498
+ background: rgba(240, 201, 106, 0.22);
11499
+ }
11500
+
11501
+ .skm-editor__token--keyword {
11502
+ color: #e07040;
11503
+ }
11504
+
11505
+ .skm-editor__token--property {
11506
+ color: #70a8d0;
11507
+ }
11508
+
11509
+ .skm-editor__token--string {
11510
+ color: #8db870;
11511
+ }
11512
+
11513
+ .skm-editor__token--number {
11514
+ color: #d4a020;
11515
+ }
11516
+
11517
+ .skm-editor__token--comment {
11518
+ color: #6a5a3a;
11519
+ }
11520
+
11521
+ .skm-editor__token--connector {
11522
+ color: #c8b070;
11523
+ }
11524
+
11525
+ .skm-editor__token--color {
11526
+ color: var(--skm-editor-color, #f0c96a);
11527
+ box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.08);
11528
+ font-weight: 600;
11529
+ }
11530
+
11531
+ .skm-editor__error {
11532
+ display: none;
11533
+ flex-shrink: 0;
11534
+ padding: 8px 12px;
11535
+ background: #280a0a;
11536
+ border-top: 1px solid #5a1818;
11537
+ color: #f07070;
11538
+ font-size: 11px;
11539
+ line-height: 1.4;
11540
+ white-space: pre-wrap;
11541
+ }
11542
+
11543
+ .skm-editor__error.is-visible {
11544
+ display: block;
11545
+ }
11546
+ `;
11547
+ const CONNECTORS = ["<-->", "<->", "-->", "<--", "---", "--", "->", "<-"];
11548
+ const HEX_COLOR_RE = /#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b/g;
11549
+ function defaultFormatter(value) {
11550
+ return normalizeNewlines(value)
11551
+ .split("\n")
11552
+ .map((line) => line.replace(/[ \t]+$/g, ""))
11553
+ .join("\n");
11554
+ }
11555
+ function escapeHtml(value) {
11556
+ return value
11557
+ .replace(/&/g, "&amp;")
11558
+ .replace(/</g, "&lt;")
11559
+ .replace(/>/g, "&gt;")
11560
+ .replace(/"/g, "&quot;");
11561
+ }
11562
+ function wrapToken(kind, value) {
11563
+ return `<span class="skm-editor__token skm-editor__token--${kind}">${escapeHtml(value)}</span>`;
11564
+ }
11565
+ function renderColorLiteral(value) {
11566
+ return `<span class="skm-editor__token skm-editor__token--color" style="--skm-editor-color:${value}">${escapeHtml(value)}</span>`;
11567
+ }
11568
+ function renderStringToken(value) {
11569
+ HEX_COLOR_RE.lastIndex = 0;
11570
+ if (!HEX_COLOR_RE.test(value)) {
11571
+ return wrapToken("string", value);
11572
+ }
11573
+ HEX_COLOR_RE.lastIndex = 0;
11574
+ let html = "";
11575
+ let lastIndex = 0;
11576
+ let match = null;
11577
+ while ((match = HEX_COLOR_RE.exec(value))) {
11578
+ if (match.index > lastIndex) {
11579
+ html += wrapToken("string", value.slice(lastIndex, match.index));
11580
+ }
11581
+ html += renderColorLiteral(match[0]);
11582
+ lastIndex = match.index + match[0].length;
11583
+ }
11584
+ if (lastIndex < value.length) {
11585
+ html += wrapToken("string", value.slice(lastIndex));
11586
+ }
11587
+ return html;
11588
+ }
11589
+ function renderPlainToken(value, nextChar) {
11590
+ if (/^-?\d/.test(value)) {
11591
+ return wrapToken("number", value);
11592
+ }
11593
+ if (nextChar === "=") {
11594
+ return wrapToken("property", value);
11595
+ }
11596
+ if (KEYWORDS.has(value)) {
11597
+ return wrapToken("keyword", value);
11598
+ }
11599
+ return escapeHtml(value);
11600
+ }
11601
+ function highlightLine(line) {
11602
+ let html = "";
11603
+ let index = 0;
11604
+ while (index < line.length) {
11605
+ const rest = line.slice(index);
11606
+ if (rest.startsWith("//") || rest.startsWith("#")) {
11607
+ html += wrapToken("comment", rest);
11608
+ break;
11609
+ }
11610
+ if (line[index] === "\"") {
11611
+ let end = index + 1;
11612
+ while (end < line.length) {
11613
+ if (line[end] === "\"" && line[end - 1] !== "\\") {
11614
+ end += 1;
11615
+ break;
11616
+ }
11617
+ end += 1;
11618
+ }
11619
+ html += renderStringToken(line.slice(index, end));
11620
+ index = end;
11621
+ continue;
11622
+ }
11623
+ const connector = CONNECTORS.find((candidate) => line.startsWith(candidate, index));
11624
+ if (connector) {
11625
+ html += wrapToken("connector", connector);
11626
+ index += connector.length;
11627
+ continue;
11628
+ }
11629
+ const wordMatch = /^[A-Za-z_][A-Za-z0-9_-]*/.exec(rest);
11630
+ if (wordMatch) {
11631
+ const word = wordMatch[0];
11632
+ const nextChar = line[index + word.length] ?? "";
11633
+ html += renderPlainToken(word, nextChar);
11634
+ index += word.length;
11635
+ continue;
11636
+ }
11637
+ const numberMatch = /^-?\d+(?:\.\d+)?/.exec(rest);
11638
+ if (numberMatch) {
11639
+ html += wrapToken("number", numberMatch[0]);
11640
+ index += numberMatch[0].length;
11641
+ continue;
11642
+ }
11643
+ html += escapeHtml(line[index]);
11644
+ index += 1;
11645
+ }
11646
+ return html;
11647
+ }
11648
+ function renderHighlightedValue(value) {
11649
+ const normalized = normalizeNewlines(value);
11650
+ const html = normalized.split("\n").map(highlightLine).join("\n");
11651
+ return html || " ";
11652
+ }
11653
+ class SketchmarkEditor {
11654
+ constructor(options) {
11655
+ this.emitter = new EventEmitter();
11656
+ this.options = options;
11657
+ injectStyleOnce(EDITOR_STYLE_ID, EDITOR_CSS);
11658
+ const host = resolveContainer(options.container);
11659
+ host.innerHTML = "";
11660
+ this.root = document.createElement("div");
11661
+ this.root.className = "skm-editor";
11662
+ this.toolbar = document.createElement("div");
11663
+ this.toolbar.className = "skm-editor__toolbar";
11664
+ const runButton = document.createElement("button");
11665
+ runButton.type = "button";
11666
+ runButton.className = "skm-editor__button skm-editor__button--primary";
11667
+ runButton.textContent = options.runLabel ?? "Run";
11668
+ runButton.addEventListener("click", () => this.run());
11669
+ const formatButton = document.createElement("button");
11670
+ formatButton.type = "button";
11671
+ formatButton.className = "skm-editor__button";
11672
+ formatButton.textContent = options.formatLabel ?? "Format";
11673
+ formatButton.addEventListener("click", () => this.format());
11674
+ const clearButton = document.createElement("button");
11675
+ clearButton.type = "button";
11676
+ clearButton.className = "skm-editor__button";
11677
+ clearButton.textContent = options.clearLabel ?? "Clear";
11678
+ clearButton.addEventListener("click", () => this.clear());
11679
+ const hint = document.createElement("span");
11680
+ hint.className = "skm-editor__hint";
11681
+ hint.textContent = "Ctrl+Enter";
11682
+ if (options.showRunButton !== false)
11683
+ this.toolbar.appendChild(runButton);
11684
+ if (options.showFormatButton)
11685
+ this.toolbar.appendChild(formatButton);
11686
+ if (options.showClearButton !== false)
11687
+ this.toolbar.appendChild(clearButton);
11688
+ this.toolbar.appendChild(hint);
11689
+ this.surface = document.createElement("div");
11690
+ this.surface.className = "skm-editor__surface";
11691
+ this.highlightElement = document.createElement("pre");
11692
+ this.highlightElement.className = "skm-editor__highlight";
11693
+ this.highlightElement.setAttribute("aria-hidden", "true");
11694
+ this.textarea = document.createElement("textarea");
11695
+ this.textarea.className = "skm-editor__input";
11696
+ this.textarea.spellcheck = false;
11697
+ this.textarea.placeholder = options.placeholder ?? "diagram\nbox a label=\"Hello\"\nend";
11698
+ this.textarea.value = normalizeNewlines(options.value ?? DEFAULT_CLEAR_VALUE);
11699
+ this.textarea.addEventListener("input", () => {
11700
+ this.syncHighlight();
11701
+ const payload = { value: this.getValue(), editor: this };
11702
+ options.onChange?.(payload.value, this);
11703
+ this.emitter.emit("change", payload);
11704
+ });
11705
+ this.textarea.addEventListener("scroll", () => this.syncScroll());
11706
+ this.textarea.addEventListener("keydown", (event) => {
11707
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
11708
+ event.preventDefault();
11709
+ this.run();
11710
+ }
11711
+ });
11712
+ this.errorElement = document.createElement("div");
11713
+ this.errorElement.className = "skm-editor__error";
11714
+ if (options.showToolbar !== false) {
11715
+ this.root.appendChild(this.toolbar);
11716
+ }
11717
+ this.surface.appendChild(this.highlightElement);
11718
+ this.surface.appendChild(this.textarea);
11719
+ this.root.appendChild(this.surface);
11720
+ this.root.appendChild(this.errorElement);
11721
+ host.appendChild(this.root);
11722
+ this.syncHighlight();
11723
+ if (options.autoFocus) {
11724
+ this.focus();
11725
+ }
11726
+ }
11727
+ getValue() {
11728
+ return this.textarea.value;
11729
+ }
11730
+ setValue(value, emitChange = false) {
11731
+ this.textarea.value = normalizeNewlines(value);
11732
+ this.syncHighlight();
11733
+ if (emitChange) {
11734
+ const payload = { value: this.getValue(), editor: this };
11735
+ this.options.onChange?.(payload.value, this);
11736
+ this.emitter.emit("change", payload);
11737
+ }
11738
+ }
11739
+ focus() {
11740
+ this.textarea.focus();
11741
+ }
11742
+ format() {
11743
+ const formatter = this.options.formatter ?? defaultFormatter;
11744
+ const value = formatter(this.getValue());
11745
+ this.setValue(value, true);
11746
+ this.emitter.emit("format", { value, editor: this });
11747
+ }
11748
+ clear() {
11749
+ const value = this.options.clearValue ?? DEFAULT_CLEAR_VALUE;
11750
+ this.setValue(value, true);
11751
+ this.clearError();
11752
+ this.emitter.emit("clear", { value: this.getValue(), editor: this });
11753
+ }
11754
+ run() {
11755
+ const value = this.getValue();
11756
+ this.options.onRun?.(value, this);
11757
+ this.emitter.emit("run", { value, editor: this });
11758
+ }
11759
+ showError(message) {
11760
+ this.errorElement.textContent = message;
11761
+ this.errorElement.classList.add("is-visible");
11762
+ }
11763
+ clearError() {
11764
+ this.errorElement.textContent = "";
11765
+ this.errorElement.classList.remove("is-visible");
11766
+ }
11767
+ on(event, listener) {
11768
+ this.emitter.on(event, listener);
11769
+ return () => this.emitter.off(event, listener);
11770
+ }
11771
+ destroy() {
11772
+ this.root.remove();
11773
+ }
11774
+ syncHighlight() {
11775
+ this.highlightElement.innerHTML = renderHighlightedValue(this.textarea.value);
11776
+ this.syncScroll();
11777
+ }
11778
+ syncScroll() {
11779
+ this.highlightElement.scrollTop = this.textarea.scrollTop;
11780
+ this.highlightElement.scrollLeft = this.textarea.scrollLeft;
11781
+ }
11782
+ }
11783
+
11784
+ const EMBED_STYLE_ID = "sketchmark-embed-ui";
11785
+ const EMBED_CSS = `
11786
+ .skm-embed {
11787
+ display: flex;
11788
+ flex-direction: column;
11789
+ overflow: hidden;
11790
+ border: 1px solid #caba98;
11791
+ border-radius: 12px;
11792
+ background: #fff8ea;
11793
+ color: #3a2010;
11794
+ font-family: "Courier New", monospace;
11795
+ }
11796
+
11797
+ .skm-embed--dark {
11798
+ background: #12100a;
11799
+ border-color: #4a3520;
11800
+ color: #f3ddaf;
11801
+ }
11802
+
11803
+ .skm-embed__viewport {
11804
+ position: relative;
11805
+ flex: 1;
11806
+ overflow: hidden;
11807
+ min-height: 0;
11808
+ background: inherit;
11809
+ }
11810
+
11811
+ .skm-embed__world {
11812
+ position: absolute;
11813
+ top: 0;
11814
+ left: 0;
11815
+ transform-origin: 0 0;
11816
+ will-change: transform;
11817
+ }
11818
+
11819
+ .skm-embed__error {
11820
+ display: none;
11821
+ padding: 8px 12px;
11822
+ background: #280a0a;
11823
+ border-top: 1px solid #5a1818;
11824
+ color: #f07070;
11825
+ font-size: 11px;
11826
+ line-height: 1.4;
11827
+ white-space: pre-wrap;
11828
+ }
11829
+
11830
+ .skm-embed__error.is-visible {
11831
+ display: block;
11832
+ }
11833
+
11834
+ .skm-embed__controls {
11835
+ display: flex;
11836
+ align-items: center;
11837
+ gap: 8px;
11838
+ padding: 10px 12px;
11839
+ border-top: 1px solid #d8ccb1;
11840
+ background: rgba(255, 248, 234, 0.88);
11841
+ backdrop-filter: blur(6px);
11842
+ }
11843
+
11844
+ .skm-embed--dark .skm-embed__controls {
11845
+ border-top-color: #3a2a12;
11846
+ background: rgba(26, 18, 8, 0.9);
11847
+ }
11848
+
11849
+ .skm-embed__controls.is-hidden {
11850
+ display: none;
11851
+ }
11852
+
11853
+ .skm-embed__button {
11854
+ border: 1px solid #caba98;
11855
+ background: #f5eedd;
11856
+ color: #3a2010;
11857
+ border-radius: 6px;
11858
+ padding: 5px 10px;
11859
+ font: inherit;
11860
+ font-size: 11px;
11861
+ cursor: pointer;
11862
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
11863
+ }
11864
+
11865
+ .skm-embed__button:hover:not(:disabled) {
11866
+ background: #c8a060;
11867
+ border-color: #c8a060;
11868
+ color: #fff;
11869
+ }
11870
+
11871
+ .skm-embed--dark .skm-embed__button {
11872
+ border-color: #4a3520;
11873
+ background: #22190e;
11874
+ color: #f3ddaf;
11875
+ }
11876
+
11877
+ .skm-embed--dark .skm-embed__button:hover:not(:disabled) {
11878
+ background: #c8a060;
11879
+ border-color: #c8a060;
11880
+ color: #fff;
11881
+ }
11882
+
11883
+ .skm-embed__button:disabled {
11884
+ opacity: 0.45;
11885
+ cursor: default;
11886
+ }
11887
+
11888
+ .skm-embed__step {
11889
+ margin-left: auto;
11890
+ min-width: 96px;
11891
+ text-align: center;
11892
+ color: #8a6040;
11893
+ font-size: 11px;
11894
+ }
11895
+
11896
+ .skm-embed--dark .skm-embed__step {
11897
+ color: #d0b176;
11898
+ }
11899
+ `;
11900
+ class SketchmarkEmbed {
11901
+ constructor(options) {
11902
+ this.instance = null;
11903
+ this.emitter = new EventEmitter();
11904
+ this.animUnsub = null;
11905
+ this.playInFlight = false;
11906
+ this.offsetX = 0;
11907
+ this.offsetY = 0;
11908
+ this.motionFrame = null;
11909
+ this.resizeObserver = null;
11910
+ this.options = options;
11911
+ this.dsl = normalizeNewlines(options.dsl);
11912
+ this.theme = options.theme ?? "light";
11913
+ injectStyleOnce(EMBED_STYLE_ID, EMBED_CSS);
11914
+ const host = resolveContainer(options.container);
11915
+ host.innerHTML = "";
11916
+ this.root = document.createElement("div");
11917
+ this.root.className = "skm-embed";
11918
+ this.root.classList.toggle("skm-embed--dark", this.theme === "dark");
11919
+ this.applySize(options.width, options.height);
11920
+ this.root.innerHTML = `
11921
+ <div class="skm-embed__viewport">
11922
+ <div class="skm-embed__world">
11923
+ <div class="skm-embed__diagram"></div>
11924
+ </div>
11925
+ </div>
11926
+ <div class="skm-embed__error"></div>
11927
+ <div class="skm-embed__controls">
11928
+ <button type="button" class="skm-embed__button" data-action="reset">Reset</button>
11929
+ <button type="button" class="skm-embed__button" data-action="prev">Prev</button>
11930
+ <button type="button" class="skm-embed__button" data-action="next">Next</button>
11931
+ <button type="button" class="skm-embed__button" data-action="play">Play</button>
11932
+ <span class="skm-embed__step">No steps</span>
11933
+ </div>
11934
+ `;
11935
+ this.viewport = this.root.querySelector(".skm-embed__viewport");
11936
+ this.world = this.root.querySelector(".skm-embed__world");
11937
+ this.diagramWrap = this.root.querySelector(".skm-embed__diagram");
11938
+ this.errorElement = this.root.querySelector(".skm-embed__error");
11939
+ this.controlsElement = this.root.querySelector(".skm-embed__controls");
11940
+ this.stepInfoElement = this.root.querySelector(".skm-embed__step");
11941
+ this.btnReset = this.root.querySelector('[data-action="reset"]');
11942
+ this.btnPrev = this.root.querySelector('[data-action="prev"]');
11943
+ this.btnNext = this.root.querySelector('[data-action="next"]');
11944
+ this.btnPlay = this.root.querySelector('[data-action="play"]');
11945
+ this.controlsElement.classList.toggle("is-hidden", options.showControls === false);
11946
+ this.btnReset.addEventListener("click", () => this.resetAnimation());
11947
+ this.btnPrev.addEventListener("click", () => this.prevStep());
11948
+ this.btnNext.addEventListener("click", () => this.nextStep());
11949
+ this.btnPlay.addEventListener("click", () => {
11950
+ void this.play();
11951
+ });
11952
+ if (typeof ResizeObserver !== "undefined") {
11953
+ this.resizeObserver = new ResizeObserver(() => {
11954
+ this.positionViewport(false);
11955
+ });
11956
+ this.resizeObserver.observe(this.viewport);
11957
+ }
11958
+ host.appendChild(this.root);
11959
+ this.render();
11960
+ }
11961
+ getDsl() {
11962
+ return this.dsl;
11963
+ }
11964
+ setDsl(dsl, renderNow = false) {
11965
+ this.dsl = normalizeNewlines(dsl);
11966
+ if (renderNow)
11967
+ this.render();
11968
+ }
11969
+ setSize(width, height) {
11970
+ this.applySize(width, height);
11971
+ this.positionViewport(false);
11972
+ }
11973
+ setTheme(theme) {
11974
+ this.theme = theme;
11975
+ this.root.classList.toggle("skm-embed--dark", theme === "dark");
11976
+ this.render();
11977
+ }
11978
+ on(event, listener) {
11979
+ this.emitter.on(event, listener);
11980
+ return () => this.emitter.off(event, listener);
11981
+ }
11982
+ render(nextDsl) {
11983
+ if (typeof nextDsl === "string") {
11984
+ this.dsl = normalizeNewlines(nextDsl);
11985
+ }
11986
+ this.clearError();
11987
+ this.stopMotion();
11988
+ this.animUnsub?.();
11989
+ this.animUnsub = null;
11990
+ this.instance?.anim?.destroy();
11991
+ this.instance = null;
11992
+ this.diagramWrap.innerHTML = "";
11993
+ try {
11994
+ const instance = render({
11995
+ container: this.diagramWrap,
11996
+ dsl: this.dsl,
11997
+ renderer: "svg",
11998
+ svgOptions: {
11999
+ showTitle: true,
12000
+ interactive: true,
12001
+ transparent: true,
12002
+ theme: this.options.svgOptions?.theme ?? this.theme,
12003
+ ...this.options.svgOptions,
12004
+ },
12005
+ onNodeClick: this.options.onNodeClick,
12006
+ });
12007
+ this.instance = instance;
12008
+ this.animUnsub = instance.anim.on((event) => {
12009
+ this.syncControls();
12010
+ if (event.type === "step-change") {
12011
+ if (this.options.autoFocusOnStep !== false) {
12012
+ requestAnimationFrame(() => {
12013
+ window.setTimeout(() => this.positionViewport(true), 40);
12014
+ });
12015
+ }
12016
+ this.emitter.emit("stepchange", {
12017
+ stepIndex: event.stepIndex,
12018
+ step: event.step,
12019
+ embed: this,
12020
+ });
12021
+ }
12022
+ });
12023
+ this.syncControls();
12024
+ requestAnimationFrame(() => {
12025
+ this.positionViewport(false);
12026
+ });
12027
+ this.options.onRender?.(instance, this);
12028
+ this.emitter.emit("render", { instance, embed: this });
12029
+ return instance;
12030
+ }
12031
+ catch (error) {
12032
+ const normalized = toError(error);
12033
+ this.showError(normalized.message);
12034
+ this.syncControls();
12035
+ this.emitter.emit("error", { error: normalized, embed: this });
12036
+ return null;
12037
+ }
12038
+ }
12039
+ async play() {
12040
+ if (!this.instance || this.playInFlight || !this.instance.anim.total)
12041
+ return;
12042
+ this.playInFlight = true;
12043
+ this.syncControls();
12044
+ try {
12045
+ await this.instance.anim.play(this.options.playStepDelay ?? 800);
12046
+ }
12047
+ finally {
12048
+ this.playInFlight = false;
12049
+ this.syncControls();
12050
+ }
12051
+ }
12052
+ nextStep() {
12053
+ if (!this.instance)
12054
+ return;
12055
+ this.instance.anim.next();
12056
+ this.syncControls();
12057
+ this.positionViewport(true);
12058
+ }
12059
+ prevStep() {
12060
+ if (!this.instance)
12061
+ return;
12062
+ this.instance.anim.prev();
12063
+ this.syncControls();
12064
+ this.positionViewport(true);
12065
+ }
12066
+ resetAnimation() {
12067
+ if (!this.instance)
12068
+ return;
12069
+ this.instance.anim.reset();
12070
+ this.syncControls();
12071
+ this.positionViewport(true);
12072
+ }
12073
+ exportSVG(filename) {
12074
+ this.instance?.exportSVG(filename);
12075
+ }
12076
+ async exportPNG(filename) {
12077
+ await this.instance?.exportPNG(filename);
12078
+ }
12079
+ destroy() {
12080
+ this.stopMotion();
12081
+ this.animUnsub?.();
12082
+ this.instance?.anim?.destroy();
12083
+ this.instance = null;
12084
+ this.resizeObserver?.disconnect();
12085
+ this.root.remove();
12086
+ }
12087
+ applySize(width, height) {
12088
+ this.root.style.width = this.formatSize(width ?? 960);
12089
+ this.root.style.height = this.formatSize(height ?? 540);
12090
+ }
12091
+ formatSize(value) {
12092
+ return typeof value === "number" ? `${value}px` : value;
12093
+ }
12094
+ syncControls() {
12095
+ const anim = this.instance?.anim;
12096
+ if (!anim || !anim.total) {
12097
+ this.stepInfoElement.textContent = "No steps";
12098
+ this.btnReset.disabled = true;
12099
+ this.btnPrev.disabled = true;
12100
+ this.btnNext.disabled = true;
12101
+ this.btnPlay.disabled = true;
12102
+ return;
12103
+ }
12104
+ this.stepInfoElement.textContent =
12105
+ anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
12106
+ this.btnReset.disabled = false;
12107
+ this.btnPrev.disabled = !anim.canPrev;
12108
+ this.btnNext.disabled = !anim.canNext;
12109
+ this.btnPlay.disabled = this.playInFlight || !anim.canNext;
12110
+ }
12111
+ positionViewport(animated) {
12112
+ if (!this.instance?.svg)
12113
+ return;
12114
+ const svg = this.instance.svg;
12115
+ const svgWidth = parseFloat(svg.getAttribute("width") || "0");
12116
+ const svgHeight = parseFloat(svg.getAttribute("height") || "0");
12117
+ if (!svgWidth || !svgHeight)
12118
+ return;
12119
+ const viewportRect = this.viewport.getBoundingClientRect();
12120
+ const viewWidth = viewportRect.width || this.viewport.clientWidth;
12121
+ const viewHeight = viewportRect.height || this.viewport.clientHeight;
12122
+ if (!viewWidth || !viewHeight)
12123
+ return;
12124
+ const sceneIsLarge = svgWidth > viewWidth || svgHeight > viewHeight;
12125
+ const shouldFocus = sceneIsLarge &&
12126
+ this.options.autoFocus !== false &&
12127
+ !!this.getFocusTarget();
12128
+ if (!shouldFocus) {
12129
+ this.animateTo(svgWidth <= viewWidth ? (viewWidth - svgWidth) / 2 : 0, svgHeight <= viewHeight ? (viewHeight - svgHeight) / 2 : 0, animated);
12130
+ return;
12131
+ }
12132
+ const target = this.findTargetElement(this.getFocusTarget());
12133
+ if (!target) {
12134
+ this.animateTo(0, 0, animated);
12135
+ return;
12136
+ }
12137
+ const currentRect = target.getBoundingClientRect();
12138
+ const sceneX = currentRect.left - viewportRect.left - this.offsetX;
12139
+ const sceneY = currentRect.top - viewportRect.top - this.offsetY;
12140
+ const targetCenterX = sceneX + currentRect.width / 2;
12141
+ const targetCenterY = sceneY + currentRect.height / 2;
12142
+ let nextX = viewWidth / 2 - targetCenterX;
12143
+ let nextY = viewHeight / 2 - targetCenterY;
12144
+ const padding = this.options.focusPadding ?? 24;
12145
+ if (svgWidth <= viewWidth) {
12146
+ nextX = (viewWidth - svgWidth) / 2;
12147
+ }
12148
+ else {
12149
+ nextX = clamp(nextX, viewWidth - svgWidth - padding, padding);
12150
+ }
12151
+ if (svgHeight <= viewHeight) {
12152
+ nextY = (viewHeight - svgHeight) / 2;
12153
+ }
12154
+ else {
12155
+ nextY = clamp(nextY, viewHeight - svgHeight - padding, padding);
12156
+ }
12157
+ this.animateTo(nextX, nextY, animated);
12158
+ }
12159
+ animateTo(nextX, nextY, animated) {
12160
+ this.stopMotion();
12161
+ const duration = this.options.focusDuration ?? 320;
12162
+ if (!animated || duration <= 0) {
12163
+ this.offsetX = nextX;
12164
+ this.offsetY = nextY;
12165
+ this.applyTransform();
12166
+ return;
12167
+ }
12168
+ const startX = this.offsetX;
12169
+ const startY = this.offsetY;
12170
+ const start = performance.now();
12171
+ const frame = (now) => {
12172
+ const t = Math.min((now - start) / duration, 1);
12173
+ const eased = 1 - Math.pow(1 - t, 3);
12174
+ this.offsetX = startX + (nextX - startX) * eased;
12175
+ this.offsetY = startY + (nextY - startY) * eased;
12176
+ this.applyTransform();
12177
+ if (t < 1) {
12178
+ this.motionFrame = requestAnimationFrame(frame);
12179
+ }
12180
+ else {
12181
+ this.motionFrame = null;
12182
+ }
12183
+ };
12184
+ this.motionFrame = requestAnimationFrame(frame);
12185
+ }
12186
+ applyTransform() {
12187
+ this.world.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px)`;
12188
+ }
12189
+ getFocusTarget() {
12190
+ const anim = this.instance?.anim;
12191
+ if (!anim || !anim.total)
12192
+ return null;
12193
+ const startIndex = anim.currentStep >= 0 ? anim.currentStep : 0;
12194
+ for (let index = startIndex; index < anim.steps.length; index += 1) {
12195
+ const target = this.getStepTarget(anim.steps[index]);
12196
+ if (target)
12197
+ return target;
12198
+ }
12199
+ for (let index = startIndex - 1; index >= 0; index -= 1) {
12200
+ const target = this.getStepTarget(anim.steps[index]);
12201
+ if (target)
12202
+ return target;
12203
+ }
12204
+ return null;
12205
+ }
12206
+ findTargetElement(targetId) {
12207
+ const svg = this.instance?.svg;
12208
+ if (!svg)
12209
+ return null;
12210
+ const edgeTarget = this.parseEdgeTarget(targetId);
12211
+ const esc = typeof CSS !== "undefined" && typeof CSS.escape === "function"
12212
+ ? CSS.escape
12213
+ : (value) => value;
12214
+ if (edgeTarget) {
12215
+ const edgeEl = svg.querySelector(`#${esc(`edge-${edgeTarget.from}-${edgeTarget.to}`)}`);
12216
+ if (edgeEl)
12217
+ return edgeEl;
12218
+ }
12219
+ const ids = this.splitEdgeTarget(targetId);
12220
+ const prefixes = ["group-", "node-", "edge-", "table-", "chart-", "markdown-", "note-", ""];
12221
+ for (const id of ids) {
12222
+ for (const prefix of prefixes) {
12223
+ const found = svg.querySelector(`#${esc(prefix + id)}`);
12224
+ if (found)
12225
+ return found;
12226
+ }
12227
+ for (const attr of ["data-id", "data-node", "data-group", "sketchmark-id"]) {
12228
+ const found = svg.querySelector(`[${attr}="${id}"]`);
12229
+ if (found)
12230
+ return found;
12231
+ }
12232
+ }
12233
+ return null;
12234
+ }
12235
+ getStepTarget(stepItem) {
12236
+ if (!stepItem)
12237
+ return null;
12238
+ return stepItem.kind === "beat" ? stepItem.children?.[0]?.target ?? null : stepItem.target ?? null;
12239
+ }
12240
+ parseEdgeTarget(targetId) {
12241
+ const connectors = ["<-->", "<->", "-->", "<--", "---", "--", "->", "<-"];
12242
+ for (const connector of connectors) {
12243
+ if (targetId.includes(connector)) {
12244
+ const [from, to] = targetId.split(connector).map((part) => part.trim());
12245
+ if (from && to)
12246
+ return { from, to };
12247
+ }
12248
+ }
12249
+ return null;
12250
+ }
12251
+ splitEdgeTarget(targetId) {
12252
+ const connectors = ["<-->", "<->", "-->", "<--", "---", "--", "->", "<-"];
12253
+ for (const connector of connectors) {
12254
+ if (targetId.includes(connector)) {
12255
+ return targetId.split(connector).map((part) => part.trim()).filter(Boolean);
12256
+ }
12257
+ }
12258
+ return [targetId.trim()];
12259
+ }
12260
+ showError(message) {
12261
+ this.errorElement.textContent = message;
12262
+ this.errorElement.classList.add("is-visible");
12263
+ }
12264
+ clearError() {
12265
+ this.errorElement.textContent = "";
12266
+ this.errorElement.classList.remove("is-visible");
12267
+ }
12268
+ stopMotion() {
12269
+ if (this.motionFrame === null)
12270
+ return;
12271
+ cancelAnimationFrame(this.motionFrame);
12272
+ this.motionFrame = null;
12273
+ }
10756
12274
  }
10757
12275
 
10758
12276
  exports.ANIMATION_CSS = ANIMATION_CSS;
@@ -10761,6 +12279,9 @@ exports.BUILTIN_FONTS = BUILTIN_FONTS;
10761
12279
  exports.EventEmitter = EventEmitter;
10762
12280
  exports.PALETTES = PALETTES;
10763
12281
  exports.ParseError = ParseError;
12282
+ exports.SketchmarkCanvas = SketchmarkCanvas;
12283
+ exports.SketchmarkEditor = SketchmarkEditor;
12284
+ exports.SketchmarkEmbed = SketchmarkEmbed;
10764
12285
  exports.THEME_CONFIG_KEY = THEME_CONFIG_KEY;
10765
12286
  exports.THEME_NAMES = THEME_NAMES;
10766
12287
  exports.buildSceneGraph = buildSceneGraph;