@particle-academy/fancy-flow 0.8.0 → 0.10.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 (55) hide show
  1. package/README.md +104 -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-WNVBXXOL.js → chunk-CPSOC27D.js} +43 -2
  5. package/dist/chunk-CPSOC27D.js.map +1 -0
  6. package/dist/chunk-F5RPRB7A.js +56 -0
  7. package/dist/chunk-F5RPRB7A.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-M2XKGQQL.js → chunk-NCPQDVUE.js} +178 -16
  11. package/dist/chunk-NCPQDVUE.js.map +1 -0
  12. package/dist/chunk-TITD5W4Y.js +26 -0
  13. package/dist/chunk-TITD5W4Y.js.map +1 -0
  14. package/dist/{chunk-QSSQRQN4.js → chunk-VEI743ZX.js} +3 -3
  15. package/dist/{chunk-QSSQRQN4.js.map → chunk-VEI743ZX.js.map} +1 -1
  16. package/dist/engine.cjs +32 -2
  17. package/dist/engine.cjs.map +1 -1
  18. package/dist/engine.js +3 -1
  19. package/dist/index.cjs +716 -40
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +45 -6
  22. package/dist/index.d.ts +45 -6
  23. package/dist/index.js +417 -23
  24. package/dist/index.js.map +1 -1
  25. package/dist/registry/index.d.cts +112 -4
  26. package/dist/registry/index.d.ts +112 -4
  27. package/dist/registry.cjs +312 -31
  28. package/dist/registry.cjs.map +1 -1
  29. package/dist/registry.js +4 -2
  30. package/dist/rich-input.cjs +56 -0
  31. package/dist/rich-input.cjs.map +1 -0
  32. package/dist/rich-input.d.cts +27 -0
  33. package/dist/rich-input.d.ts +27 -0
  34. package/dist/rich-input.js +37 -0
  35. package/dist/rich-input.js.map +1 -0
  36. package/dist/runtime.cjs +32 -2
  37. package/dist/runtime.cjs.map +1 -1
  38. package/dist/runtime.js +4 -2
  39. package/dist/schema.cjs +41 -0
  40. package/dist/schema.cjs.map +1 -1
  41. package/dist/schema.js +2 -2
  42. package/dist/styles.css +191 -0
  43. package/dist/styles.css.map +1 -1
  44. package/dist/types-BocBFh6l.d.ts +221 -0
  45. package/dist/types-DKqaUjF_.d.cts +221 -0
  46. package/dist/ux.cjs.map +1 -1
  47. package/dist/ux.d.cts +1 -1
  48. package/dist/ux.d.ts +1 -1
  49. package/dist/ux.js +1 -1
  50. package/package.json +24 -2
  51. package/dist/chunk-M2XKGQQL.js.map +0 -1
  52. package/dist/chunk-NVULCEDX.js.map +0 -1
  53. package/dist/chunk-WNVBXXOL.js.map +0 -1
  54. package/dist/types-C0wdN6QX.d.cts +0 -121
  55. package/dist/types-DnMe9Vsf.d.ts +0 -121
package/dist/index.cjs CHANGED
@@ -8,6 +8,8 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
 
9
9
  var ReactExports__default = /*#__PURE__*/_interopDefault(ReactExports);
10
10
 
11
+ // src/registry/builtin.ts
12
+
11
13
  // src/registry/registry.ts
12
14
  var kinds = /* @__PURE__ */ new Map();
13
15
  var listeners = /* @__PURE__ */ new Set();
@@ -81,6 +83,47 @@ function validateField(field, value) {
81
83
  case "json":
82
84
  return null;
83
85
  // permissive — just JSON-shaped
86
+ case "repeater": {
87
+ if (!Array.isArray(value)) return `${field.label} must be a list`;
88
+ if (field.minItems !== void 0 && value.length < field.minItems) {
89
+ return `${field.label} needs at least ${field.minItems}`;
90
+ }
91
+ if (field.maxItems !== void 0 && value.length > field.maxItems) {
92
+ return `${field.label} allows at most ${field.maxItems}`;
93
+ }
94
+ for (let i = 0; i < value.length; i++) {
95
+ const row = value[i];
96
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
97
+ return `${field.label} item ${i + 1} must be an object`;
98
+ }
99
+ for (const sub of field.fields) {
100
+ const cell = row[sub.key];
101
+ if (sub.required && (cell === void 0 || cell === null || cell === "")) {
102
+ return `${field.label} item ${i + 1}: ${sub.label} is required`;
103
+ }
104
+ if (cell === void 0 || cell === null) continue;
105
+ const issue = validateField(sub, cell);
106
+ if (issue) return `${field.label} item ${i + 1}: ${issue}`;
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ case "keyvalue": {
112
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
113
+ return `${field.label} must be a key/value map`;
114
+ }
115
+ const allowed = field.valueOptions?.map((o) => o.value);
116
+ for (const [k, v] of Object.entries(value)) {
117
+ if (typeof v !== "string") return `${field.label}: "${k}" must be a string`;
118
+ if (allowed && !allowed.includes(v)) {
119
+ return `${field.label}: "${k}" must be one of ${allowed.join(", ")}`;
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ case "document":
125
+ return null;
126
+ // opaque to fancy-flow — the host's editor owns its shape
84
127
  default:
85
128
  return null;
86
129
  }
@@ -105,8 +148,90 @@ function categoryAccent(category) {
105
148
  return "#71717a";
106
149
  }
107
150
  }
151
+ var adapter = null;
152
+ var listeners2 = /* @__PURE__ */ new Set();
153
+ function registerRichInputAdapter(next) {
154
+ adapter = next;
155
+ for (const l of listeners2) l();
156
+ return () => {
157
+ if (adapter === next) {
158
+ adapter = null;
159
+ for (const l of listeners2) l();
160
+ }
161
+ };
162
+ }
163
+ function getRichInputAdapter() {
164
+ return adapter;
165
+ }
166
+ function isRichInputEnabled() {
167
+ return adapter !== null && (adapter.renderDocument !== void 0 || adapter.renderEditor !== void 0);
168
+ }
169
+ function onRichInputAdapterChanged(fn) {
170
+ listeners2.add(fn);
171
+ return () => listeners2.delete(fn);
172
+ }
173
+ function RichInputPreview({ config }) {
174
+ const a = getRichInputAdapter();
175
+ const title = typeof config.title === "string" && config.title.trim() !== "" ? config.title : "Untitled step";
176
+ const doc = config.document;
177
+ if (!a || !a.renderDocument && !a.renderEditor) {
178
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-rich-preview", children: [
179
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-rich-preview__title", children: title }),
180
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-rich-preview__unavailable", children: [
181
+ "Add ",
182
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "@particle-academy/fancy-cms-ui" }),
183
+ " +",
184
+ " ",
185
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "@particle-academy/react-fancy" }),
186
+ ", then",
187
+ " ",
188
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: 'import "@particle-academy/fancy-flow/rich-input"' }),
189
+ "."
190
+ ] })
191
+ ] });
192
+ }
193
+ const body = doc === void 0 || doc === null ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-rich-preview__unavailable", children: "Nothing authored yet." }) : a.renderDocument?.(doc) ?? null;
194
+ const Frame = a.FauxClient;
195
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-rich-preview", children: [
196
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-rich-preview__title", children: title }),
197
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-rich-preview__frame", children: Frame ? /* @__PURE__ */ jsxRuntime.jsx(Frame, { ...a.frameProps ?? { variant: "browser" }, children: body }) : body })
198
+ ] });
199
+ }
108
200
 
109
201
  // src/registry/builtin.ts
202
+ function casePorts(cases) {
203
+ const byPort = /* @__PURE__ */ new Map();
204
+ if (cases && typeof cases === "object" && !Array.isArray(cases)) {
205
+ for (const [match, port] of Object.entries(cases)) {
206
+ if (typeof port !== "string" || port === "" || port === "default") continue;
207
+ const matches = byPort.get(port) ?? [];
208
+ matches.push(match);
209
+ byPort.set(port, matches);
210
+ }
211
+ }
212
+ const ports = [...byPort].map(([id2, matches]) => ({
213
+ id: id2,
214
+ label: matches.join("|")
215
+ }));
216
+ return [...ports, { id: "default", label: "default" }];
217
+ }
218
+ function routePorts(routes, fallback) {
219
+ const ports = [];
220
+ const seen = /* @__PURE__ */ new Set();
221
+ if (Array.isArray(routes)) {
222
+ for (const route of routes) {
223
+ const id2 = route?.port;
224
+ if (typeof id2 !== "string" || id2.trim() === "" || seen.has(id2)) continue;
225
+ seen.add(id2);
226
+ ports.push({ id: id2, label: id2 });
227
+ }
228
+ }
229
+ if (fallback !== false && !seen.has("fallback")) {
230
+ ports.push({ id: "fallback", label: "fallback" });
231
+ }
232
+ if (ports.length === 0) ports.push({ id: "out" });
233
+ return ports;
234
+ }
110
235
  var HTTP_METHODS = [
111
236
  { type: "select", key: "method", label: "Method", options: [
112
237
  { value: "GET", label: "GET" },
@@ -175,15 +300,59 @@ var KINDS = [
175
300
  configSchema: [
176
301
  { type: "text", key: "title", label: "Form title", default: "Need your input" },
177
302
  {
178
- type: "json",
303
+ type: "repeater",
179
304
  key: "fields",
180
- label: "Fields (JSON)",
181
- language: "json",
182
- rows: 6,
183
- default: [{ key: "answer", label: "Your answer", type: "textarea" }]
305
+ label: "Fields",
306
+ description: "The form the run pauses on.",
307
+ titleKey: "label",
308
+ addLabel: "Add field",
309
+ minItems: 1,
310
+ fields: [
311
+ { type: "text", key: "key", label: "Key", required: true, placeholder: "answer" },
312
+ { type: "text", key: "label", label: "Label", required: true, placeholder: "Your answer" },
313
+ {
314
+ type: "select",
315
+ key: "type",
316
+ label: "Type",
317
+ default: "text",
318
+ options: [
319
+ { value: "text", label: "Text" },
320
+ { value: "textarea", label: "Long text" },
321
+ { value: "number", label: "Number" },
322
+ { value: "select", label: "Select" },
323
+ { value: "switch", label: "Switch" }
324
+ ]
325
+ },
326
+ { type: "switch", key: "required", label: "Required", default: false }
327
+ ],
328
+ default: [{ key: "answer", label: "Your answer", type: "textarea", required: true }]
184
329
  }
185
330
  ]
186
331
  },
332
+ {
333
+ name: "rich_user_input",
334
+ category: "human",
335
+ label: "Rich User Input",
336
+ description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
337
+ icon: "\u25A4",
338
+ inputs: [{ id: "in" }],
339
+ outputs: [{ id: "out", label: "values" }],
340
+ configSchema: [
341
+ { type: "text", key: "title", label: "Step title", default: "Please review" },
342
+ {
343
+ type: "document",
344
+ key: "document",
345
+ label: "Page content",
346
+ documentType: "stages",
347
+ description: "Authored with the host's document editor (fancy-cms Stages)."
348
+ },
349
+ { type: "switch", key: "requireConfirm", label: "Require explicit confirmation", default: true },
350
+ { type: "text", key: "submitLabel", label: "Submit button", default: "Continue" }
351
+ ],
352
+ // Preview the authored page inside a FauxClient frame, so the canvas shows
353
+ // what the person hitting this step will actually see.
354
+ renderBody: (ctx) => ReactExports.createElement(RichInputPreview, { config: ctx.config ?? {} })
355
+ },
187
356
  // ───────────── Logic ─────────────
188
357
  {
189
358
  name: "branch",
@@ -204,10 +373,24 @@ var KINDS = [
204
373
  description: "Route to one of N labelled outputs based on a key.",
205
374
  icon: "\u2933",
206
375
  inputs: [{ id: "in" }],
207
- outputs: [{ id: "case_a", label: "a" }, { id: "case_b", label: "b" }, { id: "default", label: "default" }],
376
+ // Ports ARE the config: every distinct port a case routes to becomes an
377
+ // output handle, plus the always-present `default`. Editing the cases map
378
+ // moves the ports on the canvas and the ports the runtime activates.
379
+ outputs: (config) => casePorts(config?.cases),
208
380
  configSchema: [
209
381
  { type: "expression", key: "value", label: "Switch on", example: "{{ $json.kind }}", required: true },
210
- { type: "json", key: "cases", label: "Cases (JSON)", default: { a: "case_a", b: "case_b" } }
382
+ {
383
+ type: "keyvalue",
384
+ key: "cases",
385
+ label: "Cases",
386
+ description: "Match value \u2192 output port. Unmatched input takes `default`.",
387
+ keyLabel: "When value is",
388
+ valueLabel: "Route to port",
389
+ keyPlaceholder: "a",
390
+ valuePlaceholder: "case_a",
391
+ addLabel: "Add case",
392
+ default: { a: "case_a", b: "case_b" }
393
+ }
211
394
  ]
212
395
  },
213
396
  {
@@ -362,6 +545,76 @@ var KINDS = [
362
545
  { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
363
546
  ]
364
547
  },
548
+ {
549
+ name: "llm_branch",
550
+ category: "ai",
551
+ label: "LLM Router",
552
+ description: "Let a model choose which route the flow takes.",
553
+ icon: "\u2727",
554
+ inputs: [{ id: "in" }],
555
+ // Each declared route is a port. The executor returns `{ __port: id }`
556
+ // (or `Port.only(id)` on the PHP runtime) to pick one.
557
+ outputs: (config) => routePorts(config?.routes, config?.fallback),
558
+ configSchema: [
559
+ {
560
+ type: "textarea",
561
+ key: "system",
562
+ label: "System prompt",
563
+ rows: 3,
564
+ description: "Optional framing for the routing decision."
565
+ },
566
+ {
567
+ type: "expression",
568
+ key: "prompt",
569
+ label: "What to route on",
570
+ example: "{{ $json.message }}",
571
+ required: true
572
+ },
573
+ {
574
+ type: "repeater",
575
+ key: "routes",
576
+ label: "Routes",
577
+ description: "The model picks exactly one. Descriptions are what it chooses between \u2014 make them distinct.",
578
+ titleKey: "port",
579
+ addLabel: "Add route",
580
+ minItems: 2,
581
+ fields: [
582
+ { type: "text", key: "port", label: "Port", required: true, placeholder: "billing" },
583
+ {
584
+ type: "text",
585
+ key: "description",
586
+ label: "When to choose it",
587
+ required: true,
588
+ placeholder: "The user is asking about an invoice, refund, or payment."
589
+ }
590
+ ],
591
+ default: [
592
+ { port: "a", description: "Describe when the model should pick this route." },
593
+ { port: "b", description: "Describe when the model should pick this route." }
594
+ ]
595
+ },
596
+ {
597
+ type: "select",
598
+ key: "provider",
599
+ label: "Provider",
600
+ default: "anthropic",
601
+ options: [
602
+ { value: "anthropic", label: "Anthropic" },
603
+ { value: "openai", label: "OpenAI" },
604
+ { value: "custom", label: "Custom" }
605
+ ]
606
+ },
607
+ { type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5" },
608
+ {
609
+ type: "switch",
610
+ key: "fallback",
611
+ label: "Add a `fallback` port",
612
+ default: true,
613
+ description: "Where the flow goes if the model returns no usable route."
614
+ },
615
+ { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
616
+ ]
617
+ },
365
618
  {
366
619
  name: "tool_use",
367
620
  category: "ai",
@@ -1474,7 +1727,7 @@ function defaultTouchable() {
1474
1727
  return navigator.maxTouchPoints || "ontouchstart" in this;
1475
1728
  }
1476
1729
  function drag_default() {
1477
- var filter2 = defaultFilter, container = defaultContainer, subject = defaultSubject, touchable = defaultTouchable, gestures = {}, listeners2 = dispatch_default("start", "drag", "end"), active = 0, mousedownx, mousedowny, mousemoving, touchending, clickDistance2 = 0;
1730
+ var filter2 = defaultFilter, container = defaultContainer, subject = defaultSubject, touchable = defaultTouchable, gestures = {}, listeners3 = dispatch_default("start", "drag", "end"), active = 0, mousedownx, mousedowny, mousemoving, touchending, clickDistance2 = 0;
1478
1731
  function drag(selection2) {
1479
1732
  selection2.on("mousedown.drag", mousedowned).filter(touchable).on("touchstart.drag", touchstarted).on("touchmove.drag", touchmoved, nonpassive).on("touchend.drag touchcancel.drag", touchended).style("touch-action", "none").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
1480
1733
  }
@@ -1537,7 +1790,7 @@ function drag_default() {
1537
1790
  }
1538
1791
  }
1539
1792
  function beforestart(that, container2, event, d, identifier, touch) {
1540
- var dispatch2 = listeners2.copy(), p = pointer_default(touch || event, container2), dx, dy, s;
1793
+ var dispatch2 = listeners3.copy(), p = pointer_default(touch || event, container2), dx, dy, s;
1541
1794
  if ((s = subject.call(that, new DragEvent("beforestart", {
1542
1795
  sourceEvent: event,
1543
1796
  target: drag,
@@ -1596,8 +1849,8 @@ function drag_default() {
1596
1849
  return arguments.length ? (touchable = typeof _ === "function" ? _ : constant_default2(!!_), drag) : touchable;
1597
1850
  };
1598
1851
  drag.on = function() {
1599
- var value = listeners2.on.apply(listeners2, arguments);
1600
- return value === listeners2 ? drag : value;
1852
+ var value = listeners3.on.apply(listeners3, arguments);
1853
+ return value === listeners3 ? drag : value;
1601
1854
  };
1602
1855
  drag.clickDistance = function(_) {
1603
1856
  return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);
@@ -3151,7 +3404,7 @@ function defaultConstrain(transform2, extent, translateExtent) {
3151
3404
  );
3152
3405
  }
3153
3406
  function zoom_default2() {
3154
- var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta2 = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners2 = dispatch_default("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
3407
+ var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta2 = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners3 = dispatch_default("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
3155
3408
  function zoom(selection2) {
3156
3409
  selection2.property("__zoom", defaultTransform).on("wheel.zoom", wheeled, { passive: false }).on("mousedown.zoom", mousedowned).on("dblclick.zoom", dblclicked).filter(touchable).on("touchstart.zoom", touchstarted).on("touchmove.zoom", touchmoved).on("touchend.zoom touchcancel.zoom", touchended).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
3157
3410
  }
@@ -3263,14 +3516,14 @@ function zoom_default2() {
3263
3516
  },
3264
3517
  emit: function(type) {
3265
3518
  var d = select_default2(this.that).datum();
3266
- listeners2.call(
3519
+ listeners3.call(
3267
3520
  type,
3268
3521
  this.that,
3269
3522
  new ZoomEvent(type, {
3270
3523
  sourceEvent: this.sourceEvent,
3271
3524
  target: zoom,
3272
3525
  transform: this.that.__zoom,
3273
- dispatch: listeners2
3526
+ dispatch: listeners3
3274
3527
  }),
3275
3528
  d
3276
3529
  );
@@ -3420,8 +3673,8 @@ function zoom_default2() {
3420
3673
  return arguments.length ? (interpolate = _, zoom) : interpolate;
3421
3674
  };
3422
3675
  zoom.on = function() {
3423
- var value = listeners2.on.apply(listeners2, arguments);
3424
- return value === listeners2 ? zoom : value;
3676
+ var value = listeners3.on.apply(listeners3, arguments);
3677
+ return value === listeners3 ? zoom : value;
3425
3678
  };
3426
3679
  zoom.clickDistance = function(_) {
3427
3680
  return arguments.length ? (clickDistance2 = (_ = +_) * _, zoom) : Math.sqrt(clickDistance2);
@@ -6094,20 +6347,20 @@ var use_sync_external_store_with_selector_default = { useSyncExternalStoreWithSe
6094
6347
  // node_modules/zustand/esm/vanilla.mjs
6095
6348
  var createStoreImpl = (createState) => {
6096
6349
  let state;
6097
- const listeners2 = /* @__PURE__ */ new Set();
6350
+ const listeners3 = /* @__PURE__ */ new Set();
6098
6351
  const setState = (partial, replace) => {
6099
6352
  const nextState = typeof partial === "function" ? partial(state) : partial;
6100
6353
  if (!Object.is(nextState, state)) {
6101
6354
  const previousState = state;
6102
6355
  state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
6103
- listeners2.forEach((listener) => listener(state, previousState));
6356
+ listeners3.forEach((listener) => listener(state, previousState));
6104
6357
  }
6105
6358
  };
6106
6359
  const getState = () => state;
6107
6360
  const getInitialState2 = () => initialState;
6108
6361
  const subscribe = (listener) => {
6109
- listeners2.add(listener);
6110
- return () => listeners2.delete(listener);
6362
+ listeners3.add(listener);
6363
+ return () => listeners3.delete(listener);
6111
6364
  };
6112
6365
  const destroy = () => {
6113
6366
  if ((undefined ? undefined.MODE : void 0) !== "production") {
@@ -6115,7 +6368,7 @@ var createStoreImpl = (createState) => {
6115
6368
  "[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."
6116
6369
  );
6117
6370
  }
6118
- listeners2.clear();
6371
+ listeners3.clear();
6119
6372
  };
6120
6373
  const api = { setState, getState, getInitialState: getInitialState2, subscribe, destroy };
6121
6374
  const initialState = state = createState(setState, getState, api);
@@ -9647,9 +9900,18 @@ function paletteDropHandlers(onDrop) {
9647
9900
  }
9648
9901
  };
9649
9902
  }
9650
- function ConfigFieldRenderer({ field, value, onChange, renderCredentialField }) {
9903
+ function ConfigFieldRenderer({
9904
+ field,
9905
+ value,
9906
+ onChange,
9907
+ renderCredentialField,
9908
+ renderDocumentField
9909
+ }) {
9651
9910
  switch (field.type) {
9652
- case "text":
9911
+ case "text": {
9912
+ if (field.choices?.length) {
9913
+ return /* @__PURE__ */ jsxRuntime.jsx(ChoiceField, { field, value, onChange });
9914
+ }
9653
9915
  return /* @__PURE__ */ jsxRuntime.jsx(
9654
9916
  "input",
9655
9917
  {
@@ -9660,6 +9922,7 @@ function ConfigFieldRenderer({ field, value, onChange, renderCredentialField })
9660
9922
  onChange: (e) => onChange(e.target.value)
9661
9923
  }
9662
9924
  );
9925
+ }
9663
9926
  case "textarea":
9664
9927
  return /* @__PURE__ */ jsxRuntime.jsx(
9665
9928
  "textarea",
@@ -9737,10 +10000,244 @@ function ConfigFieldRenderer({ field, value, onChange, renderCredentialField })
9737
10000
  onChange: (e) => onChange(e.target.value)
9738
10001
  }
9739
10002
  );
10003
+ case "repeater":
10004
+ return /* @__PURE__ */ jsxRuntime.jsx(
10005
+ RepeaterField,
10006
+ {
10007
+ field,
10008
+ value,
10009
+ onChange,
10010
+ renderCredentialField,
10011
+ renderDocumentField
10012
+ }
10013
+ );
10014
+ case "keyvalue":
10015
+ return /* @__PURE__ */ jsxRuntime.jsx(KeyValueField, { field, value, onChange });
10016
+ case "document":
10017
+ if (renderDocumentField) {
10018
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderDocumentField({ documentType: field.documentType, value, onChange }) });
10019
+ }
10020
+ return /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "ff-panel__hint ff-panel__hint--missing", children: [
10021
+ "No document editor supplied. Pass ",
10022
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "renderDocumentField" }),
10023
+ " to NodeConfigPanel to author this field."
10024
+ ] });
9740
10025
  default:
9741
10026
  return null;
9742
10027
  }
9743
10028
  }
10029
+ function normalizeChoices(choices) {
10030
+ return choices.map(
10031
+ (c) => typeof c === "string" ? { value: c, label: c } : { value: c.value, label: c.label ?? c.value }
10032
+ );
10033
+ }
10034
+ function ChoiceField({
10035
+ field,
10036
+ value,
10037
+ onChange
10038
+ }) {
10039
+ const options = normalizeChoices(field.choices ?? []);
10040
+ const current = typeof value === "string" ? value : "";
10041
+ const known = options.some((o) => o.value === current);
10042
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10043
+ "select",
10044
+ {
10045
+ className: "ff-panel__input",
10046
+ value: current,
10047
+ onChange: (e) => onChange(e.target.value),
10048
+ children: [
10049
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: field.placeholder ?? "\u2014" }),
10050
+ options.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o.value, children: o.label }, o.value)),
10051
+ current !== "" && !known && /* @__PURE__ */ jsxRuntime.jsxs("option", { value: current, children: [
10052
+ current,
10053
+ " (not in list)"
10054
+ ] })
10055
+ ]
10056
+ }
10057
+ );
10058
+ }
10059
+ function RepeaterField({
10060
+ field,
10061
+ value,
10062
+ onChange,
10063
+ renderCredentialField,
10064
+ renderDocumentField
10065
+ }) {
10066
+ const rows = Array.isArray(value) ? value : [];
10067
+ const max = field.maxItems ?? Infinity;
10068
+ const min = field.minItems ?? 0;
10069
+ const replace = (next) => onChange(next);
10070
+ const addRow = () => {
10071
+ const blank = {};
10072
+ for (const f of field.fields) {
10073
+ if ("default" in f && f.default !== void 0) blank[f.key] = f.default;
10074
+ }
10075
+ replace([...rows, blank]);
10076
+ };
10077
+ const removeRow = (i) => replace(rows.filter((_, idx) => idx !== i));
10078
+ const moveRow = (i, delta) => {
10079
+ const target = i + delta;
10080
+ if (target < 0 || target >= rows.length) return;
10081
+ const next = [...rows];
10082
+ [next[i], next[target]] = [next[target], next[i]];
10083
+ replace(next);
10084
+ };
10085
+ const setCell = (i, key, cell) => replace(rows.map((row, idx) => idx === i ? { ...row, [key]: cell } : row));
10086
+ const rowTitle = (row, i) => {
10087
+ const key = field.titleKey ?? field.fields[0]?.key;
10088
+ const raw = key ? row[key] : void 0;
10089
+ if (typeof raw === "string" && raw.trim() !== "") return raw;
10090
+ if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
10091
+ return `Item ${i + 1}`;
10092
+ };
10093
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-repeater", "data-ff-repeater": field.key, children: [
10094
+ rows.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-repeater__empty", children: "None yet." }),
10095
+ rows.map((row, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-repeater__row", "data-ff-repeater-row": i, children: [
10096
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-repeater__row-head", children: [
10097
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-repeater__row-title", children: rowTitle(row, i) }),
10098
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-repeater__row-actions", children: [
10099
+ /* @__PURE__ */ jsxRuntime.jsx(
10100
+ "button",
10101
+ {
10102
+ type: "button",
10103
+ className: "ff-repeater__btn",
10104
+ onClick: () => moveRow(i, -1),
10105
+ disabled: i === 0,
10106
+ "aria-label": `Move ${rowTitle(row, i)} up`,
10107
+ children: "\u2191"
10108
+ }
10109
+ ),
10110
+ /* @__PURE__ */ jsxRuntime.jsx(
10111
+ "button",
10112
+ {
10113
+ type: "button",
10114
+ className: "ff-repeater__btn",
10115
+ onClick: () => moveRow(i, 1),
10116
+ disabled: i === rows.length - 1,
10117
+ "aria-label": `Move ${rowTitle(row, i)} down`,
10118
+ children: "\u2193"
10119
+ }
10120
+ ),
10121
+ /* @__PURE__ */ jsxRuntime.jsx(
10122
+ "button",
10123
+ {
10124
+ type: "button",
10125
+ className: "ff-repeater__btn ff-repeater__btn--danger",
10126
+ onClick: () => removeRow(i),
10127
+ disabled: rows.length <= min,
10128
+ "aria-label": `Remove ${rowTitle(row, i)}`,
10129
+ children: "\u2715"
10130
+ }
10131
+ )
10132
+ ] })
10133
+ ] }),
10134
+ field.fields.map((sub) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-repeater__cell", children: [
10135
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "ff-panel__label ff-panel__label--sub", children: [
10136
+ sub.label,
10137
+ sub.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-panel__required", "aria-hidden": true, children: " *" })
10138
+ ] }),
10139
+ /* @__PURE__ */ jsxRuntime.jsx(
10140
+ ConfigFieldRenderer,
10141
+ {
10142
+ field: sub,
10143
+ value: row[sub.key],
10144
+ onChange: (cell) => setCell(i, sub.key, cell),
10145
+ renderCredentialField,
10146
+ renderDocumentField
10147
+ }
10148
+ )
10149
+ ] }, sub.key))
10150
+ ] }, i)),
10151
+ /* @__PURE__ */ jsxRuntime.jsxs(
10152
+ "button",
10153
+ {
10154
+ type: "button",
10155
+ className: "ff-repeater__add",
10156
+ onClick: addRow,
10157
+ disabled: rows.length >= max,
10158
+ children: [
10159
+ "+ ",
10160
+ field.addLabel ?? "Add"
10161
+ ]
10162
+ }
10163
+ )
10164
+ ] });
10165
+ }
10166
+ function KeyValueField({
10167
+ field,
10168
+ value,
10169
+ onChange
10170
+ }) {
10171
+ const map = value && typeof value === "object" && !Array.isArray(value) ? value : {};
10172
+ const entries = Object.entries(map);
10173
+ const commit = (next) => {
10174
+ const obj = {};
10175
+ for (const [k, v] of next) {
10176
+ if (k === "") continue;
10177
+ obj[k] = v;
10178
+ }
10179
+ onChange(obj);
10180
+ };
10181
+ const setKey = (i, key) => commit(entries.map(([k, v], idx) => idx === i ? [key, v] : [k, v]));
10182
+ const setVal = (i, val) => commit(entries.map(([k, v], idx) => idx === i ? [k, val] : [k, v]));
10183
+ const remove2 = (i) => commit(entries.filter((_, idx) => idx !== i));
10184
+ const add = () => commit([...entries, ["", ""]]);
10185
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-keyvalue", "data-ff-keyvalue": field.key, children: [
10186
+ entries.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-keyvalue__head", children: [
10187
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: field.keyLabel ?? "Key" }),
10188
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: field.valueLabel ?? "Value" }),
10189
+ /* @__PURE__ */ jsxRuntime.jsx("span", {})
10190
+ ] }),
10191
+ entries.map(([k, v], i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-keyvalue__row", "data-ff-keyvalue-row": i, children: [
10192
+ /* @__PURE__ */ jsxRuntime.jsx(
10193
+ "input",
10194
+ {
10195
+ className: "ff-panel__input",
10196
+ value: k,
10197
+ placeholder: field.keyPlaceholder,
10198
+ "aria-label": `${field.keyLabel ?? "Key"} ${i + 1}`,
10199
+ onChange: (e) => setKey(i, e.target.value)
10200
+ }
10201
+ ),
10202
+ field.valueOptions ? /* @__PURE__ */ jsxRuntime.jsxs(
10203
+ "select",
10204
+ {
10205
+ className: "ff-panel__input",
10206
+ value: v ?? "",
10207
+ "aria-label": `${field.valueLabel ?? "Value"} ${i + 1}`,
10208
+ onChange: (e) => setVal(i, e.target.value),
10209
+ children: [
10210
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u2014" }),
10211
+ field.valueOptions.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o.value, children: o.label }, o.value))
10212
+ ]
10213
+ }
10214
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
10215
+ "input",
10216
+ {
10217
+ className: "ff-panel__input",
10218
+ value: v ?? "",
10219
+ placeholder: field.valuePlaceholder,
10220
+ "aria-label": `${field.valueLabel ?? "Value"} ${i + 1}`,
10221
+ onChange: (e) => setVal(i, e.target.value)
10222
+ }
10223
+ ),
10224
+ /* @__PURE__ */ jsxRuntime.jsx(
10225
+ "button",
10226
+ {
10227
+ type: "button",
10228
+ className: "ff-repeater__btn ff-repeater__btn--danger",
10229
+ onClick: () => remove2(i),
10230
+ "aria-label": `Remove ${k || `entry ${i + 1}`}`,
10231
+ children: "\u2715"
10232
+ }
10233
+ )
10234
+ ] }, i)),
10235
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "ff-repeater__add", onClick: add, children: [
10236
+ "+ ",
10237
+ field.addLabel ?? "Add"
10238
+ ] })
10239
+ ] });
10240
+ }
9744
10241
  function JsonField({ value, onChange, rows }) {
9745
10242
  const initial = ReactExports.useMemo(() => {
9746
10243
  try {
@@ -9775,6 +10272,7 @@ function NodeConfigPanel({
9775
10272
  onChange,
9776
10273
  header,
9777
10274
  renderCredentialField,
10275
+ renderDocumentField,
9778
10276
  className,
9779
10277
  style: style2
9780
10278
  }) {
@@ -9800,6 +10298,8 @@ function NodeConfigPanel({
9800
10298
  const setDescription = (description) => onChange({ ...node, data: { ...node.data, description } });
9801
10299
  const setConfigValue = (key, value) => onChange({ ...node, data: { ...node.data, config: { ...config, [key]: value } } });
9802
10300
  const issues = validateConfig(kind, config);
10301
+ const adapter2 = getRichInputAdapter();
10302
+ const documentField = renderDocumentField ?? (adapter2?.renderEditor ? ({ value, onChange: set3 }) => adapter2.renderEditor({ value, onChange: set3 }) : void 0);
9803
10303
  return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ["ff-panel", className ?? ""].filter(Boolean).join(" "), style: style2, children: [
9804
10304
  header,
9805
10305
  /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "ff-panel__header", children: [
@@ -9848,7 +10348,8 @@ function NodeConfigPanel({
9848
10348
  field,
9849
10349
  value: config[field.key],
9850
10350
  onChange: (v) => setConfigValue(field.key, v),
9851
- renderCredentialField
10351
+ renderCredentialField,
10352
+ renderDocumentField: documentField
9852
10353
  }
9853
10354
  )
9854
10355
  ] }, field.key))
@@ -9897,6 +10398,29 @@ function useFlowState(initial) {
9897
10398
  return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
9898
10399
  }
9899
10400
 
10401
+ // src/registry/ports.ts
10402
+ function resolvePortSpec(spec, config) {
10403
+ if (spec === void 0) return void 0;
10404
+ if (typeof spec !== "function") return spec;
10405
+ try {
10406
+ const resolved = spec(config);
10407
+ return Array.isArray(resolved) ? resolved : void 0;
10408
+ } catch {
10409
+ return void 0;
10410
+ }
10411
+ }
10412
+ function nodeConfig(node) {
10413
+ return node.data?.config ?? {};
10414
+ }
10415
+ function resolveNodePorts(node, kind) {
10416
+ const config = nodeConfig(node);
10417
+ const data = node.data;
10418
+ return {
10419
+ inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
10420
+ outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
10421
+ };
10422
+ }
10423
+
9900
10424
  // src/runtime/run-flow.ts
9901
10425
  async function runFlow(graph, executors, onEvent = () => {
9902
10426
  }, options = {}) {
@@ -10028,8 +10552,9 @@ function activatedPorts(node, result) {
10028
10552
  return { ports: [r.branch], value: r.value ?? r };
10029
10553
  }
10030
10554
  }
10031
- const declared = node.data.outputs?.map((p) => p.id) ?? ["out"];
10032
- return { ports: declared, value: result };
10555
+ const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
10556
+ const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
10557
+ return { ports: declared?.length ? declared : ["out"], value: result };
10033
10558
  }
10034
10559
 
10035
10560
  // src/runtime/use-flow-run.ts
@@ -10248,9 +10773,10 @@ function RegistryNodeInner(props) {
10248
10773
  const data = props.data;
10249
10774
  const status = data.status ?? "idle";
10250
10775
  const accent = kind.accent ?? categoryAccent(kind.category);
10251
- const inputs = data.inputs ?? kind.inputs ?? defaultInputs2(kind.category);
10252
- const outputs = data.outputs ?? kind.outputs ?? defaultOutputs2(kind.category);
10253
- const config = data.config ?? {};
10776
+ const resolved = resolveNodePorts(props, kind);
10777
+ const inputs = resolved.inputs ?? defaultInputs2(kind.category);
10778
+ const outputs = resolved.outputs ?? defaultOutputs2(kind.category);
10779
+ const config = nodeConfig(props);
10254
10780
  const label = data.label ?? kind.label;
10255
10781
  return /* @__PURE__ */ jsxRuntime.jsxs(
10256
10782
  "div",
@@ -10363,6 +10889,17 @@ function removeEdges(edges, ids) {
10363
10889
  const doomed = new Set(ids);
10364
10890
  return edges.filter((e) => !doomed.has(e.id));
10365
10891
  }
10892
+ function setEdgeLabel(edges, id2, label) {
10893
+ const next = label?.trim();
10894
+ return edges.map((e) => {
10895
+ if (e.id !== id2) return e;
10896
+ if (!next) {
10897
+ const { label: _drop, ...rest } = e;
10898
+ return rest;
10899
+ }
10900
+ return { ...e, label: next };
10901
+ });
10902
+ }
10366
10903
  function duplicateNode(node, id2, offset = 40) {
10367
10904
  return {
10368
10905
  ...node,
@@ -10410,6 +10947,7 @@ function FlowEditorInner({
10410
10947
  onChange,
10411
10948
  onSelectionChange,
10412
10949
  onDelete,
10950
+ onEdgeDelete,
10413
10951
  apiRef
10414
10952
  }) {
10415
10953
  const internal = useFlowState(initial);
@@ -10427,18 +10965,39 @@ function FlowEditorInner({
10427
10965
  const [selectedId, setSelectedId] = ReactExports.useState(null);
10428
10966
  const selected2 = ReactExports.useMemo(() => flow.nodes.find((n) => n.id === selectedId) ?? null, [flow.nodes, selectedId]);
10429
10967
  ReactExports.useEffect(() => onSelectionChange?.(selected2), [selected2, onSelectionChange]);
10430
- const handleNodeClick2 = (_e, node) => setSelectedId(node.id);
10968
+ const [selectedEdgeId, setSelectedEdgeId] = ReactExports.useState(null);
10969
+ const selectedEdge = ReactExports.useMemo(
10970
+ () => flow.edges.find((e) => e.id === selectedEdgeId) ?? null,
10971
+ [flow.edges, selectedEdgeId]
10972
+ );
10973
+ const handleNodeClick2 = (_e, node) => {
10974
+ setSelectedId(node.id);
10975
+ setSelectedEdgeId(null);
10976
+ };
10431
10977
  const [menu, setMenu] = ReactExports.useState(null);
10978
+ const [labelEdit, setLabelEdit] = ReactExports.useState(null);
10432
10979
  const closeMenu = ReactExports.useCallback(() => setMenu(null), []);
10433
10980
  const handleNodeContextMenu = (event, node) => {
10434
10981
  event.preventDefault();
10435
10982
  setSelectedId(node.id);
10436
- setMenu({ x: event.clientX, y: event.clientY, nodeId: node.id });
10983
+ setSelectedEdgeId(null);
10984
+ setMenu({ x: event.clientX, y: event.clientY, target: { type: "node", id: node.id } });
10985
+ };
10986
+ const handleEdgeClick = (_e, edge) => {
10987
+ setSelectedEdgeId(edge.id);
10988
+ };
10989
+ const handleEdgeContextMenu = (event, edge) => {
10990
+ event.preventDefault();
10991
+ setSelectedEdgeId(edge.id);
10992
+ setMenu({ x: event.clientX, y: event.clientY, target: { type: "edge", id: edge.id } });
10437
10993
  };
10438
10994
  ReactExports.useEffect(() => {
10439
- if (menu === null) return;
10440
- const close = () => setMenu(null);
10441
- const onKey = (e) => e.key === "Escape" && setMenu(null);
10995
+ if (menu === null && labelEdit === null) return;
10996
+ const close = () => {
10997
+ setMenu(null);
10998
+ setLabelEdit(null);
10999
+ };
11000
+ const onKey = (e) => e.key === "Escape" && close();
10442
11001
  window.addEventListener("click", close);
10443
11002
  window.addEventListener("scroll", close, true);
10444
11003
  window.addEventListener("keydown", onKey);
@@ -10447,7 +11006,7 @@ function FlowEditorInner({
10447
11006
  window.removeEventListener("scroll", close, true);
10448
11007
  window.removeEventListener("keydown", onKey);
10449
11008
  };
10450
- }, [menu]);
11009
+ }, [menu, labelEdit]);
10451
11010
  ReactExports.useEffect(() => {
10452
11011
  if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
10453
11012
  }, [flow.nodes, flow.edges, onChange, controlled]);
@@ -10490,14 +11049,29 @@ function FlowEditorInner({
10490
11049
  edges: flow.edges,
10491
11050
  selectedId,
10492
11051
  selected: selected2,
11052
+ selectedEdgeId,
11053
+ selectedEdge,
10493
11054
  running: runner.running,
10494
11055
  statuses: runner.statuses,
10495
11056
  select: setSelectedId,
11057
+ selectEdge: setSelectedEdgeId,
10496
11058
  addNode,
10497
11059
  updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
10498
11060
  deleteNodes,
10499
11061
  deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
10500
- deleteEdges: (ids) => flow.setEdges((all) => removeEdges(all, ids)),
11062
+ deleteEdges: (ids) => {
11063
+ if (ids.length === 0) return;
11064
+ flow.setEdges((all) => removeEdges(all, ids));
11065
+ setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
11066
+ onEdgeDelete?.(ids);
11067
+ },
11068
+ deleteSelectedEdge: () => {
11069
+ if (!selectedEdgeId) return;
11070
+ flow.setEdges((all) => removeEdges(all, [selectedEdgeId]));
11071
+ setSelectedEdgeId(null);
11072
+ onEdgeDelete?.([selectedEdgeId]);
11073
+ },
11074
+ setEdgeLabel: (id2, label) => flow.setEdges((all) => setEdgeLabel(all, id2, label)),
10501
11075
  duplicateNode: (id2) => {
10502
11076
  const src = flow.nodes.find((n) => n.id === id2);
10503
11077
  if (!src) return null;
@@ -10569,6 +11143,9 @@ function FlowEditorInner({
10569
11143
  onConnect: flow.onConnect,
10570
11144
  onNodeClick: handleNodeClick2,
10571
11145
  onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
11146
+ onEdgeClick: handleEdgeClick,
11147
+ onEdgeContextMenu: builtins.edgeContextMenu === false ? void 0 : handleEdgeContextMenu,
11148
+ onEdgesDelete: (deleted) => onEdgeDelete?.(deleted.map((e) => e.id)),
10572
11149
  onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
10573
11150
  deleteKeyCode: ["Delete", "Backspace"],
10574
11151
  height: "100%",
@@ -10577,14 +11154,14 @@ function FlowEditorInner({
10577
11154
  }
10578
11155
  ),
10579
11156
  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(
11157
+ menu?.target.type === "node" && builtins.contextMenu !== false && /* @__PURE__ */ jsxRuntime.jsx(
10581
11158
  "div",
10582
11159
  {
10583
11160
  className: "ff-editor__ctx",
10584
11161
  style: { top: menu.y, left: menu.x },
10585
11162
  role: "menu",
10586
11163
  onClick: (e) => e.stopPropagation(),
10587
- children: slots.contextMenu ? slots.contextMenu(api, menu.nodeId, closeMenu) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11164
+ children: slots.contextMenu ? slots.contextMenu(api, menu.target.id, closeMenu) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10588
11165
  /* @__PURE__ */ jsxRuntime.jsx(
10589
11166
  "button",
10590
11167
  {
@@ -10593,7 +11170,7 @@ function FlowEditorInner({
10593
11170
  className: "ff-editor__ctx-item",
10594
11171
  "data-action": "ctx-duplicate",
10595
11172
  onClick: () => {
10596
- api.duplicateNode(menu.nodeId);
11173
+ api.duplicateNode(menu.target.id);
10597
11174
  closeMenu();
10598
11175
  },
10599
11176
  children: "Duplicate"
@@ -10607,7 +11184,7 @@ function FlowEditorInner({
10607
11184
  className: "ff-editor__ctx-item ff-editor__ctx-item--danger",
10608
11185
  "data-action": "ctx-delete",
10609
11186
  onClick: () => {
10610
- api.deleteNodes([menu.nodeId]);
11187
+ api.deleteNodes([menu.target.id]);
10611
11188
  closeMenu();
10612
11189
  },
10613
11190
  children: "Delete"
@@ -10616,6 +11193,58 @@ function FlowEditorInner({
10616
11193
  ] })
10617
11194
  }
10618
11195
  ),
11196
+ menu?.target.type === "edge" && builtins.edgeContextMenu !== false && /* @__PURE__ */ jsxRuntime.jsx(
11197
+ "div",
11198
+ {
11199
+ className: "ff-editor__ctx",
11200
+ style: { top: menu.y, left: menu.x },
11201
+ role: "menu",
11202
+ onClick: (e) => e.stopPropagation(),
11203
+ children: slots.edgeContextMenu ? slots.edgeContextMenu(api, menu.target.id, closeMenu) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11204
+ /* @__PURE__ */ jsxRuntime.jsx(
11205
+ "button",
11206
+ {
11207
+ type: "button",
11208
+ role: "menuitem",
11209
+ className: "ff-editor__ctx-item",
11210
+ "data-action": "ctx-edge-label",
11211
+ onClick: () => {
11212
+ setLabelEdit({ x: menu.x, y: menu.y, edgeId: menu.target.id });
11213
+ setMenu(null);
11214
+ },
11215
+ children: "Label\u2026"
11216
+ }
11217
+ ),
11218
+ /* @__PURE__ */ jsxRuntime.jsx(
11219
+ "button",
11220
+ {
11221
+ type: "button",
11222
+ role: "menuitem",
11223
+ className: "ff-editor__ctx-item ff-editor__ctx-item--danger",
11224
+ "data-action": "ctx-edge-delete",
11225
+ onClick: () => {
11226
+ api.deleteEdges([menu.target.id]);
11227
+ closeMenu();
11228
+ },
11229
+ children: "Delete connection"
11230
+ }
11231
+ )
11232
+ ] })
11233
+ }
11234
+ ),
11235
+ labelEdit !== null && /* @__PURE__ */ jsxRuntime.jsx(
11236
+ EdgeLabelEditor,
11237
+ {
11238
+ x: labelEdit.x,
11239
+ y: labelEdit.y,
11240
+ initial: flow.edges.find((e) => e.id === labelEdit.edgeId)?.label ?? "",
11241
+ onCommit: (text) => {
11242
+ api.setEdgeLabel(labelEdit.edgeId, text);
11243
+ setLabelEdit(null);
11244
+ },
11245
+ onCancel: () => setLabelEdit(null)
11246
+ }
11247
+ ),
10619
11248
  showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsxRuntime.jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" }))
10620
11249
  ] }),
10621
11250
  showPanel && (slots.panel ? slots.panel(api) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__panel-wrap", children: [
@@ -10692,6 +11321,46 @@ function makeControlledFlowAdapter(value, onChange) {
10692
11321
  toGraph: () => value
10693
11322
  };
10694
11323
  }
11324
+ function EdgeLabelEditor({
11325
+ x,
11326
+ y,
11327
+ initial,
11328
+ onCommit,
11329
+ onCancel
11330
+ }) {
11331
+ const [text, setText] = ReactExports.useState(initial);
11332
+ const ref = ReactExports.useRef(null);
11333
+ ReactExports.useEffect(() => {
11334
+ ref.current?.focus();
11335
+ ref.current?.select();
11336
+ }, []);
11337
+ return /* @__PURE__ */ jsxRuntime.jsx(
11338
+ "div",
11339
+ {
11340
+ className: "ff-editor__ctx ff-editor__edge-label",
11341
+ style: { top: y, left: x },
11342
+ onClick: (e) => e.stopPropagation(),
11343
+ children: /* @__PURE__ */ jsxRuntime.jsx(
11344
+ "input",
11345
+ {
11346
+ ref,
11347
+ className: "ff-panel__input",
11348
+ value: text,
11349
+ placeholder: "Label this connection",
11350
+ "aria-label": "Connection label",
11351
+ "data-action": "edge-label-input",
11352
+ onChange: (e) => setText(e.target.value),
11353
+ onKeyDown: (e) => {
11354
+ e.stopPropagation();
11355
+ if (e.key === "Enter") onCommit(text);
11356
+ if (e.key === "Escape") onCancel();
11357
+ },
11358
+ onBlur: () => onCommit(text)
11359
+ }
11360
+ )
11361
+ }
11362
+ );
11363
+ }
10695
11364
  function defineNode(render) {
10696
11365
  function Wrapped(props) {
10697
11366
  return render({
@@ -10741,6 +11410,7 @@ exports.NodeShell = NodeShell;
10741
11410
  exports.NoteNode = NoteNode;
10742
11411
  exports.OutputNode = OutputNode2;
10743
11412
  exports.RegistryNode = RegistryNode;
11413
+ exports.RichInputPreview = RichInputPreview;
10744
11414
  exports.SubgraphNode = SubgraphNode;
10745
11415
  exports.TriggerNode = TriggerNode;
10746
11416
  exports.WORKFLOW_SCHEMA_URL = WORKFLOW_SCHEMA_URL;
@@ -10753,12 +11423,18 @@ exports.defaultNodeTypes = defaultNodeTypes;
10753
11423
  exports.defineNode = defineNode;
10754
11424
  exports.exportWorkflow = exportWorkflow;
10755
11425
  exports.getNodeKind = getNodeKind;
11426
+ exports.getRichInputAdapter = getRichInputAdapter;
10756
11427
  exports.importWorkflow = importWorkflow;
11428
+ exports.isRichInputEnabled = isRichInputEnabled;
10757
11429
  exports.listNodeKinds = listNodeKinds;
10758
11430
  exports.onNodeKindsChanged = onNodeKindsChanged;
11431
+ exports.onRichInputAdapterChanged = onRichInputAdapterChanged;
10759
11432
  exports.paletteDropHandlers = paletteDropHandlers;
10760
11433
  exports.registerBuiltinKinds = registerBuiltinKinds;
10761
11434
  exports.registerNodeKind = registerNodeKind;
11435
+ exports.registerRichInputAdapter = registerRichInputAdapter;
11436
+ exports.resolveNodePorts = resolveNodePorts;
11437
+ exports.resolvePortSpec = resolvePortSpec;
10762
11438
  exports.runFlow = runFlow;
10763
11439
  exports.useFlowEditor = useFlowEditor;
10764
11440
  exports.useFlowEditorOptional = useFlowEditorOptional;