@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/README.md CHANGED
@@ -132,6 +132,110 @@ Swap the menu for your own with `slots.contextMenu`, or turn it off with
132
132
 
133
133
  `onDelete(ids)` fires after either path, so a host can sync its own store.
134
134
 
135
+ ### Connections — breaking and labelling
136
+
137
+ - **right-click a connection** → `Label…` / `Delete connection`,
138
+ - select a connection and press <kbd>Delete</kbd> / <kbd>Backspace</kbd>,
139
+ - `api.deleteEdges(ids)`, `api.deleteSelectedEdge()`, `api.setEdgeLabel(id, text)`.
140
+
141
+ Labels ride on the edge (`edge.label`), so they survive export/import. Clearing
142
+ a label removes the key rather than storing `""`.
143
+
144
+ Replace the menu with `slots.edgeContextMenu`, disable it with
145
+ `builtins={{ edgeContextMenu: false }}`, and hook `onEdgeDelete(ids)` to sync
146
+ your own store.
147
+
148
+ ```tsx
149
+ <FlowEditor
150
+ onEdgeDelete={(ids) => console.log("broke", ids)}
151
+ slots={{
152
+ edgeContextMenu: (api, edgeId, close) => (
153
+ <button onClick={() => { api.setEdgeLabel(edgeId, "approved"); close(); }}>
154
+ Mark approved
155
+ </button>
156
+ ),
157
+ }}
158
+ />
159
+ ```
160
+
161
+ ## Config fields
162
+
163
+ Node config is declared as a `configSchema` and rendered by `NodeConfigPanel`.
164
+ Alongside `text` / `textarea` / `number` / `select` / `switch` / `json` /
165
+ `expression` / `credential`:
166
+
167
+ - **`repeater`** — a list of objects, each row authored with its own
168
+ sub-schema. Reach for this instead of `type: "json"` whenever config is
169
+ list-shaped (form fields, routes, tool bindings); it keeps the panel the
170
+ single authoring surface for humans and keeps the shape introspectable for
171
+ agents.
172
+ - **`keyvalue`** — an editable `Record<string, string>` (filter maps, headers,
173
+ case→port tables). `valueOptions` constrains the values.
174
+ - **`document`** — an opaque rich document edited by a **host-supplied**
175
+ editor (see below).
176
+
177
+ A `text` field with `choices` renders as a select instead of a free-text input,
178
+ so a kind can gain a fixed set of options without changing its type or
179
+ migrating saved config. A stored value outside the list is preserved and shown
180
+ rather than dropped:
181
+
182
+ ```ts
183
+ { type: "text", key: "region", label: "Region", choices: ["us-east", "eu-west"] }
184
+ { type: "text", key: "tier", label: "Tier", choices: [{ value: "p1", label: "Priority 1" }] }
185
+ ```
186
+
187
+ ### Ports that follow config
188
+
189
+ `inputs` / `outputs` accept a function of the node's config, for kinds whose
190
+ branches *are* their config:
191
+
192
+ ```ts
193
+ outputs: (config) => config.routes.map((r) => ({ id: r.port, label: r.port })),
194
+ ```
195
+
196
+ Both the canvas and the runtime resolve ports through the same helper, so the
197
+ handles you see and the ports a run activates cannot drift apart. `switch_case`
198
+ and `llm_branch` are built this way.
199
+
200
+ ### Rich human input
201
+
202
+ `rich_user_input` pauses a run on a fully authored page rather than a flat field
203
+ list, and previews that page **inside the node** using react-fancy's
204
+ `FauxClient` frame.
205
+
206
+ **The page it shows is a fancy-cms page** — the same `PageDoc`, rendered by the
207
+ same `CmsPage`, authored by the same `Editor`. fancy-flow defines no document
208
+ schema of its own; a step authored here stays a document fancy-cms can open.
209
+
210
+ Enable it with one import:
211
+
212
+ ```ts
213
+ import "@particle-academy/fancy-flow/rich-input";
214
+ ```
215
+
216
+ ```
217
+ npm i @particle-academy/fancy-cms-ui @particle-academy/react-fancy
218
+ ```
219
+
220
+ Those two are **optional peers** — required only by this subpath. The main entry
221
+ never imports them, so a flow that has no rich input never pays for a CMS.
222
+ Without the import the node still registers and round-trips its config; it
223
+ renders a "how to enable" body instead of an empty card.
224
+
225
+ To pass a custom element registry (the same one you give `CmsPage` at runtime,
226
+ or the edit canvas renders your node types as blank placeholders):
227
+
228
+ ```ts
229
+ import { useFancyCmsForRichInput } from "@particle-academy/fancy-flow/rich-input";
230
+
231
+ useFancyCmsForRichInput({ registry: myElements, data: previewData });
232
+ ```
233
+
234
+ The underlying seam (`registerRichInputAdapter`) stays public if you need a
235
+ different document engine, and any kind can use a `document` field with
236
+ `NodeConfigPanel`'s `renderDocumentField`. But fancy-cms is the expected path —
237
+ the point is not duplicating a document model.
238
+
135
239
  If you want none of the above chrome, skip `<FlowEditor>` entirely and compose
136
240
  `useFlowState()` + `<FlowCanvas>` + `<NodePalette>` + `<NodeConfigPanel>`
137
241
  yourself — they are all exported.
@@ -1,4 +1,4 @@
1
- import { getNodeKind, defaultConfigFor, validateConfig } from './chunk-WNVBXXOL.js';
1
+ import { getNodeKind, defaultConfigFor, validateConfig } from './chunk-CPSOC27D.js';
2
2
 
3
3
  // src/schema/workflow-schema.ts
4
4
  var WORKFLOW_SCHEMA_VERSION = 1;
@@ -106,5 +106,5 @@ function workflowToBlob(schema) {
106
106
  }
107
107
 
108
108
  export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, workflowToBlob };
109
- //# sourceMappingURL=chunk-BCXECQUC.js.map
110
- //# sourceMappingURL=chunk-BCXECQUC.js.map
109
+ //# sourceMappingURL=chunk-6GGFEZH7.js.map
110
+ //# sourceMappingURL=chunk-6GGFEZH7.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/schema/workflow-schema.ts"],"names":[],"mappings":"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","file":"chunk-BCXECQUC.js","sourcesContent":["import type { FlowEdge, FlowGraph, FlowNode } from \"../types\";\nimport { defaultConfigFor, getNodeKind, validateConfig } from \"../registry/registry\";\n\n/** Schema version. Bump on breaking shape changes; add migrations as needed. */\nexport const WORKFLOW_SCHEMA_VERSION = 1 as const;\nexport const WORKFLOW_SCHEMA_URL = \"https://particle.academy/schemas/workflow/v1.json\";\n\nexport type WorkflowSchema = {\n $schema: typeof WORKFLOW_SCHEMA_URL;\n version: typeof WORKFLOW_SCHEMA_VERSION;\n metadata?: WorkflowMetadata;\n graph: {\n nodes: WorkflowSchemaNode[];\n edges: WorkflowSchemaEdge[];\n };\n view?: {\n viewport?: { x: number; y: number; zoom: number };\n };\n};\n\nexport type WorkflowMetadata = {\n id?: string;\n name?: string;\n description?: string;\n createdAt?: number;\n updatedAt?: number;\n author?: string;\n tags?: string[];\n};\n\nexport type WorkflowSchemaNode = {\n id: string;\n /** Registry kind name (e.g. \"memory_store\"). */\n kind: string;\n position: { x: number; y: number };\n label?: string;\n description?: string;\n config?: Record<string, unknown>;\n};\n\nexport type WorkflowSchemaEdge = {\n id: string;\n source: string;\n target: string;\n sourceHandle?: string;\n targetHandle?: string;\n label?: string;\n};\n\nexport type ImportIssue = {\n level: \"error\" | \"warning\";\n nodeId?: string;\n edgeId?: string;\n message: string;\n};\n\nexport type ImportResult = {\n graph: FlowGraph;\n issues: ImportIssue[];\n /** True when the import produced a usable graph (errors may have been\n * rewritten to warnings via `lenient: true`). */\n ok: boolean;\n};\n\n/** Snapshot the in-memory graph as a portable WorkflowSchema. */\nexport function exportWorkflow(\n graph: FlowGraph,\n metadata?: WorkflowMetadata,\n view?: WorkflowSchema[\"view\"],\n): WorkflowSchema {\n return {\n $schema: WORKFLOW_SCHEMA_URL,\n version: WORKFLOW_SCHEMA_VERSION,\n metadata: metadata ? { ...metadata, updatedAt: Date.now() } : undefined,\n graph: {\n nodes: graph.nodes.map(toSchemaNode),\n edges: graph.edges.map(toSchemaEdge),\n },\n view,\n };\n}\n\nfunction toSchemaNode(n: FlowNode): WorkflowSchemaNode {\n const data: any = n.data ?? {};\n return {\n id: n.id,\n kind: data.kind ?? n.type ?? \"custom\",\n position: { x: n.position.x, y: n.position.y },\n label: data.label,\n description: data.description,\n config: data.config,\n };\n}\n\nfunction toSchemaEdge(e: FlowEdge): WorkflowSchemaEdge {\n return {\n id: e.id,\n source: e.source,\n target: e.target,\n sourceHandle: e.sourceHandle ?? undefined,\n targetHandle: e.targetHandle ?? undefined,\n label: typeof e.label === \"string\" ? e.label : undefined,\n };\n}\n\nexport type ImportOptions = {\n /** When true, unknown kinds become warnings + a \"custom\" placeholder\n * instead of errors. Default false. */\n lenient?: boolean;\n};\n\n/**\n * Hydrate a schema into runtime FlowGraph + validate kinds/configs against\n * the registry. Reports issues for unknown kinds, missing required config,\n * and dangling edges.\n */\nexport function importWorkflow(schema: unknown, options: ImportOptions = {}): ImportResult {\n const issues: ImportIssue[] = [];\n const lenient = options.lenient === true;\n\n if (!schema || typeof schema !== \"object\") {\n return { ok: false, graph: { nodes: [], edges: [] }, issues: [{ level: \"error\", message: \"Schema is not an object.\" }] };\n }\n const s = schema as Partial<WorkflowSchema>;\n if (s.version !== WORKFLOW_SCHEMA_VERSION) {\n issues.push({\n level: lenient ? \"warning\" : \"error\",\n message: `Unsupported workflow schema version: ${s.version} (expected ${WORKFLOW_SCHEMA_VERSION})`,\n });\n if (!lenient) return { ok: false, graph: { nodes: [], edges: [] }, issues };\n }\n\n const rawNodes = s.graph?.nodes ?? [];\n const rawEdges = s.graph?.edges ?? [];\n\n const nodes: FlowNode[] = rawNodes.map((n) => {\n const kind = getNodeKind(n.kind);\n if (!kind) {\n issues.push({\n level: lenient ? \"warning\" : \"error\",\n nodeId: n.id,\n message: `Unknown kind \"${n.kind}\" — register it before importing.`,\n });\n }\n const config = n.config ?? (kind ? defaultConfigFor(kind) : {});\n if (kind) {\n for (const iss of validateConfig(kind, config)) {\n issues.push({ level: \"warning\", nodeId: n.id, message: `${iss.key}: ${iss.message}` });\n }\n }\n return {\n id: n.id,\n type: n.kind,\n position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 },\n data: {\n kind: n.kind,\n label: n.label ?? kind?.label ?? n.kind,\n description: n.description,\n config,\n } as any,\n };\n });\n\n const nodeIds = new Set(nodes.map((n) => n.id));\n const edges: FlowEdge[] = rawEdges\n .map((e) => {\n if (!nodeIds.has(e.source)) {\n issues.push({ level: \"warning\", edgeId: e.id, message: `Edge source \"${e.source}\" not found.` });\n return null;\n }\n if (!nodeIds.has(e.target)) {\n issues.push({ level: \"warning\", edgeId: e.id, message: `Edge target \"${e.target}\" not found.` });\n return null;\n }\n return {\n id: e.id,\n source: e.source,\n target: e.target,\n sourceHandle: e.sourceHandle,\n targetHandle: e.targetHandle,\n label: e.label,\n } as FlowEdge;\n })\n .filter((e): e is FlowEdge => e !== null);\n\n const ok = issues.every((i) => i.level !== \"error\");\n return { ok, graph: { nodes, edges }, issues };\n}\n\n/** Convenience: serialize a schema as a downloadable JSON Blob. */\nexport function workflowToBlob(schema: WorkflowSchema): Blob {\n return new Blob([JSON.stringify(schema, null, 2)], { type: \"application/json\" });\n}\n"]}
1
+ {"version":3,"sources":["../src/schema/workflow-schema.ts"],"names":[],"mappings":"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","file":"chunk-6GGFEZH7.js","sourcesContent":["import type { FlowEdge, FlowGraph, FlowNode } from \"../types\";\nimport { defaultConfigFor, getNodeKind, validateConfig } from \"../registry/registry\";\n\n/** Schema version. Bump on breaking shape changes; add migrations as needed. */\nexport const WORKFLOW_SCHEMA_VERSION = 1 as const;\nexport const WORKFLOW_SCHEMA_URL = \"https://particle.academy/schemas/workflow/v1.json\";\n\nexport type WorkflowSchema = {\n $schema: typeof WORKFLOW_SCHEMA_URL;\n version: typeof WORKFLOW_SCHEMA_VERSION;\n metadata?: WorkflowMetadata;\n graph: {\n nodes: WorkflowSchemaNode[];\n edges: WorkflowSchemaEdge[];\n };\n view?: {\n viewport?: { x: number; y: number; zoom: number };\n };\n};\n\nexport type WorkflowMetadata = {\n id?: string;\n name?: string;\n description?: string;\n createdAt?: number;\n updatedAt?: number;\n author?: string;\n tags?: string[];\n};\n\nexport type WorkflowSchemaNode = {\n id: string;\n /** Registry kind name (e.g. \"memory_store\"). */\n kind: string;\n position: { x: number; y: number };\n label?: string;\n description?: string;\n config?: Record<string, unknown>;\n};\n\nexport type WorkflowSchemaEdge = {\n id: string;\n source: string;\n target: string;\n sourceHandle?: string;\n targetHandle?: string;\n label?: string;\n};\n\nexport type ImportIssue = {\n level: \"error\" | \"warning\";\n nodeId?: string;\n edgeId?: string;\n message: string;\n};\n\nexport type ImportResult = {\n graph: FlowGraph;\n issues: ImportIssue[];\n /** True when the import produced a usable graph (errors may have been\n * rewritten to warnings via `lenient: true`). */\n ok: boolean;\n};\n\n/** Snapshot the in-memory graph as a portable WorkflowSchema. */\nexport function exportWorkflow(\n graph: FlowGraph,\n metadata?: WorkflowMetadata,\n view?: WorkflowSchema[\"view\"],\n): WorkflowSchema {\n return {\n $schema: WORKFLOW_SCHEMA_URL,\n version: WORKFLOW_SCHEMA_VERSION,\n metadata: metadata ? { ...metadata, updatedAt: Date.now() } : undefined,\n graph: {\n nodes: graph.nodes.map(toSchemaNode),\n edges: graph.edges.map(toSchemaEdge),\n },\n view,\n };\n}\n\nfunction toSchemaNode(n: FlowNode): WorkflowSchemaNode {\n const data: any = n.data ?? {};\n return {\n id: n.id,\n kind: data.kind ?? n.type ?? \"custom\",\n position: { x: n.position.x, y: n.position.y },\n label: data.label,\n description: data.description,\n config: data.config,\n };\n}\n\nfunction toSchemaEdge(e: FlowEdge): WorkflowSchemaEdge {\n return {\n id: e.id,\n source: e.source,\n target: e.target,\n sourceHandle: e.sourceHandle ?? undefined,\n targetHandle: e.targetHandle ?? undefined,\n label: typeof e.label === \"string\" ? e.label : undefined,\n };\n}\n\nexport type ImportOptions = {\n /** When true, unknown kinds become warnings + a \"custom\" placeholder\n * instead of errors. Default false. */\n lenient?: boolean;\n};\n\n/**\n * Hydrate a schema into runtime FlowGraph + validate kinds/configs against\n * the registry. Reports issues for unknown kinds, missing required config,\n * and dangling edges.\n */\nexport function importWorkflow(schema: unknown, options: ImportOptions = {}): ImportResult {\n const issues: ImportIssue[] = [];\n const lenient = options.lenient === true;\n\n if (!schema || typeof schema !== \"object\") {\n return { ok: false, graph: { nodes: [], edges: [] }, issues: [{ level: \"error\", message: \"Schema is not an object.\" }] };\n }\n const s = schema as Partial<WorkflowSchema>;\n if (s.version !== WORKFLOW_SCHEMA_VERSION) {\n issues.push({\n level: lenient ? \"warning\" : \"error\",\n message: `Unsupported workflow schema version: ${s.version} (expected ${WORKFLOW_SCHEMA_VERSION})`,\n });\n if (!lenient) return { ok: false, graph: { nodes: [], edges: [] }, issues };\n }\n\n const rawNodes = s.graph?.nodes ?? [];\n const rawEdges = s.graph?.edges ?? [];\n\n const nodes: FlowNode[] = rawNodes.map((n) => {\n const kind = getNodeKind(n.kind);\n if (!kind) {\n issues.push({\n level: lenient ? \"warning\" : \"error\",\n nodeId: n.id,\n message: `Unknown kind \"${n.kind}\" — register it before importing.`,\n });\n }\n const config = n.config ?? (kind ? defaultConfigFor(kind) : {});\n if (kind) {\n for (const iss of validateConfig(kind, config)) {\n issues.push({ level: \"warning\", nodeId: n.id, message: `${iss.key}: ${iss.message}` });\n }\n }\n return {\n id: n.id,\n type: n.kind,\n position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 },\n data: {\n kind: n.kind,\n label: n.label ?? kind?.label ?? n.kind,\n description: n.description,\n config,\n } as any,\n };\n });\n\n const nodeIds = new Set(nodes.map((n) => n.id));\n const edges: FlowEdge[] = rawEdges\n .map((e) => {\n if (!nodeIds.has(e.source)) {\n issues.push({ level: \"warning\", edgeId: e.id, message: `Edge source \"${e.source}\" not found.` });\n return null;\n }\n if (!nodeIds.has(e.target)) {\n issues.push({ level: \"warning\", edgeId: e.id, message: `Edge target \"${e.target}\" not found.` });\n return null;\n }\n return {\n id: e.id,\n source: e.source,\n target: e.target,\n sourceHandle: e.sourceHandle,\n targetHandle: e.targetHandle,\n label: e.label,\n } as FlowEdge;\n })\n .filter((e): e is FlowEdge => e !== null);\n\n const ok = issues.every((i) => i.level !== \"error\");\n return { ok, graph: { nodes, edges }, issues };\n}\n\n/** Convenience: serialize a schema as a downloadable JSON Blob. */\nexport function workflowToBlob(schema: WorkflowSchema): Blob {\n return new Blob([JSON.stringify(schema, null, 2)], { type: \"application/json\" });\n}\n"]}
@@ -71,6 +71,47 @@ function validateField(field, value) {
71
71
  case "json":
72
72
  return null;
73
73
  // permissive — just JSON-shaped
74
+ case "repeater": {
75
+ if (!Array.isArray(value)) return `${field.label} must be a list`;
76
+ if (field.minItems !== void 0 && value.length < field.minItems) {
77
+ return `${field.label} needs at least ${field.minItems}`;
78
+ }
79
+ if (field.maxItems !== void 0 && value.length > field.maxItems) {
80
+ return `${field.label} allows at most ${field.maxItems}`;
81
+ }
82
+ for (let i = 0; i < value.length; i++) {
83
+ const row = value[i];
84
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
85
+ return `${field.label} item ${i + 1} must be an object`;
86
+ }
87
+ for (const sub of field.fields) {
88
+ const cell = row[sub.key];
89
+ if (sub.required && (cell === void 0 || cell === null || cell === "")) {
90
+ return `${field.label} item ${i + 1}: ${sub.label} is required`;
91
+ }
92
+ if (cell === void 0 || cell === null) continue;
93
+ const issue = validateField(sub, cell);
94
+ if (issue) return `${field.label} item ${i + 1}: ${issue}`;
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ case "keyvalue": {
100
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
101
+ return `${field.label} must be a key/value map`;
102
+ }
103
+ const allowed = field.valueOptions?.map((o) => o.value);
104
+ for (const [k, v] of Object.entries(value)) {
105
+ if (typeof v !== "string") return `${field.label}: "${k}" must be a string`;
106
+ if (allowed && !allowed.includes(v)) {
107
+ return `${field.label}: "${k}" must be one of ${allowed.join(", ")}`;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ case "document":
113
+ return null;
114
+ // opaque to fancy-flow — the host's editor owns its shape
74
115
  default:
75
116
  return null;
76
117
  }
@@ -97,5 +138,5 @@ function categoryAccent(category) {
97
138
  }
98
139
 
99
140
  export { categoryAccent, defaultConfigFor, getNodeKind, listNodeKinds, onNodeKindsChanged, registerNodeKind, validateConfig };
100
- //# sourceMappingURL=chunk-WNVBXXOL.js.map
101
- //# sourceMappingURL=chunk-WNVBXXOL.js.map
141
+ //# sourceMappingURL=chunk-CPSOC27D.js.map
142
+ //# sourceMappingURL=chunk-CPSOC27D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry/registry.ts"],"names":[],"mappings":"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","file":"chunk-CPSOC27D.js","sourcesContent":["import type { ConfigField, NodeKindDefinition } from \"./types\";\n\nconst kinds = new Map<string, NodeKindDefinition<any, any, any>>();\nconst listeners = new Set<() => void>();\n\n/**\n * registerNodeKind — install a node kind in the global registry. Returns\n * an `unregister` function. Calling with the same name replaces the prior\n * registration (handy for HMR).\n */\nexport function registerNodeKind<TC = any, TI = any, TO = any>(\n definition: NodeKindDefinition<TC, TI, TO>,\n): () => void {\n kinds.set(definition.name, definition as NodeKindDefinition<any, any, any>);\n notify();\n return () => {\n if (kinds.get(definition.name) === (definition as any)) {\n kinds.delete(definition.name);\n notify();\n }\n };\n}\n\n/** Get a single kind by name, or null. */\nexport function getNodeKind(name: string): NodeKindDefinition | null {\n return (kinds.get(name) as NodeKindDefinition) ?? null;\n}\n\n/** List every registered kind, optionally filtered by category. */\nexport function listNodeKinds(category?: string): NodeKindDefinition[] {\n const all = Array.from(kinds.values()) as NodeKindDefinition[];\n return category ? all.filter((k) => k.category === category) : all;\n}\n\n/** Subscribe to registry changes. Returns an unsubscribe function. */\nexport function onNodeKindsChanged(listener: () => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n}\n\nfunction notify(): void {\n for (const l of listeners) l();\n}\n\n/** Fill in defaults from a kind's configSchema for newly-created nodes. */\nexport function defaultConfigFor(kind: NodeKindDefinition): Record<string, unknown> {\n const fromKind = kind.defaultConfig ? { ...(kind.defaultConfig as Record<string, unknown>) } : {};\n for (const field of kind.configSchema ?? []) {\n if (fromKind[field.key] !== undefined) continue;\n if (\"default\" in field && (field as any).default !== undefined) {\n fromKind[field.key] = (field as any).default;\n }\n }\n return fromKind;\n}\n\n/**\n * Validate a config object against a kind's schema. Returns an array of\n * issues (empty = valid). Validation is intentionally light — type\n * coercion + required-field checks. Hosts can layer Zod / Ajv on top.\n */\nexport function validateConfig(\n kind: NodeKindDefinition,\n config: Record<string, unknown>,\n): Array<{ key: string; message: string }> {\n const issues: Array<{ key: string; message: string }> = [];\n for (const field of kind.configSchema ?? []) {\n const value = config[field.key];\n if (field.required && (value === undefined || value === null || value === \"\")) {\n issues.push({ key: field.key, message: `${field.label} is required` });\n continue;\n }\n if (value === undefined || value === null) continue;\n const issue = validateField(field, value);\n if (issue) issues.push({ key: field.key, message: issue });\n }\n return issues;\n}\n\nfunction validateField(field: ConfigField, value: unknown): string | null {\n switch (field.type) {\n case \"text\":\n case \"textarea\":\n case \"expression\":\n case \"credential\":\n return typeof value === \"string\" ? null : `${field.label} must be a string`;\n case \"number\": {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return `${field.label} must be a number`;\n if (field.min !== undefined && value < field.min) return `${field.label} must be >= ${field.min}`;\n if (field.max !== undefined && value > field.max) return `${field.label} must be <= ${field.max}`;\n return null;\n }\n case \"switch\":\n return typeof value === \"boolean\" ? null : `${field.label} must be a boolean`;\n case \"select\": {\n const allowed = field.options.map((o) => o.value);\n return allowed.includes(String(value)) ? null : `${field.label} must be one of ${allowed.join(\", \")}`;\n }\n case \"json\":\n return null; // permissive — just JSON-shaped\n case \"repeater\": {\n if (!Array.isArray(value)) return `${field.label} must be a list`;\n if (field.minItems !== undefined && value.length < field.minItems) {\n return `${field.label} needs at least ${field.minItems}`;\n }\n if (field.maxItems !== undefined && value.length > field.maxItems) {\n return `${field.label} allows at most ${field.maxItems}`;\n }\n // Surface the first offending row so the author knows WHICH one.\n for (let i = 0; i < value.length; i++) {\n const row = value[i];\n if (!row || typeof row !== \"object\" || Array.isArray(row)) {\n return `${field.label} item ${i + 1} must be an object`;\n }\n for (const sub of field.fields) {\n const cell = (row as Record<string, unknown>)[sub.key];\n if (sub.required && (cell === undefined || cell === null || cell === \"\")) {\n return `${field.label} item ${i + 1}: ${sub.label} is required`;\n }\n if (cell === undefined || cell === null) continue;\n const issue = validateField(sub, cell);\n if (issue) return `${field.label} item ${i + 1}: ${issue}`;\n }\n }\n return null;\n }\n case \"keyvalue\": {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return `${field.label} must be a key/value map`;\n }\n const allowed = field.valueOptions?.map((o) => o.value);\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (typeof v !== \"string\") return `${field.label}: \"${k}\" must be a string`;\n if (allowed && !allowed.includes(v)) {\n return `${field.label}: \"${k}\" must be one of ${allowed.join(\", \")}`;\n }\n }\n return null;\n }\n case \"document\":\n return null; // opaque to fancy-flow — the host's editor owns its shape\n default:\n return null;\n }\n}\n\n/** Default accents per category. */\nexport function categoryAccent(category: string): string {\n switch (category) {\n case \"trigger\": return \"#10b981\";\n case \"logic\": return \"#f59e0b\";\n case \"data\": return \"#0ea5e9\";\n case \"ai\": return \"#8b5cf6\";\n case \"io\": return \"#3b82f6\";\n case \"human\": return \"#ec4899\";\n case \"output\": return \"#a855f7\";\n default: return \"#71717a\";\n }\n}\n"]}
@@ -0,0 +1,56 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+
3
+ // src/registry/rich-input.tsx
4
+ var adapter = null;
5
+ var listeners = /* @__PURE__ */ new Set();
6
+ function registerRichInputAdapter(next) {
7
+ adapter = next;
8
+ for (const l of listeners) l();
9
+ return () => {
10
+ if (adapter === next) {
11
+ adapter = null;
12
+ for (const l of listeners) l();
13
+ }
14
+ };
15
+ }
16
+ function getRichInputAdapter() {
17
+ return adapter;
18
+ }
19
+ function isRichInputEnabled() {
20
+ return adapter !== null && (adapter.renderDocument !== void 0 || adapter.renderEditor !== void 0);
21
+ }
22
+ function onRichInputAdapterChanged(fn) {
23
+ listeners.add(fn);
24
+ return () => listeners.delete(fn);
25
+ }
26
+ function RichInputPreview({ config }) {
27
+ const a = getRichInputAdapter();
28
+ const title = typeof config.title === "string" && config.title.trim() !== "" ? config.title : "Untitled step";
29
+ const doc = config.document;
30
+ if (!a || !a.renderDocument && !a.renderEditor) {
31
+ return /* @__PURE__ */ jsxs("div", { className: "ff-rich-preview", children: [
32
+ /* @__PURE__ */ jsx("span", { className: "ff-rich-preview__title", children: title }),
33
+ /* @__PURE__ */ jsxs("div", { className: "ff-rich-preview__unavailable", children: [
34
+ "Add ",
35
+ /* @__PURE__ */ jsx("code", { children: "@particle-academy/fancy-cms-ui" }),
36
+ " +",
37
+ " ",
38
+ /* @__PURE__ */ jsx("code", { children: "@particle-academy/react-fancy" }),
39
+ ", then",
40
+ " ",
41
+ /* @__PURE__ */ jsx("code", { children: 'import "@particle-academy/fancy-flow/rich-input"' }),
42
+ "."
43
+ ] })
44
+ ] });
45
+ }
46
+ const body = doc === void 0 || doc === null ? /* @__PURE__ */ jsx("p", { className: "ff-rich-preview__unavailable", children: "Nothing authored yet." }) : a.renderDocument?.(doc) ?? null;
47
+ const Frame = a.FauxClient;
48
+ return /* @__PURE__ */ jsxs("div", { className: "ff-rich-preview", children: [
49
+ /* @__PURE__ */ jsx("span", { className: "ff-rich-preview__title", children: title }),
50
+ /* @__PURE__ */ jsx("div", { className: "ff-rich-preview__frame", children: Frame ? /* @__PURE__ */ jsx(Frame, { ...a.frameProps ?? { variant: "browser" }, children: body }) : body })
51
+ ] });
52
+ }
53
+
54
+ export { RichInputPreview, getRichInputAdapter, isRichInputEnabled, onRichInputAdapterChanged, registerRichInputAdapter };
55
+ //# sourceMappingURL=chunk-F5RPRB7A.js.map
56
+ //# sourceMappingURL=chunk-F5RPRB7A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry/rich-input.tsx"],"names":[],"mappings":"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","file":"chunk-F5RPRB7A.js","sourcesContent":["import type { ComponentType, ReactNode } from \"react\";\n\n/**\n * Rich user input — the injection point.\n *\n * `rich_user_input` pauses a run on a fully authored page (long-form content,\n * required reading + confirmation, multi-section forms) rather than a flat\n * field list. That page IS a fancy-cms document — fancy-flow defines no\n * document schema of its own and never re-implements one.\n *\n * The wiring ships in the box. Import the subpath once and the node lights up:\n *\n * ```ts\n * import \"@particle-academy/fancy-flow/rich-input\";\n * ```\n *\n * That module registers fancy-cms's `PageDoc` + `CmsPage` renderer + `Editor`\n * against this seam. It lives on a separate entry so `@particle-academy/\n * fancy-cms-ui` and `@particle-academy/react-fancy` stay OPTIONAL peers — most\n * flows never use a rich input, and a workflow editor should not drag a CMS\n * into every install.\n *\n * The seam stays public so a host can substitute a different document engine,\n * but that is the escape hatch, not the expected path.\n *\n * Until something registers, the node still registers and still round-trips\n * its config — it renders a \"how to enable\" body rather than an empty card.\n */\nexport type RichInputAdapter = {\n /**\n * react-fancy's `FauxClient` (or any component with the same shape) — a\n * frame that mimics a browser window or device and scales its content down\n * to fit. Used to preview the authored page inside the node card.\n */\n FauxClient?: ComponentType<any>;\n /**\n * Props for the frame — e.g. `{ variant: \"browser\", width: 1280, scale: \"fit\" }`\n * so a full-width page renders at its real width and scales down into the\n * node card instead of reflowing to a cramped layout that misrepresents it.\n */\n frameProps?: Record<string, unknown>;\n /** Render the stored document read-only, for the in-node preview. */\n renderDocument?: (doc: unknown) => ReactNode;\n /** Editor mounted in the config panel via `renderDocumentField`. */\n renderEditor?: (props: { value: unknown; onChange: (next: unknown) => void }) => ReactNode;\n};\n\nlet adapter: RichInputAdapter | null = null;\nconst listeners = new Set<() => void>();\n\n/** Install the host's document editor + preview frame. Returns an unregister fn. */\nexport function registerRichInputAdapter(next: RichInputAdapter): () => void {\n adapter = next;\n for (const l of listeners) l();\n return () => {\n if (adapter === next) {\n adapter = null;\n for (const l of listeners) l();\n }\n };\n}\n\n/** The registered adapter, or null when the host hasn't wired one. */\nexport function getRichInputAdapter(): RichInputAdapter | null {\n return adapter;\n}\n\n/** True once a host has wired an adapter — i.e. the node is usable. */\nexport function isRichInputEnabled(): boolean {\n return adapter !== null && (adapter.renderDocument !== undefined || adapter.renderEditor !== undefined);\n}\n\n/** Subscribe to adapter changes (so nodes re-render when it lands). */\nexport function onRichInputAdapterChanged(fn: () => void): () => void {\n listeners.add(fn);\n return () => listeners.delete(fn);\n}\n\n/**\n * RichInputPreview — the node card body. Shows the authored page inside a\n * FauxClient frame so an author can see, at a glance on the canvas, what the\n * person hitting this step will be looking at.\n *\n * Degrades in two steps rather than one: no adapter at all → install hint;\n * adapter but nothing authored yet → an empty frame with the step title. A\n * blank node body would read as \"broken\" in both cases.\n */\nexport function RichInputPreview({ config }: { config: Record<string, unknown> }) {\n const a = getRichInputAdapter();\n const title = typeof config.title === \"string\" && config.title.trim() !== \"\" ? config.title : \"Untitled step\";\n const doc = config.document;\n\n if (!a || (!a.renderDocument && !a.renderEditor)) {\n return (\n <div className=\"ff-rich-preview\">\n <span className=\"ff-rich-preview__title\">{title}</span>\n <div className=\"ff-rich-preview__unavailable\">\n Add <code>@particle-academy/fancy-cms-ui</code> +{\" \"}\n <code>@particle-academy/react-fancy</code>, then{\" \"}\n <code>import \"@particle-academy/fancy-flow/rich-input\"</code>.\n </div>\n </div>\n );\n }\n\n const body = doc === undefined || doc === null\n ? <p className=\"ff-rich-preview__unavailable\">Nothing authored yet.</p>\n : a.renderDocument?.(doc) ?? null;\n\n const Frame = a.FauxClient;\n\n return (\n <div className=\"ff-rich-preview\">\n <span className=\"ff-rich-preview__title\">{title}</span>\n <div className=\"ff-rich-preview__frame\">\n {Frame ? <Frame {...(a.frameProps ?? { variant: \"browser\" })}>{body}</Frame> : body}\n </div>\n </div>\n );\n}\n"]}
@@ -1,3 +1,6 @@
1
+ import { resolveNodePorts } from './chunk-TITD5W4Y.js';
2
+ import { getNodeKind } from './chunk-CPSOC27D.js';
3
+
1
4
  // src/runtime/run-flow.ts
2
5
  async function runFlow(graph, executors, onEvent = () => {
3
6
  }, options = {}) {
@@ -129,10 +132,11 @@ function activatedPorts(node, result) {
129
132
  return { ports: [r.branch], value: r.value ?? r };
130
133
  }
131
134
  }
132
- const declared = node.data.outputs?.map((p) => p.id) ?? ["out"];
133
- return { ports: declared, value: result };
135
+ const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
136
+ const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
137
+ return { ports: declared?.length ? declared : ["out"], value: result };
134
138
  }
135
139
 
136
140
  export { runFlow };
137
- //# sourceMappingURL=chunk-NVULCEDX.js.map
138
- //# sourceMappingURL=chunk-NVULCEDX.js.map
141
+ //# sourceMappingURL=chunk-HNBO4HP3.js.map
142
+ //# sourceMappingURL=chunk-HNBO4HP3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/run-flow.ts"],"names":[],"mappings":"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","file":"chunk-HNBO4HP3.js","sourcesContent":["import type {\n ExecutorRegistry,\n FlowEdge,\n FlowGraph,\n FlowNode,\n NodeExecutor,\n RunEvent,\n} from \"../types\";\n// Both modules are React-free by design — the `/engine` entry must not pull in\n// React. Import them directly rather than via the `registry` barrel, which\n// re-exports the RegistryNode component.\nimport { getNodeKind } from \"../registry/registry\";\nimport { resolveNodePorts } from \"../registry/ports\";\n\nexport type RunOptions = {\n /** Stop the run after this many ms. Default: no timeout. */\n timeoutMs?: number;\n /** Abort signal — host can cancel the run. */\n signal?: AbortSignal;\n /** Initial inputs supplied to entry-point nodes (no incoming edges). */\n initialInputs?: Record<string, Record<string, unknown>>;\n};\n\nexport type RunResult = {\n ok: boolean;\n /** Outputs collected per node, keyed by node id. */\n outputs: Record<string, unknown>;\n /** Error captured if any node threw. */\n error?: string;\n};\n\n/**\n * runFlow — topological execution of a FlowGraph against an ExecutorRegistry.\n *\n * Each node runs once, when all upstream nodes have produced outputs on the\n * connected ports. Decision nodes (or any executor that returns `{ branch:\n * 'true' }`) can short-circuit specific output ports — only edges leaving\n * an \"active\" port propagate to downstream nodes.\n *\n * Cycles are detected and abort the run with an error.\n *\n * The `onEvent` callback receives a stream of `RunEvent`s — wire it to a\n * status feed, log panel, or store.\n */\nexport async function runFlow(\n graph: FlowGraph,\n executors: ExecutorRegistry,\n onEvent: (event: RunEvent) => void = () => {},\n options: RunOptions = {},\n): Promise<RunResult> {\n const { signal, initialInputs = {}, timeoutMs } = options;\n const outputs: Record<string, unknown> = {};\n const portValues = new Map<string, unknown>(); // key: `${nodeId}:${portId}`\n const completed = new Set<string>();\n const errors: string[] = [];\n\n // Topological order via Kahn's algorithm. We allow nodes to run as soon\n // as their incoming edges' source ports have produced values, so the\n // order here is just a deterministic baseline used for cycle detection.\n const order = topoSort(graph);\n if (order === null) {\n const msg = \"Cycle detected in flow graph — aborting.\";\n onEvent({ type: \"run-error\", error: msg });\n return { ok: false, outputs, error: msg };\n }\n\n const incomingByNode = indexIncoming(graph.edges);\n const timer = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;\n\n onEvent({ type: \"run-start\" });\n\n try {\n for (const node of order) {\n if (signal?.aborted) throw new Error(\"aborted\");\n if (errors.length) break;\n\n const incoming = incomingByNode.get(node.id) ?? [];\n\n // Run a node once any upstream branch reaches it. We iterate in\n // topological order, so by the time we reach this node every upstream\n // node has been processed — each incoming edge is therefore *settled*\n // (active or dead, never still-pending). Requiring ALL incoming edges to\n // be active wrongly skipped MERGE POINTS: when a Decision routes down one\n // branch, the other branch's edge stays dead forever, so an `every` check\n // skipped the shared continuation node and halted the run after the first\n // branch (#1). Run when AT LEAST ONE incoming edge is active —\n // collectInputs() only reads from the active ones. A genuine parallel\n // join still works: in topo order both of its inputs are already active.\n if (incoming.length > 0) {\n const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? \"out\"}`));\n if (!anyActive) {\n onEvent({ type: \"node-status\", nodeId: node.id, status: \"idle\", text: \"skipped\" });\n continue;\n }\n }\n\n // Note nodes are annotations — never executed.\n if (node.type === \"note\") {\n onEvent({ type: \"node-status\", nodeId: node.id, status: \"idle\", text: \"annotation\" });\n continue;\n }\n\n onEvent({ type: \"node-status\", nodeId: node.id, status: \"running\" });\n\n const inputs = collectInputs(node, incoming, portValues, initialInputs);\n const exec = pickExecutor(executors, node);\n if (!exec) {\n const msg = `No executor registered for kind=${node.type}`;\n errors.push(msg);\n onEvent({ type: \"node-status\", nodeId: node.id, status: \"error\", text: msg });\n onEvent({ type: \"log\", nodeId: node.id, level: \"error\", message: msg });\n break;\n }\n\n try {\n const result = await Promise.resolve(\n exec({\n node,\n inputs,\n abort: (reason) => { throw new Error(reason ?? \"aborted\"); },\n emit: onEvent,\n }),\n );\n outputs[node.id] = result;\n\n // Decide which output ports were activated. Three conventions:\n // 1) If result is `{ __port: \"out\", value: x }`, only that port emits.\n // 2) If result has `branch: <portId>`, only that port emits (decision sugar).\n // 3) Otherwise, the value is published on every declared output port.\n const activated = activatedPorts(node, result);\n for (const portId of activated.ports) {\n portValues.set(`${node.id}:${portId}`, activated.value);\n onEvent({ type: \"node-output\", nodeId: node.id, portId, value: activated.value });\n }\n completed.add(node.id);\n onEvent({ type: \"node-status\", nodeId: node.id, status: \"done\" });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n errors.push(msg);\n onEvent({ type: \"node-status\", nodeId: node.id, status: \"error\", text: msg });\n onEvent({ type: \"log\", nodeId: node.id, level: \"error\", message: msg });\n break;\n }\n }\n } finally {\n if (timer) clearTimeout(timer);\n }\n\n const ok = errors.length === 0;\n onEvent({ type: \"run-end\", ok });\n return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };\n}\n\nfunction indexIncoming(edges: FlowEdge[]): Map<string, FlowEdge[]> {\n const map = new Map<string, FlowEdge[]>();\n for (const e of edges) {\n const list = map.get(e.target) ?? [];\n list.push(e);\n map.set(e.target, list);\n }\n return map;\n}\n\nfunction topoSort(graph: FlowGraph): FlowNode[] | null {\n const inDegree = new Map<string, number>();\n for (const n of graph.nodes) inDegree.set(n.id, 0);\n for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);\n const queue: string[] = [];\n for (const [id, d] of inDegree) if (d === 0) queue.push(id);\n const ordered: string[] = [];\n while (queue.length) {\n const id = queue.shift()!;\n ordered.push(id);\n for (const e of graph.edges) {\n if (e.source !== id) continue;\n const next = (inDegree.get(e.target) ?? 0) - 1;\n inDegree.set(e.target, next);\n if (next === 0) queue.push(e.target);\n }\n }\n if (ordered.length !== graph.nodes.length) return null;\n const byId = new Map(graph.nodes.map((n) => [n.id, n]));\n return ordered.map((id) => byId.get(id)!).filter(Boolean);\n}\n\nfunction collectInputs(\n node: FlowNode,\n incoming: FlowEdge[],\n portValues: Map<string, unknown>,\n initial: Record<string, Record<string, unknown>>,\n): Record<string, unknown> {\n const inputs: Record<string, unknown> = { ...(initial[node.id] ?? {}) };\n for (const e of incoming) {\n const portId = e.targetHandle ?? \"in\";\n const val = portValues.get(`${e.source}:${e.sourceHandle ?? \"out\"}`);\n inputs[portId] = val;\n }\n return inputs;\n}\n\nfunction pickExecutor(\n executors: ExecutorRegistry,\n node: FlowNode,\n): NodeExecutor | undefined {\n if (executors[node.id]) return executors[node.id];\n if (node.type && executors[node.type]) return executors[node.type];\n return executors[\"*\"];\n}\n\nfunction activatedPorts(node: FlowNode, result: unknown): { ports: string[]; value: unknown } {\n if (result && typeof result === \"object\") {\n const r = result as Record<string, unknown>;\n if (typeof r.__port === \"string\") {\n return { ports: [r.__port], value: r.value };\n }\n if (typeof r.branch === \"string\") {\n return { ports: [r.branch], value: r.value ?? r };\n }\n }\n // Resolve through the shared helper so the ports the runtime activates are\n // the same ones the canvas drew — including config-driven ports, which the\n // node's `data` does not carry. Falls back to a lone `out` when a node\n // declares nothing.\n const kind = getNodeKind((node.data as any)?.kind ?? node.type ?? \"\") ?? undefined;\n const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);\n return { ports: declared?.length ? declared : [\"out\"], value: result };\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  import { Handle, Position } from './chunk-NF6NPY5N.js';
2
- import { getNodeKind, categoryAccent, registerNodeKind, listNodeKinds } from './chunk-WNVBXXOL.js';
3
- import { memo, useMemo } from 'react';
2
+ import { resolveNodePorts, nodeConfig } from './chunk-TITD5W4Y.js';
3
+ import { getNodeKind, categoryAccent, registerNodeKind, listNodeKinds } from './chunk-CPSOC27D.js';
4
+ import { RichInputPreview } from './chunk-F5RPRB7A.js';
5
+ import { memo, useMemo, createElement } from 'react';
4
6
  import { jsxs, jsx } from 'react/jsx-runtime';
5
7
 
6
8
  function RegistryNodeInner(props) {
@@ -22,9 +24,10 @@ function RegistryNodeInner(props) {
22
24
  const data = props.data;
23
25
  const status = data.status ?? "idle";
24
26
  const accent = kind.accent ?? categoryAccent(kind.category);
25
- const inputs = data.inputs ?? kind.inputs ?? defaultInputs(kind.category);
26
- const outputs = data.outputs ?? kind.outputs ?? defaultOutputs(kind.category);
27
- const config = data.config ?? {};
27
+ const resolved = resolveNodePorts(props, kind);
28
+ const inputs = resolved.inputs ?? defaultInputs(kind.category);
29
+ const outputs = resolved.outputs ?? defaultOutputs(kind.category);
30
+ const config = nodeConfig(props);
28
31
  const label = data.label ?? kind.label;
29
32
  return /* @__PURE__ */ jsxs(
30
33
  "div",
@@ -115,8 +118,39 @@ function previewValue(v) {
115
118
  return "[object]";
116
119
  }
117
120
  }
118
-
119
- // src/registry/builtin.ts
121
+ function casePorts(cases) {
122
+ const byPort = /* @__PURE__ */ new Map();
123
+ if (cases && typeof cases === "object" && !Array.isArray(cases)) {
124
+ for (const [match, port] of Object.entries(cases)) {
125
+ if (typeof port !== "string" || port === "" || port === "default") continue;
126
+ const matches = byPort.get(port) ?? [];
127
+ matches.push(match);
128
+ byPort.set(port, matches);
129
+ }
130
+ }
131
+ const ports = [...byPort].map(([id, matches]) => ({
132
+ id,
133
+ label: matches.join("|")
134
+ }));
135
+ return [...ports, { id: "default", label: "default" }];
136
+ }
137
+ function routePorts(routes, fallback) {
138
+ const ports = [];
139
+ const seen = /* @__PURE__ */ new Set();
140
+ if (Array.isArray(routes)) {
141
+ for (const route of routes) {
142
+ const id = route?.port;
143
+ if (typeof id !== "string" || id.trim() === "" || seen.has(id)) continue;
144
+ seen.add(id);
145
+ ports.push({ id, label: id });
146
+ }
147
+ }
148
+ if (fallback !== false && !seen.has("fallback")) {
149
+ ports.push({ id: "fallback", label: "fallback" });
150
+ }
151
+ if (ports.length === 0) ports.push({ id: "out" });
152
+ return ports;
153
+ }
120
154
  var HTTP_METHODS = [
121
155
  { type: "select", key: "method", label: "Method", options: [
122
156
  { value: "GET", label: "GET" },
@@ -185,15 +219,59 @@ var KINDS = [
185
219
  configSchema: [
186
220
  { type: "text", key: "title", label: "Form title", default: "Need your input" },
187
221
  {
188
- type: "json",
222
+ type: "repeater",
189
223
  key: "fields",
190
- label: "Fields (JSON)",
191
- language: "json",
192
- rows: 6,
193
- default: [{ key: "answer", label: "Your answer", type: "textarea" }]
224
+ label: "Fields",
225
+ description: "The form the run pauses on.",
226
+ titleKey: "label",
227
+ addLabel: "Add field",
228
+ minItems: 1,
229
+ fields: [
230
+ { type: "text", key: "key", label: "Key", required: true, placeholder: "answer" },
231
+ { type: "text", key: "label", label: "Label", required: true, placeholder: "Your answer" },
232
+ {
233
+ type: "select",
234
+ key: "type",
235
+ label: "Type",
236
+ default: "text",
237
+ options: [
238
+ { value: "text", label: "Text" },
239
+ { value: "textarea", label: "Long text" },
240
+ { value: "number", label: "Number" },
241
+ { value: "select", label: "Select" },
242
+ { value: "switch", label: "Switch" }
243
+ ]
244
+ },
245
+ { type: "switch", key: "required", label: "Required", default: false }
246
+ ],
247
+ default: [{ key: "answer", label: "Your answer", type: "textarea", required: true }]
194
248
  }
195
249
  ]
196
250
  },
251
+ {
252
+ name: "rich_user_input",
253
+ category: "human",
254
+ label: "Rich User Input",
255
+ description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
256
+ icon: "\u25A4",
257
+ inputs: [{ id: "in" }],
258
+ outputs: [{ id: "out", label: "values" }],
259
+ configSchema: [
260
+ { type: "text", key: "title", label: "Step title", default: "Please review" },
261
+ {
262
+ type: "document",
263
+ key: "document",
264
+ label: "Page content",
265
+ documentType: "stages",
266
+ description: "Authored with the host's document editor (fancy-cms Stages)."
267
+ },
268
+ { type: "switch", key: "requireConfirm", label: "Require explicit confirmation", default: true },
269
+ { type: "text", key: "submitLabel", label: "Submit button", default: "Continue" }
270
+ ],
271
+ // Preview the authored page inside a FauxClient frame, so the canvas shows
272
+ // what the person hitting this step will actually see.
273
+ renderBody: (ctx) => createElement(RichInputPreview, { config: ctx.config ?? {} })
274
+ },
197
275
  // ───────────── Logic ─────────────
198
276
  {
199
277
  name: "branch",
@@ -214,10 +292,24 @@ var KINDS = [
214
292
  description: "Route to one of N labelled outputs based on a key.",
215
293
  icon: "\u2933",
216
294
  inputs: [{ id: "in" }],
217
- outputs: [{ id: "case_a", label: "a" }, { id: "case_b", label: "b" }, { id: "default", label: "default" }],
295
+ // Ports ARE the config: every distinct port a case routes to becomes an
296
+ // output handle, plus the always-present `default`. Editing the cases map
297
+ // moves the ports on the canvas and the ports the runtime activates.
298
+ outputs: (config) => casePorts(config?.cases),
218
299
  configSchema: [
219
300
  { type: "expression", key: "value", label: "Switch on", example: "{{ $json.kind }}", required: true },
220
- { type: "json", key: "cases", label: "Cases (JSON)", default: { a: "case_a", b: "case_b" } }
301
+ {
302
+ type: "keyvalue",
303
+ key: "cases",
304
+ label: "Cases",
305
+ description: "Match value \u2192 output port. Unmatched input takes `default`.",
306
+ keyLabel: "When value is",
307
+ valueLabel: "Route to port",
308
+ keyPlaceholder: "a",
309
+ valuePlaceholder: "case_a",
310
+ addLabel: "Add case",
311
+ default: { a: "case_a", b: "case_b" }
312
+ }
221
313
  ]
222
314
  },
223
315
  {
@@ -372,6 +464,76 @@ var KINDS = [
372
464
  { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
373
465
  ]
374
466
  },
467
+ {
468
+ name: "llm_branch",
469
+ category: "ai",
470
+ label: "LLM Router",
471
+ description: "Let a model choose which route the flow takes.",
472
+ icon: "\u2727",
473
+ inputs: [{ id: "in" }],
474
+ // Each declared route is a port. The executor returns `{ __port: id }`
475
+ // (or `Port.only(id)` on the PHP runtime) to pick one.
476
+ outputs: (config) => routePorts(config?.routes, config?.fallback),
477
+ configSchema: [
478
+ {
479
+ type: "textarea",
480
+ key: "system",
481
+ label: "System prompt",
482
+ rows: 3,
483
+ description: "Optional framing for the routing decision."
484
+ },
485
+ {
486
+ type: "expression",
487
+ key: "prompt",
488
+ label: "What to route on",
489
+ example: "{{ $json.message }}",
490
+ required: true
491
+ },
492
+ {
493
+ type: "repeater",
494
+ key: "routes",
495
+ label: "Routes",
496
+ description: "The model picks exactly one. Descriptions are what it chooses between \u2014 make them distinct.",
497
+ titleKey: "port",
498
+ addLabel: "Add route",
499
+ minItems: 2,
500
+ fields: [
501
+ { type: "text", key: "port", label: "Port", required: true, placeholder: "billing" },
502
+ {
503
+ type: "text",
504
+ key: "description",
505
+ label: "When to choose it",
506
+ required: true,
507
+ placeholder: "The user is asking about an invoice, refund, or payment."
508
+ }
509
+ ],
510
+ default: [
511
+ { port: "a", description: "Describe when the model should pick this route." },
512
+ { port: "b", description: "Describe when the model should pick this route." }
513
+ ]
514
+ },
515
+ {
516
+ type: "select",
517
+ key: "provider",
518
+ label: "Provider",
519
+ default: "anthropic",
520
+ options: [
521
+ { value: "anthropic", label: "Anthropic" },
522
+ { value: "openai", label: "OpenAI" },
523
+ { value: "custom", label: "Custom" }
524
+ ]
525
+ },
526
+ { type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5" },
527
+ {
528
+ type: "switch",
529
+ key: "fallback",
530
+ label: "Add a `fallback` port",
531
+ default: true,
532
+ description: "Where the flow goes if the model returns no usable route."
533
+ },
534
+ { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
535
+ ]
536
+ },
375
537
  {
376
538
  name: "tool_use",
377
539
  category: "ai",
@@ -503,5 +665,5 @@ function buildNodeTypes() {
503
665
  }
504
666
 
505
667
  export { BUILTIN_KINDS, RegistryNode, buildNodeTypes, registerBuiltinKinds };
506
- //# sourceMappingURL=chunk-M2XKGQQL.js.map
507
- //# sourceMappingURL=chunk-M2XKGQQL.js.map
668
+ //# sourceMappingURL=chunk-NCPQDVUE.js.map
669
+ //# sourceMappingURL=chunk-NCPQDVUE.js.map