@markdy/renderer-dom 1.0.18 → 1.0.20

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,7 +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
+ - **Embed controls** — opt into only the playback, timeline, viewport, export, and sharing controls a scene needs
17
17
  - **Semantic themes** — `paper`, `editorial`, `nebula`, `midnight`, `blueprint`, and `graphite`, with per-role node and edge colors
18
18
  - **Single dependency** — only `@markdy/core`
19
19
 
@@ -70,21 +70,21 @@ diagram.destroy(); // clean up DOM + cancel animations
70
70
  |---|---|---|---|
71
71
  | `container` | `HTMLElement` | *(required)* | DOM element to mount the scene into |
72
72
  | `code` | `string` | *(required)* | MarkdyScript source code |
73
- | `autoplay` | `boolean` | `plan.meta.autoplay ?? true` | Start playing immediately |
74
- | `loop` | `boolean` | `plan.meta.loop ?? true` | Loop the animation when it reaches the end |
75
- | `copyright` | `boolean` | `plan.meta.copyright ?? true` | Show a small "Powered by Markdy" badge below the animation |
73
+ | `autoplay` | `boolean` | `player.playback.autoplay ?? true` | Override whether playback starts immediately |
74
+ | `loop` | `boolean` | `player.playback.loop ?? true` | Override whether playback loops at the end |
75
+ | `copyright` | `boolean` | `player.chrome.badge ?? true` | Show the linked badge at the footer's right edge |
76
76
  | `progressBar` | `boolean \| string` | `true` | Deprecated compatibility flag for the scene-boundary progress bar |
77
- | `sceneBoundaryProgress` | `boolean \| string` | `true` | Show boundary progress bar, or pass a custom color/gradient string |
78
- | `progressColor` | `string` | `plan.meta.progressColor ?? "rainbow"` | Custom progress bar color (e.g. `"#3b82f6"`) or gradient (e.g. `"#ec4899, #8b5cf6"`) |
79
- | `playbackRate` | `number` | `plan.meta.playbackRate ?? 1` | Normalized timeline speed multiplier; `1` is Markdy's normal pace |
80
- | `interactiveViewport` | `boolean` | `controls \|\| player.interaction` | Enable wheel zoom and drag pan on the rendered viewport |
81
- | `controls` | `boolean` | `player.controls` | Show the footer toolbar (play, prev/next beat, restart, seek, speed, fit, reset view, SVG, share); also enables viewport interaction |
77
+ | `sceneBoundaryProgress` | `boolean \| string` | `player.chrome.progress` | Override boundary progress, or pass a custom color/gradient |
78
+ | `progressColor` | `string` | `player.chrome.color` or rainbow | Override the progress color or gradient |
79
+ | `playbackRate` | `number` | `player.playback.rate ?? 1` | Override the initial timeline multiplier |
80
+ | `interactiveViewport` | `boolean` | `player.interaction` | `true` supplies default gestures; `false` suppresses script gestures |
81
+ | `controls` | `boolean` | `player.controls` | `true` supplies legacy toolbar defaults; `false` suppresses script controls |
82
82
  | `shareUrl` | `string` | Markdy playground | Base URL used by the Share control when building `#code=` links |
83
83
  | `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
84
84
  | `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
85
85
  | `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
86
86
 
87
- > **Note:** Directives declared inside the MarkdyScript code (e.g. `controls true`, `interactive true`, `progressColor "#3b82f6"`) apply automatically if the corresponding JavaScript option is omitted (`undefined`). Options passed to `createDiagram()` override the in-script declarations.
87
+ > **Note:** Prefer grouped `player:` configuration in MarkdyScript. Omitted host options preserve it; host `false` suppresses controls or interaction, while host `true` supplies legacy defaults for leaves the script does not set. A speed selector renders only with at least two distinct positive `speeds` values.
88
88
 
89
89
  ### `Diagram`
90
90
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BeatRange, Diagnostic, RenderPlan } from '@markdy/core';
1
+ import { BeatRange, ThemeTokens, Diagnostic, RenderPlan } from '@markdy/core';
2
2
 
3
3
  /**
4
4
  * Markdy diagram runtime — renders a RenderPlan via WAAPI.
@@ -26,11 +26,11 @@ interface DiagramOptions {
26
26
  progressBarColor?: string;
27
27
  /** Playback speed multiplier. Defaults to 1, where 1 is Markdy's normal pace. */
28
28
  playbackRate?: number;
29
- /** Enable wheel zoom and drag pan on the rendered viewport. Defaults to false. */
29
+ /** Host interaction override. `true` enables default gestures; `false` suppresses script interaction. */
30
30
  interactiveViewport?: boolean;
31
31
  /** Base URL for the Share control. Defaults to the Markdy playground. */
32
32
  shareUrl?: string;
33
- /** Show built-in playback and viewport controls. Also enables viewport interaction. Defaults to false. */
33
+ /** Host controls override. `true` enables the legacy default set; `false` suppresses script controls. */
34
34
  controls?: boolean;
35
35
  onWarning?: (warning: Diagnostic) => void;
36
36
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
@@ -50,6 +50,7 @@ interface Diagram {
50
50
  seekToBeat(name: string): void;
51
51
  nextBeat(): void;
52
52
  prevBeat(): void;
53
+ setTheme(theme: string | ThemeTokens): void;
53
54
  destroy(): void;
54
55
  }
55
56
  declare function createDiagram(opts: DiagramOptions): Diagram;
package/dist/index.js CHANGED
@@ -8,7 +8,8 @@ import {
8
8
  import {
9
9
  compressMarkdyToUrlHash,
10
10
  parseAndCompile,
11
- resolvePlayer
11
+ resolvePlayer,
12
+ resolveTheme
12
13
  } from "@markdy/core";
13
14
 
14
15
  // src/geometry/rect.ts
@@ -316,10 +317,112 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
316
317
  const targetCenter = rectCenter(targetRect);
317
318
  const dx = targetCenter.x - sourceCenter.x;
318
319
  const dy = targetCenter.y - sourceCenter.y;
320
+ const isDominantVertical = Math.abs(dy) >= Math.abs(dx);
319
321
  const stubLen = 18;
320
322
  const allObstacles = obstacles;
321
323
  const candidates = [];
322
- if (dx >= 30) {
324
+ if (dy >= 20) {
325
+ let sX = sourceCenter.x;
326
+ let tX = targetCenter.x;
327
+ if (dx > 16) {
328
+ sX = clamp(sourceCenter.x + (lane > 0 ? Math.abs(laneShift) : 6), sourceRect.x1 + 14, sourceRect.x2 - 14);
329
+ tX = clamp(targetCenter.x - (lane > 0 ? Math.abs(laneShift) : 6), targetRect.x1 + 14, targetRect.x2 - 14);
330
+ } else if (dx < -16) {
331
+ sX = clamp(sourceCenter.x - (lane > 0 ? Math.abs(laneShift) : 6), sourceRect.x1 + 14, sourceRect.x2 - 14);
332
+ tX = clamp(targetCenter.x + (lane > 0 ? Math.abs(laneShift) : 6), targetRect.x1 + 14, targetRect.x2 - 14);
333
+ } else {
334
+ sX = clamp(sourceCenter.x + laneShift, sourceRect.x1 + 14, sourceRect.x2 - 14);
335
+ tX = clamp(targetCenter.x + laneShift, targetRect.x1 + 14, targetRect.x2 - 14);
336
+ }
337
+ const sPort = { x: sX, y: sourceRect.y2 };
338
+ const tPort = { x: tX, y: targetRect.y1 };
339
+ if (Math.abs(sX - tX) < 1) {
340
+ candidates.push([sPort, tPort]);
341
+ } else {
342
+ const midY = round1((sPort.y + tPort.y) / 2 + laneShift);
343
+ candidates.push([
344
+ sPort,
345
+ { x: sPort.x, y: midY },
346
+ { x: tPort.x, y: midY },
347
+ tPort
348
+ ]);
349
+ }
350
+ let minObstacleX = Math.min(sourceRect.x1, targetRect.x1);
351
+ let maxObstacleX = Math.max(sourceRect.x2, targetRect.x2);
352
+ for (const o of allObstacles) {
353
+ if (o.y2 > sourceRect.y2 && o.y1 < targetRect.y1) {
354
+ minObstacleX = Math.min(minObstacleX, o.x1);
355
+ maxObstacleX = Math.max(maxObstacleX, o.x2);
356
+ }
357
+ }
358
+ const bypassXLeft = Math.max(16, minObstacleX - 24 - Math.abs(laneShift));
359
+ const bypassXRight = Math.min(bounds.width - 16, maxObstacleX + 24 + Math.abs(laneShift));
360
+ candidates.push([
361
+ sPort,
362
+ { x: sPort.x, y: sPort.y + 12 },
363
+ { x: bypassXLeft, y: sPort.y + 12 },
364
+ { x: bypassXLeft, y: tPort.y - 12 },
365
+ { x: tPort.x, y: tPort.y - 12 },
366
+ tPort
367
+ ]);
368
+ candidates.push([
369
+ sPort,
370
+ { x: sPort.x, y: sPort.y + 12 },
371
+ { x: bypassXRight, y: sPort.y + 12 },
372
+ { x: bypassXRight, y: tPort.y - 12 },
373
+ { x: tPort.x, y: tPort.y - 12 },
374
+ tPort
375
+ ]);
376
+ } else if (dy <= -20) {
377
+ let sX = sourceCenter.x;
378
+ let tX = targetCenter.x;
379
+ if (dx > 16) {
380
+ sX = clamp(sourceCenter.x + (lane > 0 ? Math.abs(laneShift) : 6), sourceRect.x1 + 14, sourceRect.x2 - 14);
381
+ tX = clamp(targetCenter.x - (lane > 0 ? Math.abs(laneShift) : 6), targetRect.x1 + 14, targetRect.x2 - 14);
382
+ } else if (dx < -16) {
383
+ sX = clamp(sourceCenter.x - (lane > 0 ? Math.abs(laneShift) : 6), sourceRect.x1 + 14, sourceRect.x2 - 14);
384
+ tX = clamp(targetCenter.x + (lane > 0 ? Math.abs(laneShift) : 6), targetRect.x1 + 14, targetRect.x2 - 14);
385
+ } else {
386
+ sX = clamp(sourceCenter.x + laneShift, sourceRect.x1 + 14, sourceRect.x2 - 14);
387
+ tX = clamp(targetCenter.x + laneShift, targetRect.x1 + 14, targetRect.x2 - 14);
388
+ }
389
+ const sPort = { x: sX, y: sourceRect.y1 };
390
+ const tPort = { x: tX, y: targetRect.y2 };
391
+ if (Math.abs(sX - tX) < 1) {
392
+ candidates.push([sPort, tPort]);
393
+ } else {
394
+ const midY = round1((sPort.y + tPort.y) / 2 + laneShift);
395
+ candidates.push([
396
+ sPort,
397
+ { x: sPort.x, y: midY },
398
+ { x: tPort.x, y: midY },
399
+ tPort
400
+ ]);
401
+ }
402
+ let minObstacleX = Math.min(sourceRect.x1, targetRect.x1);
403
+ let maxObstacleX = Math.max(sourceRect.x2, targetRect.x2);
404
+ for (const o of allObstacles) {
405
+ if (o.y1 < sourceRect.y2 && o.y2 > targetRect.y1) {
406
+ minObstacleX = Math.min(minObstacleX, o.x1);
407
+ maxObstacleX = Math.max(maxObstacleX, o.x2);
408
+ }
409
+ }
410
+ const bypassXLeft = Math.max(16, minObstacleX - 28 - Math.abs(laneShift));
411
+ const bypassXRight = Math.min(bounds.width - 16, maxObstacleX + 28 + Math.abs(laneShift));
412
+ candidates.push([
413
+ { x: sourceRect.x1, y: sourceCenter.y },
414
+ { x: bypassXLeft, y: sourceCenter.y },
415
+ { x: bypassXLeft, y: targetCenter.y },
416
+ { x: targetRect.x1, y: targetCenter.y }
417
+ ]);
418
+ candidates.push([
419
+ { x: sourceRect.x2, y: sourceCenter.y },
420
+ { x: bypassXRight, y: sourceCenter.y },
421
+ { x: bypassXRight, y: targetCenter.y },
422
+ { x: targetRect.x2, y: targetCenter.y }
423
+ ]);
424
+ }
425
+ if (dx >= 20) {
323
426
  let sY = sourceCenter.y;
324
427
  let tY = targetCenter.y;
325
428
  if (Math.abs(dy) > 16) {
@@ -334,15 +437,16 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
334
437
  const tPort = { x: targetRect.x1, y: tY };
335
438
  if (Math.abs(sY - tY) < 1) {
336
439
  candidates.push([sPort, tPort]);
440
+ } else {
441
+ const rawMidX = (sPort.x + tPort.x) / 2 + laneShift;
442
+ const midX = round1(clamp(rawMidX, sourceRect.x2 + 8, targetRect.x1 - 8));
443
+ candidates.push([
444
+ sPort,
445
+ { x: midX, y: sY },
446
+ { x: midX, y: tY },
447
+ tPort
448
+ ]);
337
449
  }
338
- const rawMidX = (sPort.x + tPort.x) / 2 + laneShift;
339
- const midX = round1(clamp(rawMidX, sourceRect.x2 + 8, targetRect.x1 - 8));
340
- candidates.push([
341
- sPort,
342
- { x: midX, y: sY },
343
- { x: midX, y: tY },
344
- tPort
345
- ]);
346
450
  let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
347
451
  for (const o of allObstacles) {
348
452
  if (o.x2 > sourceRect.x2 && o.x1 < targetRect.x1) {
@@ -356,7 +460,7 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
356
460
  { x: targetCenter.x, y: bypassYTop },
357
461
  { x: targetCenter.x, y: targetRect.y1 }
358
462
  ]);
359
- } else if (dx < -30) {
463
+ } else if (dx <= -20) {
360
464
  const sX = clamp(sourceCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), sourceRect.x1 + 16, sourceRect.x2 - 16);
361
465
  const tX = clamp(targetCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), targetRect.x1 + 16, targetRect.x2 - 16);
362
466
  let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
@@ -401,23 +505,6 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
401
505
  { x: midX, y: tY },
402
506
  tPortR
403
507
  ]);
404
- } else {
405
- const sX = clamp(sourceCenter.x + laneShift, sourceRect.x1 + 16, sourceRect.x2 - 16);
406
- const tX = clamp(targetCenter.x + laneShift, targetRect.x1 + 16, targetRect.x2 - 16);
407
- const sourceDown = dy >= 0;
408
- const sPort = { x: sX, y: sourceDown ? sourceRect.y2 : sourceRect.y1 };
409
- const tPort = { x: tX, y: sourceDown ? targetRect.y1 : targetRect.y2 };
410
- if (Math.abs(sX - tX) < 1) {
411
- candidates.push([sPort, tPort]);
412
- } else {
413
- const midY = round1((sPort.y + tPort.y) / 2 + laneShift);
414
- candidates.push([
415
- sPort,
416
- { x: sPort.x, y: midY },
417
- { x: tPort.x, y: midY },
418
- tPort
419
- ]);
420
- }
421
508
  }
422
509
  for (const obstacle of allObstacles) {
423
510
  const bypassYTop = Math.max(16, obstacle.y1 - 18 - Math.abs(laneShift));
@@ -443,6 +530,12 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
443
530
  tP
444
531
  ]);
445
532
  }
533
+ if (candidates.length === 0) {
534
+ candidates.push([
535
+ { x: sourceCenter.x, y: sourceCenter.y },
536
+ { x: targetCenter.x, y: targetCenter.y }
537
+ ]);
538
+ }
446
539
  let best = candidates[0];
447
540
  let bestScore = Number.POSITIVE_INFINITY;
448
541
  for (const candidate of candidates) {
@@ -450,7 +543,27 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
450
543
  const hits = countPathIntersections(cleaned, allObstacles);
451
544
  const bends = routeBends(cleaned);
452
545
  const length = routeLength(cleaned);
453
- const score = hits * 1e6 + bends * 500 + length;
546
+ let portPenalty = 0;
547
+ const startP = cleaned[0];
548
+ const endP = cleaned[cleaned.length - 1];
549
+ if (isDominantVertical) {
550
+ if (dy >= 20) {
551
+ if (Math.abs(startP.y - sourceRect.y1) < 1) portPenalty += 25e3;
552
+ if (Math.abs(endP.y - targetRect.y2) < 1) portPenalty += 25e3;
553
+ } else if (dy <= -20) {
554
+ if (Math.abs(startP.y - sourceRect.y2) < 1) portPenalty += 25e3;
555
+ if (Math.abs(endP.y - targetRect.y1) < 1) portPenalty += 25e3;
556
+ }
557
+ } else {
558
+ if (dx >= 20) {
559
+ if (Math.abs(startP.x - sourceRect.x1) < 1) portPenalty += 25e3;
560
+ if (Math.abs(endP.x - targetRect.x2) < 1) portPenalty += 25e3;
561
+ } else if (dx <= -20) {
562
+ if (Math.abs(startP.x - sourceRect.x2) < 1) portPenalty += 25e3;
563
+ if (Math.abs(endP.x - targetRect.x1) < 1) portPenalty += 25e3;
564
+ }
565
+ }
566
+ const score = hits * 1e6 + portPenalty + bends * 500 + length;
454
567
  if (score < bestScore) {
455
568
  bestScore = score;
456
569
  best = cleaned;
@@ -522,9 +635,14 @@ function ensureEdgeLayer(scene) {
522
635
  }
523
636
  function ensureDefs(svg, theme, id) {
524
637
  const key = `data-markdy-defs-${id}`;
525
- if (svg.querySelector(`defs[${key}='1']`)) return;
526
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
527
- defs.setAttribute(key, "1");
638
+ let defs = svg.querySelector(`defs[${key}='1']`);
639
+ if (!defs) {
640
+ defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
641
+ defs.setAttribute(key, "1");
642
+ svg.prepend(defs);
643
+ } else {
644
+ defs.replaceChildren();
645
+ }
528
646
  for (const [kind, color] of Object.entries(theme.edges)) {
529
647
  const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
530
648
  arrow.setAttribute("id", `${id}-arrow-${kind}`);
@@ -571,7 +689,27 @@ function ensureDefs(svg, theme, id) {
571
689
  filter.appendChild(turb);
572
690
  filter.appendChild(disp);
573
691
  defs.appendChild(filter);
574
- svg.prepend(defs);
692
+ }
693
+ function updateEdgeLayerTheme(svg, edgeRuntimeMap, theme, sceneId) {
694
+ ensureDefs(svg, theme, sceneId);
695
+ const isDark = isDarkTheme(theme);
696
+ for (const runtime of edgeRuntimeMap.values()) {
697
+ const color = theme.edges[runtime.kind];
698
+ runtime.color = color;
699
+ runtime.path.setAttribute("stroke", color);
700
+ if (runtime.kind !== "dependency") {
701
+ runtime.path.style.filter = `drop-shadow(0 0 4px ${translucentColor(color, "44")})`;
702
+ }
703
+ runtime.dot.setAttribute("fill", color);
704
+ runtime.dot.style.filter = `drop-shadow(0 0 6px ${color}) drop-shadow(0 0 12px ${color}88)`;
705
+ if (runtime.labelPlate) {
706
+ runtime.labelPlate.setAttribute("fill", theme.canvas ?? theme.surface ?? "#ffffff");
707
+ runtime.labelPlate.setAttribute("stroke", translucentColor(color, "28"));
708
+ }
709
+ if (runtime.label) {
710
+ runtime.label.setAttribute("fill", computeEdgeLabelColor(color, isDark));
711
+ }
712
+ }
575
713
  }
576
714
  function isDarkTheme(theme) {
577
715
  if (theme.name === "paper" || theme.name === "editorial" || theme.name === "sketchy") {
@@ -1635,6 +1773,10 @@ function ensureNodeStyles(doc) {
1635
1773
  border-radius: 4px;
1636
1774
  transform: rotate(0deg);
1637
1775
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
1776
+ filter: drop-shadow(0 0 1px var(--md-hairline, rgba(15, 23, 42, 0.2))) drop-shadow(0 4px 12px var(--md-shadow, rgba(2, 6, 23, 0.15)));
1777
+ }
1778
+ .markdy-node[data-shape="diamond"][data-focal="1"] {
1779
+ filter: drop-shadow(0 0 1.5px var(--md-accent)) drop-shadow(0 4px 16px color-mix(in srgb, var(--md-accent) 30%, transparent));
1638
1780
  }
1639
1781
  .markdy-node[data-shape="diamond"] .markdy-node__body {
1640
1782
  justify-content: center;
@@ -1650,6 +1792,13 @@ function ensureNodeStyles(doc) {
1650
1792
  border-radius: 999px;
1651
1793
  min-height: 54px;
1652
1794
  }
1795
+ .markdy-node[data-shape="pill"] .markdy-node__body {
1796
+ justify-content: center;
1797
+ padding: 0 20px;
1798
+ }
1799
+ .markdy-node[data-shape="pill"] .markdy-node__label {
1800
+ text-align: center;
1801
+ }
1653
1802
  .markdy-node[data-shape="circle"],
1654
1803
  .markdy-node[data-kind="dot"] {
1655
1804
  border-radius: 50%;
@@ -1719,9 +1868,13 @@ function ensureNodeStyles(doc) {
1719
1868
  background: var(--md-node-surface, var(--md-surface));
1720
1869
  }
1721
1870
  .markdy-node[data-focal="1"] {
1722
- background: color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 100%, transparent);
1871
+ background:
1872
+ linear-gradient(180deg,
1873
+ color-mix(in srgb, var(--md-accent) 12%, var(--md-node-surface-raised, var(--md-surface-raised, #ffffff))),
1874
+ color-mix(in srgb, var(--md-accent) 6%, var(--md-node-surface, var(--md-surface, #ffffff))));
1723
1875
  box-shadow:
1724
- inset 0 0 0 1px color-mix(in srgb, var(--md-accent) 55%, transparent);
1876
+ 0 2px 8px color-mix(in srgb, var(--md-accent) 20%, transparent),
1877
+ inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent) 60%, transparent);
1725
1878
  }
1726
1879
  .markdy-scene-root[data-markdy-theme="nebula"] .markdy-node {
1727
1880
  border: 1px solid color-mix(in srgb, var(--md-role-color, var(--md-accent)) 34%, transparent);
@@ -1744,8 +1897,8 @@ function ensureNodeStyles(doc) {
1744
1897
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent) 60%, transparent);
1745
1898
  background:
1746
1899
  linear-gradient(180deg,
1747
- color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 8%, transparent),
1748
- color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 4%, transparent));
1900
+ color-mix(in srgb, var(--md-accent) 10%, var(--md-node-surface-raised, var(--md-surface-raised, #ffffff))),
1901
+ color-mix(in srgb, var(--md-accent) 5%, var(--md-node-surface, var(--md-surface, #ffffff))));
1749
1902
  }
1750
1903
  .markdy-node[data-kind="external"] {
1751
1904
  border: 1.2px dashed color-mix(in srgb, var(--md-ink, var(--md-text)) 30%, transparent);
@@ -1768,17 +1921,32 @@ function ensureNodeStyles(doc) {
1768
1921
  border-color: #ff5a36;
1769
1922
  box-shadow: 0 0 18px -8px rgba(255, 90, 54, 0.45);
1770
1923
  }
1924
+ .markdy-scene-root[data-markdy-theme="terminal"] .markdy-node[data-shape="diamond"] {
1925
+ filter: drop-shadow(0 0 1px #2b2b2b);
1926
+ }
1927
+ .markdy-scene-root[data-markdy-theme="terminal"] .markdy-node[data-shape="diamond"][data-focal="1"] {
1928
+ filter: drop-shadow(0 0 2px #ff5a36) drop-shadow(0 0 12px rgba(255, 90, 54, 0.35));
1929
+ }
1771
1930
  .markdy-scene-root[data-markdy-theme="sketchy"] .markdy-node {
1772
1931
  background: #ffffff;
1773
1932
  border: 1.5px solid #2d3142;
1774
1933
  box-shadow: 3px 3px 0 rgba(45, 49, 66, 0.10);
1775
1934
  border-radius: var(--md-radius-md, 4px);
1776
1935
  }
1936
+ .markdy-scene-root[data-markdy-theme="sketchy"] .markdy-node[data-shape="diamond"] {
1937
+ filter: drop-shadow(0 0 1px #2d3142) drop-shadow(3px 3px 0 rgba(45, 49, 66, 0.12));
1938
+ }
1777
1939
  .markdy-scene-root[data-markdy-theme="sketchy"] .markdy-node[data-focal="1"] {
1778
1940
  border-color: #eb6c36;
1779
1941
  background: rgba(235, 108, 54, 0.06);
1780
1942
  box-shadow: 3px 3px 0 rgba(235, 108, 54, 0.12);
1781
1943
  }
1944
+ .markdy-scene-root[data-markdy-theme="nebula"] .markdy-node[data-shape="diamond"] {
1945
+ filter: drop-shadow(0 0 1px var(--md-hairline)) drop-shadow(0 0 18px -4px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 60%, transparent));
1946
+ }
1947
+ .markdy-scene-root[data-markdy-theme="nebula"] .markdy-node[data-shape="diamond"][data-focal="1"] {
1948
+ filter: drop-shadow(0 0 2px var(--md-accent)) drop-shadow(0 0 24px -2px color-mix(in srgb, var(--md-accent) 70%, transparent));
1949
+ }
1782
1950
  `;
1783
1951
  doc.head.appendChild(style);
1784
1952
  }
@@ -2309,6 +2477,52 @@ function mountSequenceLayer(layer, nodes, messages, activations, theme, bounds)
2309
2477
  for (const animation of animations) animation.pause();
2310
2478
  return animations;
2311
2479
  }
2480
+ function updateSequenceLayerTheme(layer, theme) {
2481
+ const svg = layer.querySelector("svg");
2482
+ if (!svg) return;
2483
+ const defs = svg.querySelector("defs");
2484
+ if (defs) {
2485
+ Array.from(defs.querySelectorAll("marker")).forEach((marker) => {
2486
+ const id = marker.getAttribute("id") || "";
2487
+ const path = marker.querySelector("path");
2488
+ if (path) {
2489
+ if (id.includes("response")) {
2490
+ path.setAttribute("stroke", theme.edges.response);
2491
+ } else if (id.includes("event")) {
2492
+ path.setAttribute("fill", theme.edges.event);
2493
+ } else {
2494
+ path.setAttribute("fill", theme.edges.request);
2495
+ }
2496
+ }
2497
+ });
2498
+ }
2499
+ Array.from(svg.querySelectorAll(".markdy-sequence-lifeline")).forEach((line) => {
2500
+ line.setAttribute("stroke", theme.hairline ?? theme.border);
2501
+ });
2502
+ Array.from(svg.querySelectorAll(".markdy-sequence-activation")).forEach((bar) => {
2503
+ bar.setAttribute("fill", theme.accent);
2504
+ bar.style.filter = `drop-shadow(0 0 6px ${theme.accent}66)`;
2505
+ });
2506
+ Array.from(svg.querySelectorAll(".markdy-sequence-message")).forEach((group) => {
2507
+ const line = group.querySelector("line");
2508
+ if (line) {
2509
+ const markerEnd = line.getAttribute("marker-end") || "";
2510
+ if (markerEnd.includes("response")) line.setAttribute("stroke", theme.edges.response);
2511
+ else if (markerEnd.includes("event")) line.setAttribute("stroke", theme.edges.event);
2512
+ else line.setAttribute("stroke", theme.edges.request);
2513
+ }
2514
+ const plate = group.querySelector("rect");
2515
+ if (plate) {
2516
+ plate.setAttribute("fill", theme.labelPlate ?? theme.surface);
2517
+ plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 70%, transparent)`);
2518
+ }
2519
+ const text = group.querySelector("text");
2520
+ if (text) {
2521
+ text.setAttribute("fill", theme.text);
2522
+ if (theme.fonts?.mono) text.setAttribute("font-family", theme.fonts.mono);
2523
+ }
2524
+ });
2525
+ }
2312
2526
 
2313
2527
  // src/tree.ts
2314
2528
  function mountTreeBuses(layer, buses, theme) {
@@ -2511,17 +2725,20 @@ function ensureSceneStyles(doc) {
2511
2725
  justify-content: space-between;
2512
2726
  gap: 8px;
2513
2727
  padding: 6px 12px;
2514
- background: var(--md-footer-bg, rgba(15, 23, 42, 0.85));
2515
- backdrop-filter: blur(16px);
2516
- -webkit-backdrop-filter: blur(16px);
2517
- border-top: 1px solid var(--md-footer-border, rgba(148, 163, 184, 0.15));
2728
+ color: var(--md-text, #f8fafc);
2729
+ background: transparent;
2730
+ backdrop-filter: none;
2731
+ -webkit-backdrop-filter: none;
2732
+ border-top: none;
2518
2733
  z-index: 20;
2519
2734
  }
2520
2735
  .markdy-controls {
2521
2736
  display: flex;
2522
2737
  align-items: center;
2523
2738
  justify-content: flex-start;
2524
- width: 100%;
2739
+ flex: 1 1 auto;
2740
+ width: auto;
2741
+ min-width: 0;
2525
2742
  gap: 8px;
2526
2743
  max-width: 100%;
2527
2744
  overflow-x: auto;
@@ -2555,7 +2772,6 @@ function ensureSceneStyles(doc) {
2555
2772
  align-items: center;
2556
2773
  gap: 4px;
2557
2774
  flex-shrink: 0;
2558
- margin-left: auto;
2559
2775
  }
2560
2776
  .markdy-control-divider {
2561
2777
  width: 1px;
@@ -2626,37 +2842,24 @@ function ensureSceneStyles(doc) {
2626
2842
  transform: translateY(-1px) scale(1.02);
2627
2843
  }
2628
2844
  .markdy-badge {
2629
- position: absolute;
2630
- top: 10px;
2631
- right: 12px;
2632
- z-index: 25;
2633
2845
  display: inline-flex;
2634
2846
  align-items: center;
2635
- gap: 4px;
2636
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2847
+ text-align: right;
2848
+ font-family: system-ui, sans-serif;
2637
2849
  font-size: 10px;
2638
- font-weight: 500;
2639
- letter-spacing: 0.02em;
2640
- color: var(--md-text-muted, #94a3b8);
2850
+ font-weight: 400;
2851
+ color: var(--md-control-text, #94a3b8);
2641
2852
  text-decoration: none;
2642
- padding: 2.5px 7px;
2643
- border-radius: 5px;
2644
- background: var(--md-surface, rgba(15, 23, 42, 0.45));
2645
- border: 1px solid var(--md-border, rgba(148, 163, 184, 0.15));
2646
- backdrop-filter: blur(4px);
2647
- -webkit-backdrop-filter: blur(4px);
2648
- opacity: 0.65;
2649
- transition: all 0.2s ease;
2650
- pointer-events: auto;
2651
- user-select: none;
2853
+ padding: 0;
2854
+ opacity: 0.7;
2855
+ margin-left: auto;
2856
+ flex-shrink: 0;
2857
+ transition: opacity 0.15s ease, color 0.15s ease;
2858
+ white-space: nowrap;
2652
2859
  }
2653
2860
  .markdy-badge:hover {
2654
2861
  opacity: 1;
2655
- color: var(--accent, #10b981);
2656
- border-color: var(--accent, #10b981);
2657
- background: var(--md-surface-raised, rgba(15, 23, 42, 0.75));
2658
- transform: translateY(-1px);
2659
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
2862
+ color: var(--md-text, #f8fafc);
2660
2863
  }
2661
2864
  .markdy-speed-group {
2662
2865
  display: inline-flex;
@@ -2689,57 +2892,6 @@ function ensureSceneStyles(doc) {
2689
2892
  color: #ffffff !important;
2690
2893
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25) !important;
2691
2894
  }
2692
- .markdy-control-seek {
2693
- appearance: none;
2694
- -webkit-appearance: none;
2695
- width: 100%;
2696
- height: 5px;
2697
- border-radius: 999px;
2698
- background: linear-gradient(
2699
- to right,
2700
- var(--accent, #10b981) 0%,
2701
- var(--accent, #10b981) var(--seek-pct, 0%),
2702
- rgba(148, 163, 184, 0.22) var(--seek-pct, 0%),
2703
- rgba(148, 163, 184, 0.22) 100%
2704
- );
2705
- outline: none;
2706
- cursor: pointer;
2707
- margin: 0;
2708
- transition: height 0.15s ease;
2709
- }
2710
- .markdy-control-seek:hover {
2711
- height: 7px;
2712
- }
2713
- .markdy-control-seek::-webkit-slider-thumb {
2714
- appearance: none;
2715
- -webkit-appearance: none;
2716
- width: 12px;
2717
- height: 12px;
2718
- border-radius: 50%;
2719
- background: #ffffff;
2720
- border: 2px solid var(--accent, #10b981);
2721
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
2722
- cursor: pointer;
2723
- transition: transform 0.15s ease, box-shadow 0.15s ease;
2724
- }
2725
- .markdy-control-seek:hover::-webkit-slider-thumb {
2726
- transform: scale(1.25);
2727
- box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2728
- }
2729
- .markdy-control-seek::-moz-range-thumb {
2730
- width: 12px;
2731
- height: 12px;
2732
- border-radius: 50%;
2733
- background: #ffffff;
2734
- border: 2px solid var(--accent, #10b981);
2735
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
2736
- cursor: pointer;
2737
- transition: transform 0.15s ease, box-shadow 0.15s ease;
2738
- }
2739
- .markdy-control-seek:hover::-moz-range-thumb {
2740
- transform: scale(1.25);
2741
- box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2742
- }
2743
2895
  .markdy-control-time {
2744
2896
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
2745
2897
  font-size: 10px;
@@ -2925,44 +3077,61 @@ function ensureSceneStyles(doc) {
2925
3077
  .markdy-control-seek::-webkit-slider-runnable-track {
2926
3078
  width: 100%;
2927
3079
  height: 4.5px;
2928
- background: rgba(148, 163, 184, 0.35);
3080
+ background: linear-gradient(
3081
+ to right,
3082
+ var(--accent, #10b981) 0%,
3083
+ var(--accent, #10b981) var(--seek-pct, 0%),
3084
+ rgba(148, 163, 184, 0.35) var(--seek-pct, 0%),
3085
+ rgba(148, 163, 184, 0.35) 100%
3086
+ );
2929
3087
  border-radius: 9999px;
2930
- transition: background 0.15s ease;
2931
- }
2932
- .markdy-control-seek:hover::-webkit-slider-runnable-track {
2933
- background: rgba(148, 163, 184, 0.55);
2934
3088
  }
2935
3089
  .markdy-control-seek::-webkit-slider-thumb {
3090
+ appearance: none;
2936
3091
  -webkit-appearance: none;
2937
3092
  height: 13px;
2938
3093
  width: 13px;
2939
3094
  border-radius: 50%;
2940
- background: #2563eb;
3095
+ background: var(--accent, #10b981);
2941
3096
  border: 2px solid #ffffff;
2942
3097
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
2943
3098
  margin-top: -4.25px;
2944
3099
  cursor: grab;
2945
- transition: transform 0.1s ease, background 0.15s ease;
3100
+ transition: transform 0.1s ease, box-shadow 0.15s ease;
3101
+ }
3102
+ .markdy-control-seek:hover::-webkit-slider-thumb {
3103
+ transform: scale(1.2);
3104
+ box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2946
3105
  }
2947
3106
  .markdy-control-seek:active::-webkit-slider-thumb {
2948
3107
  cursor: grabbing;
2949
3108
  transform: scale(1.2);
2950
- background: #1d4ed8;
2951
3109
  }
2952
3110
  .markdy-control-seek::-moz-range-track {
2953
3111
  width: 100%;
2954
3112
  height: 4.5px;
2955
- background: rgba(148, 163, 184, 0.35);
3113
+ background: linear-gradient(
3114
+ to right,
3115
+ var(--accent, #10b981) 0%,
3116
+ var(--accent, #10b981) var(--seek-pct, 0%),
3117
+ rgba(148, 163, 184, 0.35) var(--seek-pct, 0%),
3118
+ rgba(148, 163, 184, 0.35) 100%
3119
+ );
2956
3120
  border-radius: 9999px;
2957
3121
  }
2958
3122
  .markdy-control-seek::-moz-range-thumb {
2959
3123
  height: 13px;
2960
3124
  width: 13px;
2961
3125
  border-radius: 50%;
2962
- background: #2563eb;
3126
+ background: var(--accent, #10b981);
2963
3127
  border: 2px solid #ffffff;
2964
3128
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
2965
3129
  cursor: grab;
3130
+ transition: transform 0.1s ease, box-shadow 0.15s ease;
3131
+ }
3132
+ .markdy-control-seek:hover::-moz-range-thumb {
3133
+ transform: scale(1.2);
3134
+ box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2966
3135
  }
2967
3136
 
2968
3137
  [data-markdy-theme="midnight"] .markdy-controls button,
@@ -2983,9 +3152,9 @@ function ensureSceneStyles(doc) {
2983
3152
  [data-markdy-theme="nebula"] .markdy-controls button:hover:not([aria-pressed="true"]),
2984
3153
  :root[data-theme="dark"] .markdy-controls button:hover:not([aria-pressed="true"]),
2985
3154
  .theme-dark .markdy-controls button:hover:not([aria-pressed="true"]) {
2986
- background: rgba(51, 65, 85, 0.95);
2987
- color: #f8fafc;
2988
- border-color: #94a3b8;
3155
+ background: var(--md-control-hover-bg, rgba(51, 65, 85, 0.95));
3156
+ color: var(--md-control-hover-text, #f8fafc);
3157
+ border-color: var(--md-control-hover-border, #94a3b8);
2989
3158
  }
2990
3159
  [data-markdy-theme="midnight"] .markdy-controls button[aria-pressed="true"],
2991
3160
  [data-markdy-theme="blueprint"] .markdy-controls button[aria-pressed="true"],
@@ -2994,10 +3163,10 @@ function ensureSceneStyles(doc) {
2994
3163
  [data-markdy-theme="nebula"] .markdy-controls button[aria-pressed="true"],
2995
3164
  :root[data-theme="dark"] .markdy-controls button[aria-pressed="true"],
2996
3165
  .theme-dark .markdy-controls button[aria-pressed="true"] {
2997
- background: #3b82f6 !important;
3166
+ background: var(--accent, #3b82f6) !important;
2998
3167
  color: #ffffff !important;
2999
- border-color: #3b82f6 !important;
3000
- box-shadow: 0 0 10px rgba(59, 130, 246, 0.5) !important;
3168
+ border-color: var(--accent, #3b82f6) !important;
3169
+ box-shadow: 0 0 10px var(--accent-glow, rgba(59, 130, 246, 0.5)) !important;
3001
3170
  }
3002
3171
  .markdy-footer a {
3003
3172
  transition: opacity 0.15s ease, color 0.15s ease;
@@ -3091,41 +3260,57 @@ function ensureSceneStyles(doc) {
3091
3260
  `;
3092
3261
  doc.head.appendChild(style);
3093
3262
  }
3094
- function applyThemeToScene(scene, theme) {
3095
- scene.style.background = theme.canvas;
3096
- scene.style.color = theme.text;
3097
- scene.style.setProperty("--md-canvas", theme.canvas);
3098
- scene.style.setProperty("--md-surface", theme.surface);
3099
- scene.style.setProperty("--md-surface-raised", theme.surfaceRaised);
3100
- scene.style.setProperty("--md-border", theme.border);
3101
- scene.style.setProperty("--md-text", theme.text);
3102
- scene.style.setProperty("--md-text-muted", theme.textMuted);
3103
- scene.style.setProperty("--md-paper", theme.paper ?? theme.canvas);
3104
- scene.style.setProperty("--md-ink", theme.ink ?? theme.text);
3105
- scene.style.setProperty("--md-muted", theme.muted ?? theme.textMuted);
3106
- scene.style.setProperty("--md-rule", theme.rule ?? theme.border);
3107
- scene.style.setProperty("--md-grid-minor", theme.gridMinor);
3108
- scene.style.setProperty("--md-grid-major", theme.gridMajor);
3109
- scene.style.setProperty("--md-vignette", theme.vignette);
3110
- scene.style.setProperty("--md-accent", theme.accent);
3111
- if (theme.link) scene.style.setProperty("--md-link", theme.link);
3112
- if (theme.soft) scene.style.setProperty("--md-soft", theme.soft);
3113
- if (theme.accentTint) scene.style.setProperty("--md-accent-tint", theme.accentTint);
3114
- scene.style.setProperty("--md-node-surface", theme.nodeSurface ?? theme.surface);
3115
- scene.style.setProperty("--md-node-surface-raised", theme.nodeSurfaceRaised ?? theme.surfaceRaised);
3116
- scene.style.setProperty("--md-hairline", theme.hairline ?? `color-mix(in srgb, ${theme.border} 50%, transparent)`);
3117
- scene.style.setProperty("--md-shadow", theme.shadow ?? "rgba(2, 6, 23, 0.55)");
3118
- if (theme.fonts?.title) scene.style.setProperty("--md-font-title", theme.fonts.title);
3119
- if (theme.fonts?.nodeName) scene.style.setProperty("--md-font-node", theme.fonts.nodeName);
3120
- if (theme.fonts?.mono) scene.style.setProperty("--md-font-mono", theme.fonts.mono);
3121
- if (theme.radiusMd) scene.style.setProperty("--md-radius-md", `${theme.radiusMd}px`);
3263
+ function applyThemeVariables(element, theme) {
3264
+ element.style.setProperty("--md-canvas", theme.canvas);
3265
+ element.style.setProperty("--md-surface", theme.surface);
3266
+ element.style.setProperty("--md-surface-raised", theme.surfaceRaised);
3267
+ element.style.setProperty("--md-border", theme.border);
3268
+ element.style.setProperty("--md-text", theme.text);
3269
+ element.style.setProperty("--md-text-muted", theme.textMuted);
3270
+ element.style.setProperty("--md-paper", theme.paper ?? theme.canvas);
3271
+ element.style.setProperty("--md-ink", theme.ink ?? theme.text);
3272
+ element.style.setProperty("--md-muted", theme.muted ?? theme.textMuted);
3273
+ element.style.setProperty("--md-rule", theme.rule ?? theme.border);
3274
+ element.style.setProperty("--md-grid-minor", theme.gridMinor);
3275
+ element.style.setProperty("--md-grid-major", theme.gridMajor);
3276
+ element.style.setProperty("--md-vignette", theme.vignette);
3277
+ element.style.setProperty("--md-accent", theme.accent);
3278
+ element.style.setProperty("--accent", theme.accent);
3279
+ element.style.setProperty("--accent-glow", `color-mix(in srgb, ${theme.accent} 40%, transparent)`);
3280
+ element.style.setProperty("--accent-light", theme.accentTint ?? `color-mix(in srgb, ${theme.accent} 15%, transparent)`);
3281
+ if (theme.link) element.style.setProperty("--md-link", theme.link);
3282
+ if (theme.soft) element.style.setProperty("--md-soft", theme.soft);
3283
+ if (theme.accentTint) element.style.setProperty("--md-accent-tint", theme.accentTint);
3284
+ element.style.setProperty("--md-node-surface", theme.nodeSurface ?? theme.surface);
3285
+ element.style.setProperty("--md-node-surface-raised", theme.nodeSurfaceRaised ?? theme.surfaceRaised);
3286
+ element.style.setProperty("--md-hairline", theme.hairline ?? `color-mix(in srgb, ${theme.border} 50%, transparent)`);
3287
+ element.style.setProperty("--md-shadow", theme.shadow ?? "rgba(2, 6, 23, 0.55)");
3288
+ element.style.setProperty("--md-footer-bg", "transparent");
3289
+ element.style.setProperty("--md-footer-border", "transparent");
3290
+ element.style.setProperty("--md-control-bg", theme.surfaceRaised);
3291
+ element.style.setProperty("--md-control-border", theme.border);
3292
+ element.style.setProperty("--md-control-text", theme.textMuted);
3293
+ element.style.setProperty("--md-control-hover-bg", theme.canvas);
3294
+ element.style.setProperty("--md-control-hover-text", theme.text);
3295
+ element.style.setProperty("--md-control-hover-border", theme.accent);
3296
+ element.style.setProperty("--md-segmented-bg", `color-mix(in srgb, ${theme.surfaceRaised} 60%, transparent)`);
3297
+ element.style.setProperty("--md-divider", theme.border);
3298
+ if (theme.fonts?.title) element.style.setProperty("--md-font-title", theme.fonts.title);
3299
+ if (theme.fonts?.nodeName) element.style.setProperty("--md-font-node", theme.fonts.nodeName);
3300
+ if (theme.fonts?.mono) element.style.setProperty("--md-font-mono", theme.fonts.mono);
3301
+ if (theme.radiusMd) element.style.setProperty("--md-radius-md", `${theme.radiusMd}px`);
3122
3302
  if (theme.spacing) {
3123
3303
  for (const [key, value] of Object.entries(theme.spacing)) {
3124
- scene.style.setProperty(`--md-space-${key}`, `${value}px`);
3304
+ element.style.setProperty(`--md-space-${key}`, `${value}px`);
3125
3305
  }
3126
3306
  }
3127
- scene.dataset.markdyTheme = theme.name;
3128
- if (theme.flatCards) scene.dataset.flat = "1";
3307
+ element.dataset.markdyTheme = theme.name;
3308
+ if (theme.flatCards) element.dataset.flat = "1";
3309
+ }
3310
+ function applyThemeToScene(scene, theme) {
3311
+ applyThemeVariables(scene, theme);
3312
+ scene.style.background = theme.canvas;
3313
+ scene.style.color = theme.text;
3129
3314
  }
3130
3315
 
3131
3316
  // src/diagram.ts
@@ -3277,7 +3462,7 @@ function createDiagram(opts) {
3277
3462
  function updateProgressBar(pct) {
3278
3463
  if (!progressEl) return;
3279
3464
  if (progressMode === "bar") {
3280
- progressEl.style.background = customColor ?? "#2563eb";
3465
+ progressEl.style.background = customColor ?? plan.theme.accent ?? "#2563eb";
3281
3466
  progressEl.style.transformOrigin = "left center";
3282
3467
  progressEl.style.transform = `scaleX(${pct})`;
3283
3468
  return;
@@ -3289,27 +3474,44 @@ function createDiagram(opts) {
3289
3474
  progressEl.style.webkitMask = progressEl.style.mask;
3290
3475
  progressEl.style.maskComposite = "exclude";
3291
3476
  progressEl.style.webkitMaskComposite = "xor";
3292
- progressEl.style.padding = "2px";
3477
+ progressEl.style.padding = "1px";
3293
3478
  }
3294
3479
  let footer = null;
3295
3480
  function ensureFooter() {
3296
3481
  if (footer) return footer;
3297
3482
  footer = document.createElement("div");
3298
3483
  footer.className = "markdy-footer";
3484
+ applyThemeVariables(footer, plan.theme);
3299
3485
  Object.assign(footer.style, {
3300
3486
  display: "flex",
3301
3487
  alignItems: "center",
3302
3488
  justifyContent: "space-between",
3303
- flexWrap: "wrap",
3489
+ flexWrap: "nowrap",
3304
3490
  gap: "6px",
3305
3491
  width: "100%",
3306
3492
  boxSizing: "border-box",
3307
- padding: "4px 6px 0"
3493
+ padding: "4px 6px 0",
3494
+ background: "transparent"
3308
3495
  });
3309
3496
  if (container.parentNode) container.parentNode.insertBefore(footer, container.nextSibling);
3310
3497
  else container.appendChild(footer);
3311
3498
  return footer;
3312
3499
  }
3500
+ let badge = null;
3501
+ if (copyright) {
3502
+ badge = document.createElement("a");
3503
+ badge.className = "markdy-badge";
3504
+ badge.href = `${MARKDY_PLAYGROUND_URL}#code=${encodeCodeForPlaygroundHash(code)}`;
3505
+ badge.target = "_blank";
3506
+ badge.rel = "noopener noreferrer";
3507
+ badge.title = "Open and edit in Markdy Playground";
3508
+ badge.textContent = "Powered by Markdy";
3509
+ ensureFooter().appendChild(badge);
3510
+ const badgeLink = badge;
3511
+ void compressMarkdyToUrlHash(code).then((hash) => {
3512
+ badgeLink.href = `${MARKDY_PLAYGROUND_URL}#code=${hash}`;
3513
+ }).catch(() => void 0);
3514
+ }
3313
3515
  ensureSceneStyles(document);
3314
3516
  ensureNodeStyles(document);
3315
3517
  const scene = document.createElement("div");
@@ -3326,17 +3528,6 @@ function createDiagram(opts) {
3326
3528
  });
3327
3529
  applyThemeToScene(scene, plan.theme);
3328
3530
  viewport.appendChild(scene);
3329
- let badge = null;
3330
- if (copyright) {
3331
- badge = document.createElement("a");
3332
- badge.className = "markdy-badge";
3333
- badge.href = "https://markdy.com";
3334
- badge.target = "_blank";
3335
- badge.rel = "noopener noreferrer";
3336
- badge.title = "Powered by Markdy - Diagram as Code";
3337
- badge.textContent = "Powered by Markdy";
3338
- viewport.appendChild(badge);
3339
- }
3340
3531
  const viewportTransform = document.createElement("div");
3341
3532
  viewportTransform.className = "markdy-viewport-transform";
3342
3533
  Object.assign(viewportTransform.style, {
@@ -3548,6 +3739,7 @@ function createDiagram(opts) {
3548
3739
  let controlsSeekBar = null;
3549
3740
  let controlsTimeEl = null;
3550
3741
  let controlsFitButton = null;
3742
+ let closeCodePanel = null;
3551
3743
  let fitViewActive = false;
3552
3744
  const ICONS2 = {
3553
3745
  play: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
@@ -3759,6 +3951,84 @@ function createDiagram(opts) {
3759
3951
  }
3760
3952
  diagram.seek(target);
3761
3953
  },
3954
+ setTheme(theme) {
3955
+ const newTheme = typeof theme === "string" ? resolveTheme(theme) : theme;
3956
+ plan.theme = newTheme;
3957
+ applyThemeToScene(scene, newTheme);
3958
+ if (footer) {
3959
+ applyThemeVariables(footer, newTheme);
3960
+ }
3961
+ for (const node of plan.nodes) {
3962
+ const el = nodeEls.get(node.id);
3963
+ if (el) {
3964
+ if (newTheme.flatCards) {
3965
+ el.style.setProperty("--md-shadow", "transparent");
3966
+ } else {
3967
+ el.style.removeProperty("--md-shadow");
3968
+ }
3969
+ if (newTheme.accentTint) {
3970
+ el.style.setProperty("--md-accent-tint", newTheme.accentTint);
3971
+ } else {
3972
+ el.style.removeProperty("--md-accent-tint");
3973
+ }
3974
+ const roleColor = newTheme.roles[node.role] ?? newTheme.accent;
3975
+ el.style.setProperty("--md-role-color", roleColor);
3976
+ applyDeclaredNodeStyle(el, node.style);
3977
+ }
3978
+ }
3979
+ const structuralSvg = structuralEdgeHost.querySelector("svg[data-markdy-edge-layer='1']");
3980
+ if (structuralSvg) {
3981
+ updateEdgeLayerTheme(structuralSvg, edgeRuntimes, newTheme, edgeSceneId);
3982
+ }
3983
+ const cameraSvg = cameraLayer.querySelector("svg[data-markdy-edge-layer='1']");
3984
+ if (cameraSvg) {
3985
+ updateEdgeLayerTheme(cameraSvg, edgeRuntimes, newTheme, edgeSceneId);
3986
+ }
3987
+ if (plan.treeBuses && plan.treeBuses.length > 0) {
3988
+ treeLayer.replaceChildren();
3989
+ mountTreeBuses(treeLayer, plan.treeBuses, newTheme);
3990
+ }
3991
+ if (plan.diagramType === "constellation" || plan.diagramType === "radar" || plan.diagramType === "timeline") {
3992
+ constellationLayer.replaceChildren();
3993
+ if (plan.diagramType === "constellation") {
3994
+ mountConstellationLayer(
3995
+ constellationLayer,
3996
+ plan.nodes,
3997
+ newTheme,
3998
+ { width: plan.meta.width, height: plan.meta.height }
3999
+ );
4000
+ } else if (plan.diagramType === "radar") {
4001
+ mountRadarLayer(
4002
+ constellationLayer,
4003
+ plan.nodes,
4004
+ newTheme,
4005
+ { width: plan.meta.width, height: plan.meta.height }
4006
+ );
4007
+ } else if (plan.diagramType === "timeline") {
4008
+ mountTimelineLayer(
4009
+ constellationLayer,
4010
+ plan.nodes,
4011
+ newTheme,
4012
+ { width: plan.meta.width, height: plan.meta.height }
4013
+ );
4014
+ }
4015
+ }
4016
+ if (plan.diagramType === "sequence") {
4017
+ updateSequenceLayerTheme(sequenceLayer, newTheme);
4018
+ }
4019
+ if (plan.groupBoundaries && plan.groupBoundaries.length > 0) {
4020
+ groupLayer.replaceChildren();
4021
+ mountGroupBoundaries(groupLayer, plan.groupBoundaries, newTheme);
4022
+ }
4023
+ if (plan.annotations && plan.annotations.length > 0) {
4024
+ annotationLayer.replaceChildren();
4025
+ mountAnnotations(annotationLayer, plan.annotations, plan.nodes, newTheme, {
4026
+ width: plan.meta.width,
4027
+ height: plan.meta.height
4028
+ });
4029
+ }
4030
+ if (totalDurationMs > 0) updateProgressBar(sceneMs / totalDurationMs);
4031
+ },
3762
4032
  destroy() {
3763
4033
  diagram.pause();
3764
4034
  if (keyboardShortcuts && typeof window !== "undefined") {
@@ -3767,8 +4037,9 @@ function createDiagram(opts) {
3767
4037
  for (const anim of allAnims) anim.cancel();
3768
4038
  resizeObserver?.disconnect();
3769
4039
  removeFullscreenListeners?.();
4040
+ closeCodePanel?.();
4041
+ closeCodePanel = null;
3770
4042
  if (progressEl?.parentNode === viewport) viewport.removeChild(progressEl);
3771
- if (badge?.parentNode) badge.parentNode.removeChild(badge);
3772
4043
  if (footer?.parentNode) footer.parentNode.removeChild(footer);
3773
4044
  if (viewport.parentNode === container) container.removeChild(viewport);
3774
4045
  }
@@ -3924,6 +4195,7 @@ function createDiagram(opts) {
3924
4195
  const targetThemes = isDark ? lightThemes : darkThemes;
3925
4196
  const nextTheme = targetThemes[Math.floor(Math.random() * targetThemes.length)];
3926
4197
  flashControlLabel(button, nextTheme);
4198
+ diagram.setTheme(nextTheme);
3927
4199
  container.dispatchEvent(
3928
4200
  new CustomEvent("markdy-theme-switch", {
3929
4201
  bubbles: true,
@@ -3935,19 +4207,33 @@ function createDiagram(opts) {
3935
4207
  }
3936
4208
  function tintCode(raw) {
3937
4209
  return raw.split("\n").map((line) => {
3938
- if (/^\s*\/\//.test(line)) {
3939
- return `<span class="t-comment">${escHtml(line)}</span>`;
3940
- }
3941
- line = line.replace(/(->|<-|~>|--|<~)/g, '<span class="t-edge">$1</span>');
3942
- line = line.replace(/"([^"]*)"/g, '"<span class="t-string">$1</span>"');
3943
- line = line.replace(
3944
- /^(\s*)(scene|beat|group|player|show|frame|glow|focus|layout|edge|annotation|start|end|decision|service|browser|gateway|database|cache|queue|worker|function|pod|user|client|hub|station|metric|mobile)\b/,
3945
- '$1<span class="t-keyword">$2</span>'
4210
+ if (/^\s*\/\//.test(line)) return `<span class="t-comment">${escHtml(line)}</span>`;
4211
+ const keyword = line.match(
4212
+ /^(\s*)(scene|beat|group|player|show|frame|glow|focus|layout|edge|annotation|start|end|decision|service|browser|gateway|database|cache|queue|worker|function|pod|user|client|hub|station|metric|mobile)\b/
3946
4213
  );
3947
- line = line.replace(/\b(\d[\d.]*(?:ms|px|s)?)\b/g, '<span class="t-number">$1</span>');
3948
- return line;
4214
+ const prefix = keyword ? `${escHtml(keyword[1])}<span class="t-keyword">${keyword[2]}</span>` : "";
4215
+ return prefix + tintInlineCode(line.slice(keyword?.[0].length ?? 0));
3949
4216
  }).join("\n");
3950
4217
  }
4218
+ function tintInlineCode(raw) {
4219
+ const tokenPattern = /"[^"]*"|->|<-|~>|--|<~|\b\d[\d.]*(?:ms|px|s)?\b/g;
4220
+ let output = "";
4221
+ let cursor = 0;
4222
+ for (const match of raw.matchAll(tokenPattern)) {
4223
+ const token = match[0];
4224
+ const index = match.index ?? 0;
4225
+ output += escHtml(raw.slice(cursor, index));
4226
+ if (token.startsWith('"')) {
4227
+ output += `"<span class="t-string">${escHtml(token.slice(1, -1))}</span>"`;
4228
+ } else if (/^\d/.test(token)) {
4229
+ output += `<span class="t-number">${token}</span>`;
4230
+ } else {
4231
+ output += `<span class="t-edge">${escHtml(token)}</span>`;
4232
+ }
4233
+ cursor = index + token.length;
4234
+ }
4235
+ return output + escHtml(raw.slice(cursor));
4236
+ }
3951
4237
  function escHtml(str) {
3952
4238
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3953
4239
  }
@@ -3957,6 +4243,7 @@ function createDiagram(opts) {
3957
4243
  button.className = "markdy-control-code";
3958
4244
  button.setAttribute("aria-haspopup", "dialog");
3959
4245
  button.addEventListener("click", () => {
4246
+ closeCodePanel?.();
3960
4247
  const overlay = document.createElement("div");
3961
4248
  overlay.className = "markdy-code-panel-overlay";
3962
4249
  overlay.setAttribute("role", "dialog");
@@ -4014,24 +4301,28 @@ function createDiagram(opts) {
4014
4301
  overlay.appendChild(panel);
4015
4302
  document.body.appendChild(overlay);
4016
4303
  closeBtn.focus();
4304
+ let closing = false;
4305
+ function removePanel() {
4306
+ document.removeEventListener("keydown", handleEscape);
4307
+ overlay.remove();
4308
+ if (closeCodePanel === removePanel) closeCodePanel = null;
4309
+ }
4017
4310
  function closePanel() {
4311
+ if (closing) return;
4312
+ closing = true;
4018
4313
  overlay.dataset.closing = "1";
4019
- overlay.addEventListener("animationend", () => overlay.remove(), { once: true });
4314
+ document.removeEventListener("keydown", handleEscape);
4315
+ overlay.addEventListener("animationend", removePanel, { once: true });
4020
4316
  }
4021
4317
  closeBtn.addEventListener("click", closePanel);
4022
4318
  overlay.addEventListener("click", (e) => {
4023
4319
  if (e.target === overlay) closePanel();
4024
4320
  });
4025
4321
  function handleEscape(e) {
4026
- if (e.key === "Escape") {
4027
- closePanel();
4028
- document.removeEventListener("keydown", handleEscape);
4029
- }
4322
+ if (e.key === "Escape") closePanel();
4030
4323
  }
4031
4324
  document.addEventListener("keydown", handleEscape);
4032
- overlay.addEventListener("animationend", () => {
4033
- if (overlay.dataset.closing === "1") document.removeEventListener("keydown", handleEscape);
4034
- }, { once: true });
4325
+ closeCodePanel = removePanel;
4035
4326
  });
4036
4327
  toolbar.appendChild(button);
4037
4328
  }
@@ -4238,7 +4529,7 @@ function createDiagram(opts) {
4238
4529
  mountCodeControl(toolsGroup);
4239
4530
  }
4240
4531
  if (toolsGroup.children.length > 0) toolbar.appendChild(toolsGroup);
4241
- ensureFooter().appendChild(toolbar);
4532
+ ensureFooter().insertBefore(toolbar, badge);
4242
4533
  syncControls();
4243
4534
  }
4244
4535
  viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Browser renderer for diagram-native animated MarkdyScript architecture diagrams, built on the Web Animations API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,14 +45,14 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "html2canvas": "^1.4.1",
48
- "@markdy/core": "1.0.18"
48
+ "@markdy/core": "1.0.20"
49
49
  },
50
50
  "devDependencies": {
51
51
  "jsdom": "^29.1.1",
52
52
  "tsup": "^8.5.1",
53
53
  "typescript": "^5.9.3",
54
54
  "vitest": "^4.1.7",
55
- "@markdy/stdlib-systems": "1.0.18"
55
+ "@markdy/stdlib-systems": "1.0.20"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",