system-canvas-standalone 0.1.2 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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_react24 = __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
  }
@@ -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
@@ -14438,15 +14649,19 @@ var SystemCanvas = (() => {
14438
14649
  // ../react/dist/hooks/useCanvasInteraction.js
14439
14650
  var import_react2 = __toESM(require_react(), 1);
14440
14651
  function useCanvasInteraction(options) {
14441
- const { onNodeClick, onNodeDoubleClick, onEdgeClick, onEdgeDoubleClick, onContextMenu, onNavigableNodeClick, viewport, editable, onSelect, onBeginEdit, onSelectEdge, onBeginEditEdge } = options;
14652
+ const { onNodeClick, onNodeDoubleClick, onEdgeClick, onEdgeDoubleClick, onContextMenu, onNavigableNodeClick, viewport, editable, onSelect, onToggleSelect, onBeginEdit, onSelectEdge, onBeginEditEdge } = options;
14442
14653
  const handleNodeClick = (0, import_react2.useCallback)((node, event) => {
14443
14654
  event.stopPropagation();
14444
14655
  if (editable) {
14445
- onSelect?.(node.id);
14446
- onSelectEdge?.(null);
14656
+ if (event.shiftKey && onToggleSelect) {
14657
+ onToggleSelect(node.id);
14658
+ } else {
14659
+ onSelect?.(node.id);
14660
+ onSelectEdge?.(null);
14661
+ }
14447
14662
  }
14448
14663
  onNodeClick?.(node);
14449
- }, [editable, onNodeClick, onSelect, onSelectEdge]);
14664
+ }, [editable, onNodeClick, onSelect, onToggleSelect, onSelectEdge]);
14450
14665
  const handleNodeDoubleClick = (0, import_react2.useCallback)((node, event) => {
14451
14666
  event.stopPropagation();
14452
14667
  onNodeDoubleClick?.(node);
@@ -14484,7 +14699,12 @@ var SystemCanvas = (() => {
14484
14699
  onContextMenu({
14485
14700
  type,
14486
14701
  target,
14487
- position: canvasPos
14702
+ position: canvasPos,
14703
+ // Raw viewport coords from the original MouseEvent. Consumers
14704
+ // rendering a `position: fixed` floating menu need these — the
14705
+ // canvas-space `position` would be wrong because it walks
14706
+ // with the user's pan/zoom.
14707
+ screenPosition: { x: event.clientX, y: event.clientY }
14488
14708
  });
14489
14709
  };
14490
14710
  }, [onContextMenu, viewport]);
@@ -14520,11 +14740,45 @@ var SystemCanvas = (() => {
14520
14740
  var import_react3 = __toESM(require_react(), 1);
14521
14741
  var DRAG_THRESHOLD = 3;
14522
14742
  function useNodeDrag(options) {
14523
- const { viewport, nodesRef, onCommit } = options;
14743
+ const { viewport, nodesRef, onCommit, svgRef, canDropNodeOn, onNodeDrop, selectedIdsRef } = options;
14524
14744
  const [dragOverrides, setDragOverrides] = (0, import_react3.useState)(() => /* @__PURE__ */ new Map());
14525
14745
  const [isDragging, setIsDragging] = (0, import_react3.useState)(false);
14746
+ const [dropTargetId, setDropTargetId] = (0, import_react3.useState)(null);
14526
14747
  const stateRef = (0, import_react3.useRef)(null);
14527
14748
  const movedRef = (0, import_react3.useRef)(false);
14749
+ const canDropNodeOnRef = (0, import_react3.useRef)(canDropNodeOn);
14750
+ canDropNodeOnRef.current = canDropNodeOn;
14751
+ const onNodeDropRef = (0, import_react3.useRef)(onNodeDrop);
14752
+ onNodeDropRef.current = onNodeDrop;
14753
+ const svgRefRef = (0, import_react3.useRef)(svgRef);
14754
+ svgRefRef.current = svgRef;
14755
+ const emptySetRef = (0, import_react3.useRef)(/* @__PURE__ */ new Set());
14756
+ const effectiveSelectedIdsRef = selectedIdsRef ?? emptySetRef;
14757
+ const dropTargetIdRef = (0, import_react3.useRef)(null);
14758
+ const computeDropTarget = (0, import_react3.useCallback)((clientX, clientY) => {
14759
+ const cb = canDropNodeOnRef.current;
14760
+ const st = stateRef.current;
14761
+ const nodes = nodesRef.current;
14762
+ const svg = svgRefRef.current?.current;
14763
+ if (!cb || !st || !nodes || !svg)
14764
+ return null;
14765
+ const rect = svg.getBoundingClientRect();
14766
+ const vp = viewport.current ?? { x: 0, y: 0, zoom: 1 };
14767
+ const { x, y } = screenToCanvas(clientX - rect.left, clientY - rect.top, vp);
14768
+ for (let i = nodes.length - 1; i >= 0; i--) {
14769
+ const n = nodes[i];
14770
+ if (st.moving.has(n.id))
14771
+ continue;
14772
+ if (x < n.x || x > n.x + n.width)
14773
+ continue;
14774
+ if (y < n.y || y > n.y + n.height)
14775
+ continue;
14776
+ if (cb([st.source], n))
14777
+ return n.id;
14778
+ return null;
14779
+ }
14780
+ return null;
14781
+ }, [nodesRef, viewport]);
14528
14782
  const onPointerMove = (0, import_react3.useCallback)((event) => {
14529
14783
  const st = stateRef.current;
14530
14784
  if (!st || event.pointerId !== st.pointerId)
@@ -14546,7 +14800,12 @@ var SystemCanvas = (() => {
14546
14800
  next.set(id2, { x: start2.startX + dx, y: start2.startY + dy });
14547
14801
  }
14548
14802
  setDragOverrides(next);
14549
- }, [viewport]);
14803
+ const nextTarget = computeDropTarget(event.clientX, event.clientY);
14804
+ if (nextTarget !== dropTargetIdRef.current) {
14805
+ dropTargetIdRef.current = nextTarget;
14806
+ setDropTargetId(nextTarget);
14807
+ }
14808
+ }, [viewport, computeDropTarget]);
14550
14809
  const finishDrag = (0, import_react3.useCallback)((commit) => {
14551
14810
  const st = stateRef.current;
14552
14811
  if (!st)
@@ -14559,17 +14818,38 @@ var SystemCanvas = (() => {
14559
14818
  st.captureTarget.releasePointerCapture?.(st.pointerId);
14560
14819
  } catch {
14561
14820
  }
14821
+ const dropTarget = dropTargetIdRef.current;
14822
+ if (commit && movedRef.current && dropTarget) {
14823
+ const nodes = nodesRef.current;
14824
+ const target = nodes?.find((n) => n.id === dropTarget) ?? null;
14825
+ if (target && onNodeDropRef.current) {
14826
+ onNodeDropRef.current([st.source], target);
14827
+ stateRef.current = null;
14828
+ movedRef.current = false;
14829
+ dropTargetIdRef.current = null;
14830
+ setIsDragging(false);
14831
+ setDragOverrides(/* @__PURE__ */ new Map());
14832
+ setDropTargetId(null);
14833
+ return;
14834
+ }
14835
+ }
14562
14836
  if (commit && movedRef.current) {
14563
14837
  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) });
14838
+ if (overrides.length > 0) {
14839
+ const updates = overrides.map(([id2, pos]) => ({
14840
+ id: id2,
14841
+ patch: { x: Math.round(pos.x), y: Math.round(pos.y) }
14842
+ }));
14843
+ onCommit(updates);
14566
14844
  }
14567
14845
  }
14568
14846
  stateRef.current = null;
14569
14847
  movedRef.current = false;
14848
+ dropTargetIdRef.current = null;
14570
14849
  setIsDragging(false);
14571
14850
  setDragOverrides(/* @__PURE__ */ new Map());
14572
- }, [onPointerMove, onCommit]);
14851
+ setDropTargetId(null);
14852
+ }, [onPointerMove, onCommit, nodesRef]);
14573
14853
  const dragOverridesRef = (0, import_react3.useRef)(dragOverrides);
14574
14854
  dragOverridesRef.current = dragOverrides;
14575
14855
  const onPointerUpRef = (0, import_react3.useRef)(null);
@@ -14595,12 +14875,22 @@ var SystemCanvas = (() => {
14595
14875
  return;
14596
14876
  event.stopPropagation();
14597
14877
  const moving = /* @__PURE__ */ new Map();
14598
- moving.set(node.id, { startX: node.x, startY: node.y });
14599
- if (node.type === "group" && nodesRef.current) {
14600
- const children2 = getGroupChildren(node, nodesRef.current);
14601
- for (const c of children2) {
14602
- if (!moving.has(c.id)) {
14603
- moving.set(c.id, { startX: c.x, startY: c.y });
14878
+ const sel = effectiveSelectedIdsRef.current;
14879
+ if (sel && sel.size > 1 && sel.has(node.id) && nodesRef.current) {
14880
+ for (const id2 of sel) {
14881
+ const n = nodesRef.current.find((r) => r.id === id2);
14882
+ if (n) {
14883
+ moving.set(id2, { startX: n.x, startY: n.y });
14884
+ }
14885
+ }
14886
+ } else {
14887
+ moving.set(node.id, { startX: node.x, startY: node.y });
14888
+ if (node.type === "group" && nodesRef.current) {
14889
+ const children2 = getGroupChildren(node, nodesRef.current);
14890
+ for (const c of children2) {
14891
+ if (!moving.has(c.id)) {
14892
+ moving.set(c.id, { startX: c.x, startY: c.y });
14893
+ }
14604
14894
  }
14605
14895
  }
14606
14896
  }
@@ -14609,9 +14899,11 @@ var SystemCanvas = (() => {
14609
14899
  startClientY: event.clientY,
14610
14900
  captureTarget: event.currentTarget,
14611
14901
  pointerId: event.pointerId,
14612
- moving
14902
+ moving,
14903
+ source: node
14613
14904
  };
14614
14905
  movedRef.current = false;
14906
+ dropTargetIdRef.current = null;
14615
14907
  try {
14616
14908
  ;
14617
14909
  event.currentTarget.setPointerCapture?.(event.pointerId);
@@ -14621,7 +14913,7 @@ var SystemCanvas = (() => {
14621
14913
  window.addEventListener("pointerup", onPointerUp);
14622
14914
  window.addEventListener("pointercancel", onPointerCancel);
14623
14915
  }, [nodesRef, onPointerMove, onPointerUp, onPointerCancel]);
14624
- return { dragOverrides, onPointerDown, isDragging };
14916
+ return { dragOverrides, dropTargetId, onPointerDown, isDragging };
14625
14917
  }
14626
14918
 
14627
14919
  // ../react/dist/hooks/useNodeResize.js
@@ -14896,11 +15188,15 @@ var SystemCanvas = (() => {
14896
15188
  return { x, y, zoom };
14897
15189
  }
14898
15190
  function useZoomNavigation(options) {
14899
- const { enabled, config, nodes, currentCanvas, parentFrame, canvases, onResolveCanvas, onSeedCanvas, theme, getViewportSize, onEnter, onExit } = options;
15191
+ const { enabled, config, nodes, currentCanvas, parentFrame, canvases, onResolveCanvas, onSeedCanvas, theme, getViewportSize, getCursorScreenPos, onEnter, onExit } = options;
14900
15192
  const committingRef = (0, import_react6.useRef)(false);
14901
15193
  (0, import_react6.useEffect)(() => {
14902
15194
  committingRef.current = false;
14903
15195
  }, [currentCanvas]);
15196
+ const exitArmedRef = (0, import_react6.useRef)(false);
15197
+ (0, import_react6.useEffect)(() => {
15198
+ exitArmedRef.current = false;
15199
+ }, [currentCanvas, parentFrame]);
14904
15200
  const prefetchRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
14905
15201
  const prefetch = (0, import_react6.useCallback)((ref) => {
14906
15202
  if (!onResolveCanvas)
@@ -14928,36 +15224,87 @@ var SystemCanvas = (() => {
14928
15224
  const size = getViewportSize();
14929
15225
  if (!size)
14930
15226
  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;
15227
+ const cursor = getCursorScreenPos();
15228
+ const viewportCenterX = size.width / 2;
15229
+ const viewportCenterY = size.height / 2;
15230
+ let cursorNode = null;
15231
+ if (cursor) {
15232
+ for (const n of nodes) {
15233
+ if (!n.ref)
15234
+ continue;
15235
+ const screen = canvasRectToScreenRect({ x: n.x, y: n.y, width: n.width, height: n.height }, vp);
15236
+ if (cursor.x < screen.x || cursor.x > screen.x + screen.width || cursor.y < screen.y || cursor.y > screen.y + screen.height) {
15237
+ continue;
15238
+ }
15239
+ const area = screen.width * screen.height;
15240
+ if (!cursorNode || area < cursorNode.area) {
15241
+ cursorNode = { node: n, screen, area };
15242
+ }
15243
+ }
15244
+ }
15245
+ let bestCandidate = null;
15246
+ if (cursorNode) {
15247
+ const screen = cursorNode.screen;
14940
15248
  const fillFraction = Math.max(screen.width / size.width, screen.height / size.height);
14941
- if (fillFraction >= config.prefetchThreshold && fillFraction < config.enterThreshold) {
14942
- prefetch(n.ref);
15249
+ if (fillFraction >= config.prefetchThreshold && cursorNode.node.ref) {
15250
+ prefetch(cursorNode.node.ref);
14943
15251
  }
14944
15252
  if (fillFraction >= config.enterThreshold) {
14945
- const childData = canvases?.[n.ref] ?? prefetchRef.current.get(n.ref)?.data;
14946
- if (!childData) {
15253
+ bestCandidate = { node: cursorNode.node, screen, distSq: 0 };
15254
+ }
15255
+ for (const n of nodes) {
15256
+ if (!n.ref)
15257
+ continue;
15258
+ if (n === cursorNode.node)
15259
+ continue;
15260
+ const s = canvasRectToScreenRect({ x: n.x, y: n.y, width: n.width, height: n.height }, vp);
15261
+ const ff = Math.max(s.width / size.width, s.height / size.height);
15262
+ if (ff >= config.prefetchThreshold)
14947
15263
  prefetch(n.ref);
15264
+ }
15265
+ } else {
15266
+ for (const n of nodes) {
15267
+ if (!n.ref)
15268
+ continue;
15269
+ const screen = canvasRectToScreenRect({ x: n.x, y: n.y, width: n.width, height: n.height }, vp);
15270
+ const centerX = screen.x + screen.width / 2;
15271
+ const centerY = screen.y + screen.height / 2;
15272
+ const centerOnScreen = centerX >= 0 && centerX <= size.width && centerY >= 0 && centerY <= size.height;
15273
+ if (!centerOnScreen)
14948
15274
  continue;
15275
+ const fillFraction = Math.max(screen.width / size.width, screen.height / size.height);
15276
+ if (fillFraction >= config.prefetchThreshold && fillFraction < config.enterThreshold) {
15277
+ prefetch(n.ref);
15278
+ }
15279
+ if (fillFraction >= config.enterThreshold) {
15280
+ const dx = centerX - viewportCenterX;
15281
+ const dy = centerY - viewportCenterY;
15282
+ const distSq = dx * dx + dy * dy;
15283
+ if (!bestCandidate || distSq < bestCandidate.distSq) {
15284
+ bestCandidate = { node: n, screen, distSq };
15285
+ }
14949
15286
  }
15287
+ }
15288
+ }
15289
+ if (bestCandidate) {
15290
+ const n = bestCandidate.node;
15291
+ const screen = bestCandidate.screen;
15292
+ const ref = n.ref;
15293
+ const childData = canvases?.[ref] ?? prefetchRef.current.get(ref)?.data;
15294
+ if (!childData) {
15295
+ prefetch(ref);
15296
+ } else {
14950
15297
  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;
15298
+ if (resolved.nodes.length > 0) {
15299
+ const bounds = computeBoundingBox(resolved.nodes);
15300
+ const targetRect = expandRect(screen, config.landingScale);
15301
+ const landingPad = Math.min(targetRect.width, targetRect.height) * config.landingPadding;
15302
+ let targetTransform = fitBoundsIntoRect(bounds, targetRect, landingPad);
15303
+ targetTransform = clampTransformToViewport(targetTransform, bounds, size, 16);
15304
+ committingRef.current = true;
15305
+ onEnter(n, targetTransform);
15306
+ return;
15307
+ }
14961
15308
  }
14962
15309
  }
14963
15310
  if (parentFrame && nodes.length > 0) {
@@ -14971,6 +15318,12 @@ var SystemCanvas = (() => {
14971
15318
  height: bounds.height
14972
15319
  }, vp);
14973
15320
  const fillFraction = Math.max(screen.width / size.width, screen.height / size.height);
15321
+ if (!exitArmedRef.current) {
15322
+ if (fillFraction > config.exitThreshold) {
15323
+ exitArmedRef.current = true;
15324
+ }
15325
+ return;
15326
+ }
14974
15327
  if (fillFraction <= config.exitThreshold) {
14975
15328
  const targetTransform = fitBoundsIntoRect({
14976
15329
  minX: parentFrame.parentNodeRect.x,
@@ -14997,6 +15350,7 @@ var SystemCanvas = (() => {
14997
15350
  theme,
14998
15351
  config,
14999
15352
  getViewportSize,
15353
+ getCursorScreenPos,
15000
15354
  prefetch,
15001
15355
  onEnter,
15002
15356
  onExit
@@ -17754,7 +18108,7 @@ var SystemCanvas = (() => {
17754
18108
 
17755
18109
  // ../react/dist/hooks/useViewport.js
17756
18110
  function useViewport(options) {
17757
- const { minZoom, maxZoom, defaultViewport, onViewportChange } = options;
18111
+ const { minZoom, maxZoom, defaultViewport, onViewportChange, marqueeActiveRef } = options;
17758
18112
  const svgRef = (0, import_react7.useRef)(null);
17759
18113
  const groupRef = (0, import_react7.useRef)(null);
17760
18114
  const viewport = (0, import_react7.useRef)(defaultViewport ?? { x: 0, y: 0, zoom: 1 });
@@ -17767,6 +18121,8 @@ var SystemCanvas = (() => {
17767
18121
  if (!svg || !group)
17768
18122
  return;
17769
18123
  const zoomBehavior = zoom_default2().scaleExtent([minZoom, maxZoom]).filter((event) => {
18124
+ if (marqueeActiveRef?.current && event.type !== "wheel")
18125
+ return false;
17770
18126
  if (event.button)
17771
18127
  return false;
17772
18128
  if (event.type === "wheel")
@@ -17872,17 +18228,67 @@ var SystemCanvas = (() => {
17872
18228
 
17873
18229
  // ../react/dist/components/NodeIcon.js
17874
18230
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
17875
- function NodeIcon({ icon, x, y, size = 14, color: color2, opacity = 0.7, customIcons }) {
18231
+ function NodeIcon({ icon, x, y, size = 14, color: color2, opacity = 0.7, customIcons, mode = "stroke", viewBox = 16 }) {
17876
18232
  const pathData = customIcons?.[icon] ?? iconPaths[icon];
17877
18233
  if (!pathData)
17878
18234
  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)) });
18235
+ const defaultStrokeWidth = viewBox * 0.075;
18236
+ return (0, import_jsx_runtime.jsx)("g", { transform: `translate(${x}, ${y})`, pointerEvents: "none", opacity, children: pathData.map((entry, i) => {
18237
+ const spec = typeof entry === "string" ? { d: entry } : entry;
18238
+ const pathMode = spec.mode ?? mode;
18239
+ const rawStrokeWidth = spec.strokeWidth ?? defaultStrokeWidth;
18240
+ const renderedStrokeWidth = rawStrokeWidth * size / viewBox;
18241
+ return (0, import_jsx_runtime.jsx)("path", {
18242
+ d: scalePathData(spec.d, size, viewBox),
18243
+ fill: pathMode === "fill" ? color2 : "none",
18244
+ stroke: pathMode === "stroke" ? color2 : "none",
18245
+ strokeWidth: pathMode === "stroke" ? renderedStrokeWidth : 0,
18246
+ strokeLinecap: pathMode === "stroke" ? "round" : void 0,
18247
+ strokeLinejoin: pathMode === "stroke" ? "round" : void 0,
18248
+ // Brand silhouettes are typically authored as a single path
18249
+ // whose holes are expressed with subpath winding. `evenodd`
18250
+ // is the safe choice; nonzero would fill some holes solid.
18251
+ fillRule: pathMode === "fill" ? "evenodd" : void 0
18252
+ }, i);
18253
+ }) });
17880
18254
  }
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
- });
18255
+ function scalePathData(d, size, source) {
18256
+ const scale = size / source;
18257
+ const out = [];
18258
+ const numberAtStart = /^-?(?:\d+\.?\d*|\.\d+)/;
18259
+ let isArc = false;
18260
+ let arcIndex = 0;
18261
+ let i = 0;
18262
+ while (i < d.length) {
18263
+ const ch = d[i];
18264
+ if (ch === " " || ch === "," || ch === " " || ch === "\n" || ch === "\r") {
18265
+ i++;
18266
+ continue;
18267
+ }
18268
+ if (/[a-zA-Z]/.test(ch)) {
18269
+ out.push(ch);
18270
+ isArc = ch === "a" || ch === "A";
18271
+ arcIndex = 0;
18272
+ i++;
18273
+ continue;
18274
+ }
18275
+ if (isArc && (arcIndex === 3 || arcIndex === 4)) {
18276
+ out.push(ch === "0" ? "0" : "1");
18277
+ arcIndex = (arcIndex + 1) % 7;
18278
+ i++;
18279
+ continue;
18280
+ }
18281
+ const m = numberAtStart.exec(d.substring(i));
18282
+ if (!m) {
18283
+ i++;
18284
+ continue;
18285
+ }
18286
+ out.push(String(parseFloat(m[0]) * scale));
18287
+ if (isArc)
18288
+ arcIndex = (arcIndex + 1) % 7;
18289
+ i += m[0].length;
18290
+ }
18291
+ return out.join(" ");
17886
18292
  }
17887
18293
  var iconPaths = {
17888
18294
  // Database: cylinder shape
@@ -17975,11 +18381,12 @@ var SystemCanvas = (() => {
17975
18381
  // ../react/dist/components/RefIndicator.js
17976
18382
  var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1);
17977
18383
  var import_react8 = __toESM(require_react(), 1);
17978
- function RefIndicator({ node, theme, nodeX, nodeY, nodeWidth, nodeHeight, strokeColor, strokeWidth, corner = "bottom-right", size = 18, onNavigate }) {
18384
+ function RefIndicator({ node, theme, nodeX, nodeY, nodeWidth, nodeHeight, strokeColor, strokeWidth, corner = "bottom-right", size: sizeProp, onNavigate }) {
17979
18385
  const [hover, setHover] = (0, import_react8.useState)(false);
17980
18386
  const iconKind = theme.node.refIndicator.icon;
17981
18387
  if (iconKind === "none")
17982
18388
  return null;
18389
+ const size = sizeProp ?? theme.node.refIndicator.size ?? 18;
17983
18390
  const stopAll = (e) => e.stopPropagation();
17984
18391
  const right = nodeX + nodeWidth;
17985
18392
  const bottom = nodeY + nodeHeight;
@@ -18025,19 +18432,22 @@ var SystemCanvas = (() => {
18025
18432
  return (0, import_jsx_runtime2.jsxs)("g", { className: "system-canvas-ref-indicator", style: { cursor: "pointer" }, onClick: (e) => {
18026
18433
  e.stopPropagation();
18027
18434
  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 })] });
18435
+ }, 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
18436
  }
18030
- function Glyph({ kind, cx, cy, color: color2 }) {
18031
- const s = 3;
18437
+ function Glyph({ kind, cx, cy, color: color2, size }) {
18438
+ const s = size / 6;
18439
+ const sw = Math.max(1, size / 9);
18032
18440
  switch (kind) {
18033
18441
  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" });
18442
+ 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
18443
  case "arrow": {
18036
18444
  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" });
18445
+ 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" });
18446
+ }
18447
+ case "expand": {
18448
+ const sw2 = Math.max(1, size / 12);
18449
+ 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
18450
  }
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
18451
  }
18042
18452
  }
18043
18453
 
@@ -18164,7 +18574,7 @@ var SystemCanvas = (() => {
18164
18574
  // ../react/dist/primitives/NodeText.js
18165
18575
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
18166
18576
  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 }) {
18577
+ 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
18578
  const reactId = (0, import_react9.useId)();
18169
18579
  const safeId = reactId.replace(/:/g, "");
18170
18580
  if (!value)
@@ -18178,13 +18588,16 @@ var SystemCanvas = (() => {
18178
18588
  const fillAttr = gradId ? `url(#${gradId})` : color2 ?? theme.node.sublabelColor;
18179
18589
  const displayValue = uppercase ? value.toUpperCase() : value;
18180
18590
  if (!wrap) {
18591
+ const rendered = truncateToWidth(displayValue, region.width, fontSize);
18181
18592
  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 })] });
18593
+ 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
18594
  }
18184
18595
  const lines = wrapTextWithBreaks(displayValue, region.width, fontSize, maxLines);
18185
18596
  if (lines.length === 0)
18186
18597
  return null;
18187
- const baseY = region.y + fontSize;
18598
+ const blockHeight = fontSize + (lines.length - 1) * lineHeight;
18599
+ const topY = region.y + fontSize;
18600
+ const baseY = verticalAlign === "center" ? region.y + (region.height - blockHeight) / 2 + fontSize : verticalAlign === "bottom" ? region.y + region.height - blockHeight + fontSize : topY;
18188
18601
  const clipId = `sc-text-clip-${safeId}`;
18189
18602
  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)) }) })] });
18190
18603
  }
@@ -18336,6 +18749,20 @@ var SystemCanvas = (() => {
18336
18749
  const color2 = resolveAccessorOr(spec.color, nodeColor, ctx);
18337
18750
  return (0, import_jsx_runtime9.jsx)(NodeDot, { region, color: color2 });
18338
18751
  }
18752
+ case "icon": {
18753
+ const name = resolveAccessor(spec.name, ctx);
18754
+ if (!name)
18755
+ return null;
18756
+ const color2 = resolveAccessorOr(spec.color, nodeColor, ctx);
18757
+ const opacity = resolveAccessorOr(spec.opacity, 1, ctx);
18758
+ const mode = resolveAccessorOr(spec.mode, "stroke", ctx);
18759
+ const viewBox = resolveAccessorOr(spec.viewBox, 16, ctx);
18760
+ const auto = Math.max(8, Math.min(region.width, region.height) - 2);
18761
+ const size = resolveAccessorOr(spec.size, auto, ctx);
18762
+ const x = region.x + (region.width - size) / 2;
18763
+ const y = region.y + (region.height - size) / 2;
18764
+ return (0, import_jsx_runtime9.jsx)(NodeIcon, { icon: name, x, y, size, color: color2, opacity, customIcons: theme.icons, mode, viewBox });
18765
+ }
18339
18766
  case "custom": {
18340
18767
  return spec.render(ctx);
18341
18768
  }
@@ -18363,20 +18790,21 @@ var SystemCanvas = (() => {
18363
18790
  const contentY = y + reservedTop;
18364
18791
  const contentWidth = Math.max(0, width - reservedLeft - reservedRight);
18365
18792
  const contentHeight = Math.max(0, height - reservedTop - reservedBottom);
18366
- const text = node.text ?? "";
18367
- const lines = text.split("\n").filter(Boolean);
18368
- const mainLabel = lines[0] ?? node.id;
18369
- const sublabel = lines[1];
18793
+ const LABEL_PAD_X = 10;
18794
+ const LABEL_PAD_Y = 6;
18795
+ const text = node.text ?? node.id;
18370
18796
  const hasBodySlot = slots?.body !== void 0;
18371
18797
  const hasHeader = reservedTop > 0;
18372
- const labelFont = theme.node.labelFont ?? theme.node.fontFamily;
18798
+ const hasInlineLeftMarker = slots?.topLeft !== void 0 && (slots.topLeft.kind === "dot" || slots.topLeft.kind === "icon") && !hasHeader;
18799
+ const isLeftAligned = hasHeader || hasInlineLeftMarker;
18373
18800
  const labelFontSize = theme.node.fontSize + (hasHeader ? 1 : 0);
18374
- const lineHeight = labelFontSize + 4;
18375
- const totalTextHeight = sublabel ? lineHeight + theme.node.sublabelFontSize + 4 : lineHeight;
18376
- const labelAnchor = hasHeader ? "start" : "middle";
18377
- const labelX = hasHeader ? contentX : contentX + contentWidth / 2;
18378
- const textStartY = hasHeader ? contentY + labelFontSize + 2 : contentY + (contentHeight - totalTextHeight) / 2 + labelFontSize;
18379
- 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 })] });
18801
+ const labelAlign = isLeftAligned ? "start" : "center";
18802
+ 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: {
18803
+ x: contentX + LABEL_PAD_X,
18804
+ y: contentY + LABEL_PAD_Y,
18805
+ width: Math.max(0, contentWidth - LABEL_PAD_X * 2),
18806
+ height: Math.max(0, contentHeight - LABEL_PAD_Y * 2)
18807
+ }, 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 })] });
18380
18808
  }
18381
18809
 
18382
18810
  // ../react/dist/components/FileNode.js
@@ -18390,6 +18818,7 @@ var SystemCanvas = (() => {
18390
18818
  const fold = 10;
18391
18819
  const textPadding = 10 + reservedLeft;
18392
18820
  const maxTextWidth = width - textPadding - reservedRight - fold;
18821
+ const LABEL_PAD_Y = 6;
18393
18822
  const contentY = y + reservedTop;
18394
18823
  const contentHeight = Math.max(0, height - reservedTop - reservedBottom);
18395
18824
  const shapePath = [
@@ -18408,7 +18837,12 @@ var SystemCanvas = (() => {
18408
18837
  const strokeColor = node.resolvedStroke;
18409
18838
  const thinStroke = 0.75;
18410
18839
  const clipId = `file-clip-${node.id}`;
18411
- 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 })] });
18840
+ 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: {
18841
+ x: x + textPadding,
18842
+ y: dirPath ? contentY + 18 : contentY + (subpath ? 8 : LABEL_PAD_Y),
18843
+ width: maxTextWidth,
18844
+ height: dirPath ? Math.max(0, contentHeight - 18 - (subpath ? 16 : 0)) : Math.max(0, contentHeight - (subpath ? 16 : 0) - LABEL_PAD_Y * 2)
18845
+ }, 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 })] });
18412
18846
  }
18413
18847
 
18414
18848
  // ../react/dist/components/LinkNode.js
@@ -18419,14 +18853,21 @@ var SystemCanvas = (() => {
18419
18853
  const contentY = y + reservedTop;
18420
18854
  const contentWidth = Math.max(0, width - reservedLeft - reservedRight);
18421
18855
  const contentHeight = Math.max(0, height - reservedTop - reservedBottom);
18422
- const cx = contentX + contentWidth / 2;
18856
+ const glyphReserve = 20;
18857
+ const LABEL_PAD_X = 10;
18858
+ const LABEL_PAD_Y = 6;
18423
18859
  let displayUrl = node.url ?? "";
18424
18860
  try {
18425
18861
  const url = new URL(displayUrl);
18426
18862
  displayUrl = url.hostname;
18427
18863
  } catch {
18428
18864
  }
18429
- 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 })] });
18865
+ 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: {
18866
+ x: contentX + glyphReserve,
18867
+ y: contentY + LABEL_PAD_Y,
18868
+ width: Math.max(0, contentWidth - glyphReserve - LABEL_PAD_X),
18869
+ height: Math.max(0, contentHeight - LABEL_PAD_Y * 2)
18870
+ }, 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 })] });
18430
18871
  }
18431
18872
 
18432
18873
  // ../react/dist/components/GroupNode.js
@@ -18552,7 +18993,8 @@ var SystemCanvas = (() => {
18552
18993
  const isEditing = editingId === edge.id;
18553
18994
  const baseColor = edge.color ? resolveColor(edge.color, theme).stroke : theme.edge.stroke;
18554
18995
  const edgeColor = isSelected ? theme.node.labelColor : baseColor;
18555
- const strokeWidth = isSelected ? theme.edge.strokeWidth * 1.75 : theme.edge.strokeWidth;
18996
+ const baseStrokeWidth = edge.strokeWidth ?? theme.edge.strokeWidth;
18997
+ const strokeWidth = isSelected ? baseStrokeWidth * 1.75 : baseStrokeWidth;
18556
18998
  const toEnd = edge.toEnd ?? "arrow";
18557
18999
  const fromEnd = edge.fromEnd ?? "none";
18558
19000
  const arrowId = "system-canvas-arrowhead";
@@ -18615,6 +19057,10 @@ var SystemCanvas = (() => {
18615
19057
  const fontFamily = theme.node.fontFamily;
18616
19058
  const fontSize = theme.node.fontSize;
18617
19059
  const padding = 8;
19060
+ const slots = getCategorySlots(node, theme);
19061
+ const reservations = computeReflowReservations(node, theme, slots);
19062
+ const hasHeader = reservations.top > 0;
19063
+ const textAlign = node.type === "text" && !hasHeader ? "center" : "left";
18618
19064
  const commonFieldStyle = {
18619
19065
  width: "100%",
18620
19066
  height: "100%",
@@ -18628,7 +19074,7 @@ var SystemCanvas = (() => {
18628
19074
  borderRadius: node.resolvedCornerRadius,
18629
19075
  outline: "none",
18630
19076
  resize: "none",
18631
- textAlign: node.type === "text" ? "center" : "left"
19077
+ textAlign
18632
19078
  };
18633
19079
  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) => {
18634
19080
  if (e.key === "Enter" && !e.shiftKey) {
@@ -18998,7 +19444,7 @@ var SystemCanvas = (() => {
18998
19444
  // ../react/dist/components/Viewport.js
18999
19445
  var HOVER_PADDING = 10;
19000
19446
  var EDGE_PROXIMITY = 16;
19001
- 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) {
19447
+ 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) {
19002
19448
  const { svgRef, groupRef, viewport, fitToContent, zoomToNode, setTransform } = useViewport({
19003
19449
  minZoom,
19004
19450
  maxZoom,
@@ -19041,6 +19487,7 @@ var SystemCanvas = (() => {
19041
19487
  }, []);
19042
19488
  const [hoveredNodeId, setHoveredNodeId] = (0, import_react15.useState)(null);
19043
19489
  const [hoveredSide, setHoveredSide] = (0, import_react15.useState)(null);
19490
+ const cursorPosRef = (0, import_react15.useRef)(null);
19044
19491
  (0, import_react15.useImperativeHandle)(ref, () => ({
19045
19492
  zoomToNode: (node, onComplete, options) => {
19046
19493
  navigatingRef.current = true;
@@ -19049,7 +19496,8 @@ var SystemCanvas = (() => {
19049
19496
  fitToContent,
19050
19497
  setTransform,
19051
19498
  getSvgElement: () => svgRef.current,
19052
- getViewport: () => viewport.current ?? { x: 0, y: 0, zoom: 1 }
19499
+ getViewport: () => viewport.current ?? { x: 0, y: 0, zoom: 1 },
19500
+ getCursorScreenPos: () => cursorPosRef.current
19053
19501
  }));
19054
19502
  const renderNodes = (0, import_react15.useMemo)(() => {
19055
19503
  const hasDrag = dragOverrides && dragOverrides.size > 0;
@@ -19132,14 +19580,17 @@ var SystemCanvas = (() => {
19132
19580
  ]);
19133
19581
  const editingNode = editingId ? renderNodes.find((n) => n.id === editingId) ?? null : null;
19134
19582
  const handleSvgPointerMove = (0, import_react15.useCallback)((event) => {
19135
- if (!edgeCreateEnabled)
19136
- return;
19137
19583
  const svg = svgRef.current;
19138
19584
  if (!svg)
19139
19585
  return;
19140
19586
  const rect = svg.getBoundingClientRect();
19587
+ const cursorScreenX = event.clientX - rect.left;
19588
+ const cursorScreenY = event.clientY - rect.top;
19589
+ cursorPosRef.current = { x: cursorScreenX, y: cursorScreenY };
19590
+ if (!edgeCreateEnabled)
19591
+ return;
19141
19592
  const vp = viewport.current ?? { x: 0, y: 0, zoom: 1 };
19142
- const { x, y } = screenToCanvas(event.clientX - rect.left, event.clientY - rect.top, vp);
19593
+ const { x, y } = screenToCanvas(cursorScreenX, cursorScreenY, vp);
19143
19594
  const pad = HOVER_PADDING;
19144
19595
  let hit = null;
19145
19596
  for (let i = renderNodes.length - 1; i >= 0; i--) {
@@ -19183,11 +19634,13 @@ var SystemCanvas = (() => {
19183
19634
  const handleSvgPointerLeave = (0, import_react15.useCallback)(() => {
19184
19635
  setHoveredNodeId(null);
19185
19636
  setHoveredSide(null);
19637
+ cursorPosRef.current = null;
19186
19638
  }, []);
19187
19639
  const handlesNodeId = pendingEdge?.sourceId ?? hoveredNodeId;
19188
19640
  const handlesNode = edgeCreateEnabled && handlesNodeId ? renderNodeMap.get(handlesNodeId) ?? null : null;
19189
19641
  const pendingSourceNode = pendingEdge ? renderNodeMap.get(pendingEdge.sourceId) ?? null : null;
19190
19642
  const pendingTargetNode = pendingEdge?.hoveredTargetId && pendingEdge.hoveredTargetId !== pendingEdge.sourceId ? renderNodeMap.get(pendingEdge.hoveredTargetId) ?? null : null;
19643
+ const dropTargetNode = dropTargetId ? renderNodeMap.get(dropTargetId) ?? null : null;
19191
19644
  const editingEdge = editingEdgeId ? edges.find((e) => e.id === editingEdgeId) ?? null : null;
19192
19645
  const editingEdgeMidpoint = (() => {
19193
19646
  if (!editingEdge)
@@ -19207,7 +19660,7 @@ var SystemCanvas = (() => {
19207
19660
  WebkitUserSelect: "none",
19208
19661
  MozUserSelect: "none",
19209
19662
  msUserSelect: "none"
19210
- }, 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 })] })] });
19663
+ }, 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 })] })] });
19211
19664
  });
19212
19665
 
19213
19666
  // ../react/dist/components/Breadcrumbs.js
@@ -19436,7 +19889,7 @@ var SystemCanvas = (() => {
19436
19889
  const cx = visibleLeft + visibleW / 2;
19437
19890
  if (endScreen <= colsOffsetLeft || startScreen >= width)
19438
19891
  return null;
19439
- 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}`);
19892
+ 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}`);
19440
19893
  }), 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) => {
19441
19894
  const startScreen = canvasToScreen(0, row.start, viewport).y;
19442
19895
  const endScreen = canvasToScreen(0, row.start + row.size, viewport).y;
@@ -19448,10 +19901,10 @@ var SystemCanvas = (() => {
19448
19901
  const cy = visibleTop + visibleH / 2;
19449
19902
  if (endScreen <= rowsOffsetTop || startScreen >= height)
19450
19903
  return null;
19451
- 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}`);
19904
+ 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}`);
19452
19905
  }), 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 })] });
19453
19906
  }
19454
- function truncateToWidth(label, availablePx, fontSize) {
19907
+ function truncateToWidth2(label, availablePx, fontSize) {
19455
19908
  if (availablePx <= 0)
19456
19909
  return "";
19457
19910
  const charPx = fontSize * 0.6;
@@ -19493,8 +19946,10 @@ var SystemCanvas = (() => {
19493
19946
  raf = requestAnimationFrame(tick);
19494
19947
  return () => cancelAnimationFrame(raf);
19495
19948
  }, [getViewport]);
19496
- const topCenter = canvasToScreen(node.x + node.width / 2, node.y, viewport);
19497
- const bottomCenter = canvasToScreen(node.x + node.width / 2, node.y + node.height, viewport);
19949
+ const align = theme.toolbarAlign ?? "center";
19950
+ const anchorCanvasX = align === "left" ? node.x : align === "right" ? node.x + node.width : node.x + node.width / 2;
19951
+ const topAnchor = canvasToScreen(anchorCanvasX, node.y, viewport);
19952
+ const bottomAnchor = canvasToScreen(anchorCanvasX, node.y + node.height, viewport);
19498
19953
  const toolbarRef = (0, import_react19.useRef)(null);
19499
19954
  const [size, setSize] = (0, import_react19.useState)({ width: 0, height: 0 });
19500
19955
  (0, import_react19.useEffect)(() => {
@@ -19503,17 +19958,17 @@ var SystemCanvas = (() => {
19503
19958
  return;
19504
19959
  const update = () => {
19505
19960
  const r = el.getBoundingClientRect();
19506
- setSize({ width: r.width, height: r.height });
19961
+ setSize((prev) => prev.width === r.width && prev.height === r.height ? prev : { width: r.width, height: r.height });
19507
19962
  };
19508
19963
  update();
19509
19964
  const ro = new ResizeObserver(update);
19510
19965
  ro.observe(el);
19511
19966
  return () => ro.disconnect();
19512
- });
19513
- let left = topCenter.x - size.width / 2;
19514
- let top = topCenter.y - size.height - NODE_GAP;
19967
+ }, []);
19968
+ let left = align === "left" ? topAnchor.x : align === "right" ? topAnchor.x - size.width : topAnchor.x - size.width / 2;
19969
+ let top = topAnchor.y - size.height - NODE_GAP;
19515
19970
  if (top < FLIP_MARGIN) {
19516
- top = bottomCenter.y + NODE_GAP;
19971
+ top = bottomAnchor.y + NODE_GAP;
19517
19972
  }
19518
19973
  left = Math.max(FLIP_MARGIN, Math.min(left, containerWidth - size.width - FLIP_MARGIN));
19519
19974
  const patch = (update) => onPatch(update);
@@ -19552,7 +20007,7 @@ var SystemCanvas = (() => {
19552
20007
  }
19553
20008
  function DefaultToolbarContent({ node, theme, onPatch, onDelete }) {
19554
20009
  const groups = (0, import_react19.useMemo)(() => getNodeActionsForNode(node, theme), [node, theme]);
19555
- const showDelete = !theme.hideToolbarDelete;
20010
+ const showDelete = theme.showToolbarDelete === true;
19556
20011
  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 })] })] });
19557
20012
  }
19558
20013
  function Divider2({ theme }) {
@@ -19707,16 +20162,134 @@ var SystemCanvas = (() => {
19707
20162
  }, 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" }) }) });
19708
20163
  }
19709
20164
 
20165
+ // ../react/dist/components/NodeContextMenuOverlay.js
20166
+ var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
20167
+ var import_react20 = __toESM(require_react(), 1);
20168
+ var ESTIMATED_MENU_WIDTH = 200;
20169
+ var MIN_MENU_WIDTH = 160;
20170
+ var VIEWPORT_MARGIN = 8;
20171
+ function NodeContextMenuOverlay({ state, config, theme, onClose }) {
20172
+ const rootRef = (0, import_react20.useRef)(null);
20173
+ const [hoveredId, setHoveredId] = (0, import_react20.useState)(null);
20174
+ (0, import_react20.useEffect)(() => {
20175
+ if (state)
20176
+ setHoveredId(null);
20177
+ }, [state]);
20178
+ (0, import_react20.useEffect)(() => {
20179
+ if (!state)
20180
+ return;
20181
+ function onDown(e) {
20182
+ const root2 = rootRef.current;
20183
+ if (!root2)
20184
+ return;
20185
+ if (root2.contains(e.target))
20186
+ return;
20187
+ onClose();
20188
+ }
20189
+ function onKey(e) {
20190
+ if (e.key === "Escape") {
20191
+ e.stopPropagation();
20192
+ onClose();
20193
+ }
20194
+ }
20195
+ function onScroll() {
20196
+ onClose();
20197
+ }
20198
+ window.addEventListener("mousedown", onDown);
20199
+ window.addEventListener("keydown", onKey);
20200
+ window.addEventListener("scroll", onScroll, true);
20201
+ window.addEventListener("blur", onClose);
20202
+ return () => {
20203
+ window.removeEventListener("mousedown", onDown);
20204
+ window.removeEventListener("keydown", onKey);
20205
+ window.removeEventListener("scroll", onScroll, true);
20206
+ window.removeEventListener("blur", onClose);
20207
+ };
20208
+ }, [state, onClose]);
20209
+ if (!state)
20210
+ return null;
20211
+ const cm = theme.contextMenu;
20212
+ if (!cm)
20213
+ return null;
20214
+ const vw = typeof window !== "undefined" ? window.innerWidth : 0;
20215
+ const vh = typeof window !== "undefined" ? window.innerHeight : 0;
20216
+ const itemHeight = cm.itemPaddingY * 2 + cm.fontSize + 4;
20217
+ const estimatedHeight = state.items.length * itemHeight + cm.paddingY * 2;
20218
+ const left = vw ? Math.min(state.screenPosition.x, vw - ESTIMATED_MENU_WIDTH - VIEWPORT_MARGIN) : state.screenPosition.x;
20219
+ const top = vh ? Math.min(state.screenPosition.y, vh - estimatedHeight - VIEWPORT_MARGIN) : state.screenPosition.y;
20220
+ const matchCtx = { canvasRef: state.canvasRef };
20221
+ const anyIcon = state.items.some((item) => !!item.icon);
20222
+ return (0, import_jsx_runtime27.jsx)("div", {
20223
+ ref: rootRef,
20224
+ role: "menu",
20225
+ // Stop right-clicks inside the menu from bubbling into the document
20226
+ // listener and re-opening the canvas-level menu.
20227
+ onContextMenu: (e) => {
20228
+ e.preventDefault();
20229
+ e.stopPropagation();
20230
+ },
20231
+ style: {
20232
+ position: "fixed",
20233
+ left,
20234
+ top,
20235
+ zIndex: 1e3,
20236
+ minWidth: MIN_MENU_WIDTH,
20237
+ padding: `${cm.paddingY}px ${cm.paddingX}px`,
20238
+ background: cm.background,
20239
+ color: cm.itemColor,
20240
+ border: `1px solid ${cm.borderColor}`,
20241
+ borderRadius: cm.borderRadius,
20242
+ boxShadow: cm.shadow,
20243
+ fontFamily: cm.fontFamily,
20244
+ fontSize: cm.fontSize,
20245
+ backdropFilter: "blur(10px)",
20246
+ userSelect: "none",
20247
+ // `pointer-events: auto` so the menu still receives clicks even
20248
+ // when a parent canvas overlay disables them.
20249
+ pointerEvents: "auto"
20250
+ },
20251
+ children: state.items.map((item) => {
20252
+ const isDisabled = item.disabled?.(state.node, matchCtx) ?? false;
20253
+ const isHovered = !isDisabled && hoveredId === item.id;
20254
+ const color2 = item.destructive ? cm.destructiveItemColor : cm.itemColor;
20255
+ 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: () => {
20256
+ if (isDisabled)
20257
+ return;
20258
+ config.onSelect(item.id, state.node, {
20259
+ canvasRef: state.canvasRef,
20260
+ screenPosition: state.screenPosition
20261
+ });
20262
+ onClose();
20263
+ }, style: {
20264
+ display: "flex",
20265
+ alignItems: "center",
20266
+ gap: 10,
20267
+ padding: `${cm.itemPaddingY}px ${cm.itemPaddingX}px`,
20268
+ borderRadius: Math.max(0, cm.borderRadius - 4),
20269
+ cursor: isDisabled ? "not-allowed" : "pointer",
20270
+ opacity: isDisabled ? 0.45 : 1,
20271
+ background: isHovered ? cm.itemHoverBackground : "transparent",
20272
+ color: color2
20273
+ }, 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 ? (
20274
+ // Only reserve a spacer when other items in the same menu
20275
+ // do have icons — keeps labels vertically aligned in a
20276
+ // mixed menu without padding lone-item menus.
20277
+ (0, import_jsx_runtime27.jsx)("span", { style: { width: 14, flexShrink: 0 }, "aria-hidden": true })
20278
+ ) : null, (0, import_jsx_runtime27.jsx)("span", { children: item.label })] }, item.id);
20279
+ })
20280
+ });
20281
+ }
20282
+
19710
20283
  // ../react/dist/components/SystemCanvas.js
19711
20284
  var CASCADE_WINDOW_MS = 1500;
19712
20285
  var CASCADE_OFFSET = 20;
19713
- 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) {
19714
- const zoomNavConfig = (0, import_react20.useMemo)(() => {
20286
+ 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) {
20287
+ const zoomNavConfig = (0, import_react21.useMemo)(() => {
19715
20288
  const defaults = {
19716
20289
  enterThreshold: 0.66,
19717
20290
  exitThreshold: 0.33,
19718
20291
  prefetchThreshold: 0.4,
19719
- landingScale: 1.2,
20292
+ landingScale: 1.3,
19720
20293
  landingPadding: 0.08,
19721
20294
  fadeDuration: 216
19722
20295
  };
@@ -19736,16 +20309,16 @@ var SystemCanvas = (() => {
19736
20309
  }, [zoomNavigation]);
19737
20310
  const effectiveMaxZoom = maxZoom ?? (zoomNavConfig.enabled ? 16 : 4);
19738
20311
  const effectiveMinZoom = minZoomProp ?? (zoomNavConfig.enabled ? 0.01 : 0.1);
19739
- (0, import_react20.useEffect)(() => {
20312
+ (0, import_react21.useEffect)(() => {
19740
20313
  const env = globalThis.process?.env?.NODE_ENV;
19741
20314
  if (editable && !canvases && env !== "production") {
19742
20315
  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.");
19743
20316
  }
19744
20317
  }, [editable, canvases]);
19745
- const [parentFrames, setParentFrames] = (0, import_react20.useState)([]);
19746
- const [pendingHandoff, setPendingHandoff] = (0, import_react20.useState)(null);
19747
- const suppressNextHandoffClearRef = (0, import_react20.useRef)(false);
19748
- const handleBreadcrumbClick = (0, import_react20.useCallback)((index) => {
20318
+ const [parentFrames, setParentFrames] = (0, import_react21.useState)([]);
20319
+ const [pendingHandoff, setPendingHandoff] = (0, import_react21.useState)(null);
20320
+ const suppressNextHandoffClearRef = (0, import_react21.useRef)(false);
20321
+ const handleBreadcrumbClick = (0, import_react21.useCallback)((index) => {
19749
20322
  setParentFrames((prev) => prev.slice(0, index));
19750
20323
  if (suppressNextHandoffClearRef.current) {
19751
20324
  suppressNextHandoffClearRef.current = false;
@@ -19762,7 +20335,10 @@ var SystemCanvas = (() => {
19762
20335
  onNavigate,
19763
20336
  onBreadcrumbClick: handleBreadcrumbClick
19764
20337
  });
19765
- const theme = (0, import_react20.useMemo)(() => {
20338
+ (0, import_react21.useEffect)(() => {
20339
+ onBreadcrumbsChange?.(breadcrumbs);
20340
+ }, [breadcrumbs, onBreadcrumbsChange]);
20341
+ const theme = (0, import_react21.useMemo)(() => {
19766
20342
  const registry = { ...themes, ...customThemes };
19767
20343
  const resolveByName = (name) => name && registry[name] ? registry[name] : null;
19768
20344
  if (themeProp) {
@@ -19773,22 +20349,22 @@ var SystemCanvas = (() => {
19773
20349
  }
19774
20350
  return resolveByName(currentCanvas.theme?.base) ?? resolveByName(canvas.theme?.base) ?? darkTheme;
19775
20351
  }, [themeProp, customThemes, currentCanvas.theme?.base, canvas.theme?.base]);
19776
- const { nodes, edges, nodeMap } = (0, import_react20.useMemo)(() => {
20352
+ const { nodes, edges, nodeMap } = (0, import_react21.useMemo)(() => {
19777
20353
  const resolved = resolveCanvas(currentCanvas, theme);
19778
20354
  const map = buildNodeMap(resolved.nodes);
19779
20355
  return { nodes: resolved.nodes, edges: resolved.edges, nodeMap: map };
19780
20356
  }, [currentCanvas, theme]);
19781
- const nodesRef = (0, import_react20.useRef)(nodes);
20357
+ const nodesRef = (0, import_react21.useRef)(nodes);
19782
20358
  nodesRef.current = nodes;
19783
- const viewportStateRef = (0, import_react20.useRef)(defaultViewport ?? { x: 0, y: 0, zoom: 1 });
19784
- const viewportHandleRef = (0, import_react20.useRef)(null);
19785
- const navigateToRefRef = (0, import_react20.useRef)(navigateToRef);
20359
+ const viewportStateRef = (0, import_react21.useRef)(defaultViewport ?? { x: 0, y: 0, zoom: 1 });
20360
+ const viewportHandleRef = (0, import_react21.useRef)(null);
20361
+ const navigateToRefRef = (0, import_react21.useRef)(navigateToRef);
19786
20362
  navigateToRefRef.current = navigateToRef;
19787
- const navigateToBreadcrumbRef = (0, import_react20.useRef)(navigateToBreadcrumb);
20363
+ const navigateToBreadcrumbRef = (0, import_react21.useRef)(navigateToBreadcrumb);
19788
20364
  navigateToBreadcrumbRef.current = navigateToBreadcrumb;
19789
- const breadcrumbsRef = (0, import_react20.useRef)(breadcrumbs);
20365
+ const breadcrumbsRef = (0, import_react21.useRef)(breadcrumbs);
19790
20366
  breadcrumbsRef.current = breadcrumbs;
19791
- (0, import_react20.useImperativeHandle)(forwardedRef, () => ({
20367
+ (0, import_react21.useImperativeHandle)(forwardedRef, () => ({
19792
20368
  zoomIntoNode: (nodeId, options) => {
19793
20369
  return new Promise((resolve) => {
19794
20370
  const node = nodesRef.current.find((n) => n.id === nodeId);
@@ -19820,19 +20396,51 @@ var SystemCanvas = (() => {
19820
20396
  navigateToBreadcrumbRef.current(0);
19821
20397
  }
19822
20398
  }), [forwardedRef]);
19823
- const [selectedId, setSelectedId] = (0, import_react20.useState)(null);
19824
- const [editingId, setEditingId] = (0, import_react20.useState)(null);
19825
- const [selectedEdgeId, setSelectedEdgeId] = (0, import_react20.useState)(null);
19826
- const [editingEdgeId, setEditingEdgeId] = (0, import_react20.useState)(null);
19827
- (0, import_react20.useEffect)(() => {
20399
+ const [selectedId, setSelectedId] = (0, import_react21.useState)(null);
20400
+ const [editingId, setEditingId] = (0, import_react21.useState)(null);
20401
+ const [selectedEdgeId, setSelectedEdgeId] = (0, import_react21.useState)(null);
20402
+ const [editingEdgeId, setEditingEdgeId] = (0, import_react21.useState)(null);
20403
+ (0, import_react21.useEffect)(() => {
19828
20404
  setSelectedId(null);
19829
20405
  setEditingId(null);
19830
20406
  setSelectedEdgeId(null);
19831
20407
  setEditingEdgeId(null);
19832
20408
  }, [currentCanvasRef]);
19833
- const containerRef = (0, import_react20.useRef)(null);
19834
- const [containerSize, setContainerSize] = (0, import_react20.useState)({ width: 0, height: 0 });
19835
- (0, import_react20.useEffect)(() => {
20409
+ const onSelectionChangeRef = (0, import_react21.useRef)(onSelectionChange);
20410
+ (0, import_react21.useEffect)(() => {
20411
+ onSelectionChangeRef.current = onSelectionChange;
20412
+ }, [onSelectionChange]);
20413
+ const lastEmittedSelectionRef = (0, import_react21.useRef)({ kind: null, id: null, canvasRef: void 0 });
20414
+ (0, import_react21.useEffect)(() => {
20415
+ const cb = onSelectionChangeRef.current;
20416
+ let next = null;
20417
+ if (selectedId) {
20418
+ const node = nodeMap.get(selectedId);
20419
+ if (node)
20420
+ next = { kind: "node", node, canvasRef: currentCanvasRef };
20421
+ }
20422
+ if (!next && selectedEdgeId) {
20423
+ const edge = edges.find((e) => e.id === selectedEdgeId);
20424
+ if (edge)
20425
+ next = { kind: "edge", edge, canvasRef: currentCanvasRef };
20426
+ }
20427
+ const last = lastEmittedSelectionRef.current;
20428
+ const nextKind = next?.kind ?? null;
20429
+ const nextId = next ? next.kind === "node" ? next.node.id : next.edge.id : null;
20430
+ const nextRef = next?.canvasRef;
20431
+ if (last.kind === nextKind && last.id === nextId && last.canvasRef === nextRef) {
20432
+ return;
20433
+ }
20434
+ lastEmittedSelectionRef.current = {
20435
+ kind: nextKind,
20436
+ id: nextId,
20437
+ canvasRef: nextRef
20438
+ };
20439
+ cb?.(next);
20440
+ }, [selectedId, selectedEdgeId, currentCanvasRef, nodeMap, edges]);
20441
+ const containerRef = (0, import_react21.useRef)(null);
20442
+ const [containerSize, setContainerSize] = (0, import_react21.useState)({ width: 0, height: 0 });
20443
+ (0, import_react21.useEffect)(() => {
19836
20444
  const el = containerRef.current;
19837
20445
  if (!el)
19838
20446
  return;
@@ -19847,42 +20455,67 @@ var SystemCanvas = (() => {
19847
20455
  }, []);
19848
20456
  const hasLanes = currentCanvas.columns && currentCanvas.columns.length > 0 || currentCanvas.rows && currentCanvas.rows.length > 0;
19849
20457
  const showLaneHeaders = hasLanes && laneHeaders !== "none";
19850
- const getViewportState = (0, import_react20.useCallback)(() => viewportStateRef.current ?? { x: 0, y: 0, zoom: 1 }, []);
19851
- const commitDrag = (0, import_react20.useCallback)((id2, patch) => {
20458
+ const getViewportState = (0, import_react21.useCallback)(() => viewportStateRef.current ?? { x: 0, y: 0, zoom: 1 }, []);
20459
+ const applyLaneSnap = (0, import_react21.useCallback)((id2, patch) => {
20460
+ if (!snapToLanes)
20461
+ return patch;
20462
+ const cols = currentCanvas.columns;
20463
+ const rows = currentCanvas.rows;
20464
+ const node = nodesRef.current.find((n) => n.id === id2);
19852
20465
  let final = patch;
19853
- if (snapToLanes) {
19854
- const cols = currentCanvas.columns;
19855
- const rows = currentCanvas.rows;
19856
- const node = nodesRef.current.find((n) => n.id === id2);
19857
- const nx = patch.x;
19858
- const ny = patch.y;
19859
- if (cols && cols.length > 0 && nx != null) {
19860
- const snapped = snapToLane(nx, cols, {
19861
- edge: "center",
19862
- size: node?.width ?? 0
19863
- });
19864
- final = { ...final, x: Math.round(snapped) };
19865
- }
19866
- if (rows && rows.length > 0 && ny != null) {
19867
- const snapped = snapToLane(ny, rows, {
19868
- edge: "center",
19869
- size: node?.height ?? 0
19870
- });
19871
- final = { ...final, y: Math.round(snapped) };
19872
- }
20466
+ const nx = patch.x;
20467
+ const ny = patch.y;
20468
+ if (cols && cols.length > 0 && nx != null) {
20469
+ const snapped = snapToLane(nx, cols, {
20470
+ edge: "center",
20471
+ size: node?.width ?? 0
20472
+ });
20473
+ final = { ...final, x: Math.round(snapped) };
20474
+ }
20475
+ if (rows && rows.length > 0 && ny != null) {
20476
+ const snapped = snapToLane(ny, rows, {
20477
+ edge: "center",
20478
+ size: node?.height ?? 0
20479
+ });
20480
+ final = { ...final, y: Math.round(snapped) };
20481
+ }
20482
+ return final;
20483
+ }, [snapToLanes, currentCanvas.columns, currentCanvas.rows]);
20484
+ const commitResize = (0, import_react21.useCallback)((id2, patch) => {
20485
+ onNodeUpdate?.(id2, applyLaneSnap(id2, patch), currentCanvasRef);
20486
+ }, [onNodeUpdate, currentCanvasRef, applyLaneSnap]);
20487
+ const commitDragBatch = (0, import_react21.useCallback)((updates) => {
20488
+ const final = updates.map((u) => ({
20489
+ id: u.id,
20490
+ patch: applyLaneSnap(u.id, u.patch)
20491
+ }));
20492
+ if (onNodesUpdate) {
20493
+ onNodesUpdate(final, currentCanvasRef);
20494
+ return;
19873
20495
  }
19874
- onNodeUpdate?.(id2, final, currentCanvasRef);
19875
- }, [onNodeUpdate, currentCanvasRef, snapToLanes, currentCanvas.columns, currentCanvas.rows]);
19876
- const { dragOverrides, onPointerDown: onNodePointerDown } = useNodeDrag({
20496
+ if (!onNodeUpdate)
20497
+ return;
20498
+ for (const { id: id2, patch } of final) {
20499
+ onNodeUpdate(id2, patch, currentCanvasRef);
20500
+ }
20501
+ }, [onNodeUpdate, onNodesUpdate, currentCanvasRef, applyLaneSnap]);
20502
+ const svgProxyRef = (0, import_react21.useRef)(null);
20503
+ const handleNodeDrop = (0, import_react21.useCallback)((sources, target) => {
20504
+ onNodeDrop?.(sources, target, { canvasRef: currentCanvasRef });
20505
+ }, [onNodeDrop, currentCanvasRef]);
20506
+ const { dragOverrides, dropTargetId, onPointerDown: onNodePointerDown } = useNodeDrag({
19877
20507
  viewport: viewportStateRef,
19878
20508
  nodesRef,
19879
- onCommit: commitDrag
20509
+ onCommit: commitDragBatch,
20510
+ svgRef: svgProxyRef,
20511
+ canDropNodeOn,
20512
+ onNodeDrop: handleNodeDrop
19880
20513
  });
19881
20514
  const { resizeOverrides, onHandlePointerDown: onResizeHandlePointerDown } = useNodeResize({
19882
20515
  viewport: viewportStateRef,
19883
- onCommit: commitDrag
20516
+ onCommit: commitResize
19884
20517
  });
19885
- const selectedResolvedNode = (0, import_react20.useMemo)(() => {
20518
+ const selectedResolvedNode = (0, import_react21.useMemo)(() => {
19886
20519
  if (!selectedId)
19887
20520
  return null;
19888
20521
  const base = nodeMap.get(selectedId);
@@ -19898,9 +20531,8 @@ var SystemCanvas = (() => {
19898
20531
  }
19899
20532
  return base;
19900
20533
  }, [selectedId, nodeMap, dragOverrides, resizeOverrides]);
19901
- const svgProxyRef = (0, import_react20.useRef)(null);
19902
20534
  svgProxyRef.current = viewportHandleRef.current?.getSvgElement() ?? null;
19903
- const handleEdgeCreated = (0, import_react20.useCallback)((edge) => {
20535
+ const handleEdgeCreated = (0, import_react21.useCallback)((edge) => {
19904
20536
  onEdgeAdd?.(edge, currentCanvasRef);
19905
20537
  }, [onEdgeAdd, currentCanvasRef]);
19906
20538
  const { pending: pendingEdge, onHandlePointerDown: onConnectionHandlePointerDown } = useEdgeCreate({
@@ -19909,7 +20541,7 @@ var SystemCanvas = (() => {
19909
20541
  nodesRef,
19910
20542
  onCreate: handleEdgeCreated
19911
20543
  });
19912
- const handleNavigableNodeClick = (0, import_react20.useCallback)((node) => {
20544
+ const handleNavigableNodeClick = (0, import_react21.useCallback)((node) => {
19913
20545
  const frame2 = {
19914
20546
  parentCanvasRef: currentCanvasRef,
19915
20547
  parentNodeRect: {
@@ -19936,7 +20568,7 @@ var SystemCanvas = (() => {
19936
20568
  navigateToRef(node);
19937
20569
  }
19938
20570
  }, [navigateToRef, currentCanvasRef, zoomNavConfig.enabled]);
19939
- const handleZoomEnter = (0, import_react20.useCallback)((node, targetTransform) => {
20571
+ const handleZoomEnter = (0, import_react21.useCallback)((node, targetTransform) => {
19940
20572
  const frame2 = {
19941
20573
  parentCanvasRef: currentCanvasRef,
19942
20574
  parentNodeRect: {
@@ -19950,7 +20582,7 @@ var SystemCanvas = (() => {
19950
20582
  setPendingHandoff(targetTransform);
19951
20583
  navigateToRef(node);
19952
20584
  }, [currentCanvasRef, navigateToRef]);
19953
- const handleZoomExit = (0, import_react20.useCallback)((targetTransform) => {
20585
+ const handleZoomExit = (0, import_react21.useCallback)((targetTransform) => {
19954
20586
  setPendingHandoff(targetTransform);
19955
20587
  suppressNextHandoffClearRef.current = true;
19956
20588
  navigateToBreadcrumb(breadcrumbs.length - 2);
@@ -19973,30 +20605,58 @@ var SystemCanvas = (() => {
19973
20605
  const rect = svg.getBoundingClientRect();
19974
20606
  return { width: rect.width, height: rect.height };
19975
20607
  },
20608
+ getCursorScreenPos: () => viewportHandleRef.current?.getCursorScreenPos() ?? null,
19976
20609
  onEnter: handleZoomEnter,
19977
20610
  onExit: handleZoomExit
19978
20611
  });
19979
- const handleViewportChange = (0, import_react20.useCallback)((vp) => {
20612
+ const handleViewportChange = (0, import_react21.useCallback)((vp) => {
19980
20613
  viewportStateRef.current = vp;
19981
20614
  handleZoomNavViewportChange(vp);
19982
20615
  onViewportChange?.(vp);
19983
20616
  }, [handleZoomNavViewportChange, onViewportChange]);
19984
- const handleHandoffApplied = (0, import_react20.useCallback)(() => {
20617
+ const handleHandoffApplied = (0, import_react21.useCallback)(() => {
19985
20618
  setPendingHandoff(null);
19986
20619
  clearZoomNavCommitting();
19987
20620
  }, [clearZoomNavCommitting]);
19988
- const handleBeginEdit = (0, import_react20.useCallback)((node) => {
20621
+ const handleBeginEdit = (0, import_react21.useCallback)((node) => {
19989
20622
  setEditingId(node.id);
19990
20623
  }, []);
19991
- const handleBeginEditEdge = (0, import_react20.useCallback)((edge) => {
20624
+ const handleBeginEditEdge = (0, import_react21.useCallback)((edge) => {
19992
20625
  setEditingEdgeId(edge.id);
19993
20626
  }, []);
20627
+ const [contextMenuState, setContextMenuState] = (0, import_react21.useState)(null);
20628
+ (0, import_react21.useEffect)(() => {
20629
+ setContextMenuState(null);
20630
+ }, [currentCanvasRef]);
20631
+ const handleContextMenu = (0, import_react21.useCallback)((event) => {
20632
+ onContextMenu?.(event);
20633
+ if (!nodeContextMenu)
20634
+ return;
20635
+ if (event.type !== "node")
20636
+ return;
20637
+ const node = event.target;
20638
+ if (!node)
20639
+ return;
20640
+ const matched = filterContextMenuItems(nodeContextMenu.items, node, {
20641
+ canvasRef: currentCanvasRef ?? null
20642
+ });
20643
+ if (matched.length === 0) {
20644
+ setContextMenuState(null);
20645
+ return;
20646
+ }
20647
+ setContextMenuState({
20648
+ items: matched,
20649
+ node,
20650
+ screenPosition: event.screenPosition,
20651
+ canvasRef: currentCanvasRef ?? null
20652
+ });
20653
+ }, [onContextMenu, nodeContextMenu, currentCanvasRef]);
19994
20654
  const { handleNodeClick, handleNodeDoubleClick, handleNodeNavigate, handleEdgeClick, handleEdgeDoubleClick, handleCanvasClick, handleCanvasContextMenu, handleNodeContextMenu, handleEdgeContextMenu } = useCanvasInteraction({
19995
20655
  onNodeClick,
19996
20656
  onNodeDoubleClick,
19997
20657
  onEdgeClick,
19998
20658
  onEdgeDoubleClick,
19999
- onContextMenu,
20659
+ onContextMenu: handleContextMenu,
20000
20660
  onNavigableNodeClick: handleNavigableNodeClick,
20001
20661
  viewport: viewportStateRef,
20002
20662
  editable,
@@ -20005,27 +20665,27 @@ var SystemCanvas = (() => {
20005
20665
  onSelectEdge: setSelectedEdgeId,
20006
20666
  onBeginEditEdge: handleBeginEditEdge
20007
20667
  });
20008
- const handleEditorCommit = (0, import_react20.useCallback)((patch) => {
20668
+ const handleEditorCommit = (0, import_react21.useCallback)((patch) => {
20009
20669
  if (editingId) {
20010
20670
  onNodeUpdate?.(editingId, patch, currentCanvasRef);
20011
20671
  }
20012
20672
  setEditingId(null);
20013
20673
  }, [editingId, onNodeUpdate, currentCanvasRef]);
20014
- const handleEditorCancel = (0, import_react20.useCallback)(() => {
20674
+ const handleEditorCancel = (0, import_react21.useCallback)(() => {
20015
20675
  setEditingId(null);
20016
20676
  }, []);
20017
- const handleEdgeEditorCommit = (0, import_react20.useCallback)((patch) => {
20677
+ const handleEdgeEditorCommit = (0, import_react21.useCallback)((patch) => {
20018
20678
  if (editingEdgeId) {
20019
20679
  onEdgeUpdate?.(editingEdgeId, patch, currentCanvasRef);
20020
20680
  }
20021
20681
  setEditingEdgeId(null);
20022
20682
  }, [editingEdgeId, onEdgeUpdate, currentCanvasRef]);
20023
- const handleEdgeEditorCancel = (0, import_react20.useCallback)(() => {
20683
+ const handleEdgeEditorCancel = (0, import_react21.useCallback)(() => {
20024
20684
  setEditingEdgeId(null);
20025
20685
  }, []);
20026
- const lastAddRef = (0, import_react20.useRef)(null);
20027
- const menuOptions = (0, import_react20.useMemo)(() => getNodeMenuOptions(currentCanvas, theme), [currentCanvas, theme]);
20028
- const addNode2 = (0, import_react20.useCallback)((option, position) => {
20686
+ const lastAddRef = (0, import_react21.useRef)(null);
20687
+ const menuOptions = (0, import_react21.useMemo)(() => getNodeMenuOptions(currentCanvas, theme), [currentCanvas, theme]);
20688
+ const addNode2 = (0, import_react21.useCallback)((option, position) => {
20029
20689
  let x, y;
20030
20690
  if (position) {
20031
20691
  x = position.x;
@@ -20057,7 +20717,7 @@ var SystemCanvas = (() => {
20057
20717
  const node = createNodeFromOption(option, Math.round(x), Math.round(y), void 0, theme);
20058
20718
  onNodeAdd?.(node, currentCanvasRef);
20059
20719
  }, [onNodeAdd, currentCanvasRef, theme]);
20060
- const handleKeyDown = (0, import_react20.useCallback)((e) => {
20720
+ const handleKeyDown = (0, import_react21.useCallback)((e) => {
20061
20721
  if (!editable)
20062
20722
  return;
20063
20723
  if (e.key === "Escape") {
@@ -20091,14 +20751,14 @@ var SystemCanvas = (() => {
20091
20751
  currentCanvasRef
20092
20752
  ]);
20093
20753
  const renderProps = { options: menuOptions, addNode: addNode2, theme };
20094
- return (0, import_jsx_runtime27.jsxs)("div", { ref: containerRef, className: `system-canvas ${className ?? ""}`, tabIndex: editable ? 0 : -1, onKeyDown: handleKeyDown, style: {
20754
+ return (0, import_jsx_runtime28.jsxs)("div", { ref: containerRef, className: `system-canvas ${className ?? ""}`, tabIndex: editable ? 0 : -1, onKeyDown: handleKeyDown, style: {
20095
20755
  position: "relative",
20096
20756
  width: "100%",
20097
20757
  height: "100%",
20098
20758
  overflow: "hidden",
20099
20759
  outline: "none",
20100
20760
  ...style
20101
- }, 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: {
20761
+ }, 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: {
20102
20762
  position: "absolute",
20103
20763
  top: 12,
20104
20764
  right: 12,
@@ -20110,14 +20770,20 @@ var SystemCanvas = (() => {
20110
20770
  fontFamily: theme.node.fontFamily,
20111
20771
  fontSize: 12,
20112
20772
  backdropFilter: "blur(8px)"
20113
- }, 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) => {
20773
+ }, 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) => {
20114
20774
  onNodeUpdate?.(selectedResolvedNode.id, update, currentCanvasRef);
20115
20775
  }, onDelete: () => {
20116
20776
  onNodeDelete?.(selectedResolvedNode.id, currentCanvasRef);
20117
20777
  setSelectedId(null);
20118
- }, getViewport: getViewportState, containerWidth: containerSize.width, containerHeight: containerSize.height, render: renderNodeToolbar }), editable && (renderAddNodeButton ? renderAddNodeButton(renderProps) : (0, import_jsx_runtime27.jsx)(AddNodeButton, { ...renderProps }))] });
20778
+ }, 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) })] });
20119
20779
  });
20120
20780
 
20781
+ // ../react/dist/hooks/useMultiSelect.js
20782
+ var import_react22 = __toESM(require_react(), 1);
20783
+
20784
+ // ../react/dist/hooks/useMultiSelectClipboard.js
20785
+ var import_react23 = __toESM(require_react(), 1);
20786
+
20121
20787
  // src/index.tsx
20122
20788
  function resolveThemeOption(theme) {
20123
20789
  if (typeof theme === "string") {
@@ -20226,7 +20892,7 @@ var SystemCanvas = (() => {
20226
20892
  onEdgeUpdate: handleEdgeUpdate,
20227
20893
  onEdgeDelete: handleEdgeDelete
20228
20894
  };
20229
- root2.render(import_react21.default.createElement(SystemCanvas, props));
20895
+ root2.render(import_react24.default.createElement(SystemCanvas, props));
20230
20896
  };
20231
20897
  doRender();
20232
20898
  return {