@particle-academy/fancy-flow 0.2.2

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1677 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var react$1 = require('@xyflow/react');
5
+ require('@xyflow/react/dist/style.css');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/registry/registry.ts
9
+ var kinds = /* @__PURE__ */ new Map();
10
+ var listeners = /* @__PURE__ */ new Set();
11
+ function registerNodeKind(definition) {
12
+ kinds.set(definition.name, definition);
13
+ notify();
14
+ return () => {
15
+ if (kinds.get(definition.name) === definition) {
16
+ kinds.delete(definition.name);
17
+ notify();
18
+ }
19
+ };
20
+ }
21
+ function getNodeKind(name) {
22
+ return kinds.get(name) ?? null;
23
+ }
24
+ function listNodeKinds(category) {
25
+ const all = Array.from(kinds.values());
26
+ return category ? all.filter((k) => k.category === category) : all;
27
+ }
28
+ function onNodeKindsChanged(listener) {
29
+ listeners.add(listener);
30
+ return () => listeners.delete(listener);
31
+ }
32
+ function notify() {
33
+ for (const l of listeners) l();
34
+ }
35
+ function defaultConfigFor(kind) {
36
+ const fromKind = kind.defaultConfig ? { ...kind.defaultConfig } : {};
37
+ for (const field of kind.configSchema ?? []) {
38
+ if (fromKind[field.key] !== void 0) continue;
39
+ if ("default" in field && field.default !== void 0) {
40
+ fromKind[field.key] = field.default;
41
+ }
42
+ }
43
+ return fromKind;
44
+ }
45
+ function validateConfig(kind, config) {
46
+ const issues = [];
47
+ for (const field of kind.configSchema ?? []) {
48
+ const value = config[field.key];
49
+ if (field.required && (value === void 0 || value === null || value === "")) {
50
+ issues.push({ key: field.key, message: `${field.label} is required` });
51
+ continue;
52
+ }
53
+ if (value === void 0 || value === null) continue;
54
+ const issue = validateField(field, value);
55
+ if (issue) issues.push({ key: field.key, message: issue });
56
+ }
57
+ return issues;
58
+ }
59
+ function validateField(field, value) {
60
+ switch (field.type) {
61
+ case "text":
62
+ case "textarea":
63
+ case "expression":
64
+ case "credential":
65
+ return typeof value === "string" ? null : `${field.label} must be a string`;
66
+ case "number": {
67
+ if (typeof value !== "number" || !Number.isFinite(value)) return `${field.label} must be a number`;
68
+ if (field.min !== void 0 && value < field.min) return `${field.label} must be >= ${field.min}`;
69
+ if (field.max !== void 0 && value > field.max) return `${field.label} must be <= ${field.max}`;
70
+ return null;
71
+ }
72
+ case "switch":
73
+ return typeof value === "boolean" ? null : `${field.label} must be a boolean`;
74
+ case "select": {
75
+ const allowed = field.options.map((o) => o.value);
76
+ return allowed.includes(String(value)) ? null : `${field.label} must be one of ${allowed.join(", ")}`;
77
+ }
78
+ case "json":
79
+ return null;
80
+ // permissive — just JSON-shaped
81
+ default:
82
+ return null;
83
+ }
84
+ }
85
+ function categoryAccent(category) {
86
+ switch (category) {
87
+ case "trigger":
88
+ return "#10b981";
89
+ case "logic":
90
+ return "#f59e0b";
91
+ case "data":
92
+ return "#0ea5e9";
93
+ case "ai":
94
+ return "#8b5cf6";
95
+ case "io":
96
+ return "#3b82f6";
97
+ case "human":
98
+ return "#ec4899";
99
+ case "output":
100
+ return "#a855f7";
101
+ default:
102
+ return "#71717a";
103
+ }
104
+ }
105
+
106
+ // src/registry/builtin.ts
107
+ var HTTP_METHODS = [
108
+ { type: "select", key: "method", label: "Method", options: [
109
+ { value: "GET", label: "GET" },
110
+ { value: "POST", label: "POST" },
111
+ { value: "PUT", label: "PUT" },
112
+ { value: "PATCH", label: "PATCH" },
113
+ { value: "DELETE", label: "DELETE" }
114
+ ], default: "GET", required: true }
115
+ ];
116
+ var KINDS = [
117
+ // ───────────── Triggers ─────────────
118
+ {
119
+ name: "manual_trigger",
120
+ category: "trigger",
121
+ label: "Manual",
122
+ description: "Entry point fired when the user clicks Run.",
123
+ icon: "\u26A1",
124
+ inputs: [],
125
+ outputs: [{ id: "out" }]
126
+ },
127
+ {
128
+ name: "webhook_trigger",
129
+ category: "trigger",
130
+ label: "Webhook",
131
+ description: "Triggered by an inbound HTTP request to a host-provided URL.",
132
+ icon: "\u{1F4E1}",
133
+ inputs: [],
134
+ outputs: [{ id: "out", label: "payload" }],
135
+ configSchema: [
136
+ { type: "text", key: "path", label: "Path", placeholder: "/hooks/my-flow", required: true },
137
+ { type: "select", key: "method", label: "Method", options: [
138
+ { value: "POST", label: "POST" },
139
+ { value: "GET", label: "GET" }
140
+ ], default: "POST" },
141
+ { type: "credential", key: "secret", label: "Verifying secret", credentialType: "webhook_secret" }
142
+ ]
143
+ },
144
+ {
145
+ name: "schedule_trigger",
146
+ category: "trigger",
147
+ label: "Schedule",
148
+ description: "Fires on a cron schedule (host-implemented).",
149
+ icon: "\u23F1",
150
+ inputs: [],
151
+ outputs: [{ id: "out" }],
152
+ configSchema: [
153
+ {
154
+ type: "text",
155
+ key: "cron",
156
+ label: "Cron",
157
+ placeholder: "*/5 * * * *",
158
+ required: true,
159
+ description: "Standard 5-field cron expression."
160
+ },
161
+ { type: "text", key: "timezone", label: "Timezone", placeholder: "UTC", default: "UTC" }
162
+ ]
163
+ },
164
+ {
165
+ name: "user_input",
166
+ category: "human",
167
+ label: "User Input",
168
+ description: "Pause the flow until the user submits the configured form.",
169
+ icon: "\u270E",
170
+ inputs: [{ id: "in" }],
171
+ outputs: [{ id: "out", label: "values" }],
172
+ configSchema: [
173
+ { type: "text", key: "title", label: "Form title", default: "Need your input" },
174
+ {
175
+ type: "json",
176
+ key: "fields",
177
+ label: "Fields (JSON)",
178
+ language: "json",
179
+ rows: 6,
180
+ default: [{ key: "answer", label: "Your answer", type: "textarea" }]
181
+ }
182
+ ]
183
+ },
184
+ // ───────────── Logic ─────────────
185
+ {
186
+ name: "branch",
187
+ category: "logic",
188
+ label: "Branch",
189
+ description: "Multi-way branch on a condition or value.",
190
+ icon: "\u25C7",
191
+ inputs: [{ id: "in" }],
192
+ outputs: [{ id: "true", label: "true" }, { id: "false", label: "false" }],
193
+ configSchema: [
194
+ { type: "expression", key: "condition", label: "Condition", example: "{{ $json.active }}", required: true }
195
+ ]
196
+ },
197
+ {
198
+ name: "switch_case",
199
+ category: "logic",
200
+ label: "Switch",
201
+ description: "Route to one of N labelled outputs based on a key.",
202
+ icon: "\u2933",
203
+ inputs: [{ id: "in" }],
204
+ outputs: [{ id: "case_a", label: "a" }, { id: "case_b", label: "b" }, { id: "default", label: "default" }],
205
+ configSchema: [
206
+ { type: "expression", key: "value", label: "Switch on", example: "{{ $json.kind }}", required: true },
207
+ { type: "json", key: "cases", label: "Cases (JSON)", default: { a: "case_a", b: "case_b" } }
208
+ ]
209
+ },
210
+ {
211
+ name: "for_each",
212
+ category: "logic",
213
+ label: "For Each",
214
+ description: "Iterate over a list, emitting each item on `item`.",
215
+ icon: "\u21BB",
216
+ inputs: [{ id: "in" }],
217
+ outputs: [{ id: "item", label: "item" }, { id: "done", label: "done" }],
218
+ configSchema: [
219
+ { type: "expression", key: "source", label: "List", example: "{{ $json.users }}", required: true },
220
+ { type: "number", key: "concurrency", label: "Concurrency", default: 1, min: 1, max: 50 }
221
+ ]
222
+ },
223
+ {
224
+ name: "merge",
225
+ category: "logic",
226
+ label: "Merge",
227
+ description: "Combine multiple inputs into one object or array.",
228
+ icon: "\u2295",
229
+ inputs: [{ id: "a" }, { id: "b" }],
230
+ outputs: [{ id: "out" }],
231
+ configSchema: [
232
+ {
233
+ type: "select",
234
+ key: "mode",
235
+ label: "Mode",
236
+ default: "merge",
237
+ options: [{ value: "merge", label: "Object merge" }, { value: "concat", label: "Array concat" }]
238
+ }
239
+ ]
240
+ },
241
+ {
242
+ name: "wait",
243
+ category: "logic",
244
+ label: "Wait",
245
+ description: "Sleep or wait for an external event.",
246
+ icon: "\u23F8",
247
+ configSchema: [
248
+ {
249
+ type: "select",
250
+ key: "mode",
251
+ label: "Mode",
252
+ default: "duration",
253
+ options: [{ value: "duration", label: "Duration" }, { value: "until", label: "Until timestamp" }, { value: "event", label: "External event" }]
254
+ },
255
+ { type: "text", key: "duration", label: "Duration", placeholder: "5s, 10m, 1h", description: "Used when mode = duration." }
256
+ ]
257
+ },
258
+ {
259
+ name: "transform",
260
+ category: "logic",
261
+ label: "Transform",
262
+ description: "Reshape data with an expression.",
263
+ icon: "\u0192",
264
+ configSchema: [
265
+ {
266
+ type: "expression",
267
+ key: "expression",
268
+ label: "Expression",
269
+ example: "{{ { id: $json.id, name: $json.first + ' ' + $json.last } }}",
270
+ required: true
271
+ }
272
+ ]
273
+ },
274
+ // ───────────── Data ─────────────
275
+ {
276
+ name: "memory_store",
277
+ category: "data",
278
+ label: "Memory Store",
279
+ description: "Read or write per-conversation memory.",
280
+ icon: "\u{1F9E0}",
281
+ configSchema: [
282
+ {
283
+ type: "select",
284
+ key: "operation",
285
+ label: "Operation",
286
+ required: true,
287
+ default: "read",
288
+ options: [{ value: "read", label: "Read" }, { value: "write", label: "Write" }, { value: "append", label: "Append" }]
289
+ },
290
+ { type: "text", key: "key", label: "Key", placeholder: "user.preferences", required: true },
291
+ { type: "expression", key: "value", label: "Value (write/append only)", example: "{{ $json }}" },
292
+ { type: "credential", key: "store", label: "Memory store", credentialType: "memory_store" }
293
+ ]
294
+ },
295
+ {
296
+ name: "data_store",
297
+ category: "data",
298
+ label: "Data Store",
299
+ description: "Key-value or table read/write against a host store.",
300
+ icon: "\u{1F5C3}",
301
+ configSchema: [
302
+ {
303
+ type: "select",
304
+ key: "operation",
305
+ label: "Operation",
306
+ required: true,
307
+ default: "get",
308
+ options: [
309
+ { value: "get", label: "Get" },
310
+ { value: "set", label: "Set" },
311
+ { value: "delete", label: "Delete" },
312
+ { value: "query", label: "Query" },
313
+ { value: "list", label: "List" }
314
+ ]
315
+ },
316
+ { type: "text", key: "table", label: "Table / collection", required: true },
317
+ { type: "text", key: "key", label: "Key" },
318
+ { type: "json", key: "where", label: "Where (JSON)", description: "For query/list operations." },
319
+ { type: "expression", key: "value", label: "Value (set only)", example: "{{ $json }}" },
320
+ { type: "credential", key: "store", label: "Data store", credentialType: "data_store" }
321
+ ]
322
+ },
323
+ {
324
+ name: "variable",
325
+ category: "data",
326
+ label: "Variable",
327
+ description: "Workflow-scoped value used by other nodes.",
328
+ icon: "\u{1D4CD}",
329
+ configSchema: [
330
+ { type: "text", key: "name", label: "Name", required: true },
331
+ { type: "expression", key: "value", label: "Value", required: true }
332
+ ]
333
+ },
334
+ // ───────────── AI ─────────────
335
+ {
336
+ name: "llm_call",
337
+ category: "ai",
338
+ label: "LLM Call",
339
+ description: "Send a prompt + context to a model and receive a response.",
340
+ icon: "\u2726",
341
+ configSchema: [
342
+ {
343
+ type: "select",
344
+ key: "provider",
345
+ label: "Provider",
346
+ default: "anthropic",
347
+ options: [
348
+ { value: "anthropic", label: "Anthropic" },
349
+ { value: "openai", label: "OpenAI" },
350
+ { value: "custom", label: "Custom" }
351
+ ]
352
+ },
353
+ { type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5", required: true },
354
+ { type: "textarea", key: "system", label: "System prompt", rows: 4 },
355
+ { type: "expression", key: "prompt", label: "User prompt", example: "{{ $json.question }}", required: true },
356
+ { type: "number", key: "temperature", label: "Temperature", min: 0, max: 2, step: 0.1, default: 0.7 },
357
+ { type: "number", key: "max_tokens", label: "Max tokens", min: 1, max: 8192, default: 1024 },
358
+ { type: "json", key: "tools", label: "Tools (JSON)", description: "Optional Anthropic-style tool definitions." },
359
+ { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
360
+ ]
361
+ },
362
+ {
363
+ name: "tool_use",
364
+ category: "ai",
365
+ label: "Tool Use",
366
+ description: "Hand control to a host-registered tool by name.",
367
+ icon: "\u{1F6E0}",
368
+ configSchema: [
369
+ { type: "text", key: "tool", label: "Tool name", placeholder: "search_index", required: true },
370
+ { type: "expression", key: "args", label: "Arguments", example: "{{ { query: $json.q } }}" }
371
+ ]
372
+ },
373
+ {
374
+ name: "embed_search",
375
+ category: "ai",
376
+ label: "Embed & Search",
377
+ description: "Embed a query and search a vector store.",
378
+ icon: "\u273A",
379
+ configSchema: [
380
+ { type: "expression", key: "query", label: "Query", required: true, example: "{{ $json.question }}" },
381
+ { type: "number", key: "topK", label: "Top K", default: 5, min: 1, max: 50 },
382
+ { type: "credential", key: "vectorStore", label: "Vector store", credentialType: "vector_store" }
383
+ ]
384
+ },
385
+ // ───────────── IO ─────────────
386
+ {
387
+ name: "api_request",
388
+ category: "io",
389
+ label: "API Request",
390
+ description: "HTTP request to any URL.",
391
+ icon: "\u2194",
392
+ configSchema: [
393
+ ...HTTP_METHODS,
394
+ { type: "text", key: "url", label: "URL", placeholder: "https://api.example.com/...", required: true },
395
+ { type: "json", key: "headers", label: "Headers", default: { "content-type": "application/json" } },
396
+ { type: "json", key: "body", label: "Body" },
397
+ { type: "credential", key: "auth", label: "Auth", credentialType: "api_credential" }
398
+ ]
399
+ },
400
+ {
401
+ name: "webhook_out",
402
+ category: "io",
403
+ label: "Send Webhook",
404
+ description: "POST a payload to a configured URL.",
405
+ icon: "\u2197",
406
+ configSchema: [
407
+ { type: "text", key: "url", label: "URL", required: true },
408
+ { type: "json", key: "headers", label: "Headers" },
409
+ { type: "expression", key: "payload", label: "Payload", required: true, example: "{{ $json }}" }
410
+ ]
411
+ },
412
+ // ───────────── Human ─────────────
413
+ {
414
+ name: "human_approval",
415
+ category: "human",
416
+ label: "Human Approval",
417
+ description: "Pause until a human approves or denies.",
418
+ icon: "\u2713",
419
+ inputs: [{ id: "in" }],
420
+ outputs: [{ id: "approved", label: "approved" }, { id: "denied", label: "denied" }],
421
+ configSchema: [
422
+ { type: "text", key: "title", label: "Approval title", default: "Approve action" },
423
+ { type: "textarea", key: "description", label: "Description for approver", rows: 3 },
424
+ { type: "credential", key: "channel", label: "Notify channel", credentialType: "notify_channel" }
425
+ ]
426
+ },
427
+ {
428
+ name: "notify",
429
+ category: "human",
430
+ label: "Notify",
431
+ description: "Send a message via Slack / email / SMS / etc.",
432
+ icon: "\u{1F514}",
433
+ configSchema: [
434
+ {
435
+ type: "select",
436
+ key: "channel",
437
+ label: "Channel",
438
+ default: "slack",
439
+ options: [
440
+ { value: "slack", label: "Slack" },
441
+ { value: "email", label: "Email" },
442
+ { value: "sms", label: "SMS" },
443
+ { value: "discord", label: "Discord" }
444
+ ]
445
+ },
446
+ { type: "text", key: "to", label: "To", required: true },
447
+ { type: "expression", key: "message", label: "Message", required: true, example: "{{ $json.summary }}" }
448
+ ]
449
+ },
450
+ // ───────────── Output ─────────────
451
+ {
452
+ name: "output",
453
+ category: "output",
454
+ label: "Output",
455
+ description: "Terminal node \u2014 captures the workflow's result.",
456
+ icon: "\u25CF",
457
+ inputs: [{ id: "in" }],
458
+ outputs: []
459
+ },
460
+ {
461
+ name: "log",
462
+ category: "output",
463
+ label: "Log",
464
+ description: "Send to the run feed.",
465
+ icon: "\u2261",
466
+ inputs: [{ id: "in" }],
467
+ outputs: [],
468
+ configSchema: [
469
+ {
470
+ type: "select",
471
+ key: "level",
472
+ label: "Level",
473
+ default: "info",
474
+ options: [{ value: "info", label: "info" }, { value: "warn", label: "warn" }, { value: "error", label: "error" }]
475
+ },
476
+ { type: "expression", key: "message", label: "Message", required: true, example: "{{ $json }}" }
477
+ ]
478
+ }
479
+ ];
480
+ function registerBuiltinKinds() {
481
+ for (const k of KINDS) registerNodeKind(k);
482
+ }
483
+ var BUILTIN_KINDS = KINDS;
484
+ function NodeShellInner({
485
+ node,
486
+ accent,
487
+ tag,
488
+ icon,
489
+ showInputs = true,
490
+ showOutputs = true,
491
+ children
492
+ }) {
493
+ const data = node.data;
494
+ const status = data.status ?? "idle";
495
+ const inputs = data.inputs ?? defaultInputs(showInputs);
496
+ const outputs = data.outputs ?? defaultOutputs(showOutputs);
497
+ return /* @__PURE__ */ jsxRuntime.jsxs(
498
+ "div",
499
+ {
500
+ className: ["ff-node", `ff-node--status-${status}`, node.selected ? "ff-node--selected" : ""].filter(Boolean).join(" "),
501
+ style: { borderColor: node.selected ? accent : void 0 },
502
+ children: [
503
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "ff-node__header", style: { background: accent }, children: [
504
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__icon", "aria-hidden": true, children: icon ?? null }),
505
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__tag", children: tag }),
506
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__label", children: data.label }),
507
+ status !== "idle" && /* @__PURE__ */ jsxRuntime.jsx(StatusDot, { status })
508
+ ] }),
509
+ data.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-node__desc", children: data.description }),
510
+ children && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-node__body", children }),
511
+ data.statusText && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-node__status-text", children: data.statusText }),
512
+ inputs.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx(
513
+ react$1.Handle,
514
+ {
515
+ type: "target",
516
+ position: react$1.Position.Left,
517
+ id: p.id,
518
+ style: portStyle(i, inputs.length),
519
+ title: p.label ?? p.id
520
+ },
521
+ p.id
522
+ )),
523
+ outputs.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx(
524
+ react$1.Handle,
525
+ {
526
+ type: "source",
527
+ position: react$1.Position.Right,
528
+ id: p.id,
529
+ style: portStyle(i, outputs.length),
530
+ title: p.label ?? p.id
531
+ },
532
+ p.id
533
+ ))
534
+ ]
535
+ }
536
+ );
537
+ }
538
+ var NodeShell = react.memo(NodeShellInner);
539
+ function defaultInputs(show) {
540
+ return show ? [{ id: "in" }] : [];
541
+ }
542
+ function defaultOutputs(show) {
543
+ return show ? [{ id: "out" }] : [];
544
+ }
545
+ function portStyle(i, total) {
546
+ if (total <= 1) return {};
547
+ const slot = 100 / (total + 1) * (i + 1);
548
+ return { top: `${slot}%` };
549
+ }
550
+ function StatusDot({ status }) {
551
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `ff-node__dot ff-node__dot--${status}`, "aria-label": `status ${status}` });
552
+ }
553
+ function TriggerNodeInner(props) {
554
+ return /* @__PURE__ */ jsxRuntime.jsx(NodeShell, { node: props, accent: "#10b981", tag: "TRIGGER", icon: "\u26A1", showInputs: false });
555
+ }
556
+ var TriggerNode = react.memo(TriggerNodeInner);
557
+ function ActionNodeInner(props) {
558
+ return /* @__PURE__ */ jsxRuntime.jsx(NodeShell, { node: props, accent: "#3b82f6", tag: "ACTION", icon: "\u25B8" });
559
+ }
560
+ var ActionNode = react.memo(ActionNodeInner);
561
+ var DEFAULT_BRANCHES = [
562
+ { id: "true", label: "true" },
563
+ { id: "false", label: "false" }
564
+ ];
565
+ function DecisionNodeInner(props) {
566
+ if (!props.data.outputs) {
567
+ props = { ...props, data: { ...props.data, outputs: DEFAULT_BRANCHES } };
568
+ }
569
+ return /* @__PURE__ */ jsxRuntime.jsx(NodeShell, { node: props, accent: "#f59e0b", tag: "DECISION", icon: "\u25C7" });
570
+ }
571
+ var DecisionNode = react.memo(DecisionNodeInner);
572
+ function OutputNodeInner(props) {
573
+ return /* @__PURE__ */ jsxRuntime.jsx(NodeShell, { node: props, accent: "#a855f7", tag: "OUTPUT", icon: "\u25CF", showOutputs: false });
574
+ }
575
+ var OutputNode = react.memo(OutputNodeInner);
576
+ function NoteNodeInner({ data, selected }) {
577
+ const noteData = data;
578
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `ff-note ${selected ? "ff-note--selected" : ""}`, children: [
579
+ noteData.label && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__title", children: noteData.label }),
580
+ noteData.body && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-note__body", children: noteData.body })
581
+ ] });
582
+ }
583
+ var NoteNode = react.memo(NoteNodeInner);
584
+ function SubgraphNodeInner(props) {
585
+ const data = props.data;
586
+ const childCount = data.childIds?.length ?? 0;
587
+ return /* @__PURE__ */ jsxRuntime.jsx(NodeShell, { node: props, accent: "#0ea5e9", tag: "SUBGRAPH", icon: "\u2750", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-subgraph__meta", children: [
588
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
589
+ childCount,
590
+ " node",
591
+ childCount === 1 ? "" : "s"
592
+ ] }),
593
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: data.collapsed === false ? "expanded" : "collapsed" })
594
+ ] }) });
595
+ }
596
+ var SubgraphNode = react.memo(SubgraphNodeInner);
597
+
598
+ // src/components/nodes/index.ts
599
+ var defaultNodeTypes = {
600
+ trigger: TriggerNode,
601
+ action: ActionNode,
602
+ decision: DecisionNode,
603
+ output: OutputNode,
604
+ note: NoteNode,
605
+ subgraph: SubgraphNode
606
+ };
607
+ var DEFAULT_FIT_VIEW = { padding: 0.2 };
608
+ var DEFAULT_EDGE_OPTIONS = {
609
+ type: "smoothstep",
610
+ animated: false
611
+ };
612
+ function FlowCanvas({
613
+ nodes,
614
+ edges,
615
+ background = react$1.BackgroundVariant.Dots,
616
+ showControls = true,
617
+ showMinimap = false,
618
+ height = 600,
619
+ toolbar,
620
+ nodeTypes,
621
+ edgeTypes,
622
+ className,
623
+ style,
624
+ ...rest
625
+ }) {
626
+ const mergedNodeTypes = react.useMemo(
627
+ () => ({ ...defaultNodeTypes, ...nodeTypes ?? {} }),
628
+ [nodeTypes]
629
+ );
630
+ const mergedEdgeTypes = react.useMemo(
631
+ () => edgeTypes ? { ...edgeTypes } : void 0,
632
+ [edgeTypes]
633
+ );
634
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["ff-canvas", className ?? ""].filter(Boolean).join(" "), style: { height, ...style }, children: [
635
+ toolbar && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-canvas__toolbar", children: toolbar }),
636
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-canvas__surface", children: /* @__PURE__ */ jsxRuntime.jsxs(
637
+ react$1.ReactFlow,
638
+ {
639
+ nodes,
640
+ edges,
641
+ nodeTypes: mergedNodeTypes,
642
+ edgeTypes: mergedEdgeTypes,
643
+ fitView: true,
644
+ fitViewOptions: DEFAULT_FIT_VIEW,
645
+ defaultEdgeOptions: DEFAULT_EDGE_OPTIONS,
646
+ proOptions: { hideAttribution: true },
647
+ ...rest,
648
+ children: [
649
+ background !== "none" && /* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { variant: background, gap: 20, size: 1, color: "rgba(0,0,0,0.18)" }),
650
+ showControls && /* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { className: "ff-controls", position: "bottom-right" }),
651
+ showMinimap && /* @__PURE__ */ jsxRuntime.jsx(react$1.MiniMap, { className: "ff-minimap", pannable: true, zoomable: true })
652
+ ]
653
+ }
654
+ ) })
655
+ ] });
656
+ }
657
+ var CATEGORY_ORDER = ["trigger", "logic", "data", "ai", "io", "human", "output", "custom"];
658
+ var CATEGORY_LABELS = {
659
+ trigger: "Triggers",
660
+ logic: "Logic",
661
+ data: "Data",
662
+ ai: "AI",
663
+ io: "Connectors",
664
+ human: "Human",
665
+ output: "Output",
666
+ custom: "Custom"
667
+ };
668
+ function NodePalette({ categories, onPick, className, style }) {
669
+ const [, setRev] = react.useState(0);
670
+ react.useEffect(() => onNodeKindsChanged(() => setRev((n) => n + 1)), []);
671
+ const [query, setQuery] = react.useState("");
672
+ const visibleCats = categories ?? CATEGORY_ORDER;
673
+ const grouped = react.useMemo(() => {
674
+ const all = listNodeKinds();
675
+ const q = query.trim().toLowerCase();
676
+ const filtered = q ? all.filter((k) => k.name.includes(q) || k.label.toLowerCase().includes(q) || (k.description ?? "").toLowerCase().includes(q)) : all;
677
+ const map = /* @__PURE__ */ new Map();
678
+ for (const cat of visibleCats) map.set(cat, []);
679
+ for (const k of filtered) {
680
+ if (!map.has(k.category)) map.set(k.category, []);
681
+ map.get(k.category).push(k);
682
+ }
683
+ return map;
684
+ }, [query, visibleCats]);
685
+ return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ["ff-palette", className ?? ""].filter(Boolean).join(" "), style, children: [
686
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-palette__search", children: /* @__PURE__ */ jsxRuntime.jsx(
687
+ "input",
688
+ {
689
+ className: "ff-palette__search-input",
690
+ placeholder: "Search nodes\u2026",
691
+ value: query,
692
+ onChange: (e) => setQuery(e.target.value)
693
+ }
694
+ ) }),
695
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-palette__list", children: Array.from(grouped.entries()).map(
696
+ ([cat, kinds2]) => kinds2.length === 0 ? null : /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "ff-palette__group", children: [
697
+ /* @__PURE__ */ jsxRuntime.jsx("header", { className: "ff-palette__group-label", children: CATEGORY_LABELS[cat] ?? cat }),
698
+ kinds2.map((k) => /* @__PURE__ */ jsxRuntime.jsx(KindRow, { kind: k, onPick }, k.name))
699
+ ] }, cat)
700
+ ) })
701
+ ] });
702
+ }
703
+ function KindRow({ kind, onPick }) {
704
+ const accent = kind.accent ?? categoryAccent(kind.category);
705
+ const onDragStart = (e) => {
706
+ e.dataTransfer.effectAllowed = "copy";
707
+ e.dataTransfer.setData("application/x-fancy-flow-kind", kind.name);
708
+ e.dataTransfer.setData("text/plain", kind.name);
709
+ };
710
+ return /* @__PURE__ */ jsxRuntime.jsxs(
711
+ "button",
712
+ {
713
+ type: "button",
714
+ className: "ff-palette__row",
715
+ draggable: true,
716
+ onDragStart,
717
+ onClick: () => onPick?.(kind),
718
+ title: kind.description ?? kind.name,
719
+ children: [
720
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-palette__row-dot", style: { background: accent }, children: kind.icon ?? "" }),
721
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-palette__row-text", children: [
722
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-palette__row-label", children: kind.label }),
723
+ kind.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-palette__row-desc", children: kind.description })
724
+ ] })
725
+ ]
726
+ }
727
+ );
728
+ }
729
+ function paletteDropHandlers(onDrop) {
730
+ return {
731
+ onDragOver: (e) => {
732
+ if (e.dataTransfer.types.includes("application/x-fancy-flow-kind")) {
733
+ e.preventDefault();
734
+ e.dataTransfer.dropEffect = "copy";
735
+ }
736
+ },
737
+ onDrop: (e) => {
738
+ const name = e.dataTransfer.getData("application/x-fancy-flow-kind");
739
+ if (!name) return;
740
+ e.preventDefault();
741
+ onDrop(name, e);
742
+ }
743
+ };
744
+ }
745
+ function ConfigFieldRenderer({ field, value, onChange, renderCredentialField }) {
746
+ switch (field.type) {
747
+ case "text":
748
+ return /* @__PURE__ */ jsxRuntime.jsx(
749
+ "input",
750
+ {
751
+ className: "ff-panel__input",
752
+ type: "text",
753
+ value: value ?? "",
754
+ placeholder: field.placeholder,
755
+ onChange: (e) => onChange(e.target.value)
756
+ }
757
+ );
758
+ case "textarea":
759
+ return /* @__PURE__ */ jsxRuntime.jsx(
760
+ "textarea",
761
+ {
762
+ className: "ff-panel__input ff-panel__input--textarea",
763
+ rows: field.rows ?? 4,
764
+ value: value ?? "",
765
+ placeholder: field.placeholder,
766
+ onChange: (e) => onChange(e.target.value)
767
+ }
768
+ );
769
+ case "number":
770
+ return /* @__PURE__ */ jsxRuntime.jsx(
771
+ "input",
772
+ {
773
+ className: "ff-panel__input",
774
+ type: "number",
775
+ value: value ?? "",
776
+ min: field.min,
777
+ max: field.max,
778
+ step: field.step ?? 1,
779
+ onChange: (e) => onChange(e.target.value === "" ? void 0 : Number(e.target.value))
780
+ }
781
+ );
782
+ case "switch":
783
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "ff-panel__switch", children: [
784
+ /* @__PURE__ */ jsxRuntime.jsx(
785
+ "input",
786
+ {
787
+ type: "checkbox",
788
+ checked: !!value,
789
+ onChange: (e) => onChange(e.target.checked)
790
+ }
791
+ ),
792
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-panel__switch-slider" })
793
+ ] });
794
+ case "select":
795
+ return /* @__PURE__ */ jsxRuntime.jsxs(
796
+ "select",
797
+ {
798
+ className: "ff-panel__input",
799
+ value: value ?? "",
800
+ onChange: (e) => onChange(e.target.value),
801
+ children: [
802
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u2014" }),
803
+ field.options.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o.value, children: o.label }, o.value))
804
+ ]
805
+ }
806
+ );
807
+ case "json":
808
+ return /* @__PURE__ */ jsxRuntime.jsx(JsonField, { value, onChange, rows: field.rows });
809
+ case "expression":
810
+ return /* @__PURE__ */ jsxRuntime.jsx(
811
+ "textarea",
812
+ {
813
+ className: "ff-panel__input ff-panel__input--expression",
814
+ rows: 2,
815
+ value: value ?? "",
816
+ placeholder: field.example ?? "{{ $json.field }}",
817
+ spellCheck: false,
818
+ onChange: (e) => onChange(e.target.value)
819
+ }
820
+ );
821
+ case "credential":
822
+ if (renderCredentialField) {
823
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderCredentialField({ credentialType: field.credentialType, value, onChange }) });
824
+ }
825
+ return /* @__PURE__ */ jsxRuntime.jsx(
826
+ "input",
827
+ {
828
+ className: "ff-panel__input ff-panel__input--credential",
829
+ type: "text",
830
+ value: value ?? "",
831
+ placeholder: `Credential reference (${field.credentialType})`,
832
+ onChange: (e) => onChange(e.target.value)
833
+ }
834
+ );
835
+ default:
836
+ return null;
837
+ }
838
+ }
839
+ function JsonField({ value, onChange, rows }) {
840
+ const initial = react.useMemo(() => {
841
+ try {
842
+ return value === void 0 ? "" : JSON.stringify(value, null, 2);
843
+ } catch {
844
+ return "";
845
+ }
846
+ }, [value]);
847
+ return /* @__PURE__ */ jsxRuntime.jsx(
848
+ "textarea",
849
+ {
850
+ className: "ff-panel__input ff-panel__input--json",
851
+ rows: rows ?? 6,
852
+ defaultValue: initial,
853
+ spellCheck: false,
854
+ onBlur: (e) => {
855
+ const text = e.target.value.trim();
856
+ if (text === "") {
857
+ onChange(void 0);
858
+ return;
859
+ }
860
+ try {
861
+ onChange(JSON.parse(text));
862
+ } catch {
863
+ }
864
+ }
865
+ }
866
+ );
867
+ }
868
+ function NodeConfigPanel({
869
+ node,
870
+ onChange,
871
+ header,
872
+ renderCredentialField,
873
+ className,
874
+ style
875
+ }) {
876
+ if (!node) {
877
+ return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ["ff-panel", "ff-panel--empty", className ?? ""].filter(Boolean).join(" "), style, children: [
878
+ header,
879
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-panel__empty", children: "Select a node to configure it." })
880
+ ] });
881
+ }
882
+ const kindName = node.data.kind ?? node.type;
883
+ const kind = react.useMemo(() => getNodeKind(kindName), [kindName]);
884
+ const config = react.useMemo(() => node.data.config ?? {}, [node.data]);
885
+ if (!kind) {
886
+ return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ["ff-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
887
+ header,
888
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "ff-panel__empty", children: [
889
+ "Unknown kind: ",
890
+ kindName
891
+ ] })
892
+ ] });
893
+ }
894
+ const setLabel = (label) => onChange({ ...node, data: { ...node.data, label } });
895
+ const setDescription = (description) => onChange({ ...node, data: { ...node.data, description } });
896
+ const setConfigValue = (key, value) => onChange({ ...node, data: { ...node.data, config: { ...config, [key]: value } } });
897
+ const issues = validateConfig(kind, config);
898
+ return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ["ff-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
899
+ header,
900
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "ff-panel__header", children: [
901
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-panel__kind-tag", children: kind.label }),
902
+ kind.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-panel__kind-desc", children: kind.description })
903
+ ] }),
904
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-panel__field", children: [
905
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "ff-panel__label", children: "Label" }),
906
+ /* @__PURE__ */ jsxRuntime.jsx(
907
+ "input",
908
+ {
909
+ className: "ff-panel__input",
910
+ value: node.data.label ?? "",
911
+ onChange: (e) => setLabel(e.target.value),
912
+ placeholder: kind.label
913
+ }
914
+ )
915
+ ] }),
916
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-panel__field", children: [
917
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "ff-panel__label", children: "Description" }),
918
+ /* @__PURE__ */ jsxRuntime.jsx(
919
+ "textarea",
920
+ {
921
+ className: "ff-panel__input ff-panel__input--textarea",
922
+ rows: 2,
923
+ value: node.data.description ?? "",
924
+ onChange: (e) => setDescription(e.target.value)
925
+ }
926
+ )
927
+ ] }),
928
+ kind.renderPanel ? kind.renderPanel({
929
+ config,
930
+ onChange: (next) => onChange({ ...node, data: { ...node.data, config: next } }),
931
+ nodeId: node.id
932
+ }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
933
+ (kind.configSchema ?? []).length > 0 && /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "ff-panel__divider" }),
934
+ (kind.configSchema ?? []).map((field) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-panel__field", children: [
935
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "ff-panel__label", children: [
936
+ field.label,
937
+ field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-panel__required", "aria-hidden": true, children: " *" })
938
+ ] }),
939
+ field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-panel__hint", children: field.description }),
940
+ /* @__PURE__ */ jsxRuntime.jsx(
941
+ ConfigFieldRenderer,
942
+ {
943
+ field,
944
+ value: config[field.key],
945
+ onChange: (v) => setConfigValue(field.key, v),
946
+ renderCredentialField
947
+ }
948
+ )
949
+ ] }, field.key))
950
+ ] }),
951
+ issues.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-panel__issues", children: issues.map((iss) => /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "ff-panel__issue", children: [
952
+ "\u26A0 ",
953
+ iss.message
954
+ ] }, iss.key)) })
955
+ ] });
956
+ }
957
+ function FlowRunControls({ running, onRun, onCancel, onReset, className, style }) {
958
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["ff-run-controls", className ?? ""].filter(Boolean).join(" "), style, children: [
959
+ !running ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ff-run-controls__btn ff-run-controls__btn--run", onClick: onRun, children: "\u25B6 Run" }) : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ff-run-controls__btn ff-run-controls__btn--cancel", onClick: onCancel, children: "\u23F9 Cancel" }),
960
+ onReset && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ff-run-controls__btn ff-run-controls__btn--reset", onClick: onReset, disabled: running, children: "Reset" })
961
+ ] });
962
+ }
963
+ function FlowRunFeed({ entries, className, style }) {
964
+ const scrollRef = react.useRef(null);
965
+ react.useEffect(() => {
966
+ const el = scrollRef.current;
967
+ if (el) el.scrollTop = el.scrollHeight;
968
+ }, [entries.length]);
969
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: ["ff-run-feed", className ?? ""].filter(Boolean).join(" "), style, ref: scrollRef, children: entries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-run-feed__empty", children: "No run events yet." }) : entries.map((e) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `ff-run-feed__row ff-run-feed__row--${e.level}`, children: [
970
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-run-feed__time", children: formatTime(e.at) }),
971
+ e.nodeId && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-run-feed__node", children: e.nodeId }),
972
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-run-feed__text", children: e.text })
973
+ ] }, e.id)) });
974
+ }
975
+ function formatTime(at) {
976
+ const d = new Date(at);
977
+ return `${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}.${Math.floor(d.getMilliseconds() / 100)}`;
978
+ }
979
+ function useFlowState(initial) {
980
+ const [nodes, setNodes] = react.useState(initial.nodes);
981
+ const [edges, setEdges] = react.useState(initial.edges);
982
+ const onNodesChange = react.useCallback((changes) => {
983
+ setNodes((ns) => react$1.applyNodeChanges(changes, ns));
984
+ }, []);
985
+ const onEdgesChange = react.useCallback((changes) => {
986
+ setEdges((es) => react$1.applyEdgeChanges(changes, es));
987
+ }, []);
988
+ const onConnect = react.useCallback((connection) => {
989
+ setEdges((es) => react$1.addEdge(connection, es));
990
+ }, []);
991
+ const toGraph = react.useCallback(() => ({ nodes, edges }), [nodes, edges]);
992
+ return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
993
+ }
994
+
995
+ // src/runtime/run-flow.ts
996
+ async function runFlow(graph, executors, onEvent = () => {
997
+ }, options = {}) {
998
+ const { signal, initialInputs = {}, timeoutMs } = options;
999
+ const outputs = {};
1000
+ const portValues = /* @__PURE__ */ new Map();
1001
+ const completed = /* @__PURE__ */ new Set();
1002
+ const errors = [];
1003
+ const order = topoSort(graph);
1004
+ if (order === null) {
1005
+ const msg = "Cycle detected in flow graph \u2014 aborting.";
1006
+ onEvent({ type: "run-error", error: msg });
1007
+ return { ok: false, outputs, error: msg };
1008
+ }
1009
+ const incomingByNode = indexIncoming(graph.edges);
1010
+ const timer = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
1011
+ onEvent({ type: "run-start" });
1012
+ try {
1013
+ for (const node of order) {
1014
+ if (signal?.aborted) throw new Error("aborted");
1015
+ if (errors.length) break;
1016
+ const incoming = incomingByNode.get(node.id) ?? [];
1017
+ if (incoming.length > 0) {
1018
+ const allActive = incoming.every((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
1019
+ if (!allActive) {
1020
+ onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
1021
+ continue;
1022
+ }
1023
+ }
1024
+ if (node.type === "note") {
1025
+ onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "annotation" });
1026
+ continue;
1027
+ }
1028
+ onEvent({ type: "node-status", nodeId: node.id, status: "running" });
1029
+ const inputs = collectInputs(node, incoming, portValues, initialInputs);
1030
+ const exec = pickExecutor(executors, node);
1031
+ if (!exec) {
1032
+ const msg = `No executor registered for kind=${node.type}`;
1033
+ errors.push(msg);
1034
+ onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
1035
+ onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
1036
+ break;
1037
+ }
1038
+ try {
1039
+ const result = await Promise.resolve(
1040
+ exec({
1041
+ node,
1042
+ inputs,
1043
+ abort: (reason) => {
1044
+ throw new Error(reason ?? "aborted");
1045
+ },
1046
+ emit: onEvent
1047
+ })
1048
+ );
1049
+ outputs[node.id] = result;
1050
+ const activated = activatedPorts(node, result);
1051
+ for (const portId of activated.ports) {
1052
+ portValues.set(`${node.id}:${portId}`, activated.value);
1053
+ onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
1054
+ }
1055
+ completed.add(node.id);
1056
+ onEvent({ type: "node-status", nodeId: node.id, status: "done" });
1057
+ } catch (e) {
1058
+ const msg = e instanceof Error ? e.message : String(e);
1059
+ errors.push(msg);
1060
+ onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
1061
+ onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
1062
+ break;
1063
+ }
1064
+ }
1065
+ } finally {
1066
+ if (timer) clearTimeout(timer);
1067
+ }
1068
+ const ok = errors.length === 0;
1069
+ onEvent({ type: "run-end", ok });
1070
+ return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
1071
+ }
1072
+ function indexIncoming(edges) {
1073
+ const map = /* @__PURE__ */ new Map();
1074
+ for (const e of edges) {
1075
+ const list = map.get(e.target) ?? [];
1076
+ list.push(e);
1077
+ map.set(e.target, list);
1078
+ }
1079
+ return map;
1080
+ }
1081
+ function topoSort(graph) {
1082
+ const inDegree = /* @__PURE__ */ new Map();
1083
+ for (const n of graph.nodes) inDegree.set(n.id, 0);
1084
+ for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
1085
+ const queue = [];
1086
+ for (const [id, d] of inDegree) if (d === 0) queue.push(id);
1087
+ const ordered = [];
1088
+ while (queue.length) {
1089
+ const id = queue.shift();
1090
+ ordered.push(id);
1091
+ for (const e of graph.edges) {
1092
+ if (e.source !== id) continue;
1093
+ const next = (inDegree.get(e.target) ?? 0) - 1;
1094
+ inDegree.set(e.target, next);
1095
+ if (next === 0) queue.push(e.target);
1096
+ }
1097
+ }
1098
+ if (ordered.length !== graph.nodes.length) return null;
1099
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
1100
+ return ordered.map((id) => byId.get(id)).filter(Boolean);
1101
+ }
1102
+ function collectInputs(node, incoming, portValues, initial) {
1103
+ const inputs = { ...initial[node.id] ?? {} };
1104
+ for (const e of incoming) {
1105
+ const portId = e.targetHandle ?? "in";
1106
+ const val = portValues.get(`${e.source}:${e.sourceHandle ?? "out"}`);
1107
+ inputs[portId] = val;
1108
+ }
1109
+ return inputs;
1110
+ }
1111
+ function pickExecutor(executors, node) {
1112
+ if (executors[node.id]) return executors[node.id];
1113
+ if (node.type && executors[node.type]) return executors[node.type];
1114
+ return executors["*"];
1115
+ }
1116
+ function activatedPorts(node, result) {
1117
+ if (result && typeof result === "object") {
1118
+ const r = result;
1119
+ if (typeof r.__port === "string") {
1120
+ return { ports: [r.__port], value: r.value };
1121
+ }
1122
+ if (typeof r.branch === "string") {
1123
+ return { ports: [r.branch], value: r.value ?? r };
1124
+ }
1125
+ }
1126
+ const declared = node.data.outputs?.map((p) => p.id) ?? ["out"];
1127
+ return { ports: declared, value: result };
1128
+ }
1129
+
1130
+ // src/runtime/use-flow-run.ts
1131
+ function useFlowRun({ maxFeed = 200 } = {}) {
1132
+ const [statuses, setStatuses] = react.useState({});
1133
+ const [statusText, setStatusText] = react.useState({});
1134
+ const [feed, setFeed] = react.useState([]);
1135
+ const [running, setRunning] = react.useState(false);
1136
+ const [lastResult, setLastResult] = react.useState(null);
1137
+ const abortRef = react.useRef(null);
1138
+ const handleEvent = react.useCallback(
1139
+ (e) => {
1140
+ switch (e.type) {
1141
+ case "node-status":
1142
+ setStatuses((s) => ({ ...s, [e.nodeId]: e.status }));
1143
+ setStatusText((t) => ({ ...t, [e.nodeId]: e.text }));
1144
+ appendFeed({ level: "status", text: `${e.nodeId} \u2192 ${e.status}${e.text ? ` (${e.text})` : ""}`, nodeId: e.nodeId });
1145
+ break;
1146
+ case "node-output":
1147
+ appendFeed({ level: "info", text: `${e.nodeId}.${e.portId} = ${preview(e.value)}`, nodeId: e.nodeId, detail: e.value });
1148
+ break;
1149
+ case "log":
1150
+ appendFeed({ level: e.level, text: e.message, nodeId: e.nodeId, detail: e.detail });
1151
+ break;
1152
+ case "run-start":
1153
+ appendFeed({ level: "info", text: "\u25B6 run started" });
1154
+ break;
1155
+ case "run-end":
1156
+ appendFeed({ level: e.ok ? "info" : "error", text: e.ok ? "\u2713 run complete" : "\u2717 run failed" });
1157
+ break;
1158
+ case "run-error":
1159
+ appendFeed({ level: "error", text: e.error });
1160
+ break;
1161
+ }
1162
+ function appendFeed(partial) {
1163
+ setFeed((f) => {
1164
+ const entry = { id: `${Date.now()}_${f.length}`, at: Date.now(), ...partial };
1165
+ const next = [...f, entry];
1166
+ return next.length > maxFeed ? next.slice(next.length - maxFeed) : next;
1167
+ });
1168
+ }
1169
+ },
1170
+ [maxFeed]
1171
+ );
1172
+ const run = react.useCallback(
1173
+ async (graph, executors, options = {}) => {
1174
+ if (running) {
1175
+ return { ok: false, outputs: {}, error: "another run is already in progress" };
1176
+ }
1177
+ const controller = new AbortController();
1178
+ abortRef.current = controller;
1179
+ const idleStatuses = {};
1180
+ for (const n of graph.nodes) idleStatuses[n.id] = "idle";
1181
+ setStatuses(idleStatuses);
1182
+ setStatusText({});
1183
+ setRunning(true);
1184
+ try {
1185
+ const result = await runFlow(graph, executors, handleEvent, { ...options, signal: controller.signal });
1186
+ setLastResult(result);
1187
+ return result;
1188
+ } finally {
1189
+ setRunning(false);
1190
+ abortRef.current = null;
1191
+ }
1192
+ },
1193
+ [handleEvent, running]
1194
+ );
1195
+ const cancel = react.useCallback(() => abortRef.current?.abort(), []);
1196
+ const reset = react.useCallback(() => {
1197
+ setStatuses({});
1198
+ setStatusText({});
1199
+ setFeed([]);
1200
+ setLastResult(null);
1201
+ }, []);
1202
+ return { statuses, statusText, feed, running, lastResult, run, cancel, reset };
1203
+ }
1204
+ function applyStatusesToNodes(nodes, statuses, statusText) {
1205
+ return nodes.map((n) => ({
1206
+ ...n,
1207
+ data: {
1208
+ ...n.data,
1209
+ status: statuses[n.id] ?? n.data?.status ?? "idle",
1210
+ statusText: statusText[n.id] ?? n.data?.statusText
1211
+ }
1212
+ }));
1213
+ }
1214
+ function preview(v) {
1215
+ try {
1216
+ const s = JSON.stringify(v);
1217
+ return s && s.length > 60 ? s.slice(0, 57) + "\u2026" : s ?? String(v);
1218
+ } catch {
1219
+ return String(v);
1220
+ }
1221
+ }
1222
+
1223
+ // src/schema/workflow-schema.ts
1224
+ var WORKFLOW_SCHEMA_VERSION = 1;
1225
+ var WORKFLOW_SCHEMA_URL = "https://particle.academy/schemas/workflow/v1.json";
1226
+ function exportWorkflow(graph, metadata, view) {
1227
+ return {
1228
+ $schema: WORKFLOW_SCHEMA_URL,
1229
+ version: WORKFLOW_SCHEMA_VERSION,
1230
+ metadata: metadata ? { ...metadata, updatedAt: Date.now() } : void 0,
1231
+ graph: {
1232
+ nodes: graph.nodes.map(toSchemaNode),
1233
+ edges: graph.edges.map(toSchemaEdge)
1234
+ },
1235
+ view
1236
+ };
1237
+ }
1238
+ function toSchemaNode(n) {
1239
+ const data = n.data ?? {};
1240
+ return {
1241
+ id: n.id,
1242
+ kind: data.kind ?? n.type ?? "custom",
1243
+ position: { x: n.position.x, y: n.position.y },
1244
+ label: data.label,
1245
+ description: data.description,
1246
+ config: data.config
1247
+ };
1248
+ }
1249
+ function toSchemaEdge(e) {
1250
+ return {
1251
+ id: e.id,
1252
+ source: e.source,
1253
+ target: e.target,
1254
+ sourceHandle: e.sourceHandle ?? void 0,
1255
+ targetHandle: e.targetHandle ?? void 0,
1256
+ label: typeof e.label === "string" ? e.label : void 0
1257
+ };
1258
+ }
1259
+ function importWorkflow(schema, options = {}) {
1260
+ const issues = [];
1261
+ const lenient = options.lenient === true;
1262
+ if (!schema || typeof schema !== "object") {
1263
+ return { ok: false, graph: { nodes: [], edges: [] }, issues: [{ level: "error", message: "Schema is not an object." }] };
1264
+ }
1265
+ const s = schema;
1266
+ if (s.version !== WORKFLOW_SCHEMA_VERSION) {
1267
+ issues.push({
1268
+ level: lenient ? "warning" : "error",
1269
+ message: `Unsupported workflow schema version: ${s.version} (expected ${WORKFLOW_SCHEMA_VERSION})`
1270
+ });
1271
+ if (!lenient) return { ok: false, graph: { nodes: [], edges: [] }, issues };
1272
+ }
1273
+ const rawNodes = s.graph?.nodes ?? [];
1274
+ const rawEdges = s.graph?.edges ?? [];
1275
+ const nodes = rawNodes.map((n) => {
1276
+ const kind = getNodeKind(n.kind);
1277
+ if (!kind) {
1278
+ issues.push({
1279
+ level: lenient ? "warning" : "error",
1280
+ nodeId: n.id,
1281
+ message: `Unknown kind "${n.kind}" \u2014 register it before importing.`
1282
+ });
1283
+ }
1284
+ const config = n.config ?? (kind ? defaultConfigFor(kind) : {});
1285
+ if (kind) {
1286
+ for (const iss of validateConfig(kind, config)) {
1287
+ issues.push({ level: "warning", nodeId: n.id, message: `${iss.key}: ${iss.message}` });
1288
+ }
1289
+ }
1290
+ return {
1291
+ id: n.id,
1292
+ type: n.kind,
1293
+ position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 },
1294
+ data: {
1295
+ kind: n.kind,
1296
+ label: n.label ?? kind?.label ?? n.kind,
1297
+ description: n.description,
1298
+ config
1299
+ }
1300
+ };
1301
+ });
1302
+ const nodeIds = new Set(nodes.map((n) => n.id));
1303
+ const edges = rawEdges.map((e) => {
1304
+ if (!nodeIds.has(e.source)) {
1305
+ issues.push({ level: "warning", edgeId: e.id, message: `Edge source "${e.source}" not found.` });
1306
+ return null;
1307
+ }
1308
+ if (!nodeIds.has(e.target)) {
1309
+ issues.push({ level: "warning", edgeId: e.id, message: `Edge target "${e.target}" not found.` });
1310
+ return null;
1311
+ }
1312
+ return {
1313
+ id: e.id,
1314
+ source: e.source,
1315
+ target: e.target,
1316
+ sourceHandle: e.sourceHandle,
1317
+ targetHandle: e.targetHandle,
1318
+ label: e.label
1319
+ };
1320
+ }).filter((e) => e !== null);
1321
+ const ok = issues.every((i) => i.level !== "error");
1322
+ return { ok, graph: { nodes, edges }, issues };
1323
+ }
1324
+ function workflowToBlob(schema) {
1325
+ return new Blob([JSON.stringify(schema, null, 2)], { type: "application/json" });
1326
+ }
1327
+ function RegistryNodeInner(props) {
1328
+ const kindName = props.data.kind ?? props.type;
1329
+ const kind = react.useMemo(() => getNodeKind(kindName), [kindName]);
1330
+ if (!kind) {
1331
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-node ff-node--unknown", children: [
1332
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-node__header", style: { background: "#71717a" }, children: [
1333
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__tag", children: "UNKNOWN" }),
1334
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__label", children: kindName })
1335
+ ] }),
1336
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "ff-node__desc", children: [
1337
+ 'No registered kind for "',
1338
+ kindName,
1339
+ '".'
1340
+ ] })
1341
+ ] });
1342
+ }
1343
+ const data = props.data;
1344
+ const status = data.status ?? "idle";
1345
+ const accent = kind.accent ?? categoryAccent(kind.category);
1346
+ const inputs = data.inputs ?? kind.inputs ?? defaultInputs2(kind.category);
1347
+ const outputs = data.outputs ?? kind.outputs ?? defaultOutputs2(kind.category);
1348
+ const config = data.config ?? {};
1349
+ const label = data.label ?? kind.label;
1350
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1351
+ "div",
1352
+ {
1353
+ className: [
1354
+ "ff-node",
1355
+ `ff-node--status-${status}`,
1356
+ `ff-node--cat-${kind.category}`,
1357
+ props.selected ? "ff-node--selected" : ""
1358
+ ].filter(Boolean).join(" "),
1359
+ style: { borderColor: props.selected ? accent : void 0 },
1360
+ children: [
1361
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "ff-node__header", style: { background: accent }, children: [
1362
+ kind.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__icon", "aria-hidden": true, children: kind.icon }),
1363
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__tag", children: kind.label.toUpperCase() }),
1364
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__label", children: label }),
1365
+ status !== "idle" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: `ff-node__dot ff-node__dot--${status}`, "aria-label": `status ${status}` })
1366
+ ] }),
1367
+ data.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-node__desc", children: data.description }),
1368
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-node__body", children: kind.renderBody ? kind.renderBody({ nodeId: props.id, config, selected: props.selected ?? false }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultBody, { config, kind: kind.configSchema }) }),
1369
+ data.statusText && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-node__status-text", children: data.statusText }),
1370
+ inputs.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx(
1371
+ react$1.Handle,
1372
+ {
1373
+ type: "target",
1374
+ position: react$1.Position.Left,
1375
+ id: p.id,
1376
+ style: portStyle2(i, inputs.length),
1377
+ title: p.label ?? p.id
1378
+ },
1379
+ p.id
1380
+ )),
1381
+ outputs.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx(
1382
+ react$1.Handle,
1383
+ {
1384
+ type: "source",
1385
+ position: react$1.Position.Right,
1386
+ id: p.id,
1387
+ style: portStyle2(i, outputs.length),
1388
+ title: p.label ?? p.id
1389
+ },
1390
+ p.id
1391
+ ))
1392
+ ]
1393
+ }
1394
+ );
1395
+ }
1396
+ var RegistryNode = react.memo(RegistryNodeInner);
1397
+ function defaultInputs2(category) {
1398
+ return category === "trigger" ? [] : [{ id: "in" }];
1399
+ }
1400
+ function defaultOutputs2(category) {
1401
+ return category === "output" ? [] : [{ id: "out" }];
1402
+ }
1403
+ function portStyle2(i, total) {
1404
+ if (total <= 1) return {};
1405
+ const slot = 100 / (total + 1) * (i + 1);
1406
+ return { top: `${slot}%` };
1407
+ }
1408
+ function DefaultBody({ config, kind }) {
1409
+ const fields = kind ?? [];
1410
+ const visible = fields.map((f) => ({ field: f, value: config[f.key] })).filter(({ value }) => value !== void 0 && value !== "" && value !== null);
1411
+ if (visible.length === 0) {
1412
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-node__body-empty", children: "\u2014 configure in the panel" });
1413
+ }
1414
+ return /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "ff-node__summary", children: [
1415
+ visible.slice(0, 4).map(({ field, value }) => /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
1416
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-node__summary-key", children: [
1417
+ field.label,
1418
+ ":"
1419
+ ] }),
1420
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-node__summary-value", children: previewValue(value) })
1421
+ ] }, field.key)),
1422
+ visible.length > 4 && /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "ff-node__summary-more", children: [
1423
+ "+ ",
1424
+ visible.length - 4,
1425
+ " more"
1426
+ ] })
1427
+ ] });
1428
+ }
1429
+ function previewValue(v) {
1430
+ if (typeof v === "string") return v.length > 30 ? v.slice(0, 27) + "\u2026" : v;
1431
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
1432
+ try {
1433
+ const j = JSON.stringify(v);
1434
+ return j.length > 30 ? j.slice(0, 27) + "\u2026" : j;
1435
+ } catch {
1436
+ return "[object]";
1437
+ }
1438
+ }
1439
+
1440
+ // src/registry/index.ts
1441
+ function buildNodeTypes() {
1442
+ const map = {};
1443
+ for (const k of listNodeKinds()) map[k.name] = RegistryNode;
1444
+ return map;
1445
+ }
1446
+ function FlowEditor(props) {
1447
+ return /* @__PURE__ */ jsxRuntime.jsx(react$1.ReactFlowProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(
1448
+ "div",
1449
+ {
1450
+ className: ["ff-editor", props.className ?? ""].filter(Boolean).join(" "),
1451
+ style: { height: props.height ?? 720, ...props.style },
1452
+ children: /* @__PURE__ */ jsxRuntime.jsx(FlowEditorInner, { ...props })
1453
+ }
1454
+ ) });
1455
+ }
1456
+ function FlowEditorInner({
1457
+ initial = { nodes: [], edges: [] },
1458
+ value,
1459
+ executors = {},
1460
+ metadata,
1461
+ showPalette = true,
1462
+ showPanel = true,
1463
+ showFeed = true,
1464
+ extraToolbar,
1465
+ onChange
1466
+ }) {
1467
+ const internal = useFlowState(initial);
1468
+ const runner = useFlowRun();
1469
+ const controlled = value !== void 0;
1470
+ const flow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
1471
+ const [, force] = react.useState(0);
1472
+ react.useEffect(() => onNodeKindsChanged(() => force((n) => n + 1)), []);
1473
+ const nodeTypes = react.useMemo(() => buildNodeTypes(), [listNodeKinds().length]);
1474
+ const renderedNodes = react.useMemo(
1475
+ () => applyStatusesToNodes(flow.nodes, runner.statuses, runner.statusText),
1476
+ [flow.nodes, runner.statuses, runner.statusText]
1477
+ );
1478
+ const [selectedId, setSelectedId] = react.useState(null);
1479
+ const selected = react.useMemo(() => flow.nodes.find((n) => n.id === selectedId) ?? null, [flow.nodes, selectedId]);
1480
+ const handleNodeClick = (_e, node) => setSelectedId(node.id);
1481
+ react.useEffect(() => {
1482
+ if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
1483
+ }, [flow.nodes, flow.edges, onChange, controlled]);
1484
+ return /* @__PURE__ */ jsxRuntime.jsx(
1485
+ FlowEditorBody,
1486
+ {
1487
+ showPalette,
1488
+ showPanel,
1489
+ showFeed,
1490
+ flow,
1491
+ runner,
1492
+ executors,
1493
+ metadata,
1494
+ nodeTypes,
1495
+ renderedNodes,
1496
+ selected,
1497
+ setSelectedId,
1498
+ handleNodeClick,
1499
+ extraToolbar
1500
+ }
1501
+ );
1502
+ }
1503
+ function makeControlledFlowAdapter(value, onChange) {
1504
+ const apply = (next) => onChange?.(next);
1505
+ return {
1506
+ nodes: value.nodes,
1507
+ edges: value.edges,
1508
+ setNodes: (next) => {
1509
+ const nextNodes = typeof next === "function" ? next(value.nodes) : next;
1510
+ apply({ nodes: nextNodes, edges: value.edges });
1511
+ },
1512
+ setEdges: (next) => {
1513
+ const nextEdges = typeof next === "function" ? next(value.edges) : next;
1514
+ apply({ nodes: value.nodes, edges: nextEdges });
1515
+ },
1516
+ onNodesChange: (changes) => {
1517
+ apply({ nodes: react$1.applyNodeChanges(changes, value.nodes), edges: value.edges });
1518
+ },
1519
+ onEdgesChange: (changes) => {
1520
+ apply({ nodes: value.nodes, edges: react$1.applyEdgeChanges(changes, value.edges) });
1521
+ },
1522
+ onConnect: (connection) => {
1523
+ apply({ nodes: value.nodes, edges: react$1.addEdge(connection, value.edges) });
1524
+ },
1525
+ toGraph: () => value
1526
+ };
1527
+ }
1528
+ function FlowEditorBody({
1529
+ showPalette,
1530
+ showPanel,
1531
+ showFeed,
1532
+ flow,
1533
+ runner,
1534
+ executors,
1535
+ metadata,
1536
+ nodeTypes,
1537
+ renderedNodes,
1538
+ selected,
1539
+ setSelectedId,
1540
+ handleNodeClick,
1541
+ extraToolbar
1542
+ }) {
1543
+ const rf = react$1.useReactFlow();
1544
+ const dropHandlers = paletteDropHandlers((kindName, evt) => {
1545
+ const kind = getNodeKind(kindName);
1546
+ if (!kind) return;
1547
+ const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
1548
+ const id = `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
1549
+ flow.setNodes((all) => [
1550
+ ...all,
1551
+ {
1552
+ id,
1553
+ type: kind.name,
1554
+ position: { x: point.x - 100, y: point.y - 30 },
1555
+ data: { kind: kind.name, label: kind.label, config: defaultConfigFor(kind) }
1556
+ }
1557
+ ]);
1558
+ setSelectedId(id);
1559
+ });
1560
+ const doExport = () => {
1561
+ const schema = exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
1562
+ const url = URL.createObjectURL(workflowToBlob(schema));
1563
+ const a = document.createElement("a");
1564
+ a.href = url;
1565
+ a.download = `${metadata?.id ?? "workflow"}.json`;
1566
+ a.click();
1567
+ URL.revokeObjectURL(url);
1568
+ };
1569
+ const doImport = () => {
1570
+ const input = document.createElement("input");
1571
+ input.type = "file";
1572
+ input.accept = "application/json";
1573
+ input.onchange = async () => {
1574
+ const file = input.files?.[0];
1575
+ if (!file) return;
1576
+ const text = await file.text();
1577
+ try {
1578
+ const result = importWorkflow(JSON.parse(text), { lenient: true });
1579
+ flow.setNodes(result.graph.nodes);
1580
+ flow.setEdges(result.graph.edges);
1581
+ } catch (e) {
1582
+ console.error("import failed", e);
1583
+ }
1584
+ };
1585
+ input.click();
1586
+ };
1587
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1588
+ showPalette && /* @__PURE__ */ jsxRuntime.jsx(NodePalette, { className: "ff-editor__palette" }),
1589
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__main", ...dropHandlers, children: [
1590
+ /* @__PURE__ */ jsxRuntime.jsx(
1591
+ FlowCanvas,
1592
+ {
1593
+ nodes: renderedNodes,
1594
+ edges: flow.edges,
1595
+ nodeTypes,
1596
+ onNodesChange: flow.onNodesChange,
1597
+ onEdgesChange: flow.onEdgesChange,
1598
+ onConnect: flow.onConnect,
1599
+ onNodeClick: handleNodeClick,
1600
+ height: "100%",
1601
+ toolbar: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1602
+ /* @__PURE__ */ jsxRuntime.jsx(
1603
+ FlowRunControls,
1604
+ {
1605
+ running: runner.running,
1606
+ onRun: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
1607
+ onCancel: runner.cancel,
1608
+ onReset: runner.reset
1609
+ }
1610
+ ),
1611
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
1612
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", onClick: doExport, children: "\u2193 Export" }),
1613
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", onClick: doImport, children: "\u2191 Import" }),
1614
+ extraToolbar,
1615
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-editor__count", children: [
1616
+ flow.nodes.length,
1617
+ " nodes \xB7 ",
1618
+ flow.edges.length,
1619
+ " edges"
1620
+ ] })
1621
+ ] })
1622
+ }
1623
+ ),
1624
+ showFeed && /* @__PURE__ */ jsxRuntime.jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" })
1625
+ ] }),
1626
+ showPanel && /* @__PURE__ */ jsxRuntime.jsx(
1627
+ NodeConfigPanel,
1628
+ {
1629
+ className: "ff-editor__panel",
1630
+ node: selected,
1631
+ onChange: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x))
1632
+ }
1633
+ )
1634
+ ] });
1635
+ }
1636
+
1637
+ // src/index.ts
1638
+ registerBuiltinKinds();
1639
+
1640
+ exports.ActionNode = ActionNode;
1641
+ exports.BUILTIN_KINDS = BUILTIN_KINDS;
1642
+ exports.ConfigFieldRenderer = ConfigFieldRenderer;
1643
+ exports.DecisionNode = DecisionNode;
1644
+ exports.FlowCanvas = FlowCanvas;
1645
+ exports.FlowEditor = FlowEditor;
1646
+ exports.FlowRunControls = FlowRunControls;
1647
+ exports.FlowRunFeed = FlowRunFeed;
1648
+ exports.NodeConfigPanel = NodeConfigPanel;
1649
+ exports.NodePalette = NodePalette;
1650
+ exports.NodeShell = NodeShell;
1651
+ exports.NoteNode = NoteNode;
1652
+ exports.OutputNode = OutputNode;
1653
+ exports.RegistryNode = RegistryNode;
1654
+ exports.SubgraphNode = SubgraphNode;
1655
+ exports.TriggerNode = TriggerNode;
1656
+ exports.WORKFLOW_SCHEMA_URL = WORKFLOW_SCHEMA_URL;
1657
+ exports.WORKFLOW_SCHEMA_VERSION = WORKFLOW_SCHEMA_VERSION;
1658
+ exports.applyStatusesToNodes = applyStatusesToNodes;
1659
+ exports.buildNodeTypes = buildNodeTypes;
1660
+ exports.categoryAccent = categoryAccent;
1661
+ exports.defaultConfigFor = defaultConfigFor;
1662
+ exports.defaultNodeTypes = defaultNodeTypes;
1663
+ exports.exportWorkflow = exportWorkflow;
1664
+ exports.getNodeKind = getNodeKind;
1665
+ exports.importWorkflow = importWorkflow;
1666
+ exports.listNodeKinds = listNodeKinds;
1667
+ exports.onNodeKindsChanged = onNodeKindsChanged;
1668
+ exports.paletteDropHandlers = paletteDropHandlers;
1669
+ exports.registerBuiltinKinds = registerBuiltinKinds;
1670
+ exports.registerNodeKind = registerNodeKind;
1671
+ exports.runFlow = runFlow;
1672
+ exports.useFlowRun = useFlowRun;
1673
+ exports.useFlowState = useFlowState;
1674
+ exports.validateConfig = validateConfig;
1675
+ exports.workflowToBlob = workflowToBlob;
1676
+ //# sourceMappingURL=index.cjs.map
1677
+ //# sourceMappingURL=index.cjs.map