@markdy/renderer-dom 0.8.16 → 0.8.18

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; also enables viewport interaction |
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. Also enables viewport interaction. 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
@@ -1863,7 +1863,8 @@ function createDiagram(opts) {
1863
1863
  progressBar,
1864
1864
  sceneBoundaryProgress,
1865
1865
  playbackRate: initialPlaybackRate = DEFAULT_PLAYBACK_RATE,
1866
- interactiveViewport = false,
1866
+ controls = false,
1867
+ interactiveViewport = controls,
1867
1868
  onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
1868
1869
  onTimeUpdate,
1869
1870
  onPlayStateChange
@@ -2074,9 +2075,17 @@ function createDiagram(opts) {
2074
2075
  let dragLastY = 0;
2075
2076
  let dragMoved = false;
2076
2077
  let suppressNextClick = false;
2078
+ let controlsPlayButton = null;
2079
+ let controlsRateButtons = [];
2077
2080
  function applyViewportTransform() {
2078
2081
  viewportTransform.style.transform = `translate(${viewportPanX}px, ${viewportPanY}px) scale(${viewportScale})`;
2079
2082
  }
2083
+ function resetViewportTransform() {
2084
+ viewportScale = 1;
2085
+ viewportPanX = 0;
2086
+ viewportPanY = 0;
2087
+ applyViewportTransform();
2088
+ }
2080
2089
  function handleViewportWheel(event) {
2081
2090
  event.preventDefault();
2082
2091
  const rect = viewport.getBoundingClientRect();
@@ -2123,6 +2132,28 @@ function createDiagram(opts) {
2123
2132
  dragMoved = false;
2124
2133
  viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
2125
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
+ }
2126
2157
  function applyCurrentTime() {
2127
2158
  for (const anim of allAnims) anim.currentTime = sceneMs;
2128
2159
  onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
@@ -2136,7 +2167,7 @@ function createDiagram(opts) {
2136
2167
  sceneMs = totalDurationMs;
2137
2168
  applyCurrentTime();
2138
2169
  isPlaying = false;
2139
- onPlayStateChange?.(false);
2170
+ emitPlayStateChange(false);
2140
2171
  lastRafTs = null;
2141
2172
  rafId = null;
2142
2173
  return;
@@ -2150,14 +2181,14 @@ function createDiagram(opts) {
2150
2181
  play() {
2151
2182
  if (isPlaying) return;
2152
2183
  isPlaying = true;
2153
- onPlayStateChange?.(true);
2184
+ emitPlayStateChange(true);
2154
2185
  lastRafTs = null;
2155
2186
  rafId = requestAnimationFrame(rafTick);
2156
2187
  },
2157
2188
  pause() {
2158
2189
  if (!isPlaying) return;
2159
2190
  isPlaying = false;
2160
- onPlayStateChange?.(false);
2191
+ emitPlayStateChange(false);
2161
2192
  if (rafId !== null) cancelAnimationFrame(rafId);
2162
2193
  rafId = null;
2163
2194
  lastRafTs = null;
@@ -2170,6 +2201,7 @@ function createDiagram(opts) {
2170
2201
  setPlaybackRate(rate) {
2171
2202
  if (!Number.isFinite(rate) || rate <= 0) return;
2172
2203
  playbackRate = rate;
2204
+ syncControls();
2173
2205
  },
2174
2206
  playbackRate() {
2175
2207
  return playbackRate;
@@ -2199,6 +2231,91 @@ function createDiagram(opts) {
2199
2231
  if (viewport.parentNode === container) container.removeChild(viewport);
2200
2232
  }
2201
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
+ }
2202
2319
  viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
2203
2320
  if (interactiveViewport) {
2204
2321
  viewport.style.touchAction = "none";
@@ -2207,17 +2324,15 @@ function createDiagram(opts) {
2207
2324
  viewport.addEventListener("pointermove", handleViewportPointerMove);
2208
2325
  viewport.addEventListener("pointerup", handleViewportPointerEnd);
2209
2326
  viewport.addEventListener("pointercancel", handleViewportPointerEnd);
2327
+ viewport.addEventListener("dblclick", handleViewportDoubleClick);
2210
2328
  }
2329
+ if (controls) mountControls();
2211
2330
  viewport.addEventListener("click", () => {
2212
2331
  if (suppressNextClick) {
2213
2332
  suppressNextClick = false;
2214
2333
  return;
2215
2334
  }
2216
- if (isPlaying) diagram.pause();
2217
- else {
2218
- if (!loop && sceneMs >= totalDurationMs) sceneMs = 0;
2219
- diagram.play();
2220
- }
2335
+ togglePlayback();
2221
2336
  });
2222
2337
  if (autoplay) diagram.play();
2223
2338
  return diagram;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.16",
3
+ "version": "0.8.18",
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.16"
47
+ "@markdy/core": "0.8.18"
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.16"
54
+ "@markdy/stdlib-systems": "0.8.18"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsup",