system-canvas-standalone 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12759,12 +12759,12 @@ var SystemCanvas = (() => {
12759
12759
  render: () => render,
12760
12760
  themes: () => themes
12761
12761
  });
12762
- var import_react26 = __toESM(require_react(), 1);
12762
+ var import_react27 = __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_runtime28 = __toESM(require_jsx_runtime(), 1);
12767
- var import_react25 = __toESM(require_react(), 1);
12766
+ var import_jsx_runtime29 = __toESM(require_jsx_runtime(), 1);
12767
+ var import_react26 = __toESM(require_react(), 1);
12768
12768
 
12769
12769
  // ../core/dist/themes/dark.js
12770
12770
  var darkTheme = {
@@ -14565,6 +14565,136 @@ var SystemCanvas = (() => {
14565
14565
  return { ...canvas, edges };
14566
14566
  }
14567
14567
 
14568
+ // ../core/dist/grid.js
14569
+ function snapToGrid(value, size) {
14570
+ if (size <= 0)
14571
+ return value;
14572
+ return Math.round(value / size) * size;
14573
+ }
14574
+
14575
+ // ../core/dist/alignment.js
14576
+ function getAnchors(n) {
14577
+ return [
14578
+ { axis: "x", value: n.x, kind: "edge", perpStart: n.y, perpEnd: n.y + n.height },
14579
+ { axis: "x", value: n.x + n.width / 2, kind: "center", perpStart: n.y, perpEnd: n.y + n.height },
14580
+ { axis: "x", value: n.x + n.width, kind: "edge", perpStart: n.y, perpEnd: n.y + n.height },
14581
+ { axis: "y", value: n.y, kind: "edge", perpStart: n.x, perpEnd: n.x + n.width },
14582
+ { axis: "y", value: n.y + n.height / 2, kind: "center", perpStart: n.x, perpEnd: n.x + n.width },
14583
+ { axis: "y", value: n.y + n.height, kind: "edge", perpStart: n.x, perpEnd: n.x + n.width }
14584
+ ];
14585
+ }
14586
+ function computeAlignmentGuides(dragging, others, threshold) {
14587
+ if (dragging.length === 0 || others.length === 0)
14588
+ return [];
14589
+ const map = /* @__PURE__ */ new Map();
14590
+ for (const d of dragging) {
14591
+ const dAnchors = getAnchors(d);
14592
+ for (const o of others) {
14593
+ const oAnchors = getAnchors(o);
14594
+ for (const da of dAnchors) {
14595
+ for (const oa of oAnchors) {
14596
+ if (da.axis !== oa.axis || da.kind !== oa.kind)
14597
+ continue;
14598
+ if (Math.abs(da.value - oa.value) > threshold)
14599
+ continue;
14600
+ const position = oa.value;
14601
+ const spanStart = Math.min(da.perpStart, oa.perpStart);
14602
+ const spanEnd = Math.max(da.perpEnd, oa.perpEnd);
14603
+ const key = `${da.axis}:${position}`;
14604
+ const existing = map.get(key);
14605
+ if (!existing || spanEnd - spanStart > existing.span.end - existing.span.start) {
14606
+ map.set(key, { axis: da.axis, position, span: { start: spanStart, end: spanEnd }, kind: da.kind });
14607
+ }
14608
+ }
14609
+ }
14610
+ }
14611
+ }
14612
+ return Array.from(map.values());
14613
+ }
14614
+ function alignNodes(nodes, direction) {
14615
+ if (nodes.length === 0)
14616
+ return [];
14617
+ let ref;
14618
+ switch (direction) {
14619
+ case "left":
14620
+ ref = Math.min(...nodes.map((n) => n.x));
14621
+ break;
14622
+ case "right":
14623
+ ref = Math.max(...nodes.map((n) => n.x + n.width));
14624
+ break;
14625
+ case "top":
14626
+ ref = Math.min(...nodes.map((n) => n.y));
14627
+ break;
14628
+ case "bottom":
14629
+ ref = Math.max(...nodes.map((n) => n.y + n.height));
14630
+ break;
14631
+ case "centerH":
14632
+ ref = nodes.reduce((s, n) => s + (n.x + n.width / 2), 0) / nodes.length;
14633
+ break;
14634
+ case "centerV":
14635
+ ref = nodes.reduce((s, n) => s + (n.y + n.height / 2), 0) / nodes.length;
14636
+ break;
14637
+ }
14638
+ const result = [];
14639
+ for (const n of nodes) {
14640
+ let newX;
14641
+ let newY;
14642
+ switch (direction) {
14643
+ case "left":
14644
+ newX = ref;
14645
+ break;
14646
+ case "right":
14647
+ newX = ref - n.width;
14648
+ break;
14649
+ case "top":
14650
+ newY = ref;
14651
+ break;
14652
+ case "bottom":
14653
+ newY = ref - n.height;
14654
+ break;
14655
+ case "centerH":
14656
+ newX = ref - n.width / 2;
14657
+ break;
14658
+ case "centerV":
14659
+ newY = ref - n.height / 2;
14660
+ break;
14661
+ }
14662
+ const patch = {};
14663
+ if (newX !== void 0 && newX !== n.x)
14664
+ patch.x = newX;
14665
+ if (newY !== void 0 && newY !== n.y)
14666
+ patch.y = newY;
14667
+ if (Object.keys(patch).length > 0)
14668
+ result.push({ id: n.id, patch });
14669
+ }
14670
+ return result;
14671
+ }
14672
+ function distributeNodes(nodes, axis) {
14673
+ if (nodes.length < 3)
14674
+ return [];
14675
+ const isH = axis === "horizontal";
14676
+ const sorted = [...nodes].sort((a, b) => isH ? a.x - b.x : a.y - b.y);
14677
+ const first = sorted[0];
14678
+ const last = sorted[sorted.length - 1];
14679
+ const outerStart = isH ? first.x : first.y;
14680
+ const outerEnd = isH ? last.x + last.width : last.y + last.height;
14681
+ const sumInnerSizes = sorted.slice(1, -1).reduce((s, n) => s + (isH ? n.width : n.height), 0);
14682
+ const totalOuterSize = isH ? first.width + last.width : first.height + last.height;
14683
+ const spacing = (outerEnd - outerStart - totalOuterSize - sumInnerSizes) / (sorted.length - 1);
14684
+ const result = [];
14685
+ let cursor = outerStart + (isH ? first.width : first.height) + spacing;
14686
+ for (let i = 1; i < sorted.length - 1; i++) {
14687
+ const n = sorted[i];
14688
+ const newVal = cursor;
14689
+ const oldVal = isH ? n.x : n.y;
14690
+ if (newVal !== oldVal) {
14691
+ result.push({ id: n.id, patch: isH ? { x: newVal } : { y: newVal } });
14692
+ }
14693
+ cursor += (isH ? n.width : n.height) + spacing;
14694
+ }
14695
+ return result;
14696
+ }
14697
+
14568
14698
  // ../core/dist/index.js
14569
14699
  var themes = {
14570
14700
  dark: darkTheme,
@@ -14973,7 +15103,7 @@ var SystemCanvas = (() => {
14973
15103
  var import_react4 = __toESM(require_react(), 1);
14974
15104
  var DRAG_THRESHOLD = 3;
14975
15105
  function useNodeDrag(options) {
14976
- const { viewport, nodesRef, onCommit, svgRef, canDropNodeOn, onNodeDrop, selectedIdsRef } = options;
15106
+ const { viewport, nodesRef, onCommit, svgRef, canDropNodeOn, onNodeDrop, selectedIdsRef, snapGridSize = 0 } = options;
14977
15107
  const [dragOverrides, setDragOverrides] = (0, import_react4.useState)(() => /* @__PURE__ */ new Map());
14978
15108
  const [isDragging, setIsDragging] = (0, import_react4.useState)(false);
14979
15109
  const [dropTargetId, setDropTargetId] = (0, import_react4.useState)(null);
@@ -15030,7 +15160,9 @@ var SystemCanvas = (() => {
15030
15160
  const dy = dyScreen / zoom;
15031
15161
  const next = /* @__PURE__ */ new Map();
15032
15162
  for (const [id2, start2] of st.moving) {
15033
- next.set(id2, { x: start2.startX + dx, y: start2.startY + dy });
15163
+ const sx = snapGridSize > 0 ? snapToGrid(start2.startX + dx, snapGridSize) : start2.startX + dx;
15164
+ const sy = snapGridSize > 0 ? snapToGrid(start2.startY + dy, snapGridSize) : start2.startY + dy;
15165
+ next.set(id2, { x: sx, y: sy });
15034
15166
  }
15035
15167
  setDragOverrides(next);
15036
15168
  const nextTarget = computeDropTarget(event.clientX, event.clientY);
@@ -15157,7 +15289,7 @@ var SystemCanvas = (() => {
15157
15289
  var import_react5 = __toESM(require_react(), 1);
15158
15290
  var DEFAULT_MIN_SIZE = 40;
15159
15291
  function useNodeResize(options) {
15160
- const { viewport, onCommit, minSize = DEFAULT_MIN_SIZE } = options;
15292
+ const { viewport, onCommit, minSize = DEFAULT_MIN_SIZE, snapGridSize = 0 } = options;
15161
15293
  const [resizeOverrides, setResizeOverrides] = (0, import_react5.useState)(() => /* @__PURE__ */ new Map());
15162
15294
  const [isResizing, setIsResizing] = (0, import_react5.useState)(false);
15163
15295
  const stateRef = (0, import_react5.useRef)(null);
@@ -15205,6 +15337,14 @@ var SystemCanvas = (() => {
15205
15337
  }
15206
15338
  nh = minSize;
15207
15339
  }
15340
+ if (snapGridSize > 0) {
15341
+ nw = Math.max(minSize, snapToGrid(nw, snapGridSize));
15342
+ nh = Math.max(minSize, snapToGrid(nh, snapGridSize));
15343
+ if (st.corner === "sw" || st.corner === "nw")
15344
+ nx = st.startX + st.startW - nw;
15345
+ if (st.corner === "ne" || st.corner === "nw")
15346
+ ny = st.startY + st.startH - nh;
15347
+ }
15208
15348
  const next = new Map(overridesRef.current);
15209
15349
  next.set(st.nodeId, { x: nx, y: ny, width: nw, height: nh });
15210
15350
  setResizeOverrides(next);
@@ -16196,7 +16336,7 @@ var SystemCanvas = (() => {
16196
16336
  }
16197
16337
 
16198
16338
  // ../react/dist/components/Viewport.js
16199
- var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
16339
+ var import_jsx_runtime23 = __toESM(require_jsx_runtime(), 1);
16200
16340
  var import_react19 = __toESM(require_react(), 1);
16201
16341
 
16202
16342
  // ../react/dist/hooks/useViewport.js
@@ -20276,10 +20416,18 @@ var SystemCanvas = (() => {
20276
20416
  })] });
20277
20417
  }
20278
20418
 
20419
+ // ../react/dist/components/AlignmentGuidesLayer.js
20420
+ var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
20421
+ function AlignmentGuidesLayer({ guides }) {
20422
+ if (guides.length === 0)
20423
+ return null;
20424
+ return (0, import_jsx_runtime22.jsx)(import_jsx_runtime22.Fragment, { children: guides.map((g, i) => (0, import_jsx_runtime22.jsx)("line", { x1: g.axis === "x" ? g.position : g.span.start, y1: g.axis === "y" ? g.position : g.span.start, x2: g.axis === "x" ? g.position : g.span.end, y2: g.axis === "y" ? g.position : g.span.end, stroke: "#38BDF8", strokeWidth: 1, strokeDasharray: "4 3", pointerEvents: "none" }, i)) });
20425
+ }
20426
+
20279
20427
  // ../react/dist/components/Viewport.js
20280
20428
  var HOVER_PADDING = 10;
20281
20429
  var EDGE_PROXIMITY = 16;
20282
- var Viewport = (0, import_react19.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, selectedIds, editingId, selectedEdgeId, editingEdgeId, dragOverrides, dropTargetId, marqueeRect, marqueeActiveRef, resizeOverrides, onResizeHandlePointerDown, onEditorCommit, onEditorCancel, onEdgeEditorCommit, onEdgeEditorCancel, pendingEdge, onConnectionHandlePointerDown, edgeCreateEnabled, autoFit = "canvas-change", canvasRef, handoffTransform, onHandoffApplied, handoffFadeMs = 0 }, ref) {
20430
+ var Viewport = (0, import_react19.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, selectedIds, editingId, selectedEdgeId, editingEdgeId, dragOverrides, dropTargetId, marqueeRect, marqueeActiveRef, resizeOverrides, onResizeHandlePointerDown, onEditorCommit, onEditorCancel, onEdgeEditorCommit, onEdgeEditorCancel, pendingEdge, onConnectionHandlePointerDown, edgeCreateEnabled, alignmentGuides, autoFit = "canvas-change", canvasRef, handoffTransform, onHandoffApplied, handoffFadeMs = 0 }, ref) {
20283
20431
  const { svgRef, groupRef, viewport, fitToContent, zoomToNode, setTransform } = useViewport({
20284
20432
  minZoom,
20285
20433
  maxZoom,
@@ -20487,7 +20635,7 @@ var SystemCanvas = (() => {
20487
20635
  return null;
20488
20636
  return computeEdgeMidpoint(editingEdge, from, to);
20489
20637
  })();
20490
- return (0, import_jsx_runtime22.jsxs)("svg", { ref: svgRef, className: "system-canvas-viewport", style: {
20638
+ return (0, import_jsx_runtime23.jsxs)("svg", { ref: svgRef, className: "system-canvas-viewport", style: {
20491
20639
  width: "100%",
20492
20640
  height: "100%",
20493
20641
  display: "block",
@@ -20496,21 +20644,21 @@ var SystemCanvas = (() => {
20496
20644
  WebkitUserSelect: "none",
20497
20645
  MozUserSelect: "none",
20498
20646
  msUserSelect: "none"
20499
- }, 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, selectedIds, 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, selectedIds, 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" }), selectedIds && selectedIds.size > 0 && Array.from(selectedIds).map((id2) => {
20647
+ }, onClick: onCanvasClick, onContextMenu: onCanvasContextMenu, onPointerMove: handleSvgPointerMove, onPointerLeave: handleSvgPointerLeave, children: [(0, import_jsx_runtime23.jsx)("defs", { children: (0, import_jsx_runtime23.jsx)("pattern", { id: "system-canvas-grid", width: theme.grid.size, height: theme.grid.size, patternUnits: "userSpaceOnUse", children: (0, import_jsx_runtime23.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_runtime23.jsx)("rect", { x: "-50000", y: "-50000", width: "100000", height: "100000", fill: "url(#system-canvas-grid)" }), (0, import_jsx_runtime23.jsxs)("g", { ref: groupRef, children: [(0, import_jsx_runtime23.jsx)(LanesBackground, { columns, rows, theme }), (0, import_jsx_runtime23.jsx)(NodeRenderer, { nodes: renderNodes, theme, onClick: onNodeClick, onDoubleClick: onNodeDoubleClick, onContextMenu: onNodeContextMenu, onNavigate: onNodeNavigate, onPointerDown: onNodePointerDown, selectedIds, editingId, canvases, only: "groups" }), (0, import_jsx_runtime23.jsx)(EdgeRenderer, { edges, nodeMap: renderNodeMap, theme, defaultEdgeStyle: edgeStyle, onClick: onEdgeClick, onDoubleClick: onEdgeDoubleClick, onContextMenu: onEdgeContextMenu, selectedId: selectedEdgeId, editingId: editingEdgeId }), (0, import_jsx_runtime23.jsx)(NodeRenderer, { nodes: renderNodes, theme, onClick: onNodeClick, onDoubleClick: onNodeDoubleClick, onContextMenu: onNodeContextMenu, onNavigate: onNodeNavigate, onPointerDown: onNodePointerDown, selectedIds, editingId, onResizeHandlePointerDown, canvases, only: "non-groups" }), pendingTargetNode && (0, import_jsx_runtime23.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_runtime23.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" }), selectedIds && selectedIds.size > 0 && Array.from(selectedIds).map((id2) => {
20500
20648
  const node = renderNodeMap.get(id2);
20501
20649
  if (!node)
20502
20650
  return null;
20503
- return (0, import_jsx_runtime22.jsx)("rect", { x: node.x - 3, y: node.y - 3, width: node.width + 6, height: node.height + 6, rx: (node.resolvedCornerRadius ?? 0) + 3, fill: "none", stroke: theme.node.labelColor, strokeWidth: 1.5, opacity: 0.7, pointerEvents: "none" }, `halo-${id2}`);
20504
- }), 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 })] }), marqueeRect && (0, import_jsx_runtime22.jsx)("rect", { x: Math.min(marqueeRect.x1, marqueeRect.x2), y: Math.min(marqueeRect.y1, marqueeRect.y2), width: Math.abs(marqueeRect.x2 - marqueeRect.x1), height: Math.abs(marqueeRect.y2 - marqueeRect.y1), fill: theme.node.labelColor + "18", stroke: theme.node.labelColor, strokeWidth: 1, opacity: 0.9, pointerEvents: "none" })] });
20651
+ return (0, import_jsx_runtime23.jsx)("rect", { x: node.x - 3, y: node.y - 3, width: node.width + 6, height: node.height + 6, rx: (node.resolvedCornerRadius ?? 0) + 3, fill: "none", stroke: theme.node.labelColor, strokeWidth: 1.5, opacity: 0.7, pointerEvents: "none" }, `halo-${id2}`);
20652
+ }), pendingEdge && pendingSourceNode && (0, import_jsx_runtime23.jsx)(PendingEdgeRenderer, { sourceNode: pendingSourceNode, sourceSide: pendingEdge.sourceSide, cursor: pendingEdge.cursor, targetNode: pendingTargetNode, theme, defaultEdgeStyle: edgeStyle }), handlesNode && onConnectionHandlePointerDown && (0, import_jsx_runtime23.jsx)(ConnectionHandles, { node: handlesNode, theme, onHandlePointerDown: onConnectionHandlePointerDown, immediate: !!pendingEdge, activeSide: hoveredSide }), alignmentGuides && alignmentGuides.length > 0 && (0, import_jsx_runtime23.jsx)(AlignmentGuidesLayer, { guides: alignmentGuides }), editingNode && onEditorCommit && onEditorCancel && (0, import_jsx_runtime23.jsx)(NodeEditor, { node: editingNode, theme, onCommit: onEditorCommit, onCancel: onEditorCancel }), editingEdge && editingEdgeMidpoint && onEdgeEditorCommit && onEdgeEditorCancel && (0, import_jsx_runtime23.jsx)(EdgeLabelEditor, { initialLabel: editingEdge.label ?? "", midpoint: editingEdgeMidpoint, theme, onCommit: onEdgeEditorCommit, onCancel: onEdgeEditorCancel })] }), marqueeRect && (0, import_jsx_runtime23.jsx)("rect", { x: Math.min(marqueeRect.x1, marqueeRect.x2), y: Math.min(marqueeRect.y1, marqueeRect.y2), width: Math.abs(marqueeRect.x2 - marqueeRect.x1), height: Math.abs(marqueeRect.y2 - marqueeRect.y1), fill: theme.node.labelColor + "18", stroke: theme.node.labelColor, strokeWidth: 1, opacity: 0.9, pointerEvents: "none" })] });
20505
20653
  });
20506
20654
 
20507
20655
  // ../react/dist/components/Breadcrumbs.js
20508
- var import_jsx_runtime23 = __toESM(require_jsx_runtime(), 1);
20656
+ var import_jsx_runtime24 = __toESM(require_jsx_runtime(), 1);
20509
20657
  var import_react20 = __toESM(require_react(), 1);
20510
20658
  function Breadcrumbs({ breadcrumbs, theme, onNavigate }) {
20511
20659
  if (breadcrumbs.length <= 1)
20512
20660
  return null;
20513
- return (0, import_jsx_runtime23.jsx)("div", { className: "system-canvas-breadcrumbs", style: {
20661
+ return (0, import_jsx_runtime24.jsx)("div", { className: "system-canvas-breadcrumbs", style: {
20514
20662
  position: "absolute",
20515
20663
  top: 12,
20516
20664
  left: 12,
@@ -20527,10 +20675,10 @@ var SystemCanvas = (() => {
20527
20675
  backdropFilter: "blur(8px)"
20528
20676
  }, children: breadcrumbs.map((crumb, index) => {
20529
20677
  const isLast = index === breadcrumbs.length - 1;
20530
- return (0, import_jsx_runtime23.jsxs)(import_react20.default.Fragment, { children: [index > 0 && (0, import_jsx_runtime23.jsx)("span", { style: {
20678
+ return (0, import_jsx_runtime24.jsxs)(import_react20.default.Fragment, { children: [index > 0 && (0, import_jsx_runtime24.jsx)("span", { style: {
20531
20679
  color: theme.separatorColor,
20532
20680
  margin: "0 2px"
20533
- }, children: "/" }), (0, import_jsx_runtime23.jsx)("span", { onClick: isLast ? void 0 : () => onNavigate(index), style: {
20681
+ }, children: "/" }), (0, import_jsx_runtime24.jsx)("span", { onClick: isLast ? void 0 : () => onNavigate(index), style: {
20534
20682
  color: isLast ? theme.activeColor : theme.textColor,
20535
20683
  cursor: isLast ? "default" : "pointer",
20536
20684
  fontWeight: isLast ? 600 : 400,
@@ -20550,7 +20698,7 @@ var SystemCanvas = (() => {
20550
20698
  }
20551
20699
 
20552
20700
  // ../react/dist/components/AddNodeButton.js
20553
- var import_jsx_runtime24 = __toESM(require_jsx_runtime(), 1);
20701
+ var import_jsx_runtime25 = __toESM(require_jsx_runtime(), 1);
20554
20702
  var import_react21 = __toESM(require_react(), 1);
20555
20703
  function AddNodeButton({ options, addNode: addNode2, theme }) {
20556
20704
  const [open, setOpen] = (0, import_react21.useState)(false);
@@ -20578,7 +20726,7 @@ var SystemCanvas = (() => {
20578
20726
  }, [open]);
20579
20727
  const categoryOptions = options.filter((o) => o.kind === "category");
20580
20728
  const typeOptions = options.filter((o) => o.kind === "type");
20581
- return (0, import_jsx_runtime24.jsxs)("div", { ref: rootRef, className: "system-canvas-add-node", style: {
20729
+ return (0, import_jsx_runtime25.jsxs)("div", { ref: rootRef, className: "system-canvas-add-node", style: {
20582
20730
  position: "absolute",
20583
20731
  bottom: 16,
20584
20732
  right: 16,
@@ -20586,7 +20734,7 @@ var SystemCanvas = (() => {
20586
20734
  fontFamily: theme.breadcrumbs.fontFamily,
20587
20735
  fontSize: theme.breadcrumbs.fontSize,
20588
20736
  userSelect: "none"
20589
- }, children: [open && (0, import_jsx_runtime24.jsxs)("div", { style: {
20737
+ }, children: [open && (0, import_jsx_runtime25.jsxs)("div", { style: {
20590
20738
  position: "absolute",
20591
20739
  bottom: 52,
20592
20740
  right: 0,
@@ -20599,13 +20747,13 @@ var SystemCanvas = (() => {
20599
20747
  borderRadius: 10,
20600
20748
  boxShadow: "0 8px 24px rgba(0,0,0,0.35)",
20601
20749
  backdropFilter: "blur(10px)"
20602
- }, children: [categoryOptions.length > 0 && (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [(0, import_jsx_runtime24.jsx)(SectionLabel, { theme, children: "Categories" }), categoryOptions.map((opt) => (0, import_jsx_runtime24.jsx)(MenuRow, { theme, option: opt, onClick: () => {
20750
+ }, children: [categoryOptions.length > 0 && (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [(0, import_jsx_runtime25.jsx)(SectionLabel, { theme, children: "Categories" }), categoryOptions.map((opt) => (0, import_jsx_runtime25.jsx)(MenuRow, { theme, option: opt, onClick: () => {
20603
20751
  addNode2(opt);
20604
20752
  setOpen(false);
20605
- } }, `cat-${opt.value}`)), (0, import_jsx_runtime24.jsx)(Divider, { theme })] }), (0, import_jsx_runtime24.jsx)(SectionLabel, { theme, children: "Basic" }), typeOptions.map((opt) => (0, import_jsx_runtime24.jsx)(MenuRow, { theme, option: opt, onClick: () => {
20753
+ } }, `cat-${opt.value}`)), (0, import_jsx_runtime25.jsx)(Divider, { theme })] }), (0, import_jsx_runtime25.jsx)(SectionLabel, { theme, children: "Basic" }), typeOptions.map((opt) => (0, import_jsx_runtime25.jsx)(MenuRow, { theme, option: opt, onClick: () => {
20606
20754
  addNode2(opt);
20607
20755
  setOpen(false);
20608
- } }, `type-${opt.value}`))] }), (0, import_jsx_runtime24.jsx)("button", { type: "button", "aria-label": open ? "Close add node menu" : "Add node", onClick: () => setOpen((v) => !v), style: {
20756
+ } }, `type-${opt.value}`))] }), (0, import_jsx_runtime25.jsx)("button", { type: "button", "aria-label": open ? "Close add node menu" : "Add node", onClick: () => setOpen((v) => !v), style: {
20609
20757
  width: 44,
20610
20758
  height: 44,
20611
20759
  borderRadius: 22,
@@ -20626,7 +20774,7 @@ var SystemCanvas = (() => {
20626
20774
  }, children: "+" })] });
20627
20775
  }
20628
20776
  function SectionLabel({ theme, children: children2 }) {
20629
- return (0, import_jsx_runtime24.jsx)("div", { style: {
20777
+ return (0, import_jsx_runtime25.jsx)("div", { style: {
20630
20778
  padding: "4px 8px",
20631
20779
  fontSize: theme.breadcrumbs.fontSize - 2,
20632
20780
  color: theme.breadcrumbs.textColor,
@@ -20636,7 +20784,7 @@ var SystemCanvas = (() => {
20636
20784
  }, children: children2 });
20637
20785
  }
20638
20786
  function Divider({ theme }) {
20639
- return (0, import_jsx_runtime24.jsx)("div", { style: {
20787
+ return (0, import_jsx_runtime25.jsx)("div", { style: {
20640
20788
  height: 1,
20641
20789
  margin: "4px 0",
20642
20790
  background: theme.breadcrumbs.separatorColor,
@@ -20646,7 +20794,7 @@ var SystemCanvas = (() => {
20646
20794
  function MenuRow({ theme, option, onClick }) {
20647
20795
  const [hover, setHover] = (0, import_react21.useState)(false);
20648
20796
  const swatchSize = 18;
20649
- return (0, import_jsx_runtime24.jsxs)("div", { role: "button", onClick, onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), style: {
20797
+ return (0, import_jsx_runtime25.jsxs)("div", { role: "button", onClick, onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), style: {
20650
20798
  display: "flex",
20651
20799
  alignItems: "center",
20652
20800
  gap: 8,
@@ -20655,7 +20803,7 @@ var SystemCanvas = (() => {
20655
20803
  cursor: "pointer",
20656
20804
  background: hover ? "rgba(255,255,255,0.06)" : "transparent",
20657
20805
  color: theme.breadcrumbs.activeColor
20658
- }, children: [option.kind === "category" ? (0, import_jsx_runtime24.jsx)("span", { style: {
20806
+ }, children: [option.kind === "category" ? (0, import_jsx_runtime25.jsx)("span", { style: {
20659
20807
  width: swatchSize,
20660
20808
  height: swatchSize,
20661
20809
  borderRadius: 4,
@@ -20665,13 +20813,13 @@ var SystemCanvas = (() => {
20665
20813
  alignItems: "center",
20666
20814
  justifyContent: "center",
20667
20815
  flexShrink: 0
20668
- }, children: option.icon && (0, import_jsx_runtime24.jsx)("svg", { width: 12, height: 12, viewBox: "0 0 14 14", style: { overflow: "visible" }, children: (0, import_jsx_runtime24.jsx)("g", { transform: "translate(-1, -1)", children: (0, import_jsx_runtime24.jsx)(NodeIcon, { icon: option.icon, x: 0, y: 0, size: 14, color: option.stroke ?? theme.breadcrumbs.activeColor, opacity: 0.9, customIcons: theme.icons }) }) }) }) : (0, import_jsx_runtime24.jsx)("span", { style: {
20816
+ }, children: option.icon && (0, import_jsx_runtime25.jsx)("svg", { width: 12, height: 12, viewBox: "0 0 14 14", style: { overflow: "visible" }, children: (0, import_jsx_runtime25.jsx)("g", { transform: "translate(-1, -1)", children: (0, import_jsx_runtime25.jsx)(NodeIcon, { icon: option.icon, x: 0, y: 0, size: 14, color: option.stroke ?? theme.breadcrumbs.activeColor, opacity: 0.9, customIcons: theme.icons }) }) }) }) : (0, import_jsx_runtime25.jsx)("span", { style: {
20669
20817
  width: swatchSize,
20670
20818
  height: swatchSize,
20671
20819
  borderRadius: 4,
20672
20820
  border: `1px dashed ${theme.breadcrumbs.separatorColor}`,
20673
20821
  flexShrink: 0
20674
- } }), (0, import_jsx_runtime24.jsx)("span", { style: { textTransform: "capitalize" }, children: option.label }), (0, import_jsx_runtime24.jsx)("span", { style: {
20822
+ } }), (0, import_jsx_runtime25.jsx)("span", { style: { textTransform: "capitalize" }, children: option.label }), (0, import_jsx_runtime25.jsx)("span", { style: {
20675
20823
  marginLeft: "auto",
20676
20824
  fontSize: theme.breadcrumbs.fontSize - 2,
20677
20825
  opacity: 0.5
@@ -20679,7 +20827,7 @@ var SystemCanvas = (() => {
20679
20827
  }
20680
20828
 
20681
20829
  // ../react/dist/components/LaneHeaders.js
20682
- var import_jsx_runtime25 = __toESM(require_jsx_runtime(), 1);
20830
+ var import_jsx_runtime26 = __toESM(require_jsx_runtime(), 1);
20683
20831
  var import_react22 = __toESM(require_react(), 1);
20684
20832
  function LaneHeaders({ columns, rows, theme, getViewport, width, height, pinned = true }) {
20685
20833
  const hasColumns = columns && columns.length > 0;
@@ -20712,14 +20860,14 @@ var SystemCanvas = (() => {
20712
20860
  const pad = lanesTheme.headerPadding;
20713
20861
  const colsOffsetLeft = hasRows && pinned ? headerSize : 0;
20714
20862
  const rowsOffsetTop = hasColumns && pinned ? headerSize : 0;
20715
- return (0, import_jsx_runtime25.jsxs)("svg", { className: "system-canvas-lane-headers", style: {
20863
+ return (0, import_jsx_runtime26.jsxs)("svg", { className: "system-canvas-lane-headers", style: {
20716
20864
  position: "absolute",
20717
20865
  top: 0,
20718
20866
  left: 0,
20719
20867
  width,
20720
20868
  height,
20721
20869
  pointerEvents: "none"
20722
- }, children: [hasColumns && (0, import_jsx_runtime25.jsxs)("g", { children: [pinned && (0, import_jsx_runtime25.jsx)("rect", { x: colsOffsetLeft, y: 0, width: width - colsOffsetLeft, height: headerSize, fill: lanesTheme.headerBackground }), columns.map((col) => {
20870
+ }, children: [hasColumns && (0, import_jsx_runtime26.jsxs)("g", { children: [pinned && (0, import_jsx_runtime26.jsx)("rect", { x: colsOffsetLeft, y: 0, width: width - colsOffsetLeft, height: headerSize, fill: lanesTheme.headerBackground }), columns.map((col) => {
20723
20871
  const startScreen = canvasToScreen(col.start, 0, viewport).x;
20724
20872
  const endScreen = canvasToScreen(col.start + col.size, 0, viewport).x;
20725
20873
  const w = endScreen - startScreen;
@@ -20730,8 +20878,8 @@ var SystemCanvas = (() => {
20730
20878
  const cx = visibleLeft + visibleW / 2;
20731
20879
  if (endScreen <= colsOffsetLeft || startScreen >= width)
20732
20880
  return null;
20733
- 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}`);
20734
- }), 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) => {
20881
+ return (0, import_jsx_runtime26.jsxs)("g", { children: [(0, import_jsx_runtime26.jsx)("line", { x1: startScreen, y1: pinned ? headerSize : y + headerSize, x2: startScreen, y2: pinned ? 0 : y, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth }), (0, import_jsx_runtime26.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}`);
20882
+ }), pinned && (0, import_jsx_runtime26.jsx)("line", { x1: colsOffsetLeft, y1: headerSize, x2: width, y2: headerSize, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth })] }), hasRows && (0, import_jsx_runtime26.jsxs)("g", { children: [pinned && (0, import_jsx_runtime26.jsx)("rect", { x: 0, y: rowsOffsetTop, width: headerSize, height: height - rowsOffsetTop, fill: lanesTheme.headerBackground }), rows.map((row) => {
20735
20883
  const startScreen = canvasToScreen(0, row.start, viewport).y;
20736
20884
  const endScreen = canvasToScreen(0, row.start + row.size, viewport).y;
20737
20885
  const h = endScreen - startScreen;
@@ -20742,8 +20890,8 @@ var SystemCanvas = (() => {
20742
20890
  const cy = visibleTop + visibleH / 2;
20743
20891
  if (endScreen <= rowsOffsetTop || startScreen >= height)
20744
20892
  return null;
20745
- 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}`);
20746
- }), 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 })] });
20893
+ return (0, import_jsx_runtime26.jsxs)("g", { children: [(0, import_jsx_runtime26.jsx)("line", { x1: pinned ? 0 : x, y1: startScreen, x2: pinned ? headerSize : x + headerSize, y2: startScreen, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth }), (0, import_jsx_runtime26.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}`);
20894
+ }), pinned && (0, import_jsx_runtime26.jsx)("line", { x1: headerSize, y1: rowsOffsetTop, x2: headerSize, y2: height, stroke: lanesTheme.dividerColor, strokeWidth: lanesTheme.dividerWidth })] }), hasColumns && hasRows && pinned && (0, import_jsx_runtime26.jsx)("rect", { x: 0, y: 0, width: headerSize, height: headerSize, fill: lanesTheme.headerBackground })] });
20747
20895
  }
20748
20896
  function truncateToWidth2(label, availablePx, fontSize) {
20749
20897
  if (availablePx <= 0)
@@ -20758,7 +20906,7 @@ var SystemCanvas = (() => {
20758
20906
  }
20759
20907
 
20760
20908
  // ../react/dist/components/NodeToolbar.js
20761
- var import_jsx_runtime26 = __toESM(require_jsx_runtime(), 1);
20909
+ var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
20762
20910
  var import_react23 = __toESM(require_react(), 1);
20763
20911
  var NODE_GAP = 10;
20764
20912
  var FLIP_MARGIN = 8;
@@ -20767,7 +20915,7 @@ var SystemCanvas = (() => {
20767
20915
  var SWATCH_SIZE = 16;
20768
20916
  var BUTTON_SIZE = 28;
20769
20917
  var DELETE_SIZE = 14;
20770
- function NodeToolbar({ node, theme, onPatch, onDelete, getViewport, containerWidth, containerHeight, render: render2, selectedNodes, onMultiPatch }) {
20918
+ function NodeToolbar({ node, theme, onPatch, onDelete, getViewport, containerWidth, containerHeight, render: render2, selectedNodes, onMultiPatch, onAlign, onDistribute }) {
20771
20919
  const [viewport, setViewport] = (0, import_react23.useState)(() => getViewport());
20772
20920
  (0, import_react23.useEffect)(() => {
20773
20921
  let raf = 0;
@@ -20834,7 +20982,7 @@ var SystemCanvas = (() => {
20834
20982
  left = Math.max(FLIP_MARGIN, Math.min(left, containerWidth - size.width - FLIP_MARGIN));
20835
20983
  const patch = (update) => onPatch(update);
20836
20984
  const deleteNode = () => onDelete();
20837
- return (0, import_jsx_runtime26.jsx)("div", {
20985
+ return (0, import_jsx_runtime27.jsx)("div", {
20838
20986
  ref: toolbarRef,
20839
20987
  className: "system-canvas-node-toolbar",
20840
20988
  // Stop pointer events from bubbling to the canvas (which would
@@ -20863,16 +21011,16 @@ var SystemCanvas = (() => {
20863
21011
  userSelect: "none",
20864
21012
  whiteSpace: "nowrap"
20865
21013
  },
20866
- children: isMulti && selectedNodes && onMultiPatch ? (0, import_jsx_runtime26.jsx)(MultiToolbarContent, { selectedNodes, theme, onMultiPatch, onDelete: deleteNode }) : render2 ? render2({ node, theme, patch, deleteNode }) : (0, import_jsx_runtime26.jsx)(DefaultToolbarContent, { node, theme, onPatch: patch, onDelete: deleteNode })
21014
+ children: isMulti && selectedNodes && onMultiPatch ? (0, import_jsx_runtime27.jsx)(MultiToolbarContent, { selectedNodes, theme, onMultiPatch, onDelete: deleteNode, onAlign, onDistribute }) : render2 ? render2({ node, theme, patch, deleteNode }) : (0, import_jsx_runtime27.jsx)(DefaultToolbarContent, { node, theme, onPatch: patch, onDelete: deleteNode })
20867
21015
  });
20868
21016
  }
20869
- function MultiToolbarContent({ selectedNodes, theme, onMultiPatch, onDelete }) {
21017
+ function MultiToolbarContent({ selectedNodes, theme, onMultiPatch, onDelete, onAlign, onDistribute }) {
20870
21018
  const representativeNode = selectedNodes[0];
20871
21019
  const groups = (0, import_react23.useMemo)(() => getNodeActionsForNode(representativeNode, theme), [representativeNode, theme]);
20872
21020
  const showDelete = theme.showToolbarDelete === true;
20873
21021
  const swatchGroups = groups.filter((g) => g.kind === "swatches" || g.kind == null);
20874
21022
  const otherGroups = groups.filter((g) => g.kind !== "swatches" && g.kind != null && g.kind !== "menu");
20875
- return (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [(0, import_jsx_runtime26.jsxs)("span", { style: {
21023
+ return (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [(0, import_jsx_runtime27.jsxs)("span", { style: {
20876
21024
  fontSize: 11,
20877
21025
  color: theme.breadcrumbs.textColor,
20878
21026
  opacity: 0.75,
@@ -20882,35 +21030,75 @@ var SystemCanvas = (() => {
20882
21030
  const actions = filterActionsForNode(group, representativeNode);
20883
21031
  if (actions.length === 0)
20884
21032
  return null;
20885
- return (0, import_jsx_runtime26.jsxs)(import_react23.default.Fragment, { children: [i > 0 && (0, import_jsx_runtime26.jsx)(Divider2, { theme }), (0, import_jsx_runtime26.jsx)("div", { style: { display: "flex", alignItems: "center", gap: BUTTON_GAP }, children: actions.map((action) => {
21033
+ return (0, import_jsx_runtime27.jsxs)(import_react23.default.Fragment, { children: [i > 0 && (0, import_jsx_runtime27.jsx)(Divider2, { theme }), (0, import_jsx_runtime27.jsx)("div", { style: { display: "flex", alignItems: "center", gap: BUTTON_GAP }, children: actions.map((action) => {
20886
21034
  const active = action.isActive?.(representativeNode) ?? false;
20887
21035
  const handleClick = () => {
20888
21036
  const patch = resolveActionPatch(action, representativeNode);
20889
21037
  onMultiPatch(patch);
20890
21038
  };
20891
- return (0, import_jsx_runtime26.jsx)(SwatchButton, { action, active, theme, onClick: handleClick }, action.id);
21039
+ return (0, import_jsx_runtime27.jsx)(SwatchButton, { action, active, theme, onClick: handleClick }, action.id);
20892
21040
  }) })] }, group.id);
20893
21041
  }), otherGroups.map((group) => {
20894
21042
  const actions = filterActionsForNode(group, representativeNode);
20895
21043
  if (actions.length === 0)
20896
21044
  return null;
20897
- return (0, import_jsx_runtime26.jsxs)(import_react23.default.Fragment, { children: [(0, import_jsx_runtime26.jsx)(Divider2, { theme }), (0, import_jsx_runtime26.jsx)("div", { style: { display: "flex", alignItems: "center", gap: BUTTON_GAP }, children: actions.map((action) => {
21045
+ return (0, import_jsx_runtime27.jsxs)(import_react23.default.Fragment, { children: [(0, import_jsx_runtime27.jsx)(Divider2, { theme }), (0, import_jsx_runtime27.jsx)("div", { style: { display: "flex", alignItems: "center", gap: BUTTON_GAP }, children: actions.map((action) => {
20898
21046
  const active = action.isActive?.(representativeNode) ?? false;
20899
21047
  const handleClick = () => {
20900
21048
  const patch = resolveActionPatch(action, representativeNode);
20901
21049
  onMultiPatch(patch);
20902
21050
  };
20903
- return (0, import_jsx_runtime26.jsx)(IconButton, { action, active, theme, onClick: handleClick }, action.id);
21051
+ return (0, import_jsx_runtime27.jsx)(IconButton, { action, active, theme, onClick: handleClick }, action.id);
20904
21052
  }) })] }, group.id);
20905
- }), 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 })] })] });
21053
+ }), onAlign && (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [(0, import_jsx_runtime27.jsx)(Divider2, { theme }), (0, import_jsx_runtime27.jsx)("div", { style: { display: "flex", alignItems: "center", gap: BUTTON_GAP }, children: [
21054
+ { dir: "left", label: "Align Left", path: "M 3 3 L 3 13 M 5 6 L 13 6 M 5 10 L 11 10" },
21055
+ { dir: "right", label: "Align Right", path: "M 13 3 L 13 13 M 3 6 L 11 6 M 5 10 L 11 10" },
21056
+ { dir: "top", label: "Align Top", path: "M 3 3 L 13 3 M 6 5 L 6 13 M 10 5 L 10 11" },
21057
+ { dir: "bottom", label: "Align Bottom", path: "M 3 13 L 13 13 M 6 3 L 6 11 M 10 5 L 10 11" },
21058
+ { dir: "centerH", label: "Center Horizontally", path: "M 8 3 L 8 13 M 5 6 L 11 6 M 4 10 L 12 10" },
21059
+ { dir: "centerV", label: "Center Vertically", path: "M 3 8 L 13 8 M 6 5 L 6 11 M 10 4 L 10 12" }
21060
+ ].map(({ dir, label, path }) => (0, import_jsx_runtime27.jsx)("button", { type: "button", title: label, onClick: () => onAlign(dir), onMouseDown: (e) => e.preventDefault(), style: {
21061
+ width: BUTTON_SIZE,
21062
+ height: BUTTON_SIZE,
21063
+ display: "inline-flex",
21064
+ alignItems: "center",
21065
+ justifyContent: "center",
21066
+ background: "transparent",
21067
+ border: `1px solid ${theme.breadcrumbs.separatorColor}`,
21068
+ borderRadius: 6,
21069
+ color: theme.breadcrumbs.textColor,
21070
+ cursor: "pointer",
21071
+ padding: 0,
21072
+ outline: "none"
21073
+ }, children: (0, import_jsx_runtime27.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 16 16", children: (0, import_jsx_runtime27.jsx)("path", { d: path, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }) }) }, dir)) })] }), onDistribute && (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [(0, import_jsx_runtime27.jsx)(Divider2, { theme }), (0, import_jsx_runtime27.jsx)("div", { style: { display: "flex", alignItems: "center", gap: BUTTON_GAP }, children: [
21074
+ { axis: "horizontal", label: "Distribute Horizontally", path: "M 3 3 L 3 13 M 13 3 L 13 13 M 7 6 L 7 10 M 9 6 L 9 10" },
21075
+ { axis: "vertical", label: "Distribute Vertically", path: "M 3 3 L 13 3 M 3 13 L 13 13 M 6 7 L 10 7 M 6 9 L 10 9" }
21076
+ ].map(({ axis, label, path }) => {
21077
+ const disabled = selectedNodes.length < 3;
21078
+ return (0, import_jsx_runtime27.jsx)("button", { type: "button", title: label, disabled, onClick: () => !disabled && onDistribute(axis), onMouseDown: (e) => e.preventDefault(), style: {
21079
+ width: BUTTON_SIZE,
21080
+ height: BUTTON_SIZE,
21081
+ display: "inline-flex",
21082
+ alignItems: "center",
21083
+ justifyContent: "center",
21084
+ background: "transparent",
21085
+ border: `1px solid ${theme.breadcrumbs.separatorColor}`,
21086
+ borderRadius: 6,
21087
+ color: disabled ? theme.breadcrumbs.separatorColor : theme.breadcrumbs.textColor,
21088
+ cursor: disabled ? "not-allowed" : "pointer",
21089
+ opacity: disabled ? 0.4 : 1,
21090
+ padding: 0,
21091
+ outline: "none"
21092
+ }, children: (0, import_jsx_runtime27.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 16 16", children: (0, import_jsx_runtime27.jsx)("path", { d: path, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }) }) }, axis);
21093
+ }) })] }), showDelete && (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [(0, import_jsx_runtime27.jsx)(Divider2, { theme }), (0, import_jsx_runtime27.jsx)(DeleteButton, { theme, onDelete })] })] });
20906
21094
  }
20907
21095
  function DefaultToolbarContent({ node, theme, onPatch, onDelete }) {
20908
21096
  const groups = (0, import_react23.useMemo)(() => getNodeActionsForNode(node, theme), [node, theme]);
20909
21097
  const showDelete = theme.showToolbarDelete === true;
20910
- return (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [groups.map((group, i) => (0, import_jsx_runtime26.jsxs)(import_react23.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 })] })] });
21098
+ return (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [groups.map((group, i) => (0, import_jsx_runtime27.jsxs)(import_react23.default.Fragment, { children: [i > 0 && (0, import_jsx_runtime27.jsx)(Divider2, { theme }), (0, import_jsx_runtime27.jsx)(ActionGroupView, { group, node, theme, onPatch })] }, group.id)), showDelete && (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [(0, import_jsx_runtime27.jsx)(Divider2, { theme }), (0, import_jsx_runtime27.jsx)(DeleteButton, { theme, onDelete })] })] });
20911
21099
  }
20912
21100
  function Divider2({ theme }) {
20913
- return (0, import_jsx_runtime26.jsx)("div", { style: {
21101
+ return (0, import_jsx_runtime27.jsx)("div", { style: {
20914
21102
  width: 1,
20915
21103
  alignSelf: "stretch",
20916
21104
  background: theme.breadcrumbs.separatorColor,
@@ -20923,23 +21111,23 @@ var SystemCanvas = (() => {
20923
21111
  return null;
20924
21112
  const kind = group.kind ?? "buttons";
20925
21113
  if (kind === "menu") {
20926
- return (0, import_jsx_runtime26.jsx)(MenuGroup, { group, actions, node, theme, onPatch });
21114
+ return (0, import_jsx_runtime27.jsx)(MenuGroup, { group, actions, node, theme, onPatch });
20927
21115
  }
20928
- return (0, import_jsx_runtime26.jsx)("div", { title: group.label, style: { display: "flex", alignItems: "center", gap: BUTTON_GAP }, children: actions.map((action) => {
21116
+ return (0, import_jsx_runtime27.jsx)("div", { title: group.label, style: { display: "flex", alignItems: "center", gap: BUTTON_GAP }, children: actions.map((action) => {
20929
21117
  const handleClick = () => {
20930
21118
  const patch = resolveActionPatch(action, node);
20931
21119
  onPatch(patch);
20932
21120
  };
20933
21121
  const active = action.isActive?.(node) ?? false;
20934
21122
  if (kind === "swatches") {
20935
- return (0, import_jsx_runtime26.jsx)(SwatchButton, { action, active, theme, onClick: handleClick }, action.id);
21123
+ return (0, import_jsx_runtime27.jsx)(SwatchButton, { action, active, theme, onClick: handleClick }, action.id);
20936
21124
  }
20937
- return (0, import_jsx_runtime26.jsx)(IconButton, { action, active, theme, onClick: handleClick }, action.id);
21125
+ return (0, import_jsx_runtime27.jsx)(IconButton, { action, active, theme, onClick: handleClick }, action.id);
20938
21126
  }) });
20939
21127
  }
20940
21128
  function SwatchButton({ action, active, theme, onClick }) {
20941
21129
  const color2 = action.swatch ?? theme.node.labelColor;
20942
- return (0, import_jsx_runtime26.jsx)("button", { type: "button", title: action.label, onClick, style: {
21130
+ return (0, import_jsx_runtime27.jsx)("button", { type: "button", title: action.label, onClick, style: {
20943
21131
  width: SWATCH_SIZE,
20944
21132
  height: SWATCH_SIZE,
20945
21133
  borderRadius: "50%",
@@ -20953,7 +21141,7 @@ var SystemCanvas = (() => {
20953
21141
  }, onMouseDown: (e) => e.preventDefault() });
20954
21142
  }
20955
21143
  function IconButton({ action, active, theme, onClick }) {
20956
- return (0, import_jsx_runtime26.jsx)("button", { type: "button", title: action.label, onClick, style: {
21144
+ return (0, import_jsx_runtime27.jsx)("button", { type: "button", title: action.label, onClick, style: {
20957
21145
  width: BUTTON_SIZE,
20958
21146
  height: BUTTON_SIZE,
20959
21147
  display: "inline-flex",
@@ -20966,12 +21154,12 @@ var SystemCanvas = (() => {
20966
21154
  cursor: "pointer",
20967
21155
  padding: 0,
20968
21156
  outline: "none"
20969
- }, onMouseDown: (e) => e.preventDefault(), children: action.icon ? (0, import_jsx_runtime26.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 16 16", children: (0, import_jsx_runtime26.jsx)(NodeIcon, { icon: action.icon, x: 0, y: 0, size: 16, color: active ? theme.breadcrumbs.activeColor : theme.breadcrumbs.textColor, opacity: 1, customIcons: theme.icons }) }) : action.swatch ? (0, import_jsx_runtime26.jsx)("span", { style: {
21157
+ }, onMouseDown: (e) => e.preventDefault(), children: action.icon ? (0, import_jsx_runtime27.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 16 16", children: (0, import_jsx_runtime27.jsx)(NodeIcon, { icon: action.icon, x: 0, y: 0, size: 16, color: active ? theme.breadcrumbs.activeColor : theme.breadcrumbs.textColor, opacity: 1, customIcons: theme.icons }) }) : action.swatch ? (0, import_jsx_runtime27.jsx)("span", { style: {
20970
21158
  width: 10,
20971
21159
  height: 10,
20972
21160
  borderRadius: "50%",
20973
21161
  background: action.swatch
20974
- } }) : (0, import_jsx_runtime26.jsx)("span", { style: { fontSize: 10 }, children: action.label.slice(0, 2) }) });
21162
+ } }) : (0, import_jsx_runtime27.jsx)("span", { style: { fontSize: 10 }, children: action.label.slice(0, 2) }) });
20975
21163
  }
20976
21164
  function MenuGroup({ group, actions, node, theme, onPatch }) {
20977
21165
  const [open, setOpen] = (0, import_react23.useState)(false);
@@ -20989,7 +21177,7 @@ var SystemCanvas = (() => {
20989
21177
  const active = actions.find((a) => a.isActive?.(node));
20990
21178
  const triggerLabel = active?.label ?? group.label ?? "Menu";
20991
21179
  const triggerIcon = active?.icon ?? void 0;
20992
- return (0, import_jsx_runtime26.jsxs)("div", { ref: wrapRef, style: { position: "relative" }, children: [(0, import_jsx_runtime26.jsxs)("button", { type: "button", title: group.label, onClick: () => setOpen((v) => !v), style: {
21180
+ return (0, import_jsx_runtime27.jsxs)("div", { ref: wrapRef, style: { position: "relative" }, children: [(0, import_jsx_runtime27.jsxs)("button", { type: "button", title: group.label, onClick: () => setOpen((v) => !v), style: {
20993
21181
  height: BUTTON_SIZE,
20994
21182
  display: "inline-flex",
20995
21183
  alignItems: "center",
@@ -21003,7 +21191,7 @@ var SystemCanvas = (() => {
21003
21191
  fontFamily: "inherit",
21004
21192
  fontSize: "inherit",
21005
21193
  outline: "none"
21006
- }, onMouseDown: (e) => e.preventDefault(), children: [triggerIcon && (0, import_jsx_runtime26.jsx)("svg", { width: 14, height: 14, viewBox: "0 0 16 16", children: (0, import_jsx_runtime26.jsx)(NodeIcon, { icon: triggerIcon, x: 0, y: 0, size: 14, color: theme.breadcrumbs.textColor, opacity: 1, customIcons: theme.icons }) }), (0, import_jsx_runtime26.jsx)("span", { children: triggerLabel }), (0, import_jsx_runtime26.jsx)("span", { style: { opacity: 0.6, fontSize: 8 }, children: "\u25BE" })] }), open && (0, import_jsx_runtime26.jsx)("div", { style: {
21194
+ }, onMouseDown: (e) => e.preventDefault(), children: [triggerIcon && (0, import_jsx_runtime27.jsx)("svg", { width: 14, height: 14, viewBox: "0 0 16 16", children: (0, import_jsx_runtime27.jsx)(NodeIcon, { icon: triggerIcon, x: 0, y: 0, size: 14, color: theme.breadcrumbs.textColor, opacity: 1, customIcons: theme.icons }) }), (0, import_jsx_runtime27.jsx)("span", { children: triggerLabel }), (0, import_jsx_runtime27.jsx)("span", { style: { opacity: 0.6, fontSize: 8 }, children: "\u25BE" })] }), open && (0, import_jsx_runtime27.jsx)("div", { style: {
21007
21195
  position: "absolute",
21008
21196
  top: "100%",
21009
21197
  left: 0,
@@ -21018,7 +21206,7 @@ var SystemCanvas = (() => {
21018
21206
  zIndex: 1
21019
21207
  }, children: actions.map((action) => {
21020
21208
  const isActive = action.isActive?.(node) ?? false;
21021
- return (0, import_jsx_runtime26.jsxs)("button", { type: "button", onClick: () => {
21209
+ return (0, import_jsx_runtime27.jsxs)("button", { type: "button", onClick: () => {
21022
21210
  onPatch(resolveActionPatch(action, node));
21023
21211
  setOpen(false);
21024
21212
  }, style: {
@@ -21035,17 +21223,17 @@ var SystemCanvas = (() => {
21035
21223
  fontFamily: "inherit",
21036
21224
  fontSize: "inherit",
21037
21225
  textAlign: "left"
21038
- }, onMouseDown: (e) => e.preventDefault(), children: [action.icon && (0, import_jsx_runtime26.jsx)("svg", { width: 14, height: 14, viewBox: "0 0 16 16", children: (0, import_jsx_runtime26.jsx)(NodeIcon, { icon: action.icon, x: 0, y: 0, size: 14, color: isActive ? theme.breadcrumbs.activeColor : theme.breadcrumbs.textColor, opacity: 1, customIcons: theme.icons }) }), action.swatch && (0, import_jsx_runtime26.jsx)("span", { style: {
21226
+ }, onMouseDown: (e) => e.preventDefault(), children: [action.icon && (0, import_jsx_runtime27.jsx)("svg", { width: 14, height: 14, viewBox: "0 0 16 16", children: (0, import_jsx_runtime27.jsx)(NodeIcon, { icon: action.icon, x: 0, y: 0, size: 14, color: isActive ? theme.breadcrumbs.activeColor : theme.breadcrumbs.textColor, opacity: 1, customIcons: theme.icons }) }), action.swatch && (0, import_jsx_runtime27.jsx)("span", { style: {
21039
21227
  width: 10,
21040
21228
  height: 10,
21041
21229
  borderRadius: "50%",
21042
21230
  background: action.swatch,
21043
21231
  flexShrink: 0
21044
- } }), (0, import_jsx_runtime26.jsx)("span", { children: action.label })] }, action.id);
21232
+ } }), (0, import_jsx_runtime27.jsx)("span", { children: action.label })] }, action.id);
21045
21233
  }) })] });
21046
21234
  }
21047
21235
  function DeleteButton({ theme, onDelete }) {
21048
- return (0, import_jsx_runtime26.jsx)("button", { type: "button", title: "Delete", onClick: onDelete, onMouseDown: (e) => e.preventDefault(), style: {
21236
+ return (0, import_jsx_runtime27.jsx)("button", { type: "button", title: "Delete", onClick: onDelete, onMouseDown: (e) => e.preventDefault(), style: {
21049
21237
  width: BUTTON_SIZE,
21050
21238
  height: BUTTON_SIZE,
21051
21239
  display: "inline-flex",
@@ -21058,23 +21246,44 @@ var SystemCanvas = (() => {
21058
21246
  cursor: "pointer",
21059
21247
  padding: 0,
21060
21248
  outline: "none"
21061
- }, 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" }) }) });
21249
+ }, children: (0, import_jsx_runtime27.jsx)("svg", { width: DELETE_SIZE, height: DELETE_SIZE, viewBox: "0 0 16 16", children: (0, import_jsx_runtime27.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" }) }) });
21062
21250
  }
21063
21251
 
21064
- // ../react/dist/components/NodeContextMenuOverlay.js
21065
- var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
21252
+ // ../react/dist/hooks/useAlignmentGuides.js
21066
21253
  var import_react24 = __toESM(require_react(), 1);
21254
+ function useAlignmentGuides(options) {
21255
+ const { dragOverrides, nodesRef, isDragging, threshold } = options;
21256
+ return (0, import_react24.useMemo)(() => {
21257
+ if (!isDragging || dragOverrides.size === 0)
21258
+ return [];
21259
+ const all = nodesRef.current ?? [];
21260
+ const dragging = [];
21261
+ const others = [];
21262
+ for (const n of all) {
21263
+ const override = dragOverrides.get(n.id);
21264
+ if (override)
21265
+ dragging.push({ ...n, ...override });
21266
+ else
21267
+ others.push(n);
21268
+ }
21269
+ return computeAlignmentGuides(dragging, others, threshold);
21270
+ }, [dragOverrides, isDragging, nodesRef, threshold]);
21271
+ }
21272
+
21273
+ // ../react/dist/components/NodeContextMenuOverlay.js
21274
+ var import_jsx_runtime28 = __toESM(require_jsx_runtime(), 1);
21275
+ var import_react25 = __toESM(require_react(), 1);
21067
21276
  var ESTIMATED_MENU_WIDTH = 200;
21068
21277
  var MIN_MENU_WIDTH = 160;
21069
21278
  var VIEWPORT_MARGIN = 8;
21070
21279
  function NodeContextMenuOverlay({ state, config, theme, onClose }) {
21071
- const rootRef = (0, import_react24.useRef)(null);
21072
- const [hoveredId, setHoveredId] = (0, import_react24.useState)(null);
21073
- (0, import_react24.useEffect)(() => {
21280
+ const rootRef = (0, import_react25.useRef)(null);
21281
+ const [hoveredId, setHoveredId] = (0, import_react25.useState)(null);
21282
+ (0, import_react25.useEffect)(() => {
21074
21283
  if (state)
21075
21284
  setHoveredId(null);
21076
21285
  }, [state]);
21077
- (0, import_react24.useEffect)(() => {
21286
+ (0, import_react25.useEffect)(() => {
21078
21287
  if (!state)
21079
21288
  return;
21080
21289
  function onDown(e) {
@@ -21118,7 +21327,7 @@ var SystemCanvas = (() => {
21118
21327
  const top = vh ? Math.min(state.screenPosition.y, vh - estimatedHeight - VIEWPORT_MARGIN) : state.screenPosition.y;
21119
21328
  const matchCtx = { canvasRef: state.canvasRef };
21120
21329
  const anyIcon = state.items.some((item) => !!item.icon);
21121
- return (0, import_jsx_runtime27.jsx)("div", {
21330
+ return (0, import_jsx_runtime28.jsx)("div", {
21122
21331
  ref: rootRef,
21123
21332
  role: "menu",
21124
21333
  // Stop right-clicks inside the menu from bubbling into the document
@@ -21151,7 +21360,7 @@ var SystemCanvas = (() => {
21151
21360
  const isDisabled = item.disabled?.(state.node, matchCtx) ?? false;
21152
21361
  const isHovered = !isDisabled && hoveredId === item.id;
21153
21362
  const color2 = item.destructive ? cm.destructiveItemColor : cm.itemColor;
21154
- 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: () => {
21363
+ return (0, import_jsx_runtime28.jsxs)("div", { role: "menuitem", "aria-disabled": isDisabled, onMouseEnter: () => !isDisabled && setHoveredId(item.id), onMouseLeave: () => setHoveredId((id2) => id2 === item.id ? null : id2), onClick: () => {
21155
21364
  if (isDisabled)
21156
21365
  return;
21157
21366
  config.onSelect(item.id, state.node, {
@@ -21169,12 +21378,12 @@ var SystemCanvas = (() => {
21169
21378
  opacity: isDisabled ? 0.45 : 1,
21170
21379
  background: isHovered ? cm.itemHoverBackground : "transparent",
21171
21380
  color: color2
21172
- }, 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 ? (
21381
+ }, children: [item.icon ? (0, import_jsx_runtime28.jsx)("svg", { width: 14, height: 14, viewBox: "0 0 16 16", style: { flexShrink: 0, overflow: "visible" }, children: (0, import_jsx_runtime28.jsx)(NodeIcon, { icon: item.icon, x: 0, y: 0, size: 14, color: color2, opacity: 1, customIcons: theme.icons }) }) : anyIcon ? (
21173
21382
  // Only reserve a spacer when other items in the same menu
21174
21383
  // do have icons — keeps labels vertically aligned in a
21175
21384
  // mixed menu without padding lone-item menus.
21176
- (0, import_jsx_runtime27.jsx)("span", { style: { width: 14, flexShrink: 0 }, "aria-hidden": true })
21177
- ) : null, (0, import_jsx_runtime27.jsx)("span", { children: item.label })] }, item.id);
21385
+ (0, import_jsx_runtime28.jsx)("span", { style: { width: 14, flexShrink: 0 }, "aria-hidden": true })
21386
+ ) : null, (0, import_jsx_runtime28.jsx)("span", { children: item.label })] }, item.id);
21178
21387
  })
21179
21388
  });
21180
21389
  }
@@ -21182,8 +21391,8 @@ var SystemCanvas = (() => {
21182
21391
  // ../react/dist/components/SystemCanvas.js
21183
21392
  var CASCADE_WINDOW_MS = 1500;
21184
21393
  var CASCADE_OFFSET = 20;
21185
- var SystemCanvas = (0, import_react25.forwardRef)(function SystemCanvas2({ canvas, onResolveCanvas, canvases, rootLabel = "Home", onNavigate, onBreadcrumbClick, onBreadcrumbsChange, onNodeClick, onNodeDoubleClick, onEdgeClick, onEdgeDoubleClick, onContextMenu, onSelectionChange, nodeContextMenu, editable = false, onNodeAdd, onNodeUpdate, onNodesUpdate, onNodeDelete, onNodesDelete, 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, historyDepth, onUndo, onRedo, className, style }, forwardedRef) {
21186
- const zoomNavConfig = (0, import_react25.useMemo)(() => {
21394
+ var SystemCanvas = (0, import_react26.forwardRef)(function SystemCanvas2({ canvas, onResolveCanvas, canvases, rootLabel = "Home", onNavigate, onBreadcrumbClick, onBreadcrumbsChange, onNodeClick, onNodeDoubleClick, onEdgeClick, onEdgeDoubleClick, onContextMenu, onSelectionChange, nodeContextMenu, editable = false, onNodeAdd, onNodeUpdate, onNodesUpdate, onNodeDelete, onNodesDelete, 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, snapGrid, guideThreshold, alignDistributeMenu, historyDepth, onUndo, onRedo, className, style }, forwardedRef) {
21395
+ const zoomNavConfig = (0, import_react26.useMemo)(() => {
21187
21396
  const defaults = {
21188
21397
  enterThreshold: 0.66,
21189
21398
  exitThreshold: 0.33,
@@ -21208,16 +21417,16 @@ var SystemCanvas = (() => {
21208
21417
  }, [zoomNavigation]);
21209
21418
  const effectiveMaxZoom = maxZoom ?? (zoomNavConfig.enabled ? 16 : 4);
21210
21419
  const effectiveMinZoom = minZoomProp ?? (zoomNavConfig.enabled ? 0.01 : 0.1);
21211
- (0, import_react25.useEffect)(() => {
21420
+ (0, import_react26.useEffect)(() => {
21212
21421
  const env = globalThis.process?.env?.NODE_ENV;
21213
21422
  if (editable && !canvases && env !== "production") {
21214
21423
  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.");
21215
21424
  }
21216
21425
  }, [editable, canvases]);
21217
- const [parentFrames, setParentFrames] = (0, import_react25.useState)([]);
21218
- const [pendingHandoff, setPendingHandoff] = (0, import_react25.useState)(null);
21219
- const suppressNextHandoffClearRef = (0, import_react25.useRef)(false);
21220
- const handleBreadcrumbClick = (0, import_react25.useCallback)((index) => {
21426
+ const [parentFrames, setParentFrames] = (0, import_react26.useState)([]);
21427
+ const [pendingHandoff, setPendingHandoff] = (0, import_react26.useState)(null);
21428
+ const suppressNextHandoffClearRef = (0, import_react26.useRef)(false);
21429
+ const handleBreadcrumbClick = (0, import_react26.useCallback)((index) => {
21221
21430
  setParentFrames((prev) => prev.slice(0, index));
21222
21431
  if (suppressNextHandoffClearRef.current) {
21223
21432
  suppressNextHandoffClearRef.current = false;
@@ -21234,10 +21443,10 @@ var SystemCanvas = (() => {
21234
21443
  onNavigate,
21235
21444
  onBreadcrumbClick: handleBreadcrumbClick
21236
21445
  });
21237
- (0, import_react25.useEffect)(() => {
21446
+ (0, import_react26.useEffect)(() => {
21238
21447
  onBreadcrumbsChange?.(breadcrumbs);
21239
21448
  }, [breadcrumbs, onBreadcrumbsChange]);
21240
- const theme = (0, import_react25.useMemo)(() => {
21449
+ const theme = (0, import_react26.useMemo)(() => {
21241
21450
  const registry = { ...themes, ...customThemes };
21242
21451
  const resolveByName = (name) => name && registry[name] ? registry[name] : null;
21243
21452
  if (themeProp) {
@@ -21248,22 +21457,23 @@ var SystemCanvas = (() => {
21248
21457
  }
21249
21458
  return resolveByName(currentCanvas.theme?.base) ?? resolveByName(canvas.theme?.base) ?? darkTheme;
21250
21459
  }, [themeProp, customThemes, currentCanvas.theme?.base, canvas.theme?.base]);
21251
- const { nodes, edges, nodeMap } = (0, import_react25.useMemo)(() => {
21460
+ const snapGridSize = snapGrid === true ? theme.grid.size : typeof snapGrid === "number" ? snapGrid : 0;
21461
+ const { nodes, edges, nodeMap } = (0, import_react26.useMemo)(() => {
21252
21462
  const resolved = resolveCanvas(currentCanvas, theme);
21253
21463
  const map = buildNodeMap(resolved.nodes);
21254
21464
  return { nodes: resolved.nodes, edges: resolved.edges, nodeMap: map };
21255
21465
  }, [currentCanvas, theme]);
21256
- const nodesRef = (0, import_react25.useRef)(nodes);
21466
+ const nodesRef = (0, import_react26.useRef)(nodes);
21257
21467
  nodesRef.current = nodes;
21258
- const viewportStateRef = (0, import_react25.useRef)(defaultViewport ?? { x: 0, y: 0, zoom: 1 });
21259
- const viewportHandleRef = (0, import_react25.useRef)(null);
21260
- const navigateToRefRef = (0, import_react25.useRef)(navigateToRef);
21468
+ const viewportStateRef = (0, import_react26.useRef)(defaultViewport ?? { x: 0, y: 0, zoom: 1 });
21469
+ const viewportHandleRef = (0, import_react26.useRef)(null);
21470
+ const navigateToRefRef = (0, import_react26.useRef)(navigateToRef);
21261
21471
  navigateToRefRef.current = navigateToRef;
21262
- const navigateToBreadcrumbRef = (0, import_react25.useRef)(navigateToBreadcrumb);
21472
+ const navigateToBreadcrumbRef = (0, import_react26.useRef)(navigateToBreadcrumb);
21263
21473
  navigateToBreadcrumbRef.current = navigateToBreadcrumb;
21264
- const breadcrumbsRef = (0, import_react25.useRef)(breadcrumbs);
21474
+ const breadcrumbsRef = (0, import_react26.useRef)(breadcrumbs);
21265
21475
  breadcrumbsRef.current = breadcrumbs;
21266
- (0, import_react25.useImperativeHandle)(forwardedRef, () => ({
21476
+ (0, import_react26.useImperativeHandle)(forwardedRef, () => ({
21267
21477
  zoomIntoNode: (nodeId, options) => {
21268
21478
  return new Promise((resolve) => {
21269
21479
  const node = nodesRef.current.find((n) => n.id === nodeId);
@@ -21295,11 +21505,11 @@ var SystemCanvas = (() => {
21295
21505
  navigateToBreadcrumbRef.current(0);
21296
21506
  }
21297
21507
  }), [forwardedRef]);
21298
- const [editingId, setEditingId] = (0, import_react25.useState)(null);
21299
- const [selectedEdgeId, setSelectedEdgeId] = (0, import_react25.useState)(null);
21300
- const [editingEdgeId, setEditingEdgeId] = (0, import_react25.useState)(null);
21301
- const svgProxyRef = (0, import_react25.useRef)(null);
21302
- const containerRef = (0, import_react25.useRef)(null);
21508
+ const [editingId, setEditingId] = (0, import_react26.useState)(null);
21509
+ const [selectedEdgeId, setSelectedEdgeId] = (0, import_react26.useState)(null);
21510
+ const [editingEdgeId, setEditingEdgeId] = (0, import_react26.useState)(null);
21511
+ const svgProxyRef = (0, import_react26.useRef)(null);
21512
+ const containerRef = (0, import_react26.useRef)(null);
21303
21513
  const { selectedIds, selectNode, toggleNode, selectAll, clearSelection, selectMultiple, marqueeRect, marqueeActiveRef } = useMultiSelect({
21304
21514
  svgRef: svgProxyRef,
21305
21515
  viewport: viewportStateRef,
@@ -21307,12 +21517,12 @@ var SystemCanvas = (() => {
21307
21517
  containerRef,
21308
21518
  enabled: editable
21309
21519
  });
21310
- const selectedIdsRef = (0, import_react25.useRef)(selectedIds);
21311
- (0, import_react25.useEffect)(() => {
21520
+ const selectedIdsRef = (0, import_react26.useRef)(selectedIds);
21521
+ (0, import_react26.useEffect)(() => {
21312
21522
  selectedIdsRef.current = selectedIds;
21313
21523
  }, [selectedIds]);
21314
- const edgesRef = (0, import_react25.useRef)(edges);
21315
- (0, import_react25.useEffect)(() => {
21524
+ const edgesRef = (0, import_react26.useRef)(edges);
21525
+ (0, import_react26.useEffect)(() => {
21316
21526
  edgesRef.current = edges;
21317
21527
  }, [edges]);
21318
21528
  const { wrappedOnNodeAdd, wrappedOnNodeUpdate, wrappedOnNodesUpdate, wrappedOnNodeDelete, wrappedOnNodesDelete, wrappedOnEdgeAdd, wrappedOnEdgeUpdate, wrappedOnEdgeDelete, beginBatch, endBatch, undo, redo } = useCommandHistory({
@@ -21344,18 +21554,18 @@ var SystemCanvas = (() => {
21344
21554
  onBeginBatch: beginBatch,
21345
21555
  onEndBatch: endBatch
21346
21556
  });
21347
- (0, import_react25.useEffect)(() => {
21557
+ (0, import_react26.useEffect)(() => {
21348
21558
  clearSelection();
21349
21559
  setEditingId(null);
21350
21560
  setSelectedEdgeId(null);
21351
21561
  setEditingEdgeId(null);
21352
21562
  }, [currentCanvasRef]);
21353
- const onSelectionChangeRef = (0, import_react25.useRef)(onSelectionChange);
21354
- (0, import_react25.useEffect)(() => {
21563
+ const onSelectionChangeRef = (0, import_react26.useRef)(onSelectionChange);
21564
+ (0, import_react26.useEffect)(() => {
21355
21565
  onSelectionChangeRef.current = onSelectionChange;
21356
21566
  }, [onSelectionChange]);
21357
- const lastEmittedSelectionRef = (0, import_react25.useRef)({ kind: null, id: null, multiIds: null, canvasRef: void 0 });
21358
- (0, import_react25.useEffect)(() => {
21567
+ const lastEmittedSelectionRef = (0, import_react26.useRef)({ kind: null, id: null, multiIds: null, canvasRef: void 0 });
21568
+ (0, import_react26.useEffect)(() => {
21359
21569
  const cb = onSelectionChangeRef.current;
21360
21570
  let next = null;
21361
21571
  if (selectedIds.size > 1) {
@@ -21397,8 +21607,8 @@ var SystemCanvas = (() => {
21397
21607
  }
21398
21608
  cb?.(next);
21399
21609
  }, [selectedIds, selectedEdgeId, currentCanvasRef, nodeMap, edges]);
21400
- const [containerSize, setContainerSize] = (0, import_react25.useState)({ width: 0, height: 0 });
21401
- (0, import_react25.useEffect)(() => {
21610
+ const [containerSize, setContainerSize] = (0, import_react26.useState)({ width: 0, height: 0 });
21611
+ (0, import_react26.useEffect)(() => {
21402
21612
  const el = containerRef.current;
21403
21613
  if (!el)
21404
21614
  return;
@@ -21413,8 +21623,8 @@ var SystemCanvas = (() => {
21413
21623
  }, []);
21414
21624
  const hasLanes = currentCanvas.columns && currentCanvas.columns.length > 0 || currentCanvas.rows && currentCanvas.rows.length > 0;
21415
21625
  const showLaneHeaders = hasLanes && laneHeaders !== "none";
21416
- const getViewportState = (0, import_react25.useCallback)(() => viewportStateRef.current ?? { x: 0, y: 0, zoom: 1 }, []);
21417
- const applyLaneSnap = (0, import_react25.useCallback)((id2, patch) => {
21626
+ const getViewportState = (0, import_react26.useCallback)(() => viewportStateRef.current ?? { x: 0, y: 0, zoom: 1 }, []);
21627
+ const applyLaneSnap = (0, import_react26.useCallback)((id2, patch) => {
21418
21628
  if (!snapToLanes)
21419
21629
  return patch;
21420
21630
  const cols = currentCanvas.columns;
@@ -21439,10 +21649,10 @@ var SystemCanvas = (() => {
21439
21649
  }
21440
21650
  return final;
21441
21651
  }, [snapToLanes, currentCanvas.columns, currentCanvas.rows]);
21442
- const commitResize = (0, import_react25.useCallback)((id2, patch) => {
21652
+ const commitResize = (0, import_react26.useCallback)((id2, patch) => {
21443
21653
  wrappedOnNodeUpdate(id2, applyLaneSnap(id2, patch), currentCanvasRef);
21444
21654
  }, [wrappedOnNodeUpdate, currentCanvasRef, applyLaneSnap]);
21445
- const commitDragBatch = (0, import_react25.useCallback)((updates) => {
21655
+ const commitDragBatch = (0, import_react26.useCallback)((updates) => {
21446
21656
  const final = updates.map((u) => ({
21447
21657
  id: u.id,
21448
21658
  patch: applyLaneSnap(u.id, u.patch)
@@ -21457,24 +21667,32 @@ var SystemCanvas = (() => {
21457
21667
  wrappedOnNodeUpdate(id2, patch, currentCanvasRef);
21458
21668
  }
21459
21669
  }, [wrappedOnNodeUpdate, wrappedOnNodesUpdate, onNodeUpdate, onNodesUpdate, currentCanvasRef, applyLaneSnap]);
21460
- const handleNodeDrop = (0, import_react25.useCallback)((sources, target) => {
21670
+ const handleNodeDrop = (0, import_react26.useCallback)((sources, target) => {
21461
21671
  onNodeDrop?.(sources, target, { canvasRef: currentCanvasRef });
21462
21672
  }, [onNodeDrop, currentCanvasRef]);
21463
- const { dragOverrides, dropTargetId, onPointerDown: onNodePointerDown, cancelDrag } = useNodeDrag({
21673
+ const { dragOverrides, dropTargetId, onPointerDown: onNodePointerDown, cancelDrag, isDragging } = useNodeDrag({
21464
21674
  viewport: viewportStateRef,
21465
21675
  nodesRef,
21466
21676
  onCommit: commitDragBatch,
21467
21677
  svgRef: svgProxyRef,
21468
21678
  canDropNodeOn,
21469
21679
  onNodeDrop: handleNodeDrop,
21470
- selectedIdsRef
21680
+ selectedIdsRef,
21681
+ snapGridSize
21471
21682
  });
21472
21683
  const { resizeOverrides, onHandlePointerDown: onResizeHandlePointerDown } = useNodeResize({
21473
21684
  viewport: viewportStateRef,
21474
- onCommit: commitResize
21685
+ onCommit: commitResize,
21686
+ snapGridSize
21687
+ });
21688
+ const alignmentGuides = useAlignmentGuides({
21689
+ dragOverrides,
21690
+ nodesRef,
21691
+ isDragging,
21692
+ threshold: guideThreshold ?? 4
21475
21693
  });
21476
21694
  const singleSelectedId = selectedIds.size === 1 ? Array.from(selectedIds)[0] : null;
21477
- const selectedResolvedNode = (0, import_react25.useMemo)(() => {
21695
+ const selectedResolvedNode = (0, import_react26.useMemo)(() => {
21478
21696
  if (!singleSelectedId)
21479
21697
  return null;
21480
21698
  const base = nodeMap.get(singleSelectedId);
@@ -21491,7 +21709,7 @@ var SystemCanvas = (() => {
21491
21709
  return base;
21492
21710
  }, [singleSelectedId, nodeMap, dragOverrides, resizeOverrides]);
21493
21711
  svgProxyRef.current = viewportHandleRef.current?.getSvgElement() ?? null;
21494
- const handleEdgeCreated = (0, import_react25.useCallback)((edge) => {
21712
+ const handleEdgeCreated = (0, import_react26.useCallback)((edge) => {
21495
21713
  wrappedOnEdgeAdd(edge, currentCanvasRef);
21496
21714
  }, [wrappedOnEdgeAdd, currentCanvasRef]);
21497
21715
  const { pending: pendingEdge, onHandlePointerDown: onConnectionHandlePointerDown } = useEdgeCreate({
@@ -21500,7 +21718,7 @@ var SystemCanvas = (() => {
21500
21718
  nodesRef,
21501
21719
  onCreate: handleEdgeCreated
21502
21720
  });
21503
- const handleNavigableNodeClick = (0, import_react25.useCallback)((node) => {
21721
+ const handleNavigableNodeClick = (0, import_react26.useCallback)((node) => {
21504
21722
  const frame2 = {
21505
21723
  parentCanvasRef: currentCanvasRef,
21506
21724
  parentNodeRect: {
@@ -21527,7 +21745,7 @@ var SystemCanvas = (() => {
21527
21745
  navigateToRef(node);
21528
21746
  }
21529
21747
  }, [navigateToRef, currentCanvasRef, zoomNavConfig.enabled]);
21530
- const handleZoomEnter = (0, import_react25.useCallback)((node, targetTransform) => {
21748
+ const handleZoomEnter = (0, import_react26.useCallback)((node, targetTransform) => {
21531
21749
  const frame2 = {
21532
21750
  parentCanvasRef: currentCanvasRef,
21533
21751
  parentNodeRect: {
@@ -21541,7 +21759,7 @@ var SystemCanvas = (() => {
21541
21759
  setPendingHandoff(targetTransform);
21542
21760
  navigateToRef(node);
21543
21761
  }, [currentCanvasRef, navigateToRef]);
21544
- const handleZoomExit = (0, import_react25.useCallback)((targetTransform) => {
21762
+ const handleZoomExit = (0, import_react26.useCallback)((targetTransform) => {
21545
21763
  setPendingHandoff(targetTransform);
21546
21764
  suppressNextHandoffClearRef.current = true;
21547
21765
  navigateToBreadcrumb(breadcrumbs.length - 2);
@@ -21568,37 +21786,70 @@ var SystemCanvas = (() => {
21568
21786
  onEnter: handleZoomEnter,
21569
21787
  onExit: handleZoomExit
21570
21788
  });
21571
- const handleViewportChange = (0, import_react25.useCallback)((vp) => {
21789
+ const handleViewportChange = (0, import_react26.useCallback)((vp) => {
21572
21790
  viewportStateRef.current = vp;
21573
21791
  handleZoomNavViewportChange(vp);
21574
21792
  onViewportChange?.(vp);
21575
21793
  }, [handleZoomNavViewportChange, onViewportChange]);
21576
- const handleHandoffApplied = (0, import_react25.useCallback)(() => {
21794
+ const handleHandoffApplied = (0, import_react26.useCallback)(() => {
21577
21795
  setPendingHandoff(null);
21578
21796
  clearZoomNavCommitting();
21579
21797
  }, [clearZoomNavCommitting]);
21580
- const handleBeginEdit = (0, import_react25.useCallback)((node) => {
21798
+ const handleBeginEdit = (0, import_react26.useCallback)((node) => {
21581
21799
  setEditingId(node.id);
21582
21800
  }, []);
21583
- const handleBeginEditEdge = (0, import_react25.useCallback)((edge) => {
21801
+ const handleBeginEditEdge = (0, import_react26.useCallback)((edge) => {
21584
21802
  setEditingEdgeId(edge.id);
21585
21803
  }, []);
21586
- const [contextMenuState, setContextMenuState] = (0, import_react25.useState)(null);
21587
- (0, import_react25.useEffect)(() => {
21804
+ const handleAlign = (0, import_react26.useCallback)((direction) => {
21805
+ const selectedNodes = Array.from(selectedIds).map((id2) => nodesRef.current.find((n) => n.id === id2)).filter((n) => n != null);
21806
+ const updates = alignNodes(selectedNodes, direction);
21807
+ if (updates.length > 0)
21808
+ wrappedOnNodesUpdate(updates, currentCanvasRef);
21809
+ }, [selectedIds, nodesRef, wrappedOnNodesUpdate, currentCanvasRef]);
21810
+ const handleDistribute = (0, import_react26.useCallback)((axis) => {
21811
+ const selectedNodes = Array.from(selectedIds).map((id2) => nodesRef.current.find((n) => n.id === id2)).filter((n) => n != null);
21812
+ const updates = distributeNodes(selectedNodes, axis);
21813
+ if (updates.length > 0)
21814
+ wrappedOnNodesUpdate(updates, currentCanvasRef);
21815
+ }, [selectedIds, nodesRef, wrappedOnNodesUpdate, currentCanvasRef]);
21816
+ const [contextMenuState, setContextMenuState] = (0, import_react26.useState)(null);
21817
+ (0, import_react26.useEffect)(() => {
21588
21818
  setContextMenuState(null);
21589
21819
  }, [currentCanvasRef]);
21590
- const handleContextMenu = (0, import_react25.useCallback)((event) => {
21820
+ const handleContextMenu = (0, import_react26.useCallback)((event) => {
21591
21821
  onContextMenu?.(event);
21592
- if (!nodeContextMenu)
21822
+ if (!nodeContextMenu && !alignDistributeMenu)
21593
21823
  return;
21594
21824
  if (event.type !== "node")
21595
21825
  return;
21596
21826
  const node = event.target;
21597
21827
  if (!node)
21598
21828
  return;
21599
- const matched = filterContextMenuItems(nodeContextMenu.items, node, {
21600
- canvasRef: currentCanvasRef ?? null
21601
- });
21829
+ const matchCtx = { canvasRef: currentCanvasRef ?? null };
21830
+ const matched = nodeContextMenu ? filterContextMenuItems(nodeContextMenu.items, node, matchCtx) : [];
21831
+ if (alignDistributeMenu && selectedIds.size >= 2 && selectedIds.has(node.id)) {
21832
+ const canDistribute = selectedIds.size >= 3;
21833
+ const builtins = [
21834
+ { id: "__sys_align_left__", label: "Align Left" },
21835
+ { id: "__sys_align_right__", label: "Align Right" },
21836
+ { id: "__sys_align_top__", label: "Align Top" },
21837
+ { id: "__sys_align_bottom__", label: "Align Bottom" },
21838
+ { id: "__sys_align_centerH__", label: "Align Center Horizontal" },
21839
+ { id: "__sys_align_centerV__", label: "Align Center Vertical" },
21840
+ {
21841
+ id: "__sys_distribute_h__",
21842
+ label: "Distribute Horizontally",
21843
+ disabled: canDistribute ? void 0 : () => !canDistribute
21844
+ },
21845
+ {
21846
+ id: "__sys_distribute_v__",
21847
+ label: "Distribute Vertically",
21848
+ disabled: canDistribute ? void 0 : () => !canDistribute
21849
+ }
21850
+ ];
21851
+ matched.push(...builtins);
21852
+ }
21602
21853
  if (matched.length === 0) {
21603
21854
  setContextMenuState(null);
21604
21855
  return;
@@ -21609,7 +21860,48 @@ var SystemCanvas = (() => {
21609
21860
  screenPosition: event.screenPosition,
21610
21861
  canvasRef: currentCanvasRef ?? null
21611
21862
  });
21612
- }, [onContextMenu, nodeContextMenu, currentCanvasRef]);
21863
+ }, [onContextMenu, nodeContextMenu, currentCanvasRef, alignDistributeMenu, selectedIds]);
21864
+ const effectiveNodeContextMenu = (0, import_react26.useMemo)(() => {
21865
+ if (!nodeContextMenu && !alignDistributeMenu)
21866
+ return null;
21867
+ const baseItems = nodeContextMenu?.items ?? [];
21868
+ const wrappedOnSelect = (itemId, node, ctx) => {
21869
+ if (itemId === "__sys_align_left__") {
21870
+ handleAlign("left");
21871
+ return;
21872
+ }
21873
+ if (itemId === "__sys_align_right__") {
21874
+ handleAlign("right");
21875
+ return;
21876
+ }
21877
+ if (itemId === "__sys_align_top__") {
21878
+ handleAlign("top");
21879
+ return;
21880
+ }
21881
+ if (itemId === "__sys_align_bottom__") {
21882
+ handleAlign("bottom");
21883
+ return;
21884
+ }
21885
+ if (itemId === "__sys_align_centerH__") {
21886
+ handleAlign("centerH");
21887
+ return;
21888
+ }
21889
+ if (itemId === "__sys_align_centerV__") {
21890
+ handleAlign("centerV");
21891
+ return;
21892
+ }
21893
+ if (itemId === "__sys_distribute_h__") {
21894
+ handleDistribute("horizontal");
21895
+ return;
21896
+ }
21897
+ if (itemId === "__sys_distribute_v__") {
21898
+ handleDistribute("vertical");
21899
+ return;
21900
+ }
21901
+ nodeContextMenu?.onSelect(itemId, node, ctx);
21902
+ };
21903
+ return { items: baseItems, onSelect: wrappedOnSelect };
21904
+ }, [nodeContextMenu, alignDistributeMenu, handleAlign, handleDistribute]);
21613
21905
  const { handleNodeClick, handleNodeDoubleClick, handleNodeNavigate, handleEdgeClick, handleEdgeDoubleClick, handleCanvasClick, handleCanvasContextMenu, handleNodeContextMenu, handleEdgeContextMenu } = useCanvasInteraction({
21614
21906
  onNodeClick,
21615
21907
  onNodeDoubleClick,
@@ -21630,27 +21922,27 @@ var SystemCanvas = (() => {
21630
21922
  onSelectEdge: setSelectedEdgeId,
21631
21923
  onBeginEditEdge: handleBeginEditEdge
21632
21924
  });
21633
- const handleEditorCommit = (0, import_react25.useCallback)((patch) => {
21925
+ const handleEditorCommit = (0, import_react26.useCallback)((patch) => {
21634
21926
  if (editingId) {
21635
21927
  wrappedOnNodeUpdate(editingId, patch, currentCanvasRef);
21636
21928
  }
21637
21929
  setEditingId(null);
21638
21930
  }, [editingId, wrappedOnNodeUpdate, currentCanvasRef]);
21639
- const handleEditorCancel = (0, import_react25.useCallback)(() => {
21931
+ const handleEditorCancel = (0, import_react26.useCallback)(() => {
21640
21932
  setEditingId(null);
21641
21933
  }, []);
21642
- const handleEdgeEditorCommit = (0, import_react25.useCallback)((patch) => {
21934
+ const handleEdgeEditorCommit = (0, import_react26.useCallback)((patch) => {
21643
21935
  if (editingEdgeId) {
21644
21936
  wrappedOnEdgeUpdate(editingEdgeId, patch, currentCanvasRef);
21645
21937
  }
21646
21938
  setEditingEdgeId(null);
21647
21939
  }, [editingEdgeId, wrappedOnEdgeUpdate, currentCanvasRef]);
21648
- const handleEdgeEditorCancel = (0, import_react25.useCallback)(() => {
21940
+ const handleEdgeEditorCancel = (0, import_react26.useCallback)(() => {
21649
21941
  setEditingEdgeId(null);
21650
21942
  }, []);
21651
- const lastAddRef = (0, import_react25.useRef)(null);
21652
- const menuOptions = (0, import_react25.useMemo)(() => getNodeMenuOptions(currentCanvas, theme), [currentCanvas, theme]);
21653
- const addNode2 = (0, import_react25.useCallback)((option, position) => {
21943
+ const lastAddRef = (0, import_react26.useRef)(null);
21944
+ const menuOptions = (0, import_react26.useMemo)(() => getNodeMenuOptions(currentCanvas, theme), [currentCanvas, theme]);
21945
+ const addNode2 = (0, import_react26.useCallback)((option, position) => {
21654
21946
  let x, y;
21655
21947
  if (position) {
21656
21948
  x = position.x;
@@ -21717,14 +22009,14 @@ var SystemCanvas = (() => {
21717
22009
  cancelDrag
21718
22010
  });
21719
22011
  const renderProps = { options: menuOptions, addNode: addNode2, theme };
21720
- return (0, import_jsx_runtime28.jsxs)("div", { ref: containerRef, className: `system-canvas ${className ?? ""}`, tabIndex: editable ? 0 : -1, onKeyDown: handleKeyDown, style: {
22012
+ return (0, import_jsx_runtime29.jsxs)("div", { ref: containerRef, className: `system-canvas ${className ?? ""}`, tabIndex: editable ? 0 : -1, onKeyDown: handleKeyDown, style: {
21721
22013
  position: "relative",
21722
22014
  width: "100%",
21723
22015
  height: "100%",
21724
22016
  overflow: "hidden",
21725
22017
  outline: "none",
21726
22018
  ...style
21727
- }, 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: {
22019
+ }, children: [(0, import_jsx_runtime29.jsx)(Breadcrumbs, { breadcrumbs, theme: theme.breadcrumbs, onNavigate: navigateToBreadcrumb }), isLoading && (0, import_jsx_runtime29.jsx)("div", { className: "system-canvas-loading", style: {
21728
22020
  position: "absolute",
21729
22021
  top: 12,
21730
22022
  right: 12,
@@ -21736,7 +22028,7 @@ var SystemCanvas = (() => {
21736
22028
  fontFamily: theme.node.fontFamily,
21737
22029
  fontSize: 12,
21738
22030
  backdropFilter: "blur(8px)"
21739
- }, 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, selectedIds: editable ? selectedIds : void 0, marqueeRect: editable ? marqueeRect : null, marqueeActiveRef: editable ? marqueeActiveRef : void 0, 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 && selectedIds.size === 1 && selectedResolvedNode && !editingId && (0, import_jsx_runtime28.jsx)(NodeToolbar, { node: selectedResolvedNode, theme, onPatch: (update) => {
22031
+ }, children: "Loading..." }), (0, import_jsx_runtime29.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, selectedIds: editable ? selectedIds : void 0, marqueeRect: editable ? marqueeRect : null, marqueeActiveRef: editable ? marqueeActiveRef : void 0, 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, alignmentGuides: editable ? alignmentGuides : void 0 }), showLaneHeaders && (0, import_jsx_runtime29.jsx)(LaneHeaders, { columns: currentCanvas.columns, rows: currentCanvas.rows, theme, getViewport: getViewportState, width: containerSize.width, height: containerSize.height, pinned: laneHeaders === "pinned" }), editable && showNodeToolbar && selectedIds.size === 1 && selectedResolvedNode && !editingId && (0, import_jsx_runtime29.jsx)(NodeToolbar, { node: selectedResolvedNode, theme, onPatch: (update) => {
21740
22032
  wrappedOnNodeUpdate(selectedResolvedNode.id, update, currentCanvasRef);
21741
22033
  }, onDelete: () => {
21742
22034
  wrappedOnNodeDelete(selectedResolvedNode.id, currentCanvasRef);
@@ -21746,7 +22038,7 @@ var SystemCanvas = (() => {
21746
22038
  if (selectedResolvedNodes.length === 0)
21747
22039
  return null;
21748
22040
  const anchorNode = selectedResolvedNodes[0];
21749
- return (0, import_jsx_runtime28.jsx)(NodeToolbar, { node: anchorNode, selectedNodes: selectedResolvedNodes, theme, onPatch: () => {
22041
+ return (0, import_jsx_runtime29.jsx)(NodeToolbar, { node: anchorNode, selectedNodes: selectedResolvedNodes, theme, onPatch: () => {
21750
22042
  }, onMultiPatch: (patch) => {
21751
22043
  for (const id2 of selectedIds) {
21752
22044
  wrappedOnNodeUpdate(id2, patch, currentCanvasRef);
@@ -21754,8 +22046,8 @@ var SystemCanvas = (() => {
21754
22046
  }, onDelete: () => {
21755
22047
  wrappedOnNodesDelete(Array.from(selectedIds), currentCanvasRef);
21756
22048
  clearSelection();
21757
- }, getViewport: getViewportState, containerWidth: containerSize.width, containerHeight: containerSize.height });
21758
- })(), 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) })] });
22049
+ }, getViewport: getViewportState, containerWidth: containerSize.width, containerHeight: containerSize.height, onAlign: handleAlign, onDistribute: handleDistribute });
22050
+ })(), editable && (renderAddNodeButton ? renderAddNodeButton(renderProps) : (0, import_jsx_runtime29.jsx)(AddNodeButton, { ...renderProps })), effectiveNodeContextMenu && (0, import_jsx_runtime29.jsx)(NodeContextMenuOverlay, { state: contextMenuState, config: effectiveNodeContextMenu, theme, onClose: () => setContextMenuState(null) })] });
21759
22051
  });
21760
22052
 
21761
22053
  // src/index.tsx
@@ -21866,7 +22158,7 @@ var SystemCanvas = (() => {
21866
22158
  onEdgeUpdate: handleEdgeUpdate,
21867
22159
  onEdgeDelete: handleEdgeDelete
21868
22160
  };
21869
- root2.render(import_react26.default.createElement(SystemCanvas, props));
22161
+ root2.render(import_react27.default.createElement(SystemCanvas, props));
21870
22162
  };
21871
22163
  doRender();
21872
22164
  return {