@markdy/renderer-dom 0.8.5 → 0.8.6

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.
Files changed (2) hide show
  1. package/dist/index.js +149 -8
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -199,6 +199,10 @@ var EDGE_STYLES = {
199
199
  event: { dash: "2 6", marker: "dot" },
200
200
  dependency: { dash: "4 4", marker: "none" }
201
201
  };
202
+ var INITIAL_CAMERA_TRANSFORM = "translate(0px, 0px) scale(1)";
203
+ var DEFAULT_FRAME_ZOOM = 1.18;
204
+ var MAX_FRAME_ZOOM = 1.75;
205
+ var FRAME_PADDING = 88;
202
206
  function dedupePoints(points) {
203
207
  const out = [];
204
208
  for (const p of points) {
@@ -362,6 +366,27 @@ function animateEdgeReveal(runtime, startMs, durMs) {
362
366
  }
363
367
  return anims;
364
368
  }
369
+ function computeFrameTransform(targetIds, nodes, bounds, requestedZoom = DEFAULT_FRAME_ZOOM) {
370
+ const targets = new Set(targetIds);
371
+ const selected = nodes.filter((node) => targets.has(node.id));
372
+ if (selected.length === 0) return void 0;
373
+ const framesEveryNode = selected.length === nodes.length && nodes.every((node) => targets.has(node.id));
374
+ if (framesEveryNode) return INITIAL_CAMERA_TRANSFORM;
375
+ const zoom = Number.isFinite(requestedZoom) && requestedZoom > 0 ? requestedZoom : DEFAULT_FRAME_ZOOM;
376
+ const minX = Math.min(...selected.map((node) => node.x));
377
+ const minY = Math.min(...selected.map((node) => node.y));
378
+ const maxX = Math.max(...selected.map((node) => node.x + node.width));
379
+ const maxY = Math.max(...selected.map((node) => node.y + node.height));
380
+ const frameWidth = Math.max(1, maxX - minX + FRAME_PADDING * 2);
381
+ const frameHeight = Math.max(1, maxY - minY + FRAME_PADDING * 2);
382
+ const fitScale = Math.min(bounds.width / frameWidth, bounds.height / frameHeight);
383
+ const scale = Math.max(1, Math.min(zoom, fitScale, MAX_FRAME_ZOOM));
384
+ const centerX = (minX + maxX) / 2;
385
+ const centerY = (minY + maxY) / 2;
386
+ const tx = Math.round((bounds.width / 2 - centerX * scale) * 10) / 10;
387
+ const ty = Math.round((bounds.height / 2 - centerY * scale) * 10) / 10;
388
+ return `translate(${tx}px, ${ty}px) scale(${Math.round(scale * 1e3) / 1e3})`;
389
+ }
365
390
  function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds) {
366
391
  const anims = [];
367
392
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
@@ -371,6 +396,9 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds)
371
396
  const laneByPair = /* @__PURE__ */ new Map();
372
397
  const svg = ensureEdgeLayer(scene);
373
398
  const sceneId = `md-${Math.random().toString(36).slice(2, 8)}`;
399
+ let cameraTransform = scene.style.transform || INITIAL_CAMERA_TRANSFORM;
400
+ scene.style.transformOrigin = "0 0";
401
+ scene.style.transform = cameraTransform;
374
402
  anims.push(
375
403
  titleEl.animate(
376
404
  [{ opacity: 0, transform: "translateY(-6px)" }, { opacity: 1, transform: "translateY(0)" }],
@@ -418,19 +446,37 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds)
418
446
  continue;
419
447
  }
420
448
  if (cue.kind === "focus") {
449
+ const zoom = typeof cue.params.zoom === "number" && cue.params.zoom > 0 ? cue.params.zoom : 1.03;
421
450
  for (const id of cue.targets) {
422
451
  const el = nodeEls.get(id);
423
452
  if (!el) continue;
424
453
  el.dataset.focused = "1";
425
454
  anims.push(
426
455
  el.animate(
427
- [{ transform: "scale(1)" }, { transform: "scale(1.03)" }, { transform: "scale(1)" }],
456
+ [{ transform: "scale(1)" }, { transform: `scale(${zoom})` }, { transform: "scale(1)" }],
428
457
  { duration: durMs, delay: startMs, fill: "forwards" }
429
458
  )
430
459
  );
431
460
  }
432
461
  continue;
433
462
  }
463
+ if (cue.kind === "frame") {
464
+ const nextTransform = computeFrameTransform(
465
+ cue.targets,
466
+ nodes,
467
+ bounds,
468
+ typeof cue.params.zoom === "number" ? cue.params.zoom : DEFAULT_FRAME_ZOOM
469
+ );
470
+ if (!nextTransform) continue;
471
+ anims.push(
472
+ scene.animate(
473
+ [{ transform: cameraTransform }, { transform: nextTransform }],
474
+ { duration: durMs, delay: startMs, fill: "forwards", easing: "ease-in-out" }
475
+ )
476
+ );
477
+ cameraTransform = nextTransform;
478
+ continue;
479
+ }
434
480
  if (cue.kind === "flow" && cue.segments?.[0]) {
435
481
  const seg = cue.segments[0];
436
482
  const from = nodeById.get(seg.from);
@@ -577,6 +623,7 @@ function ensureNodeStyles(doc) {
577
623
  left: 44px;
578
624
  top: 26px;
579
625
  right: 44px;
626
+ z-index: 130;
580
627
  font-size: 32px;
581
628
  font-weight: 700;
582
629
  line-height: 1.2;
@@ -808,6 +855,20 @@ function createNodeMediaEl(doc, node, assets) {
808
855
  appendGlyph(doc, wrap, ICONS[iconKeyForNode(node)] ?? ICONS.service);
809
856
  return wrap;
810
857
  }
858
+ function applyDeclaredNodeStyle(el, style) {
859
+ if (!style) return;
860
+ const fill = typeof style.fill === "string" ? style.fill : void 0;
861
+ const surface = typeof style.surface === "string" ? style.surface : fill;
862
+ const surfaceRaised = typeof style.surfaceRaised === "string" ? style.surfaceRaised : surface ? `color-mix(in srgb, ${surface} 90%, #ffffff 10%)` : void 0;
863
+ const stroke = typeof style.stroke === "string" ? style.stroke : void 0;
864
+ const text = typeof style.text === "string" ? style.text : void 0;
865
+ const accent = typeof style.accent === "string" ? style.accent : void 0;
866
+ if (surface) el.style.setProperty("--md-node-surface", surface);
867
+ if (surfaceRaised) el.style.setProperty("--md-node-surface-raised", surfaceRaised);
868
+ if (stroke) el.style.setProperty("--md-hairline", stroke);
869
+ if (text) el.style.color = text;
870
+ if (accent) el.style.setProperty("--md-role-color", accent);
871
+ }
811
872
  function createNodeEl(node, theme, assets) {
812
873
  const el = document.createElement("div");
813
874
  el.className = "markdy-node markdy-scene-node";
@@ -819,6 +880,7 @@ function createNodeEl(node, theme, assets) {
819
880
  el.style.setProperty("--md-node-h", `${node.height}px`);
820
881
  const roleColor = theme.roles[node.role] ?? theme.accent;
821
882
  el.style.setProperty("--md-role-color", roleColor);
883
+ applyDeclaredNodeStyle(el, node.style);
822
884
  const typeText = node.kind.replace(/_/g, " ");
823
885
  el.dataset.kind = node.kind;
824
886
  el.dataset.icon = iconKeyForNode(node);
@@ -876,11 +938,46 @@ function ensureSceneStyles(doc) {
876
938
  opacity: 0.62;
877
939
  }
878
940
  .markdy-scene-content { z-index: 2; }
941
+ .markdy-camera-layer {
942
+ position: absolute;
943
+ inset: 0;
944
+ overflow: visible;
945
+ transform-origin: 0 0;
946
+ transform: translate(0px, 0px) scale(1);
947
+ }
879
948
  .markdy-scene-node-layer {
880
949
  position: absolute;
881
950
  inset: 0;
882
951
  overflow: visible;
883
952
  }
953
+ .markdy-beat-caption-layer {
954
+ position: absolute;
955
+ left: 44px;
956
+ right: 44px;
957
+ bottom: 30px;
958
+ z-index: 140;
959
+ display: grid;
960
+ justify-items: center;
961
+ align-items: end;
962
+ pointer-events: none;
963
+ }
964
+ .markdy-beat-caption {
965
+ grid-area: 1 / 1;
966
+ max-width: min(720px, 100%);
967
+ padding: 10px 16px;
968
+ border-radius: 999px;
969
+ color: var(--md-text);
970
+ background: color-mix(in srgb, var(--md-surface-raised) 88%, transparent);
971
+ backdrop-filter: blur(6px);
972
+ box-shadow:
973
+ 0 14px 32px -20px var(--md-shadow, rgba(2, 6, 23, 0.55)),
974
+ inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent));
975
+ font: 600 14px/1.3 Inter, ui-sans-serif, system-ui, sans-serif;
976
+ text-align: center;
977
+ opacity: 0;
978
+ transform: translateY(8px);
979
+ will-change: opacity, transform;
980
+ }
884
981
  `;
885
982
  doc.head.appendChild(style);
886
983
  }
@@ -905,6 +1002,42 @@ function applyThemeToScene(scene, theme) {
905
1002
  }
906
1003
 
907
1004
  // src/diagram.ts
1005
+ function createBeatCaptionLayer(doc, beats) {
1006
+ const layer = doc.createElement("div");
1007
+ layer.className = "markdy-beat-caption-layer";
1008
+ for (const beat of beats) {
1009
+ if (!beat.label) continue;
1010
+ const caption = doc.createElement("div");
1011
+ caption.className = "markdy-beat-caption";
1012
+ caption.textContent = beat.label;
1013
+ caption.dataset.beat = beat.name;
1014
+ layer.appendChild(caption);
1015
+ }
1016
+ return layer;
1017
+ }
1018
+ function buildBeatCaptionAnimations(beats, layer) {
1019
+ const anims = [];
1020
+ const captions = Array.from(layer.querySelectorAll(".markdy-beat-caption"));
1021
+ for (const beat of beats) {
1022
+ if (!beat.label) continue;
1023
+ const caption = captions.find((item) => item.dataset.beat === beat.name);
1024
+ if (!caption) continue;
1025
+ const startMs = beat.start * 1e3;
1026
+ const durMs = Math.max((beat.end - beat.start) * 1e3, 650);
1027
+ anims.push(
1028
+ caption.animate(
1029
+ [
1030
+ { opacity: 0, transform: "translateY(8px)" },
1031
+ { opacity: 1, transform: "translateY(0)", offset: 0.16 },
1032
+ { opacity: 1, transform: "translateY(0)", offset: 0.82 },
1033
+ { opacity: 0, transform: "translateY(-4px)" }
1034
+ ],
1035
+ { duration: durMs, delay: startMs, fill: "forwards", easing: "ease-out" }
1036
+ )
1037
+ );
1038
+ }
1039
+ return anims;
1040
+ }
908
1041
  function createDiagram(opts) {
909
1042
  const {
910
1043
  container,
@@ -1000,11 +1133,16 @@ function createDiagram(opts) {
1000
1133
  sceneContent.className = "markdy-scene-content";
1001
1134
  Object.assign(sceneContent.style, { position: "absolute", inset: "0" });
1002
1135
  scene.appendChild(sceneContent);
1136
+ const titleEl = createTitleEl(plan.title);
1137
+ sceneContent.appendChild(titleEl);
1138
+ const cameraLayer = document.createElement("div");
1139
+ cameraLayer.className = "markdy-camera-layer";
1140
+ sceneContent.appendChild(cameraLayer);
1003
1141
  const nodeLayer = document.createElement("div");
1004
1142
  nodeLayer.className = "markdy-scene-node-layer";
1005
- sceneContent.appendChild(nodeLayer);
1006
- const titleEl = createTitleEl(plan.title);
1007
- nodeLayer.appendChild(titleEl);
1143
+ cameraLayer.appendChild(nodeLayer);
1144
+ const captionLayer = createBeatCaptionLayer(document, plan.beats);
1145
+ sceneContent.appendChild(captionLayer);
1008
1146
  const nodeEls = /* @__PURE__ */ new Map();
1009
1147
  for (const node of plan.nodes) {
1010
1148
  const el = createNodeEl(node, plan.theme, assets);
@@ -1018,10 +1156,13 @@ function createDiagram(opts) {
1018
1156
  scaleScene();
1019
1157
  const resizeObserver = new ResizeObserver(scaleScene);
1020
1158
  resizeObserver.observe(viewport);
1021
- const allAnims = buildCueAnimations(plan.cues, nodeEls, plan.nodes, plan.theme, sceneContent, titleEl, {
1022
- width: plan.meta.width,
1023
- height: plan.meta.height
1024
- });
1159
+ const allAnims = [
1160
+ ...buildCueAnimations(plan.cues, nodeEls, plan.nodes, plan.theme, cameraLayer, titleEl, {
1161
+ width: plan.meta.width,
1162
+ height: plan.meta.height
1163
+ }),
1164
+ ...buildBeatCaptionAnimations(plan.beats, captionLayer)
1165
+ ];
1025
1166
  for (const anim of allAnims) {
1026
1167
  anim.pause();
1027
1168
  anim.currentTime = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
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.5"
46
+ "@markdy/core": "0.8.6"
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.5"
53
+ "@markdy/stdlib-systems": "0.8.6"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsup",