@particle-academy/fancy-flow 0.5.4 → 0.6.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/README.md CHANGED
@@ -38,6 +38,86 @@ export const ThresholdNode = defineNode<MyData>(({ data, selected }) => (
38
38
 
39
39
  `defineNode` returns a memoized component compatible with the underlying engine; `<NodePort>` renders a connection handle. Together they cover what the typical node author needs — multiple ports, source vs target, position per side — without ever importing from `@xyflow/react`.
40
40
 
41
+ ## Extending the editor
42
+
43
+ `<FlowEditor>` is batteries-included but not a black box. Four escape hatches,
44
+ smallest first — reach for the first one that fits.
45
+
46
+ **1. Custom toolbar buttons — declarative, so an agent can emit them too.**
47
+
48
+ ```tsx
49
+ <FlowEditor
50
+ actions={[
51
+ {
52
+ id: "save",
53
+ label: "Save",
54
+ placement: "start",
55
+ onSelect: (api) => persist(api.toWorkflow()),
56
+ },
57
+ {
58
+ id: "duplicate",
59
+ label: "Duplicate",
60
+ requiresSelection: true, // auto-disabled with no selection
61
+ onSelect: (api) => api.duplicateNode(api.selectedId!),
62
+ },
63
+ ]}
64
+ builtins={{ import: false }} // drop built-ins you don't want
65
+ />
66
+ ```
67
+
68
+ Each button renders with `data-action="<id>"`, so an agent gets a stable handle
69
+ instead of guessing DOM.
70
+
71
+ **2. Replace a whole region with `slots`.** Every slot receives the editor API.
72
+
73
+ ```tsx
74
+ <FlowEditor
75
+ slots={{
76
+ panel: (api) => <MyInspector node={api.selected} onChange={api.updateNode} />,
77
+ panelFooter: (api) => <button onClick={api.deleteSelected}>Delete node</button>,
78
+ empty: () => <p>Drag a node from the palette to start.</p>,
79
+ toolbar: (api) => <MyToolbar api={api} />, // replaces built-ins entirely
80
+ }}
81
+ />
82
+ ```
83
+
84
+ **3. Drive it from outside** — `ref` for imperative control, `useFlowEditor()`
85
+ inside any child:
86
+
87
+ ```tsx
88
+ const editor = useRef<FlowEditorApi>(null);
89
+ editor.current?.addNode("llm_call", { x: 120, y: 80 });
90
+ editor.current?.deleteSelected();
91
+ editor.current?.run();
92
+ ```
93
+
94
+ `FlowEditorApi` carries the graph, selection, run state, and every mutation:
95
+ `addNode` · `updateNode` · `deleteNodes` · `deleteSelected` · `deleteEdges` ·
96
+ `duplicateNode` · `setGraph` · `select` · `run` / `cancel` / `reset` ·
97
+ `toWorkflow` / `exportWorkflow` / `importWorkflow` · `fitView`.
98
+
99
+ **4. Reach React Flow directly** with `canvasProps` — context menus,
100
+ `snapToGrid`, minimap options, edge types, anything xyflow accepts:
101
+
102
+ ```tsx
103
+ <FlowEditor canvasProps={{ snapToGrid: true, showMinimap: true, onNodeContextMenu: openMenu }} />
104
+ ```
105
+
106
+ ### Deleting nodes
107
+
108
+ Three ways, all of which prune the edges attached to the node (a dangling edge
109
+ would survive into the schema and break the runner):
110
+
111
+ - the **Delete** toolbar button (enabled when a node is selected),
112
+ - the <kbd>Delete</kbd> or <kbd>Backspace</kbd> key on the canvas,
113
+ - `api.deleteSelected()` / `api.deleteNodes(ids)` from code.
114
+
115
+ `onDelete(ids)` fires after either path, so a host can sync its own store.
116
+
117
+ If you want none of the above chrome, skip `<FlowEditor>` entirely and compose
118
+ `useFlowState()` + `<FlowCanvas>` + `<NodePalette>` + `<NodeConfigPanel>`
119
+ yourself — they are all exported.
120
+
41
121
  ## Quick start
42
122
 
43
123
  ```tsx
package/dist/engine.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-TemTtb04.cjs';
2
- export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, b as FlowEdge, F as FlowNode, c as FlowNodeData, d as FlowNodeKind, N as NodeExecutor, e as NodeRunStatus, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, S as SubgraphNodeData, T as TriggerNodeData } from './types-TemTtb04.cjs';
1
+ import { a as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-BS3Gwnkq.cjs';
2
+ export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, c as FlowEdge, F as FlowNode, d as FlowNodeData, e as FlowNodeKind, N as NodeExecutor, b as NodeRunStatus, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, S as SubgraphNodeData, T as TriggerNodeData } from './types-BS3Gwnkq.cjs';
3
3
  import '@xyflow/react';
4
4
 
5
5
  type RunOptions = {
package/dist/engine.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-TemTtb04.js';
2
- export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, b as FlowEdge, F as FlowNode, c as FlowNodeData, d as FlowNodeKind, N as NodeExecutor, e as NodeRunStatus, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, S as SubgraphNodeData, T as TriggerNodeData } from './types-TemTtb04.js';
1
+ import { a as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-BS3Gwnkq.js';
2
+ export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, c as FlowEdge, F as FlowNode, d as FlowNodeData, e as FlowNodeKind, N as NodeExecutor, b as NodeRunStatus, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, S as SubgraphNodeData, T as TriggerNodeData } from './types-BS3Gwnkq.js';
3
3
  import '@xyflow/react';
4
4
 
5
5
  type RunOptions = {
package/dist/index.cjs CHANGED
@@ -10348,16 +10348,52 @@ function buildNodeTypes() {
10348
10348
  for (const k of listNodeKinds()) map[k.name] = RegistryNode;
10349
10349
  return map;
10350
10350
  }
10351
- function FlowEditor(props) {
10351
+
10352
+ // src/components/FlowEditor/graph-ops.ts
10353
+ function removeNodes(graph, ids) {
10354
+ if (ids.length === 0) return graph;
10355
+ const doomed = new Set(ids);
10356
+ return {
10357
+ nodes: graph.nodes.filter((n) => !doomed.has(n.id)),
10358
+ edges: graph.edges.filter((e) => !doomed.has(e.source) && !doomed.has(e.target))
10359
+ };
10360
+ }
10361
+ function removeEdges(edges, ids) {
10362
+ if (ids.length === 0) return edges;
10363
+ const doomed = new Set(ids);
10364
+ return edges.filter((e) => !doomed.has(e.id));
10365
+ }
10366
+ function duplicateNode(node, id2, offset = 40) {
10367
+ return {
10368
+ ...node,
10369
+ id: id2,
10370
+ position: { x: node.position.x + offset, y: node.position.y + offset },
10371
+ // Deep copy so the clone's config edits don't mutate the original.
10372
+ data: JSON.parse(JSON.stringify(node.data ?? {}))
10373
+ };
10374
+ }
10375
+ var FlowEditorContext = ReactExports.createContext(null);
10376
+ var FlowEditorProvider = FlowEditorContext.Provider;
10377
+ function useFlowEditor() {
10378
+ const api = ReactExports.useContext(FlowEditorContext);
10379
+ if (api === null) {
10380
+ throw new Error("useFlowEditor() must be called inside <FlowEditor>.");
10381
+ }
10382
+ return api;
10383
+ }
10384
+ function useFlowEditorOptional() {
10385
+ return ReactExports.useContext(FlowEditorContext);
10386
+ }
10387
+ var FlowEditor = ReactExports.forwardRef(function FlowEditor2(props, ref) {
10352
10388
  return /* @__PURE__ */ jsxRuntime.jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(
10353
10389
  "div",
10354
10390
  {
10355
10391
  className: ["ff-editor", props.className ?? ""].filter(Boolean).join(" "),
10356
10392
  style: { height: props.height ?? 720, ...props.style },
10357
- children: /* @__PURE__ */ jsxRuntime.jsx(FlowEditorInner, { ...props })
10393
+ children: /* @__PURE__ */ jsxRuntime.jsx(FlowEditorInner, { ...props, apiRef: ref })
10358
10394
  }
10359
10395
  ) });
10360
- }
10396
+ });
10361
10397
  function FlowEditorInner({
10362
10398
  initial = { nodes: [], edges: [] },
10363
10399
  value,
@@ -10367,10 +10403,18 @@ function FlowEditorInner({
10367
10403
  showPanel = true,
10368
10404
  showFeed = true,
10369
10405
  extraToolbar,
10370
- onChange
10406
+ actions = [],
10407
+ builtins = {},
10408
+ slots = {},
10409
+ canvasProps = {},
10410
+ onChange,
10411
+ onSelectionChange,
10412
+ onDelete,
10413
+ apiRef
10371
10414
  }) {
10372
10415
  const internal = useFlowState(initial);
10373
10416
  const runner = useFlowRun();
10417
+ const rf = useReactFlow();
10374
10418
  const controlled = value !== void 0;
10375
10419
  const flow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
10376
10420
  const [, force] = ReactExports.useState(0);
@@ -10382,29 +10426,187 @@ function FlowEditorInner({
10382
10426
  );
10383
10427
  const [selectedId, setSelectedId] = ReactExports.useState(null);
10384
10428
  const selected2 = ReactExports.useMemo(() => flow.nodes.find((n) => n.id === selectedId) ?? null, [flow.nodes, selectedId]);
10429
+ ReactExports.useEffect(() => onSelectionChange?.(selected2), [selected2, onSelectionChange]);
10385
10430
  const handleNodeClick2 = (_e, node) => setSelectedId(node.id);
10386
10431
  ReactExports.useEffect(() => {
10387
10432
  if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
10388
10433
  }, [flow.nodes, flow.edges, onChange, controlled]);
10434
+ const addNode = ReactExports.useCallback(
10435
+ (kindName, position) => {
10436
+ const kind = getNodeKind(kindName);
10437
+ if (!kind) return null;
10438
+ const at = position ?? { x: 80, y: 80 };
10439
+ const id2 = newNodeId();
10440
+ flow.setNodes((all) => [
10441
+ ...all,
10442
+ {
10443
+ id: id2,
10444
+ type: kind.name,
10445
+ position: at,
10446
+ data: { kind: kind.name, label: kind.label, config: defaultConfigFor(kind) }
10447
+ }
10448
+ ]);
10449
+ setSelectedId(id2);
10450
+ return id2;
10451
+ },
10452
+ [flow]
10453
+ );
10454
+ const deleteNodes = ReactExports.useCallback(
10455
+ (ids) => {
10456
+ if (ids.length === 0) return;
10457
+ const doomed = new Set(ids);
10458
+ flow.setNodes((all) => removeNodes({ nodes: all, edges: [] }, ids).nodes);
10459
+ flow.setEdges((all) => removeNodes({ nodes: [], edges: all }, ids).edges);
10460
+ setSelectedId((cur) => cur !== null && doomed.has(cur) ? null : cur);
10461
+ onDelete?.(ids);
10462
+ },
10463
+ [flow, onDelete]
10464
+ );
10465
+ const api = ReactExports.useMemo(() => {
10466
+ const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
10467
+ return {
10468
+ graph: { nodes: flow.nodes, edges: flow.edges },
10469
+ nodes: flow.nodes,
10470
+ edges: flow.edges,
10471
+ selectedId,
10472
+ selected: selected2,
10473
+ running: runner.running,
10474
+ statuses: runner.statuses,
10475
+ select: setSelectedId,
10476
+ addNode,
10477
+ updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
10478
+ deleteNodes,
10479
+ deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
10480
+ deleteEdges: (ids) => flow.setEdges((all) => removeEdges(all, ids)),
10481
+ duplicateNode: (id2) => {
10482
+ const src = flow.nodes.find((n) => n.id === id2);
10483
+ if (!src) return null;
10484
+ const copy = duplicateNode(src, newNodeId());
10485
+ flow.setNodes((all) => [...all, copy]);
10486
+ setSelectedId(copy.id);
10487
+ return copy.id;
10488
+ },
10489
+ setGraph: (graph) => {
10490
+ flow.setNodes(graph.nodes);
10491
+ flow.setEdges(graph.edges);
10492
+ },
10493
+ run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
10494
+ cancel: runner.cancel,
10495
+ reset: runner.reset,
10496
+ toWorkflow,
10497
+ exportWorkflow: () => downloadWorkflow(toWorkflow(), metadata),
10498
+ importWorkflow: () => pickWorkflow((graph) => {
10499
+ flow.setNodes(graph.nodes);
10500
+ flow.setEdges(graph.edges);
10501
+ }),
10502
+ fitView: () => rf.fitView({ padding: 0.2 })
10503
+ };
10504
+ }, [flow, selectedId, selected2, runner, executors, metadata, addNode, deleteNodes, rf]);
10505
+ ReactExports.useImperativeHandle(apiRef, () => api, [api]);
10506
+ const dropHandlers = paletteDropHandlers((kindName, evt) => {
10507
+ const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
10508
+ addNode(kindName, { x: point.x - 100, y: point.y - 30 });
10509
+ });
10510
+ const startActions = actions.filter((a) => a.placement === "start");
10511
+ const endActions = actions.filter((a) => a.placement !== "start");
10512
+ const toolbar = slots.toolbar ? slots.toolbar(api) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10513
+ startActions.map((a) => renderAction(a, api)),
10514
+ builtins.run !== false && /* @__PURE__ */ jsxRuntime.jsx(FlowRunControls, { running: api.running, onRun: api.run, onCancel: api.cancel, onReset: api.reset }),
10515
+ (builtins.delete !== false || builtins.export !== false || builtins.import !== false) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
10516
+ builtins.delete !== false && /* @__PURE__ */ jsxRuntime.jsx(
10517
+ "button",
10518
+ {
10519
+ className: "ff-editor__btn",
10520
+ "data-action": "delete",
10521
+ onClick: api.deleteSelected,
10522
+ disabled: api.selected === null,
10523
+ title: "Delete the selected node (Del / Backspace)",
10524
+ children: "\u2715 Delete"
10525
+ }
10526
+ ),
10527
+ builtins.export !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "export", onClick: api.exportWorkflow, children: "\u2193 Export" }),
10528
+ builtins.import !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "import", onClick: api.importWorkflow, children: "\u2191 Import" }),
10529
+ endActions.map((a) => renderAction(a, api)),
10530
+ extraToolbar,
10531
+ builtins.count !== false && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-editor__count", children: [
10532
+ api.nodes.length,
10533
+ " nodes \xB7 ",
10534
+ api.edges.length,
10535
+ " edges"
10536
+ ] })
10537
+ ] });
10538
+ return /* @__PURE__ */ jsxRuntime.jsxs(FlowEditorProvider, { value: api, children: [
10539
+ showPalette && (slots.palette ? slots.palette(api) : /* @__PURE__ */ jsxRuntime.jsx(NodePalette, { className: "ff-editor__palette" })),
10540
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__main", ...dropHandlers, children: [
10541
+ /* @__PURE__ */ jsxRuntime.jsx(
10542
+ FlowCanvas,
10543
+ {
10544
+ nodes: renderedNodes,
10545
+ edges: flow.edges,
10546
+ nodeTypes,
10547
+ onNodesChange: flow.onNodesChange,
10548
+ onEdgesChange: flow.onEdgesChange,
10549
+ onConnect: flow.onConnect,
10550
+ onNodeClick: handleNodeClick2,
10551
+ onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
10552
+ deleteKeyCode: ["Delete", "Backspace"],
10553
+ height: "100%",
10554
+ toolbar,
10555
+ ...canvasProps
10556
+ }
10557
+ ),
10558
+ slots.empty && api.nodes.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-editor__empty", children: slots.empty(api) }),
10559
+ showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsxRuntime.jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" }))
10560
+ ] }),
10561
+ showPanel && (slots.panel ? slots.panel(api) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__panel-wrap", children: [
10562
+ /* @__PURE__ */ jsxRuntime.jsx(NodeConfigPanel, { className: "ff-editor__panel", node: api.selected, onChange: api.updateNode }),
10563
+ slots.panelFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-editor__panel-footer", children: slots.panelFooter(api) })
10564
+ ] }))
10565
+ ] });
10566
+ }
10567
+ function renderAction(action, api) {
10568
+ if (action.visible && !action.visible(api)) return null;
10569
+ const disabled = action.disabled ? action.disabled(api) : action.requiresSelection === true && api.selected === null;
10389
10570
  return /* @__PURE__ */ jsxRuntime.jsx(
10390
- FlowEditorBody,
10571
+ "button",
10391
10572
  {
10392
- showPalette,
10393
- showPanel,
10394
- showFeed,
10395
- flow,
10396
- runner,
10397
- executors,
10398
- metadata,
10399
- nodeTypes,
10400
- renderedNodes,
10401
- selected: selected2,
10402
- setSelectedId,
10403
- handleNodeClick: handleNodeClick2,
10404
- extraToolbar
10405
- }
10573
+ className: "ff-editor__btn",
10574
+ "data-action": action.id,
10575
+ title: action.title,
10576
+ disabled,
10577
+ onClick: () => action.onSelect(api),
10578
+ children: action.label
10579
+ },
10580
+ action.id
10406
10581
  );
10407
10582
  }
10583
+ function newNodeId() {
10584
+ return `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
10585
+ }
10586
+ function downloadWorkflow(schema, metadata) {
10587
+ const url = URL.createObjectURL(workflowToBlob(schema));
10588
+ const a = document.createElement("a");
10589
+ a.href = url;
10590
+ a.download = `${metadata?.id ?? "workflow"}.json`;
10591
+ a.click();
10592
+ URL.revokeObjectURL(url);
10593
+ }
10594
+ function pickWorkflow(onLoad) {
10595
+ const input = document.createElement("input");
10596
+ input.type = "file";
10597
+ input.accept = "application/json";
10598
+ input.onchange = async () => {
10599
+ const file = input.files?.[0];
10600
+ if (!file) return;
10601
+ try {
10602
+ const result = importWorkflow(JSON.parse(await file.text()), { lenient: true });
10603
+ onLoad(result.graph);
10604
+ } catch (e) {
10605
+ console.error("import failed", e);
10606
+ }
10607
+ };
10608
+ input.click();
10609
+ }
10408
10610
  function makeControlledFlowAdapter(value, onChange) {
10409
10611
  const apply = (next) => onChange?.(next);
10410
10612
  return {
@@ -10430,114 +10632,6 @@ function makeControlledFlowAdapter(value, onChange) {
10430
10632
  toGraph: () => value
10431
10633
  };
10432
10634
  }
10433
- function FlowEditorBody({
10434
- showPalette,
10435
- showPanel,
10436
- showFeed,
10437
- flow,
10438
- runner,
10439
- executors,
10440
- metadata,
10441
- nodeTypes,
10442
- renderedNodes,
10443
- selected: selected2,
10444
- setSelectedId,
10445
- handleNodeClick: handleNodeClick2,
10446
- extraToolbar
10447
- }) {
10448
- const rf = useReactFlow();
10449
- const dropHandlers = paletteDropHandlers((kindName, evt) => {
10450
- const kind = getNodeKind(kindName);
10451
- if (!kind) return;
10452
- const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
10453
- const id2 = `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
10454
- flow.setNodes((all) => [
10455
- ...all,
10456
- {
10457
- id: id2,
10458
- type: kind.name,
10459
- position: { x: point.x - 100, y: point.y - 30 },
10460
- data: { kind: kind.name, label: kind.label, config: defaultConfigFor(kind) }
10461
- }
10462
- ]);
10463
- setSelectedId(id2);
10464
- });
10465
- const doExport = () => {
10466
- const schema = exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
10467
- const url = URL.createObjectURL(workflowToBlob(schema));
10468
- const a = document.createElement("a");
10469
- a.href = url;
10470
- a.download = `${metadata?.id ?? "workflow"}.json`;
10471
- a.click();
10472
- URL.revokeObjectURL(url);
10473
- };
10474
- const doImport = () => {
10475
- const input = document.createElement("input");
10476
- input.type = "file";
10477
- input.accept = "application/json";
10478
- input.onchange = async () => {
10479
- const file = input.files?.[0];
10480
- if (!file) return;
10481
- const text = await file.text();
10482
- try {
10483
- const result = importWorkflow(JSON.parse(text), { lenient: true });
10484
- flow.setNodes(result.graph.nodes);
10485
- flow.setEdges(result.graph.edges);
10486
- } catch (e) {
10487
- console.error("import failed", e);
10488
- }
10489
- };
10490
- input.click();
10491
- };
10492
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10493
- showPalette && /* @__PURE__ */ jsxRuntime.jsx(NodePalette, { className: "ff-editor__palette" }),
10494
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__main", ...dropHandlers, children: [
10495
- /* @__PURE__ */ jsxRuntime.jsx(
10496
- FlowCanvas,
10497
- {
10498
- nodes: renderedNodes,
10499
- edges: flow.edges,
10500
- nodeTypes,
10501
- onNodesChange: flow.onNodesChange,
10502
- onEdgesChange: flow.onEdgesChange,
10503
- onConnect: flow.onConnect,
10504
- onNodeClick: handleNodeClick2,
10505
- height: "100%",
10506
- toolbar: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10507
- /* @__PURE__ */ jsxRuntime.jsx(
10508
- FlowRunControls,
10509
- {
10510
- running: runner.running,
10511
- onRun: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
10512
- onCancel: runner.cancel,
10513
- onReset: runner.reset
10514
- }
10515
- ),
10516
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
10517
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", onClick: doExport, children: "\u2193 Export" }),
10518
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", onClick: doImport, children: "\u2191 Import" }),
10519
- extraToolbar,
10520
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-editor__count", children: [
10521
- flow.nodes.length,
10522
- " nodes \xB7 ",
10523
- flow.edges.length,
10524
- " edges"
10525
- ] })
10526
- ] })
10527
- }
10528
- ),
10529
- showFeed && /* @__PURE__ */ jsxRuntime.jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" })
10530
- ] }),
10531
- showPanel && /* @__PURE__ */ jsxRuntime.jsx(
10532
- NodeConfigPanel,
10533
- {
10534
- className: "ff-editor__panel",
10535
- node: selected2,
10536
- onChange: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x))
10537
- }
10538
- )
10539
- ] });
10540
- }
10541
10635
  function defineNode(render) {
10542
10636
  function Wrapped(props) {
10543
10637
  return render({
@@ -10606,6 +10700,8 @@ exports.paletteDropHandlers = paletteDropHandlers;
10606
10700
  exports.registerBuiltinKinds = registerBuiltinKinds;
10607
10701
  exports.registerNodeKind = registerNodeKind;
10608
10702
  exports.runFlow = runFlow;
10703
+ exports.useFlowEditor = useFlowEditor;
10704
+ exports.useFlowEditorOptional = useFlowEditorOptional;
10609
10705
  exports.useFlowRun = useFlowRun;
10610
10706
  exports.useFlowState = useFlowState;
10611
10707
  exports.validateConfig = validateConfig;