@markdy/renderer-dom 0.8.1 → 0.8.3

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
@@ -8,7 +8,9 @@ Web Animations API renderer for [MarkdyScript](../../docs/SYNTAX.md) scenes. Tra
8
8
  - **Auto-layout diagrams** — renders positioned nodes and orthogonal, obstacle-aware edges from a compiled `RenderPlan`
9
9
  - **Flow edges** — `->` request, `<-` response, `~>` event, `--` dependency, each with its own stroke, plus a pulse that travels the edge as it draws
10
10
  - **Beat-driven cues** — `show`, `hide`, `glow`, and `focus`, sequenced by named beats
11
+ - **Semantic node cards** — compact SVG glyphs for browsers, services, gateways, queues, workers, databases, storage, CDN, security, platform, and more
11
12
  - **Seek-safe** — manual `currentTime` control enables reliable `seek()` in any direction
13
+ - **Playback-rate controls** — set timeline speed to slow down or speed up diagrams without rebuilding animations
12
14
  - **Semantic themes** — `midnight` and `paper`, with per-role node colors
13
15
  - **Single dependency** — only `@markdy/core`
14
16
 
@@ -40,7 +42,7 @@ import { createPlayer } from "@markdy/renderer-dom";
40
42
  const player = createPlayer({
41
43
  container: document.getElementById("scene")!,
42
44
  code: `
43
- scene "Request" theme=midnight
45
+ scene "Request" theme=paper
44
46
  browser Web
45
47
  service API
46
48
  beat main:
@@ -68,7 +70,9 @@ player.destroy(); // clean up DOM + cancel animations
68
70
  | `autoplay` | `boolean` | `true` | Start playing immediately |
69
71
  | `loop` | `boolean` | `true` | Loop the animation when it reaches the end |
70
72
  | `copyright` | `boolean` | `true` | Show a small "Powered by Markdy" badge below the animation |
71
- | `progressBar` | `boolean` | `true` | Show a rainbow progress bar around the viewport border |
73
+ | `progressBar` | `boolean` | `true` | Deprecated compatibility flag for the rainbow scene-boundary progress bar |
74
+ | `sceneBoundaryProgress` | `boolean` | `progressBar ?? true` | Preferred flag for the rainbow scene-boundary progress bar |
75
+ | `playbackRate` | `number` | `1` | Timeline speed multiplier |
72
76
  | `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
73
77
  | `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
74
78
  | `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
@@ -80,6 +84,8 @@ player.destroy(); // clean up DOM + cancel animations
80
84
  | `play()` | Start or resume playback |
81
85
  | `pause()` | Pause at current position |
82
86
  | `seek(seconds)` | Jump to a specific time |
87
+ | `setPlaybackRate(rate)` | Change timeline speed; ignores non-positive or non-finite values |
88
+ | `playbackRate()` | Current timeline speed multiplier |
83
89
  | `currentTime()` | Current playback position in seconds |
84
90
  | `duration()` | Total scene duration in seconds |
85
91
  | `isPlaying()` | Whether the scene is currently playing |
package/dist/index.d.ts CHANGED
@@ -10,7 +10,18 @@ interface PlayerOptions {
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>;
19
+ /** @deprecated Prefer sceneBoundaryProgress. */
13
20
  progressBar?: boolean;
21
+ /** Show rainbow progress around scene boundary. Defaults to true. */
22
+ sceneBoundaryProgress?: boolean;
23
+ /** Playback speed multiplier. Defaults to 1. */
24
+ playbackRate?: number;
14
25
  onWarning?: (warning: Diagnostic) => void;
15
26
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
16
27
  onPlayStateChange?: (playing: boolean) => void;
@@ -19,6 +30,8 @@ interface Player {
19
30
  play(): void;
20
31
  pause(): void;
21
32
  seek(seconds: number): void;
33
+ setPlaybackRate(rate: number): void;
34
+ playbackRate(): number;
22
35
  currentTime(): number;
23
36
  duration(): number;
24
37
  isPlaying(): boolean;
package/dist/index.js CHANGED
@@ -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,62 +465,118 @@ 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: 16px;
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;
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);
471
502
  }
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
- padding: 10px 14px 0 18px;
483
- font-size: 10px;
484
- font-weight: 700;
485
- letter-spacing: 0.08em;
486
- text-transform: uppercase;
487
- color: var(--md-text-muted);
503
+ .markdy-node__rail { display: none; }
504
+ .markdy-node__type { display: none; }
505
+ .markdy-node__body {
506
+ height: 100%;
507
+ padding: 0 13px;
508
+ display: flex;
509
+ align-items: center;
510
+ gap: 10px;
511
+ min-width: 0;
512
+ }
513
+ .markdy-node__icon {
514
+ flex: 0 0 auto;
515
+ width: 30px;
516
+ height: 30px;
517
+ border-radius: 8px;
518
+ display: flex;
519
+ align-items: center;
520
+ justify-content: center;
521
+ color: var(--md-role-color, var(--md-accent));
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);
529
+ }
530
+ .markdy-node__icon svg {
531
+ width: 17px;
532
+ height: 17px;
533
+ display: block;
534
+ stroke: currentColor;
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
+ }
552
+ .markdy-node[data-icon="decision"] .markdy-node__icon,
553
+ .markdy-node[data-icon="flow"] .markdy-node__icon {
554
+ border-radius: 999px;
488
555
  }
489
556
  .markdy-node__label {
490
- padding: 2px 14px 0 18px;
491
- font-size: 15px;
492
- font-weight: 650;
493
- line-height: 1.25;
494
- white-space: nowrap;
557
+ flex: 1 1 auto;
558
+ min-width: 0;
559
+ padding: 0;
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;
495
568
  overflow: hidden;
496
- text-overflow: ellipsis;
569
+ overflow-wrap: anywhere;
570
+ word-break: break-word;
571
+ text-wrap: balance;
497
572
  }
498
- .markdy-node[data-role="client"] { border-radius: 20px 20px 8px 8px; }
499
- .markdy-node[data-role="data"] { border-radius: 16px 16px 28px 28px; }
500
- .markdy-node[data-role="flow"] { transform: rotate(0deg); clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }
501
- .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; }
502
575
  .markdy-scene-title {
503
576
  position: absolute;
504
- left: 64px;
505
- top: 28px;
506
- right: 64px;
577
+ left: 44px;
578
+ top: 26px;
579
+ right: 44px;
507
580
  font-size: 32px;
508
581
  font-weight: 700;
509
582
  line-height: 1.2;
@@ -519,7 +592,223 @@ function ensureNodeStyles(doc) {
519
592
  `;
520
593
  doc.head.appendChild(style);
521
594
  }
522
- function createNodeEl(node, theme) {
595
+ var ICONS = {
596
+ compute: [
597
+ ["rect", { x: "5", y: "5", width: "14", height: "14", rx: "3" }],
598
+ ["path", { d: "M9 9h6v6H9zM9 2.5v2.5M15 2.5v2.5M9 19v2.5M15 19v2.5M2.5 9h2.5M2.5 15h2.5M19 9h2.5M19 15h2.5" }]
599
+ ],
600
+ user: [
601
+ ["circle", { cx: "12", cy: "8", r: "3.4" }],
602
+ ["path", { d: "M5.5 20a6.5 6.5 0 0 1 13 0" }]
603
+ ],
604
+ browser: [
605
+ ["rect", { x: "3", y: "5", width: "18", height: "14", rx: "2.5" }],
606
+ ["path", { d: "M3 9h18" }],
607
+ ["path", { d: "M7 7h.01M10 7h.01" }]
608
+ ],
609
+ service: [
610
+ ["path", { d: "M12 3 4.5 7.2 12 11.4l7.5-4.2L12 3Z" }],
611
+ ["path", { d: "M4.5 12 12 16.2 19.5 12" }],
612
+ ["path", { d: "M4.5 16.8 12 21l7.5-4.2" }]
613
+ ],
614
+ gateway: [
615
+ ["circle", { cx: "5", cy: "12", r: "2" }],
616
+ ["circle", { cx: "19", cy: "6", r: "2" }],
617
+ ["circle", { cx: "19", cy: "18", r: "2" }],
618
+ ["path", { d: "M7 12h4l5.5-5" }],
619
+ ["path", { d: "M11 12l5.5 5" }]
620
+ ],
621
+ queue: [
622
+ ["rect", { x: "5", y: "5", width: "14", height: "4", rx: "1.4" }],
623
+ ["rect", { x: "5", y: "10", width: "14", height: "4", rx: "1.4" }],
624
+ ["rect", { x: "5", y: "15", width: "14", height: "4", rx: "1.4" }]
625
+ ],
626
+ worker: [
627
+ ["circle", { cx: "12", cy: "12", r: "3.2" }],
628
+ ["path", { d: "M12 2.8v3M12 18.2v3M2.8 12h3M18.2 12h3M5.5 5.5l2.1 2.1M16.4 16.4l2.1 2.1M18.5 5.5l-2.1 2.1M7.6 16.4l-2.1 2.1" }]
629
+ ],
630
+ database: [
631
+ ["ellipse", { cx: "12", cy: "5.5", rx: "7", ry: "3" }],
632
+ ["path", { d: "M5 5.5v10c0 1.7 3.1 3 7 3s7-1.3 7-3v-10" }],
633
+ ["path", { d: "M5 10.5c0 1.7 3.1 3 7 3s7-1.3 7-3" }]
634
+ ],
635
+ storage: [
636
+ ["path", { d: "M4 8.5 12 4l8 4.5v7L12 20l-8-4.5v-7Z" }],
637
+ ["path", { d: "M4 8.5 12 13l8-4.5" }],
638
+ ["path", { d: "M12 13v7" }]
639
+ ],
640
+ cdn: [
641
+ ["circle", { cx: "12", cy: "12", r: "8" }],
642
+ ["path", { d: "M4 12h16M12 4c2.2 2.1 3.3 4.8 3.3 8S14.2 17.9 12 20M12 4c-2.2 2.1-3.3 4.8-3.3 8S9.8 17.9 12 20" }]
643
+ ],
644
+ cache: [
645
+ ["path", { d: "M13 2 5 13h6l-1 9 9-13h-6l0-7Z" }]
646
+ ],
647
+ code: [
648
+ ["path", { d: "m9 18-6-6 6-6" }],
649
+ ["path", { d: "m15 6 6 6-6 6" }],
650
+ ["path", { d: "m14 4-4 16" }]
651
+ ],
652
+ messaging: [
653
+ ["path", { d: "M4 6h16v10H8l-4 4V6Z" }],
654
+ ["path", { d: "M8 10h8M8 13h5" }]
655
+ ],
656
+ network: [
657
+ ["circle", { cx: "6", cy: "7", r: "2" }],
658
+ ["circle", { cx: "18", cy: "7", r: "2" }],
659
+ ["circle", { cx: "12", cy: "18", r: "2" }],
660
+ ["path", { d: "M8 8.5 11 16M16 8.5 13 16M8 7h8" }]
661
+ ],
662
+ platform: [
663
+ ["rect", { x: "4", y: "5", width: "16", height: "14", rx: "2.5" }],
664
+ ["path", { d: "M8 9h8M8 13h8M8 17h4" }]
665
+ ],
666
+ security: [
667
+ ["path", { d: "M12 3 5 6v5c0 4.5 3 7.7 7 10 4-2.3 7-5.5 7-10V6l-7-3Z" }],
668
+ ["path", { d: "M9.5 12.5 11.2 14 15 10" }]
669
+ ],
670
+ delivery: [
671
+ ["path", { d: "M4 7h10" }],
672
+ ["path", { d: "M4 12h16" }],
673
+ ["path", { d: "M4 17h10" }],
674
+ ["path", { d: "m16 7 4 5-4 5" }]
675
+ ],
676
+ observability: [
677
+ ["path", { d: "M4 14s2.5-5 8-5 8 5 8 5-2.5 5-8 5-8-5-8-5Z" }],
678
+ ["circle", { cx: "12", cy: "14", r: "2.5" }]
679
+ ],
680
+ distributed: [
681
+ ["circle", { cx: "6", cy: "12", r: "2.5" }],
682
+ ["circle", { cx: "18", cy: "6", r: "2.5" }],
683
+ ["circle", { cx: "18", cy: "18", r: "2.5" }],
684
+ ["path", { d: "M8.4 11 15.6 7M8.4 13 15.6 17" }]
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
+ ],
729
+ flow: [
730
+ ["path", { d: "M5 12h14" }],
731
+ ["path", { d: "m13 6 6 6-6 6" }]
732
+ ],
733
+ decision: [
734
+ ["path", { d: "M12 3 21 12 12 21 3 12 12 3Z" }],
735
+ ["path", { d: "M12 8v4" }],
736
+ ["path", { d: "M12 16h.01" }]
737
+ ]
738
+ };
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;
742
+ if (node.kind === "api_gateway" || node.kind === "gateway" || node.kind === "load_balancer" || node.kind === "ingress") return "gateway";
743
+ if (node.kind === "db" || node.kind === "database" || node.kind === "sql" || node.kind === "nosql" || node.kind === "warehouse") return "database";
744
+ if (node.kind === "bucket" || node.kind === "object_store" || node.kind === "blob" || node.kind === "volume" || node.kind === "disk") return "storage";
745
+ if (node.kind === "cdn" || node.kind === "dns" || node.kind === "internet") return "cdn";
746
+ if (node.kind === "queue" || node.kind === "topic" || node.kind === "stream" || node.kind === "event_bus" || node.kind === "broker") return "queue";
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";
759
+ if (node.kind === "browser" || node.kind === "web" || node.kind === "frontend" || node.kind === "app") return "browser";
760
+ if (node.kind === "user" || node.kind === "client") return "user";
761
+ if (node.kind === "decision" || node.kind === "condition") return "decision";
762
+ if (node.kind === "cache") return "cache";
763
+ return ICONS[node.kind] ? node.kind : node.role;
764
+ }
765
+ function appendGlyph(doc, wrap, spec) {
766
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
767
+ svg.setAttribute("viewBox", "0 0 24 24");
768
+ svg.setAttribute("fill", "none");
769
+ svg.setAttribute("stroke-width", "1.75");
770
+ svg.setAttribute("stroke-linecap", "round");
771
+ svg.setAttribute("stroke-linejoin", "round");
772
+ for (const [tag, attrs] of spec) {
773
+ const child = doc.createElementNS("http://www.w3.org/2000/svg", tag);
774
+ for (const [name, value] of Object.entries(attrs)) child.setAttribute(name, value);
775
+ svg.appendChild(child);
776
+ }
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);
809
+ return wrap;
810
+ }
811
+ function createNodeEl(node, theme, assets) {
523
812
  const el = document.createElement("div");
524
813
  el.className = "markdy-node markdy-scene-actor";
525
814
  el.dataset.actor = node.id;
@@ -530,15 +819,19 @@ function createNodeEl(node, theme) {
530
819
  el.style.setProperty("--md-node-h", `${node.height}px`);
531
820
  const roleColor = theme.roles[node.role] ?? theme.accent;
532
821
  el.style.setProperty("--md-role-color", roleColor);
533
- const rail = document.createElement("div");
534
- rail.className = "markdy-node__rail";
535
- const type = document.createElement("div");
536
- type.className = "markdy-node__type";
537
- type.textContent = node.kind.replace(/_/g, " ");
822
+ const typeText = node.kind.replace(/_/g, " ");
823
+ el.dataset.kind = node.kind;
824
+ el.dataset.icon = iconKeyForNode(node);
825
+ el.title = `${node.label} (${typeText})`;
826
+ el.setAttribute("aria-label", el.title);
827
+ const body = document.createElement("div");
828
+ body.className = "markdy-node__body";
829
+ const icon = createNodeMediaEl(document, node, assets);
538
830
  const label = document.createElement("div");
539
831
  label.className = "markdy-node__label";
540
832
  label.textContent = node.label;
541
- el.append(rail, type, label);
833
+ body.append(icon, label);
834
+ el.append(body);
542
835
  return el;
543
836
  }
544
837
  function createTitleEl(title) {
@@ -572,15 +865,15 @@ function ensureSceneStyles(doc) {
572
865
  linear-gradient(90deg, var(--md-grid-minor) 1px, transparent 1px) 0 0 / 32px 32px,
573
866
  linear-gradient(var(--md-grid-major) 1px, transparent 1px) 0 0 / 160px 160px,
574
867
  linear-gradient(90deg, var(--md-grid-major) 1px, transparent 1px) 0 0 / 160px 160px;
575
- mask-image: linear-gradient(to bottom, transparent, #000 10%, #000 90%, transparent);
576
- opacity: 0.82;
868
+ mask-image: radial-gradient(ellipse at 50% 42%, #000 55%, transparent 100%);
869
+ opacity: 0.5;
577
870
  }
578
871
  .markdy-scene-root::after {
579
872
  z-index: 1;
580
873
  background:
581
- radial-gradient(ellipse at 50% 0%, color-mix(in srgb, var(--md-accent) 16%, transparent), transparent 42%),
582
- linear-gradient(180deg, transparent 0%, rgba(2, 6, 23, 0.08) 58%, var(--md-vignette) 100%);
583
- 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;
584
877
  }
585
878
  .markdy-scene-content { z-index: 2; }
586
879
  .markdy-scene-actor-layer {
@@ -604,6 +897,10 @@ function applyThemeToScene(scene, theme) {
604
897
  scene.style.setProperty("--md-grid-major", theme.gridMajor);
605
898
  scene.style.setProperty("--md-vignette", theme.vignette);
606
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)");
607
904
  scene.dataset.markdyTheme = theme.name;
608
905
  }
609
906
 
@@ -615,11 +912,15 @@ function createPlayer(opts) {
615
912
  autoplay = true,
616
913
  loop = true,
617
914
  copyright = true,
618
- progressBar = true,
915
+ assets,
916
+ progressBar,
917
+ sceneBoundaryProgress,
918
+ playbackRate: initialPlaybackRate = 1,
619
919
  onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
620
920
  onTimeUpdate,
621
921
  onPlayStateChange
622
922
  } = opts;
923
+ const showSceneBoundaryProgress = sceneBoundaryProgress ?? progressBar ?? true;
623
924
  const { ast, plan } = parseAndCompile(code);
624
925
  for (const w of ast.diagnostics) {
625
926
  if (w.severity === "warning") onWarning(w);
@@ -635,7 +936,7 @@ function createPlayer(opts) {
635
936
  });
636
937
  container.appendChild(viewport);
637
938
  let progressEl = null;
638
- if (progressBar) {
939
+ if (showSceneBoundaryProgress) {
639
940
  progressEl = document.createElement("div");
640
941
  Object.assign(progressEl.style, {
641
942
  position: "absolute",
@@ -706,7 +1007,7 @@ function createPlayer(opts) {
706
1007
  actorLayer.appendChild(titleEl);
707
1008
  const nodeEls = /* @__PURE__ */ new Map();
708
1009
  for (const node of plan.nodes) {
709
- const el = createNodeEl(node, plan.theme);
1010
+ const el = createNodeEl(node, plan.theme, assets);
710
1011
  actorLayer.appendChild(el);
711
1012
  nodeEls.set(node.id, el);
712
1013
  }
@@ -726,6 +1027,7 @@ function createPlayer(opts) {
726
1027
  anim.currentTime = 0;
727
1028
  }
728
1029
  let sceneMs = 0;
1030
+ let playbackRate = Number.isFinite(initialPlaybackRate) && initialPlaybackRate > 0 ? initialPlaybackRate : 1;
729
1031
  let lastRafTs = null;
730
1032
  let isPlaying = false;
731
1033
  let rafId = null;
@@ -734,7 +1036,7 @@ function createPlayer(opts) {
734
1036
  onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
735
1037
  }
736
1038
  function rafTick(timestamp) {
737
- if (lastRafTs !== null) sceneMs += timestamp - lastRafTs;
1039
+ if (lastRafTs !== null) sceneMs += (timestamp - lastRafTs) * playbackRate;
738
1040
  lastRafTs = timestamp;
739
1041
  if (totalDurationMs > 0 && sceneMs >= totalDurationMs) {
740
1042
  if (loop) sceneMs = sceneMs % totalDurationMs;
@@ -773,6 +1075,13 @@ function createPlayer(opts) {
773
1075
  applyCurrentTime();
774
1076
  if (totalDurationMs > 0) updateProgressBar(sceneMs / totalDurationMs);
775
1077
  },
1078
+ setPlaybackRate(rate) {
1079
+ if (!Number.isFinite(rate) || rate <= 0) return;
1080
+ playbackRate = rate;
1081
+ },
1082
+ playbackRate() {
1083
+ return playbackRate;
1084
+ },
776
1085
  currentTime() {
777
1086
  return sceneMs / 1e3;
778
1087
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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.1"
46
+ "@markdy/core": "0.8.3"
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.1"
53
+ "@markdy/stdlib-systems": "0.8.3"
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
  }