@particle-academy/fancy-flow 0.17.0 → 0.19.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
@@ -10285,6 +10285,40 @@ var defaultNodeTypes = {
10285
10285
  note: NoteNode,
10286
10286
  subgraph: SubgraphNode
10287
10287
  };
10288
+
10289
+ // src/registry/connection.ts
10290
+ var ANY_PORT_TYPE = "any";
10291
+ var defaultPortCompatibility = (source, target) => {
10292
+ const s = source?.type;
10293
+ const t = target?.type;
10294
+ if (!s || !t) return true;
10295
+ if (s === ANY_PORT_TYPE || t === ANY_PORT_TYPE) return true;
10296
+ return s === t;
10297
+ };
10298
+ function portsFor(node, side) {
10299
+ const kind = getNodeKind(node.data?.kind ?? node.type);
10300
+ const resolved = resolveNodePorts(node, kind ?? void 0);
10301
+ return resolved[side] ?? [];
10302
+ }
10303
+ function findPort(ports, handleId) {
10304
+ if (handleId == null) return ports.length === 1 ? ports[0] : void 0;
10305
+ return ports.find((p) => p.id === handleId);
10306
+ }
10307
+ function createConnectionValidator(getNodes, options = {}) {
10308
+ const compatible = options.compatible ?? defaultPortCompatibility;
10309
+ return (connection) => {
10310
+ const { source, target, sourceHandle, targetHandle } = connection;
10311
+ if (!source || !target) return false;
10312
+ if (!options.allowSelfConnection && source === target) return false;
10313
+ const nodes = getNodes();
10314
+ const src = nodes.find((n) => n.id === source);
10315
+ const tgt = nodes.find((n) => n.id === target);
10316
+ if (!src || !tgt) return false;
10317
+ const outPort = findPort(portsFor(src, "outputs"), sourceHandle);
10318
+ const inPort = findPort(portsFor(tgt, "inputs"), targetHandle);
10319
+ return compatible(outPort, inPort);
10320
+ };
10321
+ }
10288
10322
  var DEFAULT_FIT_VIEW = { padding: 0.2 };
10289
10323
  var DEFAULT_EDGE_OPTIONS = {
10290
10324
  type: "smoothstep",
@@ -10297,6 +10331,8 @@ function FlowCanvas({
10297
10331
  showControls = true,
10298
10332
  showMinimap = false,
10299
10333
  height = 600,
10334
+ validateConnections = true,
10335
+ isValidConnection,
10300
10336
  toolbar,
10301
10337
  nodeTypes,
10302
10338
  edgeTypes,
@@ -10308,6 +10344,16 @@ function FlowCanvas({
10308
10344
  () => ({ ...defaultNodeTypes, ...nodeTypes ?? {} }),
10309
10345
  [nodeTypes]
10310
10346
  );
10347
+ const nodesRef = ReactExports.useRef(nodes);
10348
+ nodesRef.current = nodes;
10349
+ const builtinValidator = ReactExports.useMemo(
10350
+ () => validateConnections === false ? void 0 : createConnectionValidator(
10351
+ () => nodesRef.current,
10352
+ validateConnections === true ? void 0 : validateConnections
10353
+ ),
10354
+ [validateConnections]
10355
+ );
10356
+ const resolvedIsValidConnection = isValidConnection ?? builtinValidator;
10311
10357
  const mergedEdgeTypes = ReactExports.useMemo(
10312
10358
  () => edgeTypes ? { ...edgeTypes } : void 0,
10313
10359
  [edgeTypes]
@@ -10327,6 +10373,7 @@ function FlowCanvas({
10327
10373
  proOptions: { hideAttribution: true },
10328
10374
  zoomActivationKeyCode: "Shift",
10329
10375
  preventScrolling: false,
10376
+ isValidConnection: resolvedIsValidConnection,
10330
10377
  ...rest,
10331
10378
  children: [
10332
10379
  background !== "none" && /* @__PURE__ */ jsxRuntime.jsx(Background, { variant: background, gap: 20, size: 1, color: "rgba(0,0,0,0.18)" }),
@@ -10949,8 +10996,117 @@ function useFlowState(initial) {
10949
10996
  const onConnect = ReactExports.useCallback((connection) => {
10950
10997
  setEdges((es) => addEdge2(connection, es));
10951
10998
  }, []);
10999
+ const setGraph = ReactExports.useCallback((graph) => {
11000
+ setNodes(graph.nodes);
11001
+ setEdges(graph.edges);
11002
+ }, []);
10952
11003
  const toGraph = ReactExports.useCallback(() => ({ nodes, edges }), [nodes, edges]);
10953
- return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
11004
+ return { nodes, edges, setNodes, setEdges, setGraph, onNodesChange, onEdgesChange, onConnect, toGraph };
11005
+ }
11006
+
11007
+ // src/runtime/history.ts
11008
+ function createHistory(limit = 100) {
11009
+ let past = [];
11010
+ let future = [];
11011
+ return {
11012
+ push(graph) {
11013
+ past.push(graph);
11014
+ if (past.length > limit) past.shift();
11015
+ future = [];
11016
+ },
11017
+ undo(current) {
11018
+ const prev = past.pop();
11019
+ if (prev === void 0) return null;
11020
+ future.push(current);
11021
+ return prev;
11022
+ },
11023
+ redo(current) {
11024
+ const next = future.pop();
11025
+ if (next === void 0) return null;
11026
+ past.push(current);
11027
+ return next;
11028
+ },
11029
+ canUndo: () => past.length > 0,
11030
+ canRedo: () => future.length > 0,
11031
+ clear() {
11032
+ past = [];
11033
+ future = [];
11034
+ },
11035
+ size: () => ({ past: past.length, future: future.length })
11036
+ };
11037
+ }
11038
+
11039
+ // src/runtime/use-flow-history.ts
11040
+ function useFlowHistory(flow) {
11041
+ const history = ReactExports.useRef(createHistory()).current;
11042
+ const restoring = ReactExports.useRef(false);
11043
+ const coalesced = ReactExports.useRef(false);
11044
+ const [, bump] = ReactExports.useState(0);
11045
+ const rerender = ReactExports.useCallback(() => bump((n) => n + 1), []);
11046
+ const graphRef = ReactExports.useRef({ nodes: flow.nodes, edges: flow.edges });
11047
+ graphRef.current = { nodes: flow.nodes, edges: flow.edges };
11048
+ const capture = ReactExports.useCallback(() => {
11049
+ if (restoring.current || coalesced.current) return;
11050
+ coalesced.current = true;
11051
+ queueMicrotask(() => {
11052
+ coalesced.current = false;
11053
+ });
11054
+ history.push(graphRef.current);
11055
+ rerender();
11056
+ }, [history, rerender]);
11057
+ const restore = ReactExports.useCallback(
11058
+ (g) => {
11059
+ if (!g) return;
11060
+ restoring.current = true;
11061
+ flow.setGraph(g);
11062
+ queueMicrotask(() => {
11063
+ restoring.current = false;
11064
+ });
11065
+ rerender();
11066
+ },
11067
+ [flow, rerender]
11068
+ );
11069
+ const undo = ReactExports.useCallback(() => restore(history.undo(graphRef.current)), [history, restore]);
11070
+ const redo = ReactExports.useCallback(() => restore(history.redo(graphRef.current)), [history, restore]);
11071
+ const wrapped = ReactExports.useMemo(
11072
+ () => ({
11073
+ ...flow,
11074
+ setNodes: (next) => {
11075
+ capture();
11076
+ flow.setNodes(next);
11077
+ },
11078
+ setEdges: (next) => {
11079
+ capture();
11080
+ flow.setEdges(next);
11081
+ },
11082
+ setGraph: (g) => {
11083
+ capture();
11084
+ flow.setGraph(g);
11085
+ },
11086
+ onNodesChange: (changes) => {
11087
+ if (!restoring.current && changes.some((c) => c.type === "remove")) capture();
11088
+ flow.onNodesChange(changes);
11089
+ },
11090
+ onEdgesChange: (changes) => {
11091
+ if (!restoring.current && changes.some((c) => c.type === "remove")) capture();
11092
+ flow.onEdgesChange(changes);
11093
+ },
11094
+ onConnect: (conn) => {
11095
+ capture();
11096
+ flow.onConnect(conn);
11097
+ }
11098
+ }),
11099
+ [flow, capture]
11100
+ );
11101
+ return {
11102
+ flow: wrapped,
11103
+ undo,
11104
+ redo,
11105
+ canUndo: history.canUndo(),
11106
+ canRedo: history.canRedo(),
11107
+ capture,
11108
+ onNodeDragStart: capture
11109
+ };
10954
11110
  }
10955
11111
  function useFlowRun({ maxFeed = 200 } = {}) {
10956
11112
  const [statuses, setStatuses] = ReactExports.useState({});
@@ -11406,13 +11562,16 @@ function FlowEditorInner({
11406
11562
  onSelectionChange,
11407
11563
  onDelete,
11408
11564
  onEdgeDelete,
11565
+ confirmDelete,
11409
11566
  apiRef
11410
11567
  }) {
11411
11568
  const internal = useFlowState(initial);
11412
11569
  const runner = useFlowRun();
11413
11570
  const rf = useReactFlow();
11414
11571
  const controlled = value !== void 0;
11415
- const flow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
11572
+ const baseFlow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
11573
+ const hist = useFlowHistory(baseFlow);
11574
+ const flow = hist.flow;
11416
11575
  const [, force] = ReactExports.useState(0);
11417
11576
  ReactExports.useEffect(() => onNodeKindsChanged(() => force((n) => n + 1)), []);
11418
11577
  const nodeTypes = ReactExports.useMemo(() => buildNodeTypes(), [listNodeKinds().length]);
@@ -11465,6 +11624,23 @@ function FlowEditorInner({
11465
11624
  window.removeEventListener("keydown", onKey);
11466
11625
  };
11467
11626
  }, [menu, labelEdit]);
11627
+ ReactExports.useEffect(() => {
11628
+ const onKey = (e) => {
11629
+ const t = e.target;
11630
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
11631
+ if (!(e.ctrlKey || e.metaKey)) return;
11632
+ const key = e.key.toLowerCase();
11633
+ if (key === "z" && !e.shiftKey) {
11634
+ e.preventDefault();
11635
+ hist.undo();
11636
+ } else if (key === "z" && e.shiftKey || key === "y") {
11637
+ e.preventDefault();
11638
+ hist.redo();
11639
+ }
11640
+ };
11641
+ window.addEventListener("keydown", onKey);
11642
+ return () => window.removeEventListener("keydown", onKey);
11643
+ }, [hist]);
11468
11644
  ReactExports.useEffect(() => {
11469
11645
  if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
11470
11646
  }, [flow.nodes, flow.edges, onChange, controlled]);
@@ -11489,15 +11665,32 @@ function FlowEditorInner({
11489
11665
  [flow]
11490
11666
  );
11491
11667
  const deleteNodes = ReactExports.useCallback(
11492
- (ids) => {
11668
+ async (ids) => {
11493
11669
  if (ids.length === 0) return;
11670
+ const targets = flow.nodes.filter((n) => ids.includes(n.id));
11671
+ if (targets.length === 0) return;
11672
+ if (confirmDelete) {
11673
+ const attached = flow.edges.filter((e) => ids.includes(e.source) || ids.includes(e.target));
11674
+ if (!await confirmDelete({ nodes: targets, edges: attached })) return;
11675
+ }
11494
11676
  const doomed = new Set(ids);
11495
- flow.setNodes((all) => removeNodes({ nodes: all, edges: [] }, ids).nodes);
11496
- flow.setEdges((all) => removeNodes({ nodes: [], edges: all }, ids).edges);
11677
+ flow.setGraph(removeNodes({ nodes: flow.nodes, edges: flow.edges }, ids));
11497
11678
  setSelectedId((cur) => cur !== null && doomed.has(cur) ? null : cur);
11498
11679
  onDelete?.(ids);
11499
11680
  },
11500
- [flow, onDelete]
11681
+ [flow, onDelete, confirmDelete]
11682
+ );
11683
+ const deleteEdges = ReactExports.useCallback(
11684
+ async (ids) => {
11685
+ if (ids.length === 0) return;
11686
+ const targets = flow.edges.filter((e) => ids.includes(e.id));
11687
+ if (targets.length === 0) return;
11688
+ if (confirmDelete && !await confirmDelete({ nodes: [], edges: targets })) return;
11689
+ flow.setEdges((all) => removeEdges(all, ids));
11690
+ setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
11691
+ onEdgeDelete?.(ids);
11692
+ },
11693
+ [flow, onEdgeDelete, confirmDelete]
11501
11694
  );
11502
11695
  const api = ReactExports.useMemo(() => {
11503
11696
  const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
@@ -11517,18 +11710,8 @@ function FlowEditorInner({
11517
11710
  updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
11518
11711
  deleteNodes,
11519
11712
  deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
11520
- deleteEdges: (ids) => {
11521
- if (ids.length === 0) return;
11522
- flow.setEdges((all) => removeEdges(all, ids));
11523
- setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
11524
- onEdgeDelete?.(ids);
11525
- },
11526
- deleteSelectedEdge: () => {
11527
- if (!selectedEdgeId) return;
11528
- flow.setEdges((all) => removeEdges(all, [selectedEdgeId]));
11529
- setSelectedEdgeId(null);
11530
- onEdgeDelete?.([selectedEdgeId]);
11531
- },
11713
+ deleteEdges,
11714
+ deleteSelectedEdge: () => deleteEdges(selectedEdgeId ? [selectedEdgeId] : []),
11532
11715
  setEdgeLabel: (id2, label) => flow.setEdges((all) => setEdgeLabel(all, id2, label)),
11533
11716
  duplicateNode: (id2) => {
11534
11717
  const src = flow.nodes.find((n) => n.id === id2);
@@ -11538,22 +11721,20 @@ function FlowEditorInner({
11538
11721
  setSelectedId(copy.id);
11539
11722
  return copy.id;
11540
11723
  },
11541
- setGraph: (graph) => {
11542
- flow.setNodes(graph.nodes);
11543
- flow.setEdges(graph.edges);
11544
- },
11724
+ setGraph: (graph) => flow.setGraph(graph),
11545
11725
  run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
11546
11726
  cancel: runner.cancel,
11547
11727
  reset: runner.reset,
11548
11728
  toWorkflow,
11549
11729
  exportWorkflow: () => downloadWorkflow(toWorkflow(), metadata),
11550
- importWorkflow: () => pickWorkflow((graph) => {
11551
- flow.setNodes(graph.nodes);
11552
- flow.setEdges(graph.edges);
11553
- }),
11554
- fitView: () => rf.fitView({ padding: 0.2 })
11730
+ importWorkflow: () => pickWorkflow((graph) => flow.setGraph(graph)),
11731
+ fitView: () => rf.fitView({ padding: 0.2 }),
11732
+ undo: hist.undo,
11733
+ redo: hist.redo,
11734
+ canUndo: hist.canUndo,
11735
+ canRedo: hist.canRedo
11555
11736
  };
11556
- }, [flow, selectedId, selected2, runner, executors, metadata, addNode, deleteNodes, rf]);
11737
+ }, [flow, selectedId, selected2, selectedEdgeId, selectedEdge, runner, executors, metadata, addNode, deleteNodes, deleteEdges, hist, rf]);
11557
11738
  ReactExports.useImperativeHandle(apiRef, () => api, [api]);
11558
11739
  const dropHandlers = paletteDropHandlers((kindName, evt) => {
11559
11740
  const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
@@ -11564,6 +11745,31 @@ function FlowEditorInner({
11564
11745
  const toolbar = slots.toolbar ? slots.toolbar(api) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11565
11746
  startActions.map((a) => renderAction(a, api)),
11566
11747
  builtins.run !== false && /* @__PURE__ */ jsxRuntime.jsx(FlowRunControls, { running: api.running, onRun: api.run, onCancel: api.cancel, onReset: api.reset }),
11748
+ builtins.history !== false && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11749
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
11750
+ /* @__PURE__ */ jsxRuntime.jsx(
11751
+ "button",
11752
+ {
11753
+ className: "ff-editor__btn",
11754
+ "data-action": "undo",
11755
+ title: "Undo (Ctrl+Z)",
11756
+ disabled: !api.canUndo,
11757
+ onClick: api.undo,
11758
+ children: "\u21B6 Undo"
11759
+ }
11760
+ ),
11761
+ /* @__PURE__ */ jsxRuntime.jsx(
11762
+ "button",
11763
+ {
11764
+ className: "ff-editor__btn",
11765
+ "data-action": "redo",
11766
+ title: "Redo (Ctrl+Shift+Z)",
11767
+ disabled: !api.canRedo,
11768
+ onClick: api.redo,
11769
+ children: "\u21B7 Redo"
11770
+ }
11771
+ )
11772
+ ] }),
11567
11773
  (builtins.export !== false || builtins.import !== false) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
11568
11774
  builtins.export !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "export", onClick: api.exportWorkflow, children: "\u2193 Export" }),
11569
11775
  builtins.import !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "import", onClick: api.importWorkflow, children: "\u2191 Import" }),
@@ -11588,12 +11794,14 @@ function FlowEditorInner({
11588
11794
  onNodesChange: flow.onNodesChange,
11589
11795
  onEdgesChange: flow.onEdgesChange,
11590
11796
  onConnect: flow.onConnect,
11797
+ onNodeDragStart: hist.onNodeDragStart,
11591
11798
  onNodeClick: handleNodeClick2,
11592
11799
  onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
11593
11800
  onEdgeClick: handleEdgeClick,
11594
11801
  onEdgeContextMenu: builtins.edgeContextMenu === false ? void 0 : handleEdgeContextMenu,
11595
11802
  onEdgesDelete: (deleted) => onEdgeDelete?.(deleted.map((e) => e.id)),
11596
11803
  onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
11804
+ onBeforeDelete: confirmDelete ? async ({ nodes, edges }) => confirmDelete({ nodes, edges }) : void 0,
11597
11805
  deleteKeyCode: ["Delete", "Backspace"],
11598
11806
  height: "100%",
11599
11807
  toolbar,
@@ -11764,6 +11972,8 @@ function makeControlledFlowAdapter(value, onChange) {
11764
11972
  const nextEdges = typeof next === "function" ? next(value.edges) : next;
11765
11973
  apply({ nodes: value.nodes, edges: nextEdges });
11766
11974
  },
11975
+ // Atomic both-at-once commit — the reason this exists (see UseFlowStateReturn).
11976
+ setGraph: (graph) => apply(graph),
11767
11977
  onNodesChange: (changes) => {
11768
11978
  apply({ nodes: applyNodeChanges(changes, value.nodes), edges: value.edges });
11769
11979
  },
@@ -11850,6 +12060,7 @@ function NodePort({ side, type, id: id2, style: style2, title, className }) {
11850
12060
  // src/index.ts
11851
12061
  registerBuiltinKinds();
11852
12062
 
12063
+ exports.ANY_PORT_TYPE = ANY_PORT_TYPE;
11853
12064
  exports.ActionNode = ActionNode;
11854
12065
  exports.BUILTIN_KINDS = BUILTIN_KINDS;
11855
12066
  exports.ConfigFieldRenderer = ConfigFieldRenderer;
@@ -11875,9 +12086,12 @@ exports.WORKFLOW_SCHEMA_VERSION = WORKFLOW_SCHEMA_VERSION;
11875
12086
  exports.applyStatusesToNodes = applyStatusesToNodes;
11876
12087
  exports.buildNodeTypes = buildNodeTypes;
11877
12088
  exports.categoryAccent = categoryAccent;
12089
+ exports.createConnectionValidator = createConnectionValidator;
12090
+ exports.createHistory = createHistory;
11878
12091
  exports.decodePause = decodePause;
11879
12092
  exports.defaultConfigFor = defaultConfigFor;
11880
12093
  exports.defaultNodeTypes = defaultNodeTypes;
12094
+ exports.defaultPortCompatibility = defaultPortCompatibility;
11881
12095
  exports.defineNode = defineNode;
11882
12096
  exports.encodePause = encodePause;
11883
12097
  exports.exportWorkflow = exportWorkflow;
@@ -11899,6 +12113,7 @@ exports.resolvePortSpec = resolvePortSpec;
11899
12113
  exports.runFlow = runFlow;
11900
12114
  exports.useFlowEditor = useFlowEditor;
11901
12115
  exports.useFlowEditorOptional = useFlowEditorOptional;
12116
+ exports.useFlowHistory = useFlowHistory;
11902
12117
  exports.useFlowRun = useFlowRun;
11903
12118
  exports.useFlowState = useFlowState;
11904
12119
  exports.validateConfig = validateConfig;