system-canvas-standalone 0.1.1 → 0.2.1

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.
@@ -12759,12 +12759,12 @@ var SystemCanvas = (() => {
12759
12759
  render: () => render,
12760
12760
  themes: () => themes
12761
12761
  });
12762
- var import_react21 = __toESM(require_react(), 1);
12762
+ var import_react22 = __toESM(require_react(), 1);
12763
12763
  var import_client = __toESM(require_client(), 1);
12764
12764
 
12765
12765
  // ../react/dist/components/SystemCanvas.js
12766
- var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
12767
- var import_react20 = __toESM(require_react(), 1);
12766
+ var import_jsx_runtime28 = __toESM(require_jsx_runtime(), 1);
12767
+ var import_react21 = __toESM(require_react(), 1);
12768
12768
 
12769
12769
  // ../core/dist/themes/dark.js
12770
12770
  var darkTheme = {
@@ -12827,6 +12827,25 @@ var SystemCanvas = (() => {
12827
12827
  fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', monospace",
12828
12828
  fontSize: 12
12829
12829
  },
12830
+ contextMenu: {
12831
+ background: "rgba(15, 23, 42, 0.96)",
12832
+ // slate-900 near-opaque
12833
+ borderColor: "rgba(71, 85, 105, 0.6)",
12834
+ // slate-600
12835
+ borderRadius: 8,
12836
+ itemColor: "#e2e8f0",
12837
+ // slate-200
12838
+ itemHoverBackground: "rgba(71, 85, 105, 0.35)",
12839
+ destructiveItemColor: "#fb7185",
12840
+ // rose-400
12841
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', monospace",
12842
+ fontSize: 12,
12843
+ paddingY: 4,
12844
+ paddingX: 4,
12845
+ itemPaddingY: 5,
12846
+ itemPaddingX: 10,
12847
+ shadow: "0 8px 24px rgba(0,0,0,0.35)"
12848
+ },
12830
12849
  lanes: {
12831
12850
  bandFillEven: "rgba(30, 41, 59, 0.18)",
12832
12851
  // slate-800 soft
@@ -12912,6 +12931,21 @@ var SystemCanvas = (() => {
12912
12931
  fontFamily: "'JetBrains Mono', monospace",
12913
12932
  fontSize: 11
12914
12933
  },
12934
+ contextMenu: {
12935
+ background: "rgba(0, 0, 0, 0.96)",
12936
+ borderColor: "rgba(55, 65, 81, 0.7)",
12937
+ borderRadius: 6,
12938
+ itemColor: "#d1d5db",
12939
+ itemHoverBackground: "rgba(0, 255, 136, 0.12)",
12940
+ destructiveItemColor: "#f87171",
12941
+ fontFamily: "'JetBrains Mono', monospace",
12942
+ fontSize: 11,
12943
+ paddingY: 3,
12944
+ paddingX: 3,
12945
+ itemPaddingY: 5,
12946
+ itemPaddingX: 10,
12947
+ shadow: "0 8px 24px rgba(0,255,136,0.08)"
12948
+ },
12915
12949
  lanes: {
12916
12950
  bandFillEven: "rgba(17, 17, 17, 0.7)",
12917
12951
  bandFillOdd: "rgba(0, 0, 0, 0)",
@@ -12998,6 +13032,21 @@ var SystemCanvas = (() => {
12998
13032
  fontFamily: "'Inter', 'Helvetica Neue', sans-serif",
12999
13033
  fontSize: 12
13000
13034
  },
13035
+ contextMenu: {
13036
+ background: "rgba(255, 255, 255, 0.98)",
13037
+ borderColor: "rgba(203, 213, 225, 0.9)",
13038
+ borderRadius: 8,
13039
+ itemColor: "#1e293b",
13040
+ itemHoverBackground: "rgba(148, 163, 184, 0.18)",
13041
+ destructiveItemColor: "#dc2626",
13042
+ fontFamily: "'Inter', 'Helvetica Neue', sans-serif",
13043
+ fontSize: 12,
13044
+ paddingY: 4,
13045
+ paddingX: 4,
13046
+ itemPaddingY: 5,
13047
+ itemPaddingX: 10,
13048
+ shadow: "0 8px 24px rgba(15,23,42,0.12)"
13049
+ },
13001
13050
  lanes: {
13002
13051
  bandFillEven: "rgba(148, 163, 184, 0.10)",
13003
13052
  // slate-400 soft
@@ -13077,6 +13126,21 @@ var SystemCanvas = (() => {
13077
13126
  fontFamily: "'JetBrains Mono', monospace",
13078
13127
  fontSize: 11
13079
13128
  },
13129
+ contextMenu: {
13130
+ background: "rgba(10, 22, 40, 0.97)",
13131
+ borderColor: "rgba(74, 144, 217, 0.45)",
13132
+ borderRadius: 6,
13133
+ itemColor: "#d4e5f7",
13134
+ itemHoverBackground: "rgba(74, 144, 217, 0.18)",
13135
+ destructiveItemColor: "#fb7185",
13136
+ fontFamily: "'JetBrains Mono', monospace",
13137
+ fontSize: 11,
13138
+ paddingY: 3,
13139
+ paddingX: 3,
13140
+ itemPaddingY: 5,
13141
+ itemPaddingX: 10,
13142
+ shadow: "0 8px 24px rgba(10,22,40,0.5)"
13143
+ },
13080
13144
  lanes: {
13081
13145
  bandFillEven: "rgba(19, 34, 68, 0.45)",
13082
13146
  bandFillOdd: "rgba(19, 34, 68, 0.15)",
@@ -13154,6 +13218,21 @@ var SystemCanvas = (() => {
13154
13218
  fontFamily: "'IBM Plex Mono', monospace",
13155
13219
  fontSize: 12
13156
13220
  },
13221
+ contextMenu: {
13222
+ background: "rgba(26, 23, 20, 0.97)",
13223
+ borderColor: "rgba(107, 94, 74, 0.6)",
13224
+ borderRadius: 8,
13225
+ itemColor: "#e8ddd0",
13226
+ itemHoverBackground: "rgba(107, 94, 74, 0.3)",
13227
+ destructiveItemColor: "#fb7185",
13228
+ fontFamily: "'IBM Plex Mono', monospace",
13229
+ fontSize: 12,
13230
+ paddingY: 4,
13231
+ paddingX: 4,
13232
+ itemPaddingY: 5,
13233
+ itemPaddingX: 10,
13234
+ shadow: "0 8px 24px rgba(0,0,0,0.4)"
13235
+ },
13157
13236
  lanes: {
13158
13237
  bandFillEven: "rgba(42, 37, 32, 0.55)",
13159
13238
  bandFillOdd: "rgba(42, 37, 32, 0.2)",
@@ -13232,6 +13311,21 @@ var SystemCanvas = (() => {
13232
13311
  fontFamily: "'Inter', 'Helvetica Neue', sans-serif",
13233
13312
  fontSize: 12
13234
13313
  },
13314
+ contextMenu: {
13315
+ background: "rgba(15, 17, 21, 0.96)",
13316
+ borderColor: "rgba(63, 69, 83, 0.8)",
13317
+ borderRadius: 8,
13318
+ itemColor: "#e8ebf2",
13319
+ itemHoverBackground: "rgba(90, 100, 120, 0.3)",
13320
+ destructiveItemColor: "#fb7185",
13321
+ fontFamily: "'Inter', 'Helvetica Neue', sans-serif",
13322
+ fontSize: 12,
13323
+ paddingY: 4,
13324
+ paddingX: 4,
13325
+ itemPaddingY: 5,
13326
+ itemPaddingX: 10,
13327
+ shadow: "0 8px 24px rgba(0,0,0,0.4)"
13328
+ },
13235
13329
  // Slightly stronger lane fills than the other themes — roadmap columns
13236
13330
  // should read as the primary structure of the canvas.
13237
13331
  lanes: {
@@ -13458,10 +13552,16 @@ var SystemCanvas = (() => {
13458
13552
  group: { ...base.group, ...partial.group },
13459
13553
  breadcrumbs: { ...base.breadcrumbs, ...partial.breadcrumbs },
13460
13554
  lanes: { ...base.lanes, ...partial.lanes },
13555
+ // contextMenu is optional on CanvasTheme but darkTheme always defines it,
13556
+ // so the resolved theme is guaranteed to have a populated block as long
13557
+ // as `base` is a complete theme (which is the contract).
13558
+ contextMenu: partial.contextMenu ? { ...base.contextMenu, ...partial.contextMenu } : base.contextMenu,
13461
13559
  presetColors: { ...base.presetColors, ...partial.presetColors },
13462
13560
  categories: { ...base.categories, ...partial.categories },
13463
13561
  icons: partial.icons ? { ...base.icons ?? {}, ...partial.icons } : base.icons,
13464
- nodeActions: partial.nodeActions ?? base.nodeActions
13562
+ nodeActions: partial.nodeActions ?? base.nodeActions,
13563
+ showToolbarDelete: partial.showToolbarDelete ?? base.showToolbarDelete,
13564
+ toolbarAlign: partial.toolbarAlign ?? base.toolbarAlign
13465
13565
  };
13466
13566
  }
13467
13567
  function resolveColor(color2, theme) {
@@ -13809,6 +13909,29 @@ var SystemCanvas = (() => {
13809
13909
  return getNodeActions(theme);
13810
13910
  }
13811
13911
 
13912
+ // ../core/dist/contextMenu.js
13913
+ function matchesContextMenuItem(item, node, ctx) {
13914
+ const m = item.match;
13915
+ if (!m)
13916
+ return true;
13917
+ if (m.categories) {
13918
+ if (!node.category)
13919
+ return false;
13920
+ if (!m.categories.includes(node.category))
13921
+ return false;
13922
+ }
13923
+ if (m.types) {
13924
+ if (!m.types.includes(node.type))
13925
+ return false;
13926
+ }
13927
+ if (m.when && !m.when(node, ctx))
13928
+ return false;
13929
+ return true;
13930
+ }
13931
+ function filterContextMenuItems(items, node, ctx) {
13932
+ return items.filter((item) => matchesContextMenuItem(item, node, ctx));
13933
+ }
13934
+
13812
13935
  // ../core/dist/rollup.js
13813
13936
  function rollupNodes(canvas, predicate) {
13814
13937
  const nodes = canvas?.nodes ?? [];
@@ -13839,6 +13962,23 @@ var SystemCanvas = (() => {
13839
13962
  var TAB_BADGE_EM = 1.4;
13840
13963
  var TAB_BADGE_LIFT = 10;
13841
13964
  var TAB_BADGE_OVERHANG = 8;
13965
+ function cornerSlotClearance(spec, fontSize) {
13966
+ if (!spec)
13967
+ return 0;
13968
+ if (spec.kind !== "dot" && spec.kind !== "icon")
13969
+ return 0;
13970
+ const corner = CORNER_EM * fontSize;
13971
+ let renderedWidth = corner;
13972
+ if (spec.kind === "icon") {
13973
+ const explicitSize = spec.size;
13974
+ if (typeof explicitSize === "number" && explicitSize > corner) {
13975
+ renderedWidth = explicitSize;
13976
+ }
13977
+ }
13978
+ const gap = Math.max(8, Math.round(fontSize * 0.75));
13979
+ const overhang = Math.max(0, (renderedWidth - corner) / 2);
13980
+ return overhang + corner + gap;
13981
+ }
13842
13982
  function computeCategorySlotRegions(node, theme, slots) {
13843
13983
  const { x, y, width, height } = node;
13844
13984
  const fs = theme.node.fontSize;
@@ -13906,12 +14046,16 @@ var SystemCanvas = (() => {
13906
14046
  width: tab,
13907
14047
  height: tab
13908
14048
  },
13909
- topLeft: {
13910
- x: x + CORNER_INSET,
13911
- y: y + CORNER_INSET,
13912
- width: corner,
13913
- height: corner
13914
- },
14049
+ topLeft: (() => {
14050
+ const usesInlineRow = slots?.topLeft !== void 0 && (slots.topLeft.kind === "dot" || slots.topLeft.kind === "icon") && slots.header === void 0;
14051
+ const slotY = usesInlineRow ? y + (height - corner) / 2 : y + CORNER_INSET;
14052
+ return {
14053
+ x: x + CORNER_INSET,
14054
+ y: slotY,
14055
+ width: corner,
14056
+ height: corner
14057
+ };
14058
+ })(),
13915
14059
  topRight: {
13916
14060
  x: x + width - CORNER_INSET - corner,
13917
14061
  y: y + CORNER_INSET,
@@ -13930,18 +14074,49 @@ var SystemCanvas = (() => {
13930
14074
  width: corner,
13931
14075
  height: corner
13932
14076
  },
13933
- header: {
13934
- x: x + HEADER_INSET_X,
13935
- y: y + HEADER_INSET_Y,
13936
- width: Math.max(0, width - HEADER_INSET_X * 2),
13937
- height: header
13938
- },
13939
- footer: {
13940
- x: x + HEADER_INSET_X,
13941
- y: y + height - FOOTER_INSET_Y - footer,
13942
- width: Math.max(0, width - HEADER_INSET_X * 2),
13943
- height: footer
13944
- },
14077
+ header: (() => {
14078
+ let hx = x + HEADER_INSET_X;
14079
+ let hw = Math.max(0, width - HEADER_INSET_X * 2);
14080
+ const leftClear = cornerSlotClearance(slots?.topLeft, fs);
14081
+ if (leftClear > 0) {
14082
+ const leftEdge = x + CORNER_INSET + leftClear;
14083
+ const shift = Math.max(0, leftEdge - hx);
14084
+ hx += shift;
14085
+ hw = Math.max(0, hw - shift);
14086
+ }
14087
+ const rightClear = cornerSlotClearance(slots?.topRight, fs);
14088
+ if (rightClear > 0) {
14089
+ const rightEdge = x + width - CORNER_INSET - rightClear;
14090
+ const currentRight = hx + hw;
14091
+ const shrink = Math.max(0, currentRight - rightEdge);
14092
+ hw = Math.max(0, hw - shrink);
14093
+ }
14094
+ return { x: hx, y: y + HEADER_INSET_Y, width: hw, height: header };
14095
+ })(),
14096
+ footer: (() => {
14097
+ let fx = x + HEADER_INSET_X;
14098
+ let fw = Math.max(0, width - HEADER_INSET_X * 2);
14099
+ const leftClear = cornerSlotClearance(slots?.bottomLeft, fs);
14100
+ if (leftClear > 0) {
14101
+ const leftEdge = x + CORNER_INSET + leftClear;
14102
+ const shift = Math.max(0, leftEdge - fx);
14103
+ fx += shift;
14104
+ fw = Math.max(0, fw - shift);
14105
+ }
14106
+ const rightClear = cornerSlotClearance(slots?.bottomRight, fs);
14107
+ if (rightClear > 0) {
14108
+ const rightEdge = x + width - CORNER_INSET - rightClear;
14109
+ const currentRight = fx + fw;
14110
+ const shrink = Math.max(0, currentRight - rightEdge);
14111
+ fw = Math.max(0, fw - shrink);
14112
+ }
14113
+ return {
14114
+ x: fx,
14115
+ y: y + height - FOOTER_INSET_Y - footer,
14116
+ width: fw,
14117
+ height: footer
14118
+ };
14119
+ })(),
13945
14120
  body: {
13946
14121
  x: bodyX,
13947
14122
  y: bodyY,
@@ -14064,13 +14239,26 @@ var SystemCanvas = (() => {
14064
14239
  left = regions.leftEdge.width + 4;
14065
14240
  if (slots.rightEdge)
14066
14241
  right = regions.rightEdge.width + 4;
14067
- if (slots.topLeft && slots.topLeft.kind === "dot") {
14068
- left = Math.max(left, regions.topLeft.x + regions.topLeft.width - node.x + 6);
14242
+ if (slots.topLeft && (slots.topLeft.kind === "dot" || slots.topLeft.kind === "icon")) {
14243
+ const region = regions.topLeft;
14244
+ let renderedWidth = region.width;
14245
+ if (slots.topLeft.kind === "icon") {
14246
+ const explicitSize = slots.topLeft.size;
14247
+ if (typeof explicitSize === "number" && explicitSize > region.width) {
14248
+ renderedWidth = explicitSize;
14249
+ }
14250
+ }
14251
+ const iconLeftFromNode = region.x - node.x + (region.width - renderedWidth) / 2;
14252
+ const gap = Math.max(8, Math.round(theme.node.fontSize * 0.75));
14253
+ left = Math.max(left, iconLeftFromNode + renderedWidth + gap);
14069
14254
  }
14070
- const isDashboard = slots.header !== void 0 || slots.topRight !== void 0 || slots.bodyTop !== void 0 || slots.footer !== void 0 || slots.topLeft !== void 0 && slots.topLeft.kind === "dot";
14255
+ const hasTopRowSignal = slots.header !== void 0 || slots.topRight !== void 0 || slots.bodyTop !== void 0 || slots.footer !== void 0;
14256
+ const hasInlineLeftMarker = slots.topLeft !== void 0 && (slots.topLeft.kind === "dot" || slots.topLeft.kind === "icon");
14257
+ const isDashboard = hasTopRowSignal || hasInlineLeftMarker;
14071
14258
  if (isDashboard) {
14072
- if (!slots.header)
14259
+ if (hasTopRowSignal && !slots.header) {
14073
14260
  top = Math.max(top, HEADER_INSET_Y);
14261
+ }
14074
14262
  left = Math.max(left, HEADER_INSET_X);
14075
14263
  right = Math.max(right, HEADER_INSET_X);
14076
14264
  }
@@ -14119,7 +14307,7 @@ var SystemCanvas = (() => {
14119
14307
  }
14120
14308
 
14121
14309
  // ../core/dist/text.js
14122
- var GLYPH_WIDTH_RATIO = 0.62;
14310
+ var GLYPH_WIDTH_RATIO = 0.55;
14123
14311
  function measureTextWidth(text, fontSize) {
14124
14312
  if (!text)
14125
14313
  return 0;
@@ -14168,6 +14356,29 @@ var SystemCanvas = (() => {
14168
14356
  }
14169
14357
  return out;
14170
14358
  }
14359
+ function truncateToWidth(text, maxWidth, fontSize) {
14360
+ if (!text)
14361
+ return text;
14362
+ if (maxWidth <= 0)
14363
+ return text;
14364
+ if (measureTextWidth(text, fontSize) <= maxWidth)
14365
+ return text;
14366
+ const ellipsis = "\u2026";
14367
+ let lo = 0;
14368
+ let hi = text.length;
14369
+ let best = 0;
14370
+ while (lo <= hi) {
14371
+ const mid = lo + hi >> 1;
14372
+ const candidate = text.slice(0, mid) + ellipsis;
14373
+ if (measureTextWidth(candidate, fontSize) <= maxWidth) {
14374
+ best = mid;
14375
+ lo = mid + 1;
14376
+ } else {
14377
+ hi = mid - 1;
14378
+ }
14379
+ }
14380
+ return text.slice(0, best) + ellipsis;
14381
+ }
14171
14382
  function ellipsize(line, maxWidth, fontSize) {
14172
14383
  const ellipsis = "\u2026";
14173
14384
  if (measureTextWidth(line + ellipsis, fontSize) <= maxWidth) {
@@ -14180,7 +14391,7 @@ var SystemCanvas = (() => {
14180
14391
  if (measureTextWidth(candidate, fontSize) <= maxWidth)
14181
14392
  return candidate;
14182
14393
  }
14183
- return line + ellipsis;
14394
+ return truncateToWidth(line, maxWidth, fontSize);
14184
14395
  }
14185
14396
 
14186
14397
  // ../core/dist/canvas.js
@@ -14484,7 +14695,12 @@ var SystemCanvas = (() => {
14484
14695
  onContextMenu({
14485
14696
  type,
14486
14697
  target,
14487
- position: canvasPos
14698
+ position: canvasPos,
14699
+ // Raw viewport coords from the original MouseEvent. Consumers
14700
+ // rendering a `position: fixed` floating menu need these — the
14701
+ // canvas-space `position` would be wrong because it walks
14702
+ // with the user's pan/zoom.
14703
+ screenPosition: { x: event.clientX, y: event.clientY }
14488
14704
  });
14489
14705
  };
14490
14706
  }, [onContextMenu, viewport]);
@@ -14520,11 +14736,43 @@ var SystemCanvas = (() => {
14520
14736
  var import_react3 = __toESM(require_react(), 1);
14521
14737
  var DRAG_THRESHOLD = 3;
14522
14738
  function useNodeDrag(options) {
14523
- const { viewport, nodesRef, onCommit } = options;
14739
+ const { viewport, nodesRef, onCommit, svgRef, canDropNodeOn, onNodeDrop } = options;
14524
14740
  const [dragOverrides, setDragOverrides] = (0, import_react3.useState)(() => /* @__PURE__ */ new Map());
14525
14741
  const [isDragging, setIsDragging] = (0, import_react3.useState)(false);
14742
+ const [dropTargetId, setDropTargetId] = (0, import_react3.useState)(null);
14526
14743
  const stateRef = (0, import_react3.useRef)(null);
14527
14744
  const movedRef = (0, import_react3.useRef)(false);
14745
+ const canDropNodeOnRef = (0, import_react3.useRef)(canDropNodeOn);
14746
+ canDropNodeOnRef.current = canDropNodeOn;
14747
+ const onNodeDropRef = (0, import_react3.useRef)(onNodeDrop);
14748
+ onNodeDropRef.current = onNodeDrop;
14749
+ const svgRefRef = (0, import_react3.useRef)(svgRef);
14750
+ svgRefRef.current = svgRef;
14751
+ const dropTargetIdRef = (0, import_react3.useRef)(null);
14752
+ const computeDropTarget = (0, import_react3.useCallback)((clientX, clientY) => {
14753
+ const cb = canDropNodeOnRef.current;
14754
+ const st = stateRef.current;
14755
+ const nodes = nodesRef.current;
14756
+ const svg = svgRefRef.current?.current;
14757
+ if (!cb || !st || !nodes || !svg)
14758
+ return null;
14759
+ const rect = svg.getBoundingClientRect();
14760
+ const vp = viewport.current ?? { x: 0, y: 0, zoom: 1 };
14761
+ const { x, y } = screenToCanvas(clientX - rect.left, clientY - rect.top, vp);
14762
+ for (let i = nodes.length - 1; i >= 0; i--) {
14763
+ const n = nodes[i];
14764
+ if (st.moving.has(n.id))
14765
+ continue;
14766
+ if (x < n.x || x > n.x + n.width)
14767
+ continue;
14768
+ if (y < n.y || y > n.y + n.height)
14769
+ continue;
14770
+ if (cb([st.source], n))
14771
+ return n.id;
14772
+ return null;
14773
+ }
14774
+ return null;
14775
+ }, [nodesRef, viewport]);
14528
14776
  const onPointerMove = (0, import_react3.useCallback)((event) => {
14529
14777
  const st = stateRef.current;
14530
14778
  if (!st || event.pointerId !== st.pointerId)
@@ -14546,7 +14794,12 @@ var SystemCanvas = (() => {
14546
14794
  next.set(id2, { x: start2.startX + dx, y: start2.startY + dy });
14547
14795
  }
14548
14796
  setDragOverrides(next);
14549
- }, [viewport]);
14797
+ const nextTarget = computeDropTarget(event.clientX, event.clientY);
14798
+ if (nextTarget !== dropTargetIdRef.current) {
14799
+ dropTargetIdRef.current = nextTarget;
14800
+ setDropTargetId(nextTarget);
14801
+ }
14802
+ }, [viewport, computeDropTarget]);
14550
14803
  const finishDrag = (0, import_react3.useCallback)((commit) => {
14551
14804
  const st = stateRef.current;
14552
14805
  if (!st)
@@ -14559,17 +14812,38 @@ var SystemCanvas = (() => {
14559
14812
  st.captureTarget.releasePointerCapture?.(st.pointerId);
14560
14813
  } catch {
14561
14814
  }
14815
+ const dropTarget = dropTargetIdRef.current;
14816
+ if (commit && movedRef.current && dropTarget) {
14817
+ const nodes = nodesRef.current;
14818
+ const target = nodes?.find((n) => n.id === dropTarget) ?? null;
14819
+ if (target && onNodeDropRef.current) {
14820
+ onNodeDropRef.current([st.source], target);
14821
+ stateRef.current = null;
14822
+ movedRef.current = false;
14823
+ dropTargetIdRef.current = null;
14824
+ setIsDragging(false);
14825
+ setDragOverrides(/* @__PURE__ */ new Map());
14826
+ setDropTargetId(null);
14827
+ return;
14828
+ }
14829
+ }
14562
14830
  if (commit && movedRef.current) {
14563
14831
  const overrides = Array.from(dragOverridesRef.current.entries());
14564
- for (const [id2, pos] of overrides) {
14565
- onCommit(id2, { x: Math.round(pos.x), y: Math.round(pos.y) });
14832
+ if (overrides.length > 0) {
14833
+ const updates = overrides.map(([id2, pos]) => ({
14834
+ id: id2,
14835
+ patch: { x: Math.round(pos.x), y: Math.round(pos.y) }
14836
+ }));
14837
+ onCommit(updates);
14566
14838
  }
14567
14839
  }
14568
14840
  stateRef.current = null;
14569
14841
  movedRef.current = false;
14842
+ dropTargetIdRef.current = null;
14570
14843
  setIsDragging(false);
14571
14844
  setDragOverrides(/* @__PURE__ */ new Map());
14572
- }, [onPointerMove, onCommit]);
14845
+ setDropTargetId(null);
14846
+ }, [onPointerMove, onCommit, nodesRef]);
14573
14847
  const dragOverridesRef = (0, import_react3.useRef)(dragOverrides);
14574
14848
  dragOverridesRef.current = dragOverrides;
14575
14849
  const onPointerUpRef = (0, import_react3.useRef)(null);
@@ -14609,9 +14883,11 @@ var SystemCanvas = (() => {
14609
14883
  startClientY: event.clientY,
14610
14884
  captureTarget: event.currentTarget,
14611
14885
  pointerId: event.pointerId,
14612
- moving
14886
+ moving,
14887
+ source: node
14613
14888
  };
14614
14889
  movedRef.current = false;
14890
+ dropTargetIdRef.current = null;
14615
14891
  try {
14616
14892
  ;
14617
14893
  event.currentTarget.setPointerCapture?.(event.pointerId);
@@ -14621,7 +14897,7 @@ var SystemCanvas = (() => {
14621
14897
  window.addEventListener("pointerup", onPointerUp);
14622
14898
  window.addEventListener("pointercancel", onPointerCancel);
14623
14899
  }, [nodesRef, onPointerMove, onPointerUp, onPointerCancel]);
14624
- return { dragOverrides, onPointerDown, isDragging };
14900
+ return { dragOverrides, dropTargetId, onPointerDown, isDragging };
14625
14901
  }
14626
14902
 
14627
14903
  // ../react/dist/hooks/useNodeResize.js
@@ -14896,11 +15172,15 @@ var SystemCanvas = (() => {
14896
15172
  return { x, y, zoom };
14897
15173
  }
14898
15174
  function useZoomNavigation(options) {
14899
- const { enabled, config, nodes, currentCanvas, parentFrame, canvases, onResolveCanvas, onSeedCanvas, theme, getViewportSize, onEnter, onExit } = options;
15175
+ const { enabled, config, nodes, currentCanvas, parentFrame, canvases, onResolveCanvas, onSeedCanvas, theme, getViewportSize, getCursorScreenPos, onEnter, onExit } = options;
14900
15176
  const committingRef = (0, import_react6.useRef)(false);
14901
15177
  (0, import_react6.useEffect)(() => {
14902
15178
  committingRef.current = false;
14903
15179
  }, [currentCanvas]);
15180
+ const exitArmedRef = (0, import_react6.useRef)(false);
15181
+ (0, import_react6.useEffect)(() => {
15182
+ exitArmedRef.current = false;
15183
+ }, [currentCanvas, parentFrame]);
14904
15184
  const prefetchRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
14905
15185
  const prefetch = (0, import_react6.useCallback)((ref) => {
14906
15186
  if (!onResolveCanvas)
@@ -14928,36 +15208,87 @@ var SystemCanvas = (() => {
14928
15208
  const size = getViewportSize();
14929
15209
  if (!size)
14930
15210
  return;
14931
- for (const n of nodes) {
14932
- if (!n.ref)
14933
- continue;
14934
- const screen = canvasRectToScreenRect({ x: n.x, y: n.y, width: n.width, height: n.height }, vp);
14935
- const centerX = screen.x + screen.width / 2;
14936
- const centerY = screen.y + screen.height / 2;
14937
- const centerOnScreen = centerX >= 0 && centerX <= size.width && centerY >= 0 && centerY <= size.height;
14938
- if (!centerOnScreen)
14939
- continue;
15211
+ const cursor = getCursorScreenPos();
15212
+ const viewportCenterX = size.width / 2;
15213
+ const viewportCenterY = size.height / 2;
15214
+ let cursorNode = null;
15215
+ if (cursor) {
15216
+ for (const n of nodes) {
15217
+ if (!n.ref)
15218
+ continue;
15219
+ const screen = canvasRectToScreenRect({ x: n.x, y: n.y, width: n.width, height: n.height }, vp);
15220
+ if (cursor.x < screen.x || cursor.x > screen.x + screen.width || cursor.y < screen.y || cursor.y > screen.y + screen.height) {
15221
+ continue;
15222
+ }
15223
+ const area = screen.width * screen.height;
15224
+ if (!cursorNode || area < cursorNode.area) {
15225
+ cursorNode = { node: n, screen, area };
15226
+ }
15227
+ }
15228
+ }
15229
+ let bestCandidate = null;
15230
+ if (cursorNode) {
15231
+ const screen = cursorNode.screen;
14940
15232
  const fillFraction = Math.max(screen.width / size.width, screen.height / size.height);
14941
- if (fillFraction >= config.prefetchThreshold && fillFraction < config.enterThreshold) {
14942
- prefetch(n.ref);
15233
+ if (fillFraction >= config.prefetchThreshold && cursorNode.node.ref) {
15234
+ prefetch(cursorNode.node.ref);
14943
15235
  }
14944
15236
  if (fillFraction >= config.enterThreshold) {
14945
- const childData = canvases?.[n.ref] ?? prefetchRef.current.get(n.ref)?.data;
14946
- if (!childData) {
15237
+ bestCandidate = { node: cursorNode.node, screen, distSq: 0 };
15238
+ }
15239
+ for (const n of nodes) {
15240
+ if (!n.ref)
15241
+ continue;
15242
+ if (n === cursorNode.node)
15243
+ continue;
15244
+ const s = canvasRectToScreenRect({ x: n.x, y: n.y, width: n.width, height: n.height }, vp);
15245
+ const ff = Math.max(s.width / size.width, s.height / size.height);
15246
+ if (ff >= config.prefetchThreshold)
14947
15247
  prefetch(n.ref);
15248
+ }
15249
+ } else {
15250
+ for (const n of nodes) {
15251
+ if (!n.ref)
15252
+ continue;
15253
+ const screen = canvasRectToScreenRect({ x: n.x, y: n.y, width: n.width, height: n.height }, vp);
15254
+ const centerX = screen.x + screen.width / 2;
15255
+ const centerY = screen.y + screen.height / 2;
15256
+ const centerOnScreen = centerX >= 0 && centerX <= size.width && centerY >= 0 && centerY <= size.height;
15257
+ if (!centerOnScreen)
14948
15258
  continue;
15259
+ const fillFraction = Math.max(screen.width / size.width, screen.height / size.height);
15260
+ if (fillFraction >= config.prefetchThreshold && fillFraction < config.enterThreshold) {
15261
+ prefetch(n.ref);
14949
15262
  }
15263
+ if (fillFraction >= config.enterThreshold) {
15264
+ const dx = centerX - viewportCenterX;
15265
+ const dy = centerY - viewportCenterY;
15266
+ const distSq = dx * dx + dy * dy;
15267
+ if (!bestCandidate || distSq < bestCandidate.distSq) {
15268
+ bestCandidate = { node: n, screen, distSq };
15269
+ }
15270
+ }
15271
+ }
15272
+ }
15273
+ if (bestCandidate) {
15274
+ const n = bestCandidate.node;
15275
+ const screen = bestCandidate.screen;
15276
+ const ref = n.ref;
15277
+ const childData = canvases?.[ref] ?? prefetchRef.current.get(ref)?.data;
15278
+ if (!childData) {
15279
+ prefetch(ref);
15280
+ } else {
14950
15281
  const resolved = resolveCanvas(childData, theme);
14951
- if (resolved.nodes.length === 0)
14952
- continue;
14953
- const bounds = computeBoundingBox(resolved.nodes);
14954
- const targetRect = expandRect(screen, config.landingScale);
14955
- const landingPad = Math.min(targetRect.width, targetRect.height) * config.landingPadding;
14956
- let targetTransform = fitBoundsIntoRect(bounds, targetRect, landingPad);
14957
- targetTransform = clampTransformToViewport(targetTransform, bounds, size, 16);
14958
- committingRef.current = true;
14959
- onEnter(n, targetTransform);
14960
- return;
15282
+ if (resolved.nodes.length > 0) {
15283
+ const bounds = computeBoundingBox(resolved.nodes);
15284
+ const targetRect = expandRect(screen, config.landingScale);
15285
+ const landingPad = Math.min(targetRect.width, targetRect.height) * config.landingPadding;
15286
+ let targetTransform = fitBoundsIntoRect(bounds, targetRect, landingPad);
15287
+ targetTransform = clampTransformToViewport(targetTransform, bounds, size, 16);
15288
+ committingRef.current = true;
15289
+ onEnter(n, targetTransform);
15290
+ return;
15291
+ }
14961
15292
  }
14962
15293
  }
14963
15294
  if (parentFrame && nodes.length > 0) {
@@ -14971,6 +15302,12 @@ var SystemCanvas = (() => {
14971
15302
  height: bounds.height
14972
15303
  }, vp);
14973
15304
  const fillFraction = Math.max(screen.width / size.width, screen.height / size.height);
15305
+ if (!exitArmedRef.current) {
15306
+ if (fillFraction > config.exitThreshold) {
15307
+ exitArmedRef.current = true;
15308
+ }
15309
+ return;
15310
+ }
14974
15311
  if (fillFraction <= config.exitThreshold) {
14975
15312
  const targetTransform = fitBoundsIntoRect({
14976
15313
  minX: parentFrame.parentNodeRect.x,
@@ -14997,6 +15334,7 @@ var SystemCanvas = (() => {
14997
15334
  theme,
14998
15335
  config,
14999
15336
  getViewportSize,
15337
+ getCursorScreenPos,
15000
15338
  prefetch,
15001
15339
  onEnter,
15002
15340
  onExit
@@ -17872,17 +18210,67 @@ var SystemCanvas = (() => {
17872
18210
 
17873
18211
  // ../react/dist/components/NodeIcon.js
17874
18212
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
17875
- function NodeIcon({ icon, x, y, size = 14, color: color2, opacity = 0.7, customIcons }) {
18213
+ function NodeIcon({ icon, x, y, size = 14, color: color2, opacity = 0.7, customIcons, mode = "stroke", viewBox = 16 }) {
17876
18214
  const pathData = customIcons?.[icon] ?? iconPaths[icon];
17877
18215
  if (!pathData)
17878
18216
  return null;
17879
- return (0, import_jsx_runtime.jsx)("g", { transform: `translate(${x}, ${y})`, pointerEvents: "none", opacity, children: pathData.map((d, i) => (0, import_jsx_runtime.jsx)("path", { d: scalePathData(d, size), fill: "none", stroke: color2, strokeWidth: 1.2, strokeLinecap: "round", strokeLinejoin: "round" }, i)) });
18217
+ const defaultStrokeWidth = viewBox * 0.075;
18218
+ return (0, import_jsx_runtime.jsx)("g", { transform: `translate(${x}, ${y})`, pointerEvents: "none", opacity, children: pathData.map((entry, i) => {
18219
+ const spec = typeof entry === "string" ? { d: entry } : entry;
18220
+ const pathMode = spec.mode ?? mode;
18221
+ const rawStrokeWidth = spec.strokeWidth ?? defaultStrokeWidth;
18222
+ const renderedStrokeWidth = rawStrokeWidth * size / viewBox;
18223
+ return (0, import_jsx_runtime.jsx)("path", {
18224
+ d: scalePathData(spec.d, size, viewBox),
18225
+ fill: pathMode === "fill" ? color2 : "none",
18226
+ stroke: pathMode === "stroke" ? color2 : "none",
18227
+ strokeWidth: pathMode === "stroke" ? renderedStrokeWidth : 0,
18228
+ strokeLinecap: pathMode === "stroke" ? "round" : void 0,
18229
+ strokeLinejoin: pathMode === "stroke" ? "round" : void 0,
18230
+ // Brand silhouettes are typically authored as a single path
18231
+ // whose holes are expressed with subpath winding. `evenodd`
18232
+ // is the safe choice; nonzero would fill some holes solid.
18233
+ fillRule: pathMode === "fill" ? "evenodd" : void 0
18234
+ }, i);
18235
+ }) });
17880
18236
  }
17881
- function scalePathData(d, size) {
17882
- const scale = size / 16;
17883
- return d.replace(/(-?\d+\.?\d*)/g, (match) => {
17884
- return String(parseFloat(match) * scale);
17885
- });
18237
+ function scalePathData(d, size, source) {
18238
+ const scale = size / source;
18239
+ const out = [];
18240
+ const numberAtStart = /^-?(?:\d+\.?\d*|\.\d+)/;
18241
+ let isArc = false;
18242
+ let arcIndex = 0;
18243
+ let i = 0;
18244
+ while (i < d.length) {
18245
+ const ch = d[i];
18246
+ if (ch === " " || ch === "," || ch === " " || ch === "\n" || ch === "\r") {
18247
+ i++;
18248
+ continue;
18249
+ }
18250
+ if (/[a-zA-Z]/.test(ch)) {
18251
+ out.push(ch);
18252
+ isArc = ch === "a" || ch === "A";
18253
+ arcIndex = 0;
18254
+ i++;
18255
+ continue;
18256
+ }
18257
+ if (isArc && (arcIndex === 3 || arcIndex === 4)) {
18258
+ out.push(ch === "0" ? "0" : "1");
18259
+ arcIndex = (arcIndex + 1) % 7;
18260
+ i++;
18261
+ continue;
18262
+ }
18263
+ const m = numberAtStart.exec(d.substring(i));
18264
+ if (!m) {
18265
+ i++;
18266
+ continue;
18267
+ }
18268
+ out.push(String(parseFloat(m[0]) * scale));
18269
+ if (isArc)
18270
+ arcIndex = (arcIndex + 1) % 7;
18271
+ i += m[0].length;
18272
+ }
18273
+ return out.join(" ");
17886
18274
  }
17887
18275
  var iconPaths = {
17888
18276
  // Database: cylinder shape
@@ -17975,11 +18363,12 @@ var SystemCanvas = (() => {
17975
18363
  // ../react/dist/components/RefIndicator.js
17976
18364
  var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1);
17977
18365
  var import_react8 = __toESM(require_react(), 1);
17978
- function RefIndicator({ node, theme, nodeX, nodeY, nodeWidth, nodeHeight, strokeColor, strokeWidth, corner = "bottom-right", size = 18, onNavigate }) {
18366
+ function RefIndicator({ node, theme, nodeX, nodeY, nodeWidth, nodeHeight, strokeColor, strokeWidth, corner = "bottom-right", size: sizeProp, onNavigate }) {
17979
18367
  const [hover, setHover] = (0, import_react8.useState)(false);
17980
18368
  const iconKind = theme.node.refIndicator.icon;
17981
18369
  if (iconKind === "none")
17982
18370
  return null;
18371
+ const size = sizeProp ?? theme.node.refIndicator.size ?? 18;
17983
18372
  const stopAll = (e) => e.stopPropagation();
17984
18373
  const right = nodeX + nodeWidth;
17985
18374
  const bottom = nodeY + nodeHeight;
@@ -18025,19 +18414,22 @@ var SystemCanvas = (() => {
18025
18414
  return (0, import_jsx_runtime2.jsxs)("g", { className: "system-canvas-ref-indicator", style: { cursor: "pointer" }, onClick: (e) => {
18026
18415
  e.stopPropagation();
18027
18416
  onNavigate(node, e);
18028
- }, onDoubleClick: stopAll, onPointerDown: stopAll, onMouseDown: stopAll, onPointerEnter: () => setHover(true), onPointerLeave: () => setHover(false), children: [hover && (0, import_jsx_runtime2.jsx)("path", { d: hoverPath, fill: hoverFill, pointerEvents: "none" }), (0, import_jsx_runtime2.jsx)("path", { d: carvePath, fill: "none", stroke: strokeColor, strokeWidth: sw, strokeLinecap: "butt", strokeLinejoin: "round", pointerEvents: "none" }), (0, import_jsx_runtime2.jsx)("rect", { x: squareX, y: squareY, width: size, height: size, fill: "transparent" }), (0, import_jsx_runtime2.jsx)(Glyph, { kind: iconKind, cx, cy, color: hover ? hoverGlyph : restGlyph })] });
18417
+ }, onDoubleClick: stopAll, onPointerDown: stopAll, onMouseDown: stopAll, onPointerEnter: () => setHover(true), onPointerLeave: () => setHover(false), children: [hover && (0, import_jsx_runtime2.jsx)("path", { d: hoverPath, fill: hoverFill, pointerEvents: "none" }), (0, import_jsx_runtime2.jsx)("path", { d: carvePath, fill: "none", stroke: strokeColor, strokeWidth: sw, strokeLinecap: "butt", strokeLinejoin: "round", pointerEvents: "none" }), (0, import_jsx_runtime2.jsx)("rect", { x: squareX, y: squareY, width: size, height: size, fill: "transparent" }), (0, import_jsx_runtime2.jsx)(Glyph, { kind: iconKind, cx, cy, color: hover ? hoverGlyph : restGlyph, size })] });
18029
18418
  }
18030
- function Glyph({ kind, cx, cy, color: color2 }) {
18031
- const s = 3;
18419
+ function Glyph({ kind, cx, cy, color: color2, size }) {
18420
+ const s = size / 6;
18421
+ const sw = Math.max(1, size / 9);
18032
18422
  switch (kind) {
18033
18423
  case "chevron":
18034
- return (0, import_jsx_runtime2.jsx)("path", { d: `M ${cx - s / 2} ${cy - s} L ${cx + s / 2} ${cy} L ${cx - s / 2} ${cy + s}`, fill: "none", stroke: color2, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", pointerEvents: "none" });
18424
+ return (0, import_jsx_runtime2.jsx)("path", { d: `M ${cx - s / 2} ${cy - s} L ${cx + s / 2} ${cy} L ${cx - s / 2} ${cy + s}`, fill: "none", stroke: color2, strokeWidth: sw, strokeLinecap: "round", strokeLinejoin: "round", pointerEvents: "none" });
18035
18425
  case "arrow": {
18036
18426
  const h = s * 0.9;
18037
- return (0, import_jsx_runtime2.jsx)("path", { d: `M ${cx - s} ${cy} L ${cx + s} ${cy} M ${cx + s - h} ${cy - h} L ${cx + s} ${cy} L ${cx + s - h} ${cy + h}`, fill: "none", stroke: color2, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", pointerEvents: "none" });
18427
+ return (0, import_jsx_runtime2.jsx)("path", { d: `M ${cx - s} ${cy} L ${cx + s} ${cy} M ${cx + s - h} ${cy - h} L ${cx + s} ${cy} L ${cx + s - h} ${cy + h}`, fill: "none", stroke: color2, strokeWidth: sw, strokeLinecap: "round", strokeLinejoin: "round", pointerEvents: "none" });
18428
+ }
18429
+ case "expand": {
18430
+ const sw2 = Math.max(1, size / 12);
18431
+ return (0, import_jsx_runtime2.jsxs)("g", { pointerEvents: "none", children: [(0, import_jsx_runtime2.jsx)("path", { d: `M ${cx - s} ${cy - s} L ${cx + s} ${cy - s} L ${cx + s} ${cy + s} L ${cx - s} ${cy + s} Z`, fill: "none", stroke: color2, strokeWidth: sw2, strokeLinejoin: "round" }), (0, import_jsx_runtime2.jsx)("line", { x1: cx, y1: cy - s + 1, x2: cx, y2: cy + s - 1, stroke: color2, strokeWidth: sw2, strokeLinecap: "round" }), (0, import_jsx_runtime2.jsx)("line", { x1: cx - s + 1, y1: cy, x2: cx + s - 1, y2: cy, stroke: color2, strokeWidth: sw2, strokeLinecap: "round" })] });
18038
18432
  }
18039
- case "expand":
18040
- return (0, import_jsx_runtime2.jsxs)("g", { pointerEvents: "none", children: [(0, import_jsx_runtime2.jsx)("path", { d: `M ${cx - s} ${cy - s} L ${cx + s} ${cy - s} L ${cx + s} ${cy + s} L ${cx - s} ${cy + s} Z`, fill: "none", stroke: color2, strokeWidth: 1.5, strokeLinejoin: "round" }), (0, import_jsx_runtime2.jsx)("line", { x1: cx, y1: cy - s + 1, x2: cx, y2: cy + s - 1, stroke: color2, strokeWidth: 1.5, strokeLinecap: "round" }), (0, import_jsx_runtime2.jsx)("line", { x1: cx - s + 1, y1: cy, x2: cx + s - 1, y2: cy, stroke: color2, strokeWidth: 1.5, strokeLinecap: "round" })] });
18041
18433
  }
18042
18434
  }
18043
18435
 
@@ -18164,7 +18556,7 @@ var SystemCanvas = (() => {
18164
18556
  // ../react/dist/primitives/NodeText.js
18165
18557
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
18166
18558
  var import_react9 = __toESM(require_react(), 1);
18167
- function NodeText({ region, value, theme, color: color2, fill, align = "start", fontWeight = 500, uppercase = false, useLabelFont = false, fontFamily, fontSize: fontSizeProp, wrap = false, maxLines, lineHeight: lineHeightProp }) {
18559
+ function NodeText({ region, value, theme, color: color2, fill, align = "start", fontWeight = 500, uppercase = false, useLabelFont = false, fontFamily, fontSize: fontSizeProp, wrap = false, maxLines, lineHeight: lineHeightProp, verticalAlign = "top" }) {
18168
18560
  const reactId = (0, import_react9.useId)();
18169
18561
  const safeId = reactId.replace(/:/g, "");
18170
18562
  if (!value)
@@ -18178,15 +18570,16 @@ var SystemCanvas = (() => {
18178
18570
  const fillAttr = gradId ? `url(#${gradId})` : color2 ?? theme.node.sublabelColor;
18179
18571
  const displayValue = uppercase ? value.toUpperCase() : value;
18180
18572
  if (!wrap) {
18573
+ const rendered = truncateToWidth(displayValue, region.width, fontSize);
18181
18574
  const y = region.y + region.height / 2 + fontSize * 0.36;
18182
- return (0, import_jsx_runtime7.jsxs)("g", { pointerEvents: "none", children: [fill && (0, import_jsx_runtime7.jsx)(GradientDef, { id: gradId, fill }), (0, import_jsx_runtime7.jsx)("text", { x, y, fill: fillAttr, fontSize, fontWeight, fontFamily: font, textAnchor: anchor, letterSpacing: uppercase ? 0.8 : 0.2, pointerEvents: "none", children: displayValue })] });
18575
+ return (0, import_jsx_runtime7.jsxs)("g", { pointerEvents: "none", children: [fill && (0, import_jsx_runtime7.jsx)(GradientDef, { id: gradId, fill }), (0, import_jsx_runtime7.jsx)("text", { x, y, fill: fillAttr, fontSize, fontWeight, fontFamily: font, textAnchor: anchor, letterSpacing: uppercase ? 0.8 : 0.2, pointerEvents: "none", children: rendered })] });
18183
18576
  }
18184
- const wrapInset = 4;
18185
- const wrapWidth = Math.max(0, region.width - wrapInset * 2);
18186
- const lines = wrapTextWithBreaks(displayValue, wrapWidth, fontSize, maxLines);
18577
+ const lines = wrapTextWithBreaks(displayValue, region.width, fontSize, maxLines);
18187
18578
  if (lines.length === 0)
18188
18579
  return null;
18189
- const baseY = region.y + fontSize;
18580
+ const blockHeight = fontSize + (lines.length - 1) * lineHeight;
18581
+ const topY = region.y + fontSize;
18582
+ const baseY = verticalAlign === "center" ? region.y + (region.height - blockHeight) / 2 + fontSize : verticalAlign === "bottom" ? region.y + region.height - blockHeight + fontSize : topY;
18190
18583
  const clipId = `sc-text-clip-${safeId}`;
18191
18584
  return (0, import_jsx_runtime7.jsxs)("g", { pointerEvents: "none", children: [(0, import_jsx_runtime7.jsxs)("defs", { children: [fill && (0, import_jsx_runtime7.jsx)(GradientDef, { id: gradId, fill }), (0, import_jsx_runtime7.jsx)("clipPath", { id: clipId, children: (0, import_jsx_runtime7.jsx)("rect", { x: region.x, y: region.y, width: region.width, height: region.height }) })] }), (0, import_jsx_runtime7.jsx)("g", { clipPath: `url(#${clipId})`, pointerEvents: "none", children: (0, import_jsx_runtime7.jsx)("text", { x, y: baseY, fill: fillAttr, fontSize, fontWeight, fontFamily: font, textAnchor: anchor, letterSpacing: uppercase ? 0.8 : 0.2, pointerEvents: "none", children: lines.map((line, i) => (0, import_jsx_runtime7.jsx)("tspan", { x, dy: i === 0 ? 0 : lineHeight, children: line || " " }, i)) }) })] });
18192
18585
  }
@@ -18338,6 +18731,20 @@ var SystemCanvas = (() => {
18338
18731
  const color2 = resolveAccessorOr(spec.color, nodeColor, ctx);
18339
18732
  return (0, import_jsx_runtime9.jsx)(NodeDot, { region, color: color2 });
18340
18733
  }
18734
+ case "icon": {
18735
+ const name = resolveAccessor(spec.name, ctx);
18736
+ if (!name)
18737
+ return null;
18738
+ const color2 = resolveAccessorOr(spec.color, nodeColor, ctx);
18739
+ const opacity = resolveAccessorOr(spec.opacity, 1, ctx);
18740
+ const mode = resolveAccessorOr(spec.mode, "stroke", ctx);
18741
+ const viewBox = resolveAccessorOr(spec.viewBox, 16, ctx);
18742
+ const auto = Math.max(8, Math.min(region.width, region.height) - 2);
18743
+ const size = resolveAccessorOr(spec.size, auto, ctx);
18744
+ const x = region.x + (region.width - size) / 2;
18745
+ const y = region.y + (region.height - size) / 2;
18746
+ return (0, import_jsx_runtime9.jsx)(NodeIcon, { icon: name, x, y, size, color: color2, opacity, customIcons: theme.icons, mode, viewBox });
18747
+ }
18341
18748
  case "custom": {
18342
18749
  return spec.render(ctx);
18343
18750
  }
@@ -18365,20 +18772,21 @@ var SystemCanvas = (() => {
18365
18772
  const contentY = y + reservedTop;
18366
18773
  const contentWidth = Math.max(0, width - reservedLeft - reservedRight);
18367
18774
  const contentHeight = Math.max(0, height - reservedTop - reservedBottom);
18368
- const text = node.text ?? "";
18369
- const lines = text.split("\n").filter(Boolean);
18370
- const mainLabel = lines[0] ?? node.id;
18371
- const sublabel = lines[1];
18775
+ const LABEL_PAD_X = 10;
18776
+ const LABEL_PAD_Y = 6;
18777
+ const text = node.text ?? node.id;
18372
18778
  const hasBodySlot = slots?.body !== void 0;
18373
18779
  const hasHeader = reservedTop > 0;
18374
- const labelFont = theme.node.labelFont ?? theme.node.fontFamily;
18780
+ const hasInlineLeftMarker = slots?.topLeft !== void 0 && (slots.topLeft.kind === "dot" || slots.topLeft.kind === "icon") && !hasHeader;
18781
+ const isLeftAligned = hasHeader || hasInlineLeftMarker;
18375
18782
  const labelFontSize = theme.node.fontSize + (hasHeader ? 1 : 0);
18376
- const lineHeight = labelFontSize + 4;
18377
- const totalTextHeight = sublabel ? lineHeight + theme.node.sublabelFontSize + 4 : lineHeight;
18378
- const labelAnchor = hasHeader ? "start" : "middle";
18379
- const labelX = hasHeader ? contentX : contentX + contentWidth / 2;
18380
- const textStartY = hasHeader ? contentY + labelFontSize + 2 : contentY + (contentHeight - totalTextHeight) / 2 + labelFontSize;
18381
- return (0, import_jsx_runtime10.jsxs)("g", { className: "system-canvas-node system-canvas-node--text", style: { cursor: onPointerDown ? "move" : node.isNavigable ? "pointer" : "default" }, onClick: (e) => onClick(node, e), onDoubleClick: (e) => onDoubleClick(node, e), onContextMenu: (e) => onContextMenu(node, e), onPointerDown: onPointerDown ? (e) => onPointerDown(node, e) : void 0, children: [(0, import_jsx_runtime10.jsx)("rect", { x, y, width, height, rx: node.resolvedCornerRadius, fill: theme.background }), (0, import_jsx_runtime10.jsx)("rect", { x, y, width, height, rx: node.resolvedCornerRadius, fill: node.resolvedFill, stroke: node.resolvedStroke, strokeWidth: theme.node.strokeWidth }), !isEditing && !hasBodySlot && (0, import_jsx_runtime10.jsx)("text", { x: labelX, y: textStartY, fill: theme.node.labelColor, fontSize: labelFontSize, fontWeight: 600, fontFamily: labelFont, textAnchor: labelAnchor, pointerEvents: "none", children: mainLabel }), !isEditing && !hasBodySlot && sublabel && (0, import_jsx_runtime10.jsx)("text", { x: labelX, y: textStartY + lineHeight, fill: theme.node.sublabelColor, fontSize: theme.node.sublabelFontSize, fontFamily: theme.node.fontFamily, textAnchor: labelAnchor, pointerEvents: "none", children: sublabel }), node.resolvedIcon && (0, import_jsx_runtime10.jsx)(NodeIcon, { icon: node.resolvedIcon, x: x + 8 + reservedLeft, y: contentY + contentHeight / 2 - 7, size: 14, color: node.resolvedStroke, opacity: 0.7, customIcons: theme.icons }), slots && (0, import_jsx_runtime10.jsx)(CategorySlotsLayer, { node, theme, canvases, slots }), node.isNavigable && (0, import_jsx_runtime10.jsx)(RefIndicator, { node, theme, nodeX: x, nodeY: y, nodeWidth: width, nodeHeight: height, strokeColor: node.resolvedStroke, strokeWidth: theme.node.strokeWidth, corner: toKebabCorner(refCorner), onNavigate })] });
18783
+ const labelAlign = isLeftAligned ? "start" : "center";
18784
+ return (0, import_jsx_runtime10.jsxs)("g", { className: "system-canvas-node system-canvas-node--text", style: { cursor: onPointerDown ? "move" : node.isNavigable ? "pointer" : "default" }, onClick: (e) => onClick(node, e), onDoubleClick: (e) => onDoubleClick(node, e), onContextMenu: (e) => onContextMenu(node, e), onPointerDown: onPointerDown ? (e) => onPointerDown(node, e) : void 0, children: [(0, import_jsx_runtime10.jsx)("rect", { x, y, width, height, rx: node.resolvedCornerRadius, fill: theme.background }), (0, import_jsx_runtime10.jsx)("rect", { x, y, width, height, rx: node.resolvedCornerRadius, fill: node.resolvedFill, stroke: node.resolvedStroke, strokeWidth: theme.node.strokeWidth }), !isEditing && !hasBodySlot && (0, import_jsx_runtime10.jsx)(NodeText, { region: {
18785
+ x: contentX + LABEL_PAD_X,
18786
+ y: contentY + LABEL_PAD_Y,
18787
+ width: Math.max(0, contentWidth - LABEL_PAD_X * 2),
18788
+ height: Math.max(0, contentHeight - LABEL_PAD_Y * 2)
18789
+ }, value: text, theme, color: theme.node.labelColor, align: labelAlign, fontWeight: 600, fontSize: labelFontSize, useLabelFont: true, wrap: true, verticalAlign: hasHeader ? "top" : "center" }), node.resolvedIcon && (0, import_jsx_runtime10.jsx)(NodeIcon, { icon: node.resolvedIcon, x: x + 8 + reservedLeft, y: contentY + contentHeight / 2 - 7, size: 14, color: node.resolvedStroke, opacity: 0.7, customIcons: theme.icons }), slots && (0, import_jsx_runtime10.jsx)(CategorySlotsLayer, { node, theme, canvases, slots }), node.isNavigable && (0, import_jsx_runtime10.jsx)(RefIndicator, { node, theme, nodeX: x, nodeY: y, nodeWidth: width, nodeHeight: height, strokeColor: node.resolvedStroke, strokeWidth: theme.node.strokeWidth, corner: toKebabCorner(refCorner), onNavigate })] });
18382
18790
  }
18383
18791
 
18384
18792
  // ../react/dist/components/FileNode.js
@@ -18392,6 +18800,7 @@ var SystemCanvas = (() => {
18392
18800
  const fold = 10;
18393
18801
  const textPadding = 10 + reservedLeft;
18394
18802
  const maxTextWidth = width - textPadding - reservedRight - fold;
18803
+ const LABEL_PAD_Y = 6;
18395
18804
  const contentY = y + reservedTop;
18396
18805
  const contentHeight = Math.max(0, height - reservedTop - reservedBottom);
18397
18806
  const shapePath = [
@@ -18410,7 +18819,12 @@ var SystemCanvas = (() => {
18410
18819
  const strokeColor = node.resolvedStroke;
18411
18820
  const thinStroke = 0.75;
18412
18821
  const clipId = `file-clip-${node.id}`;
18413
- return (0, import_jsx_runtime11.jsxs)("g", { className: "system-canvas-node system-canvas-node--file", style: { cursor: onPointerDown ? "move" : node.isNavigable ? "pointer" : "default" }, onClick: (e) => onClick(node, e), onDoubleClick: (e) => onDoubleClick(node, e), onContextMenu: (e) => onContextMenu(node, e), onPointerDown: onPointerDown ? (e) => onPointerDown(node, e) : void 0, children: [(0, import_jsx_runtime11.jsx)("defs", { children: (0, import_jsx_runtime11.jsx)("clipPath", { id: clipId, children: (0, import_jsx_runtime11.jsx)("rect", { x: x + textPadding, y: contentY, width: maxTextWidth, height: contentHeight }) }) }), (0, import_jsx_runtime11.jsx)("path", { d: shapePath, fill: theme.background }), (0, import_jsx_runtime11.jsx)("path", { d: shapePath, fill: node.resolvedFill, stroke: strokeColor, strokeWidth: thinStroke }), (0, import_jsx_runtime11.jsx)("path", { d: foldPath, fill: "none", stroke: strokeColor, strokeWidth: thinStroke, opacity: 0.5 }), !isEditing && !slots?.body && (0, import_jsx_runtime11.jsxs)("g", { clipPath: `url(#${clipId})`, children: [dirPath && (0, import_jsx_runtime11.jsxs)("text", { x: x + textPadding, y: contentY + 14, fill: theme.node.sublabelColor, fontSize: theme.node.sublabelFontSize - 1, fontFamily: theme.node.fontFamily, pointerEvents: "none", opacity: 0.6, children: [dirPath, "/"] }), (0, import_jsx_runtime11.jsx)("text", { x: x + textPadding, y: contentY + (dirPath ? 28 : contentHeight / 2 + (subpath ? -2 : 4)), fill: theme.node.labelColor, fontSize: theme.node.fontSize - 1, fontWeight: 500, fontFamily: theme.node.fontFamily, pointerEvents: "none", children: fileName }), subpath && (0, import_jsx_runtime11.jsx)("text", { x: x + textPadding, y: contentY + (dirPath ? 40 : contentHeight / 2 + theme.node.fontSize + 2), fill: theme.node.sublabelColor, fontSize: theme.node.sublabelFontSize, fontFamily: theme.node.fontFamily, pointerEvents: "none", children: subpath })] }), slots && (0, import_jsx_runtime11.jsx)(CategorySlotsLayer, { node, theme, canvases, slots }), node.isNavigable && (0, import_jsx_runtime11.jsx)(RefIndicator, { node, theme, nodeX: x, nodeY: y, nodeWidth: width, nodeHeight: height, strokeColor, strokeWidth: thinStroke, corner: toKebabCorner(refCorner), onNavigate })] });
18822
+ return (0, import_jsx_runtime11.jsxs)("g", { className: "system-canvas-node system-canvas-node--file", style: { cursor: onPointerDown ? "move" : node.isNavigable ? "pointer" : "default" }, onClick: (e) => onClick(node, e), onDoubleClick: (e) => onDoubleClick(node, e), onContextMenu: (e) => onContextMenu(node, e), onPointerDown: onPointerDown ? (e) => onPointerDown(node, e) : void 0, children: [(0, import_jsx_runtime11.jsx)("defs", { children: (0, import_jsx_runtime11.jsx)("clipPath", { id: clipId, children: (0, import_jsx_runtime11.jsx)("rect", { x: x + textPadding, y: contentY, width: maxTextWidth, height: contentHeight }) }) }), (0, import_jsx_runtime11.jsx)("path", { d: shapePath, fill: theme.background }), (0, import_jsx_runtime11.jsx)("path", { d: shapePath, fill: node.resolvedFill, stroke: strokeColor, strokeWidth: thinStroke }), (0, import_jsx_runtime11.jsx)("path", { d: foldPath, fill: "none", stroke: strokeColor, strokeWidth: thinStroke, opacity: 0.5 }), !isEditing && !slots?.body && (0, import_jsx_runtime11.jsxs)("g", { clipPath: `url(#${clipId})`, children: [dirPath && (0, import_jsx_runtime11.jsxs)("text", { x: x + textPadding, y: contentY + 14, fill: theme.node.sublabelColor, fontSize: theme.node.sublabelFontSize - 1, fontFamily: theme.node.fontFamily, pointerEvents: "none", opacity: 0.6, children: [dirPath, "/"] }), (0, import_jsx_runtime11.jsx)(NodeText, { region: {
18823
+ x: x + textPadding,
18824
+ y: dirPath ? contentY + 18 : contentY + (subpath ? 8 : LABEL_PAD_Y),
18825
+ width: maxTextWidth,
18826
+ height: dirPath ? Math.max(0, contentHeight - 18 - (subpath ? 16 : 0)) : Math.max(0, contentHeight - (subpath ? 16 : 0) - LABEL_PAD_Y * 2)
18827
+ }, value: fileName, theme, color: theme.node.labelColor, align: "start", fontWeight: 500, fontSize: theme.node.fontSize - 1, wrap: true, verticalAlign: dirPath ? "top" : "center" }), subpath && (0, import_jsx_runtime11.jsx)("text", { x: x + textPadding, y: contentY + (dirPath ? 40 : contentHeight / 2 + theme.node.fontSize + 2), fill: theme.node.sublabelColor, fontSize: theme.node.sublabelFontSize, fontFamily: theme.node.fontFamily, pointerEvents: "none", children: subpath })] }), slots && (0, import_jsx_runtime11.jsx)(CategorySlotsLayer, { node, theme, canvases, slots }), node.isNavigable && (0, import_jsx_runtime11.jsx)(RefIndicator, { node, theme, nodeX: x, nodeY: y, nodeWidth: width, nodeHeight: height, strokeColor, strokeWidth: thinStroke, corner: toKebabCorner(refCorner), onNavigate })] });
18414
18828
  }
18415
18829
 
18416
18830
  // ../react/dist/components/LinkNode.js
@@ -18421,14 +18835,21 @@ var SystemCanvas = (() => {
18421
18835
  const contentY = y + reservedTop;
18422
18836
  const contentWidth = Math.max(0, width - reservedLeft - reservedRight);
18423
18837
  const contentHeight = Math.max(0, height - reservedTop - reservedBottom);
18424
- const cx = contentX + contentWidth / 2;
18838
+ const glyphReserve = 20;
18839
+ const LABEL_PAD_X = 10;
18840
+ const LABEL_PAD_Y = 6;
18425
18841
  let displayUrl = node.url ?? "";
18426
18842
  try {
18427
18843
  const url = new URL(displayUrl);
18428
18844
  displayUrl = url.hostname;
18429
18845
  } catch {
18430
18846
  }
18431
- return (0, import_jsx_runtime12.jsxs)("g", { className: "system-canvas-node system-canvas-node--link", style: { cursor: onPointerDown ? "move" : "pointer" }, onClick: (e) => onClick(node, e), onDoubleClick: (e) => onDoubleClick(node, e), onContextMenu: (e) => onContextMenu(node, e), onPointerDown: onPointerDown ? (e) => onPointerDown(node, e) : void 0, children: [(0, import_jsx_runtime12.jsx)("rect", { x, y, width, height, rx: node.resolvedCornerRadius, fill: theme.background }), (0, import_jsx_runtime12.jsx)("rect", { x, y, width, height, rx: node.resolvedCornerRadius, fill: node.resolvedFill, stroke: node.resolvedStroke, strokeWidth: theme.node.strokeWidth }), !isEditing && !slots?.body && (0, import_jsx_runtime12.jsx)("text", { x: contentX + 12, y: contentY + contentHeight / 2 + 4, fill: node.resolvedStroke, fontSize: 12, fontFamily: theme.node.fontFamily, pointerEvents: "none", opacity: 0.6, children: "\u29C9" }), !isEditing && !slots?.body && (0, import_jsx_runtime12.jsx)("text", { x: cx, y: contentY + contentHeight / 2 + 4, fill: theme.node.labelColor, fontSize: theme.node.fontSize, fontWeight: 600, fontFamily: theme.node.fontFamily, textAnchor: "middle", pointerEvents: "none", textDecoration: "underline", children: displayUrl }), slots && (0, import_jsx_runtime12.jsx)(CategorySlotsLayer, { node, theme, canvases, slots }), node.isNavigable && (0, import_jsx_runtime12.jsx)(RefIndicator, { node, theme, nodeX: x, nodeY: y, nodeWidth: width, nodeHeight: height, strokeColor: node.resolvedStroke, strokeWidth: theme.node.strokeWidth, corner: toKebabCorner(refCorner), onNavigate })] });
18847
+ return (0, import_jsx_runtime12.jsxs)("g", { className: "system-canvas-node system-canvas-node--link", style: { cursor: onPointerDown ? "move" : "pointer" }, onClick: (e) => onClick(node, e), onDoubleClick: (e) => onDoubleClick(node, e), onContextMenu: (e) => onContextMenu(node, e), onPointerDown: onPointerDown ? (e) => onPointerDown(node, e) : void 0, children: [(0, import_jsx_runtime12.jsx)("rect", { x, y, width, height, rx: node.resolvedCornerRadius, fill: theme.background }), (0, import_jsx_runtime12.jsx)("rect", { x, y, width, height, rx: node.resolvedCornerRadius, fill: node.resolvedFill, stroke: node.resolvedStroke, strokeWidth: theme.node.strokeWidth }), !isEditing && !slots?.body && (0, import_jsx_runtime12.jsx)("text", { x: contentX + 12, y: contentY + contentHeight / 2 + 4, fill: node.resolvedStroke, fontSize: 12, fontFamily: theme.node.fontFamily, pointerEvents: "none", opacity: 0.6, children: "\u29C9" }), !isEditing && !slots?.body && (0, import_jsx_runtime12.jsx)(NodeText, { region: {
18848
+ x: contentX + glyphReserve,
18849
+ y: contentY + LABEL_PAD_Y,
18850
+ width: Math.max(0, contentWidth - glyphReserve - LABEL_PAD_X),
18851
+ height: Math.max(0, contentHeight - LABEL_PAD_Y * 2)
18852
+ }, value: displayUrl, theme, color: theme.node.labelColor, align: "center", fontWeight: 600, fontSize: theme.node.fontSize, wrap: true, verticalAlign: "center" }), slots && (0, import_jsx_runtime12.jsx)(CategorySlotsLayer, { node, theme, canvases, slots }), node.isNavigable && (0, import_jsx_runtime12.jsx)(RefIndicator, { node, theme, nodeX: x, nodeY: y, nodeWidth: width, nodeHeight: height, strokeColor: node.resolvedStroke, strokeWidth: theme.node.strokeWidth, corner: toKebabCorner(refCorner), onNavigate })] });
18432
18853
  }
18433
18854
 
18434
18855
  // ../react/dist/components/GroupNode.js
@@ -18554,7 +18975,8 @@ var SystemCanvas = (() => {
18554
18975
  const isEditing = editingId === edge.id;
18555
18976
  const baseColor = edge.color ? resolveColor(edge.color, theme).stroke : theme.edge.stroke;
18556
18977
  const edgeColor = isSelected ? theme.node.labelColor : baseColor;
18557
- const strokeWidth = isSelected ? theme.edge.strokeWidth * 1.75 : theme.edge.strokeWidth;
18978
+ const baseStrokeWidth = edge.strokeWidth ?? theme.edge.strokeWidth;
18979
+ const strokeWidth = isSelected ? baseStrokeWidth * 1.75 : baseStrokeWidth;
18558
18980
  const toEnd = edge.toEnd ?? "arrow";
18559
18981
  const fromEnd = edge.fromEnd ?? "none";
18560
18982
  const arrowId = "system-canvas-arrowhead";
@@ -18617,6 +19039,10 @@ var SystemCanvas = (() => {
18617
19039
  const fontFamily = theme.node.fontFamily;
18618
19040
  const fontSize = theme.node.fontSize;
18619
19041
  const padding = 8;
19042
+ const slots = getCategorySlots(node, theme);
19043
+ const reservations = computeReflowReservations(node, theme, slots);
19044
+ const hasHeader = reservations.top > 0;
19045
+ const textAlign = node.type === "text" && !hasHeader ? "center" : "left";
18620
19046
  const commonFieldStyle = {
18621
19047
  width: "100%",
18622
19048
  height: "100%",
@@ -18630,7 +19056,7 @@ var SystemCanvas = (() => {
18630
19056
  borderRadius: node.resolvedCornerRadius,
18631
19057
  outline: "none",
18632
19058
  resize: "none",
18633
- textAlign: node.type === "text" ? "center" : "left"
19059
+ textAlign
18634
19060
  };
18635
19061
  return (0, import_jsx_runtime17.jsx)("foreignObject", { x: node.x, y: node.y, width: node.width, height: node.height, onPointerDown: stopPointer, onMouseDown: stopPointer, onClick: stopPointer, onDoubleClick: stopPointer, children: node.type === "text" ? (0, import_jsx_runtime17.jsx)("textarea", { ref: textareaRef, value, onChange: (e) => setValue(e.target.value), onBlur: commit, onKeyDown: (e) => {
18636
19062
  if (e.key === "Enter" && !e.shiftKey) {
@@ -19000,7 +19426,7 @@ var SystemCanvas = (() => {
19000
19426
  // ../react/dist/components/Viewport.js
19001
19427
  var HOVER_PADDING = 10;
19002
19428
  var EDGE_PROXIMITY = 16;
19003
- var Viewport = (0, import_react15.forwardRef)(function Viewport2({ nodes, edges, nodeMap, theme, edgeStyle, columns, rows, canvases, minZoom, maxZoom, defaultViewport, onViewportChange, onNodeClick, onNodeDoubleClick, onNodeNavigate, onEdgeClick, onEdgeDoubleClick, onCanvasClick, onCanvasContextMenu, onNodeContextMenu, onEdgeContextMenu, onNodePointerDown, selectedId, editingId, selectedEdgeId, editingEdgeId, dragOverrides, resizeOverrides, onResizeHandlePointerDown, onEditorCommit, onEditorCancel, onEdgeEditorCommit, onEdgeEditorCancel, pendingEdge, onConnectionHandlePointerDown, edgeCreateEnabled, autoFit = "canvas-change", canvasRef, handoffTransform, onHandoffApplied, handoffFadeMs = 0 }, ref) {
19429
+ var Viewport = (0, import_react15.forwardRef)(function Viewport2({ nodes, edges, nodeMap, theme, edgeStyle, columns, rows, canvases, minZoom, maxZoom, defaultViewport, onViewportChange, onNodeClick, onNodeDoubleClick, onNodeNavigate, onEdgeClick, onEdgeDoubleClick, onCanvasClick, onCanvasContextMenu, onNodeContextMenu, onEdgeContextMenu, onNodePointerDown, selectedId, editingId, selectedEdgeId, editingEdgeId, dragOverrides, dropTargetId, resizeOverrides, onResizeHandlePointerDown, onEditorCommit, onEditorCancel, onEdgeEditorCommit, onEdgeEditorCancel, pendingEdge, onConnectionHandlePointerDown, edgeCreateEnabled, autoFit = "canvas-change", canvasRef, handoffTransform, onHandoffApplied, handoffFadeMs = 0 }, ref) {
19004
19430
  const { svgRef, groupRef, viewport, fitToContent, zoomToNode, setTransform } = useViewport({
19005
19431
  minZoom,
19006
19432
  maxZoom,
@@ -19043,6 +19469,7 @@ var SystemCanvas = (() => {
19043
19469
  }, []);
19044
19470
  const [hoveredNodeId, setHoveredNodeId] = (0, import_react15.useState)(null);
19045
19471
  const [hoveredSide, setHoveredSide] = (0, import_react15.useState)(null);
19472
+ const cursorPosRef = (0, import_react15.useRef)(null);
19046
19473
  (0, import_react15.useImperativeHandle)(ref, () => ({
19047
19474
  zoomToNode: (node, onComplete, options) => {
19048
19475
  navigatingRef.current = true;
@@ -19051,7 +19478,8 @@ var SystemCanvas = (() => {
19051
19478
  fitToContent,
19052
19479
  setTransform,
19053
19480
  getSvgElement: () => svgRef.current,
19054
- getViewport: () => viewport.current ?? { x: 0, y: 0, zoom: 1 }
19481
+ getViewport: () => viewport.current ?? { x: 0, y: 0, zoom: 1 },
19482
+ getCursorScreenPos: () => cursorPosRef.current
19055
19483
  }));
19056
19484
  const renderNodes = (0, import_react15.useMemo)(() => {
19057
19485
  const hasDrag = dragOverrides && dragOverrides.size > 0;
@@ -19134,14 +19562,17 @@ var SystemCanvas = (() => {
19134
19562
  ]);
19135
19563
  const editingNode = editingId ? renderNodes.find((n) => n.id === editingId) ?? null : null;
19136
19564
  const handleSvgPointerMove = (0, import_react15.useCallback)((event) => {
19137
- if (!edgeCreateEnabled)
19138
- return;
19139
19565
  const svg = svgRef.current;
19140
19566
  if (!svg)
19141
19567
  return;
19142
19568
  const rect = svg.getBoundingClientRect();
19569
+ const cursorScreenX = event.clientX - rect.left;
19570
+ const cursorScreenY = event.clientY - rect.top;
19571
+ cursorPosRef.current = { x: cursorScreenX, y: cursorScreenY };
19572
+ if (!edgeCreateEnabled)
19573
+ return;
19143
19574
  const vp = viewport.current ?? { x: 0, y: 0, zoom: 1 };
19144
- const { x, y } = screenToCanvas(event.clientX - rect.left, event.clientY - rect.top, vp);
19575
+ const { x, y } = screenToCanvas(cursorScreenX, cursorScreenY, vp);
19145
19576
  const pad = HOVER_PADDING;
19146
19577
  let hit = null;
19147
19578
  for (let i = renderNodes.length - 1; i >= 0; i--) {
@@ -19185,11 +19616,13 @@ var SystemCanvas = (() => {
19185
19616
  const handleSvgPointerLeave = (0, import_react15.useCallback)(() => {
19186
19617
  setHoveredNodeId(null);
19187
19618
  setHoveredSide(null);
19619
+ cursorPosRef.current = null;
19188
19620
  }, []);
19189
19621
  const handlesNodeId = pendingEdge?.sourceId ?? hoveredNodeId;
19190
19622
  const handlesNode = edgeCreateEnabled && handlesNodeId ? renderNodeMap.get(handlesNodeId) ?? null : null;
19191
19623
  const pendingSourceNode = pendingEdge ? renderNodeMap.get(pendingEdge.sourceId) ?? null : null;
19192
19624
  const pendingTargetNode = pendingEdge?.hoveredTargetId && pendingEdge.hoveredTargetId !== pendingEdge.sourceId ? renderNodeMap.get(pendingEdge.hoveredTargetId) ?? null : null;
19625
+ const dropTargetNode = dropTargetId ? renderNodeMap.get(dropTargetId) ?? null : null;
19193
19626
  const editingEdge = editingEdgeId ? edges.find((e) => e.id === editingEdgeId) ?? null : null;
19194
19627
  const editingEdgeMidpoint = (() => {
19195
19628
  if (!editingEdge)
@@ -19209,7 +19642,7 @@ var SystemCanvas = (() => {
19209
19642
  WebkitUserSelect: "none",
19210
19643
  MozUserSelect: "none",
19211
19644
  msUserSelect: "none"
19212
- }, onClick: onCanvasClick, onContextMenu: onCanvasContextMenu, onPointerMove: handleSvgPointerMove, onPointerLeave: handleSvgPointerLeave, children: [(0, import_jsx_runtime22.jsx)("defs", { children: (0, import_jsx_runtime22.jsx)("pattern", { id: "system-canvas-grid", width: theme.grid.size, height: theme.grid.size, patternUnits: "userSpaceOnUse", children: (0, import_jsx_runtime22.jsx)("path", { d: `M ${theme.grid.size} 0 L 0 0 0 ${theme.grid.size}`, fill: "none", stroke: theme.grid.color, strokeWidth: theme.grid.strokeWidth }) }) }), (0, import_jsx_runtime22.jsx)("rect", { x: "-50000", y: "-50000", width: "100000", height: "100000", fill: "url(#system-canvas-grid)" }), (0, import_jsx_runtime22.jsxs)("g", { ref: groupRef, children: [(0, import_jsx_runtime22.jsx)(LanesBackground, { columns, rows, theme }), (0, import_jsx_runtime22.jsx)(NodeRenderer, { nodes: renderNodes, theme, onClick: onNodeClick, onDoubleClick: onNodeDoubleClick, onContextMenu: onNodeContextMenu, onNavigate: onNodeNavigate, onPointerDown: onNodePointerDown, selectedId, editingId, canvases, only: "groups" }), (0, import_jsx_runtime22.jsx)(EdgeRenderer, { edges, nodeMap: renderNodeMap, theme, defaultEdgeStyle: edgeStyle, onClick: onEdgeClick, onDoubleClick: onEdgeDoubleClick, onContextMenu: onEdgeContextMenu, selectedId: selectedEdgeId, editingId: editingEdgeId }), (0, import_jsx_runtime22.jsx)(NodeRenderer, { nodes: renderNodes, theme, onClick: onNodeClick, onDoubleClick: onNodeDoubleClick, onContextMenu: onNodeContextMenu, onNavigate: onNodeNavigate, onPointerDown: onNodePointerDown, selectedId, editingId, onResizeHandlePointerDown, canvases, only: "non-groups" }), pendingTargetNode && (0, import_jsx_runtime22.jsx)("rect", { className: "system-canvas-drop-target", x: pendingTargetNode.x - 4, y: pendingTargetNode.y - 4, width: pendingTargetNode.width + 8, height: pendingTargetNode.height + 8, rx: pendingTargetNode.resolvedCornerRadius + 4, fill: "none", stroke: theme.node.labelColor, strokeWidth: 2, opacity: 0.85, pointerEvents: "none" }), pendingEdge && pendingSourceNode && (0, import_jsx_runtime22.jsx)(PendingEdgeRenderer, { sourceNode: pendingSourceNode, sourceSide: pendingEdge.sourceSide, cursor: pendingEdge.cursor, targetNode: pendingTargetNode, theme, defaultEdgeStyle: edgeStyle }), handlesNode && onConnectionHandlePointerDown && (0, import_jsx_runtime22.jsx)(ConnectionHandles, { node: handlesNode, theme, onHandlePointerDown: onConnectionHandlePointerDown, immediate: !!pendingEdge, activeSide: hoveredSide }), editingNode && onEditorCommit && onEditorCancel && (0, import_jsx_runtime22.jsx)(NodeEditor, { node: editingNode, theme, onCommit: onEditorCommit, onCancel: onEditorCancel }), editingEdge && editingEdgeMidpoint && onEdgeEditorCommit && onEdgeEditorCancel && (0, import_jsx_runtime22.jsx)(EdgeLabelEditor, { initialLabel: editingEdge.label ?? "", midpoint: editingEdgeMidpoint, theme, onCommit: onEdgeEditorCommit, onCancel: onEdgeEditorCancel })] })] });
19645
+ }, onClick: onCanvasClick, onContextMenu: onCanvasContextMenu, onPointerMove: handleSvgPointerMove, onPointerLeave: handleSvgPointerLeave, children: [(0, import_jsx_runtime22.jsx)("defs", { children: (0, import_jsx_runtime22.jsx)("pattern", { id: "system-canvas-grid", width: theme.grid.size, height: theme.grid.size, patternUnits: "userSpaceOnUse", children: (0, import_jsx_runtime22.jsx)("path", { d: `M ${theme.grid.size} 0 L 0 0 0 ${theme.grid.size}`, fill: "none", stroke: theme.grid.color, strokeWidth: theme.grid.strokeWidth }) }) }), (0, import_jsx_runtime22.jsx)("rect", { x: "-50000", y: "-50000", width: "100000", height: "100000", fill: "url(#system-canvas-grid)" }), (0, import_jsx_runtime22.jsxs)("g", { ref: groupRef, children: [(0, import_jsx_runtime22.jsx)(LanesBackground, { columns, rows, theme }), (0, import_jsx_runtime22.jsx)(NodeRenderer, { nodes: renderNodes, theme, onClick: onNodeClick, onDoubleClick: onNodeDoubleClick, onContextMenu: onNodeContextMenu, onNavigate: onNodeNavigate, onPointerDown: onNodePointerDown, selectedId, editingId, canvases, only: "groups" }), (0, import_jsx_runtime22.jsx)(EdgeRenderer, { edges, nodeMap: renderNodeMap, theme, defaultEdgeStyle: edgeStyle, onClick: onEdgeClick, onDoubleClick: onEdgeDoubleClick, onContextMenu: onEdgeContextMenu, selectedId: selectedEdgeId, editingId: editingEdgeId }), (0, import_jsx_runtime22.jsx)(NodeRenderer, { nodes: renderNodes, theme, onClick: onNodeClick, onDoubleClick: onNodeDoubleClick, onContextMenu: onNodeContextMenu, onNavigate: onNodeNavigate, onPointerDown: onNodePointerDown, selectedId, editingId, onResizeHandlePointerDown, canvases, only: "non-groups" }), pendingTargetNode && (0, import_jsx_runtime22.jsx)("rect", { className: "system-canvas-drop-target", x: pendingTargetNode.x - 4, y: pendingTargetNode.y - 4, width: pendingTargetNode.width + 8, height: pendingTargetNode.height + 8, rx: pendingTargetNode.resolvedCornerRadius + 4, fill: "none", stroke: theme.node.labelColor, strokeWidth: 2, opacity: 0.85, pointerEvents: "none" }), dropTargetNode && (0, import_jsx_runtime22.jsx)("rect", { className: "system-canvas-drop-target system-canvas-node-drop-target", x: dropTargetNode.x - 4, y: dropTargetNode.y - 4, width: dropTargetNode.width + 8, height: dropTargetNode.height + 8, rx: dropTargetNode.resolvedCornerRadius + 4, fill: "none", stroke: theme.node.labelColor, strokeWidth: 2, strokeDasharray: "6 4", opacity: 0.9, pointerEvents: "none" }), pendingEdge && pendingSourceNode && (0, import_jsx_runtime22.jsx)(PendingEdgeRenderer, { sourceNode: pendingSourceNode, sourceSide: pendingEdge.sourceSide, cursor: pendingEdge.cursor, targetNode: pendingTargetNode, theme, defaultEdgeStyle: edgeStyle }), handlesNode && onConnectionHandlePointerDown && (0, import_jsx_runtime22.jsx)(ConnectionHandles, { node: handlesNode, theme, onHandlePointerDown: onConnectionHandlePointerDown, immediate: !!pendingEdge, activeSide: hoveredSide }), editingNode && onEditorCommit && onEditorCancel && (0, import_jsx_runtime22.jsx)(NodeEditor, { node: editingNode, theme, onCommit: onEditorCommit, onCancel: onEditorCancel }), editingEdge && editingEdgeMidpoint && onEdgeEditorCommit && onEdgeEditorCancel && (0, import_jsx_runtime22.jsx)(EdgeLabelEditor, { initialLabel: editingEdge.label ?? "", midpoint: editingEdgeMidpoint, theme, onCommit: onEdgeEditorCommit, onCancel: onEdgeEditorCancel })] })] });
19213
19646
  });
19214
19647
 
19215
19648
  // ../react/dist/components/Breadcrumbs.js
@@ -19438,7 +19871,7 @@ var SystemCanvas = (() => {
19438
19871
  const cx = visibleLeft + visibleW / 2;
19439
19872
  if (endScreen <= colsOffsetLeft || startScreen >= width)
19440
19873
  return null;
19441
- return (0, import_jsx_runtime25.jsxs)("g", { children: [(0, import_jsx_runtime25.jsx)("line", { x1: startScreen, y1: pinned ? headerSize : y + headerSize, x2: startScreen, y2: pinned ? 0 : y, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth }), (0, import_jsx_runtime25.jsx)("text", { x: cx, y: (pinned ? 0 : y) + headerSize / 2, textAnchor: "middle", dominantBaseline: "middle", fill: lanesTheme.headerTextColor, fontFamily: lanesTheme.headerFontFamily, fontSize: lanesTheme.headerFontSize, style: { userSelect: "none" }, children: truncateToWidth(col.label, visibleW - pad * 2, lanesTheme.headerFontSize) })] }, `colh-${col.id}`);
19874
+ return (0, import_jsx_runtime25.jsxs)("g", { children: [(0, import_jsx_runtime25.jsx)("line", { x1: startScreen, y1: pinned ? headerSize : y + headerSize, x2: startScreen, y2: pinned ? 0 : y, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth }), (0, import_jsx_runtime25.jsx)("text", { x: cx, y: (pinned ? 0 : y) + headerSize / 2, textAnchor: "middle", dominantBaseline: "middle", fill: lanesTheme.headerTextColor, fontFamily: lanesTheme.headerFontFamily, fontSize: lanesTheme.headerFontSize, style: { userSelect: "none" }, children: truncateToWidth2(col.label, visibleW - pad * 2, lanesTheme.headerFontSize) })] }, `colh-${col.id}`);
19442
19875
  }), pinned && (0, import_jsx_runtime25.jsx)("line", { x1: colsOffsetLeft, y1: headerSize, x2: width, y2: headerSize, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth })] }), hasRows && (0, import_jsx_runtime25.jsxs)("g", { children: [pinned && (0, import_jsx_runtime25.jsx)("rect", { x: 0, y: rowsOffsetTop, width: headerSize, height: height - rowsOffsetTop, fill: lanesTheme.headerBackground }), rows.map((row) => {
19443
19876
  const startScreen = canvasToScreen(0, row.start, viewport).y;
19444
19877
  const endScreen = canvasToScreen(0, row.start + row.size, viewport).y;
@@ -19450,10 +19883,10 @@ var SystemCanvas = (() => {
19450
19883
  const cy = visibleTop + visibleH / 2;
19451
19884
  if (endScreen <= rowsOffsetTop || startScreen >= height)
19452
19885
  return null;
19453
- return (0, import_jsx_runtime25.jsxs)("g", { children: [(0, import_jsx_runtime25.jsx)("line", { x1: pinned ? 0 : x, y1: startScreen, x2: pinned ? headerSize : x + headerSize, y2: startScreen, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth }), (0, import_jsx_runtime25.jsx)("text", { x: (pinned ? 0 : x) + headerSize / 2, y: cy, textAnchor: "middle", dominantBaseline: "middle", fill: lanesTheme.headerTextColor, fontFamily: lanesTheme.headerFontFamily, fontSize: lanesTheme.headerFontSize, transform: `rotate(-90 ${(pinned ? 0 : x) + headerSize / 2} ${cy})`, style: { userSelect: "none" }, children: truncateToWidth(row.label, visibleH - pad * 2, lanesTheme.headerFontSize) })] }, `rowh-${row.id}`);
19886
+ return (0, import_jsx_runtime25.jsxs)("g", { children: [(0, import_jsx_runtime25.jsx)("line", { x1: pinned ? 0 : x, y1: startScreen, x2: pinned ? headerSize : x + headerSize, y2: startScreen, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth }), (0, import_jsx_runtime25.jsx)("text", { x: (pinned ? 0 : x) + headerSize / 2, y: cy, textAnchor: "middle", dominantBaseline: "middle", fill: lanesTheme.headerTextColor, fontFamily: lanesTheme.headerFontFamily, fontSize: lanesTheme.headerFontSize, transform: `rotate(-90 ${(pinned ? 0 : x) + headerSize / 2} ${cy})`, style: { userSelect: "none" }, children: truncateToWidth2(row.label, visibleH - pad * 2, lanesTheme.headerFontSize) })] }, `rowh-${row.id}`);
19454
19887
  }), pinned && (0, import_jsx_runtime25.jsx)("line", { x1: headerSize, y1: rowsOffsetTop, x2: headerSize, y2: height, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth })] }), hasColumns && hasRows && pinned && (0, import_jsx_runtime25.jsx)("rect", { x: 0, y: 0, width: headerSize, height: headerSize, fill: lanesTheme.headerBackground })] });
19455
19888
  }
19456
- function truncateToWidth(label, availablePx, fontSize) {
19889
+ function truncateToWidth2(label, availablePx, fontSize) {
19457
19890
  if (availablePx <= 0)
19458
19891
  return "";
19459
19892
  const charPx = fontSize * 0.6;
@@ -19495,8 +19928,10 @@ var SystemCanvas = (() => {
19495
19928
  raf = requestAnimationFrame(tick);
19496
19929
  return () => cancelAnimationFrame(raf);
19497
19930
  }, [getViewport]);
19498
- const topCenter = canvasToScreen(node.x + node.width / 2, node.y, viewport);
19499
- const bottomCenter = canvasToScreen(node.x + node.width / 2, node.y + node.height, viewport);
19931
+ const align = theme.toolbarAlign ?? "center";
19932
+ const anchorCanvasX = align === "left" ? node.x : align === "right" ? node.x + node.width : node.x + node.width / 2;
19933
+ const topAnchor = canvasToScreen(anchorCanvasX, node.y, viewport);
19934
+ const bottomAnchor = canvasToScreen(anchorCanvasX, node.y + node.height, viewport);
19500
19935
  const toolbarRef = (0, import_react19.useRef)(null);
19501
19936
  const [size, setSize] = (0, import_react19.useState)({ width: 0, height: 0 });
19502
19937
  (0, import_react19.useEffect)(() => {
@@ -19505,17 +19940,17 @@ var SystemCanvas = (() => {
19505
19940
  return;
19506
19941
  const update = () => {
19507
19942
  const r = el.getBoundingClientRect();
19508
- setSize({ width: r.width, height: r.height });
19943
+ setSize((prev) => prev.width === r.width && prev.height === r.height ? prev : { width: r.width, height: r.height });
19509
19944
  };
19510
19945
  update();
19511
19946
  const ro = new ResizeObserver(update);
19512
19947
  ro.observe(el);
19513
19948
  return () => ro.disconnect();
19514
- });
19515
- let left = topCenter.x - size.width / 2;
19516
- let top = topCenter.y - size.height - NODE_GAP;
19949
+ }, []);
19950
+ let left = align === "left" ? topAnchor.x : align === "right" ? topAnchor.x - size.width : topAnchor.x - size.width / 2;
19951
+ let top = topAnchor.y - size.height - NODE_GAP;
19517
19952
  if (top < FLIP_MARGIN) {
19518
- top = bottomCenter.y + NODE_GAP;
19953
+ top = bottomAnchor.y + NODE_GAP;
19519
19954
  }
19520
19955
  left = Math.max(FLIP_MARGIN, Math.min(left, containerWidth - size.width - FLIP_MARGIN));
19521
19956
  const patch = (update) => onPatch(update);
@@ -19554,7 +19989,7 @@ var SystemCanvas = (() => {
19554
19989
  }
19555
19990
  function DefaultToolbarContent({ node, theme, onPatch, onDelete }) {
19556
19991
  const groups = (0, import_react19.useMemo)(() => getNodeActionsForNode(node, theme), [node, theme]);
19557
- const showDelete = !theme.hideToolbarDelete;
19992
+ const showDelete = theme.showToolbarDelete === true;
19558
19993
  return (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [groups.map((group, i) => (0, import_jsx_runtime26.jsxs)(import_react19.default.Fragment, { children: [i > 0 && (0, import_jsx_runtime26.jsx)(Divider2, { theme }), (0, import_jsx_runtime26.jsx)(ActionGroupView, { group, node, theme, onPatch })] }, group.id)), showDelete && (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [(0, import_jsx_runtime26.jsx)(Divider2, { theme }), (0, import_jsx_runtime26.jsx)(DeleteButton, { theme, onDelete })] })] });
19559
19994
  }
19560
19995
  function Divider2({ theme }) {
@@ -19709,16 +20144,134 @@ var SystemCanvas = (() => {
19709
20144
  }, children: (0, import_jsx_runtime26.jsx)("svg", { width: DELETE_SIZE, height: DELETE_SIZE, viewBox: "0 0 16 16", children: (0, import_jsx_runtime26.jsx)("path", { d: "M 3 5 L 13 5 M 6 5 L 6 3 L 10 3 L 10 5 M 5 5 L 5.5 14 L 10.5 14 L 11 5 M 7 7 L 7 12 M 9 7 L 9 12", fill: "none", stroke: "currentColor", strokeWidth: 1.2, strokeLinecap: "round", strokeLinejoin: "round" }) }) });
19710
20145
  }
19711
20146
 
20147
+ // ../react/dist/components/NodeContextMenuOverlay.js
20148
+ var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
20149
+ var import_react20 = __toESM(require_react(), 1);
20150
+ var ESTIMATED_MENU_WIDTH = 200;
20151
+ var MIN_MENU_WIDTH = 160;
20152
+ var VIEWPORT_MARGIN = 8;
20153
+ function NodeContextMenuOverlay({ state, config, theme, onClose }) {
20154
+ const rootRef = (0, import_react20.useRef)(null);
20155
+ const [hoveredId, setHoveredId] = (0, import_react20.useState)(null);
20156
+ (0, import_react20.useEffect)(() => {
20157
+ if (state)
20158
+ setHoveredId(null);
20159
+ }, [state]);
20160
+ (0, import_react20.useEffect)(() => {
20161
+ if (!state)
20162
+ return;
20163
+ function onDown(e) {
20164
+ const root2 = rootRef.current;
20165
+ if (!root2)
20166
+ return;
20167
+ if (root2.contains(e.target))
20168
+ return;
20169
+ onClose();
20170
+ }
20171
+ function onKey(e) {
20172
+ if (e.key === "Escape") {
20173
+ e.stopPropagation();
20174
+ onClose();
20175
+ }
20176
+ }
20177
+ function onScroll() {
20178
+ onClose();
20179
+ }
20180
+ window.addEventListener("mousedown", onDown);
20181
+ window.addEventListener("keydown", onKey);
20182
+ window.addEventListener("scroll", onScroll, true);
20183
+ window.addEventListener("blur", onClose);
20184
+ return () => {
20185
+ window.removeEventListener("mousedown", onDown);
20186
+ window.removeEventListener("keydown", onKey);
20187
+ window.removeEventListener("scroll", onScroll, true);
20188
+ window.removeEventListener("blur", onClose);
20189
+ };
20190
+ }, [state, onClose]);
20191
+ if (!state)
20192
+ return null;
20193
+ const cm = theme.contextMenu;
20194
+ if (!cm)
20195
+ return null;
20196
+ const vw = typeof window !== "undefined" ? window.innerWidth : 0;
20197
+ const vh = typeof window !== "undefined" ? window.innerHeight : 0;
20198
+ const itemHeight = cm.itemPaddingY * 2 + cm.fontSize + 4;
20199
+ const estimatedHeight = state.items.length * itemHeight + cm.paddingY * 2;
20200
+ const left = vw ? Math.min(state.screenPosition.x, vw - ESTIMATED_MENU_WIDTH - VIEWPORT_MARGIN) : state.screenPosition.x;
20201
+ const top = vh ? Math.min(state.screenPosition.y, vh - estimatedHeight - VIEWPORT_MARGIN) : state.screenPosition.y;
20202
+ const matchCtx = { canvasRef: state.canvasRef };
20203
+ const anyIcon = state.items.some((item) => !!item.icon);
20204
+ return (0, import_jsx_runtime27.jsx)("div", {
20205
+ ref: rootRef,
20206
+ role: "menu",
20207
+ // Stop right-clicks inside the menu from bubbling into the document
20208
+ // listener and re-opening the canvas-level menu.
20209
+ onContextMenu: (e) => {
20210
+ e.preventDefault();
20211
+ e.stopPropagation();
20212
+ },
20213
+ style: {
20214
+ position: "fixed",
20215
+ left,
20216
+ top,
20217
+ zIndex: 1e3,
20218
+ minWidth: MIN_MENU_WIDTH,
20219
+ padding: `${cm.paddingY}px ${cm.paddingX}px`,
20220
+ background: cm.background,
20221
+ color: cm.itemColor,
20222
+ border: `1px solid ${cm.borderColor}`,
20223
+ borderRadius: cm.borderRadius,
20224
+ boxShadow: cm.shadow,
20225
+ fontFamily: cm.fontFamily,
20226
+ fontSize: cm.fontSize,
20227
+ backdropFilter: "blur(10px)",
20228
+ userSelect: "none",
20229
+ // `pointer-events: auto` so the menu still receives clicks even
20230
+ // when a parent canvas overlay disables them.
20231
+ pointerEvents: "auto"
20232
+ },
20233
+ children: state.items.map((item) => {
20234
+ const isDisabled = item.disabled?.(state.node, matchCtx) ?? false;
20235
+ const isHovered = !isDisabled && hoveredId === item.id;
20236
+ const color2 = item.destructive ? cm.destructiveItemColor : cm.itemColor;
20237
+ return (0, import_jsx_runtime27.jsxs)("div", { role: "menuitem", "aria-disabled": isDisabled, onMouseEnter: () => !isDisabled && setHoveredId(item.id), onMouseLeave: () => setHoveredId((id2) => id2 === item.id ? null : id2), onClick: () => {
20238
+ if (isDisabled)
20239
+ return;
20240
+ config.onSelect(item.id, state.node, {
20241
+ canvasRef: state.canvasRef,
20242
+ screenPosition: state.screenPosition
20243
+ });
20244
+ onClose();
20245
+ }, style: {
20246
+ display: "flex",
20247
+ alignItems: "center",
20248
+ gap: 10,
20249
+ padding: `${cm.itemPaddingY}px ${cm.itemPaddingX}px`,
20250
+ borderRadius: Math.max(0, cm.borderRadius - 4),
20251
+ cursor: isDisabled ? "not-allowed" : "pointer",
20252
+ opacity: isDisabled ? 0.45 : 1,
20253
+ background: isHovered ? cm.itemHoverBackground : "transparent",
20254
+ color: color2
20255
+ }, children: [item.icon ? (0, import_jsx_runtime27.jsx)("svg", { width: 14, height: 14, viewBox: "0 0 16 16", style: { flexShrink: 0, overflow: "visible" }, children: (0, import_jsx_runtime27.jsx)(NodeIcon, { icon: item.icon, x: 0, y: 0, size: 14, color: color2, opacity: 1, customIcons: theme.icons }) }) : anyIcon ? (
20256
+ // Only reserve a spacer when other items in the same menu
20257
+ // do have icons — keeps labels vertically aligned in a
20258
+ // mixed menu without padding lone-item menus.
20259
+ (0, import_jsx_runtime27.jsx)("span", { style: { width: 14, flexShrink: 0 }, "aria-hidden": true })
20260
+ ) : null, (0, import_jsx_runtime27.jsx)("span", { children: item.label })] }, item.id);
20261
+ })
20262
+ });
20263
+ }
20264
+
19712
20265
  // ../react/dist/components/SystemCanvas.js
19713
20266
  var CASCADE_WINDOW_MS = 1500;
19714
20267
  var CASCADE_OFFSET = 20;
19715
- var SystemCanvas = (0, import_react20.forwardRef)(function SystemCanvas2({ canvas, onResolveCanvas, canvases, rootLabel = "Home", onNavigate, onBreadcrumbClick, onNodeClick, onNodeDoubleClick, onEdgeClick, onEdgeDoubleClick, onContextMenu, editable = false, onNodeAdd, onNodeUpdate, onNodeDelete, onEdgeUpdate, onEdgeDelete, onEdgeAdd, renderAddNodeButton, showNodeToolbar = true, renderNodeToolbar, theme: themeProp, themes: customThemes, edgeStyle = "bezier", defaultViewport, minZoom: minZoomProp, maxZoom, onViewportChange, autoFit = "canvas-change", laneHeaders = "pinned", snapToLanes = false, zoomNavigation = false, className, style }, forwardedRef) {
19716
- const zoomNavConfig = (0, import_react20.useMemo)(() => {
20268
+ var SystemCanvas = (0, import_react21.forwardRef)(function SystemCanvas2({ canvas, onResolveCanvas, canvases, rootLabel = "Home", onNavigate, onBreadcrumbClick, onBreadcrumbsChange, onNodeClick, onNodeDoubleClick, onEdgeClick, onEdgeDoubleClick, onContextMenu, onSelectionChange, nodeContextMenu, editable = false, onNodeAdd, onNodeUpdate, onNodesUpdate, onNodeDelete, onEdgeUpdate, onEdgeDelete, onEdgeAdd, canDropNodeOn, onNodeDrop, renderAddNodeButton, showNodeToolbar = true, renderNodeToolbar, theme: themeProp, themes: customThemes, edgeStyle = "bezier", defaultViewport, minZoom: minZoomProp, maxZoom, onViewportChange, autoFit = "canvas-change", laneHeaders = "pinned", snapToLanes = false, zoomNavigation = false, className, style }, forwardedRef) {
20269
+ const zoomNavConfig = (0, import_react21.useMemo)(() => {
19717
20270
  const defaults = {
19718
20271
  enterThreshold: 0.66,
19719
20272
  exitThreshold: 0.33,
19720
20273
  prefetchThreshold: 0.4,
19721
- landingScale: 1.2,
20274
+ landingScale: 1.3,
19722
20275
  landingPadding: 0.08,
19723
20276
  fadeDuration: 216
19724
20277
  };
@@ -19738,16 +20291,16 @@ var SystemCanvas = (() => {
19738
20291
  }, [zoomNavigation]);
19739
20292
  const effectiveMaxZoom = maxZoom ?? (zoomNavConfig.enabled ? 16 : 4);
19740
20293
  const effectiveMinZoom = minZoomProp ?? (zoomNavConfig.enabled ? 0.01 : 0.1);
19741
- (0, import_react20.useEffect)(() => {
20294
+ (0, import_react21.useEffect)(() => {
19742
20295
  const env = globalThis.process?.env?.NODE_ENV;
19743
20296
  if (editable && !canvases && env !== "production") {
19744
20297
  console.warn("[system-canvas] `editable` is enabled but `canvases` prop is missing. Edits to sub-canvases will not be reflected without a synchronous ref \u2192 CanvasData map.");
19745
20298
  }
19746
20299
  }, [editable, canvases]);
19747
- const [parentFrames, setParentFrames] = (0, import_react20.useState)([]);
19748
- const [pendingHandoff, setPendingHandoff] = (0, import_react20.useState)(null);
19749
- const suppressNextHandoffClearRef = (0, import_react20.useRef)(false);
19750
- const handleBreadcrumbClick = (0, import_react20.useCallback)((index) => {
20300
+ const [parentFrames, setParentFrames] = (0, import_react21.useState)([]);
20301
+ const [pendingHandoff, setPendingHandoff] = (0, import_react21.useState)(null);
20302
+ const suppressNextHandoffClearRef = (0, import_react21.useRef)(false);
20303
+ const handleBreadcrumbClick = (0, import_react21.useCallback)((index) => {
19751
20304
  setParentFrames((prev) => prev.slice(0, index));
19752
20305
  if (suppressNextHandoffClearRef.current) {
19753
20306
  suppressNextHandoffClearRef.current = false;
@@ -19764,7 +20317,10 @@ var SystemCanvas = (() => {
19764
20317
  onNavigate,
19765
20318
  onBreadcrumbClick: handleBreadcrumbClick
19766
20319
  });
19767
- const theme = (0, import_react20.useMemo)(() => {
20320
+ (0, import_react21.useEffect)(() => {
20321
+ onBreadcrumbsChange?.(breadcrumbs);
20322
+ }, [breadcrumbs, onBreadcrumbsChange]);
20323
+ const theme = (0, import_react21.useMemo)(() => {
19768
20324
  const registry = { ...themes, ...customThemes };
19769
20325
  const resolveByName = (name) => name && registry[name] ? registry[name] : null;
19770
20326
  if (themeProp) {
@@ -19775,22 +20331,22 @@ var SystemCanvas = (() => {
19775
20331
  }
19776
20332
  return resolveByName(currentCanvas.theme?.base) ?? resolveByName(canvas.theme?.base) ?? darkTheme;
19777
20333
  }, [themeProp, customThemes, currentCanvas.theme?.base, canvas.theme?.base]);
19778
- const { nodes, edges, nodeMap } = (0, import_react20.useMemo)(() => {
20334
+ const { nodes, edges, nodeMap } = (0, import_react21.useMemo)(() => {
19779
20335
  const resolved = resolveCanvas(currentCanvas, theme);
19780
20336
  const map = buildNodeMap(resolved.nodes);
19781
20337
  return { nodes: resolved.nodes, edges: resolved.edges, nodeMap: map };
19782
20338
  }, [currentCanvas, theme]);
19783
- const nodesRef = (0, import_react20.useRef)(nodes);
20339
+ const nodesRef = (0, import_react21.useRef)(nodes);
19784
20340
  nodesRef.current = nodes;
19785
- const viewportStateRef = (0, import_react20.useRef)(defaultViewport ?? { x: 0, y: 0, zoom: 1 });
19786
- const viewportHandleRef = (0, import_react20.useRef)(null);
19787
- const navigateToRefRef = (0, import_react20.useRef)(navigateToRef);
20341
+ const viewportStateRef = (0, import_react21.useRef)(defaultViewport ?? { x: 0, y: 0, zoom: 1 });
20342
+ const viewportHandleRef = (0, import_react21.useRef)(null);
20343
+ const navigateToRefRef = (0, import_react21.useRef)(navigateToRef);
19788
20344
  navigateToRefRef.current = navigateToRef;
19789
- const navigateToBreadcrumbRef = (0, import_react20.useRef)(navigateToBreadcrumb);
20345
+ const navigateToBreadcrumbRef = (0, import_react21.useRef)(navigateToBreadcrumb);
19790
20346
  navigateToBreadcrumbRef.current = navigateToBreadcrumb;
19791
- const breadcrumbsRef = (0, import_react20.useRef)(breadcrumbs);
20347
+ const breadcrumbsRef = (0, import_react21.useRef)(breadcrumbs);
19792
20348
  breadcrumbsRef.current = breadcrumbs;
19793
- (0, import_react20.useImperativeHandle)(forwardedRef, () => ({
20349
+ (0, import_react21.useImperativeHandle)(forwardedRef, () => ({
19794
20350
  zoomIntoNode: (nodeId, options) => {
19795
20351
  return new Promise((resolve) => {
19796
20352
  const node = nodesRef.current.find((n) => n.id === nodeId);
@@ -19822,19 +20378,51 @@ var SystemCanvas = (() => {
19822
20378
  navigateToBreadcrumbRef.current(0);
19823
20379
  }
19824
20380
  }), [forwardedRef]);
19825
- const [selectedId, setSelectedId] = (0, import_react20.useState)(null);
19826
- const [editingId, setEditingId] = (0, import_react20.useState)(null);
19827
- const [selectedEdgeId, setSelectedEdgeId] = (0, import_react20.useState)(null);
19828
- const [editingEdgeId, setEditingEdgeId] = (0, import_react20.useState)(null);
19829
- (0, import_react20.useEffect)(() => {
20381
+ const [selectedId, setSelectedId] = (0, import_react21.useState)(null);
20382
+ const [editingId, setEditingId] = (0, import_react21.useState)(null);
20383
+ const [selectedEdgeId, setSelectedEdgeId] = (0, import_react21.useState)(null);
20384
+ const [editingEdgeId, setEditingEdgeId] = (0, import_react21.useState)(null);
20385
+ (0, import_react21.useEffect)(() => {
19830
20386
  setSelectedId(null);
19831
20387
  setEditingId(null);
19832
20388
  setSelectedEdgeId(null);
19833
20389
  setEditingEdgeId(null);
19834
20390
  }, [currentCanvasRef]);
19835
- const containerRef = (0, import_react20.useRef)(null);
19836
- const [containerSize, setContainerSize] = (0, import_react20.useState)({ width: 0, height: 0 });
19837
- (0, import_react20.useEffect)(() => {
20391
+ const onSelectionChangeRef = (0, import_react21.useRef)(onSelectionChange);
20392
+ (0, import_react21.useEffect)(() => {
20393
+ onSelectionChangeRef.current = onSelectionChange;
20394
+ }, [onSelectionChange]);
20395
+ const lastEmittedSelectionRef = (0, import_react21.useRef)({ kind: null, id: null, canvasRef: void 0 });
20396
+ (0, import_react21.useEffect)(() => {
20397
+ const cb = onSelectionChangeRef.current;
20398
+ let next = null;
20399
+ if (selectedId) {
20400
+ const node = nodeMap.get(selectedId);
20401
+ if (node)
20402
+ next = { kind: "node", node, canvasRef: currentCanvasRef };
20403
+ }
20404
+ if (!next && selectedEdgeId) {
20405
+ const edge = edges.find((e) => e.id === selectedEdgeId);
20406
+ if (edge)
20407
+ next = { kind: "edge", edge, canvasRef: currentCanvasRef };
20408
+ }
20409
+ const last = lastEmittedSelectionRef.current;
20410
+ const nextKind = next?.kind ?? null;
20411
+ const nextId = next ? next.kind === "node" ? next.node.id : next.edge.id : null;
20412
+ const nextRef = next?.canvasRef;
20413
+ if (last.kind === nextKind && last.id === nextId && last.canvasRef === nextRef) {
20414
+ return;
20415
+ }
20416
+ lastEmittedSelectionRef.current = {
20417
+ kind: nextKind,
20418
+ id: nextId,
20419
+ canvasRef: nextRef
20420
+ };
20421
+ cb?.(next);
20422
+ }, [selectedId, selectedEdgeId, currentCanvasRef, nodeMap, edges]);
20423
+ const containerRef = (0, import_react21.useRef)(null);
20424
+ const [containerSize, setContainerSize] = (0, import_react21.useState)({ width: 0, height: 0 });
20425
+ (0, import_react21.useEffect)(() => {
19838
20426
  const el = containerRef.current;
19839
20427
  if (!el)
19840
20428
  return;
@@ -19849,42 +20437,67 @@ var SystemCanvas = (() => {
19849
20437
  }, []);
19850
20438
  const hasLanes = currentCanvas.columns && currentCanvas.columns.length > 0 || currentCanvas.rows && currentCanvas.rows.length > 0;
19851
20439
  const showLaneHeaders = hasLanes && laneHeaders !== "none";
19852
- const getViewportState = (0, import_react20.useCallback)(() => viewportStateRef.current ?? { x: 0, y: 0, zoom: 1 }, []);
19853
- const commitDrag = (0, import_react20.useCallback)((id2, patch) => {
20440
+ const getViewportState = (0, import_react21.useCallback)(() => viewportStateRef.current ?? { x: 0, y: 0, zoom: 1 }, []);
20441
+ const applyLaneSnap = (0, import_react21.useCallback)((id2, patch) => {
20442
+ if (!snapToLanes)
20443
+ return patch;
20444
+ const cols = currentCanvas.columns;
20445
+ const rows = currentCanvas.rows;
20446
+ const node = nodesRef.current.find((n) => n.id === id2);
19854
20447
  let final = patch;
19855
- if (snapToLanes) {
19856
- const cols = currentCanvas.columns;
19857
- const rows = currentCanvas.rows;
19858
- const node = nodesRef.current.find((n) => n.id === id2);
19859
- const nx = patch.x;
19860
- const ny = patch.y;
19861
- if (cols && cols.length > 0 && nx != null) {
19862
- const snapped = snapToLane(nx, cols, {
19863
- edge: "center",
19864
- size: node?.width ?? 0
19865
- });
19866
- final = { ...final, x: Math.round(snapped) };
19867
- }
19868
- if (rows && rows.length > 0 && ny != null) {
19869
- const snapped = snapToLane(ny, rows, {
19870
- edge: "center",
19871
- size: node?.height ?? 0
19872
- });
19873
- final = { ...final, y: Math.round(snapped) };
19874
- }
20448
+ const nx = patch.x;
20449
+ const ny = patch.y;
20450
+ if (cols && cols.length > 0 && nx != null) {
20451
+ const snapped = snapToLane(nx, cols, {
20452
+ edge: "center",
20453
+ size: node?.width ?? 0
20454
+ });
20455
+ final = { ...final, x: Math.round(snapped) };
20456
+ }
20457
+ if (rows && rows.length > 0 && ny != null) {
20458
+ const snapped = snapToLane(ny, rows, {
20459
+ edge: "center",
20460
+ size: node?.height ?? 0
20461
+ });
20462
+ final = { ...final, y: Math.round(snapped) };
20463
+ }
20464
+ return final;
20465
+ }, [snapToLanes, currentCanvas.columns, currentCanvas.rows]);
20466
+ const commitResize = (0, import_react21.useCallback)((id2, patch) => {
20467
+ onNodeUpdate?.(id2, applyLaneSnap(id2, patch), currentCanvasRef);
20468
+ }, [onNodeUpdate, currentCanvasRef, applyLaneSnap]);
20469
+ const commitDragBatch = (0, import_react21.useCallback)((updates) => {
20470
+ const final = updates.map((u) => ({
20471
+ id: u.id,
20472
+ patch: applyLaneSnap(u.id, u.patch)
20473
+ }));
20474
+ if (onNodesUpdate) {
20475
+ onNodesUpdate(final, currentCanvasRef);
20476
+ return;
19875
20477
  }
19876
- onNodeUpdate?.(id2, final, currentCanvasRef);
19877
- }, [onNodeUpdate, currentCanvasRef, snapToLanes, currentCanvas.columns, currentCanvas.rows]);
19878
- const { dragOverrides, onPointerDown: onNodePointerDown } = useNodeDrag({
20478
+ if (!onNodeUpdate)
20479
+ return;
20480
+ for (const { id: id2, patch } of final) {
20481
+ onNodeUpdate(id2, patch, currentCanvasRef);
20482
+ }
20483
+ }, [onNodeUpdate, onNodesUpdate, currentCanvasRef, applyLaneSnap]);
20484
+ const svgProxyRef = (0, import_react21.useRef)(null);
20485
+ const handleNodeDrop = (0, import_react21.useCallback)((sources, target) => {
20486
+ onNodeDrop?.(sources, target, { canvasRef: currentCanvasRef });
20487
+ }, [onNodeDrop, currentCanvasRef]);
20488
+ const { dragOverrides, dropTargetId, onPointerDown: onNodePointerDown } = useNodeDrag({
19879
20489
  viewport: viewportStateRef,
19880
20490
  nodesRef,
19881
- onCommit: commitDrag
20491
+ onCommit: commitDragBatch,
20492
+ svgRef: svgProxyRef,
20493
+ canDropNodeOn,
20494
+ onNodeDrop: handleNodeDrop
19882
20495
  });
19883
20496
  const { resizeOverrides, onHandlePointerDown: onResizeHandlePointerDown } = useNodeResize({
19884
20497
  viewport: viewportStateRef,
19885
- onCommit: commitDrag
20498
+ onCommit: commitResize
19886
20499
  });
19887
- const selectedResolvedNode = (0, import_react20.useMemo)(() => {
20500
+ const selectedResolvedNode = (0, import_react21.useMemo)(() => {
19888
20501
  if (!selectedId)
19889
20502
  return null;
19890
20503
  const base = nodeMap.get(selectedId);
@@ -19900,9 +20513,8 @@ var SystemCanvas = (() => {
19900
20513
  }
19901
20514
  return base;
19902
20515
  }, [selectedId, nodeMap, dragOverrides, resizeOverrides]);
19903
- const svgProxyRef = (0, import_react20.useRef)(null);
19904
20516
  svgProxyRef.current = viewportHandleRef.current?.getSvgElement() ?? null;
19905
- const handleEdgeCreated = (0, import_react20.useCallback)((edge) => {
20517
+ const handleEdgeCreated = (0, import_react21.useCallback)((edge) => {
19906
20518
  onEdgeAdd?.(edge, currentCanvasRef);
19907
20519
  }, [onEdgeAdd, currentCanvasRef]);
19908
20520
  const { pending: pendingEdge, onHandlePointerDown: onConnectionHandlePointerDown } = useEdgeCreate({
@@ -19911,7 +20523,7 @@ var SystemCanvas = (() => {
19911
20523
  nodesRef,
19912
20524
  onCreate: handleEdgeCreated
19913
20525
  });
19914
- const handleNavigableNodeClick = (0, import_react20.useCallback)((node) => {
20526
+ const handleNavigableNodeClick = (0, import_react21.useCallback)((node) => {
19915
20527
  const frame2 = {
19916
20528
  parentCanvasRef: currentCanvasRef,
19917
20529
  parentNodeRect: {
@@ -19938,7 +20550,7 @@ var SystemCanvas = (() => {
19938
20550
  navigateToRef(node);
19939
20551
  }
19940
20552
  }, [navigateToRef, currentCanvasRef, zoomNavConfig.enabled]);
19941
- const handleZoomEnter = (0, import_react20.useCallback)((node, targetTransform) => {
20553
+ const handleZoomEnter = (0, import_react21.useCallback)((node, targetTransform) => {
19942
20554
  const frame2 = {
19943
20555
  parentCanvasRef: currentCanvasRef,
19944
20556
  parentNodeRect: {
@@ -19952,7 +20564,7 @@ var SystemCanvas = (() => {
19952
20564
  setPendingHandoff(targetTransform);
19953
20565
  navigateToRef(node);
19954
20566
  }, [currentCanvasRef, navigateToRef]);
19955
- const handleZoomExit = (0, import_react20.useCallback)((targetTransform) => {
20567
+ const handleZoomExit = (0, import_react21.useCallback)((targetTransform) => {
19956
20568
  setPendingHandoff(targetTransform);
19957
20569
  suppressNextHandoffClearRef.current = true;
19958
20570
  navigateToBreadcrumb(breadcrumbs.length - 2);
@@ -19975,30 +20587,58 @@ var SystemCanvas = (() => {
19975
20587
  const rect = svg.getBoundingClientRect();
19976
20588
  return { width: rect.width, height: rect.height };
19977
20589
  },
20590
+ getCursorScreenPos: () => viewportHandleRef.current?.getCursorScreenPos() ?? null,
19978
20591
  onEnter: handleZoomEnter,
19979
20592
  onExit: handleZoomExit
19980
20593
  });
19981
- const handleViewportChange = (0, import_react20.useCallback)((vp) => {
20594
+ const handleViewportChange = (0, import_react21.useCallback)((vp) => {
19982
20595
  viewportStateRef.current = vp;
19983
20596
  handleZoomNavViewportChange(vp);
19984
20597
  onViewportChange?.(vp);
19985
20598
  }, [handleZoomNavViewportChange, onViewportChange]);
19986
- const handleHandoffApplied = (0, import_react20.useCallback)(() => {
20599
+ const handleHandoffApplied = (0, import_react21.useCallback)(() => {
19987
20600
  setPendingHandoff(null);
19988
20601
  clearZoomNavCommitting();
19989
20602
  }, [clearZoomNavCommitting]);
19990
- const handleBeginEdit = (0, import_react20.useCallback)((node) => {
20603
+ const handleBeginEdit = (0, import_react21.useCallback)((node) => {
19991
20604
  setEditingId(node.id);
19992
20605
  }, []);
19993
- const handleBeginEditEdge = (0, import_react20.useCallback)((edge) => {
20606
+ const handleBeginEditEdge = (0, import_react21.useCallback)((edge) => {
19994
20607
  setEditingEdgeId(edge.id);
19995
20608
  }, []);
20609
+ const [contextMenuState, setContextMenuState] = (0, import_react21.useState)(null);
20610
+ (0, import_react21.useEffect)(() => {
20611
+ setContextMenuState(null);
20612
+ }, [currentCanvasRef]);
20613
+ const handleContextMenu = (0, import_react21.useCallback)((event) => {
20614
+ onContextMenu?.(event);
20615
+ if (!nodeContextMenu)
20616
+ return;
20617
+ if (event.type !== "node")
20618
+ return;
20619
+ const node = event.target;
20620
+ if (!node)
20621
+ return;
20622
+ const matched = filterContextMenuItems(nodeContextMenu.items, node, {
20623
+ canvasRef: currentCanvasRef ?? null
20624
+ });
20625
+ if (matched.length === 0) {
20626
+ setContextMenuState(null);
20627
+ return;
20628
+ }
20629
+ setContextMenuState({
20630
+ items: matched,
20631
+ node,
20632
+ screenPosition: event.screenPosition,
20633
+ canvasRef: currentCanvasRef ?? null
20634
+ });
20635
+ }, [onContextMenu, nodeContextMenu, currentCanvasRef]);
19996
20636
  const { handleNodeClick, handleNodeDoubleClick, handleNodeNavigate, handleEdgeClick, handleEdgeDoubleClick, handleCanvasClick, handleCanvasContextMenu, handleNodeContextMenu, handleEdgeContextMenu } = useCanvasInteraction({
19997
20637
  onNodeClick,
19998
20638
  onNodeDoubleClick,
19999
20639
  onEdgeClick,
20000
20640
  onEdgeDoubleClick,
20001
- onContextMenu,
20641
+ onContextMenu: handleContextMenu,
20002
20642
  onNavigableNodeClick: handleNavigableNodeClick,
20003
20643
  viewport: viewportStateRef,
20004
20644
  editable,
@@ -20007,27 +20647,27 @@ var SystemCanvas = (() => {
20007
20647
  onSelectEdge: setSelectedEdgeId,
20008
20648
  onBeginEditEdge: handleBeginEditEdge
20009
20649
  });
20010
- const handleEditorCommit = (0, import_react20.useCallback)((patch) => {
20650
+ const handleEditorCommit = (0, import_react21.useCallback)((patch) => {
20011
20651
  if (editingId) {
20012
20652
  onNodeUpdate?.(editingId, patch, currentCanvasRef);
20013
20653
  }
20014
20654
  setEditingId(null);
20015
20655
  }, [editingId, onNodeUpdate, currentCanvasRef]);
20016
- const handleEditorCancel = (0, import_react20.useCallback)(() => {
20656
+ const handleEditorCancel = (0, import_react21.useCallback)(() => {
20017
20657
  setEditingId(null);
20018
20658
  }, []);
20019
- const handleEdgeEditorCommit = (0, import_react20.useCallback)((patch) => {
20659
+ const handleEdgeEditorCommit = (0, import_react21.useCallback)((patch) => {
20020
20660
  if (editingEdgeId) {
20021
20661
  onEdgeUpdate?.(editingEdgeId, patch, currentCanvasRef);
20022
20662
  }
20023
20663
  setEditingEdgeId(null);
20024
20664
  }, [editingEdgeId, onEdgeUpdate, currentCanvasRef]);
20025
- const handleEdgeEditorCancel = (0, import_react20.useCallback)(() => {
20665
+ const handleEdgeEditorCancel = (0, import_react21.useCallback)(() => {
20026
20666
  setEditingEdgeId(null);
20027
20667
  }, []);
20028
- const lastAddRef = (0, import_react20.useRef)(null);
20029
- const menuOptions = (0, import_react20.useMemo)(() => getNodeMenuOptions(currentCanvas, theme), [currentCanvas, theme]);
20030
- const addNode2 = (0, import_react20.useCallback)((option, position) => {
20668
+ const lastAddRef = (0, import_react21.useRef)(null);
20669
+ const menuOptions = (0, import_react21.useMemo)(() => getNodeMenuOptions(currentCanvas, theme), [currentCanvas, theme]);
20670
+ const addNode2 = (0, import_react21.useCallback)((option, position) => {
20031
20671
  let x, y;
20032
20672
  if (position) {
20033
20673
  x = position.x;
@@ -20059,7 +20699,7 @@ var SystemCanvas = (() => {
20059
20699
  const node = createNodeFromOption(option, Math.round(x), Math.round(y), void 0, theme);
20060
20700
  onNodeAdd?.(node, currentCanvasRef);
20061
20701
  }, [onNodeAdd, currentCanvasRef, theme]);
20062
- const handleKeyDown = (0, import_react20.useCallback)((e) => {
20702
+ const handleKeyDown = (0, import_react21.useCallback)((e) => {
20063
20703
  if (!editable)
20064
20704
  return;
20065
20705
  if (e.key === "Escape") {
@@ -20093,14 +20733,14 @@ var SystemCanvas = (() => {
20093
20733
  currentCanvasRef
20094
20734
  ]);
20095
20735
  const renderProps = { options: menuOptions, addNode: addNode2, theme };
20096
- return (0, import_jsx_runtime27.jsxs)("div", { ref: containerRef, className: `system-canvas ${className ?? ""}`, tabIndex: editable ? 0 : -1, onKeyDown: handleKeyDown, style: {
20736
+ return (0, import_jsx_runtime28.jsxs)("div", { ref: containerRef, className: `system-canvas ${className ?? ""}`, tabIndex: editable ? 0 : -1, onKeyDown: handleKeyDown, style: {
20097
20737
  position: "relative",
20098
20738
  width: "100%",
20099
20739
  height: "100%",
20100
20740
  overflow: "hidden",
20101
20741
  outline: "none",
20102
20742
  ...style
20103
- }, children: [(0, import_jsx_runtime27.jsx)(Breadcrumbs, { breadcrumbs, theme: theme.breadcrumbs, onNavigate: navigateToBreadcrumb }), isLoading && (0, import_jsx_runtime27.jsx)("div", { className: "system-canvas-loading", style: {
20743
+ }, children: [(0, import_jsx_runtime28.jsx)(Breadcrumbs, { breadcrumbs, theme: theme.breadcrumbs, onNavigate: navigateToBreadcrumb }), isLoading && (0, import_jsx_runtime28.jsx)("div", { className: "system-canvas-loading", style: {
20104
20744
  position: "absolute",
20105
20745
  top: 12,
20106
20746
  right: 12,
@@ -20112,12 +20752,12 @@ var SystemCanvas = (() => {
20112
20752
  fontFamily: theme.node.fontFamily,
20113
20753
  fontSize: 12,
20114
20754
  backdropFilter: "blur(8px)"
20115
- }, children: "Loading..." }), (0, import_jsx_runtime27.jsx)(Viewport, { ref: viewportHandleRef, nodes, edges, nodeMap, theme, edgeStyle, columns: currentCanvas.columns, rows: currentCanvas.rows, canvases, minZoom: effectiveMinZoom, maxZoom: effectiveMaxZoom, defaultViewport, autoFit, canvasRef: currentCanvasRef, handoffTransform: pendingHandoff, onHandoffApplied: handleHandoffApplied, handoffFadeMs: zoomNavConfig.fadeDuration, onViewportChange: handleViewportChange, onNodeClick: handleNodeClick, onNodeDoubleClick: handleNodeDoubleClick, onNodeNavigate: handleNodeNavigate, onEdgeClick: handleEdgeClick, onEdgeDoubleClick: handleEdgeDoubleClick, onCanvasClick: editable ? handleCanvasClick : void 0, onCanvasContextMenu: handleCanvasContextMenu, onNodeContextMenu: handleNodeContextMenu, onEdgeContextMenu: handleEdgeContextMenu, onNodePointerDown: editable ? onNodePointerDown : void 0, selectedId: editable ? selectedId : null, editingId: editable ? editingId : null, selectedEdgeId: editable ? selectedEdgeId : null, editingEdgeId: editable ? editingEdgeId : null, dragOverrides, resizeOverrides, onResizeHandlePointerDown: editable ? onResizeHandlePointerDown : void 0, onEditorCommit: handleEditorCommit, onEditorCancel: handleEditorCancel, onEdgeEditorCommit: handleEdgeEditorCommit, onEdgeEditorCancel: handleEdgeEditorCancel, pendingEdge: editable ? pendingEdge : null, onConnectionHandlePointerDown: editable ? onConnectionHandlePointerDown : void 0, edgeCreateEnabled: editable }), showLaneHeaders && (0, import_jsx_runtime27.jsx)(LaneHeaders, { columns: currentCanvas.columns, rows: currentCanvas.rows, theme, getViewport: getViewportState, width: containerSize.width, height: containerSize.height, pinned: laneHeaders === "pinned" }), editable && showNodeToolbar && selectedResolvedNode && !editingId && (0, import_jsx_runtime27.jsx)(NodeToolbar, { node: selectedResolvedNode, theme, onPatch: (update) => {
20755
+ }, children: "Loading..." }), (0, import_jsx_runtime28.jsx)(Viewport, { ref: viewportHandleRef, nodes, edges, nodeMap, theme, edgeStyle, columns: currentCanvas.columns, rows: currentCanvas.rows, canvases, minZoom: effectiveMinZoom, maxZoom: effectiveMaxZoom, defaultViewport, autoFit, canvasRef: currentCanvasRef, handoffTransform: pendingHandoff, onHandoffApplied: handleHandoffApplied, handoffFadeMs: zoomNavConfig.fadeDuration, onViewportChange: handleViewportChange, onNodeClick: handleNodeClick, onNodeDoubleClick: handleNodeDoubleClick, onNodeNavigate: handleNodeNavigate, onEdgeClick: handleEdgeClick, onEdgeDoubleClick: handleEdgeDoubleClick, onCanvasClick: editable ? handleCanvasClick : void 0, onCanvasContextMenu: handleCanvasContextMenu, onNodeContextMenu: handleNodeContextMenu, onEdgeContextMenu: handleEdgeContextMenu, onNodePointerDown: editable ? onNodePointerDown : void 0, selectedId: editable ? selectedId : null, editingId: editable ? editingId : null, selectedEdgeId: editable ? selectedEdgeId : null, editingEdgeId: editable ? editingEdgeId : null, dragOverrides, dropTargetId, resizeOverrides, onResizeHandlePointerDown: editable ? onResizeHandlePointerDown : void 0, onEditorCommit: handleEditorCommit, onEditorCancel: handleEditorCancel, onEdgeEditorCommit: handleEdgeEditorCommit, onEdgeEditorCancel: handleEdgeEditorCancel, pendingEdge: editable ? pendingEdge : null, onConnectionHandlePointerDown: editable ? onConnectionHandlePointerDown : void 0, edgeCreateEnabled: editable }), showLaneHeaders && (0, import_jsx_runtime28.jsx)(LaneHeaders, { columns: currentCanvas.columns, rows: currentCanvas.rows, theme, getViewport: getViewportState, width: containerSize.width, height: containerSize.height, pinned: laneHeaders === "pinned" }), editable && showNodeToolbar && selectedResolvedNode && !editingId && (0, import_jsx_runtime28.jsx)(NodeToolbar, { node: selectedResolvedNode, theme, onPatch: (update) => {
20116
20756
  onNodeUpdate?.(selectedResolvedNode.id, update, currentCanvasRef);
20117
20757
  }, onDelete: () => {
20118
20758
  onNodeDelete?.(selectedResolvedNode.id, currentCanvasRef);
20119
20759
  setSelectedId(null);
20120
- }, getViewport: getViewportState, containerWidth: containerSize.width, containerHeight: containerSize.height, render: renderNodeToolbar }), editable && (renderAddNodeButton ? renderAddNodeButton(renderProps) : (0, import_jsx_runtime27.jsx)(AddNodeButton, { ...renderProps }))] });
20760
+ }, getViewport: getViewportState, containerWidth: containerSize.width, containerHeight: containerSize.height, render: renderNodeToolbar }), editable && (renderAddNodeButton ? renderAddNodeButton(renderProps) : (0, import_jsx_runtime28.jsx)(AddNodeButton, { ...renderProps })), nodeContextMenu && (0, import_jsx_runtime28.jsx)(NodeContextMenuOverlay, { state: contextMenuState, config: nodeContextMenu, theme, onClose: () => setContextMenuState(null) })] });
20121
20761
  });
20122
20762
 
20123
20763
  // src/index.tsx
@@ -20228,7 +20868,7 @@ var SystemCanvas = (() => {
20228
20868
  onEdgeUpdate: handleEdgeUpdate,
20229
20869
  onEdgeDelete: handleEdgeDelete
20230
20870
  };
20231
- root2.render(import_react21.default.createElement(SystemCanvas, props));
20871
+ root2.render(import_react22.default.createElement(SystemCanvas, props));
20232
20872
  };
20233
20873
  doRender();
20234
20874
  return {