@particle-academy/fancy-flow 0.19.0 → 0.21.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.d.cts CHANGED
@@ -1,14 +1,14 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, ComponentType } from 'react';
3
- import { ReactFlowProps, Edge, BackgroundVariant, NodeProps, NodeTypes } from '@xyflow/react';
3
+ import { ReactFlowProps, Edge, BackgroundVariant, Connection, NodeProps, NodeTypes } from '@xyflow/react';
4
4
  import { ConnectionValidatorOptions } from './registry/index.cjs';
5
5
  export { ANY_PORT_TYPE, BUILTIN_KINDS, PortCompatibility, RegistryNode, RichInputAdapter, RichInputPreview, buildNodeTypes, categoryAccent, createConnectionValidator, defaultConfigFor, defaultPortCompatibility, getNodeKind, getRichInputAdapter, isRichInputEnabled, listNodeKinds, onNodeKindsChanged, onRichInputAdapterChanged, registerBuiltinKinds, registerNodeKind, registerRichInputAdapter, resolveNodePorts, resolvePortSpec, validateConfig } from './registry/index.cjs';
6
6
  import { b as FlowNode, F as FlowGraph, e as NodeRunStatus, E as ExecutorRegistry } from './types-sOmpCitB.cjs';
7
7
  export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, a as FlowEdge, c as FlowNodeData, d as FlowNodeKind, N as NodeExecutor, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, R as RunEvent, S as SubgraphNodeData, T as TriggerNodeData } from './types-sOmpCitB.cjs';
8
8
  import { WorkflowSchema, WorkflowMetadata } from './schema/index.cjs';
9
- export { ImportIssue, ImportOptions, ImportResult, WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, WorkflowSchemaEdge, WorkflowSchemaNode, exportWorkflow, importWorkflow, workflowToBlob } from './schema/index.cjs';
10
- import { N as NodeCategory, a as NodeKindDefinition, C as ConfigField } from './types-D_jOR3Z2.cjs';
11
- export { D as DocumentConfigField, K as KeyValueConfigField, P as PortSpec, R as RenderBodyContext, d as RepeaterConfigField, e as RepeaterRowField } from './types-D_jOR3Z2.cjs';
9
+ export { ImportIssue, ImportOptions, ImportResult, WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, WorkflowSchemaEdge, WorkflowSchemaNode, exportWorkflow, importWorkflow, migrateSchema, workflowToBlob } from './schema/index.cjs';
10
+ import { N as NodeCategory, a as NodeKindDefinition, C as ConfigField } from './types-LG-YckGh.cjs';
11
+ export { D as DocumentConfigField, K as KeyValueConfigField, P as PortSpec, R as RenderBodyContext, d as RepeaterConfigField, e as RepeaterRowField } from './types-LG-YckGh.cjs';
12
12
  import { FlowRunFeedEntry } from './runtime/index.cjs';
13
13
  export { HistoryController, UseFlowHistoryReturn, UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './runtime/index.cjs';
14
14
  export { R as RunOptions, a as RunResult, r as runFlow } from './run-flow-VKm57Hcj.cjs';
@@ -50,6 +50,33 @@ type FlowCanvasProps = Omit<ReactFlowProps<FlowNode, Edge>, "nodes" | "edges" |
50
50
  */
51
51
  declare function FlowCanvas({ nodes, edges, background, showControls, showMinimap, height, validateConnections, isValidConnection, toolbar, nodeTypes, edgeTypes, className, style, ...rest }: FlowCanvasProps): react.JSX.Element;
52
52
 
53
+ /**
54
+ * Clone a set of nodes AND the edges internal to that set, remapping every id.
55
+ * This is the piece `duplicateNode` lacks: a copy/paste (or duplicate-selection)
56
+ * that preserves the wiring *between* the copied nodes. Edges with an endpoint
57
+ * outside the set are dropped (they'd dangle). A `parentId` pointing inside the
58
+ * set is remapped; one pointing outside is detached.
59
+ */
60
+ declare function cloneSubgraph(nodes: FlowNode[], edges: Edge[], opts: {
61
+ makeId: () => string;
62
+ offset?: number;
63
+ }): {
64
+ nodes: FlowNode[];
65
+ edges: Edge[];
66
+ idMap: Map<string, string>;
67
+ };
68
+ /**
69
+ * Rewire one edge's endpoint(s) — wraps xyflow's `reconnectEdge`. Keeps the
70
+ * edge's id (`shouldReplaceId: false`) so its label and identity survive the
71
+ * rewire instead of being regenerated from the new endpoints.
72
+ */
73
+ declare function reconnectEdge(edges: Edge[], oldEdge: Edge, newConnection: Connection): Edge[];
74
+ type AlignEdge = "left" | "hcenter" | "right" | "top" | "vcenter" | "bottom";
75
+ /** Align a selection to a shared edge/center of its bounding box. */
76
+ declare function alignNodes(nodes: FlowNode[], edge: AlignEdge): FlowNode[];
77
+ /** Evenly distribute a selection's gaps along an axis (needs 3+ nodes). */
78
+ declare function distributeNodes(nodes: FlowNode[], axis: "h" | "v"): FlowNode[];
79
+
53
80
  /**
54
81
  * Everything the editor knows and can do, handed to every extension point.
55
82
  *
@@ -71,6 +98,10 @@ type FlowEditorApi = {
71
98
  selectedEdgeId: string | null;
72
99
  /** Currently selected edge, or null. */
73
100
  selectedEdge: Edge | null;
101
+ /** All multi-selected node ids (xyflow box / shift selection). */
102
+ selectedIds: string[];
103
+ /** All multi-selected nodes. */
104
+ selectedNodes: FlowNode[];
74
105
  /** True while a run is in flight. */
75
106
  running: boolean;
76
107
  /** Per-node run status, keyed by node id. */
@@ -99,6 +130,21 @@ type FlowEditorApi = {
99
130
  duplicateNode: (id: string) => string | null;
100
131
  /** Replace the whole graph. */
101
132
  setGraph: (graph: FlowGraph) => void;
133
+ /** Duplicate the current multi-selection, preserving edges between them. */
134
+ duplicateSelected: () => void;
135
+ /** Align the multi-selection to a shared edge/center of its bounding box. */
136
+ alignSelected: (edge: AlignEdge) => void;
137
+ /** Evenly distribute the multi-selection's gaps along an axis (needs 3+). */
138
+ distributeSelected: (axis: "h" | "v") => void;
139
+ /** Copy the current selection to the editor clipboard. */
140
+ copy: () => void;
141
+ /** Copy then delete the current selection. */
142
+ cut: () => void;
143
+ /** Paste the clipboard (optionally at a flow position) and select the result. */
144
+ paste: (at?: {
145
+ x: number;
146
+ y: number;
147
+ }) => void;
102
148
  run: () => void;
103
149
  cancel: () => void;
104
150
  reset: () => void;
@@ -476,4 +522,4 @@ type FlowRunFeedProps = {
476
522
  /** FlowRunFeed — scrolling log panel. Auto-scrolls to bottom on new entries. */
477
523
  declare function FlowRunFeed({ entries, className, style }: FlowRunFeedProps): react.JSX.Element;
478
524
 
479
- export { ActionNode, ConfigField, ConfigFieldRenderer, type ConfigFieldRendererProps, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, defaultNodeTypes, defineNode, paletteDropHandlers, useFlowEditor, useFlowEditorOptional };
525
+ export { ActionNode, type AlignEdge, ConfigField, ConfigFieldRenderer, type ConfigFieldRendererProps, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, paletteDropHandlers, reconnectEdge, useFlowEditor, useFlowEditorOptional };
package/dist/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, ComponentType } from 'react';
3
- import { ReactFlowProps, Edge, BackgroundVariant, NodeProps, NodeTypes } from '@xyflow/react';
3
+ import { ReactFlowProps, Edge, BackgroundVariant, Connection, NodeProps, NodeTypes } from '@xyflow/react';
4
4
  import { ConnectionValidatorOptions } from './registry/index.js';
5
5
  export { ANY_PORT_TYPE, BUILTIN_KINDS, PortCompatibility, RegistryNode, RichInputAdapter, RichInputPreview, buildNodeTypes, categoryAccent, createConnectionValidator, defaultConfigFor, defaultPortCompatibility, getNodeKind, getRichInputAdapter, isRichInputEnabled, listNodeKinds, onNodeKindsChanged, onRichInputAdapterChanged, registerBuiltinKinds, registerNodeKind, registerRichInputAdapter, resolveNodePorts, resolvePortSpec, validateConfig } from './registry/index.js';
6
6
  import { b as FlowNode, F as FlowGraph, e as NodeRunStatus, E as ExecutorRegistry } from './types-sOmpCitB.js';
7
7
  export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, a as FlowEdge, c as FlowNodeData, d as FlowNodeKind, N as NodeExecutor, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, R as RunEvent, S as SubgraphNodeData, T as TriggerNodeData } from './types-sOmpCitB.js';
8
8
  import { WorkflowSchema, WorkflowMetadata } from './schema/index.js';
9
- export { ImportIssue, ImportOptions, ImportResult, WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, WorkflowSchemaEdge, WorkflowSchemaNode, exportWorkflow, importWorkflow, workflowToBlob } from './schema/index.js';
10
- import { N as NodeCategory, a as NodeKindDefinition, C as ConfigField } from './types-NerkPtHS.js';
11
- export { D as DocumentConfigField, K as KeyValueConfigField, P as PortSpec, R as RenderBodyContext, d as RepeaterConfigField, e as RepeaterRowField } from './types-NerkPtHS.js';
9
+ export { ImportIssue, ImportOptions, ImportResult, WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, WorkflowSchemaEdge, WorkflowSchemaNode, exportWorkflow, importWorkflow, migrateSchema, workflowToBlob } from './schema/index.js';
10
+ import { N as NodeCategory, a as NodeKindDefinition, C as ConfigField } from './types-VGkCXEO6.js';
11
+ export { D as DocumentConfigField, K as KeyValueConfigField, P as PortSpec, R as RenderBodyContext, d as RepeaterConfigField, e as RepeaterRowField } from './types-VGkCXEO6.js';
12
12
  import { FlowRunFeedEntry } from './runtime/index.js';
13
13
  export { HistoryController, UseFlowHistoryReturn, UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './runtime/index.js';
14
14
  export { R as RunOptions, a as RunResult, r as runFlow } from './run-flow-ChBi6q-W.js';
@@ -50,6 +50,33 @@ type FlowCanvasProps = Omit<ReactFlowProps<FlowNode, Edge>, "nodes" | "edges" |
50
50
  */
51
51
  declare function FlowCanvas({ nodes, edges, background, showControls, showMinimap, height, validateConnections, isValidConnection, toolbar, nodeTypes, edgeTypes, className, style, ...rest }: FlowCanvasProps): react.JSX.Element;
52
52
 
53
+ /**
54
+ * Clone a set of nodes AND the edges internal to that set, remapping every id.
55
+ * This is the piece `duplicateNode` lacks: a copy/paste (or duplicate-selection)
56
+ * that preserves the wiring *between* the copied nodes. Edges with an endpoint
57
+ * outside the set are dropped (they'd dangle). A `parentId` pointing inside the
58
+ * set is remapped; one pointing outside is detached.
59
+ */
60
+ declare function cloneSubgraph(nodes: FlowNode[], edges: Edge[], opts: {
61
+ makeId: () => string;
62
+ offset?: number;
63
+ }): {
64
+ nodes: FlowNode[];
65
+ edges: Edge[];
66
+ idMap: Map<string, string>;
67
+ };
68
+ /**
69
+ * Rewire one edge's endpoint(s) — wraps xyflow's `reconnectEdge`. Keeps the
70
+ * edge's id (`shouldReplaceId: false`) so its label and identity survive the
71
+ * rewire instead of being regenerated from the new endpoints.
72
+ */
73
+ declare function reconnectEdge(edges: Edge[], oldEdge: Edge, newConnection: Connection): Edge[];
74
+ type AlignEdge = "left" | "hcenter" | "right" | "top" | "vcenter" | "bottom";
75
+ /** Align a selection to a shared edge/center of its bounding box. */
76
+ declare function alignNodes(nodes: FlowNode[], edge: AlignEdge): FlowNode[];
77
+ /** Evenly distribute a selection's gaps along an axis (needs 3+ nodes). */
78
+ declare function distributeNodes(nodes: FlowNode[], axis: "h" | "v"): FlowNode[];
79
+
53
80
  /**
54
81
  * Everything the editor knows and can do, handed to every extension point.
55
82
  *
@@ -71,6 +98,10 @@ type FlowEditorApi = {
71
98
  selectedEdgeId: string | null;
72
99
  /** Currently selected edge, or null. */
73
100
  selectedEdge: Edge | null;
101
+ /** All multi-selected node ids (xyflow box / shift selection). */
102
+ selectedIds: string[];
103
+ /** All multi-selected nodes. */
104
+ selectedNodes: FlowNode[];
74
105
  /** True while a run is in flight. */
75
106
  running: boolean;
76
107
  /** Per-node run status, keyed by node id. */
@@ -99,6 +130,21 @@ type FlowEditorApi = {
99
130
  duplicateNode: (id: string) => string | null;
100
131
  /** Replace the whole graph. */
101
132
  setGraph: (graph: FlowGraph) => void;
133
+ /** Duplicate the current multi-selection, preserving edges between them. */
134
+ duplicateSelected: () => void;
135
+ /** Align the multi-selection to a shared edge/center of its bounding box. */
136
+ alignSelected: (edge: AlignEdge) => void;
137
+ /** Evenly distribute the multi-selection's gaps along an axis (needs 3+). */
138
+ distributeSelected: (axis: "h" | "v") => void;
139
+ /** Copy the current selection to the editor clipboard. */
140
+ copy: () => void;
141
+ /** Copy then delete the current selection. */
142
+ cut: () => void;
143
+ /** Paste the clipboard (optionally at a flow position) and select the result. */
144
+ paste: (at?: {
145
+ x: number;
146
+ y: number;
147
+ }) => void;
102
148
  run: () => void;
103
149
  cancel: () => void;
104
150
  reset: () => void;
@@ -476,4 +522,4 @@ type FlowRunFeedProps = {
476
522
  /** FlowRunFeed — scrolling log panel. Auto-scrolls to bottom on new entries. */
477
523
  declare function FlowRunFeed({ entries, className, style }: FlowRunFeedProps): react.JSX.Element;
478
524
 
479
- export { ActionNode, ConfigField, ConfigFieldRenderer, type ConfigFieldRendererProps, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, defaultNodeTypes, defineNode, paletteDropHandlers, useFlowEditor, useFlowEditorOptional };
525
+ export { ActionNode, type AlignEdge, ConfigField, ConfigFieldRenderer, type ConfigFieldRendererProps, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, paletteDropHandlers, reconnectEdge, useFlowEditor, useFlowEditorOptional };
package/dist/index.js CHANGED
@@ -1,19 +1,19 @@
1
- import { useFlowState, useFlowRun, useFlowHistory, applyStatusesToNodes } from './chunk-PHX4SBOD.js';
2
- export { applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './chunk-PHX4SBOD.js';
3
- import { buildNodeTypes, registerBuiltinKinds, createConnectionValidator } from './chunk-FSC64BPD.js';
4
- export { ANY_PORT_TYPE, BUILTIN_KINDS, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility, registerBuiltinKinds } from './chunk-FSC64BPD.js';
5
- import { ReactFlowProvider, useReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, Position, Handle, index, BackgroundVariant, Background, Controls, MiniMap } from './chunk-NF6NPY5N.js';
1
+ import { useFlowState, useFlowRun, useFlowHistory, applyStatusesToNodes } from './chunk-G3WHXMZ3.js';
2
+ export { applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './chunk-G3WHXMZ3.js';
3
+ import { buildNodeTypes, registerBuiltinKinds, createConnectionValidator } from './chunk-JE4ENQSE.js';
4
+ export { ANY_PORT_TYPE, BUILTIN_KINDS, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility, registerBuiltinKinds } from './chunk-JE4ENQSE.js';
5
+ import { ReactFlowProvider, useReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, Position, Handle, reconnectEdge, index, BackgroundVariant, Background, Controls, MiniMap } from './chunk-ZB4HHQMR.js';
6
6
  export { LEGACY_PAUSE_PREFIXES, PAUSE_PREFIX, decodePause, encodePause, isPause, pauseForHuman } from './chunk-UEOE6B52.js';
7
7
  export { runFlow } from './chunk-L4AX73Q6.js';
8
- import { workflowToBlob, importWorkflow, exportWorkflow } from './chunk-D6W5FMCT.js';
9
- export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, workflowToBlob } from './chunk-D6W5FMCT.js';
8
+ import { workflowToBlob, importWorkflow, exportWorkflow } from './chunk-HMWYJIRP.js';
9
+ export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, migrateSchema, workflowToBlob } from './chunk-HMWYJIRP.js';
10
10
  export { resolveNodePorts, resolvePortSpec } from './chunk-TITD5W4Y.js';
11
11
  import { onNodeKindsChanged, listNodeKinds, getNodeKind, defaultConfigFor, validateConfig, categoryAccent } from './chunk-U5F22BHV.js';
12
12
  export { categoryAccent, defaultConfigFor, getNodeKind, listNodeKinds, onNodeKindsChanged, registerNodeKind, validateConfig } from './chunk-U5F22BHV.js';
13
13
  import { getRichInputAdapter } from './chunk-F5RPRB7A.js';
14
14
  export { RichInputPreview, getRichInputAdapter, isRichInputEnabled, onRichInputAdapterChanged, registerRichInputAdapter } from './chunk-F5RPRB7A.js';
15
15
  import './chunk-USL4FMFU.js';
16
- import { memo, createContext, forwardRef, useState, useEffect, useMemo, useCallback, useImperativeHandle, useRef, useContext } from 'react';
16
+ import { memo, createContext, forwardRef, useState, useEffect, useMemo, useCallback, useRef, useImperativeHandle, useContext } from 'react';
17
17
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
18
18
 
19
19
  function NodeShellInner({
@@ -839,6 +839,65 @@ function duplicateNode(node, id, offset = 40) {
839
839
  data: JSON.parse(JSON.stringify(node.data ?? {}))
840
840
  };
841
841
  }
842
+ function cloneSubgraph(nodes, edges, opts) {
843
+ const offset = opts.offset ?? 40;
844
+ const idMap = /* @__PURE__ */ new Map();
845
+ for (const n of nodes) idMap.set(n.id, opts.makeId());
846
+ const clonedNodes = nodes.map((n) => {
847
+ const cloned = duplicateNode(n, idMap.get(n.id), offset);
848
+ const parentId = n.parentId;
849
+ if (parentId && idMap.has(parentId)) cloned.parentId = idMap.get(parentId);
850
+ else if (parentId) delete cloned.parentId;
851
+ return cloned;
852
+ });
853
+ 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) }));
854
+ return { nodes: clonedNodes, edges: clonedEdges, idMap };
855
+ }
856
+ function reconnectEdge2(edges, oldEdge, newConnection) {
857
+ return reconnectEdge(oldEdge, newConnection, edges, { shouldReplaceId: false });
858
+ }
859
+ var nodeW = (n) => n.width ?? n.measured?.width ?? 0;
860
+ var nodeH = (n) => n.height ?? n.measured?.height ?? 0;
861
+ function alignNodes(nodes, edge) {
862
+ if (nodes.length < 2) return nodes;
863
+ const minL = Math.min(...nodes.map((n) => n.position.x));
864
+ const maxR = Math.max(...nodes.map((n) => n.position.x + nodeW(n)));
865
+ const minT = Math.min(...nodes.map((n) => n.position.y));
866
+ const maxB = Math.max(...nodes.map((n) => n.position.y + nodeH(n)));
867
+ const cx = (minL + maxR) / 2;
868
+ const cy = (minT + maxB) / 2;
869
+ return nodes.map((n) => {
870
+ let { x, y } = n.position;
871
+ if (edge === "left") x = minL;
872
+ else if (edge === "right") x = maxR - nodeW(n);
873
+ else if (edge === "hcenter") x = cx - nodeW(n) / 2;
874
+ else if (edge === "top") y = minT;
875
+ else if (edge === "bottom") y = maxB - nodeH(n);
876
+ else if (edge === "vcenter") y = cy - nodeH(n) / 2;
877
+ return { ...n, position: { x, y } };
878
+ });
879
+ }
880
+ function distributeNodes(nodes, axis) {
881
+ if (nodes.length < 3) return nodes;
882
+ const size = (n) => axis === "h" ? nodeW(n) : nodeH(n);
883
+ const coord = (n) => axis === "h" ? n.position.x : n.position.y;
884
+ const sorted = [...nodes].sort((a, b) => coord(a) - coord(b));
885
+ const start = coord(sorted[0]);
886
+ const last = sorted[sorted.length - 1];
887
+ const end = coord(last) + size(last);
888
+ const totalSize = sorted.reduce((s, n) => s + size(n), 0);
889
+ const gap = (end - start - totalSize) / (sorted.length - 1);
890
+ const posById = /* @__PURE__ */ new Map();
891
+ let cursor = start;
892
+ for (const n of sorted) {
893
+ posById.set(n.id, cursor);
894
+ cursor += size(n) + gap;
895
+ }
896
+ return nodes.map((n) => {
897
+ const p = posById.get(n.id);
898
+ return { ...n, position: axis === "h" ? { ...n.position, x: p } : { ...n.position, y: p } };
899
+ });
900
+ }
842
901
  var FlowEditorContext = createContext(null);
843
902
  var FlowEditorProvider = FlowEditorContext.Provider;
844
903
  function useFlowEditor() {
@@ -1008,6 +1067,89 @@ function FlowEditorInner({
1008
1067
  },
1009
1068
  [flow, onEdgeDelete, confirmDelete]
1010
1069
  );
1070
+ const selectedNodes = useMemo(() => flow.nodes.filter((n) => n.selected), [flow.nodes]);
1071
+ const selectedIds = useMemo(() => selectedNodes.map((n) => n.id), [selectedNodes]);
1072
+ const clipboard = useRef(null);
1073
+ const copySelection = useCallback(() => {
1074
+ const sel = flow.nodes.filter((n) => n.selected);
1075
+ if (sel.length === 0) return;
1076
+ const ids = new Set(sel.map((n) => n.id));
1077
+ const edges = flow.edges.filter((e) => ids.has(e.source) && ids.has(e.target));
1078
+ clipboard.current = { nodes: sel.map((n) => ({ ...n })), edges: edges.map((e) => ({ ...e })) };
1079
+ }, [flow]);
1080
+ const pasteClipboard = useCallback(
1081
+ (at) => {
1082
+ const clip = clipboard.current;
1083
+ if (!clip || clip.nodes.length === 0) return;
1084
+ const { nodes: clones, edges: cloneEdges } = cloneSubgraph(clip.nodes, clip.edges, { makeId: newNodeId, offset: 40 });
1085
+ let placed = clones;
1086
+ if (at) {
1087
+ const minX = Math.min(...clones.map((n) => n.position.x));
1088
+ const minY = Math.min(...clones.map((n) => n.position.y));
1089
+ placed = clones.map((n) => ({ ...n, position: { x: n.position.x - minX + at.x, y: n.position.y - minY + at.y } }));
1090
+ }
1091
+ flow.setGraph({
1092
+ nodes: [...flow.nodes.map((n) => ({ ...n, selected: false })), ...placed.map((n) => ({ ...n, selected: true }))],
1093
+ edges: [...flow.edges, ...cloneEdges]
1094
+ });
1095
+ },
1096
+ [flow]
1097
+ );
1098
+ const duplicateSelected = useCallback(() => {
1099
+ const sel = flow.nodes.filter((n) => n.selected);
1100
+ if (sel.length === 0) return;
1101
+ const ids = new Set(sel.map((n) => n.id));
1102
+ const internal2 = flow.edges.filter((e) => ids.has(e.source) && ids.has(e.target));
1103
+ const { nodes: clones, edges: cloneEdges } = cloneSubgraph(sel, internal2, { makeId: newNodeId, offset: 40 });
1104
+ flow.setGraph({
1105
+ nodes: [...flow.nodes.map((n) => ({ ...n, selected: false })), ...clones.map((n) => ({ ...n, selected: true }))],
1106
+ edges: [...flow.edges, ...cloneEdges]
1107
+ });
1108
+ }, [flow]);
1109
+ const alignSelected = useCallback(
1110
+ (edge) => {
1111
+ const sel = flow.nodes.filter((n) => n.selected);
1112
+ if (sel.length < 2) return;
1113
+ const byId = new Map(alignNodes(sel, edge).map((n) => [n.id, n]));
1114
+ flow.setNodes((all) => all.map((n) => byId.get(n.id) ?? n));
1115
+ },
1116
+ [flow]
1117
+ );
1118
+ const distributeSelected = useCallback(
1119
+ (axis) => {
1120
+ const sel = flow.nodes.filter((n) => n.selected);
1121
+ if (sel.length < 3) return;
1122
+ const byId = new Map(distributeNodes(sel, axis).map((n) => [n.id, n]));
1123
+ flow.setNodes((all) => all.map((n) => byId.get(n.id) ?? n));
1124
+ },
1125
+ [flow]
1126
+ );
1127
+ const onReconnect = useCallback(
1128
+ (oldEdge, conn) => flow.setEdges((eds) => reconnectEdge2(eds, oldEdge, conn)),
1129
+ [flow]
1130
+ );
1131
+ useEffect(() => {
1132
+ const onKey = (e) => {
1133
+ const t = e.target;
1134
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
1135
+ if (!(e.ctrlKey || e.metaKey)) return;
1136
+ const key = e.key.toLowerCase();
1137
+ if (key === "c") copySelection();
1138
+ else if (key === "x") {
1139
+ e.preventDefault();
1140
+ copySelection();
1141
+ deleteNodes(selectedIds);
1142
+ } else if (key === "v") {
1143
+ e.preventDefault();
1144
+ pasteClipboard();
1145
+ } else if (key === "d") {
1146
+ e.preventDefault();
1147
+ duplicateSelected();
1148
+ }
1149
+ };
1150
+ window.addEventListener("keydown", onKey);
1151
+ return () => window.removeEventListener("keydown", onKey);
1152
+ }, [copySelection, pasteClipboard, duplicateSelected, deleteNodes, selectedIds]);
1011
1153
  const api = useMemo(() => {
1012
1154
  const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
1013
1155
  return {
@@ -1018,6 +1160,8 @@ function FlowEditorInner({
1018
1160
  selected,
1019
1161
  selectedEdgeId,
1020
1162
  selectedEdge,
1163
+ selectedIds,
1164
+ selectedNodes,
1021
1165
  running: runner.running,
1022
1166
  statuses: runner.statuses,
1023
1167
  select: setSelectedId,
@@ -1038,6 +1182,15 @@ function FlowEditorInner({
1038
1182
  return copy.id;
1039
1183
  },
1040
1184
  setGraph: (graph) => flow.setGraph(graph),
1185
+ duplicateSelected,
1186
+ alignSelected,
1187
+ distributeSelected,
1188
+ copy: copySelection,
1189
+ cut: () => {
1190
+ copySelection();
1191
+ deleteNodes(selectedIds);
1192
+ },
1193
+ paste: pasteClipboard,
1041
1194
  run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
1042
1195
  cancel: runner.cancel,
1043
1196
  reset: runner.reset,
@@ -1050,7 +1203,7 @@ function FlowEditorInner({
1050
1203
  canUndo: hist.canUndo,
1051
1204
  canRedo: hist.canRedo
1052
1205
  };
1053
- }, [flow, selectedId, selected, selectedEdgeId, selectedEdge, runner, executors, metadata, addNode, deleteNodes, deleteEdges, hist, rf]);
1206
+ }, [flow, selectedId, selected, selectedEdgeId, selectedEdge, selectedIds, selectedNodes, runner, executors, metadata, addNode, deleteNodes, deleteEdges, duplicateSelected, alignSelected, distributeSelected, copySelection, pasteClipboard, hist, rf]);
1054
1207
  useImperativeHandle(apiRef, () => api, [api]);
1055
1208
  const dropHandlers = paletteDropHandlers((kindName, evt) => {
1056
1209
  const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
@@ -1110,6 +1263,8 @@ function FlowEditorInner({
1110
1263
  onNodesChange: flow.onNodesChange,
1111
1264
  onEdgesChange: flow.onEdgesChange,
1112
1265
  onConnect: flow.onConnect,
1266
+ onReconnect,
1267
+ edgesReconnectable: true,
1113
1268
  onNodeDragStart: hist.onNodeDragStart,
1114
1269
  onNodeClick: handleNodeClick,
1115
1270
  onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
@@ -1376,6 +1531,6 @@ function NodePort({ side, type, id, style, title, className }) {
1376
1531
  // src/index.ts
1377
1532
  registerBuiltinKinds();
1378
1533
 
1379
- export { ActionNode, ConfigFieldRenderer, DecisionNode, FlowCanvas, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodePort, NodeShell, NoteNode, OutputNode, SubgraphNode, TriggerNode, defaultNodeTypes, defineNode, paletteDropHandlers, useFlowEditor, useFlowEditorOptional };
1534
+ export { ActionNode, ConfigFieldRenderer, DecisionNode, FlowCanvas, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodePort, NodeShell, NoteNode, OutputNode, SubgraphNode, TriggerNode, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, paletteDropHandlers, reconnectEdge2 as reconnectEdge, useFlowEditor, useFlowEditorOptional };
1380
1535
  //# sourceMappingURL=index.js.map
1381
1536
  //# sourceMappingURL=index.js.map