@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/registry.cjs CHANGED
@@ -1,16 +1,88 @@
1
1
  'use strict';
2
2
 
3
- var ReactExports = require('react');
4
3
  var jsxRuntime = require('react/jsx-runtime');
4
+ var ReactExports = require('react');
5
5
  require('react-dom');
6
6
 
7
7
  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/ports.ts
12
+ function resolvePortSpec(spec, config) {
13
+ if (spec === void 0) return void 0;
14
+ if (typeof spec !== "function") return spec;
15
+ try {
16
+ const resolved = spec(config);
17
+ return Array.isArray(resolved) ? resolved : void 0;
18
+ } catch {
19
+ return void 0;
20
+ }
21
+ }
22
+ function nodeConfig(node) {
23
+ return node.data?.config ?? {};
24
+ }
25
+ function resolveNodePorts(node, kind) {
26
+ const config = nodeConfig(node);
27
+ const data = node.data;
28
+ return {
29
+ inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
30
+ outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
31
+ };
32
+ }
33
+ var adapter = null;
34
+ var listeners = /* @__PURE__ */ new Set();
35
+ function registerRichInputAdapter(next) {
36
+ adapter = next;
37
+ for (const l of listeners) l();
38
+ return () => {
39
+ if (adapter === next) {
40
+ adapter = null;
41
+ for (const l of listeners) l();
42
+ }
43
+ };
44
+ }
45
+ function getRichInputAdapter() {
46
+ return adapter;
47
+ }
48
+ function isRichInputEnabled() {
49
+ return adapter !== null && (adapter.renderDocument !== void 0 || adapter.renderEditor !== void 0);
50
+ }
51
+ function onRichInputAdapterChanged(fn) {
52
+ listeners.add(fn);
53
+ return () => listeners.delete(fn);
54
+ }
55
+ function RichInputPreview({ config }) {
56
+ const a = getRichInputAdapter();
57
+ const title = typeof config.title === "string" && config.title.trim() !== "" ? config.title : "Untitled step";
58
+ const doc = config.document;
59
+ if (!a || !a.renderDocument && !a.renderEditor) {
60
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-rich-preview", children: [
61
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-rich-preview__title", children: title }),
62
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-rich-preview__unavailable", children: [
63
+ "Add ",
64
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "@particle-academy/fancy-cms-ui" }),
65
+ " +",
66
+ " ",
67
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "@particle-academy/react-fancy" }),
68
+ ", then",
69
+ " ",
70
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: 'import "@particle-academy/fancy-flow/rich-input"' }),
71
+ "."
72
+ ] })
73
+ ] });
74
+ }
75
+ const body = doc === void 0 || doc === null ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-rich-preview__unavailable", children: "Nothing authored yet." }) : a.renderDocument?.(doc) ?? null;
76
+ const Frame = a.FauxClient;
77
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-rich-preview", children: [
78
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-rich-preview__title", children: title }),
79
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-rich-preview__frame", children: Frame ? /* @__PURE__ */ jsxRuntime.jsx(Frame, { ...a.frameProps ?? { variant: "browser" }, children: body }) : body })
80
+ ] });
81
+ }
82
+
11
83
  // src/registry/registry.ts
12
84
  var kinds = /* @__PURE__ */ new Map();
13
- var listeners = /* @__PURE__ */ new Set();
85
+ var listeners2 = /* @__PURE__ */ new Set();
14
86
  function registerNodeKind(definition) {
15
87
  kinds.set(definition.name, definition);
16
88
  notify();
@@ -29,11 +101,11 @@ function listNodeKinds(category) {
29
101
  return category ? all.filter((k) => k.category === category) : all;
30
102
  }
31
103
  function onNodeKindsChanged(listener) {
32
- listeners.add(listener);
33
- return () => listeners.delete(listener);
104
+ listeners2.add(listener);
105
+ return () => listeners2.delete(listener);
34
106
  }
35
107
  function notify() {
36
- for (const l of listeners) l();
108
+ for (const l of listeners2) l();
37
109
  }
38
110
  function defaultConfigFor(kind) {
39
111
  const fromKind = kind.defaultConfig ? { ...kind.defaultConfig } : {};
@@ -81,6 +153,47 @@ function validateField(field, value) {
81
153
  case "json":
82
154
  return null;
83
155
  // permissive — just JSON-shaped
156
+ case "repeater": {
157
+ if (!Array.isArray(value)) return `${field.label} must be a list`;
158
+ if (field.minItems !== void 0 && value.length < field.minItems) {
159
+ return `${field.label} needs at least ${field.minItems}`;
160
+ }
161
+ if (field.maxItems !== void 0 && value.length > field.maxItems) {
162
+ return `${field.label} allows at most ${field.maxItems}`;
163
+ }
164
+ for (let i = 0; i < value.length; i++) {
165
+ const row = value[i];
166
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
167
+ return `${field.label} item ${i + 1} must be an object`;
168
+ }
169
+ for (const sub of field.fields) {
170
+ const cell = row[sub.key];
171
+ if (sub.required && (cell === void 0 || cell === null || cell === "")) {
172
+ return `${field.label} item ${i + 1}: ${sub.label} is required`;
173
+ }
174
+ if (cell === void 0 || cell === null) continue;
175
+ const issue = validateField(sub, cell);
176
+ if (issue) return `${field.label} item ${i + 1}: ${issue}`;
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+ case "keyvalue": {
182
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
183
+ return `${field.label} must be a key/value map`;
184
+ }
185
+ const allowed = field.valueOptions?.map((o) => o.value);
186
+ for (const [k, v] of Object.entries(value)) {
187
+ if (typeof v !== "string") return `${field.label}: "${k}" must be a string`;
188
+ if (allowed && !allowed.includes(v)) {
189
+ return `${field.label}: "${k}" must be one of ${allowed.join(", ")}`;
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+ case "document":
195
+ return null;
196
+ // opaque to fancy-flow — the host's editor owns its shape
84
197
  default:
85
198
  return null;
86
199
  }
@@ -1095,7 +1208,7 @@ function defaultTouchable() {
1095
1208
  return navigator.maxTouchPoints || "ontouchstart" in this;
1096
1209
  }
1097
1210
  function drag_default() {
1098
- var filter2 = defaultFilter, container = defaultContainer, subject = defaultSubject, touchable = defaultTouchable, gestures = {}, listeners2 = dispatch_default("start", "drag", "end"), active = 0, mousedownx, mousedowny, mousemoving, touchending, clickDistance2 = 0;
1211
+ var filter2 = defaultFilter, container = defaultContainer, subject = defaultSubject, touchable = defaultTouchable, gestures = {}, listeners3 = dispatch_default("start", "drag", "end"), active = 0, mousedownx, mousedowny, mousemoving, touchending, clickDistance2 = 0;
1099
1212
  function drag(selection2) {
1100
1213
  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)");
1101
1214
  }
@@ -1158,7 +1271,7 @@ function drag_default() {
1158
1271
  }
1159
1272
  }
1160
1273
  function beforestart(that, container2, event, d, identifier, touch) {
1161
- var dispatch2 = listeners2.copy(), p = pointer_default(touch || event, container2), dx, dy, s;
1274
+ var dispatch2 = listeners3.copy(), p = pointer_default(touch || event, container2), dx, dy, s;
1162
1275
  if ((s = subject.call(that, new DragEvent("beforestart", {
1163
1276
  sourceEvent: event,
1164
1277
  target: drag,
@@ -1217,8 +1330,8 @@ function drag_default() {
1217
1330
  return arguments.length ? (touchable = typeof _ === "function" ? _ : constant_default2(!!_), drag) : touchable;
1218
1331
  };
1219
1332
  drag.on = function() {
1220
- var value = listeners2.on.apply(listeners2, arguments);
1221
- return value === listeners2 ? drag : value;
1333
+ var value = listeners3.on.apply(listeners3, arguments);
1334
+ return value === listeners3 ? drag : value;
1222
1335
  };
1223
1336
  drag.clickDistance = function(_) {
1224
1337
  return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);
@@ -2772,7 +2885,7 @@ function defaultConstrain(transform2, extent, translateExtent) {
2772
2885
  );
2773
2886
  }
2774
2887
  function zoom_default2() {
2775
- 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;
2888
+ 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;
2776
2889
  function zoom(selection2) {
2777
2890
  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)");
2778
2891
  }
@@ -2884,14 +2997,14 @@ function zoom_default2() {
2884
2997
  },
2885
2998
  emit: function(type) {
2886
2999
  var d = select_default2(this.that).datum();
2887
- listeners2.call(
3000
+ listeners3.call(
2888
3001
  type,
2889
3002
  this.that,
2890
3003
  new ZoomEvent(type, {
2891
3004
  sourceEvent: this.sourceEvent,
2892
3005
  target: zoom,
2893
3006
  transform: this.that.__zoom,
2894
- dispatch: listeners2
3007
+ dispatch: listeners3
2895
3008
  }),
2896
3009
  d
2897
3010
  );
@@ -3041,8 +3154,8 @@ function zoom_default2() {
3041
3154
  return arguments.length ? (interpolate = _, zoom) : interpolate;
3042
3155
  };
3043
3156
  zoom.on = function() {
3044
- var value = listeners2.on.apply(listeners2, arguments);
3045
- return value === listeners2 ? zoom : value;
3157
+ var value = listeners3.on.apply(listeners3, arguments);
3158
+ return value === listeners3 ? zoom : value;
3046
3159
  };
3047
3160
  zoom.clickDistance = function(_) {
3048
3161
  return arguments.length ? (clickDistance2 = (_ = +_) * _, zoom) : Math.sqrt(clickDistance2);
@@ -5715,20 +5828,20 @@ var use_sync_external_store_with_selector_default = { useSyncExternalStoreWithSe
5715
5828
  // node_modules/zustand/esm/vanilla.mjs
5716
5829
  var createStoreImpl = (createState) => {
5717
5830
  let state;
5718
- const listeners2 = /* @__PURE__ */ new Set();
5831
+ const listeners3 = /* @__PURE__ */ new Set();
5719
5832
  const setState = (partial, replace) => {
5720
5833
  const nextState = typeof partial === "function" ? partial(state) : partial;
5721
5834
  if (!Object.is(nextState, state)) {
5722
5835
  const previousState = state;
5723
5836
  state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
5724
- listeners2.forEach((listener) => listener(state, previousState));
5837
+ listeners3.forEach((listener) => listener(state, previousState));
5725
5838
  }
5726
5839
  };
5727
5840
  const getState = () => state;
5728
5841
  const getInitialState2 = () => initialState;
5729
5842
  const subscribe = (listener) => {
5730
- listeners2.add(listener);
5731
- return () => listeners2.delete(listener);
5843
+ listeners3.add(listener);
5844
+ return () => listeners3.delete(listener);
5732
5845
  };
5733
5846
  const destroy = () => {
5734
5847
  if ((undefined ? undefined.MODE : void 0) !== "production") {
@@ -5736,7 +5849,7 @@ var createStoreImpl = (createState) => {
5736
5849
  "[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."
5737
5850
  );
5738
5851
  }
5739
- listeners2.clear();
5852
+ listeners3.clear();
5740
5853
  };
5741
5854
  const api = { setState, getState, getInitialState: getInitialState2, subscribe, destroy };
5742
5855
  const initialState = state = createState(setState, getState, api);
@@ -9024,9 +9137,10 @@ function RegistryNodeInner(props) {
9024
9137
  const data = props.data;
9025
9138
  const status = data.status ?? "idle";
9026
9139
  const accent = kind.accent ?? categoryAccent(kind.category);
9027
- const inputs = data.inputs ?? kind.inputs ?? defaultInputs(kind.category);
9028
- const outputs = data.outputs ?? kind.outputs ?? defaultOutputs(kind.category);
9029
- const config = data.config ?? {};
9140
+ const resolved = resolveNodePorts(props, kind);
9141
+ const inputs = resolved.inputs ?? defaultInputs(kind.category);
9142
+ const outputs = resolved.outputs ?? defaultOutputs(kind.category);
9143
+ const config = nodeConfig(props);
9030
9144
  const label = data.label ?? kind.label;
9031
9145
  return /* @__PURE__ */ jsxRuntime.jsxs(
9032
9146
  "div",
@@ -9117,8 +9231,39 @@ function previewValue(v) {
9117
9231
  return "[object]";
9118
9232
  }
9119
9233
  }
9120
-
9121
- // src/registry/builtin.ts
9234
+ function casePorts(cases) {
9235
+ const byPort = /* @__PURE__ */ new Map();
9236
+ if (cases && typeof cases === "object" && !Array.isArray(cases)) {
9237
+ for (const [match, port] of Object.entries(cases)) {
9238
+ if (typeof port !== "string" || port === "" || port === "default") continue;
9239
+ const matches = byPort.get(port) ?? [];
9240
+ matches.push(match);
9241
+ byPort.set(port, matches);
9242
+ }
9243
+ }
9244
+ const ports = [...byPort].map(([id2, matches]) => ({
9245
+ id: id2,
9246
+ label: matches.join("|")
9247
+ }));
9248
+ return [...ports, { id: "default", label: "default" }];
9249
+ }
9250
+ function routePorts(routes, fallback) {
9251
+ const ports = [];
9252
+ const seen = /* @__PURE__ */ new Set();
9253
+ if (Array.isArray(routes)) {
9254
+ for (const route of routes) {
9255
+ const id2 = route?.port;
9256
+ if (typeof id2 !== "string" || id2.trim() === "" || seen.has(id2)) continue;
9257
+ seen.add(id2);
9258
+ ports.push({ id: id2, label: id2 });
9259
+ }
9260
+ }
9261
+ if (fallback !== false && !seen.has("fallback")) {
9262
+ ports.push({ id: "fallback", label: "fallback" });
9263
+ }
9264
+ if (ports.length === 0) ports.push({ id: "out" });
9265
+ return ports;
9266
+ }
9122
9267
  var HTTP_METHODS = [
9123
9268
  { type: "select", key: "method", label: "Method", options: [
9124
9269
  { value: "GET", label: "GET" },
@@ -9187,15 +9332,59 @@ var KINDS = [
9187
9332
  configSchema: [
9188
9333
  { type: "text", key: "title", label: "Form title", default: "Need your input" },
9189
9334
  {
9190
- type: "json",
9335
+ type: "repeater",
9191
9336
  key: "fields",
9192
- label: "Fields (JSON)",
9193
- language: "json",
9194
- rows: 6,
9195
- default: [{ key: "answer", label: "Your answer", type: "textarea" }]
9337
+ label: "Fields",
9338
+ description: "The form the run pauses on.",
9339
+ titleKey: "label",
9340
+ addLabel: "Add field",
9341
+ minItems: 1,
9342
+ fields: [
9343
+ { type: "text", key: "key", label: "Key", required: true, placeholder: "answer" },
9344
+ { type: "text", key: "label", label: "Label", required: true, placeholder: "Your answer" },
9345
+ {
9346
+ type: "select",
9347
+ key: "type",
9348
+ label: "Type",
9349
+ default: "text",
9350
+ options: [
9351
+ { value: "text", label: "Text" },
9352
+ { value: "textarea", label: "Long text" },
9353
+ { value: "number", label: "Number" },
9354
+ { value: "select", label: "Select" },
9355
+ { value: "switch", label: "Switch" }
9356
+ ]
9357
+ },
9358
+ { type: "switch", key: "required", label: "Required", default: false }
9359
+ ],
9360
+ default: [{ key: "answer", label: "Your answer", type: "textarea", required: true }]
9196
9361
  }
9197
9362
  ]
9198
9363
  },
9364
+ {
9365
+ name: "rich_user_input",
9366
+ category: "human",
9367
+ label: "Rich User Input",
9368
+ description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
9369
+ icon: "\u25A4",
9370
+ inputs: [{ id: "in" }],
9371
+ outputs: [{ id: "out", label: "values" }],
9372
+ configSchema: [
9373
+ { type: "text", key: "title", label: "Step title", default: "Please review" },
9374
+ {
9375
+ type: "document",
9376
+ key: "document",
9377
+ label: "Page content",
9378
+ documentType: "stages",
9379
+ description: "Authored with the host's document editor (fancy-cms Stages)."
9380
+ },
9381
+ { type: "switch", key: "requireConfirm", label: "Require explicit confirmation", default: true },
9382
+ { type: "text", key: "submitLabel", label: "Submit button", default: "Continue" }
9383
+ ],
9384
+ // Preview the authored page inside a FauxClient frame, so the canvas shows
9385
+ // what the person hitting this step will actually see.
9386
+ renderBody: (ctx) => ReactExports.createElement(RichInputPreview, { config: ctx.config ?? {} })
9387
+ },
9199
9388
  // ───────────── Logic ─────────────
9200
9389
  {
9201
9390
  name: "branch",
@@ -9216,10 +9405,24 @@ var KINDS = [
9216
9405
  description: "Route to one of N labelled outputs based on a key.",
9217
9406
  icon: "\u2933",
9218
9407
  inputs: [{ id: "in" }],
9219
- outputs: [{ id: "case_a", label: "a" }, { id: "case_b", label: "b" }, { id: "default", label: "default" }],
9408
+ // Ports ARE the config: every distinct port a case routes to becomes an
9409
+ // output handle, plus the always-present `default`. Editing the cases map
9410
+ // moves the ports on the canvas and the ports the runtime activates.
9411
+ outputs: (config) => casePorts(config?.cases),
9220
9412
  configSchema: [
9221
9413
  { type: "expression", key: "value", label: "Switch on", example: "{{ $json.kind }}", required: true },
9222
- { type: "json", key: "cases", label: "Cases (JSON)", default: { a: "case_a", b: "case_b" } }
9414
+ {
9415
+ type: "keyvalue",
9416
+ key: "cases",
9417
+ label: "Cases",
9418
+ description: "Match value \u2192 output port. Unmatched input takes `default`.",
9419
+ keyLabel: "When value is",
9420
+ valueLabel: "Route to port",
9421
+ keyPlaceholder: "a",
9422
+ valuePlaceholder: "case_a",
9423
+ addLabel: "Add case",
9424
+ default: { a: "case_a", b: "case_b" }
9425
+ }
9223
9426
  ]
9224
9427
  },
9225
9428
  {
@@ -9374,6 +9577,76 @@ var KINDS = [
9374
9577
  { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
9375
9578
  ]
9376
9579
  },
9580
+ {
9581
+ name: "llm_branch",
9582
+ category: "ai",
9583
+ label: "LLM Router",
9584
+ description: "Let a model choose which route the flow takes.",
9585
+ icon: "\u2727",
9586
+ inputs: [{ id: "in" }],
9587
+ // Each declared route is a port. The executor returns `{ __port: id }`
9588
+ // (or `Port.only(id)` on the PHP runtime) to pick one.
9589
+ outputs: (config) => routePorts(config?.routes, config?.fallback),
9590
+ configSchema: [
9591
+ {
9592
+ type: "textarea",
9593
+ key: "system",
9594
+ label: "System prompt",
9595
+ rows: 3,
9596
+ description: "Optional framing for the routing decision."
9597
+ },
9598
+ {
9599
+ type: "expression",
9600
+ key: "prompt",
9601
+ label: "What to route on",
9602
+ example: "{{ $json.message }}",
9603
+ required: true
9604
+ },
9605
+ {
9606
+ type: "repeater",
9607
+ key: "routes",
9608
+ label: "Routes",
9609
+ description: "The model picks exactly one. Descriptions are what it chooses between \u2014 make them distinct.",
9610
+ titleKey: "port",
9611
+ addLabel: "Add route",
9612
+ minItems: 2,
9613
+ fields: [
9614
+ { type: "text", key: "port", label: "Port", required: true, placeholder: "billing" },
9615
+ {
9616
+ type: "text",
9617
+ key: "description",
9618
+ label: "When to choose it",
9619
+ required: true,
9620
+ placeholder: "The user is asking about an invoice, refund, or payment."
9621
+ }
9622
+ ],
9623
+ default: [
9624
+ { port: "a", description: "Describe when the model should pick this route." },
9625
+ { port: "b", description: "Describe when the model should pick this route." }
9626
+ ]
9627
+ },
9628
+ {
9629
+ type: "select",
9630
+ key: "provider",
9631
+ label: "Provider",
9632
+ default: "anthropic",
9633
+ options: [
9634
+ { value: "anthropic", label: "Anthropic" },
9635
+ { value: "openai", label: "OpenAI" },
9636
+ { value: "custom", label: "Custom" }
9637
+ ]
9638
+ },
9639
+ { type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5" },
9640
+ {
9641
+ type: "switch",
9642
+ key: "fallback",
9643
+ label: "Add a `fallback` port",
9644
+ default: true,
9645
+ description: "Where the flow goes if the model returns no usable route."
9646
+ },
9647
+ { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
9648
+ ]
9649
+ },
9377
9650
  {
9378
9651
  name: "tool_use",
9379
9652
  category: "ai",
@@ -9506,14 +9779,22 @@ function buildNodeTypes() {
9506
9779
 
9507
9780
  exports.BUILTIN_KINDS = BUILTIN_KINDS;
9508
9781
  exports.RegistryNode = RegistryNode;
9782
+ exports.RichInputPreview = RichInputPreview;
9509
9783
  exports.buildNodeTypes = buildNodeTypes;
9510
9784
  exports.categoryAccent = categoryAccent;
9511
9785
  exports.defaultConfigFor = defaultConfigFor;
9512
9786
  exports.getNodeKind = getNodeKind;
9787
+ exports.getRichInputAdapter = getRichInputAdapter;
9788
+ exports.isRichInputEnabled = isRichInputEnabled;
9513
9789
  exports.listNodeKinds = listNodeKinds;
9790
+ exports.nodeConfig = nodeConfig;
9514
9791
  exports.onNodeKindsChanged = onNodeKindsChanged;
9792
+ exports.onRichInputAdapterChanged = onRichInputAdapterChanged;
9515
9793
  exports.registerBuiltinKinds = registerBuiltinKinds;
9516
9794
  exports.registerNodeKind = registerNodeKind;
9795
+ exports.registerRichInputAdapter = registerRichInputAdapter;
9796
+ exports.resolveNodePorts = resolveNodePorts;
9797
+ exports.resolvePortSpec = resolvePortSpec;
9517
9798
  exports.validateConfig = validateConfig;
9518
9799
  //# sourceMappingURL=registry.cjs.map
9519
9800
  //# sourceMappingURL=registry.cjs.map