@particle-academy/fancy-flow 0.6.0 → 0.7.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
@@ -108,10 +108,28 @@ editor.current?.run();
108
108
  Three ways, all of which prune the edges attached to the node (a dangling edge
109
109
  would survive into the schema and break the runner):
110
110
 
111
+ - **right-click a node** → Delete / Duplicate,
111
112
  - the **Delete** toolbar button (enabled when a node is selected),
112
113
  - the <kbd>Delete</kbd> or <kbd>Backspace</kbd> key on the canvas,
113
114
  - `api.deleteSelected()` / `api.deleteNodes(ids)` from code.
114
115
 
116
+ Swap the menu for your own with `slots.contextMenu`, or turn it off with
117
+ `builtins={{ contextMenu: false }}` (passing your own
118
+ `canvasProps.onNodeContextMenu` also takes over):
119
+
120
+ ```tsx
121
+ <FlowEditor
122
+ slots={{
123
+ contextMenu: (api, nodeId, close) => (
124
+ <>
125
+ <button onClick={() => { api.duplicateNode(nodeId); close(); }}>Duplicate</button>
126
+ <button onClick={() => { pinNode(nodeId); close(); }}>Pin</button>
127
+ </>
128
+ ),
129
+ }}
130
+ />
131
+ ```
132
+
115
133
  `onDelete(ids)` fires after either path, so a host can sync its own store.
116
134
 
117
135
  If you want none of the above chrome, skip `<FlowEditor>` entirely and compose
package/dist/index.cjs CHANGED
@@ -10428,6 +10428,26 @@ function FlowEditorInner({
10428
10428
  const selected2 = ReactExports.useMemo(() => flow.nodes.find((n) => n.id === selectedId) ?? null, [flow.nodes, selectedId]);
10429
10429
  ReactExports.useEffect(() => onSelectionChange?.(selected2), [selected2, onSelectionChange]);
10430
10430
  const handleNodeClick2 = (_e, node) => setSelectedId(node.id);
10431
+ const [menu, setMenu] = ReactExports.useState(null);
10432
+ const closeMenu = ReactExports.useCallback(() => setMenu(null), []);
10433
+ const handleNodeContextMenu = (event, node) => {
10434
+ event.preventDefault();
10435
+ setSelectedId(node.id);
10436
+ setMenu({ x: event.clientX, y: event.clientY, nodeId: node.id });
10437
+ };
10438
+ ReactExports.useEffect(() => {
10439
+ if (menu === null) return;
10440
+ const close = () => setMenu(null);
10441
+ const onKey = (e) => e.key === "Escape" && setMenu(null);
10442
+ window.addEventListener("click", close);
10443
+ window.addEventListener("scroll", close, true);
10444
+ window.addEventListener("keydown", onKey);
10445
+ return () => {
10446
+ window.removeEventListener("click", close);
10447
+ window.removeEventListener("scroll", close, true);
10448
+ window.removeEventListener("keydown", onKey);
10449
+ };
10450
+ }, [menu]);
10431
10451
  ReactExports.useEffect(() => {
10432
10452
  if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
10433
10453
  }, [flow.nodes, flow.edges, onChange, controlled]);
@@ -10548,6 +10568,7 @@ function FlowEditorInner({
10548
10568
  onEdgesChange: flow.onEdgesChange,
10549
10569
  onConnect: flow.onConnect,
10550
10570
  onNodeClick: handleNodeClick2,
10571
+ onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
10551
10572
  onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
10552
10573
  deleteKeyCode: ["Delete", "Backspace"],
10553
10574
  height: "100%",
@@ -10556,6 +10577,45 @@ function FlowEditorInner({
10556
10577
  }
10557
10578
  ),
10558
10579
  slots.empty && api.nodes.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-editor__empty", children: slots.empty(api) }),
10580
+ menu !== null && builtins.contextMenu !== false && /* @__PURE__ */ jsxRuntime.jsx(
10581
+ "div",
10582
+ {
10583
+ className: "ff-editor__ctx",
10584
+ style: { top: menu.y, left: menu.x },
10585
+ role: "menu",
10586
+ onClick: (e) => e.stopPropagation(),
10587
+ children: slots.contextMenu ? slots.contextMenu(api, menu.nodeId, closeMenu) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10588
+ /* @__PURE__ */ jsxRuntime.jsx(
10589
+ "button",
10590
+ {
10591
+ type: "button",
10592
+ role: "menuitem",
10593
+ className: "ff-editor__ctx-item",
10594
+ "data-action": "ctx-duplicate",
10595
+ onClick: () => {
10596
+ api.duplicateNode(menu.nodeId);
10597
+ closeMenu();
10598
+ },
10599
+ children: "Duplicate"
10600
+ }
10601
+ ),
10602
+ /* @__PURE__ */ jsxRuntime.jsx(
10603
+ "button",
10604
+ {
10605
+ type: "button",
10606
+ role: "menuitem",
10607
+ className: "ff-editor__ctx-item ff-editor__ctx-item--danger",
10608
+ "data-action": "ctx-delete",
10609
+ onClick: () => {
10610
+ api.deleteNodes([menu.nodeId]);
10611
+ closeMenu();
10612
+ },
10613
+ children: "Delete"
10614
+ }
10615
+ )
10616
+ ] })
10617
+ }
10618
+ ),
10559
10619
  showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsxRuntime.jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" }))
10560
10620
  ] }),
10561
10621
  showPanel && (slots.panel ? slots.panel(api) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__panel-wrap", children: [