@synergenius/flow-weaver 0.21.5 → 0.21.7

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.
@@ -39,7 +39,7 @@ function checkPackEngineVersion(pkg) {
39
39
  */
40
40
  function deriveNamespace(packageName) {
41
41
  const base = packageName.replace(/^@[^/]+\//, '');
42
- return base.replace(/^(flowweaver|flow-weaver)-pack-/, '');
42
+ return base.replace(/^flow-weaver-pack-/, '');
43
43
  }
44
44
  export async function registerPackCommands(program) {
45
45
  const projectDir = process.cwd();
@@ -18,6 +18,8 @@ export declare const SCOPE_PADDING_Y = 40;
18
18
  export declare const SCOPE_PORT_COLUMN = 50;
19
19
  export declare const SCOPE_INNER_GAP_X = 240;
20
20
  export declare const ORTHOGONAL_DISTANCE_THRESHOLD = 300;
21
+ export declare const STUB_DISTANCE_THRESHOLD = 500;
22
+ export declare const STUB_LENGTH = 30;
21
23
  /** Measure text width using pre-computed Montserrat 600/10px SVG character widths */
22
24
  export declare function measureText(text: string): number;
23
25
  /** Compute the full badge width for a port label (matches renderer badge layout) */
@@ -25,6 +25,12 @@ export const SCOPE_INNER_GAP_X = 240; // horizontal gap between children inside
25
25
  // Routing mode threshold — connections longer than this use orthogonal routing
26
26
  // (midpoint of original 250–350 hysteresis thresholds)
27
27
  export const ORTHOGONAL_DISTANCE_THRESHOLD = 300;
28
+ // Connections beyond this x-distance show as stubs only (no full path).
29
+ // Must be higher than ORTHOGONAL_DISTANCE_THRESHOLD so adjacent-layer connections
30
+ // still render their full orthogonal path.
31
+ export const STUB_DISTANCE_THRESHOLD = 500;
32
+ // Stub length for long-distance connections (short segment from port center outward)
33
+ export const STUB_LENGTH = 30;
28
34
  // ---- Font metrics (Montserrat 600-weight, 10px — measured via SVG getBBox) ----
29
35
  const CHAR_WIDTHS = {
30
36
  ' ': 2.78, '!': 3.34, '"': 4.74, '#': 5.56, '$': 5.56, '%': 8.9, '&': 7.23,
@@ -999,23 +1005,49 @@ export function buildDiagramGraph(ast, options = {}) {
999
1005
  const dy = ty - sy;
1000
1006
  const distance = Math.sqrt(dx * dx + dy * dy);
1001
1007
  const useCurve = forceCurveSet.has(pc);
1008
+ const sourceColor = getPortColor(pc.sourcePort.dataType, pc.sourcePort.isFailure, themeName);
1009
+ const targetColor = getPortColor(pc.targetPort.dataType, pc.targetPort.isFailure, themeName);
1010
+ const xDistance = Math.abs(tx - sx);
1011
+ // Always compute stubs so the HTML viewer can toggle between path/stubs on drag.
1012
+ // In static SVG (labels always visible), stubs start after the port label badge.
1013
+ // In HTML, stubs start from port center by default and push out when labels appear.
1014
+ const dashed = pc.sourcePort.dataType !== 'STEP';
1015
+ const srcLabelEnd = portLabelExtent(pc.sourcePort);
1016
+ const tgtLabelEnd = portLabelExtent(pc.targetPort);
1017
+ const sourceStub = {
1018
+ x: sx + srcLabelEnd, y: sy,
1019
+ endX: sx + srcLabelEnd + STUB_LENGTH,
1020
+ labelOffset: srcLabelEnd,
1021
+ color: sourceColor,
1022
+ dashed,
1023
+ };
1024
+ const targetStub = {
1025
+ x: tx - tgtLabelEnd, y: ty,
1026
+ endX: tx - tgtLabelEnd - STUB_LENGTH,
1027
+ labelOffset: tgtLabelEnd,
1028
+ color: targetColor,
1029
+ dashed,
1030
+ };
1002
1031
  let path;
1003
- if (!useCurve && distance > ORTHOGONAL_DISTANCE_THRESHOLD) {
1004
- // Try orthogonal routing for long-distance connections
1032
+ if (xDistance > STUB_DISTANCE_THRESHOLD) {
1033
+ // Long-distance: static SVG hides the full path, only shows stubs
1034
+ path = '';
1035
+ }
1036
+ else if (!useCurve && distance > ORTHOGONAL_DISTANCE_THRESHOLD) {
1005
1037
  const orthoPath = calculateOrthogonalPathSafe([sx, sy], [tx, ty], nodeBoxes, pc.fromNodeId, pc.toNodeId, { fromPortIndex: pc.fromPortIndex, toPortIndex: pc.toPortIndex }, allocator);
1006
1038
  path = orthoPath ?? computeConnectionPath(sx, sy, tx, ty);
1007
1039
  }
1008
1040
  else {
1009
1041
  path = computeConnectionPath(sx, sy, tx, ty);
1010
1042
  }
1011
- const sourceColor = getPortColor(pc.sourcePort.dataType, pc.sourcePort.isFailure, themeName);
1012
- const targetColor = getPortColor(pc.targetPort.dataType, pc.targetPort.isFailure, themeName);
1013
1043
  connections.push({
1014
1044
  fromNode: pc.fromNodeId, fromPort: pc.fromPortName,
1015
1045
  toNode: pc.toNodeId, toPort: pc.toPortName,
1016
1046
  sourceColor, targetColor,
1017
1047
  isStepConnection: pc.sourcePort.dataType === 'STEP',
1018
1048
  path,
1049
+ sourceStub,
1050
+ targetStub,
1019
1051
  });
1020
1052
  }
1021
1053
  // Recompute scope connection paths with the same routing logic as external connections
@@ -67,19 +67,19 @@ body {
67
67
  transition: opacity 0.15s ease-in-out;
68
68
  }
69
69
 
70
- /* Connection hover & dimming (attribute selector covers both main and scope connections) */
71
- path[data-source] { transition: opacity 0.2s ease, stroke-width 0.15s ease; }
72
- path[data-source]:hover { stroke-width: 4; cursor: pointer; }
73
- body.node-active path[data-source].dimmed,
74
- body.port-active path[data-source].dimmed { opacity: 0.1; }
75
- body.port-hovered path[data-source].dimmed { opacity: 0.25; }
70
+ /* Connection hover & dimming (covers paths + stub lines) */
71
+ [data-source] { transition: opacity 0.2s ease, stroke-width 0.15s ease; }
72
+ [data-source]:hover { stroke-width: 4; cursor: pointer; }
73
+ body.node-active [data-source].dimmed,
74
+ body.port-active [data-source].dimmed { opacity: 0.1; }
75
+ body.port-hovered [data-source].dimmed { opacity: 0.25; }
76
76
 
77
77
  /* Port circles are interactive */
78
78
  circle[data-port-id] { cursor: pointer; }
79
79
  circle[data-port-id]:hover { stroke-width: 3; filter: brightness(1.3); }
80
80
 
81
81
  /* Port-click highlighting */
82
- path[data-source].highlighted { opacity: 1; }
82
+ [data-source].highlighted { opacity: 1; }
83
83
  circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor); stroke-width: 4; }
84
84
 
85
85
  /* Node selection glow */
@@ -91,7 +91,7 @@ circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor);
91
91
  .node-glow { fill: none; pointer-events: none; animation: select-pop 0.3s ease-out forwards; }
92
92
 
93
93
  /* Port hover path highlight */
94
- path[data-source].port-hover { opacity: 1; }
94
+ [data-source].port-hover { opacity: 1; }
95
95
 
96
96
  /* Node hover glow + draggable cursor */
97
97
  .nodes g[data-node-id] { cursor: grab; }
@@ -541,12 +541,16 @@ path[data-source].port-hover { opacity: 1; }
541
541
  labelMap[lbl.getAttribute('data-port-label')] = lbl;
542
542
  });
543
543
 
544
- // Build adjacency: portId -> array of connected portIds
544
+ // Build adjacency: portId -> array of connected portIds (covers paths + stubs)
545
545
  var portConnections = {};
546
- content.querySelectorAll('path[data-source]').forEach(function(p) {
546
+ var seenEdges = {};
547
+ content.querySelectorAll('[data-source]').forEach(function(p) {
547
548
  var src = p.getAttribute('data-source');
548
549
  var tgt = p.getAttribute('data-target');
549
550
  if (!src || !tgt) return;
551
+ var key = src + '|' + tgt;
552
+ if (seenEdges[key]) return; // avoid duplicates from path+stub pairs
553
+ seenEdges[key] = true;
550
554
  if (!portConnections[src]) portConnections[src] = [];
551
555
  if (!portConnections[tgt]) portConnections[tgt] = [];
552
556
  portConnections[src].push(tgt);
@@ -556,6 +560,7 @@ path[data-source].port-hover { opacity: 1; }
556
560
  // ---- Connection path computation (bezier from geometry.ts + orthogonal router) ----
557
561
  var TRACK_SPACING = 15, EDGE_OFFSET = 5, MAX_CANDIDATES = 5;
558
562
  var MIN_SEG_LEN = 3, JOG_THRESHOLD = 10, ORTHO_THRESHOLD = 300;
563
+ var STUB_THRESHOLD = 500, STUB_LEN = 30;
559
564
 
560
565
  function quadCurveControl(ax, ay, bx, by, ux, uy) {
561
566
  var dn = Math.abs(ay - by);
@@ -905,8 +910,66 @@ path[data-source].port-hover { opacity: 1; }
905
910
  var srcIdx = portIndexMap[srcNode] ? portIndexMap[srcNode].output.indexOf(src) : 0;
906
911
  var tgtIdx = portIndexMap[tgtNode] ? portIndexMap[tgtNode].input.indexOf(tgt) : 0;
907
912
  connIndex.push({ el: p, src: src, tgt: tgt, srcNode: srcNode, tgtNode: tgtNode,
908
- scopeOf: p.getAttribute('data-scope') || null, srcIdx: Math.max(0, srcIdx), tgtIdx: Math.max(0, tgtIdx) });
913
+ scopeOf: p.getAttribute('data-scope') || null, srcIdx: Math.max(0, srcIdx), tgtIdx: Math.max(0, tgtIdx),
914
+ stubs: null });
915
+ });
916
+
917
+ // Build stub index and link to connIndex entries.
918
+ // Compute labelOffset per stub (distance from port center to SVG stub start).
919
+ // In HTML, stubs start from port center; on label hover they push out past the badge.
920
+ var stubMap = {};
921
+ content.querySelectorAll('.stubs g.stub[data-source]').forEach(function(el) {
922
+ var src = el.getAttribute('data-source');
923
+ var tgt = el.getAttribute('data-target');
924
+ var key = src + '|' + tgt;
925
+ if (!stubMap[key]) stubMap[key] = { src: null, tgt: null, srcOff: 0, tgtOff: 0 };
926
+ var dir = el.getAttribute('data-stub');
927
+ var line = el.querySelector('line');
928
+ if (dir === 'source') {
929
+ stubMap[key].src = el;
930
+ if (line) {
931
+ var pp = portPositions[src];
932
+ if (pp) stubMap[key].srcOff = parseFloat(line.getAttribute('x1')) - pp.cx;
933
+ }
934
+ } else {
935
+ stubMap[key].tgt = el;
936
+ if (line) {
937
+ var pp = portPositions[tgt];
938
+ if (pp) stubMap[key].tgtOff = parseFloat(line.getAttribute('x1')) - pp.cx;
939
+ }
940
+ }
909
941
  });
942
+ for (var ci = 0; ci < connIndex.length; ci++) {
943
+ var entry = connIndex[ci];
944
+ var stubKey = entry.src + '|' + entry.tgt;
945
+ if (stubMap[stubKey]) entry.stubs = stubMap[stubKey];
946
+ }
947
+
948
+ // Pull all visible stubs to port center on init (labels are hidden in HTML)
949
+ for (var si = 0; si < connIndex.length; si++) {
950
+ var sc = connIndex[si];
951
+ if (!sc.stubs) continue;
952
+ if (sc.stubs.src) {
953
+ var sLine = sc.stubs.src.querySelector('line');
954
+ var sCirc = sc.stubs.src.querySelector('circle');
955
+ var spp = portPositions[sc.src];
956
+ if (sLine && spp) {
957
+ sLine.setAttribute('x1', spp.cx); sLine.setAttribute('y1', spp.cy);
958
+ sLine.setAttribute('x2', spp.cx + STUB_LEN); sLine.setAttribute('y2', spp.cy);
959
+ if (sCirc) { sCirc.setAttribute('cx', spp.cx + STUB_LEN); sCirc.setAttribute('cy', spp.cy); }
960
+ }
961
+ }
962
+ if (sc.stubs.tgt) {
963
+ var tLine = sc.stubs.tgt.querySelector('line');
964
+ var tCirc = sc.stubs.tgt.querySelector('circle');
965
+ var tpp = portPositions[sc.tgt];
966
+ if (tLine && tpp) {
967
+ tLine.setAttribute('x1', tpp.cx); tLine.setAttribute('y1', tpp.cy);
968
+ tLine.setAttribute('x2', tpp.cx - STUB_LEN); tLine.setAttribute('y2', tpp.cy);
969
+ if (tCirc) { tCirc.setAttribute('cx', tpp.cx - STUB_LEN); tCirc.setAttribute('cy', tpp.cy); }
970
+ }
971
+ }
972
+ }
910
973
 
911
974
  // Snapshot of original port positions for reset
912
975
  var origPortPositions = {};
@@ -919,8 +982,10 @@ path[data-source].port-hover { opacity: 1; }
919
982
  origNodeBoxMap[nid] = { id: b.id, x: b.x, y: b.y, w: b.w, h: b.h };
920
983
  }
921
984
 
922
- // ---- Recalculate all connection paths using orthogonal + bezier routing ----
985
+ // ---- Recalculate all connection paths + stubs using orthogonal + bezier routing ----
923
986
  function recalcAllPaths() {
987
+ // Cancel all running stub animations since we're hard-repositioning
988
+ for (var ak in activeAnims) { cancelAnimationFrame(activeAnims[ak]); delete activeAnims[ak]; }
924
989
  var boxes = [];
925
990
  for (var nid in nodeBoxMap) boxes.push(nodeBoxMap[nid]);
926
991
  var sorted = connIndex.slice().sort(function(a, b) {
@@ -943,15 +1008,46 @@ path[data-source].port-hover { opacity: 1; }
943
1008
  var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
944
1009
  sx -= pOff.dx; sy -= pOff.dy; tx -= pOff.dx; ty -= pOff.dy;
945
1010
  }
946
- var ddx = tx - sx, ddy = ty - sy, dist = Math.sqrt(ddx * ddx + ddy * ddy);
947
- var path;
948
- if (dist > ORTHO_THRESHOLD) {
949
- path = calcOrthogonalPath([sx, sy], [tx, ty], boxes, c.srcNode, c.tgtNode, c.srcIdx, c.tgtIdx, alloc);
950
- if (!path) path = computeConnectionPath(sx, sy, tx, ty);
1011
+ var xDist = Math.abs(tx - sx);
1012
+
1013
+ if (xDist > STUB_THRESHOLD) {
1014
+ // Stub mode: hide path, show and reposition stubs
1015
+ c.el.setAttribute('display', 'none');
1016
+ if (c.stubs) {
1017
+ if (c.stubs.src) {
1018
+ var sLen = isLabelVisible(c.src) ? c.stubs.srcOff + STUB_LEN : STUB_LEN;
1019
+ var sLine = c.stubs.src.querySelector('line');
1020
+ var sCirc = c.stubs.src.querySelector('circle');
1021
+ if (sLine) { sLine.setAttribute('x1', sx); sLine.setAttribute('y1', sy); sLine.setAttribute('x2', sx + sLen); sLine.setAttribute('y2', sy); }
1022
+ if (sCirc) { sCirc.setAttribute('cx', sx + sLen); sCirc.setAttribute('cy', sy); }
1023
+ c.stubs.src.removeAttribute('display');
1024
+ }
1025
+ if (c.stubs.tgt) {
1026
+ var tLen = isLabelVisible(c.tgt) ? c.stubs.tgtOff - STUB_LEN : -STUB_LEN;
1027
+ var tLine = c.stubs.tgt.querySelector('line');
1028
+ var tCirc = c.stubs.tgt.querySelector('circle');
1029
+ if (tLine) { tLine.setAttribute('x1', tx); tLine.setAttribute('y1', ty); tLine.setAttribute('x2', tx + tLen); tLine.setAttribute('y2', ty); }
1030
+ if (tCirc) { tCirc.setAttribute('cx', tx + tLen); tCirc.setAttribute('cy', ty); }
1031
+ c.stubs.tgt.removeAttribute('display');
1032
+ }
1033
+ }
951
1034
  } else {
952
- path = computeConnectionPath(sx, sy, tx, ty);
1035
+ // Path mode: show path, hide stubs
1036
+ var ddx = tx - sx, ddy = ty - sy, dist = Math.sqrt(ddx * ddx + ddy * ddy);
1037
+ var path;
1038
+ if (dist > ORTHO_THRESHOLD) {
1039
+ path = calcOrthogonalPath([sx, sy], [tx, ty], boxes, c.srcNode, c.tgtNode, c.srcIdx, c.tgtIdx, alloc);
1040
+ if (!path) path = computeConnectionPath(sx, sy, tx, ty);
1041
+ } else {
1042
+ path = computeConnectionPath(sx, sy, tx, ty);
1043
+ }
1044
+ c.el.setAttribute('d', path);
1045
+ c.el.removeAttribute('display');
1046
+ if (c.stubs) {
1047
+ if (c.stubs.src) c.stubs.src.setAttribute('display', 'none');
1048
+ if (c.stubs.tgt) c.stubs.tgt.setAttribute('display', 'none');
1049
+ }
953
1050
  }
954
- c.el.setAttribute('d', path);
955
1051
  }
956
1052
  }
957
1053
 
@@ -1069,9 +1165,75 @@ path[data-source].port-hover { opacity: 1; }
1069
1165
 
1070
1166
  var allLabelIds = Object.keys(labelMap);
1071
1167
  var hoveredPort = null;
1168
+ var hoveredNode = null;
1169
+ var activeAnims = {};
1170
+
1171
+ // Build reverse map: portId -> array of { connEntry, isSource }
1172
+ var portStubMap = {};
1173
+ for (var psi = 0; psi < connIndex.length; psi++) {
1174
+ var psc = connIndex[psi];
1175
+ if (!psc.stubs) continue;
1176
+ if (psc.stubs.src) {
1177
+ if (!portStubMap[psc.src]) portStubMap[psc.src] = [];
1178
+ portStubMap[psc.src].push({ c: psc, isSource: true });
1179
+ }
1180
+ if (psc.stubs.tgt) {
1181
+ if (!portStubMap[psc.tgt]) portStubMap[psc.tgt] = [];
1182
+ portStubMap[psc.tgt].push({ c: psc, isSource: false });
1183
+ }
1184
+ }
1185
+
1186
+ function isLabelVisible(id) {
1187
+ var l = labelMap[id];
1188
+ return l && l.style.opacity === '1';
1189
+ }
1190
+
1191
+ // Batch mode: defer stub sync until all label changes are done
1192
+ var stubSyncDeferred = false;
1193
+ var stubSyncPorts = {};
1072
1194
 
1073
- function showLabel(id) { var l = labelMap[id]; if (l) { l.style.opacity = '1'; l.style.pointerEvents = 'auto'; } }
1074
- function hideLabel(id) { var l = labelMap[id]; if (l) { l.style.opacity = '0'; l.style.pointerEvents = 'none'; } }
1195
+ function showLabel(id) {
1196
+ var l = labelMap[id];
1197
+ if (l) { l.style.opacity = '1'; l.style.pointerEvents = 'auto'; }
1198
+ if (stubSyncDeferred) { stubSyncPorts[id] = true; }
1199
+ else { syncStubsForPort(id); }
1200
+ }
1201
+ function hideLabel(id) {
1202
+ var l = labelMap[id];
1203
+ if (l) { l.style.opacity = '0'; l.style.pointerEvents = 'none'; }
1204
+ if (stubSyncDeferred) { stubSyncPorts[id] = true; }
1205
+ else { syncStubsForPort(id); }
1206
+ }
1207
+
1208
+ // Batch label changes: do all shows/hides, then sync stubs once
1209
+ function batchLabelChanges(fn) {
1210
+ stubSyncDeferred = true;
1211
+ stubSyncPorts = {};
1212
+ fn();
1213
+ stubSyncDeferred = false;
1214
+ for (var pid in stubSyncPorts) syncStubsForPort(pid);
1215
+ }
1216
+
1217
+ function syncStubsForPort(portId) {
1218
+ var entries = portStubMap[portId];
1219
+ if (!entries) return;
1220
+ var visible = isLabelVisible(portId);
1221
+ for (var ei = 0; ei < entries.length; ei++) {
1222
+ growStub(entries[ei].c.stubs, entries[ei].c, entries[ei].isSource, visible);
1223
+ }
1224
+ }
1225
+
1226
+ // Safety net: verify all stubs match their port label visibility.
1227
+ // Runs as a rAF so it fires after all events and microtasks settle.
1228
+ var stubVerifyScheduled = false;
1229
+ function scheduleStubVerify() {
1230
+ if (stubVerifyScheduled) return;
1231
+ stubVerifyScheduled = true;
1232
+ requestAnimationFrame(function() {
1233
+ stubVerifyScheduled = false;
1234
+ for (var pid in portStubMap) syncStubsForPort(pid);
1235
+ });
1236
+ }
1075
1237
 
1076
1238
  function showLabelsFor(nodeId) {
1077
1239
  allLabelIds.forEach(function(id) {
@@ -1084,6 +1246,52 @@ path[data-source].port-hover { opacity: 1; }
1084
1246
  });
1085
1247
  }
1086
1248
 
1249
+ // Animate a stub growing/shrinking. The line x1 stays at port center,
1250
+ // x2 and circle cx extend to targetLen from port center (signed: positive=right, negative=left).
1251
+ function animateStub(stubG, targetLen, key) {
1252
+ if (!stubG) return;
1253
+ var line = stubG.querySelector('line');
1254
+ var circ = stubG.querySelector('circle');
1255
+ if (!line) return;
1256
+ var x1 = parseFloat(line.getAttribute('x1'));
1257
+ var curX2 = parseFloat(line.getAttribute('x2'));
1258
+ var curLen = curX2 - x1;
1259
+ var goalX2 = x1 + targetLen;
1260
+ if (Math.abs(curLen - targetLen) < 0.5) {
1261
+ line.setAttribute('x2', goalX2);
1262
+ if (circ) circ.setAttribute('cx', goalX2);
1263
+ return;
1264
+ }
1265
+ if (activeAnims[key]) cancelAnimationFrame(activeAnims[key]);
1266
+ var start = null, duration = 150, startLen = curLen;
1267
+ function step(ts) {
1268
+ if (!start) start = ts;
1269
+ var t = Math.min((ts - start) / duration, 1);
1270
+ t = t * (2 - t); // ease-out quad
1271
+ var len = startLen + (targetLen - startLen) * t;
1272
+ var x2 = x1 + len;
1273
+ line.setAttribute('x2', x2);
1274
+ if (circ) circ.setAttribute('cx', x2);
1275
+ if (t < 1) activeAnims[key] = requestAnimationFrame(step);
1276
+ else delete activeAnims[key];
1277
+ }
1278
+ activeAnims[key] = requestAnimationFrame(step);
1279
+ }
1280
+
1281
+ function growStub(stubs, connEntry, isSource, grow) {
1282
+ if (!stubs) return;
1283
+ var stubG = isSource ? stubs.src : stubs.tgt;
1284
+ if (!stubG) return;
1285
+ var off = isSource ? stubs.srcOff : stubs.tgtOff;
1286
+ // Source stubs grow right (+), target stubs grow left (-).
1287
+ // off is already signed (positive for source, negative for target).
1288
+ var shortLen = isSource ? STUB_LEN : -STUB_LEN;
1289
+ var fullLen = off + shortLen;
1290
+ var targetLen = grow ? fullLen : shortLen;
1291
+ var key = connEntry.src + '|' + connEntry.tgt + (isSource ? ':s' : ':t');
1292
+ animateStub(stubG, targetLen, key);
1293
+ }
1294
+
1087
1295
  // Node hover: show all port labels for the hovered node
1088
1296
  var nodeEls = content.querySelectorAll('.nodes g[data-node-id]');
1089
1297
  nodeEls.forEach(function(nodeG) {
@@ -1091,14 +1299,27 @@ path[data-source].port-hover { opacity: 1; }
1091
1299
  var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('g[data-node-id]') : null;
1092
1300
  var parentId = parentNodeG ? parentNodeG.getAttribute('data-node-id') : null;
1093
1301
  nodeG.addEventListener('mouseenter', function() {
1302
+ hoveredNode = nodeId;
1094
1303
  if (hoveredPort) return;
1095
- if (parentId) hideLabelsFor(parentId);
1096
- showLabelsFor(nodeId);
1304
+ batchLabelChanges(function() {
1305
+ if (parentId) hideLabelsFor(parentId);
1306
+ showLabelsFor(nodeId);
1307
+ });
1308
+ scheduleStubVerify();
1097
1309
  });
1098
1310
  nodeG.addEventListener('mouseleave', function() {
1099
- if (hoveredPort) return;
1100
- hideLabelsFor(nodeId);
1101
- if (parentId) showLabelsFor(parentId);
1311
+ // Defer so port mouseenter can set hoveredPort first
1312
+ var nid = nodeId, pid = parentId;
1313
+ if (hoveredNode === nodeId) hoveredNode = null;
1314
+ Promise.resolve().then(function() {
1315
+ if (hoveredPort) return;
1316
+ if (hoveredNode === nid) return; // re-entered the node
1317
+ batchLabelChanges(function() {
1318
+ hideLabelsFor(nid);
1319
+ if (pid) showLabelsFor(pid);
1320
+ });
1321
+ scheduleStubVerify();
1322
+ });
1102
1323
  });
1103
1324
  });
1104
1325
 
@@ -1110,10 +1331,13 @@ path[data-source].port-hover { opacity: 1; }
1110
1331
 
1111
1332
  portEl.addEventListener('mouseenter', function() {
1112
1333
  hoveredPort = portId;
1113
- hideLabelsFor(nodeId);
1114
- peers.forEach(showLabel);
1334
+ batchLabelChanges(function() {
1335
+ hideLabelsFor(nodeId);
1336
+ peers.forEach(showLabel);
1337
+ });
1338
+ scheduleStubVerify();
1115
1339
  document.body.classList.add('port-hovered');
1116
- content.querySelectorAll('path[data-source]').forEach(function(p) {
1340
+ content.querySelectorAll('[data-source]').forEach(function(p) {
1117
1341
  if (p.getAttribute('data-source') === portId || p.getAttribute('data-target') === portId) {
1118
1342
  p.classList.remove('dimmed');
1119
1343
  } else {
@@ -1123,11 +1347,19 @@ path[data-source].port-hover { opacity: 1; }
1123
1347
  });
1124
1348
  portEl.addEventListener('mouseleave', function() {
1125
1349
  hoveredPort = null;
1126
- peers.forEach(hideLabel);
1127
- showLabelsFor(nodeId);
1128
- document.body.classList.remove('port-hovered');
1129
- content.querySelectorAll('path[data-source].dimmed').forEach(function(p) {
1130
- p.classList.remove('dimmed');
1350
+ // Defer so if entering another port, its mouseenter sets hoveredPort first
1351
+ var myPeers = peers, myNodeId = nodeId;
1352
+ Promise.resolve().then(function() {
1353
+ if (hoveredPort) return; // moved to another port
1354
+ batchLabelChanges(function() {
1355
+ myPeers.forEach(hideLabel);
1356
+ showLabelsFor(myNodeId);
1357
+ });
1358
+ document.body.classList.remove('port-hovered');
1359
+ content.querySelectorAll('[data-source].dimmed').forEach(function(p) {
1360
+ p.classList.remove('dimmed');
1361
+ });
1362
+ scheduleStubVerify();
1131
1363
  });
1132
1364
  });
1133
1365
  });
@@ -1162,7 +1394,7 @@ path[data-source].port-hover { opacity: 1; }
1162
1394
  content.querySelectorAll('circle.port-selected').forEach(function(c) {
1163
1395
  c.classList.remove('port-selected');
1164
1396
  });
1165
- content.querySelectorAll('path[data-source].dimmed, path[data-source].highlighted').forEach(function(p) {
1397
+ content.querySelectorAll('[data-source].dimmed, [data-source].highlighted').forEach(function(p) {
1166
1398
  p.classList.remove('dimmed');
1167
1399
  p.classList.remove('highlighted');
1168
1400
  });
@@ -1178,7 +1410,7 @@ path[data-source].port-hover { opacity: 1; }
1178
1410
  var portEl = content.querySelector('[data-port-id="' + CSS.escape(portId) + '"]');
1179
1411
  if (portEl) portEl.classList.add('port-selected');
1180
1412
 
1181
- content.querySelectorAll('path[data-source]').forEach(function(p) {
1413
+ content.querySelectorAll('[data-source]').forEach(function(p) {
1182
1414
  if (p.getAttribute('data-source') === portId || p.getAttribute('data-target') === portId) {
1183
1415
  p.classList.add('highlighted');
1184
1416
  } else {
@@ -1198,7 +1430,7 @@ path[data-source].port-hover { opacity: 1; }
1198
1430
  infoPanel.classList.remove('fullscreen');
1199
1431
  btnExpand.innerHTML = expandIcon;
1200
1432
  removeNodeGlow();
1201
- content.querySelectorAll('path[data-source].dimmed').forEach(function(p) {
1433
+ content.querySelectorAll('[data-source].dimmed').forEach(function(p) {
1202
1434
  p.classList.remove('dimmed');
1203
1435
  });
1204
1436
  }
@@ -1226,7 +1458,7 @@ path[data-source].port-hover { opacity: 1; }
1226
1458
  });
1227
1459
 
1228
1460
  // Connected paths
1229
- var allPaths = content.querySelectorAll('path[data-source]');
1461
+ var allPaths = content.querySelectorAll('[data-source]');
1230
1462
  var connectedNodes = new Set();
1231
1463
  allPaths.forEach(function(p) {
1232
1464
  var src = p.getAttribute('data-source') || '';
@@ -56,11 +56,21 @@ export function renderSVG(graph, options = {}) {
56
56
  // Background
57
57
  parts.push(`<rect x="${vbX}" y="${vbY}" width="${vbWidth}" height="${vbHeight}" fill="${theme.background}"/>`);
58
58
  parts.push(`<rect x="${vbX}" y="${vbY}" width="${vbWidth}" height="${vbHeight}" fill="url(#dot-grid)"/>`);
59
- // Connections
59
+ // Connections + stubs (same layer, below nodes and labels)
60
60
  parts.push(`<g class="connections">`);
61
61
  for (let i = 0; i < graph.connections.length; i++) {
62
- renderConnection(parts, graph.connections[i], i);
62
+ renderConnection(parts, graph.connections[i], i, !graph.connections[i].path);
63
63
  }
64
+ // Stubs sit alongside connection paths; short-distance ones start hidden for HTML viewer toggling
65
+ parts.push(` <g class="stubs">`);
66
+ for (const conn of graph.connections) {
67
+ const hideStubs = !!conn.path;
68
+ if (conn.sourceStub)
69
+ renderStub(parts, conn.sourceStub, conn, hideStubs);
70
+ if (conn.targetStub)
71
+ renderStub(parts, conn.targetStub, conn, hideStubs);
72
+ }
73
+ parts.push(` </g>`);
64
74
  parts.push(`</g>`);
65
75
  // Nodes (bodies, icons, port dots — no labels)
66
76
  parts.push(`<g class="nodes">`);
@@ -99,9 +109,23 @@ export function renderSVG(graph, options = {}) {
99
109
  return parts.join('\n');
100
110
  }
101
111
  // ---- Connection rendering ----
102
- function renderConnection(parts, conn, gradIndex) {
112
+ function renderConnection(parts, conn, gradIndex, hidden = false) {
103
113
  const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
104
- parts.push(` <path d="${conn.path}" 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"/>`);
114
+ const displayAttr = hidden ? ' display="none"' : '';
115
+ const pathD = conn.path || 'M0,0';
116
+ parts.push(` <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}/>`);
117
+ }
118
+ function renderStub(parts, stub, conn, hidden = false) {
119
+ const dashAttr = stub.dashed ? ' stroke-dasharray="6 3"' : '';
120
+ const displayAttr = hidden ? ' display="none"' : '';
121
+ const dataAttrs = `data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"`;
122
+ const isSource = stub.endX > stub.x; // source stubs go right
123
+ const stubDir = isSource ? 'source' : 'target';
124
+ parts.push(` <g class="stub" data-stub="${stubDir}" ${dataAttrs}${displayAttr}>`);
125
+ const linecap = stub.dashed ? 'butt' : 'round';
126
+ parts.push(` <line x1="${stub.x}" y1="${stub.y}" x2="${stub.endX}" y2="${stub.y}" stroke="${stub.color}" stroke-width="2"${dashAttr} stroke-linecap="${linecap}"/>`);
127
+ parts.push(` <circle cx="${stub.endX}" cy="${stub.y}" r="3" fill="${stub.color}"/>`);
128
+ parts.push(` </g>`);
105
129
  }
106
130
  function renderScopeConnection(parts, conn, allConnections, parentNodeId) {
107
131
  const gradIndex = allConnections.indexOf(conn);
@@ -177,7 +201,7 @@ function renderNodeLabel(parts, node, theme) {
177
201
  const labelTextX = isScoped ? node.x + 8 : node.x + node.width / 2;
178
202
  const labelAnchor = isScoped ? 'start' : 'middle';
179
203
  parts.push(` <g data-label-for="${escapeXml(node.id)}">`);
180
- parts.push(` <rect x="${labelBgX}" y="${labelBgY}" width="${labelBgWidth}" height="${labelBgHeight}" rx="6" fill="${theme.labelBadgeFill}" opacity="0.8"/>`);
204
+ parts.push(` <rect x="${labelBgX}" y="${labelBgY}" width="${labelBgWidth}" height="${labelBgHeight}" rx="6" fill="${theme.labelBadgeFill}" opacity="0.95"/>`);
181
205
  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>`);
182
206
  parts.push(` </g>`);
183
207
  }
@@ -36,6 +36,14 @@ export interface DiagramNode {
36
36
  outputs: DiagramPort[];
37
37
  };
38
38
  }
39
+ export interface DiagramStub {
40
+ x: number;
41
+ y: number;
42
+ endX: number;
43
+ labelOffset: number;
44
+ color: string;
45
+ dashed: boolean;
46
+ }
39
47
  export interface DiagramConnection {
40
48
  fromNode: string;
41
49
  fromPort: string;
@@ -45,6 +53,8 @@ export interface DiagramConnection {
45
53
  targetColor: string;
46
54
  isStepConnection: boolean;
47
55
  path: string;
56
+ sourceStub?: DiagramStub;
57
+ targetStub?: DiagramStub;
48
58
  }
49
59
  export interface DiagramGraph {
50
60
  nodes: DiagramNode[];
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.21.5";
1
+ export declare const VERSION = "0.21.7";
2
2
  //# sourceMappingURL=generated-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.ts — do not edit manually
2
- export const VERSION = '0.21.5';
2
+ export const VERSION = '0.21.7';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -10,7 +10,7 @@ import * as path from 'path';
10
10
  import { glob } from 'glob';
11
11
  const MARKETPLACE_KEYWORD = 'flowweaver-marketplace-pack';
12
12
  const NPM_SEARCH_URL = 'https://registry.npmjs.org/-/v1/search';
13
- const PACK_NAME_RE = /^(@[^/]+\/)?(flowweaver|flow-weaver)-pack-.+$/;
13
+ const PACK_NAME_RE = /^(@[^/]+\/)?flow-weaver-pack-.+$/;
14
14
  /**
15
15
  * Search the npm registry for marketplace packages.
16
16
  */
@@ -49,10 +49,8 @@ export async function listInstalledPackages(projectDir) {
49
49
  const nodeModules = path.join(projectDir, 'node_modules');
50
50
  if (!fs.existsSync(nodeModules))
51
51
  return [];
52
- // Look for both flowweaver-pack-* and flow-weaver-pack-* directories
52
+ // Look for flow-weaver-pack-* and @*/flow-weaver-pack-* directories
53
53
  const patterns = [
54
- path.join(nodeModules, 'flowweaver-pack-*', 'flowweaver.manifest.json'),
55
- path.join(nodeModules, '@*', 'flowweaver-pack-*', 'flowweaver.manifest.json'),
56
54
  path.join(nodeModules, 'flow-weaver-pack-*', 'flowweaver.manifest.json'),
57
55
  path.join(nodeModules, '@*', 'flow-weaver-pack-*', 'flowweaver.manifest.json'),
58
56
  ];
@@ -9,7 +9,7 @@ import { parseWorkflow, validateWorkflow } from '../api/index.js';
9
9
  function issue(code, severity, message) {
10
10
  return { code, severity, message };
11
11
  }
12
- const PACK_NAME_RE = /^(@[^/]+\/)?(flowweaver|flow-weaver)-pack-.+$/;
12
+ const PACK_NAME_RE = /^(@[^/]+\/)?flow-weaver-pack-.+$/;
13
13
  const MARKETPLACE_KEYWORD = 'flowweaver-marketplace-pack';
14
14
  // ── Package-level rules ──────────────────────────────────────────────────────
15
15
  function validatePackageJson(pkg, directory) {
@@ -696,7 +696,7 @@ flow-weaver export workflow.ts --target inngest --output dist/ --durable-steps
696
696
  flow-weaver export workflow.ts --target cloudflare --output worker/
697
697
  ```
698
698
 
699
- > Available targets depend on installed `flowweaver-pack-*` packages. See [Deployment](deployment) for installation instructions and target-specific details.
699
+ > Available targets depend on installed `flow-weaver-pack-*` packages. See [Deployment](deployment) for installation instructions and target-specific details.
700
700
 
701
701
  ---
702
702