sysml-diagram 0.20.1 → 0.20.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.
package/out/main.js CHANGED
@@ -181834,6 +181834,13 @@ var DOCK_TANGENT = {
181834
181834
  left: { x: 0, y: 1 },
181835
181835
  right: { x: 0, y: 1 }
181836
181836
  };
181837
+ var BODY_SNAP_POINT_HIT_SIZE = 12;
181838
+ function bodySnapPointOccludedByPort(point, portRects) {
181839
+ const half = BODY_SNAP_POINT_HIT_SIZE / 2;
181840
+ return portRects.some(
181841
+ (rect) => point.x + half >= rect.x && point.x - half <= rect.x + rect.width && point.y + half >= rect.y && point.y - half <= rect.y + rect.height
181842
+ );
181843
+ }
181837
181844
  function portGlyphRect(g, w, h, topReserve = 0) {
181838
181845
  const natural = endpointGlyphSize(g.port);
181839
181846
  const along = g.along ?? natural;
@@ -183265,19 +183272,24 @@ function modelToFlow(model, opts) {
183265
183272
  // could not be re-arranged. (Geometry nodes are the one exception —
183266
183273
  // their positions are model attributes, see above.)
183267
183274
  draggable: model.kind !== "gev",
183268
- selectable: true,
183269
- // issue #58 a plain (non-promoted) Interconnection View part is
183270
- // often nearly the whole visible canvas at typical zoom, so its empty
183271
- // interior must pan the canvas rather than move the part (a click
183272
- // there still selects it dragHandle only gates the DRAG gesture,
183273
- // never click). Every other diagram kind keeps the whole-body drag.
183274
- // User report 2026-08-11 the grabbable area was the SVG title TEXT
183275
- // alone, which a part with hidden internals could barely be moved by
183276
- // and which carries no move cursor: `.rf-node-header` is the same
183277
- // full-width title strip a container frame has, so a part behaves the
183278
- // same whether it draws its internals or not. The text selectors stay
183279
- // for the title of a node whose header strip is not rendered.
183280
- ...model.kind === "iv" && !isGeo ? { dragHandle: ".rf-node-header, .dnode-kind, .dnode-label" } : {}
183275
+ selectable: true
183276
+ // issue #226: NO `dragHandle` on a plain node, in any view.
183277
+ //
183278
+ // Issue #58 had scoped an Interconnection View part's drag to its title
183279
+ // strip for one reason: "as long as there is nothing to grab it should be
183280
+ // possible to pan the canvas". Issue #225 then made a left drag under the
183281
+ // select tool an editing gesture that NEVER pans (canvas-navigation.ts,
183282
+ // `panOnDrag: [1]`), and locks the nodes outright under the hand tool, where
183283
+ // a left drag pans over a part regardless. The restriction therefore bought
183284
+ // nothing any more and turned the whole body of every IV part into dead
183285
+ // area: press and drag there and the part did not move and the canvas did
183286
+ // not pan. That is the reported "sometimes I mousedown and drag and it does
183287
+ // not move". A part now moves from anywhere on its body, like every other
183288
+ // node in every other view; a click there still just selects it.
183289
+ //
183290
+ // Container FRAMES keep `.rf-frame-header` (set where they are built): their
183291
+ // interior is deliberately click-through so the connectors drawn underneath
183292
+ // stay selectable, so there is nothing there to grab in the first place.
183281
183293
  });
183282
183294
  }
183283
183295
  if (model.kind === "sv") {
@@ -197210,7 +197222,7 @@ body {
197210
197222
  * connect-target -> the whole port square lights up as the ready-to-land cue, with
197211
197223
  * the dotted anchor point (.dport-anchor) naming the dock edge. */
197212
197224
  .dnode-port.moving { stroke: var(--accent); stroke-width: 2.6; filter: drop-shadow(0 0 4px var(--accent)); }
197213
- .dnode-port.connect-target { stroke: var(--cyan); stroke-width: 2.8; fill: var(--cyan); fill-opacity: 0.24; filter: drop-shadow(0 0 5px var(--cyan)); }
197225
+ .dnode-port.connect-target { stroke: var(--cyan); stroke-width: 2.8; fill: var(--element-fill); fill-opacity: 1; filter: drop-shadow(0 0 5px var(--cyan)); }
197214
197226
  /* The connect affordance shown in front of a SELECTED port (drag it to wire). */
197215
197227
  .dport-plus { stroke: var(--accent); stroke-width: 1.6; stroke-linecap: round; pointer-events: none; }
197216
197228
  /* The four dotted anchor points a connector docks onto, shown while a connection
@@ -197861,11 +197873,16 @@ svg.react-flow__connectionline { z-index: 1; }
197861
197873
  .rf-frame .dframe-rect { pointer-events: none; }
197862
197874
  .rf-frame-header { position: absolute; pointer-events: auto; cursor: move; }
197863
197875
  /* issue #58 / user report 2026-08-11 \u2014 the matching title strip on an
197864
- * Interconnection View PART node. It is what the node's dragHandle selects, so
197865
- * a part reads and moves identically whether it draws its internals (a frame) or
197866
- * has them hidden (a node) \u2014 and shows the same move cursor either way, instead
197867
- * of React Flow's grab hand over a body that cannot actually be dragged. */
197876
+ * Interconnection View PART node, so a part reads identically whether it draws
197877
+ * its internals (a frame) or has them hidden (a node). Issue 226 removed the
197878
+ * dragHandle this strip used to select; it stays for the title tooltip and to
197879
+ * keep the title area's cursor the same as a frame's. */
197868
197880
  .rf-node-header { position: absolute; pointer-events: auto; cursor: move; }
197881
+ /* issue 226. A node whose whole body moves says so over its whole body. React
197882
+ * Flow's grab hand used to sit next to the title strip's move cross even though
197883
+ * only the strip could be grabbed; both are the move cross now. The hand tool's
197884
+ * rules above outrank this one, and a locked node loses the class anyway. */
197885
+ .react-flow.rf-tool-select .rf-node-movable { cursor: move; }
197869
197886
  /* REQ-379 \u2014 the frame's BORDER band is hover-sensitive (the interior stays
197870
197887
  * click-through): hovering it reveals and mounts the frame's side connect points,
197871
197888
  * which are drawn on that same border. Stroke-only hit testing, so nothing but the
@@ -197873,6 +197890,9 @@ svg.react-flow__connectionline { z-index: 1; }
197873
197890
  .dframe-hover-band { stroke-width: 12; pointer-events: stroke; }
197874
197891
  .rf-frame .dport-hit { pointer-events: auto; }
197875
197892
  .rf-frame .react-flow__handle { pointer-events: auto; }
197893
+ /* REQ-370: a frame is click-through, but its stacked handles still obey React
197894
+ * Flow's active end choice. An inactive twin must not cover an eligible end. */
197895
+ .react-flow.sysml-connecting .rf-frame .react-flow__handle:not(.connectionindicator) { pointer-events: none; }
197876
197896
  .rf-frame .react-flow__resize-control { pointer-events: auto; }
197877
197897
  .rf-node .react-flow__resize-control.fork-join-resize {
197878
197898
  width: 14px;
@@ -197930,7 +197950,11 @@ svg.react-flow__connectionline { z-index: 1; }
197930
197950
  * strip. All connection dots share ONE visible 5px size so the affordance reads the same
197931
197951
  * everywhere. */
197932
197952
  .react-flow__handle.rf-handle-port { width: ${PORT_CONNECT_DOT_HIT}px; height: ${PORT_CONNECT_DOT_HIT}px; --rf-dot-size: 5px; z-index: 6; }
197933
- .react-flow__handle.rf-handle-side { width: 12px; height: 12px; --rf-dot-size: 5px; z-index: 2; }
197953
+ .react-flow__handle.rf-handle-side { width: ${BODY_SNAP_POINT_HIT_SIZE}px; height: ${BODY_SNAP_POINT_HIT_SIZE}px; --rf-dot-size: 5px; z-index: 2; }
197954
+ /* REQ-370: body snap points are hollow circles. Port and pin endpoints are
197955
+ * solid squares, so adjacent targets cannot be mistaken for each other. */
197956
+ .react-flow__handle.rf-handle-side::after { background: var(--bg-1); border: 1.5px solid var(--cyan); }
197957
+ .react-flow__handle.rf-handle-port::after { border-radius: 1px; }
197934
197958
  /* The start/done centre stays available for normal node selection and dragging
197935
197959
  * even though four 12px handle targets meet over these exact-size glyphs. The
197936
197960
  * perimeter dots remain above the rest of the node and keep their full hit area. */
@@ -197944,25 +197968,32 @@ svg.react-flow__connectionline { z-index: 1; }
197944
197968
  /* The whole-glyph DROP target on a port's inner dock side. It exists ALWAYS (so a
197945
197969
  * delegation connector docking on the inner side resolves and isn't dropped by React
197946
197970
  * Flow), but is click-through AT REST so it never steals the port's click-to-select or
197947
- * the drag-to-move strip; while a connector is being dragged, only React Flow's valid
197948
- * ".connectionindicator" half becomes pointer-active, so elementFromPoint returns a
197949
- * connectable end handle for a drop ANYWHERE on the glyph \u2014 inside or on an edge. It
197971
+ * the drag-to-move strip; while a connector is being dragged, React Flow's eligible
197972
+ * ".connectionindicator" twins become pointer-active, so elementFromPoint returns a
197973
+ * connectable end handle for a drop ANYWHERE on the glyph, inside or on an edge. It
197950
197974
  * draws nothing (the drop cue is the port SQUARE highlight); its box is sized inline
197951
197975
  * to PORT_GLYPH_SIZE by the renderer. */
197952
197976
  .react-flow__handle.rf-handle-portdrop { --rf-dot-size: 0px; z-index: 5; border-radius: 3px; }
197953
197977
  .react-flow__handle.rf-handle-portdrop::after { display: none; }
197954
197978
  /* Both rules are (0,4,0), higher than React Flow's own .react-flow__handle.connectionindicator
197955
- * (0,2,0) which otherwise forces pointer-events all on every connectable handle. During a drag
197956
- * we still restrict pointer events to ".connectionindicator": each DualHandle renders a stacked
197957
- * target + source, and the non-end half must stay transparent or it can make elementFromPoint
197958
- * choose an invalid handle. */
197979
+ * (0,2,0) which otherwise forces pointer-events on every connectable handle. During a drag
197980
+ * we still restrict pointer events to ".connectionindicator". Each DualHandle renders a stacked
197981
+ * target and source. Loose-mode reconnect can end on either twin, while semantic validation
197982
+ * still rejects an incompatible model endpoint. */
197959
197983
  .react-flow:not(.sysml-connecting) .react-flow__handle.rf-handle-portdrop { pointer-events: none; }
197960
197984
  .react-flow.sysml-connecting .react-flow__handle.rf-handle-portdrop.connectionindicator { pointer-events: auto; }
197961
197985
  /* REQ-006 \u2014 graph nodes advertise their default drag relationship with modest
197962
197986
  * cyan side dots; all valid targets light up while a drag is in progress. */
197963
197987
  .rf-node-graph:hover .react-flow__handle.rf-handle-side { opacity: 0.85; }
197964
197988
  .react-flow.sysml-connecting .react-flow__handle.rf-handle-side.connectionindicator,
197965
- .react-flow.sysml-connecting .react-flow__handle.rf-handle-lifeline.connectionindicator { opacity: 0.85; }
197989
+ .react-flow.sysml-connecting .react-flow__handle.rf-handle-lifeline.connectionindicator { opacity: 0.42; }
197990
+ /* The nearest semantically valid body target is stronger than the candidate
197991
+ * points. Port and pin targets use the highlighted endpoint glyph instead. */
197992
+ .react-flow.sysml-connecting .react-flow__handle.rf-handle-side.connectingto.valid { opacity: 1; }
197993
+ .react-flow.sysml-connecting .react-flow__handle.rf-handle-side.connectingto.valid::after {
197994
+ background: var(--cyan); box-shadow: 0 0 0 3px rgba(108,209,217,0.4);
197995
+ transform: translate(-50%, -50%) scale(1.35);
197996
+ }
197966
197997
  /* issues.md (#2) \u2014 the offset "+" bubble lights up (accent, ringed) while a
197967
197998
  * connector is being CREATED from this port. React Flow flags the start handle
197968
197999
  * with the connectingfrom class; the SQUARE port (not the bubble) lights up as
@@ -197972,10 +198003,14 @@ svg.react-flow__connectionline { z-index: 1; }
197972
198003
  .react-flow__handle.rf-handle:hover { opacity: 1; box-shadow: none; }
197973
198004
  .react-flow__handle.rf-handle:hover::after { box-shadow: 0 0 0 3px rgba(108,209,217,0.4); }
197974
198005
  .react-flow__handle.rf-handle.connectionindicator { cursor: crosshair; }
198006
+ /* REQ-370: an opaque port or action pin owns its whole footprint. Keep an
198007
+ * overlapping body handle mounted so an existing edge keeps its handle ID, but
198008
+ * never paint or hit-test it in idle, hover, connect, or reconnect states. */
198009
+ .react-flow__handle.rf-handle-side.rf-handle-occluded { visibility: hidden; pointer-events: none; }
197975
198010
  /* REQ-006 \u2014 port move overlay: a transparent hit-target ON the port glyph; drag
197976
198011
  * to slide the port along its owner's edge, click to select. Kept interactive even
197977
198012
  * inside a click-through container frame. */
197978
- .port-drag-hit { pointer-events: auto; cursor: grab; }
198013
+ .port-drag-hit { pointer-events: auto; cursor: grab; z-index: 4; }
197979
198014
  .port-drag-hit:active { cursor: grabbing; }
197980
198015
  .rf-frame .port-drag-hit { pointer-events: auto; }
197981
198016
  /* REQ-386 \u2014 a nested-port host's expansion control, on the port's INNER side
@@ -199345,6 +199380,99 @@ function GeoShapeNode({ id: id2, data, selected: selected2 }) {
199345
199380
  );
199346
199381
  }
199347
199382
 
199383
+ // ../extension/src/webview/diagram/flow/connection-gesture.ts
199384
+ function validPortDropTarget(connection, isKnownPort) {
199385
+ if (!connection?.inProgress || connection.isValid !== true) return void 0;
199386
+ const handleId = connection.toHandle?.id;
199387
+ const nodeId = connection.toHandle?.nodeId;
199388
+ if (!handleId || !nodeId) return void 0;
199389
+ const portId = parsePortAnchorHandleId(handleId)?.portId ?? handleId;
199390
+ return isKnownPort(portId) ? { nodeId, handleId, portId } : void 0;
199391
+ }
199392
+
199393
+ // ../extension/src/webview/diagram/flow/pointer-gesture.ts
199394
+ var POINTER_DRAG_THRESHOLD_PX = 4;
199395
+ function beginPointerGesture(host, spec) {
199396
+ const threshold = spec.threshold ?? POINTER_DRAG_THRESHOLD_PX;
199397
+ let moved = false;
199398
+ let active = true;
199399
+ let captureHeld = false;
199400
+ let lastDelivered;
199401
+ function isOurs(event) {
199402
+ return event?.pointerId === spec.pointerId;
199403
+ }
199404
+ function releaseCapture() {
199405
+ if (!captureHeld) return;
199406
+ captureHeld = false;
199407
+ try {
199408
+ spec.captureTarget?.releasePointerCapture(spec.pointerId);
199409
+ } catch {
199410
+ }
199411
+ }
199412
+ function finish(reason, event) {
199413
+ if (!active) return;
199414
+ active = false;
199415
+ host.unbind("pointermove", onMove);
199416
+ host.unbind("pointerup", onUp);
199417
+ host.unbind("pointercancel", onCancel);
199418
+ host.unbind("blur", onInterrupt);
199419
+ releaseCapture();
199420
+ spec.onEnd({ reason, moved, event });
199421
+ }
199422
+ function deliverMove(event) {
199423
+ if (!moved && Math.hypot(event.clientX - spec.startX, event.clientY - spec.startY) < threshold) return;
199424
+ moved = true;
199425
+ if (lastDelivered?.x === event.clientX && lastDelivered.y === event.clientY) return;
199426
+ lastDelivered = { x: event.clientX, y: event.clientY };
199427
+ spec.onMove(event);
199428
+ }
199429
+ function onMove(event) {
199430
+ if (!active || !isOurs(event)) return;
199431
+ if (event.buttons === 0) {
199432
+ finish("cancel");
199433
+ return;
199434
+ }
199435
+ deliverMove(event);
199436
+ }
199437
+ function onUp(event) {
199438
+ if (!isOurs(event)) return;
199439
+ deliverMove(event);
199440
+ finish("up", event);
199441
+ }
199442
+ function onCancel(event) {
199443
+ if (isOurs(event)) finish("cancel");
199444
+ }
199445
+ function onInterrupt() {
199446
+ finish("cancel");
199447
+ }
199448
+ host.bind("pointermove", onMove);
199449
+ host.bind("pointerup", onUp);
199450
+ host.bind("pointercancel", onCancel);
199451
+ host.bind("blur", onInterrupt);
199452
+ try {
199453
+ if (spec.captureTarget) {
199454
+ spec.captureTarget.setPointerCapture(spec.pointerId);
199455
+ captureHeld = true;
199456
+ }
199457
+ } catch {
199458
+ }
199459
+ return {
199460
+ get moved() {
199461
+ return moved;
199462
+ },
199463
+ get active() {
199464
+ return active;
199465
+ },
199466
+ abort: () => finish("abort")
199467
+ };
199468
+ }
199469
+ function windowPointerHost() {
199470
+ return {
199471
+ bind: (type, listener) => window.addEventListener(type, listener),
199472
+ unbind: (type, listener) => window.removeEventListener(type, listener)
199473
+ };
199474
+ }
199475
+
199348
199476
  // ../extension/src/webview/diagram/flow/nodes.tsx
199349
199477
  var import_jsx_runtime4 = __toESM(require_jsx_runtime());
199350
199478
  var SIDES = [...PORT_DOCK_SIDES];
@@ -199395,7 +199523,9 @@ var SIDE_NORMAL = {
199395
199523
  var clampOffset = (v) => Math.max(0.06, Math.min(0.94, v));
199396
199524
  function usePortDrag(containerRef, w, h, topReserve, onPortMove, onPortPreview, onSelect) {
199397
199525
  const [dragId, setDragId] = (0, import_react8.useState)(void 0);
199398
- const onPortPointerDown = (portId, e, draggable = true) => {
199526
+ const gestureRef = (0, import_react8.useRef)(void 0);
199527
+ (0, import_react8.useEffect)(() => () => gestureRef.current?.abort(), []);
199528
+ const onPortPointerDown = (portId, e, origin, draggable = true) => {
199399
199529
  if (e.button !== 0) return;
199400
199530
  e.stopPropagation();
199401
199531
  const el2 = containerRef.current;
@@ -199403,40 +199533,47 @@ function usePortDrag(containerRef, w, h, topReserve, onPortMove, onPortPreview,
199403
199533
  onSelect?.(portId);
199404
199534
  return;
199405
199535
  }
199406
- e.currentTarget.setPointerCapture?.(e.pointerId);
199536
+ gestureRef.current?.abort();
199407
199537
  const rect = el2.getBoundingClientRect();
199408
- const sx = e.clientX, sy = e.clientY;
199409
- let moved = false;
199410
199538
  const placement = (cx, cy) => {
199411
199539
  const lx = rect.width ? (cx - rect.left) / rect.width * w : 0;
199412
199540
  const ly = rect.height ? (cy - rect.top) / rect.height * h : 0;
199413
199541
  const nearest = nearestPortOwnerPlacement(lx, ly, w, h, topReserve);
199414
199542
  return { side: nearest.side, offset: clampOffset(nearest.offset) };
199415
199543
  };
199416
- const move = (ev) => {
199417
- if (!moved && Math.hypot(ev.clientX - sx, ev.clientY - sy) < 4) return;
199418
- moved = true;
199419
- const p = placement(ev.clientX, ev.clientY);
199420
- setDragId(portId);
199421
- onPortPreview?.(portId, p.side, p.offset);
199422
- };
199423
- const up = (ev) => {
199424
- window.removeEventListener("pointermove", move);
199425
- window.removeEventListener("pointerup", up);
199426
- if (moved) {
199544
+ gestureRef.current = beginPointerGesture(windowPointerHost(), {
199545
+ pointerId: e.pointerId,
199546
+ startX: e.clientX,
199547
+ startY: e.clientY,
199548
+ captureTarget: e.currentTarget,
199549
+ onMove: (ev) => {
199427
199550
  const p = placement(ev.clientX, ev.clientY);
199428
- onPortMove(portId, p.side, p.offset);
199429
- } else onSelect?.(portId);
199430
- setDragId(void 0);
199431
- };
199432
- window.addEventListener("pointermove", move);
199433
- window.addEventListener("pointerup", up);
199551
+ setDragId(portId);
199552
+ onPortPreview?.(portId, p.side, p.offset);
199553
+ },
199554
+ onEnd: ({ reason, moved, event }) => {
199555
+ gestureRef.current = void 0;
199556
+ setDragId(void 0);
199557
+ if (!moved) {
199558
+ if (reason === "up") onSelect?.(portId);
199559
+ return;
199560
+ }
199561
+ if (reason === "up" && event) {
199562
+ const p = placement(event.clientX, event.clientY);
199563
+ onPortMove(portId, p.side, p.offset);
199564
+ return;
199565
+ }
199566
+ onPortPreview?.(portId, origin.side, origin.offset);
199567
+ }
199568
+ });
199434
199569
  };
199435
199570
  return { dragId, onPortPointerDown };
199436
199571
  }
199437
- function DualHandle({ id: id2, side, x, y, kind, active, connectable = true, startable = false, size }) {
199438
- const style2 = size !== void 0 ? { left: x, top: y, width: size, height: size, transform: "translate(-50%,-50%)" } : { left: x, top: y, transform: "translate(-50%,-50%)" };
199439
- const cls = `rf-handle rf-handle-${kind}${active ? " active" : ""}`;
199572
+ function DualHandle({ id: id2, side, x, y, kind, active, connectable = true, startable = false, size, occluded = false }) {
199573
+ const dimensions = typeof size === "number" ? { width: size, height: size } : size;
199574
+ const style2 = dimensions ? { left: x, top: y, ...dimensions, transform: "translate(-50%,-50%)" } : { left: x, top: y, transform: "translate(-50%,-50%)" };
199575
+ const enabled = connectable && !occluded;
199576
+ const cls = `rf-handle rf-handle-${kind}${active ? " active" : ""}${occluded ? " rf-handle-occluded" : ""}`;
199440
199577
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
199441
199578
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
199442
199579
  Handle,
@@ -199446,9 +199583,9 @@ function DualHandle({ id: id2, side, x, y, kind, active, connectable = true, sta
199446
199583
  position: POS[side],
199447
199584
  style: style2,
199448
199585
  className: cls,
199449
- isConnectable: connectable,
199586
+ isConnectable: enabled,
199450
199587
  isConnectableStart: false,
199451
- isConnectableEnd: connectable
199588
+ isConnectableEnd: enabled
199452
199589
  }
199453
199590
  ),
199454
199591
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -199459,9 +199596,9 @@ function DualHandle({ id: id2, side, x, y, kind, active, connectable = true, sta
199459
199596
  position: POS[side],
199460
199597
  style: style2,
199461
199598
  className: cls,
199462
- isConnectable: connectable,
199463
- isConnectableStart: connectable && startable,
199464
- isConnectableEnd: false
199599
+ isConnectable: enabled,
199600
+ isConnectableStart: enabled && startable,
199601
+ isConnectableEnd: enabled
199465
199602
  }
199466
199603
  )
199467
199604
  ] });
@@ -200188,14 +200325,12 @@ function NodeBody({ node, data, selectedPortId, hoveredPortId, movingPortId, con
200188
200325
  ))
200189
200326
  ] });
200190
200327
  }
200191
- function SysmlNode({ id: id2, data, selected: selected2, width, height }) {
200328
+ function SysmlNode({ id: id2, data, draggable, selected: selected2, width, height }) {
200192
200329
  const ctx = useDiagram();
200193
200330
  const node = data.node;
200194
200331
  const myPortIds = node.ports?.map((p) => p.id) ?? [];
200195
200332
  const connectTargetPortId = useConnection((c) => {
200196
- const t = c.inProgress ? c.toHandle?.id : void 0;
200197
- const portId = parsePortAnchorHandleId(t)?.portId ?? t;
200198
- return portId && myPortIds.includes(portId) ? portId : void 0;
200333
+ return validPortDropTarget(c, (portId) => myPortIds.includes(portId))?.portId;
200199
200334
  });
200200
200335
  const w = width ?? data.w;
200201
200336
  const h = height ?? data.h;
@@ -200247,207 +200382,219 @@ function SysmlNode({ id: id2, data, selected: selected2, width, height }) {
200247
200382
  sideConnectable,
200248
200383
  (selected2 || sidePointsHovered || perimeterControl) && !isPackageNode
200249
200384
  ));
200385
+ const visiblePortRects = ports.map((port) => portGlyphRect(port, w, h, featureCompartmentHeight));
200250
200386
  useMountedHandleRegistration(id2, sideOffsets);
200251
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
200252
- "div",
200253
- {
200254
- ref: containerRef,
200255
- className: `rf-node${selected2 ? " selected" : ""}${nodeSideStartable ? " rf-node-graph" : ""}${dropClass}`,
200256
- style: { width: w, height: h, opacity: dim ? 0.18 : 1 },
200257
- onMouseEnter: () => setSidePointsHovered(true),
200258
- onMouseLeave: () => setSidePointsHovered(false),
200259
- onContextMenu: (e) => {
200260
- e.preventDefault();
200261
- e.stopPropagation();
200262
- ctx.onContextNode(id2, e.clientX, e.clientY);
200263
- },
200264
- children: [
200265
- resizable ? (() => {
200266
- if (isForkJoinShape(node.shape)) {
200267
- const horizontal = forkJoinOrientation({ w, h }) === "horizontal";
200268
- const limits = horizontal ? { minWidth: FORK_JOIN_MIN_LENGTH, minHeight: FORK_JOIN_THICKNESS, maxHeight: FORK_JOIN_THICKNESS } : { minWidth: FORK_JOIN_THICKNESS, maxWidth: FORK_JOIN_THICKNESS, minHeight: FORK_JOIN_MIN_LENGTH };
200269
- const direction = horizontal ? "horizontal" : "vertical";
200270
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
200271
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200272
- NodeResizeControl,
200273
- {
200274
- className: "fork-join-resize",
200275
- color: "var(--accent)",
200276
- position: horizontal ? "left" : "top",
200277
- resizeDirection: direction,
200278
- ...limits
200279
- }
200280
- ),
200281
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200282
- NodeResizeControl,
200283
- {
200284
- className: "fork-join-resize",
200285
- color: "var(--accent)",
200286
- position: horizontal ? "right" : "bottom",
200287
- resizeDirection: direction,
200288
- ...limits
200289
- }
200290
- )
200291
- ] });
200292
- }
200293
- const min2 = resizeMinFor(node.shape, h);
200294
- const minWidth = showsControls ? Math.max(min2.minWidth, COLLAPSED_CONTAINER_MIN_WIDTH) : min2.minWidth;
200295
- const minHeight = Math.max(min2.minHeight, data.featureCompartmentMinHeight ?? 0);
200296
- const maxWidth = data.kind === "gv" && node.shape !== "package" ? GV_NODE_MAX_WIDTH : void 0;
200297
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200298
- NodeResizer,
200299
- {
200300
- minWidth,
200301
- minHeight,
200302
- maxWidth,
200303
- maxHeight: min2.maxHeight,
200304
- isVisible: true,
200305
- color: "var(--accent)"
200306
- }
200307
- );
200308
- })() : null,
200309
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200310
- NodeBody,
200311
- {
200312
- node,
200313
- data: liveData,
200314
- selectedPortId,
200315
- hoveredPortId,
200316
- movingPortId: dragId,
200317
- connectTargetPortId,
200318
- showPortConnectStart: data.kind === "iv" || data.kind === "afv",
200319
- onPortSelect: !ctx.onPortMove && data.ports.length ? portSource : void 0,
200320
- onPortContext: ctx.onContextNode
200321
- }
200322
- ),
200323
- data.kind === "iv" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200324
- "div",
200325
- {
200326
- className: "rf-node-header",
200327
- title: node.name,
200328
- style: {
200329
- left: 0,
200330
- top: 0,
200331
- height: NODE_COMPARTMENT_TOP,
200332
- width: containerTitleArea(w, nodeShowsControls(node)).width
200387
+ return (
200388
+ // issue #226. A movable body advertises the move cursor over its whole
200389
+ // surface, not only over the title strip. The two used to disagree because
200390
+ // only the strip could actually be grabbed (see model-to-flow, dragHandle).
200391
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
200392
+ "div",
200393
+ {
200394
+ ref: containerRef,
200395
+ className: `rf-node${selected2 ? " selected" : ""}${nodeSideStartable ? " rf-node-graph" : ""}${dropClass}${draggable === false ? "" : " rf-node-movable"}`,
200396
+ style: { width: w, height: h, opacity: dim ? 0.18 : 1 },
200397
+ onMouseEnter: () => setSidePointsHovered(true),
200398
+ onMouseLeave: () => setSidePointsHovered(false),
200399
+ onContextMenu: (e) => {
200400
+ e.preventDefault();
200401
+ e.stopPropagation();
200402
+ ctx.onContextNode(id2, e.clientX, e.clientY);
200403
+ },
200404
+ children: [
200405
+ resizable ? (() => {
200406
+ if (isForkJoinShape(node.shape)) {
200407
+ const horizontal = forkJoinOrientation({ w, h }) === "horizontal";
200408
+ const limits = horizontal ? { minWidth: FORK_JOIN_MIN_LENGTH, minHeight: FORK_JOIN_THICKNESS, maxHeight: FORK_JOIN_THICKNESS } : { minWidth: FORK_JOIN_THICKNESS, maxWidth: FORK_JOIN_THICKNESS, minHeight: FORK_JOIN_MIN_LENGTH };
200409
+ const direction = horizontal ? "horizontal" : "vertical";
200410
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
200411
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200412
+ NodeResizeControl,
200413
+ {
200414
+ className: "fork-join-resize",
200415
+ color: "var(--accent)",
200416
+ position: horizontal ? "left" : "top",
200417
+ resizeDirection: direction,
200418
+ ...limits
200419
+ }
200420
+ ),
200421
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200422
+ NodeResizeControl,
200423
+ {
200424
+ className: "fork-join-resize",
200425
+ color: "var(--accent)",
200426
+ position: horizontal ? "right" : "bottom",
200427
+ resizeDirection: direction,
200428
+ ...limits
200429
+ }
200430
+ )
200431
+ ] });
200333
200432
  }
200334
- }
200335
- ) : null,
200336
- SIDES.flatMap((side, si) => {
200337
- const n2 = SIDE_NORMAL[side];
200338
- const out = nodeSideHandleOutset(node.shape);
200339
- const offsets = sideOffsets[si];
200340
- return offsets.map((offset2, i) => {
200341
- const { x, y } = sideSurfacePoint(node.shape, side, offset2, w, h, featureCompartmentHeight);
200433
+ const min2 = resizeMinFor(node.shape, h);
200434
+ const minWidth = showsControls ? Math.max(min2.minWidth, COLLAPSED_CONTAINER_MIN_WIDTH) : min2.minWidth;
200435
+ const minHeight = Math.max(min2.minHeight, data.featureCompartmentMinHeight ?? 0);
200436
+ const maxWidth = data.kind === "gv" && node.shape !== "package" ? GV_NODE_MAX_WIDTH : void 0;
200342
200437
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200343
- DualHandle,
200438
+ NodeResizer,
200344
200439
  {
200345
- id: sideAnchorHandleId(side, offset2),
200346
- side,
200347
- x: x + n2.x * out,
200348
- y: y + n2.y * out,
200349
- kind: "side",
200350
- connectable: sideConnectable,
200351
- startable: nodeSideStartable,
200352
- size: perimeterControl ? 20 : void 0
200353
- },
200354
- `s-${side}-${i}`
200440
+ minWidth,
200441
+ minHeight,
200442
+ maxWidth,
200443
+ maxHeight: min2.maxHeight,
200444
+ isVisible: true,
200445
+ color: "var(--accent)"
200446
+ }
200355
200447
  );
200356
- });
200357
- }),
200358
- node.shape === "initial" || node.shape === "final" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "rf-circular-control-hit", "aria-hidden": "true" }) : null,
200359
- ports.flatMap((ph) => {
200360
- const rect = portGlyphRect(ph, w, h, featureCompartmentHeight);
200361
- const c = { x: rect.cx, y: rect.cy };
200362
- const startable = directRelationForHandle(data.kind, {
200363
- id: ph.port.id,
200364
- name: ph.port.name,
200365
- keyword: ph.port.pin ? "pin" : "port",
200366
- shape: ph.port.pin ? "pin" : "port",
200367
- source: ph.port.source
200368
- }) !== void 0;
200369
- const active = startable && (ph.port.id === hoveredPortId || ph.port.id === selectedPortId) || ph.port.id === connectTargetPortId;
200370
- const [outerId, innerId] = portConnectHandleIds(ph.port.id, ph.side);
200371
- const dropSize = Math.max(rect.across, 16);
200372
- const outer = portDockAt(rect, ph.side, ph.side);
200373
- return [
200374
- // the DROP target stays centred over the glyph — it is a cover,
200375
- // not a dock point (the edge geometry is `dockPoint`'s business)
200376
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: innerId, side: OPPOSITE_DOCK_SIDE[ph.side], x: c.x, y: c.y, kind: "portdrop", size: dropSize, startable: false }, `${ph.port.id}-drop`),
200377
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: outerId, side: ph.side, x: outer.x, y: outer.y, kind: "port", active, startable }, `${ph.port.id}-out`)
200378
- ];
200379
- }),
200380
- ctx.onPortMove ? ports.map((ph) => {
200381
- const rect = portGlyphRect(ph, w, h, featureCompartmentHeight);
200382
- const nestedChild = ph.port.parentPort !== void 0;
200383
- const strip = portInteractionStrip(rect, ph.side);
200384
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200448
+ })() : null,
200449
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200450
+ NodeBody,
200451
+ {
200452
+ node,
200453
+ data: liveData,
200454
+ selectedPortId,
200455
+ hoveredPortId,
200456
+ movingPortId: dragId,
200457
+ connectTargetPortId,
200458
+ showPortConnectStart: data.kind === "iv" || data.kind === "afv",
200459
+ onPortSelect: !ctx.onPortMove && data.ports.length ? portSource : void 0,
200460
+ onPortContext: ctx.onContextNode
200461
+ }
200462
+ ),
200463
+ data.kind === "iv" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200385
200464
  "div",
200386
200465
  {
200387
- className: "port-drag-hit nodrag",
200388
- style: { position: "absolute", left: strip.cx, top: strip.cy, width: strip.w, height: strip.h, transform: "translate(-50%,-50%)" },
200389
- title: nestedChild ? `${ph.port.name} - click to select; drag a dot to connect (moves with its parent port)` : `${ph.port.name} - drag the centre to move along the edge; drag a dot to connect; click to select`,
200390
- onPointerEnter: () => setHoveredPortId(ph.port.id),
200391
- onPointerLeave: () => setHoveredPortId((cur) => cur === ph.port.id ? void 0 : cur),
200392
- onPointerDown: (e) => onPortPointerDown(ph.port.id, e, !nestedChild),
200393
- onContextMenu: (e) => {
200394
- e.preventDefault();
200395
- e.stopPropagation();
200396
- ctx.onContextNode(ph.port.id, e.clientX, e.clientY);
200466
+ className: "rf-node-header",
200467
+ title: node.name,
200468
+ style: {
200469
+ left: 0,
200470
+ top: 0,
200471
+ height: NODE_COMPARTMENT_TOP,
200472
+ width: containerTitleArea(w, nodeShowsControls(node)).width
200473
+ }
200474
+ }
200475
+ ) : null,
200476
+ SIDES.flatMap((side, si) => {
200477
+ const n2 = SIDE_NORMAL[side];
200478
+ const out = nodeSideHandleOutset(node.shape);
200479
+ const offsets = sideOffsets[si];
200480
+ return offsets.map((offset2, i) => {
200481
+ const { x, y } = sideSurfacePoint(node.shape, side, offset2, w, h, featureCompartmentHeight);
200482
+ const point = { x: x + n2.x * out, y: y + n2.y * out };
200483
+ const occluded = bodySnapPointOccludedByPort(point, visiblePortRects);
200484
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200485
+ DualHandle,
200486
+ {
200487
+ id: sideAnchorHandleId(side, offset2),
200488
+ side,
200489
+ x: point.x,
200490
+ y: point.y,
200491
+ kind: "side",
200492
+ occluded,
200493
+ connectable: sideConnectable,
200494
+ startable: nodeSideStartable,
200495
+ size: perimeterControl ? 20 : void 0
200496
+ },
200497
+ `s-${side}-${i}`
200498
+ );
200499
+ });
200500
+ }),
200501
+ node.shape === "initial" || node.shape === "final" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "rf-circular-control-hit", "aria-hidden": "true" }) : null,
200502
+ ports.flatMap((ph) => {
200503
+ const rect = portGlyphRect(ph, w, h, featureCompartmentHeight);
200504
+ const c = { x: rect.cx, y: rect.cy };
200505
+ const startable = directRelationForHandle(data.kind, {
200506
+ id: ph.port.id,
200507
+ name: ph.port.name,
200508
+ keyword: ph.port.pin ? "pin" : "port",
200509
+ shape: ph.port.pin ? "pin" : "port",
200510
+ source: ph.port.source
200511
+ }) !== void 0;
200512
+ const active = startable && (ph.port.id === hoveredPortId || ph.port.id === selectedPortId) || ph.port.id === connectTargetPortId;
200513
+ const [outerId, innerId] = portConnectHandleIds(ph.port.id, ph.side);
200514
+ const dropSize = {
200515
+ width: Math.max(rect.width, 16),
200516
+ height: Math.max(rect.height, 16)
200517
+ };
200518
+ const outer = portDockAt(rect, ph.side, ph.side);
200519
+ return [
200520
+ // the DROP target stays centred over the glyph — it is a cover,
200521
+ // not a dock point (the edge geometry is `dockPoint`'s business)
200522
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: innerId, side: OPPOSITE_DOCK_SIDE[ph.side], x: c.x, y: c.y, kind: "portdrop", size: dropSize, startable: false }, `${ph.port.id}-drop`),
200523
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: outerId, side: ph.side, x: outer.x, y: outer.y, kind: "port", active, startable }, `${ph.port.id}-out`)
200524
+ ];
200525
+ }),
200526
+ ctx.onPortMove ? ports.map((ph) => {
200527
+ const rect = portGlyphRect(ph, w, h, featureCompartmentHeight);
200528
+ const nestedChild = ph.port.parentPort !== void 0;
200529
+ const strip = portInteractionStrip(rect, ph.side);
200530
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200531
+ "div",
200532
+ {
200533
+ className: "port-drag-hit nodrag",
200534
+ style: { position: "absolute", left: strip.cx, top: strip.cy, width: strip.w, height: strip.h, transform: "translate(-50%,-50%)" },
200535
+ title: nestedChild ? `${ph.port.name} - click to select; drag a dot to connect (moves with its parent port)` : `${ph.port.name} - drag the centre to move along the edge; drag a dot to connect; click to select`,
200536
+ onPointerEnter: () => setHoveredPortId(ph.port.id),
200537
+ onPointerLeave: () => setHoveredPortId((cur) => cur === ph.port.id ? void 0 : cur),
200538
+ onPointerDown: (e) => onPortPointerDown(ph.port.id, e, { side: ph.side, offset: ph.offset }, !nestedChild),
200539
+ onContextMenu: (e) => {
200540
+ e.preventDefault();
200541
+ e.stopPropagation();
200542
+ ctx.onContextNode(ph.port.id, e.clientX, e.clientY);
200543
+ },
200544
+ onClick: (e) => e.stopPropagation()
200397
200545
  },
200398
- onClick: (e) => e.stopPropagation()
200399
- },
200400
- `pd-${ph.port.id}`
200401
- );
200402
- }) : null,
200403
- node.shape === "lifeline" ? (() => {
200404
- const bottom = data.lineBottom ?? h;
200405
- const handles = [];
200406
- for (const mh of data.msgHandles ?? []) {
200407
- handles.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: mh.id, side: "right", x: w / 2, y: mh.y, kind: "lifeline", startable: directRelation !== void 0 }, mh.id));
200408
- }
200409
- for (const ih of data.insertHandles ?? []) {
200410
- if (!directRelation || ih.y > bottom - 4) continue;
200411
- handles.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: ih.id, side: "right", x: w / 2, y: ih.y, kind: "lifeline", startable: true }, ih.id));
200412
- }
200413
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: handles });
200414
- })() : null,
200415
- ctx.onToggleNestedPort ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200416
- NestedPortToggles,
200417
- {
200418
- ports,
200419
- w,
200420
- h,
200421
- topReserve: featureCompartmentHeight,
200422
- onToggle: ctx.onToggleNestedPort
200423
- }
200424
- ) : null,
200425
- showsControls ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200426
- ContainerControls,
200427
- {
200428
- id: id2,
200429
- label: node.name,
200430
- internalNoun,
200431
- internalsVisible: !hiddenContainer,
200432
- featureCompartmentsVisible,
200433
- cornerRadius: nodeCornerRadius(node.shape, node.isDef, h, (node.compartments?.length ?? 0) > 0),
200434
- onToggleInternals: ctx.onToggleInternals,
200435
- onToggleFeatureCompartments: ctx.onToggleFeatureCompartments
200436
- }
200437
- ) : null,
200438
- editingHere && ctx.onEditCommit && ctx.onEditCancel ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200439
- InlineEditor,
200440
- {
200441
- initial: editing.initial,
200442
- mode: editing.mode,
200443
- elementKind: editing.elementKind,
200444
- onCommit: ctx.onEditCommit,
200445
- onCancel: ctx.onEditCancel,
200446
- w
200447
- }
200448
- ) : null
200449
- ]
200450
- }
200546
+ `pd-${ph.port.id}`
200547
+ );
200548
+ }) : null,
200549
+ node.shape === "lifeline" ? (() => {
200550
+ const bottom = data.lineBottom ?? h;
200551
+ const handles = [];
200552
+ for (const mh of data.msgHandles ?? []) {
200553
+ handles.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: mh.id, side: "right", x: w / 2, y: mh.y, kind: "lifeline", startable: directRelation !== void 0 }, mh.id));
200554
+ }
200555
+ for (const ih of data.insertHandles ?? []) {
200556
+ if (!directRelation || ih.y > bottom - 4) continue;
200557
+ handles.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: ih.id, side: "right", x: w / 2, y: ih.y, kind: "lifeline", startable: true }, ih.id));
200558
+ }
200559
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: handles });
200560
+ })() : null,
200561
+ ctx.onToggleNestedPort ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200562
+ NestedPortToggles,
200563
+ {
200564
+ ports,
200565
+ w,
200566
+ h,
200567
+ topReserve: featureCompartmentHeight,
200568
+ onToggle: ctx.onToggleNestedPort
200569
+ }
200570
+ ) : null,
200571
+ showsControls ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200572
+ ContainerControls,
200573
+ {
200574
+ id: id2,
200575
+ label: node.name,
200576
+ internalNoun,
200577
+ internalsVisible: !hiddenContainer,
200578
+ featureCompartmentsVisible,
200579
+ cornerRadius: nodeCornerRadius(node.shape, node.isDef, h, (node.compartments?.length ?? 0) > 0),
200580
+ onToggleInternals: ctx.onToggleInternals,
200581
+ onToggleFeatureCompartments: ctx.onToggleFeatureCompartments
200582
+ }
200583
+ ) : null,
200584
+ editingHere && ctx.onEditCommit && ctx.onEditCancel ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200585
+ InlineEditor,
200586
+ {
200587
+ initial: editing.initial,
200588
+ mode: editing.mode,
200589
+ elementKind: editing.elementKind,
200590
+ onCommit: ctx.onEditCommit,
200591
+ onCancel: ctx.onEditCancel,
200592
+ w
200593
+ }
200594
+ ) : null
200595
+ ]
200596
+ }
200597
+ )
200451
200598
  );
200452
200599
  }
200453
200600
  function packageFrameTabWidth(w) {
@@ -200509,9 +200656,7 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200509
200656
  const frameBottomHeight = loopUntil ? data.frameBottomHeight ?? LOOP_UNTIL_COMPARTMENT_HEIGHT : 0;
200510
200657
  const myPortIds = isPartitionLane ? [] : frame2.ports?.map((p) => p.id) ?? [];
200511
200658
  const connectTargetPortId = useConnection((c) => {
200512
- const t = c.inProgress ? c.toHandle?.id : void 0;
200513
- const portId = parsePortAnchorHandleId(t)?.portId ?? t;
200514
- return portId && myPortIds.includes(portId) ? portId : void 0;
200659
+ return validPortDropTarget(c, (portId) => myPortIds.includes(portId))?.portId;
200515
200660
  });
200516
200661
  const w = width ?? data.w;
200517
200662
  const h = height ?? data.h;
@@ -200582,6 +200727,7 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200582
200727
  sideConnectable,
200583
200728
  (selected2 || sidePointsHovered) && !isPackageFrame
200584
200729
  ));
200730
+ const visiblePortRects = showPorts ? ports.map((port) => portGlyphRect(port, w, h, featureCompartmentHeight)) : [];
200585
200731
  useMountedHandleRegistration(id2, sideOffsets);
200586
200732
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
200587
200733
  "div",
@@ -200796,17 +200942,19 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200796
200942
  SIDES.flatMap((side, si) => {
200797
200943
  const offsets = sideOffsets[si];
200798
200944
  return offsets.map((offset2, i) => {
200799
- const { x, y } = sidePoint(side, offset2, w, h, featureCompartmentHeight);
200945
+ const point = sidePoint(side, offset2, w, h, featureCompartmentHeight);
200946
+ const occluded = bodySnapPointOccludedByPort(point, visiblePortRects);
200800
200947
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200801
200948
  DualHandle,
200802
200949
  {
200803
200950
  id: sideAnchorHandleId(side, offset2),
200804
200951
  side,
200805
- x,
200806
- y,
200952
+ x: point.x,
200953
+ y: point.y,
200807
200954
  kind: "side",
200808
200955
  connectable: sideConnectable,
200809
- startable: frameSideStartable
200956
+ startable: frameSideStartable,
200957
+ occluded
200810
200958
  },
200811
200959
  `s-${side}-${i}`
200812
200960
  );
@@ -200824,7 +200972,10 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200824
200972
  }) !== void 0;
200825
200973
  const active = startable && (ph.port.id === hoveredPortId || ph.port.id === selectedPortId) || ph.port.id === connectTargetPortId;
200826
200974
  const [outerId, innerId] = portConnectHandleIds(ph.port.id, ph.side);
200827
- const dropSize = Math.max(rect.across, 16);
200975
+ const dropSize = {
200976
+ width: Math.max(rect.width, 16),
200977
+ height: Math.max(rect.height, 16)
200978
+ };
200828
200979
  const outer = portDockAt(rect, ph.side, ph.side);
200829
200980
  return [
200830
200981
  // the DROP target stays centred over the glyph — it is a cover,
@@ -200845,7 +200996,7 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200845
200996
  title: nestedChild ? `${ph.port.name} - click to select; drag a dot to connect (moves with its parent port)` : `${ph.port.name} - drag the centre to move along the edge; drag a dot to connect; click to select`,
200846
200997
  onPointerEnter: () => setHoveredPortId(ph.port.id),
200847
200998
  onPointerLeave: () => setHoveredPortId((cur) => cur === ph.port.id ? void 0 : cur),
200848
- onPointerDown: (e) => onPortPointerDown(ph.port.id, e, !nestedChild),
200999
+ onPointerDown: (e) => onPortPointerDown(ph.port.id, e, { side: ph.side, offset: ph.offset }, !nestedChild),
200849
201000
  onContextMenu: (e) => {
200850
201001
  e.preventDefault();
200851
201002
  e.stopPropagation();
@@ -201220,7 +201371,7 @@ async function runExport(command) {
201220
201371
  }
201221
201372
 
201222
201373
  // src/main.ts
201223
- var VERSION2 = true ? "0.20.1" : "dev";
201374
+ var VERSION2 = true ? "0.20.2" : "dev";
201224
201375
  function display(file) {
201225
201376
  const rel2 = path9.relative(process.cwd(), file);
201226
201377
  return rel2 && !rel2.startsWith("..") ? rel2.split(path9.sep).join("/") : file;