@markdy/renderer-dom 0.8.15 → 0.8.17

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
@@ -13,6 +13,7 @@ Web Animations API renderer for [MarkdyScript](../../docs/SYNTAX.md) scenes. Tra
13
13
  - **Seek-safe** — manual `currentTime` control enables reliable `seek()` in any direction
14
14
  - **Playback-rate controls** — set normalized timeline speed to slow down or speed up diagrams without rebuilding animations
15
15
  - **Interactive viewport** — opt into wheel zoom and drag pan while click-to-pause stays available
16
+ - **Embed controls** — opt into a compact playback toolbar with restart, speed, and view reset controls
16
17
  - **Semantic themes** — `paper`, `editorial`, `nebula`, `midnight`, `blueprint`, and `graphite`, with per-role node and edge colors
17
18
  - **Single dependency** — only `@markdy/core`
18
19
 
@@ -76,6 +77,7 @@ diagram.destroy(); // clean up DOM + cancel animations
76
77
  | `sceneBoundaryProgress` | `boolean` | `progressBar ?? true` | Preferred flag for the rainbow scene-boundary progress bar |
77
78
  | `playbackRate` | `number` | `1` | Normalized timeline speed multiplier; `1` is Markdy's normal pace |
78
79
  | `interactiveViewport` | `boolean` | `false` | Enable wheel zoom and drag pan on the rendered viewport |
80
+ | `controls` | `boolean` | `false` | Show a compact toolbar with play/pause, restart, speed, and view reset controls when viewport interaction is enabled |
79
81
  | `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
80
82
  | `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
81
83
  | `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
package/dist/index.d.ts CHANGED
@@ -24,6 +24,8 @@ interface DiagramOptions {
24
24
  playbackRate?: number;
25
25
  /** Enable wheel zoom and drag pan on the rendered viewport. Defaults to false. */
26
26
  interactiveViewport?: boolean;
27
+ /** Show built-in playback and viewport controls. Defaults to false. */
28
+ controls?: boolean;
27
29
  onWarning?: (warning: Diagnostic) => void;
28
30
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
29
31
  onPlayStateChange?: (playing: boolean) => void;
package/dist/index.js CHANGED
@@ -1864,6 +1864,7 @@ function createDiagram(opts) {
1864
1864
  sceneBoundaryProgress,
1865
1865
  playbackRate: initialPlaybackRate = DEFAULT_PLAYBACK_RATE,
1866
1866
  interactiveViewport = false,
1867
+ controls = false,
1867
1868
  onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
1868
1869
  onTimeUpdate,
1869
1870
  onPlayStateChange
@@ -1930,15 +1931,6 @@ function createDiagram(opts) {
1930
1931
  }
1931
1932
  ensureSceneStyles(document);
1932
1933
  ensureNodeStyles(document);
1933
- const viewportTransform = document.createElement("div");
1934
- viewportTransform.className = "markdy-viewport-transform";
1935
- Object.assign(viewportTransform.style, {
1936
- position: "absolute",
1937
- inset: "0",
1938
- transformOrigin: "0 0",
1939
- willChange: interactiveViewport ? "transform" : "auto"
1940
- });
1941
- viewport.appendChild(viewportTransform);
1942
1934
  const scene = document.createElement("div");
1943
1935
  scene.className = "markdy-scene-root";
1944
1936
  Object.assign(scene.style, {
@@ -1952,11 +1944,20 @@ function createDiagram(opts) {
1952
1944
  transformOrigin: "0 0"
1953
1945
  });
1954
1946
  applyThemeToScene(scene, plan.theme);
1955
- viewportTransform.appendChild(scene);
1947
+ viewport.appendChild(scene);
1948
+ const viewportTransform = document.createElement("div");
1949
+ viewportTransform.className = "markdy-viewport-transform";
1950
+ Object.assign(viewportTransform.style, {
1951
+ position: "absolute",
1952
+ inset: "0",
1953
+ transformOrigin: "0 0",
1954
+ willChange: interactiveViewport ? "transform" : "auto"
1955
+ });
1956
+ scene.appendChild(viewportTransform);
1956
1957
  const sceneContent = document.createElement("div");
1957
1958
  sceneContent.className = "markdy-scene-content";
1958
1959
  Object.assign(sceneContent.style, { position: "absolute", inset: "0" });
1959
- scene.appendChild(sceneContent);
1960
+ viewportTransform.appendChild(sceneContent);
1960
1961
  const titleEl = createTitleEl(plan.title);
1961
1962
  sceneContent.appendChild(titleEl);
1962
1963
  const cameraLayer = document.createElement("div");
@@ -2014,9 +2015,11 @@ function createDiagram(opts) {
2014
2015
  nodeLayer.appendChild(el);
2015
2016
  nodeEls.set(node.id, el);
2016
2017
  }
2018
+ let fitScale = 1;
2017
2019
  function scaleScene() {
2018
- const s = viewport.clientWidth / plan.meta.width;
2019
- scene.style.transform = `scale(${s})`;
2020
+ const width = viewport.clientWidth || plan.meta.width;
2021
+ fitScale = width / plan.meta.width;
2022
+ scene.style.transform = `scale(${fitScale})`;
2020
2023
  }
2021
2024
  scaleScene();
2022
2025
  const resizeObserver = new ResizeObserver(scaleScene);
@@ -2072,14 +2075,22 @@ function createDiagram(opts) {
2072
2075
  let dragLastY = 0;
2073
2076
  let dragMoved = false;
2074
2077
  let suppressNextClick = false;
2078
+ let controlsPlayButton = null;
2079
+ let controlsRateButtons = [];
2075
2080
  function applyViewportTransform() {
2076
2081
  viewportTransform.style.transform = `translate(${viewportPanX}px, ${viewportPanY}px) scale(${viewportScale})`;
2077
2082
  }
2083
+ function resetViewportTransform() {
2084
+ viewportScale = 1;
2085
+ viewportPanX = 0;
2086
+ viewportPanY = 0;
2087
+ applyViewportTransform();
2088
+ }
2078
2089
  function handleViewportWheel(event) {
2079
2090
  event.preventDefault();
2080
2091
  const rect = viewport.getBoundingClientRect();
2081
- const pointerX = event.clientX - rect.left;
2082
- const pointerY = event.clientY - rect.top;
2092
+ const pointerX = (event.clientX - rect.left) / fitScale;
2093
+ const pointerY = (event.clientY - rect.top) / fitScale;
2083
2094
  const nextScale = Math.min(MAX_VIEWPORT_ZOOM, Math.max(MIN_VIEWPORT_ZOOM, viewportScale * Math.exp(-event.deltaY * VIEWPORT_ZOOM_STEP)));
2084
2095
  if (nextScale === viewportScale) return;
2085
2096
  const sceneX = (pointerX - viewportPanX) / viewportScale;
@@ -2102,8 +2113,8 @@ function createDiagram(opts) {
2102
2113
  }
2103
2114
  function handleViewportPointerMove(event) {
2104
2115
  if (event.pointerId !== activePointerId) return;
2105
- const deltaX = event.clientX - dragLastX;
2106
- const deltaY = event.clientY - dragLastY;
2116
+ const deltaX = (event.clientX - dragLastX) / fitScale;
2117
+ const deltaY = (event.clientY - dragLastY) / fitScale;
2107
2118
  const totalX = event.clientX - dragStartX;
2108
2119
  const totalY = event.clientY - dragStartY;
2109
2120
  if (!dragMoved && Math.hypot(totalX, totalY) >= DRAG_CLICK_THRESHOLD_PX) dragMoved = true;
@@ -2121,6 +2132,28 @@ function createDiagram(opts) {
2121
2132
  dragMoved = false;
2122
2133
  viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
2123
2134
  }
2135
+ function handleViewportDoubleClick(event) {
2136
+ event.preventDefault();
2137
+ suppressNextClick = false;
2138
+ resetViewportTransform();
2139
+ }
2140
+ function syncControls() {
2141
+ if (controlsPlayButton) {
2142
+ controlsPlayButton.textContent = isPlaying ? "Pause" : "Play";
2143
+ controlsPlayButton.setAttribute("aria-label", isPlaying ? "Pause diagram" : "Play diagram");
2144
+ }
2145
+ for (const button of controlsRateButtons) {
2146
+ const rate = Number(button.dataset.rate ?? "1");
2147
+ const active = Math.abs(rate - playbackRate) < 1e-3;
2148
+ button.setAttribute("aria-pressed", active ? "true" : "false");
2149
+ button.style.background = active ? "rgba(15, 23, 42, 0.92)" : "rgba(255, 255, 255, 0.92)";
2150
+ button.style.color = active ? "#ffffff" : "#111827";
2151
+ }
2152
+ }
2153
+ function emitPlayStateChange(playing) {
2154
+ onPlayStateChange?.(playing);
2155
+ syncControls();
2156
+ }
2124
2157
  function applyCurrentTime() {
2125
2158
  for (const anim of allAnims) anim.currentTime = sceneMs;
2126
2159
  onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
@@ -2134,7 +2167,7 @@ function createDiagram(opts) {
2134
2167
  sceneMs = totalDurationMs;
2135
2168
  applyCurrentTime();
2136
2169
  isPlaying = false;
2137
- onPlayStateChange?.(false);
2170
+ emitPlayStateChange(false);
2138
2171
  lastRafTs = null;
2139
2172
  rafId = null;
2140
2173
  return;
@@ -2148,14 +2181,14 @@ function createDiagram(opts) {
2148
2181
  play() {
2149
2182
  if (isPlaying) return;
2150
2183
  isPlaying = true;
2151
- onPlayStateChange?.(true);
2184
+ emitPlayStateChange(true);
2152
2185
  lastRafTs = null;
2153
2186
  rafId = requestAnimationFrame(rafTick);
2154
2187
  },
2155
2188
  pause() {
2156
2189
  if (!isPlaying) return;
2157
2190
  isPlaying = false;
2158
- onPlayStateChange?.(false);
2191
+ emitPlayStateChange(false);
2159
2192
  if (rafId !== null) cancelAnimationFrame(rafId);
2160
2193
  rafId = null;
2161
2194
  lastRafTs = null;
@@ -2168,6 +2201,7 @@ function createDiagram(opts) {
2168
2201
  setPlaybackRate(rate) {
2169
2202
  if (!Number.isFinite(rate) || rate <= 0) return;
2170
2203
  playbackRate = rate;
2204
+ syncControls();
2171
2205
  },
2172
2206
  playbackRate() {
2173
2207
  return playbackRate;
@@ -2197,6 +2231,91 @@ function createDiagram(opts) {
2197
2231
  if (viewport.parentNode === container) container.removeChild(viewport);
2198
2232
  }
2199
2233
  };
2234
+ function togglePlayback() {
2235
+ if (isPlaying) diagram.pause();
2236
+ else {
2237
+ if (!loop && sceneMs >= totalDurationMs) sceneMs = 0;
2238
+ diagram.play();
2239
+ }
2240
+ }
2241
+ function makeControlButton(label, ariaLabel) {
2242
+ const button = document.createElement("button");
2243
+ button.type = "button";
2244
+ button.textContent = label;
2245
+ button.setAttribute("aria-label", ariaLabel);
2246
+ button.title = ariaLabel;
2247
+ Object.assign(button.style, {
2248
+ appearance: "none",
2249
+ border: "1px solid rgba(15, 23, 42, 0.16)",
2250
+ borderRadius: "6px",
2251
+ background: "rgba(255, 255, 255, 0.92)",
2252
+ color: "#111827",
2253
+ cursor: "pointer",
2254
+ font: "600 11px/1.2 system-ui, sans-serif",
2255
+ padding: "6px 8px",
2256
+ minWidth: "34px",
2257
+ whiteSpace: "nowrap",
2258
+ boxShadow: "0 1px 2px rgba(15, 23, 42, 0.08)"
2259
+ });
2260
+ return button;
2261
+ }
2262
+ function mountControls() {
2263
+ const toolbar = document.createElement("div");
2264
+ toolbar.className = "markdy-controls";
2265
+ toolbar.setAttribute("role", "toolbar");
2266
+ toolbar.setAttribute("aria-label", "Diagram controls");
2267
+ Object.assign(toolbar.style, {
2268
+ position: "absolute",
2269
+ left: "50%",
2270
+ bottom: "10px",
2271
+ zIndex: "10000",
2272
+ display: "flex",
2273
+ alignItems: "center",
2274
+ justifyContent: "center",
2275
+ flexWrap: "wrap",
2276
+ gap: "6px",
2277
+ maxWidth: "calc(100% - 20px)",
2278
+ padding: "6px",
2279
+ border: "1px solid rgba(15, 23, 42, 0.12)",
2280
+ borderRadius: "8px",
2281
+ background: "rgba(255, 255, 255, 0.78)",
2282
+ boxShadow: "0 10px 28px rgba(15, 23, 42, 0.16)",
2283
+ backdropFilter: "blur(10px)",
2284
+ transform: "translateX(-50%)",
2285
+ pointerEvents: "auto"
2286
+ });
2287
+ for (const eventName of ["click", "dblclick", "pointerdown", "pointermove", "pointerup", "wheel"]) {
2288
+ toolbar.addEventListener(eventName, (event) => event.stopPropagation());
2289
+ }
2290
+ controlsPlayButton = makeControlButton("Play", "Play diagram");
2291
+ controlsPlayButton.className = "markdy-control-play";
2292
+ controlsPlayButton.addEventListener("click", togglePlayback);
2293
+ toolbar.appendChild(controlsPlayButton);
2294
+ const restartButton = makeControlButton("Restart", "Restart diagram");
2295
+ restartButton.className = "markdy-control-restart";
2296
+ restartButton.addEventListener("click", () => {
2297
+ diagram.seek(0);
2298
+ diagram.play();
2299
+ });
2300
+ toolbar.appendChild(restartButton);
2301
+ controlsRateButtons = [0.5, 1, 2].map((rate) => {
2302
+ const button = makeControlButton(`${rate}x`, `Set playback speed to ${rate}x`);
2303
+ button.className = "markdy-control-rate";
2304
+ button.dataset.rate = String(rate);
2305
+ button.setAttribute("aria-pressed", "false");
2306
+ button.addEventListener("click", () => diagram.setPlaybackRate(rate));
2307
+ toolbar.appendChild(button);
2308
+ return button;
2309
+ });
2310
+ if (interactiveViewport) {
2311
+ const resetButton = makeControlButton("Reset", "Reset diagram view");
2312
+ resetButton.className = "markdy-control-reset-view";
2313
+ resetButton.addEventListener("click", resetViewportTransform);
2314
+ toolbar.appendChild(resetButton);
2315
+ }
2316
+ viewport.appendChild(toolbar);
2317
+ syncControls();
2318
+ }
2200
2319
  viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
2201
2320
  if (interactiveViewport) {
2202
2321
  viewport.style.touchAction = "none";
@@ -2205,17 +2324,15 @@ function createDiagram(opts) {
2205
2324
  viewport.addEventListener("pointermove", handleViewportPointerMove);
2206
2325
  viewport.addEventListener("pointerup", handleViewportPointerEnd);
2207
2326
  viewport.addEventListener("pointercancel", handleViewportPointerEnd);
2327
+ viewport.addEventListener("dblclick", handleViewportDoubleClick);
2208
2328
  }
2329
+ if (controls) mountControls();
2209
2330
  viewport.addEventListener("click", () => {
2210
2331
  if (suppressNextClick) {
2211
2332
  suppressNextClick = false;
2212
2333
  return;
2213
2334
  }
2214
- if (isPlaying) diagram.pause();
2215
- else {
2216
- if (!loop && sceneMs >= totalDurationMs) sceneMs = 0;
2217
- diagram.play();
2218
- }
2335
+ togglePlayback();
2219
2336
  });
2220
2337
  if (autoplay) diagram.play();
2221
2338
  return diagram;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
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",
@@ -44,14 +44,14 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@markdy/core": "0.8.15"
47
+ "@markdy/core": "0.8.17"
48
48
  },
49
49
  "devDependencies": {
50
50
  "jsdom": "^29.1.1",
51
51
  "tsup": "^8.5.1",
52
52
  "typescript": "^5.9.3",
53
53
  "vitest": "^4.1.7",
54
- "@markdy/stdlib-systems": "0.8.15"
54
+ "@markdy/stdlib-systems": "0.8.17"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsup",