@markdy/renderer-dom 1.0.8 → 1.0.9

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
@@ -97,6 +97,17 @@ interface GifDiagramExportOptions extends SvgExportOptions {
97
97
  }
98
98
  declare function exportDiagramAsGif(container: HTMLElement, timeline: TimelineController, options?: GifDiagramExportOptions): Promise<Blob>;
99
99
 
100
+ /**
101
+ * packages/renderer-dom/src/export/png-exporter.ts
102
+ * High-DPI raster PNG export with 2x retina scaling.
103
+ * Zero external dependencies.
104
+ *
105
+ * Fix: Inline all external resources (images, fonts) as base64 data URIs
106
+ * before drawing the SVG to canvas. A foreignObject-wrapped SVG taints the
107
+ * canvas whenever it references any external URL, so every resource must be
108
+ * inlined first.
109
+ */
110
+
100
111
  interface PngExportOptions extends SvgExportOptions {
101
112
  pixelRatio?: number;
102
113
  }
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ function countPathIntersections(points, obstacles) {
49
49
  function round1(n) {
50
50
  return Math.round(n * 10) / 10;
51
51
  }
52
- function toPathD(points, cornerRadius = 8) {
52
+ function toPathD(points, cornerRadius = 14) {
53
53
  if (points.length < 2) return "";
54
54
  if (points.length === 2) {
55
55
  const [a, b] = points;
@@ -73,7 +73,7 @@ function toPathD(points, cornerRadius = 8) {
73
73
  const dy2 = next.y - cur.y;
74
74
  const len1 = Math.hypot(dx1, dy1);
75
75
  const len2 = Math.hypot(dx2, dy2);
76
- if (len1 < 0.01 || len2 < 0.01) {
76
+ if (len1 < 0.5 || len2 < 0.5) {
77
77
  parts.push(`L ${round1(cur.x)} ${round1(cur.y)}`);
78
78
  continue;
79
79
  }
@@ -89,14 +89,14 @@ function toPathD(points, cornerRadius = 8) {
89
89
  }
90
90
  function selfLoopPath(rect) {
91
91
  const top = rect.y;
92
- const left = rect.x + rect.width * 0.25;
93
- const right = rect.x + rect.width * 0.75;
94
- const apex = top - 36;
92
+ const left = rect.x + rect.width * 0.3;
93
+ const right = rect.x + rect.width * 0.7;
94
+ const apex = top - 40;
95
95
  return [
96
- { x: left, y: top },
97
- { x: left, y: apex },
98
- { x: right, y: apex },
99
- { x: right, y: top }
96
+ { x: round1(left), y: round1(top) },
97
+ { x: round1(left), y: round1(apex) },
98
+ { x: round1(right), y: round1(apex) },
99
+ { x: round1(right), y: round1(top) }
100
100
  ];
101
101
  }
102
102
  function polylineLength(points) {
@@ -109,7 +109,7 @@ function polylineLength(points) {
109
109
  function segmentLength(a, b) {
110
110
  return Math.hypot(b.x - a.x, b.y - a.y);
111
111
  }
112
- var LABEL_BOX_HEIGHT = 16;
112
+ var LABEL_BOX_HEIGHT = 20;
113
113
  function rectsOverlap(a, b) {
114
114
  return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
115
115
  }
@@ -134,9 +134,9 @@ function placeFlowLabel(points, textWidth, obstacles, bounds) {
134
134
  const horizontal = Math.abs(a.x - b.x) >= Math.abs(a.y - b.y);
135
135
  const half = textWidth / 2;
136
136
  const halfH = LABEL_BOX_HEIGHT / 2;
137
- const pad = 8;
138
- const base = horizontal ? 14 : half + 12;
139
- const step = horizontal ? LABEL_BOX_HEIGHT + 4 : textWidth + 12;
137
+ const pad = 10;
138
+ const base = horizontal ? 16 : half + 14;
139
+ const step = horizontal ? LABEL_BOX_HEIGHT + 6 : textWidth + 14;
140
140
  const order = horizontal ? [-1, 1] : [1, -1];
141
141
  const offsets = [];
142
142
  for (let k = 0; k < 8; k++) {
@@ -166,7 +166,7 @@ function clamp(n, min, max) {
166
166
  return Math.min(max, Math.max(min, n));
167
167
  }
168
168
  function clampPointToScene(point, bounds) {
169
- const pad = 14;
169
+ const pad = 16;
170
170
  return {
171
171
  x: round1(clamp(point.x, pad, bounds.width - pad)),
172
172
  y: round1(clamp(point.y, pad, bounds.height - pad))
@@ -175,7 +175,7 @@ function clampPointToScene(point, bounds) {
175
175
  function laneOffset(lane) {
176
176
  if (lane <= 0) return 0;
177
177
  const step = Math.ceil(lane / 2);
178
- return (lane % 2 === 1 ? 1 : -1) * step * 18;
178
+ return (lane % 2 === 1 ? 1 : -1) * step * 16;
179
179
  }
180
180
  function routeLength(points) {
181
181
  let total = 0;
@@ -196,26 +196,50 @@ function routeBends(points) {
196
196
  }
197
197
  return bends;
198
198
  }
199
+ function cleanCollinearPoints(points) {
200
+ if (points.length <= 2) return points;
201
+ const result = [points[0]];
202
+ for (let i = 1; i < points.length - 1; i++) {
203
+ const prev = result[result.length - 1];
204
+ const cur = points[i];
205
+ const next = points[i + 1];
206
+ const isCollinearX = Math.abs(prev.x - cur.x) < 0.01 && Math.abs(cur.x - next.x) < 0.01;
207
+ const isCollinearY = Math.abs(prev.y - cur.y) < 0.01 && Math.abs(cur.y - next.y) < 0.01;
208
+ const isDuplicate = Math.abs(prev.x - cur.x) < 0.01 && Math.abs(prev.y - cur.y) < 0.01;
209
+ if (!isCollinearX && !isCollinearY && !isDuplicate) {
210
+ result.push(cur);
211
+ }
212
+ }
213
+ result.push(points[points.length - 1]);
214
+ return result;
215
+ }
199
216
  function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
200
217
  const laneShift = laneOffset(lane);
201
218
  const sourceCenter = rectCenter(sourceRect);
202
219
  const targetCenter = rectCenter(targetRect);
203
220
  const horizontalPrimary = Math.abs(targetCenter.x - sourceCenter.x) >= Math.abs(targetCenter.y - sourceCenter.y);
221
+ const sourceRight = targetCenter.x >= sourceCenter.x;
222
+ const targetLeft = targetCenter.x >= sourceCenter.x;
223
+ const sourceDown = targetCenter.y >= sourceCenter.y;
224
+ const targetUp = targetCenter.y >= sourceCenter.y;
204
225
  const source = horizontalPrimary ? {
205
- x: targetCenter.x >= sourceCenter.x ? sourceRect.x2 : sourceRect.x1,
206
- y: clamp(sourceCenter.y + laneShift, sourceRect.y1 + 12, sourceRect.y2 - 12)
226
+ x: sourceRight ? sourceRect.x2 : sourceRect.x1,
227
+ y: clamp(sourceCenter.y + laneShift, sourceRect.y1 + 16, sourceRect.y2 - 16)
207
228
  } : {
208
- x: clamp(sourceCenter.x + laneShift, sourceRect.x1 + 12, sourceRect.x2 - 12),
209
- y: targetCenter.y >= sourceCenter.y ? sourceRect.y2 : sourceRect.y1
229
+ x: clamp(sourceCenter.x + laneShift, sourceRect.x1 + 20, sourceRect.x2 - 20),
230
+ y: sourceDown ? sourceRect.y2 : sourceRect.y1
210
231
  };
211
232
  const target = horizontalPrimary ? {
212
- x: targetCenter.x >= sourceCenter.x ? targetRect.x1 : targetRect.x2,
213
- y: clamp(targetCenter.y + laneShift, targetRect.y1 + 12, targetRect.y2 - 12)
233
+ x: targetLeft ? targetRect.x1 : targetRect.x2,
234
+ y: clamp(targetCenter.y + laneShift, targetRect.y1 + 16, targetRect.y2 - 16)
214
235
  } : {
215
- x: clamp(targetCenter.x + laneShift, targetRect.x1 + 12, targetRect.x2 - 12),
216
- y: targetCenter.y >= sourceCenter.y ? targetRect.y1 : targetRect.y2
236
+ x: clamp(targetCenter.x + laneShift, targetRect.x1 + 20, targetRect.x2 - 20),
237
+ y: targetUp ? targetRect.y1 : targetRect.y2
217
238
  };
218
- const infl = obstacles.map((o) => inflateRect(o, 8));
239
+ const stubLen = 18;
240
+ const sStub = horizontalPrimary ? { x: source.x + (sourceRight ? stubLen : -stubLen), y: source.y } : { x: source.x, y: source.y + (sourceDown ? stubLen : -stubLen) };
241
+ const tStub = horizontalPrimary ? { x: target.x + (targetLeft ? -stubLen : stubLen), y: target.y } : { x: target.x, y: target.y + (targetUp ? -stubLen : stubLen) };
242
+ const infl = obstacles.map((o) => inflateRect(o, 10));
219
243
  const candidates = [];
220
244
  if (Math.abs(source.y - target.y) < 1e-3 || Math.abs(source.x - target.x) < 1e-3) {
221
245
  candidates.push([source, target]);
@@ -223,36 +247,39 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
223
247
  const midX = round1((source.x + target.x) / 2 + laneShift);
224
248
  const midY = round1((source.y + target.y) / 2 + laneShift);
225
249
  candidates.push(
250
+ [source, sStub, { x: midX, y: source.y }, { x: midX, y: target.y }, tStub, target],
251
+ [source, sStub, { x: source.x, y: midY }, { x: target.x, y: midY }, tStub, target],
226
252
  [source, { x: midX, y: source.y }, { x: midX, y: target.y }, target],
227
253
  [source, { x: source.x, y: midY }, { x: target.x, y: midY }, target]
228
254
  );
229
255
  const minObstacleY = infl.length ? Math.min(...infl.map((o) => o.y1)) : Math.min(source.y, target.y);
230
256
  const maxObstacleY = infl.length ? Math.max(...infl.map((o) => o.y2)) : Math.max(source.y, target.y);
231
- const topLane = Math.max(16, minObstacleY - 24 - lane * 12);
232
- const bottomLane = Math.min(bounds.height - 16, maxObstacleY + 24 + lane * 12);
257
+ const topLane = Math.max(20, minObstacleY - 28 - lane * 14);
258
+ const bottomLane = Math.min(bounds.height - 20, maxObstacleY + 28 + lane * 14);
233
259
  candidates.push(
234
- [source, { x: source.x, y: topLane }, { x: target.x, y: topLane }, target],
235
- [source, { x: source.x, y: bottomLane }, { x: target.x, y: bottomLane }, target]
260
+ [source, sStub, { x: sStub.x, y: topLane }, { x: tStub.x, y: topLane }, tStub, target],
261
+ [source, sStub, { x: sStub.x, y: bottomLane }, { x: tStub.x, y: bottomLane }, tStub, target]
236
262
  );
237
263
  const minObstacleX = infl.length ? Math.min(...infl.map((o) => o.x1)) : Math.min(source.x, target.x);
238
264
  const maxObstacleX = infl.length ? Math.max(...infl.map((o) => o.x2)) : Math.max(source.x, target.x);
239
- const leftLane = Math.max(16, minObstacleX - 24 - lane * 12);
240
- const rightLane = Math.min(bounds.width - 16, maxObstacleX + 24 + lane * 12);
265
+ const leftLane = Math.max(20, minObstacleX - 28 - lane * 14);
266
+ const rightLane = Math.min(bounds.width - 20, maxObstacleX + 28 + lane * 14);
241
267
  candidates.push(
242
- [source, { x: leftLane, y: source.y }, { x: leftLane, y: target.y }, target],
243
- [source, { x: rightLane, y: source.y }, { x: rightLane, y: target.y }, target]
268
+ [source, sStub, { x: leftLane, y: sStub.y }, { x: leftLane, y: tStub.y }, tStub, target],
269
+ [source, sStub, { x: rightLane, y: sStub.y }, { x: rightLane, y: tStub.y }, tStub, target]
244
270
  );
245
271
  let best = candidates[0];
246
272
  let bestScore = Number.POSITIVE_INFINITY;
247
273
  for (const candidate of candidates) {
248
- const hits = countPathIntersections(candidate, infl);
249
- const score = hits * 1e5 + routeBends(candidate) * 800 + routeLength(candidate);
274
+ const cleaned = cleanCollinearPoints(candidate);
275
+ const hits = countPathIntersections(cleaned, infl);
276
+ const score = hits * 1e5 + routeBends(cleaned) * 800 + routeLength(cleaned);
250
277
  if (score < bestScore) {
251
- best = candidate;
278
+ best = cleaned;
252
279
  bestScore = score;
253
280
  }
254
281
  }
255
- return best.map((point) => clampPointToScene(point, bounds));
282
+ return cleanCollinearPoints(best.map((point) => clampPointToScene(point, bounds)));
256
283
  }
257
284
 
258
285
  // src/edges.ts
@@ -324,23 +351,25 @@ function ensureDefs(svg, theme, id) {
324
351
  for (const [kind, color] of Object.entries(theme.edges)) {
325
352
  const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
326
353
  arrow.setAttribute("id", `${id}-arrow-${kind}`);
327
- arrow.setAttribute("viewBox", "0 0 10 10");
328
- arrow.setAttribute("refX", "8.5");
329
- arrow.setAttribute("refY", "5");
330
- arrow.setAttribute("markerWidth", "7");
331
- arrow.setAttribute("markerHeight", "7");
354
+ arrow.setAttribute("viewBox", "0 0 12 12");
355
+ arrow.setAttribute("refX", "9.5");
356
+ arrow.setAttribute("refY", "6");
357
+ arrow.setAttribute("markerWidth", "8");
358
+ arrow.setAttribute("markerHeight", "8");
332
359
  arrow.setAttribute("orient", "auto-start-reverse");
333
360
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
334
361
  if (kind === "response") {
335
- path.setAttribute("d", "M 1.5 1.6 L 9 5 L 1.5 8.4");
362
+ path.setAttribute("d", "M 2.5 2.5 L 9.5 6 L 2.5 9.5");
336
363
  path.setAttribute("fill", "none");
337
364
  path.setAttribute("stroke", color);
338
- path.setAttribute("stroke-width", "1.4");
365
+ path.setAttribute("stroke-width", "1.6");
366
+ path.setAttribute("stroke-linecap", "round");
367
+ path.setAttribute("stroke-linejoin", "round");
339
368
  } else if (kind === "event") {
340
- path.setAttribute("d", "M 5 2 A 3 3 0 1 1 5 8 A 3 3 0 1 1 5 2");
369
+ path.setAttribute("d", "M 6 2.5 A 3.5 3.5 0 1 1 6 9.5 A 3.5 3.5 0 1 1 6 2.5");
341
370
  path.setAttribute("fill", color);
342
371
  } else {
343
- path.setAttribute("d", "M 1.5 1.6 L 9 5 L 1.5 8.4 L 3.4 5 Z");
372
+ path.setAttribute("d", "M 2 2.5 L 10 6 L 2 9.5 L 4 6 Z");
344
373
  path.setAttribute("fill", color);
345
374
  }
346
375
  arrow.appendChild(path);
@@ -390,40 +419,43 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
390
419
  path.setAttribute("marker-end", `url(#${sceneId}-arrow-${kind})`);
391
420
  }
392
421
  if (kind !== "dependency") {
393
- path.style.filter = `drop-shadow(0 0 3px ${translucentColor(color, "33")})`;
422
+ path.style.filter = `drop-shadow(0 0 4px ${translucentColor(color, "44")})`;
394
423
  }
395
424
  const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
396
- dot.setAttribute("r", "3.5");
425
+ dot.setAttribute("r", "4");
397
426
  dot.setAttribute("fill", color);
398
427
  dot.style.opacity = "0";
399
- dot.style.filter = `drop-shadow(0 0 4px ${color}aa)`;
428
+ dot.style.filter = `drop-shadow(0 0 6px ${color}) drop-shadow(0 0 12px ${color}88)`;
400
429
  group.append(path, dot);
401
430
  let labelEl;
402
431
  let labelRect;
403
432
  if (label) {
404
- const textWidth = label.length * 6.6 + 10;
433
+ const textWidth = label.length * 6.8 + 14;
405
434
  const placement = placeFlowLabel(points, textWidth, labelObstacles, bounds);
406
435
  labelRect = placement.rect;
407
436
  const plate = document.createElementNS("http://www.w3.org/2000/svg", "rect");
408
- const padX = 7;
437
+ const padX = 8;
409
438
  const halfW = textWidth / 2;
410
439
  plate.setAttribute("x", String(placement.x - halfW - padX));
411
- plate.setAttribute("y", String(placement.y - 9));
440
+ plate.setAttribute("y", String(placement.y - 10));
412
441
  plate.setAttribute("width", String(textWidth + padX * 2));
413
- plate.setAttribute("height", "18");
442
+ plate.setAttribute("height", "20");
414
443
  plate.setAttribute("rx", "6");
415
444
  plate.setAttribute("fill", theme.labelPlate ?? theme.surface);
416
- plate.setAttribute("fill-opacity", "0.9");
417
- plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 60%, transparent)`);
445
+ plate.setAttribute("fill-opacity", "0.96");
446
+ plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 70%, transparent)`);
418
447
  plate.setAttribute("stroke-width", "1");
419
448
  plate.style.opacity = "0";
449
+ plate.style.filter = "drop-shadow(0 1px 3px rgba(0,0,0,0.12))";
420
450
  group.appendChild(plate);
421
451
  labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
422
452
  labelEl.setAttribute("x", String(placement.x));
423
- labelEl.setAttribute("y", String(placement.y));
453
+ labelEl.setAttribute("y", String(placement.y + 0.5));
424
454
  labelEl.setAttribute("text-anchor", "middle");
425
455
  labelEl.setAttribute("dominant-baseline", "middle");
426
456
  labelEl.setAttribute("font-size", "11");
457
+ labelEl.setAttribute("font-weight", "500");
458
+ labelEl.setAttribute("letter-spacing", "0.02em");
427
459
  labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, monospace");
428
460
  labelEl.setAttribute("fill", theme.text);
429
461
  labelEl.textContent = label;
@@ -492,20 +524,35 @@ function animateEdgeReveal(runtime, startMs, durMs) {
492
524
  path.style.strokeDasharray = String(pathLen);
493
525
  path.style.strokeDashoffset = String(pathLen);
494
526
  }
495
- const drawMs = Math.min(220, durMs * 0.5);
496
- anims.push(group.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 120, delay: startMs, fill: "forwards" }));
527
+ const drawMs = Math.min(260, durMs * 0.65);
528
+ anims.push(
529
+ group.animate(
530
+ [{ opacity: 0 }, { opacity: 1 }],
531
+ { duration: 140, delay: startMs, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
532
+ )
533
+ );
497
534
  if (runtime.drawReveal) {
498
535
  anims.push(
499
536
  path.animate(
500
537
  [{ strokeDashoffset: pathLen }, { strokeDashoffset: 0 }],
501
- { duration: drawMs, delay: startMs, fill: "forwards", easing: "ease-out" }
538
+ { duration: drawMs, delay: startMs, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
502
539
  )
503
540
  );
504
541
  }
505
542
  if (label) {
506
- anims.push(label.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180, delay: startMs + 80, fill: "forwards" }));
543
+ anims.push(
544
+ label.animate(
545
+ [{ opacity: 0 }, { opacity: 1 }],
546
+ { duration: 200, delay: startMs + 90, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
547
+ )
548
+ );
507
549
  if (runtime.labelPlate) {
508
- anims.push(runtime.labelPlate.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180, delay: startMs + 80, fill: "forwards" }));
550
+ anims.push(
551
+ runtime.labelPlate.animate(
552
+ [{ opacity: 0 }, { opacity: 1 }],
553
+ { duration: 200, delay: startMs + 90, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
554
+ )
555
+ );
509
556
  }
510
557
  }
511
558
  if (points.length >= 2) {
@@ -514,7 +561,7 @@ function animateEdgeReveal(runtime, startMs, durMs) {
514
561
  duration: Math.max(drawMs, durMs),
515
562
  delay: startMs,
516
563
  fill: "forwards",
517
- easing: "ease-in-out"
564
+ easing: "cubic-bezier(0.2, 0.85, 0.4, 1)"
518
565
  })
519
566
  );
520
567
  }
@@ -606,14 +653,14 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
606
653
  if (el) {
607
654
  anims.push(
608
655
  el.animate(
609
- [{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "translateY(0)" }],
610
- { duration: durMs, delay, fill: "forwards", easing: "ease-out" }
656
+ [{ opacity: 0, transform: "translateY(10px) scale(0.98)" }, { opacity: 1, transform: "translateY(0) scale(1)" }],
657
+ { duration: durMs, delay, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
611
658
  )
612
659
  );
613
660
  return;
614
661
  }
615
662
  const runtime = edgeRuntimes.get(id);
616
- if (runtime) anims.push(runtime.group.animate([{ opacity: 0 }, { opacity: 1 }], { duration: durMs, delay, fill: "forwards", easing: "ease-out" }));
663
+ if (runtime) anims.push(runtime.group.animate([{ opacity: 0 }, { opacity: 1 }], { duration: durMs, delay, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }));
617
664
  });
618
665
  continue;
619
666
  }
@@ -644,7 +691,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
644
691
  { filter: `drop-shadow(0 0 ${Math.max(4, 4 + strength * 5)}px ${glowColor}) brightness(${peak})` },
645
692
  { filter: "brightness(1)" }
646
693
  ],
647
- { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
694
+ { duration: durMs, delay: startMs, fill: "none", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
648
695
  )
649
696
  );
650
697
  continue;
@@ -666,7 +713,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
666
713
  { transform: `scale(${zoom})`, filter: "drop-shadow(0 0 7px var(--md-accent))" },
667
714
  { transform: "scale(1)", filter: "none" }
668
715
  ],
669
- { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
716
+ { duration: durMs, delay: startMs, fill: "none", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
670
717
  )
671
718
  );
672
719
  continue;
@@ -691,7 +738,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
691
738
  anims.push(
692
739
  scene.animate(
693
740
  [{ transform: cameraTransform }, { transform: nextTransform }],
694
- { duration: durMs, delay: startMs, fill: "forwards", easing: "ease-in-out" }
741
+ { duration: durMs, delay: startMs, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
695
742
  )
696
743
  );
697
744
  cameraTransform = nextTransform;
@@ -718,6 +765,19 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
718
765
  if (edgeId) edgeRuntimes.set(edgeId, runtime);
719
766
  }
720
767
  anims.push(...animateEdgeReveal(runtime, startMs, durMs));
768
+ const toEl = nodeEls.get(seg.to);
769
+ if (toEl) {
770
+ anims.push(
771
+ toEl.animate(
772
+ [
773
+ { transform: "scale(1)", filter: "none" },
774
+ { transform: "scale(1.02)", filter: `drop-shadow(0 0 8px ${translucentColor(runtime.color, "66")})` },
775
+ { transform: "scale(1)", filter: "none" }
776
+ ],
777
+ { duration: 280, delay: startMs + durMs * 0.75, fill: "none", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
778
+ )
779
+ );
780
+ }
721
781
  }
722
782
  }
723
783
  for (const a of anims) a.pause();
@@ -830,6 +890,8 @@ function mountAnnotations(layer, annotations, nodes, theme, bounds) {
830
890
  const pos = positionForAnnotation(ann.position, bounds, index);
831
891
  const textEl = doc.createElement("div");
832
892
  textEl.className = "markdy-annotation";
893
+ textEl.dataset.visible = "1";
894
+ if (ann.intent) textEl.dataset.intent = ann.intent;
833
895
  textEl.textContent = ann.text;
834
896
  textEl.style.left = `${pos.x}px`;
835
897
  textEl.style.top = `${pos.y}px`;
@@ -837,11 +899,13 @@ function mountAnnotations(layer, annotations, nodes, theme, bounds) {
837
899
  const target = ann.target ? nodeById.get(ann.target) : void 0;
838
900
  if (!target) return;
839
901
  const tx = target.x + target.width / 2;
840
- const ty = target.y + target.height / 2;
841
- const ax = pos.x + 8;
842
- const ay = pos.y + 16;
902
+ const ty = target.y - 2;
903
+ const ax = pos.x > tx ? pos.x : pos.x + 160;
904
+ const ay = pos.y + 12;
843
905
  const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
844
- path.setAttribute("d", `M ${ax} ${ay} Q ${(ax + tx) / 2} ${(ay + ty) / 2 - 20} ${tx} ${ty}`);
906
+ const midX = (ax + tx) / 2;
907
+ const midY = Math.min(ay, ty) - 16;
908
+ path.setAttribute("d", `M ${ax} ${ay} Q ${midX} ${midY} ${tx} ${ty}`);
845
909
  path.setAttribute("fill", "none");
846
910
  const intent = typeof ann.intent === "string" ? ann.intent : "neutral";
847
911
  const leaderColor = intent === "accent" ? theme.accent : intent === "muted" ? theme.soft ?? theme.textMuted : theme.textMuted;
@@ -854,8 +918,8 @@ function mountAnnotations(layer, annotations, nodes, theme, bounds) {
854
918
  const dot = doc.createElementNS("http://www.w3.org/2000/svg", "circle");
855
919
  dot.setAttribute("cx", String(tx));
856
920
  dot.setAttribute("cy", String(ty));
857
- dot.setAttribute("r", "2");
858
- dot.setAttribute("fill", theme.text);
921
+ dot.setAttribute("r", "2.5");
922
+ dot.setAttribute("fill", leaderColor);
859
923
  svg.appendChild(dot);
860
924
  });
861
925
  }
@@ -956,6 +1020,141 @@ function mountConstellationLayer(layer, nodes, theme, bounds) {
956
1020
  }
957
1021
  }
958
1022
 
1023
+ // src/radar.ts
1024
+ function mountRadarLayer(layer, nodes, theme, bounds) {
1025
+ if (nodes.length < 3) return;
1026
+ const doc = layer.ownerDocument;
1027
+ Object.assign(layer.style, {
1028
+ position: "absolute",
1029
+ inset: "0",
1030
+ zIndex: "38",
1031
+ pointerEvents: "none"
1032
+ });
1033
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
1034
+ svg.setAttribute("viewBox", `0 0 ${bounds.width} ${bounds.height}`);
1035
+ Object.assign(svg.style, {
1036
+ position: "absolute",
1037
+ inset: "0",
1038
+ width: "100%",
1039
+ height: "100%",
1040
+ overflow: "visible"
1041
+ });
1042
+ layer.appendChild(svg);
1043
+ const centerX = bounds.width / 2;
1044
+ const centerY = (bounds.height + 40) / 2;
1045
+ const nodeCenters = nodes.map((n) => ({
1046
+ x: n.x + n.width / 2,
1047
+ y: n.y + n.height / 2
1048
+ }));
1049
+ const strokeBorder = theme?.border ?? "#cbd5e1";
1050
+ const strokeHairline = theme?.hairline ?? theme?.border ?? "#e2e8f0";
1051
+ const accentColor = theme?.accent ?? "#38bdf8";
1052
+ for (const nc of nodeCenters) {
1053
+ const line = doc.createElementNS("http://www.w3.org/2000/svg", "line");
1054
+ line.setAttribute("x1", String(centerX));
1055
+ line.setAttribute("y1", String(centerY));
1056
+ line.setAttribute("x2", String(nc.x));
1057
+ line.setAttribute("y2", String(nc.y));
1058
+ line.setAttribute("stroke", strokeBorder);
1059
+ line.setAttribute("stroke-width", "1");
1060
+ line.setAttribute("stroke-dasharray", "4 4");
1061
+ line.setAttribute("opacity", "0.45");
1062
+ svg.appendChild(line);
1063
+ }
1064
+ const fractions = [0.33, 0.66, 1];
1065
+ for (const f of fractions) {
1066
+ const points = nodeCenters.map((nc) => {
1067
+ const px = centerX + (nc.x - centerX) * f;
1068
+ const py = centerY + (nc.y - centerY) * f;
1069
+ return `${px.toFixed(1)},${py.toFixed(1)}`;
1070
+ }).join(" ");
1071
+ const poly = doc.createElementNS("http://www.w3.org/2000/svg", "polygon");
1072
+ poly.setAttribute("points", points);
1073
+ poly.setAttribute("fill", "none");
1074
+ poly.setAttribute("stroke", strokeHairline);
1075
+ poly.setAttribute("stroke-width", "1");
1076
+ poly.setAttribute("stroke-dasharray", f === 1 ? "none" : "3 3");
1077
+ poly.setAttribute("opacity", String(0.3 + f * 0.25));
1078
+ svg.appendChild(poly);
1079
+ }
1080
+ const areaPoints = nodeCenters.map((nc, idx) => {
1081
+ const f = 0.75 + idx % 3 * 0.12;
1082
+ const px = centerX + (nc.x - centerX) * f;
1083
+ const py = centerY + (nc.y - centerY) * f;
1084
+ return `${px.toFixed(1)},${py.toFixed(1)}`;
1085
+ }).join(" ");
1086
+ const area = doc.createElementNS("http://www.w3.org/2000/svg", "polygon");
1087
+ area.setAttribute("points", areaPoints);
1088
+ area.setAttribute("fill", accentColor);
1089
+ area.setAttribute("fill-opacity", "0.08");
1090
+ area.setAttribute("stroke", accentColor);
1091
+ area.setAttribute("stroke-width", "1.5");
1092
+ area.setAttribute("stroke-dasharray", "4 4");
1093
+ area.setAttribute("opacity", "0.6");
1094
+ svg.appendChild(area);
1095
+ }
1096
+
1097
+ // src/timeline.ts
1098
+ function mountTimelineLayer(layer, nodes, theme, bounds) {
1099
+ if (nodes.length === 0) return;
1100
+ const doc = layer.ownerDocument;
1101
+ Object.assign(layer.style, {
1102
+ position: "absolute",
1103
+ inset: "0",
1104
+ zIndex: "38",
1105
+ pointerEvents: "none"
1106
+ });
1107
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
1108
+ svg.setAttribute("viewBox", `0 0 ${bounds.width} ${bounds.height}`);
1109
+ Object.assign(svg.style, {
1110
+ position: "absolute",
1111
+ inset: "0",
1112
+ width: "100%",
1113
+ height: "100%",
1114
+ overflow: "visible"
1115
+ });
1116
+ layer.appendChild(svg);
1117
+ const baselineY = (bounds.height + 40) / 2;
1118
+ const minX = Math.min(...nodes.map((n) => n.x)) - 20;
1119
+ const maxX = Math.max(...nodes.map((n) => n.x + n.width)) + 20;
1120
+ const strokeAxis = theme?.hairline ?? theme?.border ?? "#cbd5e1";
1121
+ const strokeBorder = theme?.border ?? "#cbd5e1";
1122
+ const accentColor = theme?.accent ?? "#38bdf8";
1123
+ const surfaceFill = theme?.surfaceRaised ?? theme?.surface ?? "#ffffff";
1124
+ const axis = doc.createElementNS("http://www.w3.org/2000/svg", "line");
1125
+ axis.setAttribute("x1", String(Math.max(40, minX)));
1126
+ axis.setAttribute("y1", String(baselineY));
1127
+ axis.setAttribute("x2", String(Math.min(bounds.width - 40, maxX)));
1128
+ axis.setAttribute("y2", String(baselineY));
1129
+ axis.setAttribute("stroke", strokeAxis);
1130
+ axis.setAttribute("stroke-width", "2");
1131
+ axis.setAttribute("opacity", "0.6");
1132
+ svg.appendChild(axis);
1133
+ for (const node of nodes) {
1134
+ const nodeCenterX = node.x + node.width / 2;
1135
+ const isAbove = node.y + node.height <= baselineY + 10;
1136
+ const targetY = isAbove ? node.y + node.height : node.y;
1137
+ const stem = doc.createElementNS("http://www.w3.org/2000/svg", "line");
1138
+ stem.setAttribute("x1", String(nodeCenterX));
1139
+ stem.setAttribute("y1", String(baselineY));
1140
+ stem.setAttribute("x2", String(nodeCenterX));
1141
+ stem.setAttribute("y2", String(targetY));
1142
+ stem.setAttribute("stroke", node.focal ? accentColor : strokeBorder);
1143
+ stem.setAttribute("stroke-width", node.focal ? "1.5" : "1");
1144
+ stem.setAttribute("stroke-dasharray", node.focal ? "none" : "3 3");
1145
+ stem.setAttribute("opacity", node.focal ? "0.9" : "0.5");
1146
+ svg.appendChild(stem);
1147
+ const pip = doc.createElementNS("http://www.w3.org/2000/svg", "circle");
1148
+ pip.setAttribute("cx", String(nodeCenterX));
1149
+ pip.setAttribute("cy", String(baselineY));
1150
+ pip.setAttribute("r", node.focal ? "5" : "3.5");
1151
+ pip.setAttribute("fill", node.focal ? accentColor : surfaceFill);
1152
+ pip.setAttribute("stroke", node.focal ? accentColor : strokeBorder);
1153
+ pip.setAttribute("stroke-width", "1.5");
1154
+ svg.appendChild(pip);
1155
+ }
1156
+ }
1157
+
959
1158
  // src/groups.ts
960
1159
  var STYLE_ID2 = "markdy-group-boundary-styles";
961
1160
  function ensureGroupStyles(doc) {
@@ -966,26 +1165,27 @@ function ensureGroupStyles(doc) {
966
1165
  .markdy-group-boundary {
967
1166
  position: absolute;
968
1167
  box-sizing: border-box;
969
- border: 1px dashed var(--md-group-border, color-mix(in srgb, var(--md-border) 70%, transparent));
970
- border-radius: var(--md-radius-md, 8px);
971
- background: color-mix(in srgb, var(--md-surface-raised) 40%, transparent);
1168
+ border: 1px dashed var(--md-group-border, color-mix(in srgb, var(--md-border) 60%, transparent));
1169
+ border-radius: 14px;
1170
+ background: color-mix(in srgb, var(--md-surface-raised) 25%, transparent);
972
1171
  pointer-events: none;
973
1172
  z-index: 40;
974
1173
  }
975
1174
  .markdy-group-boundary__label {
976
1175
  position: absolute;
977
- left: 12px;
978
- top: -10px;
979
- padding: 2px 8px;
1176
+ left: 14px;
1177
+ top: 10px;
1178
+ padding: 3px 8px;
980
1179
  font-size: 10px;
981
1180
  font-weight: 600;
982
1181
  letter-spacing: 0.08em;
983
1182
  text-transform: uppercase;
984
1183
  color: var(--md-text-muted);
985
- background: var(--md-canvas);
986
- border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 60%, transparent));
987
- border-radius: 4px;
988
- font-family: var(--md-font-mono, ui-monospace, monospace);
1184
+ background: color-mix(in srgb, var(--md-surface-raised) 80%, transparent);
1185
+ border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 50%, transparent));
1186
+ border-radius: 5px;
1187
+ font-family: var(--md-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1188
+ backdrop-filter: blur(4px);
989
1189
  }
990
1190
  `;
991
1191
  doc.head.appendChild(style);
@@ -1024,20 +1224,20 @@ function ensureNodeStyles(doc) {
1024
1224
  .markdy-node {
1025
1225
  position: absolute;
1026
1226
  box-sizing: border-box;
1027
- width: var(--md-node-w, 184px);
1028
- height: var(--md-node-h, 88px);
1227
+ width: var(--md-node-w, 180px);
1228
+ height: var(--md-node-h, 76px);
1029
1229
  border-radius: 12px;
1030
1230
  background:
1031
1231
  linear-gradient(180deg,
1032
- var(--md-node-surface-raised, color-mix(in srgb, var(--md-surface-raised) 88%, #ffffff 12%)),
1232
+ var(--md-node-surface-raised, color-mix(in srgb, var(--md-surface-raised) 92%, #ffffff 8%)),
1033
1233
  var(--md-node-surface, var(--md-surface)));
1034
1234
  color: var(--md-text);
1035
1235
  box-shadow:
1036
- 0 1px 1px color-mix(in srgb, var(--md-shadow, rgba(2, 6, 23, 0.5)) 50%, transparent),
1037
- 0 10px 22px -12px var(--md-shadow, rgba(2, 6, 23, 0.55)),
1038
- inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent)),
1039
- inset 0 1px 0 rgba(255, 255, 255, 0.05);
1040
- font-family: var(--md-font-node, Inter, ui-sans-serif, system-ui, sans-serif);
1236
+ 0 1px 2px color-mix(in srgb, var(--md-shadow, rgba(2, 6, 23, 0.4)) 40%, transparent),
1237
+ 0 8px 20px -8px var(--md-shadow, rgba(2, 6, 23, 0.45)),
1238
+ inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 45%, transparent)),
1239
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
1240
+ font-family: var(--md-font-node, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif);
1041
1241
  overflow: hidden;
1042
1242
  opacity: 0;
1043
1243
  transform: translateY(8px);
@@ -1049,23 +1249,23 @@ function ensureNodeStyles(doc) {
1049
1249
  }
1050
1250
  .markdy-node[data-focused="1"] {
1051
1251
  box-shadow:
1052
- 0 2px 4px rgba(2, 6, 23, 0.32),
1053
- 0 16px 34px -14px rgba(2, 6, 23, 0.6),
1054
- inset 0 0 0 1px color-mix(in srgb, var(--md-accent) 65%, transparent),
1055
- 0 0 0 3px color-mix(in srgb, var(--md-accent) 20%, transparent);
1252
+ 0 2px 6px rgba(2, 6, 23, 0.35),
1253
+ 0 16px 36px -12px rgba(2, 6, 23, 0.65),
1254
+ inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent) 75%, transparent),
1255
+ 0 0 0 3px color-mix(in srgb, var(--md-accent) 22%, transparent);
1056
1256
  }
1057
1257
  .markdy-node[data-glow="1"] {
1058
1258
  box-shadow:
1059
- 0 2px 4px rgba(2, 6, 23, 0.32),
1060
- 0 0 0 1px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 55%, transparent),
1061
- 0 0 20px -2px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 42%, transparent),
1259
+ 0 2px 6px rgba(2, 6, 23, 0.35),
1260
+ 0 0 0 1.5px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 65%, transparent),
1261
+ 0 0 24px -2px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 48%, transparent),
1062
1262
  inset 0 0 18px -8px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 45%, transparent);
1063
1263
  }
1064
1264
  .markdy-node__rail { display: none; }
1065
1265
  .markdy-node__type { display: none; }
1066
1266
  .markdy-node__body {
1067
1267
  height: 100%;
1068
- padding: 0 13px;
1268
+ padding: 0 14px;
1069
1269
  display: flex;
1070
1270
  align-items: center;
1071
1271
  gap: 10px;
@@ -1073,20 +1273,20 @@ function ensureNodeStyles(doc) {
1073
1273
  }
1074
1274
  .markdy-node__icon {
1075
1275
  flex: 0 0 auto;
1076
- width: 30px;
1077
- height: 30px;
1078
- border-radius: 8px;
1276
+ width: 32px;
1277
+ height: 32px;
1278
+ border-radius: 9px;
1079
1279
  display: flex;
1080
1280
  align-items: center;
1081
1281
  justify-content: center;
1082
1282
  color: var(--md-role-color, var(--md-accent));
1083
1283
  background:
1084
1284
  linear-gradient(180deg,
1085
- color-mix(in srgb, var(--md-role-color, var(--md-accent)) 24%, transparent),
1086
- color-mix(in srgb, var(--md-role-color, var(--md-accent)) 11%, transparent));
1285
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 22%, transparent),
1286
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 10%, transparent));
1087
1287
  box-shadow:
1088
- inset 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 34%, transparent),
1089
- inset 0 1px 0 rgba(255, 255, 255, 0.12);
1288
+ inset 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 35%, transparent),
1289
+ inset 0 1px 0 rgba(255, 255, 255, 0.15);
1090
1290
  }
1091
1291
  .markdy-node__icon svg {
1092
1292
  width: 17px;
@@ -1118,10 +1318,10 @@ function ensureNodeStyles(doc) {
1118
1318
  flex: 1 1 auto;
1119
1319
  min-width: 0;
1120
1320
  padding: 0;
1121
- font-size: 14px;
1321
+ font-size: 13.5px;
1122
1322
  font-weight: 600;
1123
1323
  letter-spacing: -0.01em;
1124
- line-height: 1.18;
1324
+ line-height: 1.22;
1125
1325
  display: -webkit-box;
1126
1326
  -webkit-box-orient: vertical;
1127
1327
  -webkit-line-clamp: 3;
@@ -1129,30 +1329,30 @@ function ensureNodeStyles(doc) {
1129
1329
  overflow: hidden;
1130
1330
  overflow-wrap: anywhere;
1131
1331
  word-break: break-word;
1132
- text-wrap: balance;
1332
+ text-wrap: pretty;
1133
1333
  }
1134
1334
  .markdy-node__value {
1135
1335
  flex: 0 0 auto;
1136
- font-size: 18px;
1336
+ font-size: 17px;
1137
1337
  font-weight: 700;
1138
1338
  color: var(--md-ink, var(--md-text));
1139
1339
  font-variant-numeric: tabular-nums;
1140
1340
  }
1141
- .markdy-node[data-role="client"] { border-radius: 15px 15px 9px 9px; }
1142
- .markdy-node[data-role="data"] { border-radius: 12px 12px 20px 20px; }
1341
+ .markdy-node[data-role="client"] { border-radius: 14px 14px 10px 10px; }
1342
+ .markdy-node[data-role="data"] { border-radius: 12px 12px 18px 18px; }
1143
1343
  .markdy-scene-title {
1144
1344
  position: absolute;
1145
- left: 44px;
1146
- top: 26px;
1147
- right: 44px;
1345
+ left: 48px;
1346
+ top: 28px;
1347
+ right: 48px;
1148
1348
  z-index: 130;
1149
- font-size: 32px;
1349
+ font-size: 30px;
1150
1350
  font-weight: 700;
1151
1351
  line-height: 1.2;
1152
1352
  color: var(--md-text);
1153
1353
  opacity: 0;
1154
1354
  transform: translateY(-6px);
1155
- font-family: var(--md-font-title, Inter, ui-sans-serif, system-ui, sans-serif);
1355
+ font-family: var(--md-font-title, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif);
1156
1356
  }
1157
1357
  .markdy-scene-title[data-visible="1"] {
1158
1358
  opacity: 1;
@@ -1165,12 +1365,35 @@ function ensureNodeStyles(doc) {
1165
1365
  }
1166
1366
  .markdy-node[data-shape="pill"] {
1167
1367
  border-radius: 999px;
1168
- min-height: 56px;
1368
+ min-height: 54px;
1169
1369
  }
1170
1370
  .markdy-node[data-shape="circle"],
1171
1371
  .markdy-node[data-kind="dot"] {
1172
1372
  border-radius: 50%;
1173
1373
  }
1374
+ .markdy-node[data-shape="circle"] {
1375
+ display: flex;
1376
+ align-items: center;
1377
+ justify-content: center;
1378
+ text-align: center;
1379
+ background:
1380
+ radial-gradient(circle at 35% 35%,
1381
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 16%, var(--md-surface) 84%),
1382
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 6%, var(--md-surface) 94%));
1383
+ border: 1.5px solid color-mix(in srgb, var(--md-role-color, var(--md-accent)) 50%, transparent);
1384
+ }
1385
+ .markdy-node[data-shape="circle"] .markdy-node__body {
1386
+ flex-direction: column;
1387
+ justify-content: center;
1388
+ padding: 16px;
1389
+ gap: 8px;
1390
+ text-align: center;
1391
+ }
1392
+ .markdy-node[data-shape="circle"] .markdy-node__label {
1393
+ text-align: center;
1394
+ -webkit-line-clamp: 4;
1395
+ line-clamp: 4;
1396
+ }
1174
1397
  .markdy-node[data-kind="dot"] {
1175
1398
  width: 64px;
1176
1399
  height: 64px;
@@ -1188,11 +1411,24 @@ function ensureNodeStyles(doc) {
1188
1411
  .markdy-node[data-kind="token_strip"] {
1189
1412
  border-radius: 999px;
1190
1413
  }
1414
+ .markdy-node[data-is-container="1"] {
1415
+ background: color-mix(in srgb, var(--md-surface-raised) 25%, transparent);
1416
+ border: 1.5px dashed color-mix(in srgb, var(--md-role-color, var(--md-accent)) 45%, var(--md-border) 55%);
1417
+ box-shadow: inset 0 0 0 1px var(--md-hairline);
1418
+ }
1419
+ .markdy-node[data-is-container="1"] .markdy-node__body {
1420
+ align-items: flex-start;
1421
+ padding: 12px 16px;
1422
+ }
1423
+ .markdy-node[data-is-container="1"][data-focal="1"] {
1424
+ background: color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 14%, transparent);
1425
+ border: 1.5px solid color-mix(in srgb, var(--md-accent) 65%, transparent);
1426
+ }
1191
1427
  .markdy-node[data-shape="rounded"] {
1192
1428
  border-radius: 16px;
1193
1429
  }
1194
1430
  .markdy-node[data-shape="terminal"] {
1195
- border-radius: 6px;
1431
+ border-radius: 8px;
1196
1432
  font-family: var(--md-font-mono, ui-monospace, monospace);
1197
1433
  box-shadow: none;
1198
1434
  background: var(--md-node-surface, var(--md-surface));
@@ -1584,6 +1820,7 @@ function createNodeEl(node, theme, assets) {
1584
1820
  el.dataset.icon = iconKeyForNode(node);
1585
1821
  if (node.shape) el.dataset.shape = node.shape;
1586
1822
  if (node.focal) el.dataset.focal = "1";
1823
+ if (node.shape === "container") el.dataset.isContainer = "1";
1587
1824
  el.title = `${node.label} (${typeText})`;
1588
1825
  el.setAttribute("aria-label", el.title);
1589
1826
  const body = document.createElement("div");
@@ -1690,25 +1927,26 @@ function mountSequenceLayer(layer, nodes, messages, activations, theme, bounds)
1690
1927
  lifeline.classList.add("markdy-sequence-lifeline");
1691
1928
  lifeline.setAttribute("x1", String(x));
1692
1929
  lifeline.setAttribute("x2", String(x));
1693
- lifeline.setAttribute("y1", String(node.y + node.height + 12));
1694
- lifeline.setAttribute("y2", String(bounds.height - 28));
1695
- lifeline.setAttribute("stroke", theme.rule ?? theme.soft ?? theme.border);
1696
- lifeline.setAttribute("stroke-width", "1");
1697
- lifeline.setAttribute("stroke-dasharray", "4 5");
1698
- lifeline.setAttribute("opacity", "0.75");
1930
+ lifeline.setAttribute("y1", String(node.y + node.height + 8));
1931
+ lifeline.setAttribute("y2", String(bounds.height - 32));
1932
+ lifeline.setAttribute("stroke", theme.hairline ?? theme.border);
1933
+ lifeline.setAttribute("stroke-width", "1.2");
1934
+ lifeline.setAttribute("stroke-dasharray", "4 6");
1935
+ lifeline.setAttribute("opacity", "0.85");
1699
1936
  svg.appendChild(lifeline);
1700
1937
  }
1701
1938
  for (const activation of activations) {
1702
1939
  const x = centerX(activation.participant);
1703
1940
  const bar = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
1704
1941
  bar.classList.add("markdy-sequence-activation");
1705
- bar.setAttribute("x", String(x - 5));
1942
+ bar.setAttribute("x", String(x - 6));
1706
1943
  bar.setAttribute("y", String(activation.y));
1707
- bar.setAttribute("width", "10");
1944
+ bar.setAttribute("width", "12");
1708
1945
  bar.setAttribute("height", String(activation.height));
1709
- bar.setAttribute("rx", "3");
1946
+ bar.setAttribute("rx", "4");
1710
1947
  bar.setAttribute("fill", theme.accent);
1711
1948
  bar.setAttribute("opacity", "0");
1949
+ bar.style.filter = `drop-shadow(0 0 6px ${theme.accent}66)`;
1712
1950
  svg.appendChild(bar);
1713
1951
  }
1714
1952
  const animations = [];
@@ -1737,17 +1975,19 @@ function mountSequenceLayer(layer, nodes, messages, activations, theme, bounds)
1737
1975
  if (message.label) {
1738
1976
  const midX = (fromX + toX) / 2;
1739
1977
  const plate = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
1740
- const width = message.label.length * 6.6 + 16;
1978
+ const width = message.label.length * 6.8 + 16;
1741
1979
  plate.setAttribute("x", String(midX - width / 2));
1742
- plate.setAttribute("y", String(message.y - 24));
1980
+ plate.setAttribute("y", String(message.y - 25));
1743
1981
  plate.setAttribute("width", String(width));
1744
- plate.setAttribute("height", "18");
1745
- plate.setAttribute("rx", "5");
1982
+ plate.setAttribute("height", "20");
1983
+ plate.setAttribute("rx", "6");
1746
1984
  plate.setAttribute("fill", theme.labelPlate ?? theme.surface);
1747
- plate.setAttribute("stroke", theme.hairline ?? theme.border);
1985
+ plate.setAttribute("fill-opacity", "0.96");
1986
+ plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 70%, transparent)`);
1748
1987
  plate.setAttribute("stroke-width", "1");
1988
+ plate.style.filter = "drop-shadow(0 1px 3px rgba(0,0,0,0.12))";
1749
1989
  group.appendChild(plate);
1750
- group.appendChild(createText(doc, midX, message.y - 15, message.label, theme));
1990
+ group.appendChild(createText(doc, midX, message.y - 14.5, message.label, theme));
1751
1991
  }
1752
1992
  svg.appendChild(group);
1753
1993
  animations.push(
@@ -1805,25 +2045,29 @@ function mountTreeBuses(layer, buses, theme) {
1805
2045
  for (const bus of buses) {
1806
2046
  const group = doc.createElementNS("http://www.w3.org/2000/svg", "g");
1807
2047
  group.setAttribute("data-tree-bus", bus.id);
1808
- const parentLeg = doc.createElementNS("http://www.w3.org/2000/svg", "path");
1809
- parentLeg.setAttribute("d", `M ${bus.parentX} ${bus.parentY} L ${bus.parentX} ${bus.branchY}`);
1810
- const childXs = [...bus.childXs].sort((a, b) => a - b);
1811
- const branch = doc.createElementNS("http://www.w3.org/2000/svg", "path");
1812
- const branchStart = childXs[0] ?? bus.parentX;
1813
- const branchEnd = childXs[childXs.length - 1] ?? bus.parentX;
1814
- branch.setAttribute("d", `M ${branchStart} ${bus.branchY} L ${branchEnd} ${bus.branchY}`);
1815
- group.append(parentLeg, branch);
1816
2048
  for (const childX of bus.childXs) {
1817
- const leg = doc.createElementNS("http://www.w3.org/2000/svg", "path");
1818
- leg.setAttribute("d", `M ${childX} ${bus.branchY} L ${childX} ${bus.childY}`);
1819
- group.appendChild(leg);
1820
- }
1821
- for (const path of Array.from(group.querySelectorAll("path"))) {
1822
- path.setAttribute("fill", "none");
1823
- path.setAttribute("stroke", stroke);
1824
- path.setAttribute("stroke-width", "1.5");
1825
- path.setAttribute("stroke-linecap", "round");
1826
- path.setAttribute("stroke-linejoin", "round");
2049
+ const pathEl = doc.createElementNS("http://www.w3.org/2000/svg", "path");
2050
+ let d;
2051
+ if (Math.abs(childX - bus.parentX) < 1) {
2052
+ d = toPathD([
2053
+ { x: bus.parentX, y: bus.parentY },
2054
+ { x: childX, y: bus.childY }
2055
+ ], 12);
2056
+ } else {
2057
+ d = toPathD([
2058
+ { x: bus.parentX, y: bus.parentY },
2059
+ { x: bus.parentX, y: bus.branchY },
2060
+ { x: childX, y: bus.branchY },
2061
+ { x: childX, y: bus.childY }
2062
+ ], 12);
2063
+ }
2064
+ pathEl.setAttribute("d", d);
2065
+ pathEl.setAttribute("fill", "none");
2066
+ pathEl.setAttribute("stroke", stroke);
2067
+ pathEl.setAttribute("stroke-width", "1.6");
2068
+ pathEl.setAttribute("stroke-linecap", "round");
2069
+ pathEl.setAttribute("stroke-linejoin", "round");
2070
+ group.appendChild(pathEl);
1827
2071
  }
1828
2072
  svg.appendChild(group);
1829
2073
  }
@@ -2250,6 +2494,20 @@ function createDiagram(opts) {
2250
2494
  plan.theme,
2251
2495
  { width: plan.meta.width, height: plan.meta.height }
2252
2496
  );
2497
+ } else if (plan.diagramType === "radar") {
2498
+ mountRadarLayer(
2499
+ constellationLayer,
2500
+ plan.nodes,
2501
+ plan.theme,
2502
+ { width: plan.meta.width, height: plan.meta.height }
2503
+ );
2504
+ } else if (plan.diagramType === "timeline") {
2505
+ mountTimelineLayer(
2506
+ constellationLayer,
2507
+ plan.nodes,
2508
+ plan.theme,
2509
+ { width: plan.meta.width, height: plan.meta.height }
2510
+ );
2253
2511
  }
2254
2512
  const groupLayer = document.createElement("div");
2255
2513
  groupLayer.className = "markdy-group-layer";
@@ -2336,16 +2594,8 @@ function createDiagram(opts) {
2336
2594
  const vHeight = viewport.clientHeight || container.clientHeight || vWidth * plan.meta.height / plan.meta.width;
2337
2595
  const canvasScaleX = vWidth / plan.meta.width;
2338
2596
  const canvasScaleY = vHeight / plan.meta.height;
2339
- const baseCanvasScale = Math.min(canvasScaleX, canvasScaleY);
2340
- const bounds = computeContentBounds();
2341
- const contentScaleX = vWidth * 0.94 / bounds.width;
2342
- const contentScaleY = vHeight * 0.94 / bounds.height;
2343
- const optimalContentScale = Math.min(contentScaleX, contentScaleY);
2344
- const chosenScale = Math.max(
2345
- baseCanvasScale,
2346
- Math.min(optimalContentScale, baseCanvasScale * 1.45)
2347
- );
2348
- fitScale = Number.isFinite(chosenScale) && chosenScale > 0 ? chosenScale : 1;
2597
+ fitScale = Math.min(canvasScaleX, canvasScaleY);
2598
+ if (!Number.isFinite(fitScale) || fitScale <= 0) fitScale = 1;
2349
2599
  const scaledWidth = plan.meta.width * fitScale;
2350
2600
  const scaledHeight = plan.meta.height * fitScale;
2351
2601
  sceneOffsetX = (vWidth - scaledWidth) / 2;
@@ -2866,9 +3116,6 @@ function encodeGifSequence(frames, options = {}) {
2866
3116
  return new Uint8Array(buffer);
2867
3117
  }
2868
3118
 
2869
- // src/export/png-exporter.ts
2870
- import html2canvas from "html2canvas";
2871
-
2872
3119
  // src/export/svg-exporter.ts
2873
3120
  function copyRenderedStyles(source, clone) {
2874
3121
  if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
@@ -3118,6 +3365,8 @@ async function rasterizeDiagramToCanvas(containerEl, options = {}, pixelRatio =
3118
3365
  document.body.appendChild(host);
3119
3366
  try {
3120
3367
  await document.fonts?.ready;
3368
+ const html2canvasModule = await import("html2canvas");
3369
+ const html2canvas = html2canvasModule.default ?? html2canvasModule;
3121
3370
  return await html2canvas(clonedScene, {
3122
3371
  allowTaint: false,
3123
3372
  backgroundColor: options.transparentBackground ? null : void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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.8"
48
+ "@markdy/core": "1.0.9"
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.8"
55
+ "@markdy/stdlib-systems": "1.0.9"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",