@markdy/renderer-dom 1.0.19 → 1.0.21

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/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.
@@ -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
@@ -188,11 +189,24 @@ function toPathD(points, cornerRadius = 14, existingPaths = []) {
188
189
  }
189
190
  return parts.join(" ");
190
191
  }
191
- function selfLoopPath(rect) {
192
+ function selfLoopPath(rect, bounds) {
192
193
  const top = rect.y;
194
+ const loopHeight = 36;
195
+ if (bounds && top - loopHeight < 16) {
196
+ const bottom = rect.y + rect.height;
197
+ const left2 = rect.x + rect.width * 0.3;
198
+ const right2 = rect.x + rect.width * 0.7;
199
+ const apex2 = Math.min(bounds.height - 16, bottom + loopHeight);
200
+ return [
201
+ { x: round1(right2), y: round1(bottom) },
202
+ { x: round1(right2), y: round1(apex2) },
203
+ { x: round1(left2), y: round1(apex2) },
204
+ { x: round1(left2), y: round1(bottom) }
205
+ ];
206
+ }
193
207
  const left = rect.x + rect.width * 0.3;
194
208
  const right = rect.x + rect.width * 0.7;
195
- const apex = top - 40;
209
+ const apex = Math.max(12, top - loopHeight);
196
210
  return [
197
211
  { x: round1(left), y: round1(top) },
198
212
  { x: round1(left), y: round1(apex) },
@@ -316,10 +330,112 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
316
330
  const targetCenter = rectCenter(targetRect);
317
331
  const dx = targetCenter.x - sourceCenter.x;
318
332
  const dy = targetCenter.y - sourceCenter.y;
333
+ const isDominantVertical = Math.abs(dy) >= Math.abs(dx);
319
334
  const stubLen = 18;
320
335
  const allObstacles = obstacles;
321
336
  const candidates = [];
322
- if (dx >= 30) {
337
+ if (dy >= 20) {
338
+ let sX = sourceCenter.x;
339
+ let tX = targetCenter.x;
340
+ if (dx > 16) {
341
+ sX = clamp(sourceCenter.x + (lane > 0 ? Math.abs(laneShift) : 6), sourceRect.x1 + 14, sourceRect.x2 - 14);
342
+ tX = clamp(targetCenter.x - (lane > 0 ? Math.abs(laneShift) : 6), targetRect.x1 + 14, targetRect.x2 - 14);
343
+ } else if (dx < -16) {
344
+ sX = clamp(sourceCenter.x - (lane > 0 ? Math.abs(laneShift) : 6), sourceRect.x1 + 14, sourceRect.x2 - 14);
345
+ tX = clamp(targetCenter.x + (lane > 0 ? Math.abs(laneShift) : 6), targetRect.x1 + 14, targetRect.x2 - 14);
346
+ } else {
347
+ sX = clamp(sourceCenter.x + laneShift, sourceRect.x1 + 14, sourceRect.x2 - 14);
348
+ tX = clamp(targetCenter.x + laneShift, targetRect.x1 + 14, targetRect.x2 - 14);
349
+ }
350
+ const sPort = { x: sX, y: sourceRect.y2 };
351
+ const tPort = { x: tX, y: targetRect.y1 };
352
+ if (Math.abs(sX - tX) < 1) {
353
+ candidates.push([sPort, tPort]);
354
+ } else {
355
+ const midY = round1((sPort.y + tPort.y) / 2 + laneShift);
356
+ candidates.push([
357
+ sPort,
358
+ { x: sPort.x, y: midY },
359
+ { x: tPort.x, y: midY },
360
+ tPort
361
+ ]);
362
+ }
363
+ let minObstacleX = Math.min(sourceRect.x1, targetRect.x1);
364
+ let maxObstacleX = Math.max(sourceRect.x2, targetRect.x2);
365
+ for (const o of allObstacles) {
366
+ if (o.y2 > sourceRect.y2 && o.y1 < targetRect.y1) {
367
+ minObstacleX = Math.min(minObstacleX, o.x1);
368
+ maxObstacleX = Math.max(maxObstacleX, o.x2);
369
+ }
370
+ }
371
+ const bypassXLeft = Math.max(16, minObstacleX - 24 - Math.abs(laneShift));
372
+ const bypassXRight = Math.min(bounds.width - 16, maxObstacleX + 24 + Math.abs(laneShift));
373
+ candidates.push([
374
+ sPort,
375
+ { x: sPort.x, y: sPort.y + 12 },
376
+ { x: bypassXLeft, y: sPort.y + 12 },
377
+ { x: bypassXLeft, y: tPort.y - 12 },
378
+ { x: tPort.x, y: tPort.y - 12 },
379
+ tPort
380
+ ]);
381
+ candidates.push([
382
+ sPort,
383
+ { x: sPort.x, y: sPort.y + 12 },
384
+ { x: bypassXRight, y: sPort.y + 12 },
385
+ { x: bypassXRight, y: tPort.y - 12 },
386
+ { x: tPort.x, y: tPort.y - 12 },
387
+ tPort
388
+ ]);
389
+ } else if (dy <= -20) {
390
+ let sX = sourceCenter.x;
391
+ let tX = targetCenter.x;
392
+ if (dx > 16) {
393
+ sX = clamp(sourceCenter.x + (lane > 0 ? Math.abs(laneShift) : 6), sourceRect.x1 + 14, sourceRect.x2 - 14);
394
+ tX = clamp(targetCenter.x - (lane > 0 ? Math.abs(laneShift) : 6), targetRect.x1 + 14, targetRect.x2 - 14);
395
+ } else if (dx < -16) {
396
+ sX = clamp(sourceCenter.x - (lane > 0 ? Math.abs(laneShift) : 6), sourceRect.x1 + 14, sourceRect.x2 - 14);
397
+ tX = clamp(targetCenter.x + (lane > 0 ? Math.abs(laneShift) : 6), targetRect.x1 + 14, targetRect.x2 - 14);
398
+ } else {
399
+ sX = clamp(sourceCenter.x + laneShift, sourceRect.x1 + 14, sourceRect.x2 - 14);
400
+ tX = clamp(targetCenter.x + laneShift, targetRect.x1 + 14, targetRect.x2 - 14);
401
+ }
402
+ const sPort = { x: sX, y: sourceRect.y1 };
403
+ const tPort = { x: tX, y: targetRect.y2 };
404
+ if (Math.abs(sX - tX) < 1) {
405
+ candidates.push([sPort, tPort]);
406
+ } else {
407
+ const midY = round1((sPort.y + tPort.y) / 2 + laneShift);
408
+ candidates.push([
409
+ sPort,
410
+ { x: sPort.x, y: midY },
411
+ { x: tPort.x, y: midY },
412
+ tPort
413
+ ]);
414
+ }
415
+ let minObstacleX = Math.min(sourceRect.x1, targetRect.x1);
416
+ let maxObstacleX = Math.max(sourceRect.x2, targetRect.x2);
417
+ for (const o of allObstacles) {
418
+ if (o.y1 < sourceRect.y2 && o.y2 > targetRect.y1) {
419
+ minObstacleX = Math.min(minObstacleX, o.x1);
420
+ maxObstacleX = Math.max(maxObstacleX, o.x2);
421
+ }
422
+ }
423
+ const bypassXLeft = Math.max(16, minObstacleX - 28 - Math.abs(laneShift));
424
+ const bypassXRight = Math.min(bounds.width - 16, maxObstacleX + 28 + Math.abs(laneShift));
425
+ candidates.push([
426
+ { x: sourceRect.x1, y: sourceCenter.y },
427
+ { x: bypassXLeft, y: sourceCenter.y },
428
+ { x: bypassXLeft, y: targetCenter.y },
429
+ { x: targetRect.x1, y: targetCenter.y }
430
+ ]);
431
+ candidates.push([
432
+ { x: sourceRect.x2, y: sourceCenter.y },
433
+ { x: bypassXRight, y: sourceCenter.y },
434
+ { x: bypassXRight, y: targetCenter.y },
435
+ { x: targetRect.x2, y: targetCenter.y }
436
+ ]);
437
+ }
438
+ if (dx >= 20) {
323
439
  let sY = sourceCenter.y;
324
440
  let tY = targetCenter.y;
325
441
  if (Math.abs(dy) > 16) {
@@ -334,15 +450,16 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
334
450
  const tPort = { x: targetRect.x1, y: tY };
335
451
  if (Math.abs(sY - tY) < 1) {
336
452
  candidates.push([sPort, tPort]);
453
+ } else {
454
+ const rawMidX = (sPort.x + tPort.x) / 2 + laneShift;
455
+ const midX = round1(clamp(rawMidX, sourceRect.x2 + 8, targetRect.x1 - 8));
456
+ candidates.push([
457
+ sPort,
458
+ { x: midX, y: sY },
459
+ { x: midX, y: tY },
460
+ tPort
461
+ ]);
337
462
  }
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
463
  let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
347
464
  for (const o of allObstacles) {
348
465
  if (o.x2 > sourceRect.x2 && o.x1 < targetRect.x1) {
@@ -356,7 +473,7 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
356
473
  { x: targetCenter.x, y: bypassYTop },
357
474
  { x: targetCenter.x, y: targetRect.y1 }
358
475
  ]);
359
- } else if (dx < -30) {
476
+ } else if (dx <= -20) {
360
477
  const sX = clamp(sourceCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), sourceRect.x1 + 16, sourceRect.x2 - 16);
361
478
  const tX = clamp(targetCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), targetRect.x1 + 16, targetRect.x2 - 16);
362
479
  let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
@@ -401,23 +518,6 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
401
518
  { x: midX, y: tY },
402
519
  tPortR
403
520
  ]);
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
521
  }
422
522
  for (const obstacle of allObstacles) {
423
523
  const bypassYTop = Math.max(16, obstacle.y1 - 18 - Math.abs(laneShift));
@@ -443,6 +543,12 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
443
543
  tP
444
544
  ]);
445
545
  }
546
+ if (candidates.length === 0) {
547
+ candidates.push([
548
+ { x: sourceCenter.x, y: sourceCenter.y },
549
+ { x: targetCenter.x, y: targetCenter.y }
550
+ ]);
551
+ }
446
552
  let best = candidates[0];
447
553
  let bestScore = Number.POSITIVE_INFINITY;
448
554
  for (const candidate of candidates) {
@@ -450,7 +556,27 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
450
556
  const hits = countPathIntersections(cleaned, allObstacles);
451
557
  const bends = routeBends(cleaned);
452
558
  const length = routeLength(cleaned);
453
- const score = hits * 1e6 + bends * 500 + length;
559
+ let portPenalty = 0;
560
+ const startP = cleaned[0];
561
+ const endP = cleaned[cleaned.length - 1];
562
+ if (isDominantVertical) {
563
+ if (dy >= 20) {
564
+ if (Math.abs(startP.y - sourceRect.y1) < 1) portPenalty += 25e3;
565
+ if (Math.abs(endP.y - targetRect.y2) < 1) portPenalty += 25e3;
566
+ } else if (dy <= -20) {
567
+ if (Math.abs(startP.y - sourceRect.y2) < 1) portPenalty += 25e3;
568
+ if (Math.abs(endP.y - targetRect.y1) < 1) portPenalty += 25e3;
569
+ }
570
+ } else {
571
+ if (dx >= 20) {
572
+ if (Math.abs(startP.x - sourceRect.x1) < 1) portPenalty += 25e3;
573
+ if (Math.abs(endP.x - targetRect.x2) < 1) portPenalty += 25e3;
574
+ } else if (dx <= -20) {
575
+ if (Math.abs(startP.x - sourceRect.x2) < 1) portPenalty += 25e3;
576
+ if (Math.abs(endP.x - targetRect.x1) < 1) portPenalty += 25e3;
577
+ }
578
+ }
579
+ const score = hits * 1e6 + portPenalty + bends * 500 + length;
454
580
  if (score < bestScore) {
455
581
  bestScore = score;
456
582
  best = cleaned;
@@ -522,9 +648,14 @@ function ensureEdgeLayer(scene) {
522
648
  }
523
649
  function ensureDefs(svg, theme, id) {
524
650
  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");
651
+ let defs = svg.querySelector(`defs[${key}='1']`);
652
+ if (!defs) {
653
+ defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
654
+ defs.setAttribute(key, "1");
655
+ svg.prepend(defs);
656
+ } else {
657
+ defs.replaceChildren();
658
+ }
528
659
  for (const [kind, color] of Object.entries(theme.edges)) {
529
660
  const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
530
661
  arrow.setAttribute("id", `${id}-arrow-${kind}`);
@@ -571,7 +702,27 @@ function ensureDefs(svg, theme, id) {
571
702
  filter.appendChild(turb);
572
703
  filter.appendChild(disp);
573
704
  defs.appendChild(filter);
574
- svg.prepend(defs);
705
+ }
706
+ function updateEdgeLayerTheme(svg, edgeRuntimeMap, theme, sceneId) {
707
+ ensureDefs(svg, theme, sceneId);
708
+ const isDark = isDarkTheme(theme);
709
+ for (const runtime of edgeRuntimeMap.values()) {
710
+ const color = theme.edges[runtime.kind];
711
+ runtime.color = color;
712
+ runtime.path.setAttribute("stroke", color);
713
+ if (runtime.kind !== "dependency") {
714
+ runtime.path.style.filter = `drop-shadow(0 0 4px ${translucentColor(color, "44")})`;
715
+ }
716
+ runtime.dot.setAttribute("fill", color);
717
+ runtime.dot.style.filter = `drop-shadow(0 0 6px ${color}) drop-shadow(0 0 12px ${color}88)`;
718
+ if (runtime.labelPlate) {
719
+ runtime.labelPlate.setAttribute("fill", theme.canvas ?? theme.surface ?? "#ffffff");
720
+ runtime.labelPlate.setAttribute("stroke", translucentColor(color, "28"));
721
+ }
722
+ if (runtime.label) {
723
+ runtime.label.setAttribute("fill", computeEdgeLabelColor(color, isDark));
724
+ }
725
+ }
575
726
  }
576
727
  function isDarkTheme(theme) {
577
728
  if (theme.name === "paper" || theme.name === "editorial" || theme.name === "sketchy") {
@@ -599,7 +750,7 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
599
750
  const color = theme.edges[kind];
600
751
  const style = EDGE_STYLES[kind];
601
752
  const isSelfLoop = from.id === to.id;
602
- const points = isSelfLoop ? dedupePoints(selfLoopPath(from)) : dedupePoints(routeEdgePoints(from, to, routeObstacles, bounds, lane));
753
+ const points = isSelfLoop ? dedupePoints(selfLoopPath(from, bounds)) : dedupePoints(routeEdgePoints(from, to, routeObstacles, bounds, lane));
603
754
  const d = toPathD(points, 14, existingPaths);
604
755
  const len = polylineLength(points);
605
756
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
@@ -1635,6 +1786,10 @@ function ensureNodeStyles(doc) {
1635
1786
  border-radius: 4px;
1636
1787
  transform: rotate(0deg);
1637
1788
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
1789
+ 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)));
1790
+ }
1791
+ .markdy-node[data-shape="diamond"][data-focal="1"] {
1792
+ filter: drop-shadow(0 0 1.5px var(--md-accent)) drop-shadow(0 4px 14px var(--md-shadow, rgba(2, 6, 23, 0.15)));
1638
1793
  }
1639
1794
  .markdy-node[data-shape="diamond"] .markdy-node__body {
1640
1795
  justify-content: center;
@@ -1650,6 +1805,13 @@ function ensureNodeStyles(doc) {
1650
1805
  border-radius: 999px;
1651
1806
  min-height: 54px;
1652
1807
  }
1808
+ .markdy-node[data-shape="pill"] .markdy-node__body {
1809
+ justify-content: center;
1810
+ padding: 0 20px;
1811
+ }
1812
+ .markdy-node[data-shape="pill"] .markdy-node__label {
1813
+ text-align: center;
1814
+ }
1653
1815
  .markdy-node[data-shape="circle"],
1654
1816
  .markdy-node[data-kind="dot"] {
1655
1817
  border-radius: 50%;
@@ -1719,9 +1881,10 @@ function ensureNodeStyles(doc) {
1719
1881
  background: var(--md-node-surface, var(--md-surface));
1720
1882
  }
1721
1883
  .markdy-node[data-focal="1"] {
1722
- background: color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 100%, transparent);
1723
1884
  box-shadow:
1724
- inset 0 0 0 1px color-mix(in srgb, var(--md-accent) 55%, transparent);
1885
+ 0 2px 10px -2px var(--md-shadow, rgba(2, 6, 23, 0.18)),
1886
+ inset 0 0 0 1.5px var(--md-accent),
1887
+ 0 0 0 1px color-mix(in srgb, var(--md-accent) 25%, transparent);
1725
1888
  }
1726
1889
  .markdy-scene-root[data-markdy-theme="nebula"] .markdy-node {
1727
1890
  border: 1px solid color-mix(in srgb, var(--md-role-color, var(--md-accent)) 34%, transparent);
@@ -1732,7 +1895,7 @@ function ensureNodeStyles(doc) {
1732
1895
  .markdy-scene-root[data-markdy-theme="nebula"] .markdy-node[data-focal="1"] {
1733
1896
  box-shadow:
1734
1897
  0 0 34px -8px color-mix(in srgb, var(--md-accent) 78%, transparent),
1735
- inset 0 0 0 1px var(--md-accent);
1898
+ inset 0 0 0 1.5px var(--md-accent);
1736
1899
  }
1737
1900
  .markdy-scene-root[data-flat="1"] .markdy-node {
1738
1901
  box-shadow: inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent));
@@ -1741,11 +1904,7 @@ function ensureNodeStyles(doc) {
1741
1904
  box-shadow: inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent));
1742
1905
  }
1743
1906
  .markdy-scene-root[data-flat="1"] .markdy-node[data-focal="1"] {
1744
- box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent) 60%, transparent);
1745
- background:
1746
- 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));
1907
+ box-shadow: inset 0 0 0 1.5px var(--md-accent);
1749
1908
  }
1750
1909
  .markdy-node[data-kind="external"] {
1751
1910
  border: 1.2px dashed color-mix(in srgb, var(--md-ink, var(--md-text)) 30%, transparent);
@@ -1768,17 +1927,32 @@ function ensureNodeStyles(doc) {
1768
1927
  border-color: #ff5a36;
1769
1928
  box-shadow: 0 0 18px -8px rgba(255, 90, 54, 0.45);
1770
1929
  }
1930
+ .markdy-scene-root[data-markdy-theme="terminal"] .markdy-node[data-shape="diamond"] {
1931
+ filter: drop-shadow(0 0 1px #2b2b2b);
1932
+ }
1933
+ .markdy-scene-root[data-markdy-theme="terminal"] .markdy-node[data-shape="diamond"][data-focal="1"] {
1934
+ filter: drop-shadow(0 0 2px #ff5a36) drop-shadow(0 0 12px rgba(255, 90, 54, 0.35));
1935
+ }
1771
1936
  .markdy-scene-root[data-markdy-theme="sketchy"] .markdy-node {
1772
1937
  background: #ffffff;
1773
1938
  border: 1.5px solid #2d3142;
1774
1939
  box-shadow: 3px 3px 0 rgba(45, 49, 66, 0.10);
1775
1940
  border-radius: var(--md-radius-md, 4px);
1776
1941
  }
1942
+ .markdy-scene-root[data-markdy-theme="sketchy"] .markdy-node[data-shape="diamond"] {
1943
+ filter: drop-shadow(0 0 1px #2d3142) drop-shadow(3px 3px 0 rgba(45, 49, 66, 0.12));
1944
+ }
1777
1945
  .markdy-scene-root[data-markdy-theme="sketchy"] .markdy-node[data-focal="1"] {
1778
1946
  border-color: #eb6c36;
1779
1947
  background: rgba(235, 108, 54, 0.06);
1780
1948
  box-shadow: 3px 3px 0 rgba(235, 108, 54, 0.12);
1781
1949
  }
1950
+ .markdy-scene-root[data-markdy-theme="nebula"] .markdy-node[data-shape="diamond"] {
1951
+ 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));
1952
+ }
1953
+ .markdy-scene-root[data-markdy-theme="nebula"] .markdy-node[data-shape="diamond"][data-focal="1"] {
1954
+ filter: drop-shadow(0 0 2px var(--md-accent)) drop-shadow(0 0 24px -2px color-mix(in srgb, var(--md-accent) 70%, transparent));
1955
+ }
1782
1956
  `;
1783
1957
  doc.head.appendChild(style);
1784
1958
  }
@@ -2309,6 +2483,52 @@ function mountSequenceLayer(layer, nodes, messages, activations, theme, bounds)
2309
2483
  for (const animation of animations) animation.pause();
2310
2484
  return animations;
2311
2485
  }
2486
+ function updateSequenceLayerTheme(layer, theme) {
2487
+ const svg = layer.querySelector("svg");
2488
+ if (!svg) return;
2489
+ const defs = svg.querySelector("defs");
2490
+ if (defs) {
2491
+ Array.from(defs.querySelectorAll("marker")).forEach((marker) => {
2492
+ const id = marker.getAttribute("id") || "";
2493
+ const path = marker.querySelector("path");
2494
+ if (path) {
2495
+ if (id.includes("response")) {
2496
+ path.setAttribute("stroke", theme.edges.response);
2497
+ } else if (id.includes("event")) {
2498
+ path.setAttribute("fill", theme.edges.event);
2499
+ } else {
2500
+ path.setAttribute("fill", theme.edges.request);
2501
+ }
2502
+ }
2503
+ });
2504
+ }
2505
+ Array.from(svg.querySelectorAll(".markdy-sequence-lifeline")).forEach((line) => {
2506
+ line.setAttribute("stroke", theme.hairline ?? theme.border);
2507
+ });
2508
+ Array.from(svg.querySelectorAll(".markdy-sequence-activation")).forEach((bar) => {
2509
+ bar.setAttribute("fill", theme.accent);
2510
+ bar.style.filter = `drop-shadow(0 0 6px ${theme.accent}66)`;
2511
+ });
2512
+ Array.from(svg.querySelectorAll(".markdy-sequence-message")).forEach((group) => {
2513
+ const line = group.querySelector("line");
2514
+ if (line) {
2515
+ const markerEnd = line.getAttribute("marker-end") || "";
2516
+ if (markerEnd.includes("response")) line.setAttribute("stroke", theme.edges.response);
2517
+ else if (markerEnd.includes("event")) line.setAttribute("stroke", theme.edges.event);
2518
+ else line.setAttribute("stroke", theme.edges.request);
2519
+ }
2520
+ const plate = group.querySelector("rect");
2521
+ if (plate) {
2522
+ plate.setAttribute("fill", theme.labelPlate ?? theme.surface);
2523
+ plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 70%, transparent)`);
2524
+ }
2525
+ const text = group.querySelector("text");
2526
+ if (text) {
2527
+ text.setAttribute("fill", theme.text);
2528
+ if (theme.fonts?.mono) text.setAttribute("font-family", theme.fonts.mono);
2529
+ }
2530
+ });
2531
+ }
2312
2532
 
2313
2533
  // src/tree.ts
2314
2534
  function mountTreeBuses(layer, buses, theme) {
@@ -2504,19 +2724,20 @@ function ensureSceneStyles(doc) {
2504
2724
  }
2505
2725
  .markdy-footer {
2506
2726
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
2727
+ position: relative;
2507
2728
  width: 100%;
2508
2729
  box-sizing: border-box;
2509
2730
  display: flex;
2510
2731
  align-items: center;
2511
2732
  justify-content: space-between;
2512
2733
  gap: 8px;
2513
- padding: 6px 12px;
2734
+ padding: 8px 12px 4px;
2514
2735
  color: var(--md-text, #f8fafc);
2515
- background: var(--md-footer-bg, rgba(15, 23, 42, 0.85));
2516
- backdrop-filter: blur(16px);
2517
- -webkit-backdrop-filter: blur(16px);
2518
- border-top: 1px solid var(--md-footer-border, rgba(148, 163, 184, 0.15));
2519
- z-index: 20;
2736
+ background: transparent;
2737
+ backdrop-filter: none;
2738
+ -webkit-backdrop-filter: none;
2739
+ border-top: none;
2740
+ z-index: 100;
2520
2741
  }
2521
2742
  .markdy-controls {
2522
2743
  display: flex;
@@ -2529,6 +2750,8 @@ function ensureSceneStyles(doc) {
2529
2750
  max-width: 100%;
2530
2751
  overflow-x: auto;
2531
2752
  scrollbar-width: none;
2753
+ padding: 4px 2px;
2754
+ margin: -4px -2px;
2532
2755
  }
2533
2756
  .markdy-controls::-webkit-scrollbar {
2534
2757
  display: none;
@@ -2590,6 +2813,8 @@ function ensureSceneStyles(doc) {
2590
2813
  transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
2591
2814
  }
2592
2815
  .markdy-controls button:hover:not([aria-pressed="true"]) {
2816
+ position: relative;
2817
+ z-index: 2;
2593
2818
  background: var(--md-control-hover-bg, rgba(255, 255, 255, 0.1));
2594
2819
  color: var(--md-control-hover-text, #f8fafc);
2595
2820
  border-color: var(--md-control-hover-border, rgba(148, 163, 184, 0.35));
@@ -2597,12 +2822,16 @@ function ensureSceneStyles(doc) {
2597
2822
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.12);
2598
2823
  }
2599
2824
  .markdy-controls button[aria-pressed="true"] {
2825
+ position: relative;
2826
+ z-index: 2;
2600
2827
  background: var(--accent, #10b981) !important;
2601
2828
  color: #ffffff !important;
2602
2829
  border-color: var(--accent, #10b981) !important;
2603
2830
  box-shadow: 0 0 10px var(--accent-glow, rgba(16, 185, 129, 0.4)) !important;
2604
2831
  }
2605
2832
  .markdy-controls button:focus-visible {
2833
+ position: relative;
2834
+ z-index: 3;
2606
2835
  outline: 2px solid var(--accent, #10b981);
2607
2836
  outline-offset: 1px;
2608
2837
  }
@@ -2696,10 +2925,10 @@ function ensureSceneStyles(doc) {
2696
2925
  .markdy-code-panel-overlay {
2697
2926
  position: fixed;
2698
2927
  inset: 0;
2699
- z-index: 9000;
2700
- background: rgba(2, 6, 23, 0.55);
2701
- backdrop-filter: blur(3px);
2702
- -webkit-backdrop-filter: blur(3px);
2928
+ z-index: 100000;
2929
+ background: rgba(2, 6, 23, 0.65);
2930
+ backdrop-filter: blur(4px);
2931
+ -webkit-backdrop-filter: blur(4px);
2703
2932
  display: flex;
2704
2933
  align-items: center;
2705
2934
  justify-content: center;
@@ -3071,14 +3300,15 @@ function applyThemeVariables(element, theme) {
3071
3300
  element.style.setProperty("--md-node-surface-raised", theme.nodeSurfaceRaised ?? theme.surfaceRaised);
3072
3301
  element.style.setProperty("--md-hairline", theme.hairline ?? `color-mix(in srgb, ${theme.border} 50%, transparent)`);
3073
3302
  element.style.setProperty("--md-shadow", theme.shadow ?? "rgba(2, 6, 23, 0.55)");
3074
- element.style.setProperty("--md-footer-bg", `color-mix(in srgb, ${theme.surface} 92%, transparent)`);
3075
- element.style.setProperty("--md-footer-border", theme.border);
3303
+ element.style.setProperty("--md-footer-bg", "transparent");
3304
+ element.style.setProperty("--md-footer-border", "transparent");
3076
3305
  element.style.setProperty("--md-control-bg", theme.surfaceRaised);
3077
3306
  element.style.setProperty("--md-control-border", theme.border);
3078
3307
  element.style.setProperty("--md-control-text", theme.textMuted);
3079
3308
  element.style.setProperty("--md-control-hover-bg", theme.canvas);
3080
3309
  element.style.setProperty("--md-control-hover-text", theme.text);
3081
3310
  element.style.setProperty("--md-control-hover-border", theme.accent);
3311
+ element.style.setProperty("--md-segmented-bg", `color-mix(in srgb, ${theme.surfaceRaised} 60%, transparent)`);
3082
3312
  element.style.setProperty("--md-divider", theme.border);
3083
3313
  if (theme.fonts?.title) element.style.setProperty("--md-font-title", theme.fonts.title);
3084
3314
  if (theme.fonts?.nodeName) element.style.setProperty("--md-font-node", theme.fonts.nodeName);
@@ -3233,6 +3463,7 @@ function createDiagram(opts) {
3233
3463
  let progressEl = null;
3234
3464
  if (showProgress) {
3235
3465
  progressEl = document.createElement("div");
3466
+ progressEl.className = progressMode === "bar" ? "markdy-progress-bar" : "markdy-boundary-progress";
3236
3467
  Object.assign(progressEl.style, {
3237
3468
  position: "absolute",
3238
3469
  ...progressMode === "bar" ? { left: "0", right: "0", bottom: "0", height: "3px" } : { inset: "0" },
@@ -3247,7 +3478,7 @@ function createDiagram(opts) {
3247
3478
  function updateProgressBar(pct) {
3248
3479
  if (!progressEl) return;
3249
3480
  if (progressMode === "bar") {
3250
- progressEl.style.background = customColor ?? "#2563eb";
3481
+ progressEl.style.background = customColor ?? plan.theme.accent ?? "#2563eb";
3251
3482
  progressEl.style.transformOrigin = "left center";
3252
3483
  progressEl.style.transform = `scaleX(${pct})`;
3253
3484
  return;
@@ -3259,7 +3490,7 @@ function createDiagram(opts) {
3259
3490
  progressEl.style.webkitMask = progressEl.style.mask;
3260
3491
  progressEl.style.maskComposite = "exclude";
3261
3492
  progressEl.style.webkitMaskComposite = "xor";
3262
- progressEl.style.padding = "2px";
3493
+ progressEl.style.padding = "1px";
3263
3494
  }
3264
3495
  let footer = null;
3265
3496
  function ensureFooter() {
@@ -3268,6 +3499,8 @@ function createDiagram(opts) {
3268
3499
  footer.className = "markdy-footer";
3269
3500
  applyThemeVariables(footer, plan.theme);
3270
3501
  Object.assign(footer.style, {
3502
+ position: "relative",
3503
+ zIndex: "100",
3271
3504
  display: "flex",
3272
3505
  alignItems: "center",
3273
3506
  justifyContent: "space-between",
@@ -3275,7 +3508,8 @@ function createDiagram(opts) {
3275
3508
  gap: "6px",
3276
3509
  width: "100%",
3277
3510
  boxSizing: "border-box",
3278
- padding: "4px 6px 0"
3511
+ padding: "6px 8px 2px",
3512
+ background: "transparent"
3279
3513
  });
3280
3514
  if (container.parentNode) container.parentNode.insertBefore(footer, container.nextSibling);
3281
3515
  else container.appendChild(footer);
@@ -3735,6 +3969,84 @@ function createDiagram(opts) {
3735
3969
  }
3736
3970
  diagram.seek(target);
3737
3971
  },
3972
+ setTheme(theme) {
3973
+ const newTheme = typeof theme === "string" ? resolveTheme(theme) : theme;
3974
+ plan.theme = newTheme;
3975
+ applyThemeToScene(scene, newTheme);
3976
+ if (footer) {
3977
+ applyThemeVariables(footer, newTheme);
3978
+ }
3979
+ for (const node of plan.nodes) {
3980
+ const el = nodeEls.get(node.id);
3981
+ if (el) {
3982
+ if (newTheme.flatCards) {
3983
+ el.style.setProperty("--md-shadow", "transparent");
3984
+ } else {
3985
+ el.style.removeProperty("--md-shadow");
3986
+ }
3987
+ if (newTheme.accentTint) {
3988
+ el.style.setProperty("--md-accent-tint", newTheme.accentTint);
3989
+ } else {
3990
+ el.style.removeProperty("--md-accent-tint");
3991
+ }
3992
+ const roleColor = newTheme.roles[node.role] ?? newTheme.accent;
3993
+ el.style.setProperty("--md-role-color", roleColor);
3994
+ applyDeclaredNodeStyle(el, node.style);
3995
+ }
3996
+ }
3997
+ const structuralSvg = structuralEdgeHost.querySelector("svg[data-markdy-edge-layer='1']");
3998
+ if (structuralSvg) {
3999
+ updateEdgeLayerTheme(structuralSvg, edgeRuntimes, newTheme, edgeSceneId);
4000
+ }
4001
+ const cameraSvg = cameraLayer.querySelector("svg[data-markdy-edge-layer='1']");
4002
+ if (cameraSvg) {
4003
+ updateEdgeLayerTheme(cameraSvg, edgeRuntimes, newTheme, edgeSceneId);
4004
+ }
4005
+ if (plan.treeBuses && plan.treeBuses.length > 0) {
4006
+ treeLayer.replaceChildren();
4007
+ mountTreeBuses(treeLayer, plan.treeBuses, newTheme);
4008
+ }
4009
+ if (plan.diagramType === "constellation" || plan.diagramType === "radar" || plan.diagramType === "timeline") {
4010
+ constellationLayer.replaceChildren();
4011
+ if (plan.diagramType === "constellation") {
4012
+ mountConstellationLayer(
4013
+ constellationLayer,
4014
+ plan.nodes,
4015
+ newTheme,
4016
+ { width: plan.meta.width, height: plan.meta.height }
4017
+ );
4018
+ } else if (plan.diagramType === "radar") {
4019
+ mountRadarLayer(
4020
+ constellationLayer,
4021
+ plan.nodes,
4022
+ newTheme,
4023
+ { width: plan.meta.width, height: plan.meta.height }
4024
+ );
4025
+ } else if (plan.diagramType === "timeline") {
4026
+ mountTimelineLayer(
4027
+ constellationLayer,
4028
+ plan.nodes,
4029
+ newTheme,
4030
+ { width: plan.meta.width, height: plan.meta.height }
4031
+ );
4032
+ }
4033
+ }
4034
+ if (plan.diagramType === "sequence") {
4035
+ updateSequenceLayerTheme(sequenceLayer, newTheme);
4036
+ }
4037
+ if (plan.groupBoundaries && plan.groupBoundaries.length > 0) {
4038
+ groupLayer.replaceChildren();
4039
+ mountGroupBoundaries(groupLayer, plan.groupBoundaries, newTheme);
4040
+ }
4041
+ if (plan.annotations && plan.annotations.length > 0) {
4042
+ annotationLayer.replaceChildren();
4043
+ mountAnnotations(annotationLayer, plan.annotations, plan.nodes, newTheme, {
4044
+ width: plan.meta.width,
4045
+ height: plan.meta.height
4046
+ });
4047
+ }
4048
+ if (totalDurationMs > 0) updateProgressBar(sceneMs / totalDurationMs);
4049
+ },
3738
4050
  destroy() {
3739
4051
  diagram.pause();
3740
4052
  if (keyboardShortcuts && typeof window !== "undefined") {
@@ -3901,6 +4213,7 @@ function createDiagram(opts) {
3901
4213
  const targetThemes = isDark ? lightThemes : darkThemes;
3902
4214
  const nextTheme = targetThemes[Math.floor(Math.random() * targetThemes.length)];
3903
4215
  flashControlLabel(button, nextTheme);
4216
+ diagram.setTheme(nextTheme);
3904
4217
  container.dispatchEvent(
3905
4218
  new CustomEvent("markdy-theme-switch", {
3906
4219
  bubbles: true,
@@ -4004,7 +4317,8 @@ function createDiagram(opts) {
4004
4317
  body.appendChild(pre);
4005
4318
  panel.append(header, body);
4006
4319
  overlay.appendChild(panel);
4007
- document.body.appendChild(overlay);
4320
+ const mountHost = document.fullscreenElement ?? document.body;
4321
+ mountHost.appendChild(overlay);
4008
4322
  closeBtn.focus();
4009
4323
  let closing = false;
4010
4324
  function removePanel() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
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.19"
48
+ "@markdy/core": "1.0.21"
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.19"
55
+ "@markdy/stdlib-systems": "1.0.21"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",