@markdy/renderer-dom 1.0.19 → 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/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
@@ -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) {
@@ -2512,10 +2726,10 @@ function ensureSceneStyles(doc) {
2512
2726
  gap: 8px;
2513
2727
  padding: 6px 12px;
2514
2728
  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));
2729
+ background: transparent;
2730
+ backdrop-filter: none;
2731
+ -webkit-backdrop-filter: none;
2732
+ border-top: none;
2519
2733
  z-index: 20;
2520
2734
  }
2521
2735
  .markdy-controls {
@@ -3071,14 +3285,15 @@ function applyThemeVariables(element, theme) {
3071
3285
  element.style.setProperty("--md-node-surface-raised", theme.nodeSurfaceRaised ?? theme.surfaceRaised);
3072
3286
  element.style.setProperty("--md-hairline", theme.hairline ?? `color-mix(in srgb, ${theme.border} 50%, transparent)`);
3073
3287
  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);
3288
+ element.style.setProperty("--md-footer-bg", "transparent");
3289
+ element.style.setProperty("--md-footer-border", "transparent");
3076
3290
  element.style.setProperty("--md-control-bg", theme.surfaceRaised);
3077
3291
  element.style.setProperty("--md-control-border", theme.border);
3078
3292
  element.style.setProperty("--md-control-text", theme.textMuted);
3079
3293
  element.style.setProperty("--md-control-hover-bg", theme.canvas);
3080
3294
  element.style.setProperty("--md-control-hover-text", theme.text);
3081
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)`);
3082
3297
  element.style.setProperty("--md-divider", theme.border);
3083
3298
  if (theme.fonts?.title) element.style.setProperty("--md-font-title", theme.fonts.title);
3084
3299
  if (theme.fonts?.nodeName) element.style.setProperty("--md-font-node", theme.fonts.nodeName);
@@ -3247,7 +3462,7 @@ function createDiagram(opts) {
3247
3462
  function updateProgressBar(pct) {
3248
3463
  if (!progressEl) return;
3249
3464
  if (progressMode === "bar") {
3250
- progressEl.style.background = customColor ?? "#2563eb";
3465
+ progressEl.style.background = customColor ?? plan.theme.accent ?? "#2563eb";
3251
3466
  progressEl.style.transformOrigin = "left center";
3252
3467
  progressEl.style.transform = `scaleX(${pct})`;
3253
3468
  return;
@@ -3259,7 +3474,7 @@ function createDiagram(opts) {
3259
3474
  progressEl.style.webkitMask = progressEl.style.mask;
3260
3475
  progressEl.style.maskComposite = "exclude";
3261
3476
  progressEl.style.webkitMaskComposite = "xor";
3262
- progressEl.style.padding = "2px";
3477
+ progressEl.style.padding = "1px";
3263
3478
  }
3264
3479
  let footer = null;
3265
3480
  function ensureFooter() {
@@ -3275,7 +3490,8 @@ function createDiagram(opts) {
3275
3490
  gap: "6px",
3276
3491
  width: "100%",
3277
3492
  boxSizing: "border-box",
3278
- padding: "4px 6px 0"
3493
+ padding: "4px 6px 0",
3494
+ background: "transparent"
3279
3495
  });
3280
3496
  if (container.parentNode) container.parentNode.insertBefore(footer, container.nextSibling);
3281
3497
  else container.appendChild(footer);
@@ -3735,6 +3951,84 @@ function createDiagram(opts) {
3735
3951
  }
3736
3952
  diagram.seek(target);
3737
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
+ },
3738
4032
  destroy() {
3739
4033
  diagram.pause();
3740
4034
  if (keyboardShortcuts && typeof window !== "undefined") {
@@ -3901,6 +4195,7 @@ function createDiagram(opts) {
3901
4195
  const targetThemes = isDark ? lightThemes : darkThemes;
3902
4196
  const nextTheme = targetThemes[Math.floor(Math.random() * targetThemes.length)];
3903
4197
  flashControlLabel(button, nextTheme);
4198
+ diagram.setTheme(nextTheme);
3904
4199
  container.dispatchEvent(
3905
4200
  new CustomEvent("markdy-theme-switch", {
3906
4201
  bubbles: true,
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.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.19"
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.19"
55
+ "@markdy/stdlib-systems": "1.0.20"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",