@markdy/renderer-dom 1.0.9 → 1.0.10

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +475 -181
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -45,7 +45,7 @@ import { createDiagram } from "@markdy/renderer-dom";
45
45
  const diagram = createDiagram({
46
46
  container: document.getElementById("scene")!,
47
47
  code: `
48
- scene "Request" theme=paper
48
+ scene theme=paper
49
49
  browser Web
50
50
  service API
51
51
  beat main:
package/dist/index.js CHANGED
@@ -35,6 +35,9 @@ function segmentIntersectsRect(a, b, rect) {
35
35
  }
36
36
  return false;
37
37
  }
38
+ function rectsOverlap(a, b) {
39
+ return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
40
+ }
38
41
  function countPathIntersections(points, obstacles) {
39
42
  let hits = 0;
40
43
  for (let i = 0; i < points.length - 1; i++) {
@@ -49,22 +52,108 @@ function countPathIntersections(points, obstacles) {
49
52
  function round1(n) {
50
53
  return Math.round(n * 10) / 10;
51
54
  }
52
- function toPathD(points, cornerRadius = 14) {
55
+ function findSegmentHops(p1, p2, existingPaths = [], hopRadius = 5, minDistFromVertex = 10) {
56
+ const isHoriz = Math.abs(p1.y - p2.y) < 0.01;
57
+ const isVert = Math.abs(p1.x - p2.x) < 0.01;
58
+ if (!isHoriz && !isVert || existingPaths.length === 0) return [];
59
+ const crossings = [];
60
+ const minX = Math.min(p1.x, p2.x);
61
+ const maxX = Math.max(p1.x, p2.x);
62
+ const minY = Math.min(p1.y, p2.y);
63
+ const maxY = Math.max(p1.y, p2.y);
64
+ for (const path of existingPaths) {
65
+ for (let j = 0; j < path.length - 1; j++) {
66
+ const q1 = path[j];
67
+ const q2 = path[j + 1];
68
+ const qHoriz = Math.abs(q1.y - q2.y) < 0.01;
69
+ const qVert = Math.abs(q1.x - q2.x) < 0.01;
70
+ if (isHoriz && qVert) {
71
+ const crossX = q1.x;
72
+ const crossY = p1.y;
73
+ const qMinY = Math.min(q1.y, q2.y);
74
+ const qMaxY = Math.max(q1.y, q2.y);
75
+ if (crossX > minX + minDistFromVertex && crossX < maxX - minDistFromVertex && crossY > qMinY + 6 && crossY < qMaxY - 6) {
76
+ crossings.push({ x: crossX, y: crossY });
77
+ }
78
+ } else if (isVert && qHoriz) {
79
+ const crossX = p1.x;
80
+ const crossY = q1.y;
81
+ const qMinX = Math.min(q1.x, q2.x);
82
+ const qMaxX = Math.max(q1.x, q2.x);
83
+ if (crossX > qMinX + 6 && crossX < qMaxX - 6 && crossY > minY + minDistFromVertex && crossY < maxY - minDistFromVertex) {
84
+ crossings.push({ x: crossX, y: crossY });
85
+ }
86
+ }
87
+ }
88
+ }
89
+ crossings.sort((a, b) => {
90
+ const da = Math.hypot(a.x - p1.x, a.y - p1.y);
91
+ const db = Math.hypot(b.x - p1.x, b.y - p1.y);
92
+ return da - db;
93
+ });
94
+ return crossings;
95
+ }
96
+ function appendSegmentWithHops(parts, start, end, existingPaths = [], hopRadius = 5) {
97
+ const isHoriz = Math.abs(start.y - end.y) < 0.01;
98
+ const isVert = Math.abs(start.x - end.x) < 0.01;
99
+ if (!isHoriz && !isVert || existingPaths.length === 0) {
100
+ parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
101
+ return;
102
+ }
103
+ const hops = findSegmentHops(start, end, existingPaths, hopRadius);
104
+ if (hops.length === 0) {
105
+ parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
106
+ return;
107
+ }
108
+ const R = hopRadius;
109
+ if (isHoriz) {
110
+ const dx = end.x - start.x;
111
+ for (const h of hops) {
112
+ if (dx > 0) {
113
+ parts.push(`L ${round1(h.x - R)} ${round1(start.y)}`);
114
+ parts.push(`A ${R} ${R} 0 0 0 ${round1(h.x + R)} ${round1(start.y)}`);
115
+ } else {
116
+ parts.push(`L ${round1(h.x + R)} ${round1(start.y)}`);
117
+ parts.push(`A ${R} ${R} 0 0 1 ${round1(h.x - R)} ${round1(start.y)}`);
118
+ }
119
+ }
120
+ parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
121
+ } else if (isVert) {
122
+ const dy = end.y - start.y;
123
+ for (const h of hops) {
124
+ if (dy > 0) {
125
+ parts.push(`L ${round1(start.x)} ${round1(h.y - R)}`);
126
+ parts.push(`A ${R} ${R} 0 0 1 ${round1(start.x)} ${round1(h.y + R)}`);
127
+ } else {
128
+ parts.push(`L ${round1(start.x)} ${round1(h.y + R)}`);
129
+ parts.push(`A ${R} ${R} 0 0 0 ${round1(start.x)} ${round1(h.y - R)}`);
130
+ }
131
+ }
132
+ parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
133
+ }
134
+ }
135
+ function toPathD(points, cornerRadius = 14, existingPaths = []) {
53
136
  if (points.length < 2) return "";
54
137
  if (points.length === 2) {
55
- const [a, b] = points;
56
- return `M ${round1(a.x)} ${round1(a.y)} L ${round1(b.x)} ${round1(b.y)}`;
138
+ const parts2 = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
139
+ appendSegmentWithHops(parts2, points[0], points[1], existingPaths);
140
+ return parts2.join(" ");
57
141
  }
58
142
  if (cornerRadius <= 0) {
59
- return points.map((p, i) => i === 0 ? `M ${round1(p.x)} ${round1(p.y)}` : `L ${round1(p.x)} ${round1(p.y)}`).join(" ");
143
+ const parts2 = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
144
+ for (let i = 1; i < points.length; i++) {
145
+ appendSegmentWithHops(parts2, points[i - 1], points[i], existingPaths);
146
+ }
147
+ return parts2.join(" ");
60
148
  }
61
149
  const parts = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
150
+ let prevCornerEnd = points[0];
62
151
  for (let i = 1; i < points.length; i++) {
63
152
  const prev = points[i - 1];
64
153
  const cur = points[i];
65
154
  const next = points[i + 1];
66
155
  if (!next) {
67
- parts.push(`L ${round1(cur.x)} ${round1(cur.y)}`);
156
+ appendSegmentWithHops(parts, prevCornerEnd, cur, existingPaths);
68
157
  continue;
69
158
  }
70
159
  const dx1 = cur.x - prev.x;
@@ -74,7 +163,8 @@ function toPathD(points, cornerRadius = 14) {
74
163
  const len1 = Math.hypot(dx1, dy1);
75
164
  const len2 = Math.hypot(dx2, dy2);
76
165
  if (len1 < 0.5 || len2 < 0.5) {
77
- parts.push(`L ${round1(cur.x)} ${round1(cur.y)}`);
166
+ appendSegmentWithHops(parts, prevCornerEnd, cur, existingPaths);
167
+ prevCornerEnd = cur;
78
168
  continue;
79
169
  }
80
170
  const r = Math.min(cornerRadius, len1 / 2, len2 / 2);
@@ -82,8 +172,9 @@ function toPathD(points, cornerRadius = 14) {
82
172
  const by = cur.y - dy1 / len1 * r;
83
173
  const ax = cur.x + dx2 / len2 * r;
84
174
  const ay = cur.y + dy2 / len2 * r;
85
- parts.push(`L ${round1(bx)} ${round1(by)}`);
175
+ appendSegmentWithHops(parts, prevCornerEnd, { x: bx, y: by }, existingPaths);
86
176
  parts.push(`Q ${round1(cur.x)} ${round1(cur.y)} ${round1(ax)} ${round1(ay)}`);
177
+ prevCornerEnd = { x: ax, y: ay };
87
178
  }
88
179
  return parts.join(" ");
89
180
  }
@@ -109,56 +200,49 @@ function polylineLength(points) {
109
200
  function segmentLength(a, b) {
110
201
  return Math.hypot(b.x - a.x, b.y - a.y);
111
202
  }
112
- var LABEL_BOX_HEIGHT = 20;
113
- function rectsOverlap(a, b) {
114
- return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
115
- }
203
+ var LABEL_BOX_HEIGHT = 18;
116
204
  function overlapCount(rect, obstacles) {
117
205
  let hits = 0;
118
206
  for (const o of obstacles) if (rectsOverlap(rect, o)) hits++;
119
207
  return hits;
120
208
  }
121
209
  function placeFlowLabel(points, textWidth, obstacles, bounds) {
122
- let bestIndex = 0;
123
- let bestLength = -1;
210
+ const halfW = textWidth / 2 + 5;
211
+ const halfH = LABEL_BOX_HEIGHT / 2 + 1;
212
+ const pad = 12;
213
+ let bestPlacement = null;
214
+ let bestScore = Number.POSITIVE_INFINITY;
215
+ const stepX = Math.max(18, halfW + 6);
216
+ const stepY = Math.max(18, halfH + 6);
124
217
  for (let i = 0; i < points.length - 1; i++) {
125
- const len = segmentLength(points[i], points[i + 1]);
126
- if (len > bestLength) {
127
- bestLength = len;
128
- bestIndex = i;
129
- }
130
- }
131
- const a = points[bestIndex] ?? { x: 0, y: 0 };
132
- const b = points[bestIndex + 1] ?? a;
133
- const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
134
- const horizontal = Math.abs(a.x - b.x) >= Math.abs(a.y - b.y);
135
- const half = textWidth / 2;
136
- const halfH = LABEL_BOX_HEIGHT / 2;
137
- const pad = 10;
138
- const base = horizontal ? 16 : half + 14;
139
- const step = horizontal ? LABEL_BOX_HEIGHT + 6 : textWidth + 14;
140
- const order = horizontal ? [-1, 1] : [1, -1];
141
- const offsets = [];
142
- for (let k = 0; k < 8; k++) {
143
- for (const sign of order) offsets.push(sign * (base + k * step));
144
- }
145
- let fallback = null;
146
- let fallbackHits = Number.POSITIVE_INFINITY;
147
- for (const off of offsets) {
148
- const cx = clamp(horizontal ? mid.x : mid.x + off, pad + half, bounds.width - pad - half);
149
- const cy = clamp(horizontal ? mid.y + off : mid.y, pad + halfH, bounds.height - pad - halfH);
150
- const rect = { x1: cx - half, y1: cy - halfH, x2: cx + half, y2: cy + halfH };
151
- const hits = overlapCount(rect, obstacles);
152
- if (hits === 0) return { x: round1(cx), y: round1(cy), rect };
153
- if (hits < fallbackHits) {
154
- fallbackHits = hits;
155
- fallback = { x: round1(cx), y: round1(cy), rect };
156
- }
157
- }
158
- return fallback ?? {
159
- x: round1(mid.x),
160
- y: round1(mid.y),
161
- rect: { x1: mid.x - half, y1: mid.y - halfH, x2: mid.x + half, y2: mid.y + halfH }
218
+ const a = points[i];
219
+ const b = points[i + 1];
220
+ const segLen = segmentLength(a, b);
221
+ if (segLen < 1) continue;
222
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
223
+ const horizontal = Math.abs(a.x - b.x) >= Math.abs(a.y - b.y);
224
+ const offsets = [0];
225
+ for (let k = 1; k <= 8; k++) {
226
+ offsets.push(-k * (horizontal ? stepX : stepY), k * (horizontal ? stepX : stepY));
227
+ }
228
+ for (const off of offsets) {
229
+ const cx = clamp(horizontal ? mid.x + off : mid.x, pad + halfW, bounds.width - pad - halfW);
230
+ const cy = clamp(horizontal ? mid.y : mid.y + off, pad + halfH, bounds.height - pad - halfH);
231
+ const rect = { x1: cx - halfW, y1: cy - halfH, x2: cx + halfW, y2: cy + halfH };
232
+ const hits = overlapCount(rect, obstacles);
233
+ const score = hits * 1e5 + (horizontal ? 0 : 40) + Math.abs(off) * 2 - Math.min(100, segLen) * 0.1;
234
+ if (score < bestScore) {
235
+ bestScore = score;
236
+ bestPlacement = { x: round1(cx), y: round1(cy), rect };
237
+ }
238
+ }
239
+ }
240
+ if (bestPlacement) return bestPlacement;
241
+ const firstMid = points.length >= 2 ? { x: (points[0].x + points[1].x) / 2, y: (points[0].y + points[1].y) / 2 } : { x: 0, y: 0 };
242
+ return {
243
+ x: round1(firstMid.x),
244
+ y: round1(firstMid.y),
245
+ rect: { x1: firstMid.x - halfW, y1: firstMid.y - halfH, x2: firstMid.x + halfW, y2: firstMid.y + halfH }
162
246
  };
163
247
  }
164
248
  function clamp(n, min, max) {
@@ -173,9 +257,12 @@ function clampPointToScene(point, bounds) {
173
257
  };
174
258
  }
175
259
  function laneOffset(lane) {
176
- if (lane <= 0) return 0;
177
- const step = Math.ceil(lane / 2);
178
- return (lane % 2 === 1 ? 1 : -1) * step * 16;
260
+ if (lane === 0) return -8;
261
+ if (lane === 1) return 8;
262
+ if (lane === 2) return -16;
263
+ if (lane === 3) return 16;
264
+ const step = Math.ceil((lane + 1) / 2);
265
+ return (lane % 2 === 1 ? 1 : -1) * step * 8;
179
266
  }
180
267
  function routeLength(points) {
181
268
  let total = 0;
@@ -217,69 +304,149 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
217
304
  const laneShift = laneOffset(lane);
218
305
  const sourceCenter = rectCenter(sourceRect);
219
306
  const targetCenter = rectCenter(targetRect);
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;
225
- const source = horizontalPrimary ? {
226
- x: sourceRight ? sourceRect.x2 : sourceRect.x1,
227
- y: clamp(sourceCenter.y + laneShift, sourceRect.y1 + 16, sourceRect.y2 - 16)
228
- } : {
229
- x: clamp(sourceCenter.x + laneShift, sourceRect.x1 + 20, sourceRect.x2 - 20),
230
- y: sourceDown ? sourceRect.y2 : sourceRect.y1
231
- };
232
- const target = horizontalPrimary ? {
233
- x: targetLeft ? targetRect.x1 : targetRect.x2,
234
- y: clamp(targetCenter.y + laneShift, targetRect.y1 + 16, targetRect.y2 - 16)
235
- } : {
236
- x: clamp(targetCenter.x + laneShift, targetRect.x1 + 20, targetRect.x2 - 20),
237
- y: targetUp ? targetRect.y1 : targetRect.y2
238
- };
307
+ const dx = targetCenter.x - sourceCenter.x;
308
+ const dy = targetCenter.y - sourceCenter.y;
239
309
  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));
310
+ const allObstacles = obstacles;
243
311
  const candidates = [];
244
- if (Math.abs(source.y - target.y) < 1e-3 || Math.abs(source.x - target.x) < 1e-3) {
245
- candidates.push([source, target]);
246
- }
247
- const midX = round1((source.x + target.x) / 2 + laneShift);
248
- const midY = round1((source.y + target.y) / 2 + laneShift);
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],
252
- [source, { x: midX, y: source.y }, { x: midX, y: target.y }, target],
253
- [source, { x: source.x, y: midY }, { x: target.x, y: midY }, target]
254
- );
255
- const minObstacleY = infl.length ? Math.min(...infl.map((o) => o.y1)) : Math.min(source.y, target.y);
256
- const maxObstacleY = infl.length ? Math.max(...infl.map((o) => o.y2)) : Math.max(source.y, target.y);
257
- const topLane = Math.max(20, minObstacleY - 28 - lane * 14);
258
- const bottomLane = Math.min(bounds.height - 20, maxObstacleY + 28 + lane * 14);
259
- candidates.push(
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]
262
- );
263
- const minObstacleX = infl.length ? Math.min(...infl.map((o) => o.x1)) : Math.min(source.x, target.x);
264
- const maxObstacleX = infl.length ? Math.max(...infl.map((o) => o.x2)) : Math.max(source.x, target.x);
265
- const leftLane = Math.max(20, minObstacleX - 28 - lane * 14);
266
- const rightLane = Math.min(bounds.width - 20, maxObstacleX + 28 + lane * 14);
267
- candidates.push(
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]
270
- );
312
+ if (dx >= 30) {
313
+ let sY = sourceCenter.y;
314
+ let tY = targetCenter.y;
315
+ if (Math.abs(dy) > 16) {
316
+ const dirSign = Math.sign(dy);
317
+ sY = clamp(sourceCenter.y + dirSign * 12, sourceRect.y1 + 10, sourceRect.y2 - 10);
318
+ tY = clamp(targetCenter.y - dirSign * 12, targetRect.y1 + 10, targetRect.y2 - 10);
319
+ } else {
320
+ sY = clamp(sourceCenter.y + (lane > 0 ? lane % 2 === 1 ? 4 : -4 : 0), sourceRect.y1 + 10, sourceRect.y2 - 10);
321
+ tY = clamp(targetCenter.y + (lane > 0 ? lane % 2 === 1 ? 4 : -4 : 0), targetRect.y1 + 10, targetRect.y2 - 10);
322
+ }
323
+ const sPort = { x: sourceRect.x2, y: sY };
324
+ const tPort = { x: targetRect.x1, y: tY };
325
+ if (Math.abs(sY - tY) < 1) {
326
+ candidates.push([sPort, tPort]);
327
+ }
328
+ const rawMidX = (sPort.x + tPort.x) / 2 + laneShift;
329
+ const midX = round1(clamp(rawMidX, sourceRect.x2 + 8, targetRect.x1 - 8));
330
+ candidates.push([
331
+ sPort,
332
+ { x: midX, y: sY },
333
+ { x: midX, y: tY },
334
+ tPort
335
+ ]);
336
+ let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
337
+ for (const o of allObstacles) {
338
+ if (o.x2 > sourceRect.x2 && o.x1 < targetRect.x1) {
339
+ minObstacleY = Math.min(minObstacleY, o.y1);
340
+ }
341
+ }
342
+ const bypassYTop = Math.max(16, minObstacleY - 24 - Math.abs(laneShift));
343
+ candidates.push([
344
+ { x: sourceCenter.x, y: sourceRect.y1 },
345
+ { x: sourceCenter.x, y: bypassYTop },
346
+ { x: targetCenter.x, y: bypassYTop },
347
+ { x: targetCenter.x, y: targetRect.y1 }
348
+ ]);
349
+ } else if (dx < -30) {
350
+ const sX = clamp(sourceCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), sourceRect.x1 + 16, sourceRect.x2 - 16);
351
+ const tX = clamp(targetCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), targetRect.x1 + 16, targetRect.x2 - 16);
352
+ let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
353
+ for (const o of allObstacles) {
354
+ if (o.x2 > targetRect.x1 && o.x1 < sourceRect.x2) {
355
+ minObstacleY = Math.min(minObstacleY, o.y1);
356
+ }
357
+ }
358
+ const highwayYTop = Math.max(16, minObstacleY - 28 - Math.abs(laneShift));
359
+ candidates.push([
360
+ { x: sX, y: sourceRect.y1 },
361
+ { x: sX, y: highwayYTop },
362
+ { x: tX, y: highwayYTop },
363
+ { x: tX, y: targetRect.y1 }
364
+ ]);
365
+ let maxObstacleY = Math.max(sourceRect.y2, targetRect.y2);
366
+ for (const o of allObstacles) {
367
+ if (o.x2 > targetRect.x1 && o.x1 < sourceRect.x2) {
368
+ maxObstacleY = Math.max(maxObstacleY, o.y2);
369
+ }
370
+ }
371
+ const highwayYBottom = Math.min(bounds.height - 16, maxObstacleY + 28 + Math.abs(laneShift));
372
+ candidates.push([
373
+ { x: sX, y: sourceRect.y2 },
374
+ { x: sX, y: highwayYBottom },
375
+ { x: tX, y: highwayYBottom },
376
+ { x: tX, y: targetRect.y2 }
377
+ ]);
378
+ let sY = sourceCenter.y;
379
+ let tY = targetCenter.y;
380
+ if (Math.abs(dy) > 16) {
381
+ const dirSign = Math.sign(dy);
382
+ sY = clamp(sourceCenter.y + dirSign * 12, sourceRect.y1 + 10, sourceRect.y2 - 10);
383
+ tY = clamp(targetCenter.y - dirSign * 12, targetRect.y1 + 10, targetRect.y2 - 10);
384
+ }
385
+ const sPortL = { x: sourceRect.x1, y: sY };
386
+ const tPortR = { x: targetRect.x2, y: tY };
387
+ const midX = round1((sPortL.x + tPortR.x) / 2 + laneShift);
388
+ candidates.push([
389
+ sPortL,
390
+ { x: midX, y: sY },
391
+ { x: midX, y: tY },
392
+ tPortR
393
+ ]);
394
+ } else {
395
+ const sX = clamp(sourceCenter.x + laneShift, sourceRect.x1 + 16, sourceRect.x2 - 16);
396
+ const tX = clamp(targetCenter.x + laneShift, targetRect.x1 + 16, targetRect.x2 - 16);
397
+ const sourceDown = dy >= 0;
398
+ const sPort = { x: sX, y: sourceDown ? sourceRect.y2 : sourceRect.y1 };
399
+ const tPort = { x: tX, y: sourceDown ? targetRect.y1 : targetRect.y2 };
400
+ if (Math.abs(sX - tX) < 1) {
401
+ candidates.push([sPort, tPort]);
402
+ } else {
403
+ const midY = round1((sPort.y + tPort.y) / 2 + laneShift);
404
+ candidates.push([
405
+ sPort,
406
+ { x: sPort.x, y: midY },
407
+ { x: tPort.x, y: midY },
408
+ tPort
409
+ ]);
410
+ }
411
+ }
412
+ for (const obstacle of allObstacles) {
413
+ const bypassYTop = Math.max(16, obstacle.y1 - 18 - Math.abs(laneShift));
414
+ const bypassYBottom = Math.min(bounds.height - 16, obstacle.y2 + 18 + Math.abs(laneShift));
415
+ const sourceRight = dx >= 0;
416
+ const targetLeft = dx >= 0;
417
+ const sP = { x: sourceRight ? sourceRect.x2 : sourceRect.x1, y: sourceCenter.y };
418
+ const tP = { x: targetLeft ? targetRect.x1 : targetRect.x2, y: targetCenter.y };
419
+ candidates.push([
420
+ sP,
421
+ { x: sP.x + (sourceRight ? stubLen : -stubLen), y: sP.y },
422
+ { x: sP.x + (sourceRight ? stubLen : -stubLen), y: bypassYTop },
423
+ { x: tP.x + (targetLeft ? -stubLen : stubLen), y: bypassYTop },
424
+ { x: tP.x + (targetLeft ? -stubLen : stubLen), y: tP.y },
425
+ tP
426
+ ]);
427
+ candidates.push([
428
+ sP,
429
+ { x: sP.x + (sourceRight ? stubLen : -stubLen), y: sP.y },
430
+ { x: sP.x + (sourceRight ? stubLen : -stubLen), y: bypassYBottom },
431
+ { x: tP.x + (targetLeft ? -stubLen : stubLen), y: bypassYBottom },
432
+ { x: tP.x + (targetLeft ? -stubLen : stubLen), y: tP.y },
433
+ tP
434
+ ]);
435
+ }
271
436
  let best = candidates[0];
272
437
  let bestScore = Number.POSITIVE_INFINITY;
273
438
  for (const candidate of candidates) {
274
439
  const cleaned = cleanCollinearPoints(candidate);
275
- const hits = countPathIntersections(cleaned, infl);
276
- const score = hits * 1e5 + routeBends(cleaned) * 800 + routeLength(cleaned);
440
+ const hits = countPathIntersections(cleaned, allObstacles);
441
+ const bends = routeBends(cleaned);
442
+ const length = routeLength(cleaned);
443
+ const score = hits * 1e6 + bends * 500 + length;
277
444
  if (score < bestScore) {
278
- best = cleaned;
279
445
  bestScore = score;
446
+ best = cleaned;
280
447
  }
281
448
  }
282
- return cleanCollinearPoints(best.map((point) => clampPointToScene(point, bounds)));
449
+ return cleanCollinearPoints(best.map((p) => clampPointToScene(p, bounds)));
283
450
  }
284
451
 
285
452
  // src/edges.ts
@@ -396,16 +563,38 @@ function ensureDefs(svg, theme, id) {
396
563
  defs.appendChild(filter);
397
564
  svg.prepend(defs);
398
565
  }
399
- function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane) {
566
+ function isDarkTheme(theme) {
567
+ if (theme.name === "paper" || theme.name === "editorial" || theme.name === "sketchy") {
568
+ return false;
569
+ }
570
+ const canvas = (theme.canvas || theme.surface || "").trim();
571
+ if (canvas.startsWith("#")) {
572
+ const hex = canvas.slice(1);
573
+ const r = parseInt(hex.length === 3 ? hex[0] + hex[0] : hex.slice(0, 2), 16) || 0;
574
+ const g = parseInt(hex.length === 3 ? hex[1] + hex[1] : hex.slice(2, 4), 16) || 0;
575
+ const b = parseInt(hex.length === 3 ? hex[2] + hex[2] : hex.slice(4, 6), 16) || 0;
576
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
577
+ return luminance < 0.5;
578
+ }
579
+ return true;
580
+ }
581
+ function computeEdgeLabelColor(edgeColor, isDark) {
582
+ if (isDark) {
583
+ return `color-mix(in srgb, ${edgeColor} 65%, #f8fafc)`;
584
+ }
585
+ return `color-mix(in srgb, ${edgeColor} 75%, #090d16)`;
586
+ }
587
+ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane, existingPaths = []) {
400
588
  ensureDefs(svg, theme, sceneId);
401
589
  const color = theme.edges[kind];
402
590
  const style = EDGE_STYLES[kind];
403
591
  const isSelfLoop = from.id === to.id;
404
592
  const points = isSelfLoop ? dedupePoints(selfLoopPath(from)) : dedupePoints(routeEdgePoints(from, to, routeObstacles, bounds, lane));
405
- const d = toPathD(points);
593
+ const d = toPathD(points, 14, existingPaths);
406
594
  const len = polylineLength(points);
407
595
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
408
596
  group.setAttribute("data-edge-kind", kind);
597
+ group.setAttribute("class", `markdy-edge markdy-edge--${kind}`);
409
598
  group.style.opacity = "0";
410
599
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
411
600
  path.setAttribute("d", d);
@@ -414,6 +603,7 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
414
603
  path.setAttribute("stroke-width", kind === "dependency" ? "1.5" : "2");
415
604
  path.setAttribute("stroke-linejoin", "round");
416
605
  path.setAttribute("stroke-linecap", "round");
606
+ path.setAttribute("class", `markdy-edge-path markdy-edge-path--${kind}`);
417
607
  if (style.dash) path.setAttribute("stroke-dasharray", style.dash);
418
608
  if (style.marker !== "none") {
419
609
  path.setAttribute("marker-end", `url(#${sceneId}-arrow-${kind})`);
@@ -424,40 +614,46 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
424
614
  const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
425
615
  dot.setAttribute("r", "4");
426
616
  dot.setAttribute("fill", color);
617
+ dot.setAttribute("class", "markdy-edge-dot");
427
618
  dot.style.opacity = "0";
428
619
  dot.style.filter = `drop-shadow(0 0 6px ${color}) drop-shadow(0 0 12px ${color}88)`;
429
620
  group.append(path, dot);
430
621
  let labelEl;
431
622
  let labelRect;
432
623
  if (label) {
433
- const textWidth = label.length * 6.8 + 14;
624
+ const isDark = isDarkTheme(theme);
625
+ const labelColor = computeEdgeLabelColor(color, isDark);
626
+ const textWidth = Math.max(36, label.length * 6.6 + 8);
434
627
  const placement = placeFlowLabel(points, textWidth, labelObstacles, bounds);
435
628
  labelRect = placement.rect;
436
629
  const plate = document.createElementNS("http://www.w3.org/2000/svg", "rect");
437
- const padX = 8;
630
+ const padX = 6;
438
631
  const halfW = textWidth / 2;
632
+ plate.setAttribute("class", "markdy-edge-plate");
439
633
  plate.setAttribute("x", String(placement.x - halfW - padX));
440
- plate.setAttribute("y", String(placement.y - 10));
634
+ plate.setAttribute("y", String(placement.y - 9));
441
635
  plate.setAttribute("width", String(textWidth + padX * 2));
442
- plate.setAttribute("height", "20");
443
- plate.setAttribute("rx", "6");
444
- plate.setAttribute("fill", theme.labelPlate ?? theme.surface);
445
- plate.setAttribute("fill-opacity", "0.96");
446
- plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 70%, transparent)`);
447
- plate.setAttribute("stroke-width", "1");
636
+ plate.setAttribute("height", "18");
637
+ plate.setAttribute("rx", "4");
638
+ plate.setAttribute("ry", "4");
639
+ plate.setAttribute("fill", theme.canvas ?? theme.surface ?? "#ffffff");
640
+ plate.setAttribute("fill-opacity", "0.85");
641
+ plate.setAttribute("stroke", translucentColor(color, "28"));
642
+ plate.setAttribute("stroke-width", "0.85");
448
643
  plate.style.opacity = "0";
449
- plate.style.filter = "drop-shadow(0 1px 3px rgba(0,0,0,0.12))";
644
+ plate.style.filter = "none";
450
645
  group.appendChild(plate);
451
646
  labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
647
+ labelEl.setAttribute("class", "markdy-edge-label");
452
648
  labelEl.setAttribute("x", String(placement.x));
453
649
  labelEl.setAttribute("y", String(placement.y + 0.5));
454
650
  labelEl.setAttribute("text-anchor", "middle");
455
651
  labelEl.setAttribute("dominant-baseline", "middle");
456
- labelEl.setAttribute("font-size", "11");
652
+ labelEl.setAttribute("font-size", "10.5");
457
653
  labelEl.setAttribute("font-weight", "500");
458
654
  labelEl.setAttribute("letter-spacing", "0.02em");
459
- labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, monospace");
460
- labelEl.setAttribute("fill", theme.text);
655
+ labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace");
656
+ labelEl.setAttribute("fill", labelColor);
461
657
  labelEl.textContent = label;
462
658
  labelEl.style.opacity = "0";
463
659
  group.appendChild(labelEl);
@@ -482,6 +678,11 @@ function setEdgeVisible(runtime, visible) {
482
678
  runtime.group.style.opacity = visible ? "1" : "0";
483
679
  if (runtime.label) runtime.label.style.opacity = visible ? "1" : "0";
484
680
  if (runtime.labelPlate) runtime.labelPlate.style.opacity = visible ? "1" : "0";
681
+ if (visible) {
682
+ runtime.path.classList.add("markdy-edge-path--flowing");
683
+ } else {
684
+ runtime.path.classList.remove("markdy-edge-path--flowing");
685
+ }
485
686
  }
486
687
  function translucentColor(color, alpha = "aa") {
487
688
  const value = color.trim();
@@ -493,8 +694,16 @@ function translucentColor(color, alpha = "aa") {
493
694
  return `color-mix(in srgb, ${value} 67%, transparent)`;
494
695
  }
495
696
  function nextEdgeLane(lanes, from, to) {
496
- const pair = [from, to].sort().join("|");
497
- const keys = [`pair:${pair}`, `out:${from}`, `in:${to}`];
697
+ const fromId = typeof from === "string" ? from : from.id;
698
+ const toId = typeof to === "string" ? to : to.id;
699
+ const pair = [fromId, toId].sort().join("|");
700
+ const keys = [`pair:${pair}`, `out:${fromId}`, `in:${toId}`];
701
+ if (typeof from !== "string" && typeof to !== "string") {
702
+ const colFrom = Math.round(from.x / 80);
703
+ const colTo = Math.round(to.x / 80);
704
+ const corridorKey = colFrom !== colTo ? `corridor-x:${Math.min(colFrom, colTo)}-${Math.max(colFrom, colTo)}` : `corridor-y:${Math.round(from.y / 60)}-${Math.round(to.y / 60)}`;
705
+ keys.push(corridorKey);
706
+ }
498
707
  const lane = Math.max(...keys.map((key) => lanes.get(key) ?? 0));
499
708
  for (const key of keys) lanes.set(key, (lanes.get(key) ?? 0) + 1);
500
709
  return lane;
@@ -568,14 +777,42 @@ function animateEdgeReveal(runtime, startMs, durMs) {
568
777
  return anims;
569
778
  }
570
779
  function animateEdgeEmphasis(runtime, startMs, durMs, strength, color) {
780
+ const anims = [];
571
781
  const baseFilter = runtime.kind === "dependency" ? "none" : `drop-shadow(0 0 3px ${translucentColor(runtime.color, "33")})`;
572
782
  const glowColor = color ?? runtime.color;
573
783
  const radius = Math.max(4, Math.min(16, 5 + strength * 4));
574
784
  const peakFilter = `drop-shadow(0 0 ${radius}px ${translucentColor(glowColor)}) brightness(${1 + Math.min(strength, 2) * 0.08})`;
575
- return runtime.path.animate(
576
- [{ filter: baseFilter }, { filter: peakFilter }, { filter: baseFilter }],
577
- { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
785
+ anims.push(
786
+ runtime.path.animate(
787
+ [{ filter: baseFilter }, { filter: peakFilter }, { filter: baseFilter }],
788
+ { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
789
+ )
578
790
  );
791
+ if (runtime.label) {
792
+ anims.push(
793
+ runtime.label.animate(
794
+ [
795
+ { filter: "none", opacity: 1 },
796
+ { filter: `drop-shadow(0 0 4px ${translucentColor(glowColor, "88")}) brightness(1.25)`, opacity: 1 },
797
+ { filter: "none", opacity: 1 }
798
+ ],
799
+ { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
800
+ )
801
+ );
802
+ }
803
+ if (runtime.labelPlate) {
804
+ anims.push(
805
+ runtime.labelPlate.animate(
806
+ [
807
+ { stroke: translucentColor(runtime.color, "22") },
808
+ { stroke: translucentColor(glowColor, "88") },
809
+ { stroke: translucentColor(runtime.color, "22") }
810
+ ],
811
+ { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
812
+ )
813
+ );
814
+ }
815
+ return anims;
579
816
  }
580
817
  function computeFrameTransform(targetIds, nodes, bounds, requestedZoom = DEFAULT_FRAME_ZOOM) {
581
818
  const targets = new Set(targetIds);
@@ -614,13 +851,14 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
614
851
  const edgeRectById = new Map(nodes.map((node) => [node.id, boxRect(node)]));
615
852
  const edgeLabels = [];
616
853
  const edgeLanes = /* @__PURE__ */ new Map();
854
+ const placedPaths = [];
617
855
  for (const edge of diagramType === "sequence" ? [] : edges) {
618
856
  if (edge.structural || edgeRuntimes.has(edge.id)) continue;
619
857
  const from = nodeById.get(edge.from);
620
858
  const to = nodeById.get(edge.to);
621
859
  if (!from || !to) continue;
622
- const routeObstacles = [...edgeRectById.entries()].filter(([id]) => id !== from.id && id !== to.id).map(([, rect]) => rect);
623
- const lane = nextEdgeLane(edgeLanes, edge.from, edge.to);
860
+ const routeObstacles = [...allNodeRects, ...edgeLabels.map((l) => inflateRect(l, 4))];
861
+ const lane = nextEdgeLane(edgeLanes, from, to);
624
862
  const runtime = createEdgeRuntime(
625
863
  svg,
626
864
  from,
@@ -630,10 +868,12 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
630
868
  theme,
631
869
  sceneId,
632
870
  routeObstacles,
633
- [...edgeRects, ...edgeLabels],
871
+ [...allNodeRects, ...edgeLabels],
634
872
  bounds,
635
- lane
873
+ lane,
874
+ placedPaths
636
875
  );
876
+ placedPaths.push(runtime.points);
637
877
  if (runtime.labelRect) edgeLabels.push(runtime.labelRect);
638
878
  edgeRuntimes.set(edge.id, runtime);
639
879
  }
@@ -697,7 +937,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
697
937
  continue;
698
938
  }
699
939
  const runtime = edgeRuntimes.get(id);
700
- if (runtime) anims.push(animateEdgeEmphasis(runtime, startMs, durMs, strength, typeof cue.params.color === "string" ? cue.params.color : void 0));
940
+ if (runtime) anims.push(...animateEdgeEmphasis(runtime, startMs, durMs, strength, typeof cue.params.color === "string" ? cue.params.color : void 0));
701
941
  }
702
942
  continue;
703
943
  }
@@ -719,7 +959,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
719
959
  continue;
720
960
  }
721
961
  const runtime = edgeRuntimes.get(id);
722
- if (runtime) anims.push(animateEdgeEmphasis(runtime, startMs, durMs, zoom, void 0));
962
+ if (runtime) anims.push(...animateEdgeEmphasis(runtime, startMs, durMs, zoom, void 0));
723
963
  }
724
964
  continue;
725
965
  }
@@ -749,18 +989,29 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
749
989
  const from = nodeById.get(seg.from);
750
990
  const to = nodeById.get(seg.to);
751
991
  if (!from || !to) continue;
752
- const routeObstacles = [];
753
- for (const [id, rect] of rectById) {
754
- if (id !== seg.from && id !== seg.to) routeObstacles.push(rect);
755
- }
756
- const lane = nextEdgeLane(laneByPair, seg.from, seg.to);
992
+ const routeObstacles = [...allNodeRects, ...placedLabels.map((l) => inflateRect(l, 4))];
993
+ const lane = nextEdgeLane(laneByPair, from, to);
757
994
  const labelObstacles = [...allNodeRects, ...placedLabels];
758
995
  const edgeId = cue.edgeId ?? edges.find(
759
996
  (edge) => !edge.structural && edge.from === seg.from && edge.to === seg.to && edge.kind === seg.op && edge.label === seg.label
760
997
  )?.id;
761
998
  let runtime = edgeId ? edgeRuntimes.get(edgeId) : void 0;
762
999
  if (!runtime) {
763
- runtime = createEdgeRuntime(svg, from, to, seg.op, seg.label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane);
1000
+ runtime = createEdgeRuntime(
1001
+ svg,
1002
+ from,
1003
+ to,
1004
+ seg.op,
1005
+ seg.label,
1006
+ theme,
1007
+ sceneId,
1008
+ routeObstacles,
1009
+ labelObstacles,
1010
+ bounds,
1011
+ lane,
1012
+ placedPaths
1013
+ );
1014
+ placedPaths.push(runtime.points);
764
1015
  if (runtime.labelRect) placedLabels.push(runtime.labelRect);
765
1016
  if (edgeId) edgeRuntimes.set(edgeId, runtime);
766
1017
  }
@@ -792,16 +1043,14 @@ function buildStructuralEdgeAnimations(edges, nodes, theme, scene, bounds, edgeR
792
1043
  const rectById = new Map(nodes.map((n) => [n.id, boxRect(n)]));
793
1044
  const allNodeRects = [...rectById.values()];
794
1045
  const placedLabels = [];
1046
+ const placedPaths = [];
795
1047
  const laneByPair = /* @__PURE__ */ new Map();
796
1048
  const svg = ensureEdgeLayer(scene);
797
1049
  for (const edge of structural) {
798
1050
  const from = nodeById.get(edge.from);
799
1051
  const to = nodeById.get(edge.to);
800
1052
  if (!from || !to) continue;
801
- const routeObstacles = [];
802
- for (const [id, rect] of rectById) {
803
- if (id !== edge.from && id !== edge.to) routeObstacles.push(rect);
804
- }
1053
+ const routeObstacles = [...allNodeRects, ...placedLabels.map((l) => inflateRect(l, 4))];
805
1054
  const lane = nextEdgeLane(laneByPair, edge.from, edge.to);
806
1055
  const labelObstacles = [...allNodeRects, ...placedLabels];
807
1056
  const runtime = createEdgeRuntime(
@@ -815,8 +1064,10 @@ function buildStructuralEdgeAnimations(edges, nodes, theme, scene, bounds, edgeR
815
1064
  routeObstacles,
816
1065
  labelObstacles,
817
1066
  bounds,
818
- lane
1067
+ lane,
1068
+ placedPaths
819
1069
  );
1070
+ placedPaths.push(runtime.points);
820
1071
  if (runtime.labelRect) placedLabels.push(runtime.labelRect);
821
1072
  setEdgeVisible(runtime, true);
822
1073
  edgeRuntimes.set(edge.id, runtime);
@@ -1165,9 +1416,14 @@ function ensureGroupStyles(doc) {
1165
1416
  .markdy-group-boundary {
1166
1417
  position: absolute;
1167
1418
  box-sizing: border-box;
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);
1419
+ border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 45%, transparent));
1420
+ border-radius: 16px;
1421
+ background: color-mix(in srgb, var(--md-surface-raised) 32%, transparent);
1422
+ box-shadow:
1423
+ 0 4px 20px -4px var(--md-shadow, rgba(0, 0, 0, 0.25)),
1424
+ inset 0 1px 0 rgba(255, 255, 255, 0.06);
1425
+ backdrop-filter: blur(8px);
1426
+ -webkit-backdrop-filter: blur(8px);
1171
1427
  pointer-events: none;
1172
1428
  z-index: 40;
1173
1429
  }
@@ -1175,17 +1431,19 @@ function ensureGroupStyles(doc) {
1175
1431
  position: absolute;
1176
1432
  left: 14px;
1177
1433
  top: 10px;
1178
- padding: 3px 8px;
1179
- font-size: 10px;
1434
+ padding: 4px 10px;
1435
+ font-size: 10.5px;
1180
1436
  font-weight: 600;
1181
1437
  letter-spacing: 0.08em;
1182
1438
  text-transform: uppercase;
1183
- color: var(--md-text-muted);
1184
- background: color-mix(in srgb, var(--md-surface-raised) 80%, transparent);
1439
+ color: var(--md-text);
1440
+ background: color-mix(in srgb, var(--md-surface-raised) 85%, transparent);
1185
1441
  border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 50%, transparent));
1186
- border-radius: 5px;
1442
+ border-radius: 6px;
1187
1443
  font-family: var(--md-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1188
- backdrop-filter: blur(4px);
1444
+ backdrop-filter: blur(6px);
1445
+ -webkit-backdrop-filter: blur(6px);
1446
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
1189
1447
  }
1190
1448
  `;
1191
1449
  doc.head.appendChild(style);
@@ -1199,10 +1457,11 @@ function createGroupBoundaryEl(boundary, theme, doc = document) {
1199
1457
  el.style.width = `${boundary.width}px`;
1200
1458
  el.style.height = `${boundary.height}px`;
1201
1459
  el.style.setProperty("--md-group-border", theme.hairline ?? theme.border);
1202
- if (boundary.label) {
1203
- const label = document.createElement("div");
1460
+ const displayLabel = boundary.label || boundary.id;
1461
+ if (displayLabel) {
1462
+ const label = doc.createElement("div");
1204
1463
  label.className = "markdy-group-boundary__label";
1205
- label.textContent = boundary.label;
1464
+ label.textContent = displayLabel;
1206
1465
  el.appendChild(label);
1207
1466
  }
1208
1467
  return el;
@@ -1226,6 +1485,8 @@ function ensureNodeStyles(doc) {
1226
1485
  box-sizing: border-box;
1227
1486
  width: var(--md-node-w, 180px);
1228
1487
  height: var(--md-node-h, 76px);
1488
+ min-width: 140px;
1489
+ min-height: 64px;
1229
1490
  border-radius: 12px;
1230
1491
  background:
1231
1492
  linear-gradient(180deg,
@@ -1233,10 +1494,10 @@ function ensureNodeStyles(doc) {
1233
1494
  var(--md-node-surface, var(--md-surface)));
1234
1495
  color: var(--md-text);
1235
1496
  box-shadow:
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);
1497
+ 0 1px 3px color-mix(in srgb, var(--md-shadow, rgba(2, 6, 23, 0.35)) 35%, transparent),
1498
+ 0 10px 24px -10px var(--md-shadow, rgba(2, 6, 23, 0.45)),
1499
+ inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent)),
1500
+ inset 0 1px 0 rgba(255, 255, 255, 0.12);
1240
1501
  font-family: var(--md-font-node, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif);
1241
1502
  overflow: hidden;
1242
1503
  opacity: 0;
@@ -1250,16 +1511,16 @@ function ensureNodeStyles(doc) {
1250
1511
  .markdy-node[data-focused="1"] {
1251
1512
  box-shadow:
1252
1513
  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);
1514
+ 0 18px 38px -10px rgba(2, 6, 23, 0.7),
1515
+ inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent) 80%, transparent),
1516
+ 0 0 0 3px color-mix(in srgb, var(--md-accent) 24%, transparent);
1256
1517
  }
1257
1518
  .markdy-node[data-glow="1"] {
1258
1519
  box-shadow:
1259
1520
  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),
1262
- inset 0 0 18px -8px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 45%, transparent);
1521
+ 0 0 0 1.5px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 70%, transparent),
1522
+ 0 0 28px -2px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 55%, transparent),
1523
+ inset 0 0 18px -8px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 50%, transparent);
1263
1524
  }
1264
1525
  .markdy-node__rail { display: none; }
1265
1526
  .markdy-node__type { display: none; }
@@ -1268,13 +1529,14 @@ function ensureNodeStyles(doc) {
1268
1529
  padding: 0 14px;
1269
1530
  display: flex;
1270
1531
  align-items: center;
1271
- gap: 10px;
1532
+ gap: 12px;
1272
1533
  min-width: 0;
1534
+ box-sizing: border-box;
1273
1535
  }
1274
1536
  .markdy-node__icon {
1275
1537
  flex: 0 0 auto;
1276
- width: 32px;
1277
- height: 32px;
1538
+ width: 34px;
1539
+ height: 34px;
1278
1540
  border-radius: 9px;
1279
1541
  display: flex;
1280
1542
  align-items: center;
@@ -1282,15 +1544,15 @@ function ensureNodeStyles(doc) {
1282
1544
  color: var(--md-role-color, var(--md-accent));
1283
1545
  background:
1284
1546
  linear-gradient(180deg,
1285
- color-mix(in srgb, var(--md-role-color, var(--md-accent)) 22%, transparent),
1547
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 24%, transparent),
1286
1548
  color-mix(in srgb, var(--md-role-color, var(--md-accent)) 10%, transparent));
1287
1549
  box-shadow:
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);
1550
+ inset 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 38%, transparent),
1551
+ inset 0 1px 0 rgba(255, 255, 255, 0.18);
1290
1552
  }
1291
1553
  .markdy-node__icon svg {
1292
- width: 17px;
1293
- height: 17px;
1554
+ width: 18px;
1555
+ height: 18px;
1294
1556
  display: block;
1295
1557
  stroke: currentColor;
1296
1558
  }
@@ -1321,7 +1583,8 @@ function ensureNodeStyles(doc) {
1321
1583
  font-size: 13.5px;
1322
1584
  font-weight: 600;
1323
1585
  letter-spacing: -0.01em;
1324
- line-height: 1.22;
1586
+ line-height: 1.24;
1587
+ color: var(--md-text);
1325
1588
  display: -webkit-box;
1326
1589
  -webkit-box-orient: vertical;
1327
1590
  -webkit-line-clamp: 3;
@@ -1333,13 +1596,13 @@ function ensureNodeStyles(doc) {
1333
1596
  }
1334
1597
  .markdy-node__value {
1335
1598
  flex: 0 0 auto;
1336
- font-size: 17px;
1599
+ font-size: 16px;
1337
1600
  font-weight: 700;
1338
1601
  color: var(--md-ink, var(--md-text));
1339
1602
  font-variant-numeric: tabular-nums;
1340
1603
  }
1341
1604
  .markdy-node[data-role="client"] { border-radius: 14px 14px 10px 10px; }
1342
- .markdy-node[data-role="data"] { border-radius: 12px 12px 18px 18px; }
1605
+ .markdy-node[data-role="data"] { border-radius: 12px 12px 16px 16px; }
1343
1606
  .markdy-scene-title {
1344
1607
  position: absolute;
1345
1608
  left: 48px;
@@ -1413,16 +1676,18 @@ function ensureNodeStyles(doc) {
1413
1676
  }
1414
1677
  .markdy-node[data-is-container="1"] {
1415
1678
  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);
1679
+ border: 1px solid color-mix(in srgb, var(--md-role-color, var(--md-accent)) 40%, var(--md-border) 60%);
1680
+ box-shadow: inset 0 0 0 1px var(--md-hairline), 0 4px 16px -4px var(--md-shadow, rgba(0,0,0,0.25));
1681
+ backdrop-filter: blur(8px);
1682
+ -webkit-backdrop-filter: blur(8px);
1418
1683
  }
1419
1684
  .markdy-node[data-is-container="1"] .markdy-node__body {
1420
1685
  align-items: flex-start;
1421
1686
  padding: 12px 16px;
1422
1687
  }
1423
1688
  .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);
1689
+ background: color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 16%, transparent);
1690
+ border: 1.5px solid color-mix(in srgb, var(--md-accent) 70%, transparent);
1426
1691
  }
1427
1692
  .markdy-node[data-shape="rounded"] {
1428
1693
  border-radius: 16px;
@@ -1844,6 +2109,9 @@ function createTitleEl(title) {
1844
2109
  const el = document.createElement("div");
1845
2110
  el.className = "markdy-scene-title";
1846
2111
  el.textContent = title;
2112
+ if (!title) {
2113
+ el.style.display = "none";
2114
+ }
1847
2115
  return el;
1848
2116
  }
1849
2117
 
@@ -2147,6 +2415,28 @@ function ensureSceneStyles(doc) {
2147
2415
  from { opacity: 0.24; transform: scale(0.85); }
2148
2416
  to { opacity: 0.9; transform: scale(1.15); }
2149
2417
  }
2418
+ @keyframes markdy-flow-dash {
2419
+ to { stroke-dashoffset: -24; }
2420
+ }
2421
+ .markdy-edge {
2422
+ transition: opacity 0.2s ease;
2423
+ }
2424
+ .markdy-edge-path--flowing {
2425
+ animation: markdy-flow-dash 1.2s linear infinite;
2426
+ }
2427
+ .markdy-edge-path {
2428
+ transition: stroke 0.2s ease, stroke-width 0.2s ease, filter 0.2s ease;
2429
+ }
2430
+ .markdy-edge-plate {
2431
+ transition: opacity 0.2s ease, fill-opacity 0.2s ease, stroke 0.2s ease;
2432
+ pointer-events: none;
2433
+ }
2434
+ .markdy-edge-label {
2435
+ user-select: none;
2436
+ -webkit-user-select: none;
2437
+ pointer-events: none;
2438
+ transition: fill 0.2s ease, opacity 0.2s ease, filter 0.2s ease;
2439
+ }
2150
2440
  .markdy-constellation-star {
2151
2441
  transform-box: fill-box;
2152
2442
  transform-origin: center;
@@ -2223,13 +2513,17 @@ function ensureSceneStyles(doc) {
2223
2513
  @media (prefers-reduced-motion: reduce) {
2224
2514
  .markdy-node,
2225
2515
  .markdy-beat-caption,
2226
- .markdy-constellation-star {
2516
+ .markdy-constellation-star,
2517
+ .markdy-edge-path,
2518
+ .markdy-edge-path--flowing {
2227
2519
  animation-duration: 0.001ms !important;
2228
2520
  animation-iteration-count: 1 !important;
2229
2521
  transition-duration: 0.001ms !important;
2522
+ animation: none !important;
2230
2523
  }
2231
2524
  .markdy-node { opacity: 1 !important; transform: none !important; }
2232
2525
  .markdy-constellation-star { animation: none !important; opacity: 0.7 !important; }
2526
+ .markdy-edge-path--flowing { animation: none !important; }
2233
2527
  }
2234
2528
  @media print {
2235
2529
  .markdy-node { opacity: 1 !important; transform: none !important; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
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.9"
48
+ "@markdy/core": "1.0.10"
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.9"
55
+ "@markdy/stdlib-systems": "1.0.10"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",