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