@particle-academy/fancy-flow 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4871,6 +4871,28 @@ var addEdge = (edgeParams, edges, options = {}) => {
4871
4871
  }
4872
4872
  return edges.concat(edge);
4873
4873
  };
4874
+ var reconnectEdge = (oldEdge, newConnection, edges, options = { shouldReplaceId: true }) => {
4875
+ const { id: oldEdgeId, ...rest } = oldEdge;
4876
+ if (!newConnection.source || !newConnection.target) {
4877
+ options.onError?.("006", errorMessages["error006"]());
4878
+ return edges;
4879
+ }
4880
+ const foundEdge = edges.find((e) => e.id === oldEdge.id);
4881
+ if (!foundEdge) {
4882
+ options.onError?.("007", errorMessages["error007"](oldEdgeId));
4883
+ return edges;
4884
+ }
4885
+ const edgeIdGenerator = options.getEdgeId || getEdgeId;
4886
+ const edge = {
4887
+ ...rest,
4888
+ id: options.shouldReplaceId ? edgeIdGenerator(newConnection) : oldEdgeId,
4889
+ source: newConnection.source,
4890
+ target: newConnection.target,
4891
+ sourceHandle: newConnection.sourceHandle,
4892
+ targetHandle: newConnection.targetHandle
4893
+ };
4894
+ return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
4895
+ };
4874
4896
  function getStraightPath({ sourceX, sourceY, targetX, targetY }) {
4875
4897
  const [labelX, labelY, offsetX, offsetY] = getEdgeCenter({
4876
4898
  sourceX,
@@ -7509,6 +7531,12 @@ function addEdge2(edgeParams, edges, options = {}) {
7509
7531
  onError: options.onError ?? defaultOnError
7510
7532
  });
7511
7533
  }
7534
+ function reconnectEdge2(oldEdge, newConnection, edges, options = { shouldReplaceId: true }) {
7535
+ return reconnectEdge(oldEdge, newConnection, edges, {
7536
+ ...options,
7537
+ onError: options.onError ?? defaultOnError
7538
+ });
7539
+ }
7512
7540
  var isNode = (element) => isNodeBase(element);
7513
7541
  var isEdge = (element) => isEdgeBase(element);
7514
7542
  function fixedForwardRef(render) {
@@ -10996,8 +11024,117 @@ function useFlowState(initial) {
10996
11024
  const onConnect = ReactExports.useCallback((connection) => {
10997
11025
  setEdges((es) => addEdge2(connection, es));
10998
11026
  }, []);
11027
+ const setGraph = ReactExports.useCallback((graph) => {
11028
+ setNodes(graph.nodes);
11029
+ setEdges(graph.edges);
11030
+ }, []);
10999
11031
  const toGraph = ReactExports.useCallback(() => ({ nodes, edges }), [nodes, edges]);
11000
- return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
11032
+ return { nodes, edges, setNodes, setEdges, setGraph, onNodesChange, onEdgesChange, onConnect, toGraph };
11033
+ }
11034
+
11035
+ // src/runtime/history.ts
11036
+ function createHistory(limit = 100) {
11037
+ let past = [];
11038
+ let future = [];
11039
+ return {
11040
+ push(graph) {
11041
+ past.push(graph);
11042
+ if (past.length > limit) past.shift();
11043
+ future = [];
11044
+ },
11045
+ undo(current) {
11046
+ const prev = past.pop();
11047
+ if (prev === void 0) return null;
11048
+ future.push(current);
11049
+ return prev;
11050
+ },
11051
+ redo(current) {
11052
+ const next = future.pop();
11053
+ if (next === void 0) return null;
11054
+ past.push(current);
11055
+ return next;
11056
+ },
11057
+ canUndo: () => past.length > 0,
11058
+ canRedo: () => future.length > 0,
11059
+ clear() {
11060
+ past = [];
11061
+ future = [];
11062
+ },
11063
+ size: () => ({ past: past.length, future: future.length })
11064
+ };
11065
+ }
11066
+
11067
+ // src/runtime/use-flow-history.ts
11068
+ function useFlowHistory(flow) {
11069
+ const history = ReactExports.useRef(createHistory()).current;
11070
+ const restoring = ReactExports.useRef(false);
11071
+ const coalesced = ReactExports.useRef(false);
11072
+ const [, bump] = ReactExports.useState(0);
11073
+ const rerender = ReactExports.useCallback(() => bump((n) => n + 1), []);
11074
+ const graphRef = ReactExports.useRef({ nodes: flow.nodes, edges: flow.edges });
11075
+ graphRef.current = { nodes: flow.nodes, edges: flow.edges };
11076
+ const capture = ReactExports.useCallback(() => {
11077
+ if (restoring.current || coalesced.current) return;
11078
+ coalesced.current = true;
11079
+ queueMicrotask(() => {
11080
+ coalesced.current = false;
11081
+ });
11082
+ history.push(graphRef.current);
11083
+ rerender();
11084
+ }, [history, rerender]);
11085
+ const restore = ReactExports.useCallback(
11086
+ (g) => {
11087
+ if (!g) return;
11088
+ restoring.current = true;
11089
+ flow.setGraph(g);
11090
+ queueMicrotask(() => {
11091
+ restoring.current = false;
11092
+ });
11093
+ rerender();
11094
+ },
11095
+ [flow, rerender]
11096
+ );
11097
+ const undo = ReactExports.useCallback(() => restore(history.undo(graphRef.current)), [history, restore]);
11098
+ const redo = ReactExports.useCallback(() => restore(history.redo(graphRef.current)), [history, restore]);
11099
+ const wrapped = ReactExports.useMemo(
11100
+ () => ({
11101
+ ...flow,
11102
+ setNodes: (next) => {
11103
+ capture();
11104
+ flow.setNodes(next);
11105
+ },
11106
+ setEdges: (next) => {
11107
+ capture();
11108
+ flow.setEdges(next);
11109
+ },
11110
+ setGraph: (g) => {
11111
+ capture();
11112
+ flow.setGraph(g);
11113
+ },
11114
+ onNodesChange: (changes) => {
11115
+ if (!restoring.current && changes.some((c) => c.type === "remove")) capture();
11116
+ flow.onNodesChange(changes);
11117
+ },
11118
+ onEdgesChange: (changes) => {
11119
+ if (!restoring.current && changes.some((c) => c.type === "remove")) capture();
11120
+ flow.onEdgesChange(changes);
11121
+ },
11122
+ onConnect: (conn) => {
11123
+ capture();
11124
+ flow.onConnect(conn);
11125
+ }
11126
+ }),
11127
+ [flow, capture]
11128
+ );
11129
+ return {
11130
+ flow: wrapped,
11131
+ undo,
11132
+ redo,
11133
+ canUndo: history.canUndo(),
11134
+ canRedo: history.canRedo(),
11135
+ capture,
11136
+ onNodeDragStart: capture
11137
+ };
11001
11138
  }
11002
11139
  function useFlowRun({ maxFeed = 200 } = {}) {
11003
11140
  const [statuses, setStatuses] = ReactExports.useState({});
@@ -11414,6 +11551,65 @@ function duplicateNode(node, id2, offset = 40) {
11414
11551
  data: JSON.parse(JSON.stringify(node.data ?? {}))
11415
11552
  };
11416
11553
  }
11554
+ function cloneSubgraph(nodes, edges, opts) {
11555
+ const offset = opts.offset ?? 40;
11556
+ const idMap = /* @__PURE__ */ new Map();
11557
+ for (const n of nodes) idMap.set(n.id, opts.makeId());
11558
+ const clonedNodes = nodes.map((n) => {
11559
+ const cloned = duplicateNode(n, idMap.get(n.id), offset);
11560
+ const parentId = n.parentId;
11561
+ if (parentId && idMap.has(parentId)) cloned.parentId = idMap.get(parentId);
11562
+ else if (parentId) delete cloned.parentId;
11563
+ return cloned;
11564
+ });
11565
+ const clonedEdges = edges.filter((e) => idMap.has(e.source) && idMap.has(e.target)).map((e) => ({ ...e, id: opts.makeId(), source: idMap.get(e.source), target: idMap.get(e.target) }));
11566
+ return { nodes: clonedNodes, edges: clonedEdges, idMap };
11567
+ }
11568
+ function reconnectEdge3(edges, oldEdge, newConnection) {
11569
+ return reconnectEdge2(oldEdge, newConnection, edges, { shouldReplaceId: false });
11570
+ }
11571
+ var nodeW = (n) => n.width ?? n.measured?.width ?? 0;
11572
+ var nodeH = (n) => n.height ?? n.measured?.height ?? 0;
11573
+ function alignNodes(nodes, edge) {
11574
+ if (nodes.length < 2) return nodes;
11575
+ const minL = Math.min(...nodes.map((n) => n.position.x));
11576
+ const maxR = Math.max(...nodes.map((n) => n.position.x + nodeW(n)));
11577
+ const minT = Math.min(...nodes.map((n) => n.position.y));
11578
+ const maxB = Math.max(...nodes.map((n) => n.position.y + nodeH(n)));
11579
+ const cx = (minL + maxR) / 2;
11580
+ const cy = (minT + maxB) / 2;
11581
+ return nodes.map((n) => {
11582
+ let { x, y } = n.position;
11583
+ if (edge === "left") x = minL;
11584
+ else if (edge === "right") x = maxR - nodeW(n);
11585
+ else if (edge === "hcenter") x = cx - nodeW(n) / 2;
11586
+ else if (edge === "top") y = minT;
11587
+ else if (edge === "bottom") y = maxB - nodeH(n);
11588
+ else if (edge === "vcenter") y = cy - nodeH(n) / 2;
11589
+ return { ...n, position: { x, y } };
11590
+ });
11591
+ }
11592
+ function distributeNodes(nodes, axis) {
11593
+ if (nodes.length < 3) return nodes;
11594
+ const size = (n) => axis === "h" ? nodeW(n) : nodeH(n);
11595
+ const coord = (n) => axis === "h" ? n.position.x : n.position.y;
11596
+ const sorted = [...nodes].sort((a, b) => coord(a) - coord(b));
11597
+ const start2 = coord(sorted[0]);
11598
+ const last = sorted[sorted.length - 1];
11599
+ const end = coord(last) + size(last);
11600
+ const totalSize = sorted.reduce((s, n) => s + size(n), 0);
11601
+ const gap = (end - start2 - totalSize) / (sorted.length - 1);
11602
+ const posById = /* @__PURE__ */ new Map();
11603
+ let cursor = start2;
11604
+ for (const n of sorted) {
11605
+ posById.set(n.id, cursor);
11606
+ cursor += size(n) + gap;
11607
+ }
11608
+ return nodes.map((n) => {
11609
+ const p = posById.get(n.id);
11610
+ return { ...n, position: axis === "h" ? { ...n.position, x: p } : { ...n.position, y: p } };
11611
+ });
11612
+ }
11417
11613
  var FlowEditorContext = ReactExports.createContext(null);
11418
11614
  var FlowEditorProvider = FlowEditorContext.Provider;
11419
11615
  function useFlowEditor() {
@@ -11453,13 +11649,16 @@ function FlowEditorInner({
11453
11649
  onSelectionChange,
11454
11650
  onDelete,
11455
11651
  onEdgeDelete,
11652
+ confirmDelete,
11456
11653
  apiRef
11457
11654
  }) {
11458
11655
  const internal = useFlowState(initial);
11459
11656
  const runner = useFlowRun();
11460
11657
  const rf = useReactFlow();
11461
11658
  const controlled = value !== void 0;
11462
- const flow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
11659
+ const baseFlow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
11660
+ const hist = useFlowHistory(baseFlow);
11661
+ const flow = hist.flow;
11463
11662
  const [, force] = ReactExports.useState(0);
11464
11663
  ReactExports.useEffect(() => onNodeKindsChanged(() => force((n) => n + 1)), []);
11465
11664
  const nodeTypes = ReactExports.useMemo(() => buildNodeTypes(), [listNodeKinds().length]);
@@ -11512,6 +11711,23 @@ function FlowEditorInner({
11512
11711
  window.removeEventListener("keydown", onKey);
11513
11712
  };
11514
11713
  }, [menu, labelEdit]);
11714
+ ReactExports.useEffect(() => {
11715
+ const onKey = (e) => {
11716
+ const t = e.target;
11717
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
11718
+ if (!(e.ctrlKey || e.metaKey)) return;
11719
+ const key = e.key.toLowerCase();
11720
+ if (key === "z" && !e.shiftKey) {
11721
+ e.preventDefault();
11722
+ hist.undo();
11723
+ } else if (key === "z" && e.shiftKey || key === "y") {
11724
+ e.preventDefault();
11725
+ hist.redo();
11726
+ }
11727
+ };
11728
+ window.addEventListener("keydown", onKey);
11729
+ return () => window.removeEventListener("keydown", onKey);
11730
+ }, [hist]);
11515
11731
  ReactExports.useEffect(() => {
11516
11732
  if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
11517
11733
  }, [flow.nodes, flow.edges, onChange, controlled]);
@@ -11536,16 +11752,116 @@ function FlowEditorInner({
11536
11752
  [flow]
11537
11753
  );
11538
11754
  const deleteNodes = ReactExports.useCallback(
11539
- (ids) => {
11755
+ async (ids) => {
11540
11756
  if (ids.length === 0) return;
11757
+ const targets = flow.nodes.filter((n) => ids.includes(n.id));
11758
+ if (targets.length === 0) return;
11759
+ if (confirmDelete) {
11760
+ const attached = flow.edges.filter((e) => ids.includes(e.source) || ids.includes(e.target));
11761
+ if (!await confirmDelete({ nodes: targets, edges: attached })) return;
11762
+ }
11541
11763
  const doomed = new Set(ids);
11542
- flow.setNodes((all) => removeNodes({ nodes: all, edges: [] }, ids).nodes);
11543
- flow.setEdges((all) => removeNodes({ nodes: [], edges: all }, ids).edges);
11764
+ flow.setGraph(removeNodes({ nodes: flow.nodes, edges: flow.edges }, ids));
11544
11765
  setSelectedId((cur) => cur !== null && doomed.has(cur) ? null : cur);
11545
11766
  onDelete?.(ids);
11546
11767
  },
11547
- [flow, onDelete]
11768
+ [flow, onDelete, confirmDelete]
11769
+ );
11770
+ const deleteEdges = ReactExports.useCallback(
11771
+ async (ids) => {
11772
+ if (ids.length === 0) return;
11773
+ const targets = flow.edges.filter((e) => ids.includes(e.id));
11774
+ if (targets.length === 0) return;
11775
+ if (confirmDelete && !await confirmDelete({ nodes: [], edges: targets })) return;
11776
+ flow.setEdges((all) => removeEdges(all, ids));
11777
+ setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
11778
+ onEdgeDelete?.(ids);
11779
+ },
11780
+ [flow, onEdgeDelete, confirmDelete]
11781
+ );
11782
+ const selectedNodes = ReactExports.useMemo(() => flow.nodes.filter((n) => n.selected), [flow.nodes]);
11783
+ const selectedIds = ReactExports.useMemo(() => selectedNodes.map((n) => n.id), [selectedNodes]);
11784
+ const clipboard = ReactExports.useRef(null);
11785
+ const copySelection = ReactExports.useCallback(() => {
11786
+ const sel = flow.nodes.filter((n) => n.selected);
11787
+ if (sel.length === 0) return;
11788
+ const ids = new Set(sel.map((n) => n.id));
11789
+ const edges = flow.edges.filter((e) => ids.has(e.source) && ids.has(e.target));
11790
+ clipboard.current = { nodes: sel.map((n) => ({ ...n })), edges: edges.map((e) => ({ ...e })) };
11791
+ }, [flow]);
11792
+ const pasteClipboard = ReactExports.useCallback(
11793
+ (at) => {
11794
+ const clip = clipboard.current;
11795
+ if (!clip || clip.nodes.length === 0) return;
11796
+ const { nodes: clones, edges: cloneEdges } = cloneSubgraph(clip.nodes, clip.edges, { makeId: newNodeId, offset: 40 });
11797
+ let placed = clones;
11798
+ if (at) {
11799
+ const minX = Math.min(...clones.map((n) => n.position.x));
11800
+ const minY = Math.min(...clones.map((n) => n.position.y));
11801
+ placed = clones.map((n) => ({ ...n, position: { x: n.position.x - minX + at.x, y: n.position.y - minY + at.y } }));
11802
+ }
11803
+ flow.setGraph({
11804
+ nodes: [...flow.nodes.map((n) => ({ ...n, selected: false })), ...placed.map((n) => ({ ...n, selected: true }))],
11805
+ edges: [...flow.edges, ...cloneEdges]
11806
+ });
11807
+ },
11808
+ [flow]
11809
+ );
11810
+ const duplicateSelected = ReactExports.useCallback(() => {
11811
+ const sel = flow.nodes.filter((n) => n.selected);
11812
+ if (sel.length === 0) return;
11813
+ const ids = new Set(sel.map((n) => n.id));
11814
+ const internal2 = flow.edges.filter((e) => ids.has(e.source) && ids.has(e.target));
11815
+ const { nodes: clones, edges: cloneEdges } = cloneSubgraph(sel, internal2, { makeId: newNodeId, offset: 40 });
11816
+ flow.setGraph({
11817
+ nodes: [...flow.nodes.map((n) => ({ ...n, selected: false })), ...clones.map((n) => ({ ...n, selected: true }))],
11818
+ edges: [...flow.edges, ...cloneEdges]
11819
+ });
11820
+ }, [flow]);
11821
+ const alignSelected = ReactExports.useCallback(
11822
+ (edge) => {
11823
+ const sel = flow.nodes.filter((n) => n.selected);
11824
+ if (sel.length < 2) return;
11825
+ const byId = new Map(alignNodes(sel, edge).map((n) => [n.id, n]));
11826
+ flow.setNodes((all) => all.map((n) => byId.get(n.id) ?? n));
11827
+ },
11828
+ [flow]
11829
+ );
11830
+ const distributeSelected = ReactExports.useCallback(
11831
+ (axis) => {
11832
+ const sel = flow.nodes.filter((n) => n.selected);
11833
+ if (sel.length < 3) return;
11834
+ const byId = new Map(distributeNodes(sel, axis).map((n) => [n.id, n]));
11835
+ flow.setNodes((all) => all.map((n) => byId.get(n.id) ?? n));
11836
+ },
11837
+ [flow]
11838
+ );
11839
+ const onReconnect = ReactExports.useCallback(
11840
+ (oldEdge, conn) => flow.setEdges((eds) => reconnectEdge3(eds, oldEdge, conn)),
11841
+ [flow]
11548
11842
  );
11843
+ ReactExports.useEffect(() => {
11844
+ const onKey = (e) => {
11845
+ const t = e.target;
11846
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
11847
+ if (!(e.ctrlKey || e.metaKey)) return;
11848
+ const key = e.key.toLowerCase();
11849
+ if (key === "c") copySelection();
11850
+ else if (key === "x") {
11851
+ e.preventDefault();
11852
+ copySelection();
11853
+ deleteNodes(selectedIds);
11854
+ } else if (key === "v") {
11855
+ e.preventDefault();
11856
+ pasteClipboard();
11857
+ } else if (key === "d") {
11858
+ e.preventDefault();
11859
+ duplicateSelected();
11860
+ }
11861
+ };
11862
+ window.addEventListener("keydown", onKey);
11863
+ return () => window.removeEventListener("keydown", onKey);
11864
+ }, [copySelection, pasteClipboard, duplicateSelected, deleteNodes, selectedIds]);
11549
11865
  const api = ReactExports.useMemo(() => {
11550
11866
  const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
11551
11867
  return {
@@ -11556,6 +11872,8 @@ function FlowEditorInner({
11556
11872
  selected: selected2,
11557
11873
  selectedEdgeId,
11558
11874
  selectedEdge,
11875
+ selectedIds,
11876
+ selectedNodes,
11559
11877
  running: runner.running,
11560
11878
  statuses: runner.statuses,
11561
11879
  select: setSelectedId,
@@ -11564,18 +11882,8 @@ function FlowEditorInner({
11564
11882
  updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
11565
11883
  deleteNodes,
11566
11884
  deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
11567
- deleteEdges: (ids) => {
11568
- if (ids.length === 0) return;
11569
- flow.setEdges((all) => removeEdges(all, ids));
11570
- setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
11571
- onEdgeDelete?.(ids);
11572
- },
11573
- deleteSelectedEdge: () => {
11574
- if (!selectedEdgeId) return;
11575
- flow.setEdges((all) => removeEdges(all, [selectedEdgeId]));
11576
- setSelectedEdgeId(null);
11577
- onEdgeDelete?.([selectedEdgeId]);
11578
- },
11885
+ deleteEdges,
11886
+ deleteSelectedEdge: () => deleteEdges(selectedEdgeId ? [selectedEdgeId] : []),
11579
11887
  setEdgeLabel: (id2, label) => flow.setEdges((all) => setEdgeLabel(all, id2, label)),
11580
11888
  duplicateNode: (id2) => {
11581
11889
  const src = flow.nodes.find((n) => n.id === id2);
@@ -11585,22 +11893,29 @@ function FlowEditorInner({
11585
11893
  setSelectedId(copy.id);
11586
11894
  return copy.id;
11587
11895
  },
11588
- setGraph: (graph) => {
11589
- flow.setNodes(graph.nodes);
11590
- flow.setEdges(graph.edges);
11896
+ setGraph: (graph) => flow.setGraph(graph),
11897
+ duplicateSelected,
11898
+ alignSelected,
11899
+ distributeSelected,
11900
+ copy: copySelection,
11901
+ cut: () => {
11902
+ copySelection();
11903
+ deleteNodes(selectedIds);
11591
11904
  },
11905
+ paste: pasteClipboard,
11592
11906
  run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
11593
11907
  cancel: runner.cancel,
11594
11908
  reset: runner.reset,
11595
11909
  toWorkflow,
11596
11910
  exportWorkflow: () => downloadWorkflow(toWorkflow(), metadata),
11597
- importWorkflow: () => pickWorkflow((graph) => {
11598
- flow.setNodes(graph.nodes);
11599
- flow.setEdges(graph.edges);
11600
- }),
11601
- fitView: () => rf.fitView({ padding: 0.2 })
11911
+ importWorkflow: () => pickWorkflow((graph) => flow.setGraph(graph)),
11912
+ fitView: () => rf.fitView({ padding: 0.2 }),
11913
+ undo: hist.undo,
11914
+ redo: hist.redo,
11915
+ canUndo: hist.canUndo,
11916
+ canRedo: hist.canRedo
11602
11917
  };
11603
- }, [flow, selectedId, selected2, runner, executors, metadata, addNode, deleteNodes, rf]);
11918
+ }, [flow, selectedId, selected2, selectedEdgeId, selectedEdge, selectedIds, selectedNodes, runner, executors, metadata, addNode, deleteNodes, deleteEdges, duplicateSelected, alignSelected, distributeSelected, copySelection, pasteClipboard, hist, rf]);
11604
11919
  ReactExports.useImperativeHandle(apiRef, () => api, [api]);
11605
11920
  const dropHandlers = paletteDropHandlers((kindName, evt) => {
11606
11921
  const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
@@ -11611,6 +11926,31 @@ function FlowEditorInner({
11611
11926
  const toolbar = slots.toolbar ? slots.toolbar(api) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11612
11927
  startActions.map((a) => renderAction(a, api)),
11613
11928
  builtins.run !== false && /* @__PURE__ */ jsxRuntime.jsx(FlowRunControls, { running: api.running, onRun: api.run, onCancel: api.cancel, onReset: api.reset }),
11929
+ builtins.history !== false && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11930
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
11931
+ /* @__PURE__ */ jsxRuntime.jsx(
11932
+ "button",
11933
+ {
11934
+ className: "ff-editor__btn",
11935
+ "data-action": "undo",
11936
+ title: "Undo (Ctrl+Z)",
11937
+ disabled: !api.canUndo,
11938
+ onClick: api.undo,
11939
+ children: "\u21B6 Undo"
11940
+ }
11941
+ ),
11942
+ /* @__PURE__ */ jsxRuntime.jsx(
11943
+ "button",
11944
+ {
11945
+ className: "ff-editor__btn",
11946
+ "data-action": "redo",
11947
+ title: "Redo (Ctrl+Shift+Z)",
11948
+ disabled: !api.canRedo,
11949
+ onClick: api.redo,
11950
+ children: "\u21B7 Redo"
11951
+ }
11952
+ )
11953
+ ] }),
11614
11954
  (builtins.export !== false || builtins.import !== false) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
11615
11955
  builtins.export !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "export", onClick: api.exportWorkflow, children: "\u2193 Export" }),
11616
11956
  builtins.import !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "import", onClick: api.importWorkflow, children: "\u2191 Import" }),
@@ -11635,12 +11975,16 @@ function FlowEditorInner({
11635
11975
  onNodesChange: flow.onNodesChange,
11636
11976
  onEdgesChange: flow.onEdgesChange,
11637
11977
  onConnect: flow.onConnect,
11978
+ onReconnect,
11979
+ edgesReconnectable: true,
11980
+ onNodeDragStart: hist.onNodeDragStart,
11638
11981
  onNodeClick: handleNodeClick2,
11639
11982
  onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
11640
11983
  onEdgeClick: handleEdgeClick,
11641
11984
  onEdgeContextMenu: builtins.edgeContextMenu === false ? void 0 : handleEdgeContextMenu,
11642
11985
  onEdgesDelete: (deleted) => onEdgeDelete?.(deleted.map((e) => e.id)),
11643
11986
  onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
11987
+ onBeforeDelete: confirmDelete ? async ({ nodes, edges }) => confirmDelete({ nodes, edges }) : void 0,
11644
11988
  deleteKeyCode: ["Delete", "Backspace"],
11645
11989
  height: "100%",
11646
11990
  toolbar,
@@ -11811,6 +12155,8 @@ function makeControlledFlowAdapter(value, onChange) {
11811
12155
  const nextEdges = typeof next === "function" ? next(value.edges) : next;
11812
12156
  apply({ nodes: value.nodes, edges: nextEdges });
11813
12157
  },
12158
+ // Atomic both-at-once commit — the reason this exists (see UseFlowStateReturn).
12159
+ setGraph: (graph) => apply(graph),
11814
12160
  onNodesChange: (changes) => {
11815
12161
  apply({ nodes: applyNodeChanges(changes, value.nodes), edges: value.edges });
11816
12162
  },
@@ -11920,15 +12266,19 @@ exports.SubgraphNode = SubgraphNode;
11920
12266
  exports.TriggerNode = TriggerNode;
11921
12267
  exports.WORKFLOW_SCHEMA_URL = WORKFLOW_SCHEMA_URL;
11922
12268
  exports.WORKFLOW_SCHEMA_VERSION = WORKFLOW_SCHEMA_VERSION;
12269
+ exports.alignNodes = alignNodes;
11923
12270
  exports.applyStatusesToNodes = applyStatusesToNodes;
11924
12271
  exports.buildNodeTypes = buildNodeTypes;
11925
12272
  exports.categoryAccent = categoryAccent;
12273
+ exports.cloneSubgraph = cloneSubgraph;
11926
12274
  exports.createConnectionValidator = createConnectionValidator;
12275
+ exports.createHistory = createHistory;
11927
12276
  exports.decodePause = decodePause;
11928
12277
  exports.defaultConfigFor = defaultConfigFor;
11929
12278
  exports.defaultNodeTypes = defaultNodeTypes;
11930
12279
  exports.defaultPortCompatibility = defaultPortCompatibility;
11931
12280
  exports.defineNode = defineNode;
12281
+ exports.distributeNodes = distributeNodes;
11932
12282
  exports.encodePause = encodePause;
11933
12283
  exports.exportWorkflow = exportWorkflow;
11934
12284
  exports.getNodeKind = getNodeKind;
@@ -11941,6 +12291,7 @@ exports.onNodeKindsChanged = onNodeKindsChanged;
11941
12291
  exports.onRichInputAdapterChanged = onRichInputAdapterChanged;
11942
12292
  exports.paletteDropHandlers = paletteDropHandlers;
11943
12293
  exports.pauseForHuman = pauseForHuman;
12294
+ exports.reconnectEdge = reconnectEdge3;
11944
12295
  exports.registerBuiltinKinds = registerBuiltinKinds;
11945
12296
  exports.registerNodeKind = registerNodeKind;
11946
12297
  exports.registerRichInputAdapter = registerRichInputAdapter;
@@ -11949,6 +12300,7 @@ exports.resolvePortSpec = resolvePortSpec;
11949
12300
  exports.runFlow = runFlow;
11950
12301
  exports.useFlowEditor = useFlowEditor;
11951
12302
  exports.useFlowEditorOptional = useFlowEditorOptional;
12303
+ exports.useFlowHistory = useFlowHistory;
11952
12304
  exports.useFlowRun = useFlowRun;
11953
12305
  exports.useFlowState = useFlowState;
11954
12306
  exports.validateConfig = validateConfig;