@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.js ADDED
@@ -0,0 +1,705 @@
1
+ import { useFlowState, useFlowRun, applyStatusesToNodes } from './chunk-JNYYJHNJ.js';
2
+ export { applyStatusesToNodes, runFlow, useFlowRun, useFlowState } from './chunk-JNYYJHNJ.js';
3
+ import { registerBuiltinKinds, buildNodeTypes } from './chunk-XZEUFVJ2.js';
4
+ export { BUILTIN_KINDS, RegistryNode, buildNodeTypes, registerBuiltinKinds } from './chunk-XZEUFVJ2.js';
5
+ import { exportWorkflow, workflowToBlob, importWorkflow } from './chunk-BCXECQUC.js';
6
+ export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, workflowToBlob } from './chunk-BCXECQUC.js';
7
+ import { onNodeKindsChanged, listNodeKinds, categoryAccent, getNodeKind, validateConfig, defaultConfigFor } from './chunk-WNVBXXOL.js';
8
+ export { categoryAccent, defaultConfigFor, getNodeKind, listNodeKinds, onNodeKindsChanged, registerNodeKind, validateConfig } from './chunk-WNVBXXOL.js';
9
+ import { memo, useMemo, useState, useEffect, useRef } from 'react';
10
+ import { Handle, Position, ReactFlow, BackgroundVariant, Background, Controls, MiniMap, ReactFlowProvider, addEdge, applyEdgeChanges, applyNodeChanges, useReactFlow } from '@xyflow/react';
11
+ import '@xyflow/react/dist/style.css';
12
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
13
+
14
+ function NodeShellInner({
15
+ node,
16
+ accent,
17
+ tag,
18
+ icon,
19
+ showInputs = true,
20
+ showOutputs = true,
21
+ children
22
+ }) {
23
+ const data = node.data;
24
+ const status = data.status ?? "idle";
25
+ const inputs = data.inputs ?? defaultInputs(showInputs);
26
+ const outputs = data.outputs ?? defaultOutputs(showOutputs);
27
+ return /* @__PURE__ */ jsxs(
28
+ "div",
29
+ {
30
+ className: ["ff-node", `ff-node--status-${status}`, node.selected ? "ff-node--selected" : ""].filter(Boolean).join(" "),
31
+ style: { borderColor: node.selected ? accent : void 0 },
32
+ children: [
33
+ /* @__PURE__ */ jsxs("header", { className: "ff-node__header", style: { background: accent }, children: [
34
+ /* @__PURE__ */ jsx("span", { className: "ff-node__icon", "aria-hidden": true, children: icon ?? null }),
35
+ /* @__PURE__ */ jsx("span", { className: "ff-node__tag", children: tag }),
36
+ /* @__PURE__ */ jsx("span", { className: "ff-node__label", children: data.label }),
37
+ status !== "idle" && /* @__PURE__ */ jsx(StatusDot, { status })
38
+ ] }),
39
+ data.description && /* @__PURE__ */ jsx("p", { className: "ff-node__desc", children: data.description }),
40
+ children && /* @__PURE__ */ jsx("div", { className: "ff-node__body", children }),
41
+ data.statusText && /* @__PURE__ */ jsx("p", { className: "ff-node__status-text", children: data.statusText }),
42
+ inputs.map((p, i) => /* @__PURE__ */ jsx(
43
+ Handle,
44
+ {
45
+ type: "target",
46
+ position: Position.Left,
47
+ id: p.id,
48
+ style: portStyle(i, inputs.length),
49
+ title: p.label ?? p.id
50
+ },
51
+ p.id
52
+ )),
53
+ outputs.map((p, i) => /* @__PURE__ */ jsx(
54
+ Handle,
55
+ {
56
+ type: "source",
57
+ position: Position.Right,
58
+ id: p.id,
59
+ style: portStyle(i, outputs.length),
60
+ title: p.label ?? p.id
61
+ },
62
+ p.id
63
+ ))
64
+ ]
65
+ }
66
+ );
67
+ }
68
+ var NodeShell = memo(NodeShellInner);
69
+ function defaultInputs(show) {
70
+ return show ? [{ id: "in" }] : [];
71
+ }
72
+ function defaultOutputs(show) {
73
+ return show ? [{ id: "out" }] : [];
74
+ }
75
+ function portStyle(i, total) {
76
+ if (total <= 1) return {};
77
+ const slot = 100 / (total + 1) * (i + 1);
78
+ return { top: `${slot}%` };
79
+ }
80
+ function StatusDot({ status }) {
81
+ return /* @__PURE__ */ jsx("span", { className: `ff-node__dot ff-node__dot--${status}`, "aria-label": `status ${status}` });
82
+ }
83
+ function TriggerNodeInner(props) {
84
+ return /* @__PURE__ */ jsx(NodeShell, { node: props, accent: "#10b981", tag: "TRIGGER", icon: "\u26A1", showInputs: false });
85
+ }
86
+ var TriggerNode = memo(TriggerNodeInner);
87
+ function ActionNodeInner(props) {
88
+ return /* @__PURE__ */ jsx(NodeShell, { node: props, accent: "#3b82f6", tag: "ACTION", icon: "\u25B8" });
89
+ }
90
+ var ActionNode = memo(ActionNodeInner);
91
+ var DEFAULT_BRANCHES = [
92
+ { id: "true", label: "true" },
93
+ { id: "false", label: "false" }
94
+ ];
95
+ function DecisionNodeInner(props) {
96
+ if (!props.data.outputs) {
97
+ props = { ...props, data: { ...props.data, outputs: DEFAULT_BRANCHES } };
98
+ }
99
+ return /* @__PURE__ */ jsx(NodeShell, { node: props, accent: "#f59e0b", tag: "DECISION", icon: "\u25C7" });
100
+ }
101
+ var DecisionNode = memo(DecisionNodeInner);
102
+ function OutputNodeInner(props) {
103
+ return /* @__PURE__ */ jsx(NodeShell, { node: props, accent: "#a855f7", tag: "OUTPUT", icon: "\u25CF", showOutputs: false });
104
+ }
105
+ var OutputNode = memo(OutputNodeInner);
106
+ function NoteNodeInner({ data, selected }) {
107
+ const noteData = data;
108
+ return /* @__PURE__ */ jsxs("div", { className: `ff-note ${selected ? "ff-note--selected" : ""}`, children: [
109
+ noteData.label && /* @__PURE__ */ jsx("div", { className: "ff-note__title", children: noteData.label }),
110
+ noteData.body && /* @__PURE__ */ jsx("p", { className: "ff-note__body", children: noteData.body })
111
+ ] });
112
+ }
113
+ var NoteNode = memo(NoteNodeInner);
114
+ function SubgraphNodeInner(props) {
115
+ const data = props.data;
116
+ const childCount = data.childIds?.length ?? 0;
117
+ return /* @__PURE__ */ jsx(NodeShell, { node: props, accent: "#0ea5e9", tag: "SUBGRAPH", icon: "\u2750", children: /* @__PURE__ */ jsxs("div", { className: "ff-subgraph__meta", children: [
118
+ /* @__PURE__ */ jsxs("span", { children: [
119
+ childCount,
120
+ " node",
121
+ childCount === 1 ? "" : "s"
122
+ ] }),
123
+ /* @__PURE__ */ jsx("span", { children: data.collapsed === false ? "expanded" : "collapsed" })
124
+ ] }) });
125
+ }
126
+ var SubgraphNode = memo(SubgraphNodeInner);
127
+
128
+ // src/components/nodes/index.ts
129
+ var defaultNodeTypes = {
130
+ trigger: TriggerNode,
131
+ action: ActionNode,
132
+ decision: DecisionNode,
133
+ output: OutputNode,
134
+ note: NoteNode,
135
+ subgraph: SubgraphNode
136
+ };
137
+ var DEFAULT_FIT_VIEW = { padding: 0.2 };
138
+ var DEFAULT_EDGE_OPTIONS = {
139
+ type: "smoothstep",
140
+ animated: false
141
+ };
142
+ function FlowCanvas({
143
+ nodes,
144
+ edges,
145
+ background = BackgroundVariant.Dots,
146
+ showControls = true,
147
+ showMinimap = false,
148
+ height = 600,
149
+ toolbar,
150
+ nodeTypes,
151
+ edgeTypes,
152
+ className,
153
+ style,
154
+ ...rest
155
+ }) {
156
+ const mergedNodeTypes = useMemo(
157
+ () => ({ ...defaultNodeTypes, ...nodeTypes ?? {} }),
158
+ [nodeTypes]
159
+ );
160
+ const mergedEdgeTypes = useMemo(
161
+ () => edgeTypes ? { ...edgeTypes } : void 0,
162
+ [edgeTypes]
163
+ );
164
+ return /* @__PURE__ */ jsxs("div", { className: ["ff-canvas", className ?? ""].filter(Boolean).join(" "), style: { height, ...style }, children: [
165
+ toolbar && /* @__PURE__ */ jsx("div", { className: "ff-canvas__toolbar", children: toolbar }),
166
+ /* @__PURE__ */ jsx("div", { className: "ff-canvas__surface", children: /* @__PURE__ */ jsxs(
167
+ ReactFlow,
168
+ {
169
+ nodes,
170
+ edges,
171
+ nodeTypes: mergedNodeTypes,
172
+ edgeTypes: mergedEdgeTypes,
173
+ fitView: true,
174
+ fitViewOptions: DEFAULT_FIT_VIEW,
175
+ defaultEdgeOptions: DEFAULT_EDGE_OPTIONS,
176
+ proOptions: { hideAttribution: true },
177
+ ...rest,
178
+ children: [
179
+ background !== "none" && /* @__PURE__ */ jsx(Background, { variant: background, gap: 20, size: 1, color: "rgba(0,0,0,0.18)" }),
180
+ showControls && /* @__PURE__ */ jsx(Controls, { className: "ff-controls", position: "bottom-right" }),
181
+ showMinimap && /* @__PURE__ */ jsx(MiniMap, { className: "ff-minimap", pannable: true, zoomable: true })
182
+ ]
183
+ }
184
+ ) })
185
+ ] });
186
+ }
187
+ var CATEGORY_ORDER = ["trigger", "logic", "data", "ai", "io", "human", "output", "custom"];
188
+ var CATEGORY_LABELS = {
189
+ trigger: "Triggers",
190
+ logic: "Logic",
191
+ data: "Data",
192
+ ai: "AI",
193
+ io: "Connectors",
194
+ human: "Human",
195
+ output: "Output",
196
+ custom: "Custom"
197
+ };
198
+ function NodePalette({ categories, onPick, className, style }) {
199
+ const [, setRev] = useState(0);
200
+ useEffect(() => onNodeKindsChanged(() => setRev((n) => n + 1)), []);
201
+ const [query, setQuery] = useState("");
202
+ const visibleCats = categories ?? CATEGORY_ORDER;
203
+ const grouped = useMemo(() => {
204
+ const all = listNodeKinds();
205
+ const q = query.trim().toLowerCase();
206
+ const filtered = q ? all.filter((k) => k.name.includes(q) || k.label.toLowerCase().includes(q) || (k.description ?? "").toLowerCase().includes(q)) : all;
207
+ const map = /* @__PURE__ */ new Map();
208
+ for (const cat of visibleCats) map.set(cat, []);
209
+ for (const k of filtered) {
210
+ if (!map.has(k.category)) map.set(k.category, []);
211
+ map.get(k.category).push(k);
212
+ }
213
+ return map;
214
+ }, [query, visibleCats]);
215
+ return /* @__PURE__ */ jsxs("aside", { className: ["ff-palette", className ?? ""].filter(Boolean).join(" "), style, children: [
216
+ /* @__PURE__ */ jsx("div", { className: "ff-palette__search", children: /* @__PURE__ */ jsx(
217
+ "input",
218
+ {
219
+ className: "ff-palette__search-input",
220
+ placeholder: "Search nodes\u2026",
221
+ value: query,
222
+ onChange: (e) => setQuery(e.target.value)
223
+ }
224
+ ) }),
225
+ /* @__PURE__ */ jsx("div", { className: "ff-palette__list", children: Array.from(grouped.entries()).map(
226
+ ([cat, kinds]) => kinds.length === 0 ? null : /* @__PURE__ */ jsxs("section", { className: "ff-palette__group", children: [
227
+ /* @__PURE__ */ jsx("header", { className: "ff-palette__group-label", children: CATEGORY_LABELS[cat] ?? cat }),
228
+ kinds.map((k) => /* @__PURE__ */ jsx(KindRow, { kind: k, onPick }, k.name))
229
+ ] }, cat)
230
+ ) })
231
+ ] });
232
+ }
233
+ function KindRow({ kind, onPick }) {
234
+ const accent = kind.accent ?? categoryAccent(kind.category);
235
+ const onDragStart = (e) => {
236
+ e.dataTransfer.effectAllowed = "copy";
237
+ e.dataTransfer.setData("application/x-fancy-flow-kind", kind.name);
238
+ e.dataTransfer.setData("text/plain", kind.name);
239
+ };
240
+ return /* @__PURE__ */ jsxs(
241
+ "button",
242
+ {
243
+ type: "button",
244
+ className: "ff-palette__row",
245
+ draggable: true,
246
+ onDragStart,
247
+ onClick: () => onPick?.(kind),
248
+ title: kind.description ?? kind.name,
249
+ children: [
250
+ /* @__PURE__ */ jsx("span", { className: "ff-palette__row-dot", style: { background: accent }, children: kind.icon ?? "" }),
251
+ /* @__PURE__ */ jsxs("span", { className: "ff-palette__row-text", children: [
252
+ /* @__PURE__ */ jsx("span", { className: "ff-palette__row-label", children: kind.label }),
253
+ kind.description && /* @__PURE__ */ jsx("span", { className: "ff-palette__row-desc", children: kind.description })
254
+ ] })
255
+ ]
256
+ }
257
+ );
258
+ }
259
+ function paletteDropHandlers(onDrop) {
260
+ return {
261
+ onDragOver: (e) => {
262
+ if (e.dataTransfer.types.includes("application/x-fancy-flow-kind")) {
263
+ e.preventDefault();
264
+ e.dataTransfer.dropEffect = "copy";
265
+ }
266
+ },
267
+ onDrop: (e) => {
268
+ const name = e.dataTransfer.getData("application/x-fancy-flow-kind");
269
+ if (!name) return;
270
+ e.preventDefault();
271
+ onDrop(name, e);
272
+ }
273
+ };
274
+ }
275
+ function ConfigFieldRenderer({ field, value, onChange, renderCredentialField }) {
276
+ switch (field.type) {
277
+ case "text":
278
+ return /* @__PURE__ */ jsx(
279
+ "input",
280
+ {
281
+ className: "ff-panel__input",
282
+ type: "text",
283
+ value: value ?? "",
284
+ placeholder: field.placeholder,
285
+ onChange: (e) => onChange(e.target.value)
286
+ }
287
+ );
288
+ case "textarea":
289
+ return /* @__PURE__ */ jsx(
290
+ "textarea",
291
+ {
292
+ className: "ff-panel__input ff-panel__input--textarea",
293
+ rows: field.rows ?? 4,
294
+ value: value ?? "",
295
+ placeholder: field.placeholder,
296
+ onChange: (e) => onChange(e.target.value)
297
+ }
298
+ );
299
+ case "number":
300
+ return /* @__PURE__ */ jsx(
301
+ "input",
302
+ {
303
+ className: "ff-panel__input",
304
+ type: "number",
305
+ value: value ?? "",
306
+ min: field.min,
307
+ max: field.max,
308
+ step: field.step ?? 1,
309
+ onChange: (e) => onChange(e.target.value === "" ? void 0 : Number(e.target.value))
310
+ }
311
+ );
312
+ case "switch":
313
+ return /* @__PURE__ */ jsxs("label", { className: "ff-panel__switch", children: [
314
+ /* @__PURE__ */ jsx(
315
+ "input",
316
+ {
317
+ type: "checkbox",
318
+ checked: !!value,
319
+ onChange: (e) => onChange(e.target.checked)
320
+ }
321
+ ),
322
+ /* @__PURE__ */ jsx("span", { className: "ff-panel__switch-slider" })
323
+ ] });
324
+ case "select":
325
+ return /* @__PURE__ */ jsxs(
326
+ "select",
327
+ {
328
+ className: "ff-panel__input",
329
+ value: value ?? "",
330
+ onChange: (e) => onChange(e.target.value),
331
+ children: [
332
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u2014" }),
333
+ field.options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
334
+ ]
335
+ }
336
+ );
337
+ case "json":
338
+ return /* @__PURE__ */ jsx(JsonField, { value, onChange, rows: field.rows });
339
+ case "expression":
340
+ return /* @__PURE__ */ jsx(
341
+ "textarea",
342
+ {
343
+ className: "ff-panel__input ff-panel__input--expression",
344
+ rows: 2,
345
+ value: value ?? "",
346
+ placeholder: field.example ?? "{{ $json.field }}",
347
+ spellCheck: false,
348
+ onChange: (e) => onChange(e.target.value)
349
+ }
350
+ );
351
+ case "credential":
352
+ if (renderCredentialField) {
353
+ return /* @__PURE__ */ jsx(Fragment, { children: renderCredentialField({ credentialType: field.credentialType, value, onChange }) });
354
+ }
355
+ return /* @__PURE__ */ jsx(
356
+ "input",
357
+ {
358
+ className: "ff-panel__input ff-panel__input--credential",
359
+ type: "text",
360
+ value: value ?? "",
361
+ placeholder: `Credential reference (${field.credentialType})`,
362
+ onChange: (e) => onChange(e.target.value)
363
+ }
364
+ );
365
+ default:
366
+ return null;
367
+ }
368
+ }
369
+ function JsonField({ value, onChange, rows }) {
370
+ const initial = useMemo(() => {
371
+ try {
372
+ return value === void 0 ? "" : JSON.stringify(value, null, 2);
373
+ } catch {
374
+ return "";
375
+ }
376
+ }, [value]);
377
+ return /* @__PURE__ */ jsx(
378
+ "textarea",
379
+ {
380
+ className: "ff-panel__input ff-panel__input--json",
381
+ rows: rows ?? 6,
382
+ defaultValue: initial,
383
+ spellCheck: false,
384
+ onBlur: (e) => {
385
+ const text = e.target.value.trim();
386
+ if (text === "") {
387
+ onChange(void 0);
388
+ return;
389
+ }
390
+ try {
391
+ onChange(JSON.parse(text));
392
+ } catch {
393
+ }
394
+ }
395
+ }
396
+ );
397
+ }
398
+ function NodeConfigPanel({
399
+ node,
400
+ onChange,
401
+ header,
402
+ renderCredentialField,
403
+ className,
404
+ style
405
+ }) {
406
+ if (!node) {
407
+ return /* @__PURE__ */ jsxs("aside", { className: ["ff-panel", "ff-panel--empty", className ?? ""].filter(Boolean).join(" "), style, children: [
408
+ header,
409
+ /* @__PURE__ */ jsx("p", { className: "ff-panel__empty", children: "Select a node to configure it." })
410
+ ] });
411
+ }
412
+ const kindName = node.data.kind ?? node.type;
413
+ const kind = useMemo(() => getNodeKind(kindName), [kindName]);
414
+ const config = useMemo(() => node.data.config ?? {}, [node.data]);
415
+ if (!kind) {
416
+ return /* @__PURE__ */ jsxs("aside", { className: ["ff-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
417
+ header,
418
+ /* @__PURE__ */ jsxs("p", { className: "ff-panel__empty", children: [
419
+ "Unknown kind: ",
420
+ kindName
421
+ ] })
422
+ ] });
423
+ }
424
+ const setLabel = (label) => onChange({ ...node, data: { ...node.data, label } });
425
+ const setDescription = (description) => onChange({ ...node, data: { ...node.data, description } });
426
+ const setConfigValue = (key, value) => onChange({ ...node, data: { ...node.data, config: { ...config, [key]: value } } });
427
+ const issues = validateConfig(kind, config);
428
+ return /* @__PURE__ */ jsxs("aside", { className: ["ff-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
429
+ header,
430
+ /* @__PURE__ */ jsxs("header", { className: "ff-panel__header", children: [
431
+ /* @__PURE__ */ jsx("span", { className: "ff-panel__kind-tag", children: kind.label }),
432
+ kind.description && /* @__PURE__ */ jsx("p", { className: "ff-panel__kind-desc", children: kind.description })
433
+ ] }),
434
+ /* @__PURE__ */ jsxs("div", { className: "ff-panel__field", children: [
435
+ /* @__PURE__ */ jsx("label", { className: "ff-panel__label", children: "Label" }),
436
+ /* @__PURE__ */ jsx(
437
+ "input",
438
+ {
439
+ className: "ff-panel__input",
440
+ value: node.data.label ?? "",
441
+ onChange: (e) => setLabel(e.target.value),
442
+ placeholder: kind.label
443
+ }
444
+ )
445
+ ] }),
446
+ /* @__PURE__ */ jsxs("div", { className: "ff-panel__field", children: [
447
+ /* @__PURE__ */ jsx("label", { className: "ff-panel__label", children: "Description" }),
448
+ /* @__PURE__ */ jsx(
449
+ "textarea",
450
+ {
451
+ className: "ff-panel__input ff-panel__input--textarea",
452
+ rows: 2,
453
+ value: node.data.description ?? "",
454
+ onChange: (e) => setDescription(e.target.value)
455
+ }
456
+ )
457
+ ] }),
458
+ kind.renderPanel ? kind.renderPanel({
459
+ config,
460
+ onChange: (next) => onChange({ ...node, data: { ...node.data, config: next } }),
461
+ nodeId: node.id
462
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
463
+ (kind.configSchema ?? []).length > 0 && /* @__PURE__ */ jsx("hr", { className: "ff-panel__divider" }),
464
+ (kind.configSchema ?? []).map((field) => /* @__PURE__ */ jsxs("div", { className: "ff-panel__field", children: [
465
+ /* @__PURE__ */ jsxs("label", { className: "ff-panel__label", children: [
466
+ field.label,
467
+ field.required && /* @__PURE__ */ jsx("span", { className: "ff-panel__required", "aria-hidden": true, children: " *" })
468
+ ] }),
469
+ field.description && /* @__PURE__ */ jsx("p", { className: "ff-panel__hint", children: field.description }),
470
+ /* @__PURE__ */ jsx(
471
+ ConfigFieldRenderer,
472
+ {
473
+ field,
474
+ value: config[field.key],
475
+ onChange: (v) => setConfigValue(field.key, v),
476
+ renderCredentialField
477
+ }
478
+ )
479
+ ] }, field.key))
480
+ ] }),
481
+ issues.length > 0 && /* @__PURE__ */ jsx("div", { className: "ff-panel__issues", children: issues.map((iss) => /* @__PURE__ */ jsxs("p", { className: "ff-panel__issue", children: [
482
+ "\u26A0 ",
483
+ iss.message
484
+ ] }, iss.key)) })
485
+ ] });
486
+ }
487
+ function FlowRunControls({ running, onRun, onCancel, onReset, className, style }) {
488
+ return /* @__PURE__ */ jsxs("div", { className: ["ff-run-controls", className ?? ""].filter(Boolean).join(" "), style, children: [
489
+ !running ? /* @__PURE__ */ jsx("button", { type: "button", className: "ff-run-controls__btn ff-run-controls__btn--run", onClick: onRun, children: "\u25B6 Run" }) : /* @__PURE__ */ jsx("button", { type: "button", className: "ff-run-controls__btn ff-run-controls__btn--cancel", onClick: onCancel, children: "\u23F9 Cancel" }),
490
+ onReset && /* @__PURE__ */ jsx("button", { type: "button", className: "ff-run-controls__btn ff-run-controls__btn--reset", onClick: onReset, disabled: running, children: "Reset" })
491
+ ] });
492
+ }
493
+ function FlowRunFeed({ entries, className, style }) {
494
+ const scrollRef = useRef(null);
495
+ useEffect(() => {
496
+ const el = scrollRef.current;
497
+ if (el) el.scrollTop = el.scrollHeight;
498
+ }, [entries.length]);
499
+ return /* @__PURE__ */ jsx("div", { className: ["ff-run-feed", className ?? ""].filter(Boolean).join(" "), style, ref: scrollRef, children: entries.length === 0 ? /* @__PURE__ */ jsx("p", { className: "ff-run-feed__empty", children: "No run events yet." }) : entries.map((e) => /* @__PURE__ */ jsxs("div", { className: `ff-run-feed__row ff-run-feed__row--${e.level}`, children: [
500
+ /* @__PURE__ */ jsx("span", { className: "ff-run-feed__time", children: formatTime(e.at) }),
501
+ e.nodeId && /* @__PURE__ */ jsx("span", { className: "ff-run-feed__node", children: e.nodeId }),
502
+ /* @__PURE__ */ jsx("span", { className: "ff-run-feed__text", children: e.text })
503
+ ] }, e.id)) });
504
+ }
505
+ function formatTime(at) {
506
+ const d = new Date(at);
507
+ return `${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}.${Math.floor(d.getMilliseconds() / 100)}`;
508
+ }
509
+ function FlowEditor(props) {
510
+ return /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(
511
+ "div",
512
+ {
513
+ className: ["ff-editor", props.className ?? ""].filter(Boolean).join(" "),
514
+ style: { height: props.height ?? 720, ...props.style },
515
+ children: /* @__PURE__ */ jsx(FlowEditorInner, { ...props })
516
+ }
517
+ ) });
518
+ }
519
+ function FlowEditorInner({
520
+ initial = { nodes: [], edges: [] },
521
+ value,
522
+ executors = {},
523
+ metadata,
524
+ showPalette = true,
525
+ showPanel = true,
526
+ showFeed = true,
527
+ extraToolbar,
528
+ onChange
529
+ }) {
530
+ const internal = useFlowState(initial);
531
+ const runner = useFlowRun();
532
+ const controlled = value !== void 0;
533
+ const flow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
534
+ const [, force] = useState(0);
535
+ useEffect(() => onNodeKindsChanged(() => force((n) => n + 1)), []);
536
+ const nodeTypes = useMemo(() => buildNodeTypes(), [listNodeKinds().length]);
537
+ const renderedNodes = useMemo(
538
+ () => applyStatusesToNodes(flow.nodes, runner.statuses, runner.statusText),
539
+ [flow.nodes, runner.statuses, runner.statusText]
540
+ );
541
+ const [selectedId, setSelectedId] = useState(null);
542
+ const selected = useMemo(() => flow.nodes.find((n) => n.id === selectedId) ?? null, [flow.nodes, selectedId]);
543
+ const handleNodeClick = (_e, node) => setSelectedId(node.id);
544
+ useEffect(() => {
545
+ if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
546
+ }, [flow.nodes, flow.edges, onChange, controlled]);
547
+ return /* @__PURE__ */ jsx(
548
+ FlowEditorBody,
549
+ {
550
+ showPalette,
551
+ showPanel,
552
+ showFeed,
553
+ flow,
554
+ runner,
555
+ executors,
556
+ metadata,
557
+ nodeTypes,
558
+ renderedNodes,
559
+ selected,
560
+ setSelectedId,
561
+ handleNodeClick,
562
+ extraToolbar
563
+ }
564
+ );
565
+ }
566
+ function makeControlledFlowAdapter(value, onChange) {
567
+ const apply = (next) => onChange?.(next);
568
+ return {
569
+ nodes: value.nodes,
570
+ edges: value.edges,
571
+ setNodes: (next) => {
572
+ const nextNodes = typeof next === "function" ? next(value.nodes) : next;
573
+ apply({ nodes: nextNodes, edges: value.edges });
574
+ },
575
+ setEdges: (next) => {
576
+ const nextEdges = typeof next === "function" ? next(value.edges) : next;
577
+ apply({ nodes: value.nodes, edges: nextEdges });
578
+ },
579
+ onNodesChange: (changes) => {
580
+ apply({ nodes: applyNodeChanges(changes, value.nodes), edges: value.edges });
581
+ },
582
+ onEdgesChange: (changes) => {
583
+ apply({ nodes: value.nodes, edges: applyEdgeChanges(changes, value.edges) });
584
+ },
585
+ onConnect: (connection) => {
586
+ apply({ nodes: value.nodes, edges: addEdge(connection, value.edges) });
587
+ },
588
+ toGraph: () => value
589
+ };
590
+ }
591
+ function FlowEditorBody({
592
+ showPalette,
593
+ showPanel,
594
+ showFeed,
595
+ flow,
596
+ runner,
597
+ executors,
598
+ metadata,
599
+ nodeTypes,
600
+ renderedNodes,
601
+ selected,
602
+ setSelectedId,
603
+ handleNodeClick,
604
+ extraToolbar
605
+ }) {
606
+ const rf = useReactFlow();
607
+ const dropHandlers = paletteDropHandlers((kindName, evt) => {
608
+ const kind = getNodeKind(kindName);
609
+ if (!kind) return;
610
+ const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
611
+ const id = `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
612
+ flow.setNodes((all) => [
613
+ ...all,
614
+ {
615
+ id,
616
+ type: kind.name,
617
+ position: { x: point.x - 100, y: point.y - 30 },
618
+ data: { kind: kind.name, label: kind.label, config: defaultConfigFor(kind) }
619
+ }
620
+ ]);
621
+ setSelectedId(id);
622
+ });
623
+ const doExport = () => {
624
+ const schema = exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
625
+ const url = URL.createObjectURL(workflowToBlob(schema));
626
+ const a = document.createElement("a");
627
+ a.href = url;
628
+ a.download = `${metadata?.id ?? "workflow"}.json`;
629
+ a.click();
630
+ URL.revokeObjectURL(url);
631
+ };
632
+ const doImport = () => {
633
+ const input = document.createElement("input");
634
+ input.type = "file";
635
+ input.accept = "application/json";
636
+ input.onchange = async () => {
637
+ const file = input.files?.[0];
638
+ if (!file) return;
639
+ const text = await file.text();
640
+ try {
641
+ const result = importWorkflow(JSON.parse(text), { lenient: true });
642
+ flow.setNodes(result.graph.nodes);
643
+ flow.setEdges(result.graph.edges);
644
+ } catch (e) {
645
+ console.error("import failed", e);
646
+ }
647
+ };
648
+ input.click();
649
+ };
650
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
651
+ showPalette && /* @__PURE__ */ jsx(NodePalette, { className: "ff-editor__palette" }),
652
+ /* @__PURE__ */ jsxs("div", { className: "ff-editor__main", ...dropHandlers, children: [
653
+ /* @__PURE__ */ jsx(
654
+ FlowCanvas,
655
+ {
656
+ nodes: renderedNodes,
657
+ edges: flow.edges,
658
+ nodeTypes,
659
+ onNodesChange: flow.onNodesChange,
660
+ onEdgesChange: flow.onEdgesChange,
661
+ onConnect: flow.onConnect,
662
+ onNodeClick: handleNodeClick,
663
+ height: "100%",
664
+ toolbar: /* @__PURE__ */ jsxs(Fragment, { children: [
665
+ /* @__PURE__ */ jsx(
666
+ FlowRunControls,
667
+ {
668
+ running: runner.running,
669
+ onRun: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
670
+ onCancel: runner.cancel,
671
+ onReset: runner.reset
672
+ }
673
+ ),
674
+ /* @__PURE__ */ jsx("span", { className: "ff-editor__sep" }),
675
+ /* @__PURE__ */ jsx("button", { className: "ff-editor__btn", onClick: doExport, children: "\u2193 Export" }),
676
+ /* @__PURE__ */ jsx("button", { className: "ff-editor__btn", onClick: doImport, children: "\u2191 Import" }),
677
+ extraToolbar,
678
+ /* @__PURE__ */ jsxs("span", { className: "ff-editor__count", children: [
679
+ flow.nodes.length,
680
+ " nodes \xB7 ",
681
+ flow.edges.length,
682
+ " edges"
683
+ ] })
684
+ ] })
685
+ }
686
+ ),
687
+ showFeed && /* @__PURE__ */ jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" })
688
+ ] }),
689
+ showPanel && /* @__PURE__ */ jsx(
690
+ NodeConfigPanel,
691
+ {
692
+ className: "ff-editor__panel",
693
+ node: selected,
694
+ onChange: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x))
695
+ }
696
+ )
697
+ ] });
698
+ }
699
+
700
+ // src/index.ts
701
+ registerBuiltinKinds();
702
+
703
+ export { ActionNode, ConfigFieldRenderer, DecisionNode, FlowCanvas, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodeShell, NoteNode, OutputNode, SubgraphNode, TriggerNode, defaultNodeTypes, paletteDropHandlers };
704
+ //# sourceMappingURL=index.js.map
705
+ //# sourceMappingURL=index.js.map