@markdy/renderer-dom 0.8.2 → 0.8.4

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
@@ -37,12 +37,12 @@ This package consumes parsed AST and drives DOM + Web Animations API timelines.
37
37
  ## Usage
38
38
 
39
39
  ```typescript
40
- import { createPlayer } from "@markdy/renderer-dom";
40
+ import { createDiagram } from "@markdy/renderer-dom";
41
41
 
42
- const player = createPlayer({
42
+ const diagram = createDiagram({
43
43
  container: document.getElementById("scene")!,
44
44
  code: `
45
- scene "Request" theme=midnight
45
+ scene "Request" theme=paper
46
46
  browser Web
47
47
  service API
48
48
  beat main:
@@ -53,15 +53,15 @@ const player = createPlayer({
53
53
  });
54
54
 
55
55
  // Playback control
56
- player.pause();
57
- player.seek(1.5); // jump to 1.5 seconds
58
- player.play();
59
- player.destroy(); // clean up DOM + cancel animations
56
+ diagram.pause();
57
+ diagram.seek(1.5); // jump to 1.5 seconds
58
+ diagram.play();
59
+ diagram.destroy(); // clean up DOM + cancel animations
60
60
  ```
61
61
 
62
62
  ## API
63
63
 
64
- ### `createPlayer(options: PlayerOptions): Player`
64
+ ### `createDiagram(options: DiagramOptions): Diagram`
65
65
 
66
66
  | Option | Type | Default | Description |
67
67
  |---|---|---|---|
@@ -77,7 +77,7 @@ player.destroy(); // clean up DOM + cancel animations
77
77
  | `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
78
78
  | `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
79
79
 
80
- ### `Player`
80
+ ### `Diagram`
81
81
 
82
82
  | Method | Description |
83
83
  |---|---|
@@ -97,8 +97,8 @@ player.destroy(); // clean up DOM + cancel animations
97
97
 
98
98
  ```
99
99
  src/
100
- index.ts — Barrel exports (createPlayer)
101
- player.ts — Public API, rAF loop, progress bar, responsive scaling
100
+ index.ts — Barrel exports (createDiagram)
101
+ diagram.ts — Public API, rAF loop, progress bar, responsive scaling
102
102
  nodes.ts — Node element factory + scene title
103
103
  edges.ts — Flow-edge SVG runtime, routing, and cue animations
104
104
  geometry/
package/dist/index.d.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  import { BeatRange, Diagnostic } from '@markdy/core';
2
2
 
3
3
  /**
4
- * Diagram-native Markdy player — renders RenderPlan via WAAPI.
4
+ * Markdy diagram runtime — renders a RenderPlan via WAAPI.
5
5
  */
6
6
 
7
- interface PlayerOptions {
7
+ interface DiagramOptions {
8
8
  container: HTMLElement;
9
9
  code: string;
10
10
  autoplay?: boolean;
11
11
  loop?: boolean;
12
12
  copyright?: boolean;
13
+ /**
14
+ * Maps node `image=`/`logo=` values to resolved URLs. Lets a host (Astro,
15
+ * MDX) remap DSL asset references to CDN/data URLs. Values not present here
16
+ * are used verbatim as the image `src`.
17
+ */
18
+ assets?: Record<string, string>;
13
19
  /** @deprecated Prefer sceneBoundaryProgress. */
14
20
  progressBar?: boolean;
15
21
  /** Show rainbow progress around scene boundary. Defaults to true. */
@@ -20,7 +26,7 @@ interface PlayerOptions {
20
26
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
21
27
  onPlayStateChange?: (playing: boolean) => void;
22
28
  }
23
- interface Player {
29
+ interface Diagram {
24
30
  play(): void;
25
31
  pause(): void;
26
32
  seek(seconds: number): void;
@@ -33,6 +39,6 @@ interface Player {
33
39
  seekToBeat(name: string): void;
34
40
  destroy(): void;
35
41
  }
36
- declare function createPlayer(opts: PlayerOptions): Player;
42
+ declare function createDiagram(opts: DiagramOptions): Diagram;
37
43
 
38
- export { type Player, type PlayerOptions, createPlayer };
44
+ export { type Diagram, type DiagramOptions, createDiagram };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // src/player.ts
1
+ // src/diagram.ts
2
2
  import { parseAndCompile } from "@markdy/core";
3
3
 
4
4
  // src/geometry/rect.ts
@@ -233,13 +233,13 @@ function ensureDefs(svg, theme, id) {
233
233
  const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
234
234
  arrow.setAttribute("id", `${id}-arrow-${kind}`);
235
235
  arrow.setAttribute("viewBox", "0 0 10 10");
236
- arrow.setAttribute("refX", "8");
236
+ arrow.setAttribute("refX", "8.5");
237
237
  arrow.setAttribute("refY", "5");
238
- arrow.setAttribute("markerWidth", "6");
239
- arrow.setAttribute("markerHeight", "6");
238
+ arrow.setAttribute("markerWidth", "7");
239
+ arrow.setAttribute("markerHeight", "7");
240
240
  arrow.setAttribute("orient", "auto-start-reverse");
241
241
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
242
- path.setAttribute("d", "M 1 1 L 9 5 L 1 9 z");
242
+ path.setAttribute("d", "M 1.5 1.6 L 9 5 L 1.5 8.4 L 3.4 5 Z");
243
243
  path.setAttribute("fill", color);
244
244
  arrow.appendChild(path);
245
245
  defs.appendChild(arrow);
@@ -260,19 +260,21 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
260
260
  path.setAttribute("d", d);
261
261
  path.setAttribute("fill", "none");
262
262
  path.setAttribute("stroke", color);
263
- path.setAttribute("stroke-width", kind === "dependency" ? "1.5" : "2.5");
263
+ path.setAttribute("stroke-width", kind === "dependency" ? "1.5" : "2");
264
264
  path.setAttribute("stroke-linejoin", "round");
265
265
  path.setAttribute("stroke-linecap", "round");
266
266
  if (style.dash) path.setAttribute("stroke-dasharray", style.dash);
267
267
  if (style.marker !== "none") {
268
268
  path.setAttribute("marker-end", `url(#${sceneId}-arrow-${kind})`);
269
269
  }
270
- path.style.filter = `drop-shadow(0 0 4px ${color}55)`;
270
+ if (kind !== "dependency") {
271
+ path.style.filter = `drop-shadow(0 0 3px ${color}33)`;
272
+ }
271
273
  const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
272
- dot.setAttribute("r", "4");
274
+ dot.setAttribute("r", "3.5");
273
275
  dot.setAttribute("fill", color);
274
276
  dot.style.opacity = "0";
275
- dot.style.filter = `drop-shadow(0 0 6px ${color})`;
277
+ dot.style.filter = `drop-shadow(0 0 4px ${color}aa)`;
276
278
  group.append(path, dot);
277
279
  let labelEl;
278
280
  let labelRect;
@@ -280,6 +282,20 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
280
282
  const textWidth = label.length * 6.6 + 10;
281
283
  const placement = placeFlowLabel(points, textWidth, labelObstacles, bounds);
282
284
  labelRect = placement.rect;
285
+ const plate = document.createElementNS("http://www.w3.org/2000/svg", "rect");
286
+ const padX = 7;
287
+ const halfW = textWidth / 2;
288
+ plate.setAttribute("x", String(placement.x - halfW - padX));
289
+ plate.setAttribute("y", String(placement.y - 9));
290
+ plate.setAttribute("width", String(textWidth + padX * 2));
291
+ plate.setAttribute("height", "18");
292
+ plate.setAttribute("rx", "6");
293
+ plate.setAttribute("fill", theme.labelPlate ?? theme.surface);
294
+ plate.setAttribute("fill-opacity", "0.9");
295
+ plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 60%, transparent)`);
296
+ plate.setAttribute("stroke-width", "1");
297
+ plate.style.opacity = "0";
298
+ group.appendChild(plate);
283
299
  labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
284
300
  labelEl.setAttribute("x", String(placement.x));
285
301
  labelEl.setAttribute("y", String(placement.y));
@@ -287,14 +303,11 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
287
303
  labelEl.setAttribute("dominant-baseline", "middle");
288
304
  labelEl.setAttribute("font-size", "11");
289
305
  labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, monospace");
290
- labelEl.setAttribute("fill", theme.textMuted);
291
- labelEl.setAttribute("stroke", theme.canvas);
292
- labelEl.setAttribute("stroke-width", "3");
293
- labelEl.setAttribute("paint-order", "stroke");
294
- labelEl.setAttribute("stroke-linejoin", "round");
306
+ labelEl.setAttribute("fill", theme.text);
295
307
  labelEl.textContent = label;
296
308
  labelEl.style.opacity = "0";
297
309
  group.appendChild(labelEl);
310
+ labelEl.__plate = plate;
298
311
  }
299
312
  svg.appendChild(group);
300
313
  return { group, path, label: labelEl, dot, pathLen: len, points, labelRect };
@@ -332,6 +345,10 @@ function animateEdgeReveal(runtime, startMs, durMs) {
332
345
  );
333
346
  if (label) {
334
347
  anims.push(label.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180, delay: startMs + 80, fill: "forwards" }));
348
+ const plate = label.__plate;
349
+ if (plate) {
350
+ anims.push(plate.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180, delay: startMs + 80, fill: "forwards" }));
351
+ }
335
352
  }
336
353
  if (points.length >= 2) {
337
354
  anims.push(
@@ -448,42 +465,46 @@ function ensureNodeStyles(doc) {
448
465
  box-sizing: border-box;
449
466
  width: var(--md-node-w, 184px);
450
467
  height: var(--md-node-h, 88px);
451
- border-radius: 14px;
452
- border: 1px solid var(--md-border);
453
- background: linear-gradient(145deg, var(--md-surface-raised), var(--md-surface));
468
+ border-radius: 12px;
469
+ background:
470
+ linear-gradient(180deg,
471
+ var(--md-node-surface-raised, color-mix(in srgb, var(--md-surface-raised) 88%, #ffffff 12%)),
472
+ var(--md-node-surface, var(--md-surface)));
454
473
  color: var(--md-text);
455
- box-shadow: 0 12px 32px rgba(2, 6, 23, 0.28), 0 0 0 1px rgba(255,255,255,0.04) inset;
474
+ box-shadow:
475
+ 0 1px 1px color-mix(in srgb, var(--md-shadow, rgba(2, 6, 23, 0.5)) 50%, transparent),
476
+ 0 10px 22px -12px var(--md-shadow, rgba(2, 6, 23, 0.55)),
477
+ inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent)),
478
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
456
479
  font-family: Inter, ui-sans-serif, system-ui, sans-serif;
457
480
  overflow: hidden;
458
481
  opacity: 0;
459
482
  transform: translateY(8px);
460
- transition: box-shadow 0.3s ease;
483
+ transition: box-shadow 0.25s ease, transform 0.25s ease;
461
484
  }
462
485
  .markdy-node[data-visible="1"] {
463
486
  opacity: 1;
464
487
  transform: translateY(0);
465
488
  }
466
489
  .markdy-node[data-focused="1"] {
467
- box-shadow: 0 0 0 2px var(--md-accent), 0 16px 40px rgba(2, 6, 23, 0.35);
490
+ box-shadow:
491
+ 0 2px 4px rgba(2, 6, 23, 0.32),
492
+ 0 16px 34px -14px rgba(2, 6, 23, 0.6),
493
+ inset 0 0 0 1px color-mix(in srgb, var(--md-accent) 65%, transparent),
494
+ 0 0 0 3px color-mix(in srgb, var(--md-accent) 20%, transparent);
468
495
  }
469
496
  .markdy-node[data-glow="1"] {
470
- box-shadow: 0 0 24px var(--md-glow-color, var(--md-accent)), 0 0 0 1px var(--md-glow-color, var(--md-accent)) inset;
471
- }
472
- .markdy-node__rail {
473
- position: absolute;
474
- left: 0;
475
- top: 0;
476
- bottom: 0;
477
- width: 4px;
478
- background: var(--md-role-color, var(--md-accent));
479
- border-radius: 16px 0 0 16px;
480
- }
481
- .markdy-node__type {
482
- display: none;
497
+ box-shadow:
498
+ 0 2px 4px rgba(2, 6, 23, 0.32),
499
+ 0 0 0 1px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 55%, transparent),
500
+ 0 0 20px -2px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 42%, transparent),
501
+ inset 0 0 18px -8px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 45%, transparent);
483
502
  }
503
+ .markdy-node__rail { display: none; }
504
+ .markdy-node__type { display: none; }
484
505
  .markdy-node__body {
485
506
  height: 100%;
486
- padding: 0 14px 0 18px;
507
+ padding: 0 13px;
487
508
  display: flex;
488
509
  align-items: center;
489
510
  gap: 10px;
@@ -493,43 +514,69 @@ function ensureNodeStyles(doc) {
493
514
  flex: 0 0 auto;
494
515
  width: 30px;
495
516
  height: 30px;
496
- border-radius: 10px;
517
+ border-radius: 8px;
497
518
  display: flex;
498
519
  align-items: center;
499
520
  justify-content: center;
500
521
  color: var(--md-role-color, var(--md-accent));
501
- background: color-mix(in srgb, var(--md-role-color, var(--md-accent)) 16%, transparent);
502
- box-shadow: 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 28%, transparent) inset;
522
+ background:
523
+ linear-gradient(180deg,
524
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 24%, transparent),
525
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 11%, transparent));
526
+ box-shadow:
527
+ inset 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 34%, transparent),
528
+ inset 0 1px 0 rgba(255, 255, 255, 0.12);
503
529
  }
504
530
  .markdy-node__icon svg {
505
- width: 18px;
506
- height: 18px;
531
+ width: 17px;
532
+ height: 17px;
507
533
  display: block;
508
534
  stroke: currentColor;
509
535
  }
536
+ .markdy-node__icon[data-media="image"] {
537
+ background: color-mix(in srgb, var(--md-surface-raised) 68%, #ffffff 32%);
538
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-border) 60%, transparent);
539
+ padding: 3px;
540
+ overflow: hidden;
541
+ }
542
+ .markdy-node__icon img {
543
+ width: 100%;
544
+ height: 100%;
545
+ object-fit: contain;
546
+ border-radius: 6px;
547
+ display: block;
548
+ }
549
+ .markdy-node__icon[data-fit="cover"] img {
550
+ object-fit: cover;
551
+ }
510
552
  .markdy-node[data-icon="decision"] .markdy-node__icon,
511
553
  .markdy-node[data-icon="flow"] .markdy-node__icon {
512
554
  border-radius: 999px;
513
555
  }
514
556
  .markdy-node__label {
557
+ flex: 1 1 auto;
515
558
  min-width: 0;
516
559
  padding: 0;
517
- font-size: 15px;
518
- font-weight: 650;
519
- line-height: 1.25;
520
- white-space: nowrap;
560
+ font-size: 14px;
561
+ font-weight: 600;
562
+ letter-spacing: -0.01em;
563
+ line-height: 1.18;
564
+ display: -webkit-box;
565
+ -webkit-box-orient: vertical;
566
+ -webkit-line-clamp: 3;
567
+ line-clamp: 3;
521
568
  overflow: hidden;
522
- text-overflow: ellipsis;
569
+ overflow-wrap: anywhere;
570
+ word-break: break-word;
571
+ text-wrap: balance;
523
572
  }
524
- .markdy-node[data-role="client"] { border-radius: 20px 20px 8px 8px; }
525
- .markdy-node[data-role="data"] { border-radius: 16px 16px 28px 28px; }
526
- .markdy-node[data-role="flow"] { transform: rotate(0deg); clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }
527
- .markdy-node[data-role="network"] { clip-path: polygon(8% 0%, 92% 0%, 100% 50%, 92% 100%, 8% 100%, 0% 50%); }
573
+ .markdy-node[data-role="client"] { border-radius: 15px 15px 9px 9px; }
574
+ .markdy-node[data-role="data"] { border-radius: 12px 12px 20px 20px; }
528
575
  .markdy-scene-title {
529
576
  position: absolute;
530
- left: 64px;
531
- top: 28px;
532
- right: 64px;
577
+ left: 44px;
578
+ top: 26px;
579
+ right: 44px;
533
580
  font-size: 32px;
534
581
  font-weight: 700;
535
582
  line-height: 1.2;
@@ -636,6 +683,49 @@ var ICONS = {
636
683
  ["circle", { cx: "18", cy: "18", r: "2.5" }],
637
684
  ["path", { d: "M8.4 11 15.6 7M8.4 13 15.6 17" }]
638
685
  ],
686
+ server: [
687
+ ["rect", { x: "4", y: "4.5", width: "16", height: "6.5", rx: "2" }],
688
+ ["rect", { x: "4", y: "13", width: "16", height: "6.5", rx: "2" }],
689
+ ["path", { d: "M7.5 7.75h.01M7.5 16.25h.01" }],
690
+ ["path", { d: "M11 7.75h6M11 16.25h6" }]
691
+ ],
692
+ scheduler: [
693
+ ["circle", { cx: "12", cy: "12", r: "8" }],
694
+ ["path", { d: "M12 7.5V12l3.2 2" }]
695
+ ],
696
+ key: [
697
+ ["circle", { cx: "8.5", cy: "8.5", r: "3.6" }],
698
+ ["path", { d: "M11.1 11.1 19 19" }],
699
+ ["path", { d: "M16.4 16.4l1.8 1.8M18.2 13.6l1.8 1.8" }]
700
+ ],
701
+ pod: [
702
+ ["path", { d: "M12 3 20 7.5v9L12 21 4 16.5v-9L12 3Z" }],
703
+ ["path", { d: "M8.75 10.5h6.5v5h-6.5z" }]
704
+ ],
705
+ function: [
706
+ ["rect", { x: "4", y: "4", width: "16", height: "16", rx: "4.5" }],
707
+ ["path", { d: "M13.2 7.5 9.5 12.4h3.4L11 16.5" }]
708
+ ],
709
+ lock: [
710
+ ["rect", { x: "5", y: "11", width: "14", height: "9", rx: "2.2" }],
711
+ ["path", { d: "M8 11V8a4 4 0 0 1 8 0v3" }],
712
+ ["path", { d: "M12 14.8v2.4" }]
713
+ ],
714
+ metrics: [
715
+ ["path", { d: "M4 20h16" }],
716
+ ["rect", { x: "5", y: "11", width: "2.6", height: "6", rx: "0.6" }],
717
+ ["rect", { x: "10.7", y: "6.5", width: "2.6", height: "10.5", rx: "0.6" }],
718
+ ["rect", { x: "16.4", y: "13", width: "2.6", height: "4", rx: "0.6" }]
719
+ ],
720
+ mobile: [
721
+ ["rect", { x: "7", y: "3", width: "10", height: "18", rx: "2.6" }],
722
+ ["path", { d: "M10.5 18h3" }]
723
+ ],
724
+ registry: [
725
+ ["rect", { x: "3.5", y: "13", width: "7", height: "6", rx: "1" }],
726
+ ["rect", { x: "13.5", y: "13", width: "7", height: "6", rx: "1" }],
727
+ ["rect", { x: "8.5", y: "5", width: "7", height: "6", rx: "1" }]
728
+ ],
639
729
  flow: [
640
730
  ["path", { d: "M5 12h14" }],
641
731
  ["path", { d: "m13 6 6 6-6 6" }]
@@ -647,41 +737,81 @@ var ICONS = {
647
737
  ]
648
738
  };
649
739
  function iconKeyForNode(node) {
740
+ const override = typeof node.props?.icon === "string" ? node.props.icon.toLowerCase() : void 0;
741
+ if (override && ICONS[override]) return override;
650
742
  if (node.kind === "api_gateway" || node.kind === "gateway" || node.kind === "load_balancer" || node.kind === "ingress") return "gateway";
651
743
  if (node.kind === "db" || node.kind === "database" || node.kind === "sql" || node.kind === "nosql" || node.kind === "warehouse") return "database";
652
744
  if (node.kind === "bucket" || node.kind === "object_store" || node.kind === "blob" || node.kind === "volume" || node.kind === "disk") return "storage";
653
745
  if (node.kind === "cdn" || node.kind === "dns" || node.kind === "internet") return "cdn";
654
746
  if (node.kind === "queue" || node.kind === "topic" || node.kind === "stream" || node.kind === "event_bus" || node.kind === "broker") return "queue";
655
- if (node.kind === "worker" || node.kind === "job" || node.kind === "scheduler" || node.kind === "cron" || node.kind === "batch") return "worker";
747
+ if (node.kind === "scheduler" || node.kind === "cron") return "scheduler";
748
+ if (node.kind === "worker" || node.kind === "job" || node.kind === "batch") return "worker";
749
+ if (node.kind === "function" || node.kind === "lambda") return "function";
750
+ if (node.kind === "pod" || node.kind === "container" || node.kind === "sidecar") return "pod";
751
+ if (node.kind === "secret" || node.kind === "key" || node.kind === "certificate" || node.kind === "vault") return "key";
752
+ if (node.kind === "auth" || node.kind === "oauth" || node.kind === "oidc" || node.kind === "jwt" || node.kind === "lock") return "lock";
753
+ if (node.kind === "firewall" || node.kind === "waf" || node.kind === "vpn" || node.kind === "bastion") return "security";
754
+ if (node.kind === "proxy" || node.kind === "reverse_proxy" || node.kind === "router" || node.kind === "nat" || node.kind === "switch") return "gateway";
755
+ if (node.kind === "monitor" || node.kind === "metrics" || node.kind === "dashboard" || node.kind === "slo" || node.kind === "probe") return "metrics";
756
+ if (node.kind === "registry" || node.kind === "artifact") return "registry";
757
+ if (node.kind === "mobile") return "mobile";
758
+ if (node.kind === "api" || node.kind === "service" || node.kind === "microservice" || node.kind === "backend" || node.kind === "server" || node.kind === "handler" || node.kind === "controller") return "server";
656
759
  if (node.kind === "browser" || node.kind === "web" || node.kind === "frontend" || node.kind === "app") return "browser";
657
760
  if (node.kind === "user" || node.kind === "client") return "user";
658
761
  if (node.kind === "decision" || node.kind === "condition") return "decision";
659
762
  if (node.kind === "cache") return "cache";
660
763
  return ICONS[node.kind] ? node.kind : node.role;
661
764
  }
662
- function createIconEl(doc, node) {
663
- const wrap = doc.createElement("div");
664
- wrap.className = "markdy-node__icon";
665
- wrap.setAttribute("aria-hidden", "true");
765
+ function appendGlyph(doc, wrap, spec) {
666
766
  const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
667
767
  svg.setAttribute("viewBox", "0 0 24 24");
668
768
  svg.setAttribute("fill", "none");
669
- svg.setAttribute("stroke-width", "2");
769
+ svg.setAttribute("stroke-width", "1.75");
670
770
  svg.setAttribute("stroke-linecap", "round");
671
771
  svg.setAttribute("stroke-linejoin", "round");
672
- const spec = ICONS[iconKeyForNode(node)] ?? ICONS.service;
673
772
  for (const [tag, attrs] of spec) {
674
773
  const child = doc.createElementNS("http://www.w3.org/2000/svg", tag);
675
774
  for (const [name, value] of Object.entries(attrs)) child.setAttribute(name, value);
676
775
  svg.appendChild(child);
677
776
  }
678
777
  wrap.appendChild(svg);
778
+ }
779
+ function resolveImageSrc(node, assets) {
780
+ const raw = node.props?.image ?? node.props?.logo;
781
+ if (typeof raw !== "string" || raw.length === 0) return void 0;
782
+ return assets?.[raw] ?? raw;
783
+ }
784
+ function createNodeMediaEl(doc, node, assets) {
785
+ const wrap = doc.createElement("div");
786
+ wrap.className = "markdy-node__icon";
787
+ wrap.setAttribute("aria-hidden", "true");
788
+ const imgSrc = resolveImageSrc(node, assets);
789
+ if (imgSrc) {
790
+ wrap.dataset.media = "image";
791
+ const fit = typeof node.props?.imageFit === "string" ? node.props.imageFit.toLowerCase() : "contain";
792
+ wrap.dataset.fit = fit === "cover" ? "cover" : "contain";
793
+ const img = doc.createElement("img");
794
+ img.src = imgSrc;
795
+ img.alt = "";
796
+ img.decoding = "async";
797
+ img.loading = "lazy";
798
+ img.referrerPolicy = "no-referrer";
799
+ img.addEventListener("error", () => {
800
+ wrap.removeAttribute("data-media");
801
+ wrap.removeAttribute("data-fit");
802
+ wrap.textContent = "";
803
+ appendGlyph(doc, wrap, ICONS[iconKeyForNode(node)] ?? ICONS.service);
804
+ });
805
+ wrap.appendChild(img);
806
+ return wrap;
807
+ }
808
+ appendGlyph(doc, wrap, ICONS[iconKeyForNode(node)] ?? ICONS.service);
679
809
  return wrap;
680
810
  }
681
- function createNodeEl(node, theme) {
811
+ function createNodeEl(node, theme, assets) {
682
812
  const el = document.createElement("div");
683
- el.className = "markdy-node markdy-scene-actor";
684
- el.dataset.actor = node.id;
813
+ el.className = "markdy-node markdy-scene-node";
814
+ el.dataset.node = node.id;
685
815
  el.dataset.role = node.role;
686
816
  el.style.left = `${node.x}px`;
687
817
  el.style.top = `${node.y}px`;
@@ -694,19 +824,14 @@ function createNodeEl(node, theme) {
694
824
  el.dataset.icon = iconKeyForNode(node);
695
825
  el.title = `${node.label} (${typeText})`;
696
826
  el.setAttribute("aria-label", el.title);
697
- const rail = document.createElement("div");
698
- rail.className = "markdy-node__rail";
699
- const type = document.createElement("div");
700
- type.className = "markdy-node__type";
701
- type.textContent = typeText;
702
827
  const body = document.createElement("div");
703
828
  body.className = "markdy-node__body";
704
- const icon = createIconEl(document, node);
829
+ const icon = createNodeMediaEl(document, node, assets);
705
830
  const label = document.createElement("div");
706
831
  label.className = "markdy-node__label";
707
832
  label.textContent = node.label;
708
833
  body.append(icon, label);
709
- el.append(rail, type, body);
834
+ el.append(body);
710
835
  return el;
711
836
  }
712
837
  function createTitleEl(title) {
@@ -740,18 +865,18 @@ function ensureSceneStyles(doc) {
740
865
  linear-gradient(90deg, var(--md-grid-minor) 1px, transparent 1px) 0 0 / 32px 32px,
741
866
  linear-gradient(var(--md-grid-major) 1px, transparent 1px) 0 0 / 160px 160px,
742
867
  linear-gradient(90deg, var(--md-grid-major) 1px, transparent 1px) 0 0 / 160px 160px;
743
- mask-image: linear-gradient(to bottom, transparent, #000 10%, #000 90%, transparent);
744
- opacity: 0.82;
868
+ mask-image: radial-gradient(ellipse at 50% 42%, #000 55%, transparent 100%);
869
+ opacity: 0.5;
745
870
  }
746
871
  .markdy-scene-root::after {
747
872
  z-index: 1;
748
873
  background:
749
- radial-gradient(ellipse at 50% 0%, color-mix(in srgb, var(--md-accent) 16%, transparent), transparent 42%),
750
- linear-gradient(180deg, transparent 0%, rgba(2, 6, 23, 0.08) 58%, var(--md-vignette) 100%);
751
- opacity: 0.8;
874
+ radial-gradient(ellipse at 50% -8%, color-mix(in srgb, var(--md-accent) 10%, transparent), transparent 46%),
875
+ linear-gradient(180deg, transparent 0%, rgba(2, 6, 23, 0.05) 62%, var(--md-vignette) 100%);
876
+ opacity: 0.62;
752
877
  }
753
878
  .markdy-scene-content { z-index: 2; }
754
- .markdy-scene-actor-layer {
879
+ .markdy-scene-node-layer {
755
880
  position: absolute;
756
881
  inset: 0;
757
882
  overflow: visible;
@@ -772,17 +897,22 @@ function applyThemeToScene(scene, theme) {
772
897
  scene.style.setProperty("--md-grid-major", theme.gridMajor);
773
898
  scene.style.setProperty("--md-vignette", theme.vignette);
774
899
  scene.style.setProperty("--md-accent", theme.accent);
900
+ scene.style.setProperty("--md-node-surface", theme.nodeSurface ?? theme.surface);
901
+ scene.style.setProperty("--md-node-surface-raised", theme.nodeSurfaceRaised ?? theme.surfaceRaised);
902
+ scene.style.setProperty("--md-hairline", theme.hairline ?? `color-mix(in srgb, ${theme.border} 50%, transparent)`);
903
+ scene.style.setProperty("--md-shadow", theme.shadow ?? "rgba(2, 6, 23, 0.55)");
775
904
  scene.dataset.markdyTheme = theme.name;
776
905
  }
777
906
 
778
- // src/player.ts
779
- function createPlayer(opts) {
907
+ // src/diagram.ts
908
+ function createDiagram(opts) {
780
909
  const {
781
910
  container,
782
911
  code,
783
912
  autoplay = true,
784
913
  loop = true,
785
914
  copyright = true,
915
+ assets,
786
916
  progressBar,
787
917
  sceneBoundaryProgress,
788
918
  playbackRate: initialPlaybackRate = 1,
@@ -870,15 +1000,15 @@ function createPlayer(opts) {
870
1000
  sceneContent.className = "markdy-scene-content";
871
1001
  Object.assign(sceneContent.style, { position: "absolute", inset: "0" });
872
1002
  scene.appendChild(sceneContent);
873
- const actorLayer = document.createElement("div");
874
- actorLayer.className = "markdy-scene-actor-layer";
875
- sceneContent.appendChild(actorLayer);
1003
+ const nodeLayer = document.createElement("div");
1004
+ nodeLayer.className = "markdy-scene-node-layer";
1005
+ sceneContent.appendChild(nodeLayer);
876
1006
  const titleEl = createTitleEl(plan.title);
877
- actorLayer.appendChild(titleEl);
1007
+ nodeLayer.appendChild(titleEl);
878
1008
  const nodeEls = /* @__PURE__ */ new Map();
879
1009
  for (const node of plan.nodes) {
880
- const el = createNodeEl(node, plan.theme);
881
- actorLayer.appendChild(el);
1010
+ const el = createNodeEl(node, plan.theme, assets);
1011
+ nodeLayer.appendChild(el);
882
1012
  nodeEls.set(node.id, el);
883
1013
  }
884
1014
  function scaleScene() {
@@ -924,7 +1054,7 @@ function createPlayer(opts) {
924
1054
  if (totalDurationMs > 0) updateProgressBar(sceneMs / totalDurationMs);
925
1055
  rafId = requestAnimationFrame(rafTick);
926
1056
  }
927
- const player = {
1057
+ const diagram = {
928
1058
  play() {
929
1059
  if (isPlaying) return;
930
1060
  isPlaying = true;
@@ -966,10 +1096,10 @@ function createPlayer(opts) {
966
1096
  },
967
1097
  seekToBeat(name) {
968
1098
  const beat = plan.beats.find((b) => b.name === name);
969
- if (beat) player.seek(beat.start);
1099
+ if (beat) diagram.seek(beat.start);
970
1100
  },
971
1101
  destroy() {
972
- player.pause();
1102
+ diagram.pause();
973
1103
  for (const anim of allAnims) anim.cancel();
974
1104
  resizeObserver.disconnect();
975
1105
  if (progressEl?.parentNode === viewport) viewport.removeChild(progressEl);
@@ -979,15 +1109,15 @@ function createPlayer(opts) {
979
1109
  };
980
1110
  viewport.style.cursor = "pointer";
981
1111
  viewport.addEventListener("click", () => {
982
- if (isPlaying) player.pause();
1112
+ if (isPlaying) diagram.pause();
983
1113
  else {
984
1114
  if (!loop && sceneMs >= totalDurationMs) sceneMs = 0;
985
- player.play();
1115
+ diagram.play();
986
1116
  }
987
1117
  });
988
- if (autoplay) player.play();
989
- return player;
1118
+ if (autoplay) diagram.play();
1119
+ return diagram;
990
1120
  }
991
1121
  export {
992
- createPlayer
1122
+ createDiagram
993
1123
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Browser renderer for animated MarkdyScript architecture diagrams, built on the Web Animations API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,19 +43,20 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@markdy/core": "0.8.2"
46
+ "@markdy/core": "0.8.4"
47
47
  },
48
48
  "devDependencies": {
49
49
  "jsdom": "^29.1.1",
50
50
  "tsup": "^8.5.1",
51
51
  "typescript": "^5.9.3",
52
52
  "vitest": "^4.1.7",
53
- "@markdy/stdlib-systems": "0.8.2"
53
+ "@markdy/stdlib-systems": "0.8.4"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsup",
57
57
  "typecheck": "tsc --noEmit",
58
58
  "lint": "tsc --noEmit",
59
- "test": "vitest run"
59
+ "test": "vitest run",
60
+ "harness": "pnpm --filter @markdy/core run build && pnpm run build && node harness/serve.mjs"
60
61
  }
61
62
  }