@markdy/renderer-dom 0.8.1 → 0.8.2

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
 
@@ -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,12 @@ interface PlayerOptions {
10
10
  autoplay?: boolean;
11
11
  loop?: boolean;
12
12
  copyright?: boolean;
13
+ /** @deprecated Prefer sceneBoundaryProgress. */
13
14
  progressBar?: boolean;
15
+ /** Show rainbow progress around scene boundary. Defaults to true. */
16
+ sceneBoundaryProgress?: boolean;
17
+ /** Playback speed multiplier. Defaults to 1. */
18
+ playbackRate?: number;
14
19
  onWarning?: (warning: Diagnostic) => void;
15
20
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
16
21
  onPlayStateChange?: (playing: boolean) => void;
@@ -19,6 +24,8 @@ interface Player {
19
24
  play(): void;
20
25
  pause(): void;
21
26
  seek(seconds: number): void;
27
+ setPlaybackRate(rate: number): void;
28
+ playbackRate(): number;
22
29
  currentTime(): number;
23
30
  duration(): number;
24
31
  isPlaying(): boolean;
package/dist/index.js CHANGED
@@ -448,7 +448,7 @@ function ensureNodeStyles(doc) {
448
448
  box-sizing: border-box;
449
449
  width: var(--md-node-w, 184px);
450
450
  height: var(--md-node-h, 88px);
451
- border-radius: 16px;
451
+ border-radius: 14px;
452
452
  border: 1px solid var(--md-border);
453
453
  background: linear-gradient(145deg, var(--md-surface-raised), var(--md-surface));
454
454
  color: var(--md-text);
@@ -479,15 +479,41 @@ function ensureNodeStyles(doc) {
479
479
  border-radius: 16px 0 0 16px;
480
480
  }
481
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);
482
+ display: none;
483
+ }
484
+ .markdy-node__body {
485
+ height: 100%;
486
+ padding: 0 14px 0 18px;
487
+ display: flex;
488
+ align-items: center;
489
+ gap: 10px;
490
+ min-width: 0;
491
+ }
492
+ .markdy-node__icon {
493
+ flex: 0 0 auto;
494
+ width: 30px;
495
+ height: 30px;
496
+ border-radius: 10px;
497
+ display: flex;
498
+ align-items: center;
499
+ justify-content: center;
500
+ 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;
503
+ }
504
+ .markdy-node__icon svg {
505
+ width: 18px;
506
+ height: 18px;
507
+ display: block;
508
+ stroke: currentColor;
509
+ }
510
+ .markdy-node[data-icon="decision"] .markdy-node__icon,
511
+ .markdy-node[data-icon="flow"] .markdy-node__icon {
512
+ border-radius: 999px;
488
513
  }
489
514
  .markdy-node__label {
490
- padding: 2px 14px 0 18px;
515
+ min-width: 0;
516
+ padding: 0;
491
517
  font-size: 15px;
492
518
  font-weight: 650;
493
519
  line-height: 1.25;
@@ -519,6 +545,139 @@ function ensureNodeStyles(doc) {
519
545
  `;
520
546
  doc.head.appendChild(style);
521
547
  }
548
+ var ICONS = {
549
+ compute: [
550
+ ["rect", { x: "5", y: "5", width: "14", height: "14", rx: "3" }],
551
+ ["path", { d: "M9 9h6v6H9zM9 2.5v2.5M15 2.5v2.5M9 19v2.5M15 19v2.5M2.5 9h2.5M2.5 15h2.5M19 9h2.5M19 15h2.5" }]
552
+ ],
553
+ user: [
554
+ ["circle", { cx: "12", cy: "8", r: "3.4" }],
555
+ ["path", { d: "M5.5 20a6.5 6.5 0 0 1 13 0" }]
556
+ ],
557
+ browser: [
558
+ ["rect", { x: "3", y: "5", width: "18", height: "14", rx: "2.5" }],
559
+ ["path", { d: "M3 9h18" }],
560
+ ["path", { d: "M7 7h.01M10 7h.01" }]
561
+ ],
562
+ service: [
563
+ ["path", { d: "M12 3 4.5 7.2 12 11.4l7.5-4.2L12 3Z" }],
564
+ ["path", { d: "M4.5 12 12 16.2 19.5 12" }],
565
+ ["path", { d: "M4.5 16.8 12 21l7.5-4.2" }]
566
+ ],
567
+ gateway: [
568
+ ["circle", { cx: "5", cy: "12", r: "2" }],
569
+ ["circle", { cx: "19", cy: "6", r: "2" }],
570
+ ["circle", { cx: "19", cy: "18", r: "2" }],
571
+ ["path", { d: "M7 12h4l5.5-5" }],
572
+ ["path", { d: "M11 12l5.5 5" }]
573
+ ],
574
+ queue: [
575
+ ["rect", { x: "5", y: "5", width: "14", height: "4", rx: "1.4" }],
576
+ ["rect", { x: "5", y: "10", width: "14", height: "4", rx: "1.4" }],
577
+ ["rect", { x: "5", y: "15", width: "14", height: "4", rx: "1.4" }]
578
+ ],
579
+ worker: [
580
+ ["circle", { cx: "12", cy: "12", r: "3.2" }],
581
+ ["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" }]
582
+ ],
583
+ database: [
584
+ ["ellipse", { cx: "12", cy: "5.5", rx: "7", ry: "3" }],
585
+ ["path", { d: "M5 5.5v10c0 1.7 3.1 3 7 3s7-1.3 7-3v-10" }],
586
+ ["path", { d: "M5 10.5c0 1.7 3.1 3 7 3s7-1.3 7-3" }]
587
+ ],
588
+ storage: [
589
+ ["path", { d: "M4 8.5 12 4l8 4.5v7L12 20l-8-4.5v-7Z" }],
590
+ ["path", { d: "M4 8.5 12 13l8-4.5" }],
591
+ ["path", { d: "M12 13v7" }]
592
+ ],
593
+ cdn: [
594
+ ["circle", { cx: "12", cy: "12", r: "8" }],
595
+ ["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" }]
596
+ ],
597
+ cache: [
598
+ ["path", { d: "M13 2 5 13h6l-1 9 9-13h-6l0-7Z" }]
599
+ ],
600
+ code: [
601
+ ["path", { d: "m9 18-6-6 6-6" }],
602
+ ["path", { d: "m15 6 6 6-6 6" }],
603
+ ["path", { d: "m14 4-4 16" }]
604
+ ],
605
+ messaging: [
606
+ ["path", { d: "M4 6h16v10H8l-4 4V6Z" }],
607
+ ["path", { d: "M8 10h8M8 13h5" }]
608
+ ],
609
+ network: [
610
+ ["circle", { cx: "6", cy: "7", r: "2" }],
611
+ ["circle", { cx: "18", cy: "7", r: "2" }],
612
+ ["circle", { cx: "12", cy: "18", r: "2" }],
613
+ ["path", { d: "M8 8.5 11 16M16 8.5 13 16M8 7h8" }]
614
+ ],
615
+ platform: [
616
+ ["rect", { x: "4", y: "5", width: "16", height: "14", rx: "2.5" }],
617
+ ["path", { d: "M8 9h8M8 13h8M8 17h4" }]
618
+ ],
619
+ security: [
620
+ ["path", { d: "M12 3 5 6v5c0 4.5 3 7.7 7 10 4-2.3 7-5.5 7-10V6l-7-3Z" }],
621
+ ["path", { d: "M9.5 12.5 11.2 14 15 10" }]
622
+ ],
623
+ delivery: [
624
+ ["path", { d: "M4 7h10" }],
625
+ ["path", { d: "M4 12h16" }],
626
+ ["path", { d: "M4 17h10" }],
627
+ ["path", { d: "m16 7 4 5-4 5" }]
628
+ ],
629
+ observability: [
630
+ ["path", { d: "M4 14s2.5-5 8-5 8 5 8 5-2.5 5-8 5-8-5-8-5Z" }],
631
+ ["circle", { cx: "12", cy: "14", r: "2.5" }]
632
+ ],
633
+ distributed: [
634
+ ["circle", { cx: "6", cy: "12", r: "2.5" }],
635
+ ["circle", { cx: "18", cy: "6", r: "2.5" }],
636
+ ["circle", { cx: "18", cy: "18", r: "2.5" }],
637
+ ["path", { d: "M8.4 11 15.6 7M8.4 13 15.6 17" }]
638
+ ],
639
+ flow: [
640
+ ["path", { d: "M5 12h14" }],
641
+ ["path", { d: "m13 6 6 6-6 6" }]
642
+ ],
643
+ decision: [
644
+ ["path", { d: "M12 3 21 12 12 21 3 12 12 3Z" }],
645
+ ["path", { d: "M12 8v4" }],
646
+ ["path", { d: "M12 16h.01" }]
647
+ ]
648
+ };
649
+ function iconKeyForNode(node) {
650
+ if (node.kind === "api_gateway" || node.kind === "gateway" || node.kind === "load_balancer" || node.kind === "ingress") return "gateway";
651
+ if (node.kind === "db" || node.kind === "database" || node.kind === "sql" || node.kind === "nosql" || node.kind === "warehouse") return "database";
652
+ if (node.kind === "bucket" || node.kind === "object_store" || node.kind === "blob" || node.kind === "volume" || node.kind === "disk") return "storage";
653
+ if (node.kind === "cdn" || node.kind === "dns" || node.kind === "internet") return "cdn";
654
+ 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";
656
+ if (node.kind === "browser" || node.kind === "web" || node.kind === "frontend" || node.kind === "app") return "browser";
657
+ if (node.kind === "user" || node.kind === "client") return "user";
658
+ if (node.kind === "decision" || node.kind === "condition") return "decision";
659
+ if (node.kind === "cache") return "cache";
660
+ return ICONS[node.kind] ? node.kind : node.role;
661
+ }
662
+ function createIconEl(doc, node) {
663
+ const wrap = doc.createElement("div");
664
+ wrap.className = "markdy-node__icon";
665
+ wrap.setAttribute("aria-hidden", "true");
666
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
667
+ svg.setAttribute("viewBox", "0 0 24 24");
668
+ svg.setAttribute("fill", "none");
669
+ svg.setAttribute("stroke-width", "2");
670
+ svg.setAttribute("stroke-linecap", "round");
671
+ svg.setAttribute("stroke-linejoin", "round");
672
+ const spec = ICONS[iconKeyForNode(node)] ?? ICONS.service;
673
+ for (const [tag, attrs] of spec) {
674
+ const child = doc.createElementNS("http://www.w3.org/2000/svg", tag);
675
+ for (const [name, value] of Object.entries(attrs)) child.setAttribute(name, value);
676
+ svg.appendChild(child);
677
+ }
678
+ wrap.appendChild(svg);
679
+ return wrap;
680
+ }
522
681
  function createNodeEl(node, theme) {
523
682
  const el = document.createElement("div");
524
683
  el.className = "markdy-node markdy-scene-actor";
@@ -530,15 +689,24 @@ function createNodeEl(node, theme) {
530
689
  el.style.setProperty("--md-node-h", `${node.height}px`);
531
690
  const roleColor = theme.roles[node.role] ?? theme.accent;
532
691
  el.style.setProperty("--md-role-color", roleColor);
692
+ const typeText = node.kind.replace(/_/g, " ");
693
+ el.dataset.kind = node.kind;
694
+ el.dataset.icon = iconKeyForNode(node);
695
+ el.title = `${node.label} (${typeText})`;
696
+ el.setAttribute("aria-label", el.title);
533
697
  const rail = document.createElement("div");
534
698
  rail.className = "markdy-node__rail";
535
699
  const type = document.createElement("div");
536
700
  type.className = "markdy-node__type";
537
- type.textContent = node.kind.replace(/_/g, " ");
701
+ type.textContent = typeText;
702
+ const body = document.createElement("div");
703
+ body.className = "markdy-node__body";
704
+ const icon = createIconEl(document, node);
538
705
  const label = document.createElement("div");
539
706
  label.className = "markdy-node__label";
540
707
  label.textContent = node.label;
541
- el.append(rail, type, label);
708
+ body.append(icon, label);
709
+ el.append(rail, type, body);
542
710
  return el;
543
711
  }
544
712
  function createTitleEl(title) {
@@ -615,11 +783,14 @@ function createPlayer(opts) {
615
783
  autoplay = true,
616
784
  loop = true,
617
785
  copyright = true,
618
- progressBar = true,
786
+ progressBar,
787
+ sceneBoundaryProgress,
788
+ playbackRate: initialPlaybackRate = 1,
619
789
  onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
620
790
  onTimeUpdate,
621
791
  onPlayStateChange
622
792
  } = opts;
793
+ const showSceneBoundaryProgress = sceneBoundaryProgress ?? progressBar ?? true;
623
794
  const { ast, plan } = parseAndCompile(code);
624
795
  for (const w of ast.diagnostics) {
625
796
  if (w.severity === "warning") onWarning(w);
@@ -635,7 +806,7 @@ function createPlayer(opts) {
635
806
  });
636
807
  container.appendChild(viewport);
637
808
  let progressEl = null;
638
- if (progressBar) {
809
+ if (showSceneBoundaryProgress) {
639
810
  progressEl = document.createElement("div");
640
811
  Object.assign(progressEl.style, {
641
812
  position: "absolute",
@@ -726,6 +897,7 @@ function createPlayer(opts) {
726
897
  anim.currentTime = 0;
727
898
  }
728
899
  let sceneMs = 0;
900
+ let playbackRate = Number.isFinite(initialPlaybackRate) && initialPlaybackRate > 0 ? initialPlaybackRate : 1;
729
901
  let lastRafTs = null;
730
902
  let isPlaying = false;
731
903
  let rafId = null;
@@ -734,7 +906,7 @@ function createPlayer(opts) {
734
906
  onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
735
907
  }
736
908
  function rafTick(timestamp) {
737
- if (lastRafTs !== null) sceneMs += timestamp - lastRafTs;
909
+ if (lastRafTs !== null) sceneMs += (timestamp - lastRafTs) * playbackRate;
738
910
  lastRafTs = timestamp;
739
911
  if (totalDurationMs > 0 && sceneMs >= totalDurationMs) {
740
912
  if (loop) sceneMs = sceneMs % totalDurationMs;
@@ -773,6 +945,13 @@ function createPlayer(opts) {
773
945
  applyCurrentTime();
774
946
  if (totalDurationMs > 0) updateProgressBar(sceneMs / totalDurationMs);
775
947
  },
948
+ setPlaybackRate(rate) {
949
+ if (!Number.isFinite(rate) || rate <= 0) return;
950
+ playbackRate = rate;
951
+ },
952
+ playbackRate() {
953
+ return playbackRate;
954
+ },
776
955
  currentTime() {
777
956
  return sceneMs / 1e3;
778
957
  },
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.2",
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,14 +43,14 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@markdy/core": "0.8.1"
46
+ "@markdy/core": "0.8.2"
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.2"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsup",