@synergenius/flow-weaver 0.33.0 → 0.33.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5987,7 +5987,7 @@ var VERSION;
5987
5987
  var init_generated_version = __esm({
5988
5988
  "src/generated-version.ts"() {
5989
5989
  "use strict";
5990
- VERSION = "0.33.0";
5990
+ VERSION = "0.33.2";
5991
5991
  }
5992
5992
  });
5993
5993
 
@@ -30483,19 +30483,6 @@ function getPortColor(dataType, isFailure, theme = "dark") {
30483
30483
  const colors2 = theme === "dark" ? DARK_PORT_COLORS : LIGHT_PORT_COLORS;
30484
30484
  return colors2[dataType] ?? colors2.ANY;
30485
30485
  }
30486
- function getPortRingColor(dataType, isFailure, theme = "dark") {
30487
- const color = getPortColor(dataType, isFailure, theme);
30488
- return darkenHex(color, 0.4);
30489
- }
30490
- function darkenHex(hex, amount) {
30491
- const r = parseInt(hex.slice(1, 3), 16);
30492
- const g = parseInt(hex.slice(3, 5), 16);
30493
- const b = parseInt(hex.slice(5, 7), 16);
30494
- const nr = Math.round(r * (1 - amount));
30495
- const ng = Math.round(g * (1 - amount));
30496
- const nb = Math.round(b * (1 - amount));
30497
- return `#${nr.toString(16).padStart(2, "0")}${ng.toString(16).padStart(2, "0")}${nb.toString(16).padStart(2, "0")}`;
30498
- }
30499
30486
  var DARK_PORT_COLORS, LIGHT_PORT_COLORS, DARK_FAILURE_COLOR, LIGHT_FAILURE_COLOR, NODE_DEFAULT_COLOR, NODE_VARIANT_COLORS, DARK_PALETTE, LIGHT_PALETTE, TYPE_ABBREVIATIONS, NODE_ICON_PATHS, VALID_NODE_ICONS;
30500
30487
  var init_theme = __esm({
30501
30488
  "src/diagram/theme.ts"() {
@@ -30540,23 +30527,23 @@ var init_theme = __esm({
30540
30527
  LIGHT_FAILURE_COLOR = "#e34646";
30541
30528
  NODE_DEFAULT_COLOR = "#334155";
30542
30529
  NODE_VARIANT_COLORS = {
30543
- blue: { border: "#548ce3", darkBorder: "#5e9eff" },
30544
- // blue-shade-2 / blue-dark-shade-1
30545
- purple: { border: "#9f5fe3", darkBorder: "#b36bff" },
30530
+ blue: { border: "#548ce3", darkBorder: "#5e9eff", icon: "#3a6bbf", darkIcon: "#4a7ad4" },
30531
+ // blue-shade-2 / blue-dark-shade-1 / blue-shade-3 / blue-dark-shade-3
30532
+ purple: { border: "#9f5fe3", darkBorder: "#b36bff", icon: "#7d44bf", darkIcon: "#9050d4" },
30546
30533
  // purple-shade-2 / purple-dark-shade-1
30547
- cyan: { border: "#63ccc4", darkBorder: "#6fe5dc" },
30534
+ cyan: { border: "#63ccc4", darkBorder: "#6fe5dc", icon: "#4aada6", darkIcon: "#56c4bb" },
30548
30535
  // cyan-shade-2 / cyan-dark-shade-1
30549
- teal: { border: "#63ccc4", darkBorder: "#6fe5dc" },
30536
+ teal: { border: "#63ccc4", darkBorder: "#6fe5dc", icon: "#4aada6", darkIcon: "#56c4bb" },
30550
30537
  // alias for cyan
30551
- orange: { border: "#e3732d", darkBorder: "#ff8133" },
30538
+ orange: { border: "#e3732d", darkBorder: "#ff8133", icon: "#bf5a1e", darkIcon: "#d46a28" },
30552
30539
  // orange-shade-2 / orange-dark-shade-1
30553
- pink: { border: "#e349c2", darkBorder: "#ff52da" },
30540
+ pink: { border: "#e349c2", darkBorder: "#ff52da", icon: "#bf349f", darkIcon: "#d43fb5" },
30554
30541
  // pink-shade-2 / pink-dark-shade-1
30555
- green: { border: "#0ec850", darkBorder: "#10e15a" },
30542
+ green: { border: "#0ec850", darkBorder: "#10e15a", icon: "#0aa53f", darkIcon: "#0dbd4a" },
30556
30543
  // green-shade-2 / green-dark-shade-1
30557
- red: { border: "#e34646", darkBorder: "#ff4f4f" },
30544
+ red: { border: "#e34646", darkBorder: "#ff4f4f", icon: "#bf3333", darkIcon: "#d43b3b" },
30558
30545
  // red-shade-2 / red-dark-shade-1
30559
- yellow: { border: "#e3a82b", darkBorder: "#ffbd30" }
30546
+ yellow: { border: "#e3a82b", darkBorder: "#ffbd30", icon: "#bf8c21", darkIcon: "#d49e28" }
30560
30547
  // yellow-shade-2 / yellow-dark-shade-1
30561
30548
  };
30562
30549
  DARK_PALETTE = {
@@ -30572,18 +30559,18 @@ var init_theme = __esm({
30572
30559
  // color-text-subtle = dark-shade-30
30573
30560
  connectionColor: "#5f5f6d",
30574
30561
  // color-border-subtle = dark-shade-70
30575
- dotColor: "#8e9eff",
30576
- // color-background-dots = primary-dark-tint-1
30562
+ dotColor: "#7b8cd9",
30563
+ // color-background-dots-secondary = primary-dark-tint-2
30577
30564
  labelBadgeFill: "#252538",
30578
30565
  // color-surface-low = dark-shade-95
30579
30566
  labelBadgeBorder: "#313143",
30580
30567
  // color-surface-lowest = dark-shade-90
30581
- nodeIconColor: "#a4beff",
30582
- // secondary-dark-base (matches label color)
30568
+ nodeIconColor: "#8e9eff",
30569
+ // color-brand-main = primary-dark-tint-1
30583
30570
  scopeAreaStroke: "#5f5f6d",
30584
30571
  // color-border-subtle = dark-shade-70
30585
- nodeShadowOpacity: 0.15,
30586
- dotOpacity: 0.5
30572
+ nodeShadowOpacity: 0,
30573
+ dotOpacity: 0.4
30587
30574
  };
30588
30575
  LIGHT_PALETTE = {
30589
30576
  background: "#f6f7ff",
@@ -30598,18 +30585,18 @@ var init_theme = __esm({
30598
30585
  // shade-50
30599
30586
  connectionColor: "#b3b3b3",
30600
30587
  // shade-30
30601
- dotColor: "#5468ff",
30602
- // color-background-dots = primary-shade-1
30588
+ dotColor: "#4a5ce0",
30589
+ // color-background-dots-secondary = primary-shade-2
30603
30590
  labelBadgeFill: "#ffffff",
30604
30591
  // surface-main — 80% opacity applied in renderer
30605
30592
  labelBadgeBorder: "#e6e6e6",
30606
30593
  // shade-10
30607
30594
  nodeIconColor: "#5468ff",
30608
- // primary-base (light)
30595
+ // color-brand-main = primary-base
30609
30596
  scopeAreaStroke: "#cccccc",
30610
30597
  // color-border-default
30611
- nodeShadowOpacity: 0.08,
30612
- dotOpacity: 0.45
30598
+ nodeShadowOpacity: 0,
30599
+ dotOpacity: 0.4
30613
30600
  };
30614
30601
  TYPE_ABBREVIATIONS = {
30615
30602
  STEP: "STP",
@@ -48175,7 +48162,9 @@ function segmentOverlapsBox(xMin, xMax, y, box) {
48175
48162
  return xMin < box.right && xMax > box.left && y >= box.top && y <= box.bottom;
48176
48163
  }
48177
48164
  function verticalSegmentClear(x2, yMin, yMax, boxes) {
48178
- return !boxes.some((box) => x2 >= box.left && x2 <= box.right && yMin < box.bottom && yMax > box.top);
48165
+ return !boxes.some(
48166
+ (box) => x2 >= box.left && x2 <= box.right && yMin < box.bottom && yMax > box.top
48167
+ );
48179
48168
  }
48180
48169
  function findClearY(xMin, xMax, candidateY, boxes) {
48181
48170
  const isBlocked = (y) => boxes.some((box) => segmentOverlapsBox(xMin, xMax, y, box));
@@ -48205,7 +48194,7 @@ function findClearY(xMin, xMax, candidateY, boxes) {
48205
48194
  if (bestDist === Infinity) {
48206
48195
  const allMin = Math.min(...edges) - EDGE_OFFSET * 2;
48207
48196
  const allMax = Math.max(...edges) + EDGE_OFFSET * 2;
48208
- bestY = Math.abs(allMin - candidateY) <= Math.abs(allMax - candidateY) ? allMin : allMax;
48197
+ bestY = Math.abs(allMin - candidateY) < Math.abs(allMax - candidateY) ? allMin : allMax;
48209
48198
  if (isBlocked(bestY)) {
48210
48199
  for (let offset = TRACK_SPACING; offset < 800; offset += TRACK_SPACING) {
48211
48200
  if (!isBlocked(bestY - offset)) {
@@ -48275,25 +48264,27 @@ function simplifyWaypoints(waypoints) {
48275
48264
  const a = pts[i], b = pts[i + 1], c = pts[i + 2], d = pts[i + 3];
48276
48265
  const jogH = Math.abs(b[1] - c[1]);
48277
48266
  if (Math.abs(a[1] - b[1]) < 0.5 && Math.abs(b[0] - c[0]) < 0.5 && Math.abs(c[1] - d[1]) < 0.5 && jogH > 0.5 && jogH < JOG_THRESHOLD) {
48278
- const jogMid = (b[1] + c[1]) / 2;
48279
- const snapY = Math.abs(a[1] - jogMid) <= Math.abs(d[1] - jogMid) ? a[1] : d[1];
48280
- const newPts = pts.slice();
48281
- newPts[i + 1] = [b[0], snapY];
48282
- newPts[i + 2] = [c[0], snapY];
48283
- pts = newPts;
48284
- jogFound = true;
48285
- break;
48267
+ if (Math.abs(a[1] - d[1]) < 0.5) {
48268
+ const snapY = a[1];
48269
+ const newPts = pts.slice();
48270
+ newPts[i + 1] = [b[0], snapY];
48271
+ newPts[i + 2] = [c[0], snapY];
48272
+ pts = newPts;
48273
+ jogFound = true;
48274
+ break;
48275
+ }
48286
48276
  }
48287
48277
  const jogW = Math.abs(b[0] - c[0]);
48288
48278
  if (Math.abs(a[0] - b[0]) < 0.5 && Math.abs(b[1] - c[1]) < 0.5 && Math.abs(c[0] - d[0]) < 0.5 && jogW > 0.5 && jogW < JOG_THRESHOLD) {
48289
- const jogMid = (b[0] + c[0]) / 2;
48290
- const snapX = Math.abs(a[0] - jogMid) <= Math.abs(d[0] - jogMid) ? a[0] : d[0];
48291
- const newPts = pts.slice();
48292
- newPts[i + 1] = [snapX, b[1]];
48293
- newPts[i + 2] = [snapX, c[1]];
48294
- pts = newPts;
48295
- jogFound = true;
48296
- break;
48279
+ if (Math.abs(a[0] - d[0]) < 0.5) {
48280
+ const snapX = a[0];
48281
+ const newPts = pts.slice();
48282
+ newPts[i + 1] = [snapX, b[1]];
48283
+ newPts[i + 2] = [snapX, c[1]];
48284
+ pts = newPts;
48285
+ jogFound = true;
48286
+ break;
48287
+ }
48297
48288
  }
48298
48289
  }
48299
48290
  }
@@ -48303,7 +48294,10 @@ function simplifyWaypoints(waypoints) {
48303
48294
  const curr = pts[i];
48304
48295
  const next = pts[i + 1];
48305
48296
  const distToPrev = Math.abs(prev[0] - curr[0]) + Math.abs(prev[1] - curr[1]);
48306
- if (distToPrev < MIN_SEGMENT_LENGTH) continue;
48297
+ if (distToPrev < MIN_SEGMENT_LENGTH) {
48298
+ const wouldDiag = Math.abs(prev[0] - next[0]) > 0.5 && Math.abs(prev[1] - next[1]) > 0.5;
48299
+ if (!wouldDiag) continue;
48300
+ }
48307
48301
  const sameX = Math.abs(prev[0] - curr[0]) < 0.01 && Math.abs(curr[0] - next[0]) < 0.01;
48308
48302
  const sameY = Math.abs(prev[1] - curr[1]) < 0.01 && Math.abs(curr[1] - next[1]) < 0.01;
48309
48303
  if (!sameX && !sameY) {
@@ -48344,7 +48338,7 @@ function waypointsToSvgPath(waypoints, cornerRadius) {
48344
48338
  const curr = waypoints[i];
48345
48339
  const next = waypoints[i + 1];
48346
48340
  const r = radii[i];
48347
- if (r < 2) {
48341
+ if (r < 0.01) {
48348
48342
  path54 += ` L ${curr[0]},${curr[1]}`;
48349
48343
  continue;
48350
48344
  }
@@ -48354,89 +48348,82 @@ function waypointsToSvgPath(waypoints, cornerRadius) {
48354
48348
  const lenNext = Math.sqrt(dNext[0] * dNext[0] + dNext[1] * dNext[1]);
48355
48349
  const uPrev = [dPrev[0] / lenPrev, dPrev[1] / lenPrev];
48356
48350
  const uNext = [dNext[0] / lenNext, dNext[1] / lenNext];
48357
- const arcStart = [curr[0] + uPrev[0] * r, curr[1] + uPrev[1] * r];
48358
- const arcEnd = [curr[0] + uNext[0] * r, curr[1] + uNext[1] * r];
48359
- const cross = dPrev[0] * dNext[1] - dPrev[1] * dNext[0];
48351
+ const cross = uPrev[0] * uNext[1] - uPrev[1] * uNext[0];
48352
+ const deflection = Math.abs(cross);
48353
+ const scaledR = r * deflection;
48354
+ if (scaledR < 0.5) {
48355
+ path54 += ` L ${curr[0]},${curr[1]}`;
48356
+ continue;
48357
+ }
48358
+ const arcStart = [curr[0] + uPrev[0] * scaledR, curr[1] + uPrev[1] * scaledR];
48359
+ const arcEnd = [curr[0] + uNext[0] * scaledR, curr[1] + uNext[1] * scaledR];
48360
48360
  const sweep = cross > 0 ? 0 : 1;
48361
48361
  path54 += ` L ${arcStart[0]},${arcStart[1]}`;
48362
- path54 += ` A ${r} ${r} 0 0 ${sweep} ${arcEnd[0]},${arcEnd[1]}`;
48362
+ path54 += ` A ${scaledR} ${scaledR} 0 0 ${sweep} ${arcEnd[0]},${arcEnd[1]}`;
48363
48363
  }
48364
48364
  const last = waypoints[waypoints.length - 1];
48365
48365
  path54 += ` L ${last[0]},${last[1]}`;
48366
48366
  return path54;
48367
48367
  }
48368
- function computeWaypoints(from, to, nodeBoxes, sourceNodeId, targetNodeId, padding, exitStub, entryStub, allocator) {
48369
- const isSelfConnection = sourceNodeId === targetNodeId;
48370
- const inflatedBoxes = nodeBoxes.filter((box) => isSelfConnection ? true : box.id !== sourceNodeId && box.id !== targetNodeId).map((box) => inflateBox(box, padding));
48368
+ function computeWaypoints(from, to, nodeBoxes, sourceNodeId, targetNodeId, padding, exitStub, entryStub, scopedInternal, allocator) {
48369
+ const isSelfConnection = sourceNodeId === targetNodeId && !scopedInternal;
48370
+ const inflatedBoxes = nodeBoxes.map((box) => inflateBox(box, padding));
48371
48371
  const stubExit = [from[0] + exitStub, from[1]];
48372
48372
  const stubEntry = [to[0] - entryStub, to[1]];
48373
48373
  const xMin = Math.min(stubExit[0], stubEntry[0]);
48374
48374
  const xMax = Math.max(stubExit[0], stubEntry[0]);
48375
- if (!isSelfConnection && to[0] > from[0]) {
48376
- let candidateY = (from[1] + to[1]) / 2;
48377
- const intermediateBoxes = inflatedBoxes.filter(
48378
- (box) => box.left < xMax && box.right > xMin
48379
- );
48375
+ if (!isSelfConnection && to[0] >= from[0] - exitStub) {
48376
+ let candidateY = (from[1] + to[1]) / 2 + BELOW_BIAS;
48377
+ const intermediateBoxes = inflatedBoxes.filter((box) => box.left < xMax && box.right > xMin);
48380
48378
  if (intermediateBoxes.length >= 2) {
48381
48379
  const clusterTop = Math.min(...intermediateBoxes.map((b) => b.top));
48382
48380
  const clusterBottom = Math.max(...intermediateBoxes.map((b) => b.bottom));
48383
48381
  if (candidateY > clusterTop && candidateY < clusterBottom) {
48384
48382
  const distToTop = candidateY - clusterTop;
48385
48383
  const distToBottom = clusterBottom - candidateY;
48386
- candidateY = distToTop <= distToBottom ? clusterTop - padding : clusterBottom + padding;
48384
+ candidateY = distToTop < distToBottom ? clusterTop - padding : clusterBottom + padding;
48387
48385
  }
48388
48386
  }
48389
- let clearY = findClearY(xMin, xMax, candidateY, inflatedBoxes);
48390
- if (Math.abs(from[1] - to[1]) < JOG_THRESHOLD && Math.abs(clearY - from[1]) < JOG_THRESHOLD) {
48391
- return null;
48392
- }
48393
- const midX = (stubExit[0] + stubEntry[0]) / 2;
48387
+ const clearY = allocator ? allocator.claim(xMin, xMax, findClearY(xMin, xMax, candidateY, inflatedBoxes)) : findClearY(xMin, xMax, candidateY, inflatedBoxes);
48394
48388
  const yMin = Math.min(from[1], to[1]);
48395
48389
  const yMax = Math.max(from[1], to[1]);
48396
- const clearMidX = findClearX(yMin, yMax, midX, inflatedBoxes);
48397
- const freeMidX = allocator.findFreeX(yMin, yMax, clearMidX, inflatedBoxes);
48398
- if (yMax - yMin >= JOG_THRESHOLD && freeMidX > stubExit[0] && freeMidX < stubEntry[0] && verticalSegmentClear(freeMidX, yMin, yMax, inflatedBoxes) && allocator.findFreeY(from[0], freeMidX, from[1], inflatedBoxes) === from[1] && allocator.findFreeY(freeMidX, to[0], to[1], inflatedBoxes) === to[1]) {
48399
- allocator.claim(from[0], freeMidX, from[1]);
48400
- allocator.claim(freeMidX, to[0], to[1]);
48401
- allocator.claimVertical(yMin, yMax, freeMidX);
48390
+ const stubsCross = stubExit[0] >= stubEntry[0];
48391
+ const midX = stubsCross ? (from[0] + to[0]) / 2 : stubExit[0] + (stubEntry[0] - stubExit[0]) * 0.75;
48392
+ const freeMidX = findClearX(yMin, yMax || yMin + 1, midX, inflatedBoxes);
48393
+ const lShapeXValid = stubsCross ? freeMidX >= Math.min(from[0], to[0]) && freeMidX <= Math.max(from[0], to[0]) : freeMidX > stubExit[0] && freeMidX < stubEntry[0];
48394
+ if (lShapeXValid && verticalSegmentClear(freeMidX, yMin, yMax, inflatedBoxes) && !inflatedBoxes.some((box) => segmentOverlapsBox(stubExit[0], freeMidX, from[1], box)) && !inflatedBoxes.some((box) => segmentOverlapsBox(freeMidX, stubEntry[0], to[1], box))) {
48402
48395
  return simplifyWaypoints([from, [freeMidX, from[1]], [freeMidX, to[1]], to]);
48403
48396
  }
48404
- clearY = allocator.findFreeY(xMin, xMax, clearY, inflatedBoxes);
48405
48397
  if (Math.abs(clearY - from[1]) < JOG_THRESHOLD && !inflatedBoxes.some((box) => segmentOverlapsBox(xMin, xMax, from[1], box))) {
48406
- clearY = from[1];
48398
+ candidateY = from[1];
48407
48399
  } else if (Math.abs(clearY - to[1]) < JOG_THRESHOLD && !inflatedBoxes.some((box) => segmentOverlapsBox(xMin, xMax, to[1], box))) {
48408
- clearY = to[1];
48400
+ candidateY = to[1];
48401
+ } else {
48402
+ candidateY = clearY;
48409
48403
  }
48410
- allocator.claim(xMin, xMax, clearY);
48411
- const exitYMin = Math.min(from[1], clearY);
48412
- const exitYMax = Math.max(from[1], clearY);
48404
+ const exitYMin = Math.min(from[1], candidateY);
48405
+ const exitYMax = Math.max(from[1], candidateY);
48413
48406
  let exitX = findClearX(exitYMin, exitYMax, stubExit[0], inflatedBoxes);
48414
- exitX = allocator.findFreeX(exitYMin, exitYMax, exitX, inflatedBoxes);
48415
48407
  if (exitX < from[0]) {
48416
48408
  exitX = stubExit[0];
48417
48409
  if (!verticalSegmentClear(exitX, exitYMin, exitYMax, inflatedBoxes)) {
48418
48410
  exitX = findClearX(exitYMin, exitYMax, stubExit[0] + TRACK_SPACING, inflatedBoxes);
48419
- exitX = allocator.findFreeX(exitYMin, exitYMax, exitX, inflatedBoxes);
48420
48411
  }
48421
48412
  }
48422
- allocator.claimVertical(exitYMin, exitYMax, exitX);
48423
- const entryYMin = Math.min(to[1], clearY);
48424
- const entryYMax = Math.max(to[1], clearY);
48413
+ const entryYMin = Math.min(to[1], candidateY);
48414
+ const entryYMax = Math.max(to[1], candidateY);
48425
48415
  let entryX = findClearX(entryYMin, entryYMax, stubEntry[0], inflatedBoxes);
48426
- entryX = allocator.findFreeX(entryYMin, entryYMax, entryX, inflatedBoxes);
48427
48416
  if (entryX > to[0]) {
48428
48417
  entryX = stubEntry[0];
48429
48418
  if (!verticalSegmentClear(entryX, entryYMin, entryYMax, inflatedBoxes)) {
48430
48419
  entryX = findClearX(entryYMin, entryYMax, stubEntry[0] - TRACK_SPACING, inflatedBoxes);
48431
- entryX = allocator.findFreeX(entryYMin, entryYMax, entryX, inflatedBoxes);
48432
48420
  }
48433
48421
  }
48434
- allocator.claimVertical(entryYMin, entryYMax, entryX);
48435
48422
  return simplifyWaypoints([
48436
48423
  from,
48437
48424
  [exitX, from[1]],
48438
- [exitX, clearY],
48439
- [entryX, clearY],
48425
+ [exitX, candidateY],
48426
+ [entryX, candidateY],
48440
48427
  [entryX, to[1]],
48441
48428
  to
48442
48429
  ]);
@@ -48456,23 +48443,17 @@ function computeWaypoints(from, to, nodeBoxes, sourceNodeId, targetNodeId, paddi
48456
48443
  }
48457
48444
  const maxBottom = Math.max(...bottoms, from[1] + 50, to[1] + 50);
48458
48445
  const minTop = Math.min(...tops, from[1] - 50, to[1] - 50);
48459
- const avgY = (from[1] + to[1]) / 2;
48446
+ const avgY = (from[1] + to[1]) / 2 + BELOW_BIAS;
48460
48447
  const escapeBelow = maxBottom + padding;
48461
48448
  const escapeAbove = minTop - padding;
48462
- let escapeY = Math.abs(escapeAbove - avgY) <= Math.abs(escapeBelow - avgY) ? escapeAbove : escapeBelow;
48463
- escapeY = findClearY(xMin, xMax, escapeY, inflatedBoxes);
48464
- escapeY = allocator.findFreeY(xMin, xMax, escapeY, inflatedBoxes);
48465
- allocator.claim(xMin, xMax, escapeY);
48449
+ let escapeY = Math.abs(escapeAbove - avgY) < Math.abs(escapeBelow - avgY) ? escapeAbove : escapeBelow;
48450
+ escapeY = allocator ? allocator.claim(xMin, xMax, findClearY(xMin, xMax, escapeY, inflatedBoxes)) : findClearY(xMin, xMax, escapeY, inflatedBoxes);
48466
48451
  const bwExitYMin = Math.min(from[1], escapeY);
48467
48452
  const bwExitYMax = Math.max(from[1], escapeY);
48468
- let bwExitX = findClearX(bwExitYMin, bwExitYMax, stubExit[0], inflatedBoxes);
48469
- bwExitX = allocator.findFreeX(bwExitYMin, bwExitYMax, bwExitX, inflatedBoxes);
48470
- allocator.claimVertical(bwExitYMin, bwExitYMax, bwExitX);
48453
+ const bwExitX = findClearX(bwExitYMin, bwExitYMax, stubExit[0], inflatedBoxes);
48471
48454
  const bwEntryYMin = Math.min(to[1], escapeY);
48472
48455
  const bwEntryYMax = Math.max(to[1], escapeY);
48473
- let bwEntryX = findClearX(bwEntryYMin, bwEntryYMax, stubEntry[0], inflatedBoxes);
48474
- bwEntryX = allocator.findFreeX(bwEntryYMin, bwEntryYMax, bwEntryX, inflatedBoxes);
48475
- allocator.claimVertical(bwEntryYMin, bwEntryYMax, bwEntryX);
48456
+ const bwEntryX = findClearX(bwEntryYMin, bwEntryYMax, stubEntry[0], inflatedBoxes);
48476
48457
  return simplifyWaypoints([
48477
48458
  from,
48478
48459
  [bwExitX, from[1]],
@@ -48483,7 +48464,7 @@ function computeWaypoints(from, to, nodeBoxes, sourceNodeId, targetNodeId, paddi
48483
48464
  ]);
48484
48465
  }
48485
48466
  }
48486
- function calculateOrthogonalPath(from, to, nodeBoxes, sourceNodeId, targetNodeId, options, allocator) {
48467
+ function calculateOrthogonalPath(from, to, nodeBoxes, sourceNodeId, targetNodeId, options) {
48487
48468
  const cornerRadius = options?.cornerRadius ?? 10;
48488
48469
  const padding = options?.padding ?? 15;
48489
48470
  const stubLength = options?.stubLength ?? 20;
@@ -48493,7 +48474,6 @@ function calculateOrthogonalPath(from, to, nodeBoxes, sourceNodeId, targetNodeId
48493
48474
  const toPortIndex = options?.toPortIndex ?? 0;
48494
48475
  const exitStub = Math.min(stubLength + fromPortIndex * stubSpacing, maxStubLength);
48495
48476
  const entryStub = Math.min(stubLength + toPortIndex * stubSpacing, maxStubLength);
48496
- const alloc = allocator ?? new TrackAllocator();
48497
48477
  const waypoints = computeWaypoints(
48498
48478
  from,
48499
48479
  to,
@@ -48503,167 +48483,53 @@ function calculateOrthogonalPath(from, to, nodeBoxes, sourceNodeId, targetNodeId
48503
48483
  padding,
48504
48484
  exitStub,
48505
48485
  entryStub,
48506
- alloc
48486
+ options?.scopedInternal,
48487
+ options?.allocator
48507
48488
  );
48508
48489
  if (!waypoints) return null;
48509
48490
  return waypointsToSvgPath(waypoints, cornerRadius);
48510
48491
  }
48511
- function calculateOrthogonalPathSafe(from, to, nodeBoxes, sourceNodeId, targetNodeId, options, allocator) {
48492
+ function calculateOrthogonalPathSafe(from, to, nodeBoxes, sourceNodeId, targetNodeId, options) {
48512
48493
  try {
48513
- const path54 = calculateOrthogonalPath(
48514
- from,
48515
- to,
48516
- nodeBoxes,
48517
- sourceNodeId,
48518
- targetNodeId,
48519
- options,
48520
- allocator
48521
- );
48494
+ const path54 = calculateOrthogonalPath(from, to, nodeBoxes, sourceNodeId, targetNodeId, options);
48522
48495
  if (!path54 || path54.length < 5) return null;
48523
48496
  return path54;
48524
48497
  } catch {
48525
48498
  return null;
48526
48499
  }
48527
48500
  }
48528
- var TRACK_SPACING, EDGE_OFFSET, MAX_CANDIDATES, TrackAllocator, MIN_SEGMENT_LENGTH, JOG_THRESHOLD;
48501
+ var TRACK_SPACING, EDGE_OFFSET, BELOW_BIAS, TrackAllocator, MIN_SEGMENT_LENGTH, JOG_THRESHOLD;
48529
48502
  var init_orthogonal_router = __esm({
48530
48503
  "src/diagram/orthogonal-router.ts"() {
48531
48504
  "use strict";
48532
48505
  TRACK_SPACING = 15;
48533
48506
  EDGE_OFFSET = 5;
48534
- MAX_CANDIDATES = 5;
48507
+ BELOW_BIAS = 40;
48535
48508
  TrackAllocator = class {
48536
- claims = [];
48537
- verticalClaims = [];
48538
- /** Check if a Y value is too close to any claimed segment whose X range overlaps. */
48539
- isOccupied(xMin, xMax, y) {
48540
- for (const c of this.claims) {
48541
- if (c.xMin < xMax && c.xMax > xMin && Math.abs(c.y - y) < TRACK_SPACING) {
48542
- return true;
48543
- }
48544
- }
48545
- return false;
48546
- }
48547
- /** Check if an X value is too close to any claimed vertical segment whose Y range overlaps. */
48548
- isOccupiedVertical(yMin, yMax, x2) {
48549
- for (const c of this.verticalClaims) {
48550
- if (c.yMin < yMax && c.yMax > yMin && Math.abs(c.x - x2) < TRACK_SPACING) {
48551
- return true;
48552
- }
48553
- }
48554
- return false;
48555
- }
48556
- /** Check if a horizontal segment at Y passes through any inflated node box. */
48557
- isBlockedByNode(xMin, xMax, y, boxes) {
48558
- for (const box of boxes) {
48559
- if (xMin < box.right && xMax > box.left && y >= box.top && y <= box.bottom) {
48560
- return true;
48561
- }
48562
- }
48563
- return false;
48564
- }
48565
- /** Check if a vertical segment at X passes through any inflated node box. */
48566
- isBlockedByNodeVertical(yMin, yMax, x2, boxes) {
48567
- for (const box of boxes) {
48568
- if (x2 >= box.left && x2 <= box.right && yMin < box.bottom && yMax > box.top) {
48569
- return true;
48570
- }
48571
- }
48572
- return false;
48573
- }
48574
- /** Count claimed vertical segments that a horizontal segment at Y would cross. */
48575
- countHorizontalCrossings(xMin, xMax, y) {
48576
- let count = 0;
48577
- for (const c of this.verticalClaims) {
48578
- if (c.x > xMin && c.x < xMax && y >= c.yMin && y <= c.yMax) {
48579
- count++;
48580
- }
48581
- }
48582
- return count;
48583
- }
48584
- /** Count claimed horizontal segments that a vertical segment at X would cross. */
48585
- countVerticalCrossings(yMin, yMax, x2) {
48586
- let count = 0;
48587
- for (const c of this.claims) {
48588
- if (c.y > yMin && c.y < yMax && x2 >= c.xMin && x2 <= c.xMax) {
48589
- count++;
48590
- }
48591
- }
48592
- return count;
48593
- }
48594
- /**
48595
- * Find the nearest free Y to candidateY in the given X range, preferring fewer crossings.
48596
- * When nodeBoxes is provided, candidates inside inflated node boxes are rejected (hard constraint).
48597
- */
48598
- findFreeY(xMin, xMax, candidateY, nodeBoxes) {
48599
- const isFree = (y) => !this.isOccupied(xMin, xMax, y) && (!nodeBoxes || !this.isBlockedByNode(xMin, xMax, y, nodeBoxes));
48600
- if (isFree(candidateY)) return candidateY;
48601
- const candidates = [];
48602
- for (let offset = TRACK_SPACING; offset < 800 && candidates.length < MAX_CANDIDATES * 2; offset += TRACK_SPACING) {
48603
- const above = candidateY - offset;
48604
- if (isFree(above)) {
48605
- candidates.push({ y: above, dist: offset });
48606
- }
48607
- const below = candidateY + offset;
48608
- if (isFree(below)) {
48609
- candidates.push({ y: below, dist: offset });
48610
- }
48611
- }
48612
- if (candidates.length === 0) return candidateY;
48613
- let bestY = candidates[0].y;
48614
- let bestCrossings = this.countHorizontalCrossings(xMin, xMax, candidates[0].y);
48615
- let bestDist = candidates[0].dist;
48616
- for (let i = 1; i < candidates.length; i++) {
48617
- const c = candidates[i];
48618
- const crossings = this.countHorizontalCrossings(xMin, xMax, c.y);
48619
- if (crossings < bestCrossings || crossings === bestCrossings && c.dist < bestDist) {
48620
- bestY = c.y;
48621
- bestCrossings = crossings;
48622
- bestDist = c.dist;
48623
- }
48624
- }
48625
- return bestY;
48626
- }
48627
- /**
48628
- * Find the nearest free X to candidateX in the given Y range, preferring fewer crossings.
48629
- * When nodeBoxes is provided, candidates inside inflated node boxes are rejected (hard constraint).
48630
- */
48631
- findFreeX(yMin, yMax, candidateX, nodeBoxes) {
48632
- const isFree = (x2) => !this.isOccupiedVertical(yMin, yMax, x2) && (!nodeBoxes || !this.isBlockedByNodeVertical(yMin, yMax, x2, nodeBoxes));
48633
- if (isFree(candidateX)) return candidateX;
48634
- const candidates = [];
48635
- for (let offset = TRACK_SPACING; offset < 800 && candidates.length < MAX_CANDIDATES * 2; offset += TRACK_SPACING) {
48636
- const left = candidateX - offset;
48637
- if (isFree(left)) {
48638
- candidates.push({ x: left, dist: offset });
48639
- }
48640
- const right = candidateX + offset;
48641
- if (isFree(right)) {
48642
- candidates.push({ x: right, dist: offset });
48643
- }
48644
- }
48645
- if (candidates.length === 0) return candidateX;
48646
- let bestX = candidates[0].x;
48647
- let bestCrossings = this.countVerticalCrossings(yMin, yMax, candidates[0].x);
48648
- let bestDist = candidates[0].dist;
48649
- for (let i = 1; i < candidates.length; i++) {
48650
- const c = candidates[i];
48651
- const crossings = this.countVerticalCrossings(yMin, yMax, c.x);
48652
- if (crossings < bestCrossings || crossings === bestCrossings && c.dist < bestDist) {
48653
- bestX = c.x;
48654
- bestCrossings = crossings;
48655
- bestDist = c.dist;
48509
+ claims = /* @__PURE__ */ new Map();
48510
+ claim(xMin, xMax, candidateY) {
48511
+ const snap = (y) => Math.round(y / TRACK_SPACING) * TRACK_SPACING;
48512
+ const overlaps = (slot) => {
48513
+ const intervals = this.claims.get(slot);
48514
+ if (!intervals) return false;
48515
+ return intervals.some(([a, b]) => xMin < b && xMax > a);
48516
+ };
48517
+ const take = (slot) => {
48518
+ if (!this.claims.has(slot)) this.claims.set(slot, []);
48519
+ const slotClaims = this.claims.get(slot);
48520
+ if (slotClaims) slotClaims.push([xMin, xMax]);
48521
+ return slot;
48522
+ };
48523
+ const base = snap(candidateY);
48524
+ for (let i = 0; i < 60; i++) {
48525
+ const below = base + i * TRACK_SPACING;
48526
+ if (!overlaps(below)) return take(below);
48527
+ if (i > 0) {
48528
+ const above = base - i * TRACK_SPACING;
48529
+ if (!overlaps(above)) return take(above);
48656
48530
  }
48657
48531
  }
48658
- return bestX;
48659
- }
48660
- /** Claim a horizontal segment so later connections avoid it. */
48661
- claim(xMin, xMax, y) {
48662
- this.claims.push({ xMin, xMax, y });
48663
- }
48664
- /** Claim a vertical segment so later connections avoid it. */
48665
- claimVertical(yMin, yMax, x2) {
48666
- this.verticalClaims.push({ yMin, yMax, x: x2 });
48532
+ return candidateY;
48667
48533
  }
48668
48534
  };
48669
48535
  MIN_SEGMENT_LENGTH = 3;
@@ -48708,49 +48574,8 @@ function positionPortList(ports, cx, nodeY, _nodeHeight) {
48708
48574
  ports[i].cy = nodeY + PORT_PADDING_Y + i * (PORT_SIZE + PORT_GAP) + PORT_SIZE / 2;
48709
48575
  }
48710
48576
  }
48711
- function quadCurveControl(ax, ay, bx, by, ux, uy) {
48712
- const dn = Math.abs(ay - by);
48713
- const cx = bx + ux * dn / Math.abs(uy);
48714
- const cy = ay;
48715
- return [cx, cy];
48716
- }
48717
48577
  function computeConnectionPath(sx, sy, tx, ty) {
48718
- const e = 1e-4;
48719
- const ax = sx + e;
48720
- const ay = sy + e;
48721
- const hx = tx - e;
48722
- const hy = ty - e;
48723
- const ramp = Math.min(20, (hx - ax) / 10);
48724
- const bx = ax + ramp;
48725
- const by = ay + e;
48726
- const gx = hx - ramp;
48727
- const gy = hy - e;
48728
- const curveSizeX = Math.min(60, Math.abs(ax - hx) / 4);
48729
- const curveSizeY = Math.min(60, Math.abs(ay - hy) / 4);
48730
- const curveMag = Math.sqrt(curveSizeX * curveSizeX + curveSizeY * curveSizeY);
48731
- const bgX = gx - bx;
48732
- const bgY = gy - by;
48733
- const bgLen = Math.sqrt(bgX * bgX + bgY * bgY);
48734
- const bgUx = bgX / bgLen;
48735
- const bgUy = bgY / bgLen;
48736
- const dx = bx + bgUx * curveMag;
48737
- const dy = by + bgUy * curveMag / 2;
48738
- const ex = gx - bgUx * curveMag;
48739
- const ey = gy - bgUy * curveMag / 2;
48740
- const deX = ex - dx;
48741
- const deY = ey - dy;
48742
- const deLen = Math.sqrt(deX * deX + deY * deY);
48743
- const deUx = deX / deLen;
48744
- const deUy = deY / deLen;
48745
- const [cx, cy] = quadCurveControl(bx, by, dx, dy, -deUx, -deUy);
48746
- const [fx, fy] = quadCurveControl(gx, gy, ex, ey, deUx, deUy);
48747
- let path54 = `M ${cx},${cy} M ${ax},${ay}`;
48748
- path54 += ` L ${bx},${by}`;
48749
- path54 += ` Q ${cx},${cy} ${dx},${dy}`;
48750
- path54 += ` L ${ex},${ey}`;
48751
- path54 += ` Q ${fx},${fy} ${gx},${gy}`;
48752
- path54 += ` L ${hx},${hy}`;
48753
- return path54;
48578
+ return `M ${sx},${sy} L ${tx},${ty}`;
48754
48579
  }
48755
48580
  function orderedPorts(ports, direction) {
48756
48581
  const cloned = {};
@@ -49510,22 +49335,22 @@ function buildDiagramGraph(ast, options = {}) {
49510
49335
  color: targetColor,
49511
49336
  dashed
49512
49337
  };
49338
+ const isStaticSvg = options.format !== "html";
49339
+ const pathSx = isStaticSvg ? sx + srcLabelEnd : sx;
49340
+ const pathTx = isStaticSvg ? tx - tgtLabelEnd : tx;
49513
49341
  let path54;
49514
49342
  if (xDistance > STUB_DISTANCE_THRESHOLD) {
49515
49343
  path54 = "";
49516
- } else if (!useCurve && distance > ORTHOGONAL_DISTANCE_THRESHOLD) {
49344
+ } else {
49517
49345
  const orthoPath = calculateOrthogonalPathSafe(
49518
- [sx, sy],
49519
- [tx, ty],
49346
+ [pathSx, sy],
49347
+ [pathTx, ty],
49520
49348
  nodeBoxes,
49521
49349
  pc3.fromNodeId,
49522
49350
  pc3.toNodeId,
49523
- { fromPortIndex: pc3.fromPortIndex, toPortIndex: pc3.toPortIndex },
49524
- allocator
49351
+ { fromPortIndex: pc3.fromPortIndex, toPortIndex: pc3.toPortIndex, allocator }
49525
49352
  );
49526
- path54 = orthoPath ?? computeConnectionPath(sx, sy, tx, ty);
49527
- } else {
49528
- path54 = computeConnectionPath(sx, sy, tx, ty);
49353
+ path54 = orthoPath ?? computeConnectionPath(pathSx, sy, pathTx, ty);
49529
49354
  }
49530
49355
  connections.push({
49531
49356
  fromNode: pc3.fromNodeId,
@@ -49615,20 +49440,15 @@ function buildDiagramGraph(ast, options = {}) {
49615
49440
  const ddy = ty - sy;
49616
49441
  const dist = Math.sqrt(ddx * ddx + ddy * ddy);
49617
49442
  const useCurve = spForceCurveSet.has(sp);
49618
- if (!useCurve && dist > ORTHOGONAL_DISTANCE_THRESHOLD) {
49619
- const orthoPath = calculateOrthogonalPathSafe(
49620
- [sx, sy],
49621
- [tx, ty],
49622
- nodeBoxes,
49623
- sp.fromNodeId,
49624
- sp.toNodeId,
49625
- { fromPortIndex: sp.fromPortIndex, toPortIndex: sp.toPortIndex },
49626
- allocator
49627
- );
49628
- sp.conn.path = orthoPath ?? computeConnectionPath(sx, sy, tx, ty);
49629
- } else {
49630
- sp.conn.path = computeConnectionPath(sx, sy, tx, ty);
49631
- }
49443
+ const orthoPath = calculateOrthogonalPathSafe(
49444
+ [sx, sy],
49445
+ [tx, ty],
49446
+ nodeBoxes,
49447
+ sp.fromNodeId,
49448
+ sp.toNodeId,
49449
+ { fromPortIndex: sp.fromPortIndex, toPortIndex: sp.toPortIndex, allocator }
49450
+ );
49451
+ sp.conn.path = orthoPath ?? computeConnectionPath(sx, sy, tx, ty);
49632
49452
  }
49633
49453
  }
49634
49454
  let originX = 0;
@@ -49741,11 +49561,11 @@ var init_geometry = __esm({
49741
49561
  LABEL_CLEARANCE = 42;
49742
49562
  MIN_EDGE_GAP = 112;
49743
49563
  NODE_GAP_Y = 60;
49744
- LABEL_HEIGHT = 20;
49564
+ LABEL_HEIGHT = 24;
49745
49565
  LABEL_GAP = 12;
49746
49566
  SCOPE_PADDING_X = 140;
49747
49567
  SCOPE_PADDING_Y = 40;
49748
- SCOPE_PORT_COLUMN = 50;
49568
+ SCOPE_PORT_COLUMN = 45;
49749
49569
  SCOPE_INNER_GAP_X = 240;
49750
49570
  ORTHOGONAL_DISTANCE_THRESHOLD = 300;
49751
49571
  STUB_DISTANCE_THRESHOLD = 500;
@@ -50646,6 +50466,15 @@ var init_describe = __esm({
50646
50466
  function escapeXml(str2) {
50647
50467
  return str2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
50648
50468
  }
50469
+ function resolveIconColor(nodeColor, themeName, theme) {
50470
+ if (nodeColor === NODE_DEFAULT_COLOR) return theme.nodeIconColor;
50471
+ for (const v2 of Object.values(NODE_VARIANT_COLORS)) {
50472
+ if (v2.darkBorder === nodeColor || v2.border === nodeColor) {
50473
+ return themeName === "dark" ? v2.darkIcon : v2.icon;
50474
+ }
50475
+ }
50476
+ return nodeColor;
50477
+ }
50649
50478
  function collectAllConnections(graph) {
50650
50479
  const all = [...graph.connections];
50651
50480
  for (const node of graph.nodes) {
@@ -50672,21 +50501,18 @@ function renderSVG(graph, options = {}) {
50672
50501
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vbX} ${vbY} ${vbWidth} ${vbHeight}" width="${svgWidth}" height="${svgHeight}">`
50673
50502
  );
50674
50503
  parts.push(`<style>`);
50675
- parts.push(` text { font-family: Montserrat, 'Segoe UI', Roboto, sans-serif; }`);
50676
- parts.push(` .node-label { font-size: 13px; font-weight: 700; fill: ${theme.labelColor}; }`);
50504
+ parts.push(` text { font-family: Montserrat, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }`);
50505
+ parts.push(` .node-label { font-size: 18px; font-weight: 600; fill: ${theme.labelColor}; }`);
50677
50506
  parts.push(` .port-label { font-size: 10px; font-weight: 600; fill: ${theme.labelColor}; }`);
50678
50507
  parts.push(` .port-type-label { font-size: 10px; font-weight: 600; }`);
50679
50508
  parts.push(`</style>`);
50680
50509
  parts.push(`<defs>`);
50681
50510
  parts.push(` <pattern id="dot-grid" width="20" height="20" patternUnits="userSpaceOnUse">`);
50682
- parts.push(` <circle cx="10" cy="10" r="1.5" fill="${theme.dotColor}" opacity="${theme.dotOpacity}"/>`);
50511
+ parts.push(` <circle cx="10" cy="10" r="0.75" fill="${theme.dotColor}" opacity="${theme.dotOpacity}"/>`);
50683
50512
  parts.push(` </pattern>`);
50684
- parts.push(` <filter id="node-shadow" x="-10%" y="-10%" width="130%" height="140%">`);
50685
- parts.push(` <feDropShadow dx="0" dy="2" stdDeviation="4" flood-opacity="${theme.nodeShadowOpacity}" flood-color="#000"/>`);
50686
- parts.push(` </filter>`);
50687
50513
  for (let i = 0; i < allConnections.length; i++) {
50688
50514
  const conn = allConnections[i];
50689
- parts.push(` <linearGradient id="conn-grad-${i}" x1="0%" y1="0%" x2="100%" y2="0%">`);
50515
+ parts.push(` <linearGradient id="conn-grad-${i}" gradientUnits="userSpaceOnUse" x1="${vbX}" y1="0" x2="${vbX + vbWidth}" y2="0">`);
50690
50516
  parts.push(` <stop offset="0%" stop-color="${conn.sourceColor}"/>`);
50691
50517
  parts.push(` <stop offset="100%" stop-color="${conn.targetColor}"/>`);
50692
50518
  parts.push(` </linearGradient>`);
@@ -50728,13 +50554,6 @@ function renderSVG(graph, options = {}) {
50728
50554
  }
50729
50555
  }
50730
50556
  parts.push(`</g>`);
50731
- const wmX = vbX + vbWidth - 16;
50732
- const wmY = vbY + vbHeight - 14;
50733
- const wmBrand = themeName === "dark" ? "#8e9eff" : "#5468ff";
50734
- parts.push(`<g opacity="0.5">`);
50735
- parts.push(` <svg x="${wmX - 118}" y="${wmY - 18}" width="22" height="22" viewBox="0 0 256 256" fill="none"><path d="M80 128C134 128 122 49 176 49" stroke="${wmBrand}" stroke-width="14" stroke-linecap="round"/><path d="M80 128C134 128 122 207 176 207" stroke="${wmBrand}" stroke-width="14" stroke-linecap="round"/><rect x="28" y="102" width="52" height="52" rx="10" stroke="${wmBrand}" stroke-width="14"/><rect x="176" y="23" width="52" height="52" rx="10" stroke="${wmBrand}" stroke-width="14"/><rect x="176" y="181" width="52" height="52" rx="10" stroke="${wmBrand}" stroke-width="14"/></svg>`);
50736
- parts.push(` <text x="${wmX}" y="${wmY}" text-anchor="end" font-size="14" font-weight="700" fill="${wmBrand}" font-family="Montserrat, 'Segoe UI', Roboto, sans-serif">Flow Weaver</text>`);
50737
- parts.push(`</g>`);
50738
50557
  parts.push(`</svg>`);
50739
50558
  return parts.join("\n");
50740
50559
  }
@@ -50743,7 +50562,7 @@ function renderConnection(parts, conn, gradIndex, hidden = false) {
50743
50562
  const displayAttr = hidden ? ' display="none"' : "";
50744
50563
  const pathD = conn.path || "M0,0";
50745
50564
  parts.push(
50746
- ` <path d="${pathD}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"${displayAttr}/>`
50565
+ ` <path d="${pathD}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="1"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"${displayAttr}/>`
50747
50566
  );
50748
50567
  }
50749
50568
  function renderStub(parts, stub, conn, hidden = false) {
@@ -50763,17 +50582,17 @@ function renderScopeConnection(parts, conn, allConnections, parentNodeId) {
50763
50582
  if (gradIndex < 0) return;
50764
50583
  const dashAttr = conn.isStepConnection ? "" : ' stroke-dasharray="8 4"';
50765
50584
  parts.push(
50766
- ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input" data-scope="${escapeXml(parentNodeId)}"/>`
50585
+ ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="1"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input" data-scope="${escapeXml(parentNodeId)}"/>`
50767
50586
  );
50768
50587
  }
50769
- function renderNodeBody(parts, node, theme, indent) {
50588
+ function renderNodeBody(parts, node, theme, themeName, indent) {
50770
50589
  const strokeColor = node.color !== NODE_DEFAULT_COLOR ? node.color : theme.nodeIconColor;
50771
50590
  parts.push(
50772
- `${indent}<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="${BORDER_RADIUS}" fill="${theme.nodeFill}" stroke="${strokeColor}" stroke-width="2" filter="url(#node-shadow)"/>`
50591
+ `${indent}<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="${BORDER_RADIUS}" fill="${theme.background}" stroke="${strokeColor}" stroke-width="2"/>`
50773
50592
  );
50774
50593
  const iconPath = NODE_ICON_PATHS[node.icon] ?? NODE_ICON_PATHS.code;
50775
- const iconColor = node.color !== NODE_DEFAULT_COLOR ? node.color : theme.nodeIconColor;
50776
- const iconSize = 40;
50594
+ const iconColor = resolveIconColor(node.color, themeName, theme);
50595
+ const iconSize = 50;
50777
50596
  const iconX = node.x + (node.width - iconSize) / 2;
50778
50597
  const iconY = node.y + (node.height - iconSize) / 2;
50779
50598
  parts.push(
@@ -50786,13 +50605,13 @@ function renderNode(node, theme, themeName, allConnections) {
50786
50605
  if (node.scopeChildren && node.scopeChildren.length > 0) {
50787
50606
  const strokeColor = node.color !== NODE_DEFAULT_COLOR ? node.color : theme.nodeIconColor;
50788
50607
  parts.push(
50789
- ` <rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="${BORDER_RADIUS}" fill="${theme.nodeFill}" stroke="${strokeColor}" stroke-width="2" filter="url(#node-shadow)"/>`
50608
+ ` <rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="${BORDER_RADIUS}" fill="${theme.background}" stroke="${strokeColor}" stroke-width="2"/>`
50790
50609
  );
50791
50610
  renderScopedContent(parts, node, theme, themeName, allConnections);
50792
50611
  } else {
50793
- renderNodeBody(parts, node, theme, " ");
50612
+ renderNodeBody(parts, node, theme, themeName, " ");
50794
50613
  }
50795
- renderPortDots(parts, node.id, node.inputs, node.outputs, themeName);
50614
+ renderPortDots(parts, node.id, node.inputs, node.outputs, themeName, theme);
50796
50615
  parts.push(` </g>`);
50797
50616
  return parts.join("\n");
50798
50617
  }
@@ -50800,38 +50619,45 @@ function renderScopedContent(parts, node, theme, themeName, allConnections) {
50800
50619
  const children = node.scopeChildren;
50801
50620
  const scopePorts = node.scopePorts;
50802
50621
  const scopeX = node.x + SCOPE_PORT_COLUMN;
50803
- const scopeY = node.y + 4;
50804
50622
  const scopeW = node.width - SCOPE_PORT_COLUMN * 2;
50805
- const scopeH = node.height - 8;
50623
+ const scopeRightX = node.x + node.width - SCOPE_PORT_COLUMN;
50624
+ const lineY1 = node.y;
50625
+ const lineY2 = node.y + node.height;
50626
+ const strokeColor = node.color !== NODE_DEFAULT_COLOR ? node.color : theme.nodeIconColor;
50627
+ parts.push(
50628
+ ` <line x1="${scopeX}" y1="${lineY1}" x2="${scopeX}" y2="${lineY2}" stroke="${strokeColor}" stroke-width="2" opacity="0.5"/>`
50629
+ );
50630
+ parts.push(
50631
+ ` <line x1="${scopeRightX}" y1="${lineY1}" x2="${scopeRightX}" y2="${lineY2}" stroke="${strokeColor}" stroke-width="2" opacity="0.5"/>`
50632
+ );
50633
+ parts.push(
50634
+ ` <line x1="${scopeX}" y1="${lineY1 + 2}" x2="${scopeRightX}" y2="${lineY1 + 2}" stroke="${theme.scopeAreaStroke}" stroke-width="1" opacity="0.3"/>`
50635
+ );
50806
50636
  parts.push(
50807
- ` <rect x="${scopeX}" y="${scopeY}" width="${scopeW}" height="${scopeH}" rx="4" fill="none" stroke="${theme.scopeAreaStroke}" stroke-width="1" stroke-dasharray="4 2" opacity="0.5"/>`
50637
+ ` <line x1="${scopeX}" y1="${lineY2 - 2}" x2="${scopeRightX}" y2="${lineY2 - 2}" stroke="${theme.scopeAreaStroke}" stroke-width="1" opacity="0.3"/>`
50808
50638
  );
50809
50639
  for (const conn of node.scopeConnections ?? []) {
50810
50640
  renderScopeConnection(parts, conn, allConnections, node.id);
50811
50641
  }
50812
50642
  if (scopePorts) {
50813
- renderPortDots(parts, node.id, scopePorts.inputs, scopePorts.outputs, themeName);
50643
+ renderPortDots(parts, node.id, scopePorts.inputs, scopePorts.outputs, themeName, theme);
50814
50644
  }
50815
50645
  for (const child of children) {
50816
50646
  parts.push(` <g data-node-id="${escapeXml(child.id)}">`);
50817
- renderNodeBody(parts, child, theme, " ");
50818
- renderPortDots(parts, child.id, child.inputs, child.outputs, themeName);
50647
+ renderNodeBody(parts, child, theme, themeName, " ");
50648
+ renderPortDots(parts, child.id, child.inputs, child.outputs, themeName, theme);
50819
50649
  parts.push(` </g>`);
50820
50650
  }
50821
50651
  }
50822
50652
  function renderNodeLabel(parts, node, theme) {
50823
50653
  const isScoped = !!(node.scopeChildren && node.scopeChildren.length > 0);
50824
50654
  const labelText = escapeXml(node.label);
50825
- const textWidth = labelText.length * 7;
50826
- const labelBgWidth = textWidth + 16;
50827
- const labelBgHeight = LABEL_HEIGHT;
50828
- const labelBgX = isScoped ? node.x : node.x + node.width / 2 - labelBgWidth / 2;
50829
- const labelBgY = node.y - LABEL_GAP - labelBgHeight;
50830
- const labelTextX = isScoped ? node.x + 8 : node.x + node.width / 2;
50655
+ const labelTextX = isScoped ? node.x + 6 : node.x + node.width / 2;
50656
+ const labelTextY = node.y - LABEL_GAP;
50831
50657
  const labelAnchor = isScoped ? "start" : "middle";
50658
+ const labelColor = node.color !== NODE_DEFAULT_COLOR ? node.color : theme.labelColor;
50832
50659
  parts.push(` <g data-label-for="${escapeXml(node.id)}">`);
50833
- parts.push(` <rect x="${labelBgX}" y="${labelBgY}" width="${labelBgWidth}" height="${labelBgHeight}" rx="6" fill="${theme.labelBadgeFill}" opacity="0.95"/>`);
50834
- parts.push(` <text class="node-label" x="${labelTextX}" y="${labelBgY + labelBgHeight / 2 + 6}" text-anchor="${labelAnchor}" fill="${node.color !== NODE_DEFAULT_COLOR ? node.color : theme.labelColor}">${labelText}</text>`);
50660
+ parts.push(` <text class="node-label" x="${labelTextX}" y="${labelTextY}" text-anchor="${labelAnchor}" fill="${labelColor}">${labelText}</text>`);
50835
50661
  parts.push(` </g>`);
50836
50662
  }
50837
50663
  function renderPortLabelsForNode(parts, node, theme, themeName, showPortLabels) {
@@ -50839,46 +50665,50 @@ function renderPortLabelsForNode(parts, node, theme, themeName, showPortLabels)
50839
50665
  renderPortLabels(parts, node.id, node.inputs, node.outputs, theme, themeName);
50840
50666
  }
50841
50667
  }
50842
- function renderPortDots(parts, nodeId, inputs, outputs, themeName) {
50668
+ function renderPortDots(parts, nodeId, inputs, outputs, themeName, theme) {
50669
+ const barWidth = 2;
50670
+ const barHeight = 14;
50671
+ const ringWidth = 2;
50672
+ const outerW = barWidth + ringWidth * 2;
50673
+ const outerH = barHeight + ringWidth * 2;
50843
50674
  for (const port of [...inputs, ...outputs]) {
50844
50675
  const color = getPortColor(port.dataType, port.isFailure, themeName);
50845
- const ringColor = getPortRingColor(port.dataType, port.isFailure, themeName);
50846
50676
  const dir = port.direction === "INPUT" ? "input" : "output";
50847
- parts.push(` <circle cx="${port.cx}" cy="${port.cy}" r="${PORT_RADIUS}" fill="${color}" stroke="${ringColor}" stroke-width="2" data-port-id="${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}" data-direction="${dir}"/>`);
50677
+ const ox = port.cx - outerW / 2;
50678
+ const oy = port.cy - outerH / 2;
50679
+ const ix = port.cx - barWidth / 2;
50680
+ const iy = port.cy - barHeight / 2;
50681
+ parts.push(` <rect x="${ox}" y="${oy}" width="${outerW}" height="${outerH}" rx="4" fill="${color}" data-port-id="${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}" data-direction="${dir}"/>`);
50682
+ parts.push(` <rect x="${ix}" y="${iy}" width="${barWidth}" height="${barHeight}" rx="2" fill="${theme.background}" pointer-events="none"/>`);
50848
50683
  }
50849
50684
  }
50850
50685
  function renderPortLabels(parts, nodeId, inputs, outputs, theme, themeName) {
50851
50686
  for (const port of [...inputs, ...outputs]) {
50852
50687
  const color = getPortColor(port.dataType, port.isFailure, themeName);
50853
50688
  const isInput = port.direction === "INPUT";
50854
- const abbrev = TYPE_ABBREVIATIONS[port.dataType] ?? port.dataType;
50855
50689
  const dir = isInput ? "input" : "output";
50856
50690
  const portId = `${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}`;
50857
50691
  const portLabel = port.label;
50858
- const typeWidth = measureText(abbrev);
50859
50692
  const labelWidth = measureText(portLabel);
50860
- const pad2 = 7;
50861
- const divGap = 4;
50862
- const badgeWidth = pad2 + typeWidth + divGap + 1 + divGap + labelWidth + pad2;
50693
+ const pad2 = 6;
50694
+ const gap = 4;
50695
+ const abbrev = TYPE_ABBREVIATIONS[port.dataType] ?? port.dataType;
50696
+ const typeWidth = measureText(abbrev);
50697
+ const badgeWidth = pad2 + typeWidth + gap + labelWidth + pad2;
50863
50698
  const badgeHeight = 16;
50864
50699
  const badgeGap = 5;
50865
- const rr = badgeHeight / 2;
50866
50700
  const badgeX = isInput ? port.cx - PORT_RADIUS - badgeGap - badgeWidth : port.cx + PORT_RADIUS + badgeGap;
50867
50701
  const badgeY = port.cy - badgeHeight / 2;
50868
50702
  parts.push(` <g data-port-label="${portId}">`);
50869
- parts.push(` <rect x="${badgeX}" y="${badgeY}" width="${badgeWidth}" height="${badgeHeight}" rx="${rr}" fill="${theme.nodeFill}" stroke="${theme.labelBadgeBorder}" stroke-width="1"/>`);
50703
+ parts.push(` <rect x="${badgeX}" y="${badgeY}" width="${badgeWidth}" height="${badgeHeight}" rx="3" fill="${theme.background}" stroke="${color}" stroke-width="1"/>`);
50870
50704
  if (isInput) {
50871
50705
  const typeX = badgeX + badgeWidth - pad2 - typeWidth / 2;
50872
- const divX = typeX - typeWidth / 2 - divGap;
50873
- const nameX = divX - divGap;
50874
- parts.push(` <line x1="${divX}" y1="${badgeY + 3}" x2="${divX}" y2="${badgeY + badgeHeight - 3}" stroke="${theme.labelBadgeBorder}" stroke-width="1"/>`);
50706
+ const nameX = typeX - typeWidth / 2 - gap;
50875
50707
  parts.push(` <text class="port-label" x="${nameX}" y="${port.cy + 3.5}" text-anchor="end">${escapeXml(portLabel)}</text>`);
50876
50708
  parts.push(` <text class="port-type-label" x="${typeX}" y="${port.cy + 3.5}" text-anchor="middle" fill="${color}">${escapeXml(abbrev)}</text>`);
50877
50709
  } else {
50878
50710
  const typeX = badgeX + pad2 + typeWidth / 2;
50879
- const divX = badgeX + pad2 + typeWidth + divGap;
50880
- const nameX = divX + 1 + divGap;
50881
- parts.push(` <line x1="${divX}" y1="${badgeY + 3}" x2="${divX}" y2="${badgeY + badgeHeight - 3}" stroke="${theme.labelBadgeBorder}" stroke-width="1"/>`);
50711
+ const nameX = typeX + typeWidth / 2 + gap;
50882
50712
  parts.push(` <text class="port-type-label" x="${typeX}" y="${port.cy + 3.5}" text-anchor="middle" fill="${color}">${escapeXml(abbrev)}</text>`);
50883
50713
  parts.push(` <text class="port-label" x="${nameX}" y="${port.cy + 3.5}" text-anchor="start">${escapeXml(portLabel)}</text>`);
50884
50714
  }
@@ -50955,13 +50785,12 @@ body.node-active [data-source].dimmed,
50955
50785
  body.port-active [data-source].dimmed { opacity: 0.1; }
50956
50786
  body.port-hovered [data-source].dimmed { opacity: 0.25; }
50957
50787
 
50958
- /* Port circles are interactive */
50959
- circle[data-port-id] { cursor: pointer; }
50960
- circle[data-port-id]:hover { stroke-width: 3; filter: brightness(1.3); }
50788
+ /* Port indicators are interactive \u2014 expands to circle on hover (matches platform) */
50789
+ [data-port-id] { cursor: pointer; }
50961
50790
 
50962
50791
  /* Port-click highlighting */
50963
50792
  [data-source].highlighted { opacity: 1; }
50964
- circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor); stroke-width: 4; }
50793
+ [data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor); }
50965
50794
 
50966
50795
  /* Node selection glow */
50967
50796
  @keyframes select-pop {
@@ -51166,7 +50995,7 @@ circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor);
51166
50995
  <svg id="canvas" xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}">
51167
50996
  <defs>
51168
50997
  <pattern id="viewer-dots" width="20" height="20" patternUnits="userSpaceOnUse">
51169
- <circle cx="10" cy="10" r="1.5" fill="${dotColor}" opacity="0.6"/>
50998
+ <circle cx="10" cy="10" r="0.75" fill="${dotColor}" opacity="0.4"/>
51170
50999
  </pattern>
51171
51000
  </defs>
51172
51001
  <rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}" pointer-events="none"/>
@@ -51443,33 +51272,9 @@ circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor);
51443
51272
  var MIN_SEG_LEN = 3, JOG_THRESHOLD = 10, ORTHO_THRESHOLD = 300;
51444
51273
  var STUB_THRESHOLD = 500, STUB_LEN = 30;
51445
51274
 
51446
- function quadCurveControl(ax, ay, bx, by, ux, uy) {
51447
- var dn = Math.abs(ay - by);
51448
- return [bx + (ux * dn) / Math.abs(uy), ay];
51449
- }
51450
-
51275
+ // Straight-line fallback when orthogonal routing fails (matches platform)
51451
51276
  function computeConnectionPath(sx, sy, tx, ty) {
51452
- var e = 0.0001;
51453
- var ax = sx + e, ay = sy + e, hx = tx - e, hy = ty - e;
51454
- var ramp = Math.min(20, (hx - ax) / 10);
51455
- var bx = ax + ramp, by = ay + e, gx = hx - ramp, gy = hy - e;
51456
- var curveSizeX = Math.min(60, Math.abs(ax - hx) / 4);
51457
- var curveSizeY = Math.min(60, Math.abs(ay - hy) / 4);
51458
- var curveMag = Math.sqrt(curveSizeX * curveSizeX + curveSizeY * curveSizeY);
51459
- var bgX = gx - bx, bgY = gy - by;
51460
- var bgLen = Math.sqrt(bgX * bgX + bgY * bgY);
51461
- var bgUx = bgX / bgLen, bgUy = bgY / bgLen;
51462
- var dx = bx + bgUx * curveMag, dy = by + (bgUy * curveMag) / 2;
51463
- var ex = gx - bgUx * curveMag, ey = gy - (bgUy * curveMag) / 2;
51464
- var deX = ex - dx, deY = ey - dy;
51465
- var deLen = Math.sqrt(deX * deX + deY * deY);
51466
- var deUx = deX / deLen, deUy = deY / deLen;
51467
- var c = quadCurveControl(bx, by, dx, dy, -deUx, -deUy);
51468
- var f = quadCurveControl(gx, gy, ex, ey, deUx, deUy);
51469
- return 'M ' + c[0] + ',' + c[1] + ' M ' + ax + ',' + ay +
51470
- ' L ' + bx + ',' + by + ' Q ' + c[0] + ',' + c[1] + ' ' + dx + ',' + dy +
51471
- ' L ' + ex + ',' + ey + ' Q ' + f[0] + ',' + f[1] + ' ' + gx + ',' + gy +
51472
- ' L ' + hx + ',' + hy;
51277
+ return 'M ' + sx + ',' + sy + ' L ' + tx + ',' + ty;
51473
51278
  }
51474
51279
 
51475
51280
  // ---- Orthogonal router (ported from orthogonal-router.ts) ----
@@ -51752,7 +51557,16 @@ circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor);
51752
51557
  var portPositions = {};
51753
51558
  content.querySelectorAll('[data-port-id]').forEach(function(el) {
51754
51559
  var id = el.getAttribute('data-port-id');
51755
- portPositions[id] = { cx: parseFloat(el.getAttribute('cx')), cy: parseFloat(el.getAttribute('cy')) };
51560
+ // Ports may be circle (cx/cy) or rect (x/y/width/height) elements
51561
+ var cx, cy;
51562
+ if (el.tagName === 'circle') {
51563
+ cx = parseFloat(el.getAttribute('cx'));
51564
+ cy = parseFloat(el.getAttribute('cy'));
51565
+ } else {
51566
+ cx = parseFloat(el.getAttribute('x')) + parseFloat(el.getAttribute('width')) / 2;
51567
+ cy = parseFloat(el.getAttribute('y')) + parseFloat(el.getAttribute('height')) / 2;
51568
+ }
51569
+ portPositions[id] = { cx: cx, cy: cy };
51756
51570
  });
51757
51571
 
51758
51572
  // Extract node bounding boxes from SVG rect elements
@@ -51914,14 +51728,9 @@ circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor);
51914
51728
  }
51915
51729
  } else {
51916
51730
  // Path mode: show path, hide stubs
51917
- var ddx = tx - sx, ddy = ty - sy, dist = Math.sqrt(ddx * ddx + ddy * ddy);
51918
- var path;
51919
- if (dist > ORTHO_THRESHOLD) {
51920
- path = calcOrthogonalPath([sx, sy], [tx, ty], boxes, c.srcNode, c.tgtNode, c.srcIdx, c.tgtIdx, alloc);
51921
- if (!path) path = computeConnectionPath(sx, sy, tx, ty);
51922
- } else {
51923
- path = computeConnectionPath(sx, sy, tx, ty);
51924
- }
51731
+ // Always try orthogonal routing first (matches platform)
51732
+ var path = calcOrthogonalPath([sx, sy], [tx, ty], boxes, c.srcNode, c.tgtNode, c.srcIdx, c.tgtIdx, alloc);
51733
+ if (!path) path = computeConnectionPath(sx, sy, tx, ty);
51925
51734
  c.el.setAttribute('d', path);
51926
51735
  c.el.removeAttribute('display');
51927
51736
  if (c.stubs) {
@@ -52204,14 +52013,79 @@ circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor);
52204
52013
  });
52205
52014
  });
52206
52015
 
52207
- // Port hover: show this port's label + all connected port labels
52016
+ // Port hover: animated ring\u2192circle expansion (matches platform 0.12s ease-out)
52017
+ var OUTER_W = 6, OUTER_H = 18, OUTER_RX = 4;
52018
+ var HOVER_OUTER = 20, HOVER_OUTER_RX = 10;
52019
+ var HOVER_INNER = 14, HOVER_INNER_RX = 7;
52020
+ var PORT_ANIM_MS = 120; // matches platform transition duration
52021
+
52022
+ // Tween an SVG rect from current attrs to target attrs
52023
+ function tweenRect(el, target, duration, onDone) {
52024
+ if (!el) { if (onDone) onDone(); return; }
52025
+ var start = {
52026
+ x: parseFloat(el.getAttribute('x')),
52027
+ y: parseFloat(el.getAttribute('y')),
52028
+ w: parseFloat(el.getAttribute('width')),
52029
+ h: parseFloat(el.getAttribute('height')),
52030
+ rx: parseFloat(el.getAttribute('rx'))
52031
+ };
52032
+ var t0 = null;
52033
+ var frameId = el.__tweenFrame;
52034
+ if (frameId) cancelAnimationFrame(frameId);
52035
+ function step(ts) {
52036
+ if (!t0) t0 = ts;
52037
+ var p = Math.min((ts - t0) / duration, 1);
52038
+ // ease-out: cubic
52039
+ var e = 1 - Math.pow(1 - p, 3);
52040
+ el.setAttribute('x', String(start.x + (target.x - start.x) * e));
52041
+ el.setAttribute('y', String(start.y + (target.y - start.y) * e));
52042
+ el.setAttribute('width', String(start.w + (target.w - start.w) * e));
52043
+ el.setAttribute('height', String(start.h + (target.h - start.h) * e));
52044
+ el.setAttribute('rx', String(start.rx + (target.rx - start.rx) * e));
52045
+ if (p < 1) {
52046
+ el.__tweenFrame = requestAnimationFrame(step);
52047
+ } else {
52048
+ el.__tweenFrame = null;
52049
+ if (onDone) onDone();
52050
+ }
52051
+ }
52052
+ el.__tweenFrame = requestAnimationFrame(step);
52053
+ }
52054
+
52208
52055
  content.querySelectorAll('[data-port-id]').forEach(function(portEl) {
52209
52056
  var portId = portEl.getAttribute('data-port-id');
52210
52057
  var nodeId = portId.split('.')[0];
52211
52058
  var peers = (portConnections[portId] || []).concat(portId);
52212
52059
 
52060
+ var origX = parseFloat(portEl.getAttribute('x'));
52061
+ var origY = parseFloat(portEl.getAttribute('y'));
52062
+ var innerBar = portEl.nextElementSibling;
52063
+ var hasInner = innerBar && innerBar.getAttribute('pointer-events') === 'none';
52064
+ var innerOrig = hasInner ? {
52065
+ x: parseFloat(innerBar.getAttribute('x')),
52066
+ y: parseFloat(innerBar.getAttribute('y')),
52067
+ w: parseFloat(innerBar.getAttribute('width')),
52068
+ h: parseFloat(innerBar.getAttribute('height')),
52069
+ rx: parseFloat(innerBar.getAttribute('rx'))
52070
+ } : null;
52071
+
52072
+ var cx = origX + OUTER_W / 2;
52073
+ var cy = origY + OUTER_H / 2;
52074
+
52213
52075
  portEl.addEventListener('mouseenter', function() {
52214
52076
  hoveredPort = portId;
52077
+ if (portEl.tagName === 'rect') {
52078
+ tweenRect(portEl, {
52079
+ x: cx - HOVER_OUTER / 2, y: cy - HOVER_OUTER / 2,
52080
+ w: HOVER_OUTER, h: HOVER_OUTER, rx: HOVER_OUTER_RX
52081
+ }, PORT_ANIM_MS);
52082
+ if (hasInner) {
52083
+ tweenRect(innerBar, {
52084
+ x: cx - HOVER_INNER / 2, y: cy - HOVER_INNER / 2,
52085
+ w: HOVER_INNER, h: HOVER_INNER, rx: HOVER_INNER_RX
52086
+ }, PORT_ANIM_MS);
52087
+ }
52088
+ }
52215
52089
  batchLabelChanges(function() {
52216
52090
  hideLabelsFor(nodeId);
52217
52091
  peers.forEach(showLabel);
@@ -52228,6 +52102,18 @@ circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor);
52228
52102
  });
52229
52103
  portEl.addEventListener('mouseleave', function() {
52230
52104
  hoveredPort = null;
52105
+ if (portEl.tagName === 'rect') {
52106
+ tweenRect(portEl, {
52107
+ x: origX, y: origY,
52108
+ w: OUTER_W, h: OUTER_H, rx: OUTER_RX
52109
+ }, PORT_ANIM_MS);
52110
+ if (hasInner) {
52111
+ tweenRect(innerBar, {
52112
+ x: innerOrig.x, y: innerOrig.y,
52113
+ w: innerOrig.w, h: innerOrig.h, rx: innerOrig.rx
52114
+ }, PORT_ANIM_MS);
52115
+ }
52116
+ }
52231
52117
  // Defer so if entering another port, its mouseenter sets hoveredPort first
52232
52118
  var myPeers = peers, myNodeId = nodeId;
52233
52119
  Promise.resolve().then(function() {
@@ -52272,7 +52158,7 @@ circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor);
52272
52158
  if (!selectedPortId) return;
52273
52159
  selectedPortId = null;
52274
52160
  document.body.classList.remove('port-active');
52275
- content.querySelectorAll('circle.port-selected').forEach(function(c) {
52161
+ content.querySelectorAll('.port-selected').forEach(function(c) {
52276
52162
  c.classList.remove('port-selected');
52277
52163
  });
52278
52164
  content.querySelectorAll('[data-source].dimmed, [data-source].highlighted').forEach(function(p) {
@@ -52697,7 +52583,7 @@ async function diagramCommand(input, options = {}) {
52697
52583
  if (ASCII_FORMATS.has(format)) {
52698
52584
  result = fileToASCII(filePath, { ...diagramOptions, format });
52699
52585
  } else if (format === "html") {
52700
- result = fileToHTML(filePath, diagramOptions);
52586
+ result = fileToHTML(filePath, { ...diagramOptions, format });
52701
52587
  } else {
52702
52588
  result = fileToSVG(filePath, diagramOptions);
52703
52589
  }
@@ -84248,6 +84134,169 @@ var init_tools_debug = __esm({
84248
84134
  }
84249
84135
  });
84250
84136
 
84137
+ // src/mcp/run-registry.ts
84138
+ function storePendingRun(run) {
84139
+ pendingRuns.set(run.runId, run);
84140
+ }
84141
+ function getPendingRun(runId) {
84142
+ return pendingRuns.get(runId);
84143
+ }
84144
+ function removePendingRun(runId) {
84145
+ pendingRuns.delete(runId);
84146
+ }
84147
+ var pendingRuns;
84148
+ var init_run_registry = __esm({
84149
+ "src/mcp/run-registry.ts"() {
84150
+ "use strict";
84151
+ pendingRuns = /* @__PURE__ */ new Map();
84152
+ }
84153
+ });
84154
+
84155
+ // src/mcp/tools-workflow-run.ts
84156
+ function makeToolResult2(data) {
84157
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
84158
+ }
84159
+ function makeErrorResult2(code, message) {
84160
+ return {
84161
+ content: [{ type: "text", text: JSON.stringify({ error: code, message }) }],
84162
+ isError: true
84163
+ };
84164
+ }
84165
+ function generateRunId() {
84166
+ return `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
84167
+ }
84168
+ async function raceAgentPause(executionPromise, agentChannel) {
84169
+ try {
84170
+ const outcome = await Promise.race([
84171
+ executionPromise.then((r) => ({
84172
+ type: "completed",
84173
+ result: r?.result ?? r
84174
+ })),
84175
+ agentChannel.onPause().then((request) => ({
84176
+ type: "waiting_for_agent",
84177
+ request
84178
+ }))
84179
+ ]);
84180
+ return outcome;
84181
+ } catch (err) {
84182
+ return {
84183
+ type: "error",
84184
+ message: err instanceof Error ? err.message : String(err)
84185
+ };
84186
+ }
84187
+ }
84188
+ async function runWorkflowWithAgent(filePath, params, workflowName) {
84189
+ const agentChannel = new AgentChannel();
84190
+ const runId = generateRunId();
84191
+ const executionPromise = executeWorkflowFromFile(
84192
+ filePath,
84193
+ params,
84194
+ {
84195
+ workflowName,
84196
+ agentChannel,
84197
+ includeTrace: true
84198
+ }
84199
+ );
84200
+ const outcome = await raceAgentPause(executionPromise, agentChannel);
84201
+ if (outcome.type === "completed") {
84202
+ return { status: "completed", result: outcome.result };
84203
+ }
84204
+ if (outcome.type === "waiting_for_agent") {
84205
+ storePendingRun({
84206
+ runId,
84207
+ filePath,
84208
+ workflowName,
84209
+ executionPromise,
84210
+ agentChannel,
84211
+ request: outcome.request,
84212
+ createdAt: Date.now(),
84213
+ tmpFiles: []
84214
+ });
84215
+ return { status: "waiting_for_agent", runId, agentRequest: outcome.request, agentChannel };
84216
+ }
84217
+ return { status: "error", message: outcome.message };
84218
+ }
84219
+ async function resumeWorkflow(runId, agentResult) {
84220
+ const pendingRun = getPendingRun(runId);
84221
+ if (!pendingRun) {
84222
+ return { status: "error", message: `No pending run found with ID "${runId}".` };
84223
+ }
84224
+ pendingRun.agentChannel.resume(agentResult);
84225
+ const outcome = await raceAgentPause(pendingRun.executionPromise, pendingRun.agentChannel);
84226
+ if (outcome.type === "completed") {
84227
+ removePendingRun(runId);
84228
+ return { status: "completed", result: outcome.result };
84229
+ }
84230
+ if (outcome.type === "waiting_for_agent") {
84231
+ pendingRun.request = outcome.request;
84232
+ return { status: "waiting_for_agent", runId, agentRequest: outcome.request, agentChannel: pendingRun.agentChannel };
84233
+ }
84234
+ removePendingRun(runId);
84235
+ return { status: "error", message: outcome.message };
84236
+ }
84237
+ function registerWorkflowRunTools(mcp) {
84238
+ mcp.tool(
84239
+ "fw_workflow_run",
84240
+ "Run a workflow. If it hits a waitForAgent node, returns the agent request so you can respond. If it completes without pausing, returns the result directly.",
84241
+ {
84242
+ filePath: external_exports.string().describe("Path to the workflow .ts file"),
84243
+ params: external_exports.record(external_exports.unknown()).optional().describe("Input parameters for the workflow"),
84244
+ workflowName: external_exports.string().optional().describe("Specific workflow export name (for multi-workflow files)")
84245
+ },
84246
+ async (args) => {
84247
+ try {
84248
+ const result = await runWorkflowWithAgent(
84249
+ args.filePath,
84250
+ args.params ?? {},
84251
+ args.workflowName
84252
+ );
84253
+ if (result.status === "error") {
84254
+ return makeErrorResult2("EXECUTION_ERROR", result.message);
84255
+ }
84256
+ const { agentChannel: _ac, ...output } = result;
84257
+ return makeToolResult2(output);
84258
+ } catch (err) {
84259
+ return makeErrorResult2(
84260
+ "EXECUTION_ERROR",
84261
+ err instanceof Error ? err.message : String(err)
84262
+ );
84263
+ }
84264
+ }
84265
+ );
84266
+ mcp.tool(
84267
+ "fw_workflow_resume",
84268
+ "Resume a workflow that paused at waitForAgent. Provide the agent result to continue execution.",
84269
+ {
84270
+ runId: external_exports.string().describe("Run ID from fw_workflow_run"),
84271
+ agentResult: external_exports.record(external_exports.unknown()).describe("The agent result to pass back to the workflow")
84272
+ },
84273
+ async (args) => {
84274
+ try {
84275
+ const result = await resumeWorkflow(args.runId, args.agentResult);
84276
+ if (result.status === "error") {
84277
+ return makeErrorResult2("RESUME_ERROR", result.message);
84278
+ }
84279
+ const { agentChannel: _ac, ...output } = result;
84280
+ return makeToolResult2(output);
84281
+ } catch (err) {
84282
+ return makeErrorResult2(
84283
+ "RESUME_ERROR",
84284
+ err instanceof Error ? err.message : String(err)
84285
+ );
84286
+ }
84287
+ }
84288
+ );
84289
+ }
84290
+ var init_tools_workflow_run = __esm({
84291
+ "src/mcp/tools-workflow-run.ts"() {
84292
+ "use strict";
84293
+ init_zod();
84294
+ init_workflow_executor();
84295
+ init_agent_channel();
84296
+ init_run_registry();
84297
+ }
84298
+ });
84299
+
84251
84300
  // src/mcp/tools-context.ts
84252
84301
  function registerContextTools(mcp) {
84253
84302
  mcp.tool(
@@ -84492,6 +84541,7 @@ async function startMcpServer(options) {
84492
84541
  registerDocsTools(mcp);
84493
84542
  registerModelTools(mcp);
84494
84543
  registerDebugTools(mcp);
84544
+ registerWorkflowRunTools(mcp);
84495
84545
  registerContextTools(mcp);
84496
84546
  registerResourceTools(mcp);
84497
84547
  registerPrompts(mcp);
@@ -84528,6 +84578,7 @@ var init_server3 = __esm({
84528
84578
  init_tools_docs();
84529
84579
  init_tools_model();
84530
84580
  init_tools_debug();
84581
+ init_tools_workflow_run();
84531
84582
  init_tools_context();
84532
84583
  init_tools_resources();
84533
84584
  init_prompts();
@@ -89091,7 +89142,7 @@ function parseIntStrict(value) {
89091
89142
  // src/cli/index.ts
89092
89143
  init_logger();
89093
89144
  init_error_utils();
89094
- var version2 = true ? "0.33.0" : "0.0.0-dev";
89145
+ var version2 = true ? "0.33.2" : "0.0.0-dev";
89095
89146
  var program2 = new Command();
89096
89147
  program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
89097
89148
  logger.banner(version2);