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