@particle-academy/fancy-flow 0.7.0 → 0.9.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.
Files changed (47) hide show
  1. package/README.md +93 -0
  2. package/dist/{chunk-BCXECQUC.js → chunk-6GGFEZH7.js} +3 -3
  3. package/dist/{chunk-BCXECQUC.js.map → chunk-6GGFEZH7.js.map} +1 -1
  4. package/dist/{chunk-M2XKGQQL.js → chunk-6RHAQ2LP.js} +228 -17
  5. package/dist/chunk-6RHAQ2LP.js.map +1 -0
  6. package/dist/{chunk-WNVBXXOL.js → chunk-CPSOC27D.js} +43 -2
  7. package/dist/chunk-CPSOC27D.js.map +1 -0
  8. package/dist/{chunk-NVULCEDX.js → chunk-HNBO4HP3.js} +8 -4
  9. package/dist/chunk-HNBO4HP3.js.map +1 -0
  10. package/dist/chunk-TITD5W4Y.js +26 -0
  11. package/dist/chunk-TITD5W4Y.js.map +1 -0
  12. package/dist/{chunk-QSSQRQN4.js → chunk-VEI743ZX.js} +3 -3
  13. package/dist/{chunk-QSSQRQN4.js.map → chunk-VEI743ZX.js.map} +1 -1
  14. package/dist/engine.cjs +32 -2
  15. package/dist/engine.cjs.map +1 -1
  16. package/dist/engine.js +3 -1
  17. package/dist/index.cjs +717 -40
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +45 -6
  20. package/dist/index.d.ts +45 -6
  21. package/dist/index.js +415 -23
  22. package/dist/index.js.map +1 -1
  23. package/dist/registry/index.d.cts +111 -4
  24. package/dist/registry/index.d.ts +111 -4
  25. package/dist/registry.cjs +313 -31
  26. package/dist/registry.cjs.map +1 -1
  27. package/dist/registry.js +3 -2
  28. package/dist/runtime.cjs +32 -2
  29. package/dist/runtime.cjs.map +1 -1
  30. package/dist/runtime.js +4 -2
  31. package/dist/schema.cjs +41 -0
  32. package/dist/schema.cjs.map +1 -1
  33. package/dist/schema.js +2 -2
  34. package/dist/styles.css +191 -0
  35. package/dist/styles.css.map +1 -1
  36. package/dist/types-BocBFh6l.d.ts +221 -0
  37. package/dist/types-DKqaUjF_.d.cts +221 -0
  38. package/dist/ux.cjs.map +1 -1
  39. package/dist/ux.d.cts +1 -1
  40. package/dist/ux.d.ts +1 -1
  41. package/dist/ux.js +1 -1
  42. package/package.json +4 -4
  43. package/dist/chunk-M2XKGQQL.js.map +0 -1
  44. package/dist/chunk-NVULCEDX.js.map +0 -1
  45. package/dist/chunk-WNVBXXOL.js.map +0 -1
  46. package/dist/types-C0wdN6QX.d.cts +0 -121
  47. package/dist/types-DnMe9Vsf.d.ts +0 -121
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { useFlowState, useFlowRun, applyStatusesToNodes } from './chunk-QSSQRQN4.js';
2
- export { applyStatusesToNodes, useFlowRun, useFlowState } from './chunk-QSSQRQN4.js';
3
- export { runFlow } from './chunk-NVULCEDX.js';
4
- import { buildNodeTypes, registerBuiltinKinds } from './chunk-M2XKGQQL.js';
5
- export { BUILTIN_KINDS, RegistryNode, buildNodeTypes, registerBuiltinKinds } from './chunk-M2XKGQQL.js';
1
+ import { useFlowState, useFlowRun, applyStatusesToNodes } from './chunk-VEI743ZX.js';
2
+ export { applyStatusesToNodes, useFlowRun, useFlowState } from './chunk-VEI743ZX.js';
3
+ export { runFlow } from './chunk-HNBO4HP3.js';
4
+ import { buildNodeTypes, registerBuiltinKinds, getRichInputAdapter } from './chunk-6RHAQ2LP.js';
5
+ export { BUILTIN_KINDS, RegistryNode, RichInputPreview, buildNodeTypes, getRichInputAdapter, isRichInputEnabled, onRichInputAdapterChanged, registerBuiltinKinds, registerRichInputAdapter } from './chunk-6RHAQ2LP.js';
6
6
  import { ReactFlowProvider, useReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, Position, Handle, index, BackgroundVariant, Background, Controls, MiniMap } from './chunk-NF6NPY5N.js';
7
- import { workflowToBlob, importWorkflow, exportWorkflow } from './chunk-BCXECQUC.js';
8
- export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, workflowToBlob } from './chunk-BCXECQUC.js';
9
- import { onNodeKindsChanged, listNodeKinds, getNodeKind, defaultConfigFor, validateConfig, categoryAccent } from './chunk-WNVBXXOL.js';
10
- export { categoryAccent, defaultConfigFor, getNodeKind, listNodeKinds, onNodeKindsChanged, registerNodeKind, validateConfig } from './chunk-WNVBXXOL.js';
7
+ export { resolveNodePorts, resolvePortSpec } from './chunk-TITD5W4Y.js';
8
+ import { workflowToBlob, importWorkflow, exportWorkflow } from './chunk-6GGFEZH7.js';
9
+ export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, workflowToBlob } from './chunk-6GGFEZH7.js';
10
+ import { onNodeKindsChanged, listNodeKinds, getNodeKind, defaultConfigFor, validateConfig, categoryAccent } from './chunk-CPSOC27D.js';
11
+ export { categoryAccent, defaultConfigFor, getNodeKind, listNodeKinds, onNodeKindsChanged, registerNodeKind, validateConfig } from './chunk-CPSOC27D.js';
11
12
  import { memo, createContext, forwardRef, useState, useEffect, useMemo, useCallback, useImperativeHandle, useRef, useContext } from 'react';
12
13
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
14
 
@@ -274,9 +275,18 @@ function paletteDropHandlers(onDrop) {
274
275
  }
275
276
  };
276
277
  }
277
- function ConfigFieldRenderer({ field, value, onChange, renderCredentialField }) {
278
+ function ConfigFieldRenderer({
279
+ field,
280
+ value,
281
+ onChange,
282
+ renderCredentialField,
283
+ renderDocumentField
284
+ }) {
278
285
  switch (field.type) {
279
- case "text":
286
+ case "text": {
287
+ if (field.choices?.length) {
288
+ return /* @__PURE__ */ jsx(ChoiceField, { field, value, onChange });
289
+ }
280
290
  return /* @__PURE__ */ jsx(
281
291
  "input",
282
292
  {
@@ -287,6 +297,7 @@ function ConfigFieldRenderer({ field, value, onChange, renderCredentialField })
287
297
  onChange: (e) => onChange(e.target.value)
288
298
  }
289
299
  );
300
+ }
290
301
  case "textarea":
291
302
  return /* @__PURE__ */ jsx(
292
303
  "textarea",
@@ -364,10 +375,244 @@ function ConfigFieldRenderer({ field, value, onChange, renderCredentialField })
364
375
  onChange: (e) => onChange(e.target.value)
365
376
  }
366
377
  );
378
+ case "repeater":
379
+ return /* @__PURE__ */ jsx(
380
+ RepeaterField,
381
+ {
382
+ field,
383
+ value,
384
+ onChange,
385
+ renderCredentialField,
386
+ renderDocumentField
387
+ }
388
+ );
389
+ case "keyvalue":
390
+ return /* @__PURE__ */ jsx(KeyValueField, { field, value, onChange });
391
+ case "document":
392
+ if (renderDocumentField) {
393
+ return /* @__PURE__ */ jsx(Fragment, { children: renderDocumentField({ documentType: field.documentType, value, onChange }) });
394
+ }
395
+ return /* @__PURE__ */ jsxs("p", { className: "ff-panel__hint ff-panel__hint--missing", children: [
396
+ "No document editor supplied. Pass ",
397
+ /* @__PURE__ */ jsx("code", { children: "renderDocumentField" }),
398
+ " to NodeConfigPanel to author this field."
399
+ ] });
367
400
  default:
368
401
  return null;
369
402
  }
370
403
  }
404
+ function normalizeChoices(choices) {
405
+ return choices.map(
406
+ (c) => typeof c === "string" ? { value: c, label: c } : { value: c.value, label: c.label ?? c.value }
407
+ );
408
+ }
409
+ function ChoiceField({
410
+ field,
411
+ value,
412
+ onChange
413
+ }) {
414
+ const options = normalizeChoices(field.choices ?? []);
415
+ const current = typeof value === "string" ? value : "";
416
+ const known = options.some((o) => o.value === current);
417
+ return /* @__PURE__ */ jsxs(
418
+ "select",
419
+ {
420
+ className: "ff-panel__input",
421
+ value: current,
422
+ onChange: (e) => onChange(e.target.value),
423
+ children: [
424
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: field.placeholder ?? "\u2014" }),
425
+ options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value)),
426
+ current !== "" && !known && /* @__PURE__ */ jsxs("option", { value: current, children: [
427
+ current,
428
+ " (not in list)"
429
+ ] })
430
+ ]
431
+ }
432
+ );
433
+ }
434
+ function RepeaterField({
435
+ field,
436
+ value,
437
+ onChange,
438
+ renderCredentialField,
439
+ renderDocumentField
440
+ }) {
441
+ const rows = Array.isArray(value) ? value : [];
442
+ const max = field.maxItems ?? Infinity;
443
+ const min = field.minItems ?? 0;
444
+ const replace = (next) => onChange(next);
445
+ const addRow = () => {
446
+ const blank = {};
447
+ for (const f of field.fields) {
448
+ if ("default" in f && f.default !== void 0) blank[f.key] = f.default;
449
+ }
450
+ replace([...rows, blank]);
451
+ };
452
+ const removeRow = (i) => replace(rows.filter((_, idx) => idx !== i));
453
+ const moveRow = (i, delta) => {
454
+ const target = i + delta;
455
+ if (target < 0 || target >= rows.length) return;
456
+ const next = [...rows];
457
+ [next[i], next[target]] = [next[target], next[i]];
458
+ replace(next);
459
+ };
460
+ const setCell = (i, key, cell) => replace(rows.map((row, idx) => idx === i ? { ...row, [key]: cell } : row));
461
+ const rowTitle = (row, i) => {
462
+ const key = field.titleKey ?? field.fields[0]?.key;
463
+ const raw = key ? row[key] : void 0;
464
+ if (typeof raw === "string" && raw.trim() !== "") return raw;
465
+ if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
466
+ return `Item ${i + 1}`;
467
+ };
468
+ return /* @__PURE__ */ jsxs("div", { className: "ff-repeater", "data-ff-repeater": field.key, children: [
469
+ rows.length === 0 && /* @__PURE__ */ jsx("p", { className: "ff-repeater__empty", children: "None yet." }),
470
+ rows.map((row, i) => /* @__PURE__ */ jsxs("div", { className: "ff-repeater__row", "data-ff-repeater-row": i, children: [
471
+ /* @__PURE__ */ jsxs("div", { className: "ff-repeater__row-head", children: [
472
+ /* @__PURE__ */ jsx("span", { className: "ff-repeater__row-title", children: rowTitle(row, i) }),
473
+ /* @__PURE__ */ jsxs("div", { className: "ff-repeater__row-actions", children: [
474
+ /* @__PURE__ */ jsx(
475
+ "button",
476
+ {
477
+ type: "button",
478
+ className: "ff-repeater__btn",
479
+ onClick: () => moveRow(i, -1),
480
+ disabled: i === 0,
481
+ "aria-label": `Move ${rowTitle(row, i)} up`,
482
+ children: "\u2191"
483
+ }
484
+ ),
485
+ /* @__PURE__ */ jsx(
486
+ "button",
487
+ {
488
+ type: "button",
489
+ className: "ff-repeater__btn",
490
+ onClick: () => moveRow(i, 1),
491
+ disabled: i === rows.length - 1,
492
+ "aria-label": `Move ${rowTitle(row, i)} down`,
493
+ children: "\u2193"
494
+ }
495
+ ),
496
+ /* @__PURE__ */ jsx(
497
+ "button",
498
+ {
499
+ type: "button",
500
+ className: "ff-repeater__btn ff-repeater__btn--danger",
501
+ onClick: () => removeRow(i),
502
+ disabled: rows.length <= min,
503
+ "aria-label": `Remove ${rowTitle(row, i)}`,
504
+ children: "\u2715"
505
+ }
506
+ )
507
+ ] })
508
+ ] }),
509
+ field.fields.map((sub) => /* @__PURE__ */ jsxs("div", { className: "ff-repeater__cell", children: [
510
+ /* @__PURE__ */ jsxs("label", { className: "ff-panel__label ff-panel__label--sub", children: [
511
+ sub.label,
512
+ sub.required && /* @__PURE__ */ jsx("span", { className: "ff-panel__required", "aria-hidden": true, children: " *" })
513
+ ] }),
514
+ /* @__PURE__ */ jsx(
515
+ ConfigFieldRenderer,
516
+ {
517
+ field: sub,
518
+ value: row[sub.key],
519
+ onChange: (cell) => setCell(i, sub.key, cell),
520
+ renderCredentialField,
521
+ renderDocumentField
522
+ }
523
+ )
524
+ ] }, sub.key))
525
+ ] }, i)),
526
+ /* @__PURE__ */ jsxs(
527
+ "button",
528
+ {
529
+ type: "button",
530
+ className: "ff-repeater__add",
531
+ onClick: addRow,
532
+ disabled: rows.length >= max,
533
+ children: [
534
+ "+ ",
535
+ field.addLabel ?? "Add"
536
+ ]
537
+ }
538
+ )
539
+ ] });
540
+ }
541
+ function KeyValueField({
542
+ field,
543
+ value,
544
+ onChange
545
+ }) {
546
+ const map = value && typeof value === "object" && !Array.isArray(value) ? value : {};
547
+ const entries = Object.entries(map);
548
+ const commit = (next) => {
549
+ const obj = {};
550
+ for (const [k, v] of next) {
551
+ if (k === "") continue;
552
+ obj[k] = v;
553
+ }
554
+ onChange(obj);
555
+ };
556
+ const setKey = (i, key) => commit(entries.map(([k, v], idx) => idx === i ? [key, v] : [k, v]));
557
+ const setVal = (i, val) => commit(entries.map(([k, v], idx) => idx === i ? [k, val] : [k, v]));
558
+ const remove = (i) => commit(entries.filter((_, idx) => idx !== i));
559
+ const add = () => commit([...entries, ["", ""]]);
560
+ return /* @__PURE__ */ jsxs("div", { className: "ff-keyvalue", "data-ff-keyvalue": field.key, children: [
561
+ entries.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ff-keyvalue__head", children: [
562
+ /* @__PURE__ */ jsx("span", { children: field.keyLabel ?? "Key" }),
563
+ /* @__PURE__ */ jsx("span", { children: field.valueLabel ?? "Value" }),
564
+ /* @__PURE__ */ jsx("span", {})
565
+ ] }),
566
+ entries.map(([k, v], i) => /* @__PURE__ */ jsxs("div", { className: "ff-keyvalue__row", "data-ff-keyvalue-row": i, children: [
567
+ /* @__PURE__ */ jsx(
568
+ "input",
569
+ {
570
+ className: "ff-panel__input",
571
+ value: k,
572
+ placeholder: field.keyPlaceholder,
573
+ "aria-label": `${field.keyLabel ?? "Key"} ${i + 1}`,
574
+ onChange: (e) => setKey(i, e.target.value)
575
+ }
576
+ ),
577
+ field.valueOptions ? /* @__PURE__ */ jsxs(
578
+ "select",
579
+ {
580
+ className: "ff-panel__input",
581
+ value: v ?? "",
582
+ "aria-label": `${field.valueLabel ?? "Value"} ${i + 1}`,
583
+ onChange: (e) => setVal(i, e.target.value),
584
+ children: [
585
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u2014" }),
586
+ field.valueOptions.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
587
+ ]
588
+ }
589
+ ) : /* @__PURE__ */ jsx(
590
+ "input",
591
+ {
592
+ className: "ff-panel__input",
593
+ value: v ?? "",
594
+ placeholder: field.valuePlaceholder,
595
+ "aria-label": `${field.valueLabel ?? "Value"} ${i + 1}`,
596
+ onChange: (e) => setVal(i, e.target.value)
597
+ }
598
+ ),
599
+ /* @__PURE__ */ jsx(
600
+ "button",
601
+ {
602
+ type: "button",
603
+ className: "ff-repeater__btn ff-repeater__btn--danger",
604
+ onClick: () => remove(i),
605
+ "aria-label": `Remove ${k || `entry ${i + 1}`}`,
606
+ children: "\u2715"
607
+ }
608
+ )
609
+ ] }, i)),
610
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "ff-repeater__add", onClick: add, children: [
611
+ "+ ",
612
+ field.addLabel ?? "Add"
613
+ ] })
614
+ ] });
615
+ }
371
616
  function JsonField({ value, onChange, rows }) {
372
617
  const initial = useMemo(() => {
373
618
  try {
@@ -402,6 +647,7 @@ function NodeConfigPanel({
402
647
  onChange,
403
648
  header,
404
649
  renderCredentialField,
650
+ renderDocumentField,
405
651
  className,
406
652
  style
407
653
  }) {
@@ -427,6 +673,8 @@ function NodeConfigPanel({
427
673
  const setDescription = (description) => onChange({ ...node, data: { ...node.data, description } });
428
674
  const setConfigValue = (key, value) => onChange({ ...node, data: { ...node.data, config: { ...config, [key]: value } } });
429
675
  const issues = validateConfig(kind, config);
676
+ const adapter = getRichInputAdapter();
677
+ const documentField = renderDocumentField ?? (adapter?.renderEditor ? ({ value, onChange: set }) => adapter.renderEditor({ value, onChange: set }) : void 0);
430
678
  return /* @__PURE__ */ jsxs("aside", { className: ["ff-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
431
679
  header,
432
680
  /* @__PURE__ */ jsxs("header", { className: "ff-panel__header", children: [
@@ -475,7 +723,8 @@ function NodeConfigPanel({
475
723
  field,
476
724
  value: config[field.key],
477
725
  onChange: (v) => setConfigValue(field.key, v),
478
- renderCredentialField
726
+ renderCredentialField,
727
+ renderDocumentField: documentField
479
728
  }
480
729
  )
481
730
  ] }, field.key))
@@ -523,6 +772,17 @@ function removeEdges(edges, ids) {
523
772
  const doomed = new Set(ids);
524
773
  return edges.filter((e) => !doomed.has(e.id));
525
774
  }
775
+ function setEdgeLabel(edges, id, label) {
776
+ const next = label?.trim();
777
+ return edges.map((e) => {
778
+ if (e.id !== id) return e;
779
+ if (!next) {
780
+ const { label: _drop, ...rest } = e;
781
+ return rest;
782
+ }
783
+ return { ...e, label: next };
784
+ });
785
+ }
526
786
  function duplicateNode(node, id, offset = 40) {
527
787
  return {
528
788
  ...node,
@@ -570,6 +830,7 @@ function FlowEditorInner({
570
830
  onChange,
571
831
  onSelectionChange,
572
832
  onDelete,
833
+ onEdgeDelete,
573
834
  apiRef
574
835
  }) {
575
836
  const internal = useFlowState(initial);
@@ -587,18 +848,39 @@ function FlowEditorInner({
587
848
  const [selectedId, setSelectedId] = useState(null);
588
849
  const selected = useMemo(() => flow.nodes.find((n) => n.id === selectedId) ?? null, [flow.nodes, selectedId]);
589
850
  useEffect(() => onSelectionChange?.(selected), [selected, onSelectionChange]);
590
- const handleNodeClick = (_e, node) => setSelectedId(node.id);
851
+ const [selectedEdgeId, setSelectedEdgeId] = useState(null);
852
+ const selectedEdge = useMemo(
853
+ () => flow.edges.find((e) => e.id === selectedEdgeId) ?? null,
854
+ [flow.edges, selectedEdgeId]
855
+ );
856
+ const handleNodeClick = (_e, node) => {
857
+ setSelectedId(node.id);
858
+ setSelectedEdgeId(null);
859
+ };
591
860
  const [menu, setMenu] = useState(null);
861
+ const [labelEdit, setLabelEdit] = useState(null);
592
862
  const closeMenu = useCallback(() => setMenu(null), []);
593
863
  const handleNodeContextMenu = (event, node) => {
594
864
  event.preventDefault();
595
865
  setSelectedId(node.id);
596
- setMenu({ x: event.clientX, y: event.clientY, nodeId: node.id });
866
+ setSelectedEdgeId(null);
867
+ setMenu({ x: event.clientX, y: event.clientY, target: { type: "node", id: node.id } });
868
+ };
869
+ const handleEdgeClick = (_e, edge) => {
870
+ setSelectedEdgeId(edge.id);
871
+ };
872
+ const handleEdgeContextMenu = (event, edge) => {
873
+ event.preventDefault();
874
+ setSelectedEdgeId(edge.id);
875
+ setMenu({ x: event.clientX, y: event.clientY, target: { type: "edge", id: edge.id } });
597
876
  };
598
877
  useEffect(() => {
599
- if (menu === null) return;
600
- const close = () => setMenu(null);
601
- const onKey = (e) => e.key === "Escape" && setMenu(null);
878
+ if (menu === null && labelEdit === null) return;
879
+ const close = () => {
880
+ setMenu(null);
881
+ setLabelEdit(null);
882
+ };
883
+ const onKey = (e) => e.key === "Escape" && close();
602
884
  window.addEventListener("click", close);
603
885
  window.addEventListener("scroll", close, true);
604
886
  window.addEventListener("keydown", onKey);
@@ -607,7 +889,7 @@ function FlowEditorInner({
607
889
  window.removeEventListener("scroll", close, true);
608
890
  window.removeEventListener("keydown", onKey);
609
891
  };
610
- }, [menu]);
892
+ }, [menu, labelEdit]);
611
893
  useEffect(() => {
612
894
  if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
613
895
  }, [flow.nodes, flow.edges, onChange, controlled]);
@@ -650,14 +932,29 @@ function FlowEditorInner({
650
932
  edges: flow.edges,
651
933
  selectedId,
652
934
  selected,
935
+ selectedEdgeId,
936
+ selectedEdge,
653
937
  running: runner.running,
654
938
  statuses: runner.statuses,
655
939
  select: setSelectedId,
940
+ selectEdge: setSelectedEdgeId,
656
941
  addNode,
657
942
  updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
658
943
  deleteNodes,
659
944
  deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
660
- deleteEdges: (ids) => flow.setEdges((all) => removeEdges(all, ids)),
945
+ deleteEdges: (ids) => {
946
+ if (ids.length === 0) return;
947
+ flow.setEdges((all) => removeEdges(all, ids));
948
+ setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
949
+ onEdgeDelete?.(ids);
950
+ },
951
+ deleteSelectedEdge: () => {
952
+ if (!selectedEdgeId) return;
953
+ flow.setEdges((all) => removeEdges(all, [selectedEdgeId]));
954
+ setSelectedEdgeId(null);
955
+ onEdgeDelete?.([selectedEdgeId]);
956
+ },
957
+ setEdgeLabel: (id, label) => flow.setEdges((all) => setEdgeLabel(all, id, label)),
661
958
  duplicateNode: (id) => {
662
959
  const src = flow.nodes.find((n) => n.id === id);
663
960
  if (!src) return null;
@@ -729,6 +1026,9 @@ function FlowEditorInner({
729
1026
  onConnect: flow.onConnect,
730
1027
  onNodeClick: handleNodeClick,
731
1028
  onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
1029
+ onEdgeClick: handleEdgeClick,
1030
+ onEdgeContextMenu: builtins.edgeContextMenu === false ? void 0 : handleEdgeContextMenu,
1031
+ onEdgesDelete: (deleted) => onEdgeDelete?.(deleted.map((e) => e.id)),
732
1032
  onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
733
1033
  deleteKeyCode: ["Delete", "Backspace"],
734
1034
  height: "100%",
@@ -737,14 +1037,14 @@ function FlowEditorInner({
737
1037
  }
738
1038
  ),
739
1039
  slots.empty && api.nodes.length === 0 && /* @__PURE__ */ jsx("div", { className: "ff-editor__empty", children: slots.empty(api) }),
740
- menu !== null && builtins.contextMenu !== false && /* @__PURE__ */ jsx(
1040
+ menu?.target.type === "node" && builtins.contextMenu !== false && /* @__PURE__ */ jsx(
741
1041
  "div",
742
1042
  {
743
1043
  className: "ff-editor__ctx",
744
1044
  style: { top: menu.y, left: menu.x },
745
1045
  role: "menu",
746
1046
  onClick: (e) => e.stopPropagation(),
747
- children: slots.contextMenu ? slots.contextMenu(api, menu.nodeId, closeMenu) : /* @__PURE__ */ jsxs(Fragment, { children: [
1047
+ children: slots.contextMenu ? slots.contextMenu(api, menu.target.id, closeMenu) : /* @__PURE__ */ jsxs(Fragment, { children: [
748
1048
  /* @__PURE__ */ jsx(
749
1049
  "button",
750
1050
  {
@@ -753,7 +1053,7 @@ function FlowEditorInner({
753
1053
  className: "ff-editor__ctx-item",
754
1054
  "data-action": "ctx-duplicate",
755
1055
  onClick: () => {
756
- api.duplicateNode(menu.nodeId);
1056
+ api.duplicateNode(menu.target.id);
757
1057
  closeMenu();
758
1058
  },
759
1059
  children: "Duplicate"
@@ -767,7 +1067,7 @@ function FlowEditorInner({
767
1067
  className: "ff-editor__ctx-item ff-editor__ctx-item--danger",
768
1068
  "data-action": "ctx-delete",
769
1069
  onClick: () => {
770
- api.deleteNodes([menu.nodeId]);
1070
+ api.deleteNodes([menu.target.id]);
771
1071
  closeMenu();
772
1072
  },
773
1073
  children: "Delete"
@@ -776,6 +1076,58 @@ function FlowEditorInner({
776
1076
  ] })
777
1077
  }
778
1078
  ),
1079
+ menu?.target.type === "edge" && builtins.edgeContextMenu !== false && /* @__PURE__ */ jsx(
1080
+ "div",
1081
+ {
1082
+ className: "ff-editor__ctx",
1083
+ style: { top: menu.y, left: menu.x },
1084
+ role: "menu",
1085
+ onClick: (e) => e.stopPropagation(),
1086
+ children: slots.edgeContextMenu ? slots.edgeContextMenu(api, menu.target.id, closeMenu) : /* @__PURE__ */ jsxs(Fragment, { children: [
1087
+ /* @__PURE__ */ jsx(
1088
+ "button",
1089
+ {
1090
+ type: "button",
1091
+ role: "menuitem",
1092
+ className: "ff-editor__ctx-item",
1093
+ "data-action": "ctx-edge-label",
1094
+ onClick: () => {
1095
+ setLabelEdit({ x: menu.x, y: menu.y, edgeId: menu.target.id });
1096
+ setMenu(null);
1097
+ },
1098
+ children: "Label\u2026"
1099
+ }
1100
+ ),
1101
+ /* @__PURE__ */ jsx(
1102
+ "button",
1103
+ {
1104
+ type: "button",
1105
+ role: "menuitem",
1106
+ className: "ff-editor__ctx-item ff-editor__ctx-item--danger",
1107
+ "data-action": "ctx-edge-delete",
1108
+ onClick: () => {
1109
+ api.deleteEdges([menu.target.id]);
1110
+ closeMenu();
1111
+ },
1112
+ children: "Delete connection"
1113
+ }
1114
+ )
1115
+ ] })
1116
+ }
1117
+ ),
1118
+ labelEdit !== null && /* @__PURE__ */ jsx(
1119
+ EdgeLabelEditor,
1120
+ {
1121
+ x: labelEdit.x,
1122
+ y: labelEdit.y,
1123
+ initial: flow.edges.find((e) => e.id === labelEdit.edgeId)?.label ?? "",
1124
+ onCommit: (text) => {
1125
+ api.setEdgeLabel(labelEdit.edgeId, text);
1126
+ setLabelEdit(null);
1127
+ },
1128
+ onCancel: () => setLabelEdit(null)
1129
+ }
1130
+ ),
779
1131
  showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" }))
780
1132
  ] }),
781
1133
  showPanel && (slots.panel ? slots.panel(api) : /* @__PURE__ */ jsxs("div", { className: "ff-editor__panel-wrap", children: [
@@ -852,6 +1204,46 @@ function makeControlledFlowAdapter(value, onChange) {
852
1204
  toGraph: () => value
853
1205
  };
854
1206
  }
1207
+ function EdgeLabelEditor({
1208
+ x,
1209
+ y,
1210
+ initial,
1211
+ onCommit,
1212
+ onCancel
1213
+ }) {
1214
+ const [text, setText] = useState(initial);
1215
+ const ref = useRef(null);
1216
+ useEffect(() => {
1217
+ ref.current?.focus();
1218
+ ref.current?.select();
1219
+ }, []);
1220
+ return /* @__PURE__ */ jsx(
1221
+ "div",
1222
+ {
1223
+ className: "ff-editor__ctx ff-editor__edge-label",
1224
+ style: { top: y, left: x },
1225
+ onClick: (e) => e.stopPropagation(),
1226
+ children: /* @__PURE__ */ jsx(
1227
+ "input",
1228
+ {
1229
+ ref,
1230
+ className: "ff-panel__input",
1231
+ value: text,
1232
+ placeholder: "Label this connection",
1233
+ "aria-label": "Connection label",
1234
+ "data-action": "edge-label-input",
1235
+ onChange: (e) => setText(e.target.value),
1236
+ onKeyDown: (e) => {
1237
+ e.stopPropagation();
1238
+ if (e.key === "Enter") onCommit(text);
1239
+ if (e.key === "Escape") onCancel();
1240
+ },
1241
+ onBlur: () => onCommit(text)
1242
+ }
1243
+ )
1244
+ }
1245
+ );
1246
+ }
855
1247
  function defineNode(render) {
856
1248
  function Wrapped(props) {
857
1249
  return render({