@particle-academy/fancy-flow 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +93 -0
  2. package/dist/{chunk-BCXECQUC.js → chunk-6GGFEZH7.js} +3 -3
  3. package/dist/{chunk-BCXECQUC.js.map → chunk-6GGFEZH7.js.map} +1 -1
  4. package/dist/{chunk-M2XKGQQL.js → chunk-6RHAQ2LP.js} +228 -17
  5. package/dist/chunk-6RHAQ2LP.js.map +1 -0
  6. package/dist/{chunk-WNVBXXOL.js → chunk-CPSOC27D.js} +43 -2
  7. package/dist/chunk-CPSOC27D.js.map +1 -0
  8. package/dist/{chunk-NVULCEDX.js → chunk-HNBO4HP3.js} +8 -4
  9. package/dist/chunk-HNBO4HP3.js.map +1 -0
  10. package/dist/chunk-TITD5W4Y.js +26 -0
  11. package/dist/chunk-TITD5W4Y.js.map +1 -0
  12. package/dist/{chunk-QSSQRQN4.js → chunk-VEI743ZX.js} +3 -3
  13. package/dist/{chunk-QSSQRQN4.js.map → chunk-VEI743ZX.js.map} +1 -1
  14. package/dist/engine.cjs +32 -2
  15. package/dist/engine.cjs.map +1 -1
  16. package/dist/engine.js +3 -1
  17. package/dist/index.cjs +717 -40
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +45 -6
  20. package/dist/index.d.ts +45 -6
  21. package/dist/index.js +415 -23
  22. package/dist/index.js.map +1 -1
  23. package/dist/registry/index.d.cts +111 -4
  24. package/dist/registry/index.d.ts +111 -4
  25. package/dist/registry.cjs +313 -31
  26. package/dist/registry.cjs.map +1 -1
  27. package/dist/registry.js +3 -2
  28. package/dist/runtime.cjs +32 -2
  29. package/dist/runtime.cjs.map +1 -1
  30. package/dist/runtime.js +4 -2
  31. package/dist/schema.cjs +41 -0
  32. package/dist/schema.cjs.map +1 -1
  33. package/dist/schema.js +2 -2
  34. package/dist/styles.css +191 -0
  35. package/dist/styles.css.map +1 -1
  36. package/dist/types-BocBFh6l.d.ts +221 -0
  37. package/dist/types-DKqaUjF_.d.cts +221 -0
  38. package/dist/ux.cjs.map +1 -1
  39. package/dist/ux.d.cts +1 -1
  40. package/dist/ux.d.ts +1 -1
  41. package/dist/ux.js +1 -1
  42. package/package.json +4 -4
  43. package/dist/chunk-M2XKGQQL.js.map +0 -1
  44. package/dist/chunk-NVULCEDX.js.map +0 -1
  45. package/dist/chunk-WNVBXXOL.js.map +0 -1
  46. package/dist/types-C0wdN6QX.d.cts +0 -121
  47. package/dist/types-DnMe9Vsf.d.ts +0 -121
@@ -0,0 +1,221 @@
1
+ import { ReactNode } from 'react';
2
+ import { P as PortDescriptor, N as NodeExecutor } from './types-BS3Gwnkq.js';
3
+
4
+ /** Categories used by the palette for grouping. */
5
+ type NodeCategory = "trigger" | "logic" | "data" | "ai" | "io" | "human" | "output" | "custom";
6
+ /**
7
+ * Tagged-union of config-field shapes the auto-form knows how to render.
8
+ * Each variant has a `key` (the config object property it writes to) and
9
+ * a `label`. Hosts can render fully custom panels via the kind's
10
+ * `renderPanel` instead.
11
+ */
12
+ type ConfigField = TextConfigField | TextareaConfigField | NumberConfigField | SelectConfigField | SwitchConfigField | JsonConfigField | ExpressionConfigField | CredentialConfigField | RepeaterConfigField | KeyValueConfigField | DocumentConfigField;
13
+ /**
14
+ * Field types that may appear inside a `repeater` row. Excludes `repeater`
15
+ * itself — a row of rows has no sane form rendering, and the nesting would
16
+ * make the config shape hard for an agent to emit.
17
+ */
18
+ type RepeaterRowField = Exclude<ConfigField, RepeaterConfigField>;
19
+ type ConfigFieldBase = {
20
+ key: string;
21
+ label: string;
22
+ description?: string;
23
+ required?: boolean;
24
+ };
25
+ type TextConfigField = ConfigFieldBase & {
26
+ type: "text";
27
+ placeholder?: string;
28
+ default?: string;
29
+ /**
30
+ * Optional fixed choices. When present the field renders as a select
31
+ * instead of a free-text input — the same field declaration serves both a
32
+ * free-form value and a constrained one, so a kind can gain choices later
33
+ * without changing its `type` or migrating stored config.
34
+ *
35
+ * Bare strings are shorthand for `{ value, label: value }`.
36
+ *
37
+ * Unlike `type: "select"`, a stored value outside the list is preserved and
38
+ * shown rather than rejected — choices can change after configs are saved,
39
+ * and silently dropping an author's value is worse than showing a stale one.
40
+ */
41
+ choices?: Array<string | {
42
+ value: string;
43
+ label?: string;
44
+ }>;
45
+ };
46
+ type TextareaConfigField = ConfigFieldBase & {
47
+ type: "textarea";
48
+ placeholder?: string;
49
+ rows?: number;
50
+ default?: string;
51
+ };
52
+ type NumberConfigField = ConfigFieldBase & {
53
+ type: "number";
54
+ min?: number;
55
+ max?: number;
56
+ step?: number;
57
+ default?: number;
58
+ };
59
+ type SelectConfigField = ConfigFieldBase & {
60
+ type: "select";
61
+ options: Array<{
62
+ value: string;
63
+ label: string;
64
+ }>;
65
+ default?: string;
66
+ };
67
+ type SwitchConfigField = ConfigFieldBase & {
68
+ type: "switch";
69
+ default?: boolean;
70
+ };
71
+ type JsonConfigField = ConfigFieldBase & {
72
+ type: "json";
73
+ language?: "json" | "yaml" | "javascript";
74
+ rows?: number;
75
+ default?: unknown;
76
+ };
77
+ type ExpressionConfigField = ConfigFieldBase & {
78
+ type: "expression";
79
+ /** A short example string shown as placeholder, e.g. "{{ $json.name }}". */
80
+ example?: string;
81
+ default?: string;
82
+ };
83
+ type CredentialConfigField = ConfigFieldBase & {
84
+ type: "credential";
85
+ /** Logical credential type. The host implements lookup / picker. */
86
+ credentialType: string;
87
+ };
88
+ /**
89
+ * Repeater — an editable list of objects, each row authored with its own
90
+ * sub-schema of ConfigFields. This is what a node kind should reach for
91
+ * instead of `type: "json"` whenever its config is list-shaped (form field
92
+ * lists, router routes, tool bindings). Keeping it declarative is what lets
93
+ * `NodeConfigPanel` stay the single authoring surface for humans AND keeps
94
+ * the shape introspectable for agents.
95
+ *
96
+ * Value shape: `Array<Record<string, unknown>>`.
97
+ */
98
+ type RepeaterConfigField = ConfigFieldBase & {
99
+ type: "repeater";
100
+ /** Schema applied to every row. */
101
+ fields: RepeaterRowField[];
102
+ /**
103
+ * Row field whose value titles the row in the editor. Falls back to the
104
+ * first field's value, then to "Item N".
105
+ */
106
+ titleKey?: string;
107
+ /** Label for the add button. Default: "Add". */
108
+ addLabel?: string;
109
+ /** Bounds enforced by validation + the add/remove controls. */
110
+ minItems?: number;
111
+ maxItems?: number;
112
+ default?: Array<Record<string, unknown>>;
113
+ };
114
+ /**
115
+ * Key/value map — an editable `Record<string, string>`. Use for filter maps,
116
+ * input bindings, header maps, and case→port tables.
117
+ *
118
+ * Value shape: `Record<string, string>`.
119
+ */
120
+ type KeyValueConfigField = ConfigFieldBase & {
121
+ type: "keyvalue";
122
+ keyLabel?: string;
123
+ valueLabel?: string;
124
+ keyPlaceholder?: string;
125
+ valuePlaceholder?: string;
126
+ /** Constrain values to a fixed set (e.g. the node's own port ids). */
127
+ valueOptions?: Array<{
128
+ value: string;
129
+ label: string;
130
+ }>;
131
+ addLabel?: string;
132
+ default?: Record<string, string>;
133
+ };
134
+ /**
135
+ * Document — an opaque rich/structured document stored in node config and
136
+ * edited by a HOST-SUPPLIED editor, wired through
137
+ * `NodeConfigPanel`'s `renderDocumentField`.
138
+ *
139
+ * fancy-flow deliberately does not know what a document *is* — same
140
+ * arrangement as `credential`. That keeps rich human-input surfaces (authored
141
+ * pages, required-reading steps, multi-section forms) possible without the
142
+ * editor taking a dependency on any particular document model or CMS.
143
+ */
144
+ type DocumentConfigField = ConfigFieldBase & {
145
+ type: "document";
146
+ /**
147
+ * Logical document format, passed through to the host renderer so one host
148
+ * can serve several (e.g. "stages", "markdown", "portable-text").
149
+ */
150
+ documentType?: string;
151
+ default?: unknown;
152
+ };
153
+ /**
154
+ * Port declaration — either a fixed list, or a function of the node's config
155
+ * for kinds whose branches ARE their config (switch cases, router routes).
156
+ *
157
+ * Declaring the function form keeps the canvas ports, the config, and the
158
+ * ports the runtime activates in lockstep automatically; a static list forces
159
+ * every consumer to hand-sync `data.outputs` from config in a custom renderer,
160
+ * and forgetting to breaks routing silently at execution time.
161
+ */
162
+ type PortSpec<TConfig = unknown> = PortDescriptor[] | ((config: TConfig) => PortDescriptor[]);
163
+ /**
164
+ * Context passed to a kind's optional `renderBody`. Hosts can read the
165
+ * resolved config + node id to render whatever fancy-* component they
166
+ * want inside the node card.
167
+ */
168
+ type RenderBodyContext<TConfig = unknown> = {
169
+ nodeId: string;
170
+ config: TConfig;
171
+ selected: boolean;
172
+ };
173
+ /**
174
+ * NodeKindDefinition — declares an authorable node type. Register one
175
+ * via `registerNodeKind()`. Hosts (and the agent bridge) introspect the
176
+ * registry to know what's authorable.
177
+ */
178
+ type NodeKindDefinition<TConfig = Record<string, unknown>, TIn = any, TOut = any> = {
179
+ /** Stable identifier — used as the xyflow node `type` and the schema export key. */
180
+ name: string;
181
+ /** Palette grouping. */
182
+ category: NodeCategory;
183
+ /** Display label. */
184
+ label: string;
185
+ /** One-line summary surfaced in the palette + agent bridge. */
186
+ description?: string;
187
+ /**
188
+ * Icon rendered in the node header and the palette row. A glyph string is
189
+ * fine; any ReactNode works, so a brand SVG can be dropped in directly.
190
+ */
191
+ icon?: ReactNode;
192
+ /** Hex / CSS color for the header bar. Falls back to a category default. */
193
+ accent?: string;
194
+ /** Declarative form schema for the config panel. */
195
+ configSchema?: ConfigField[];
196
+ /** Default config values used when a node of this kind is created. */
197
+ defaultConfig?: TConfig;
198
+ /** Input ports. Defaults vary by category. See `PortSpec`. */
199
+ inputs?: PortSpec<TConfig>;
200
+ /** Output ports. Defaults vary by category. See `PortSpec`. */
201
+ outputs?: PortSpec<TConfig>;
202
+ /** Optional custom body rendered inside the node card. */
203
+ renderBody?: (ctx: RenderBodyContext<TConfig>) => ReactNode;
204
+ /**
205
+ * Optional override for the config panel. Receives the current config and
206
+ * an onChange to update it. Defaults to the auto-generated form.
207
+ */
208
+ renderPanel?: (props: {
209
+ config: TConfig;
210
+ onChange: (next: TConfig) => void;
211
+ nodeId: string;
212
+ }) => ReactNode;
213
+ /**
214
+ * Executor — host-implemented function that runs at flow execution.
215
+ * Optional: built-in agentic kinds ship without one so the host wires
216
+ * the actual work (memory store backend, LLM client, HTTP fetcher, etc.).
217
+ */
218
+ executor?: NodeExecutor<TIn, TOut>;
219
+ };
220
+
221
+ export type { ConfigField as C, DocumentConfigField as D, ExpressionConfigField as E, JsonConfigField as J, KeyValueConfigField as K, NodeCategory as N, PortSpec as P, RenderBodyContext as R, SelectConfigField as S, TextConfigField as T, NodeKindDefinition as a, CredentialConfigField as b, NumberConfigField as c, RepeaterConfigField as d, RepeaterRowField as e, SwitchConfigField as f, TextareaConfigField as g };
@@ -0,0 +1,221 @@
1
+ import { ReactNode } from 'react';
2
+ import { P as PortDescriptor, N as NodeExecutor } from './types-BS3Gwnkq.cjs';
3
+
4
+ /** Categories used by the palette for grouping. */
5
+ type NodeCategory = "trigger" | "logic" | "data" | "ai" | "io" | "human" | "output" | "custom";
6
+ /**
7
+ * Tagged-union of config-field shapes the auto-form knows how to render.
8
+ * Each variant has a `key` (the config object property it writes to) and
9
+ * a `label`. Hosts can render fully custom panels via the kind's
10
+ * `renderPanel` instead.
11
+ */
12
+ type ConfigField = TextConfigField | TextareaConfigField | NumberConfigField | SelectConfigField | SwitchConfigField | JsonConfigField | ExpressionConfigField | CredentialConfigField | RepeaterConfigField | KeyValueConfigField | DocumentConfigField;
13
+ /**
14
+ * Field types that may appear inside a `repeater` row. Excludes `repeater`
15
+ * itself — a row of rows has no sane form rendering, and the nesting would
16
+ * make the config shape hard for an agent to emit.
17
+ */
18
+ type RepeaterRowField = Exclude<ConfigField, RepeaterConfigField>;
19
+ type ConfigFieldBase = {
20
+ key: string;
21
+ label: string;
22
+ description?: string;
23
+ required?: boolean;
24
+ };
25
+ type TextConfigField = ConfigFieldBase & {
26
+ type: "text";
27
+ placeholder?: string;
28
+ default?: string;
29
+ /**
30
+ * Optional fixed choices. When present the field renders as a select
31
+ * instead of a free-text input — the same field declaration serves both a
32
+ * free-form value and a constrained one, so a kind can gain choices later
33
+ * without changing its `type` or migrating stored config.
34
+ *
35
+ * Bare strings are shorthand for `{ value, label: value }`.
36
+ *
37
+ * Unlike `type: "select"`, a stored value outside the list is preserved and
38
+ * shown rather than rejected — choices can change after configs are saved,
39
+ * and silently dropping an author's value is worse than showing a stale one.
40
+ */
41
+ choices?: Array<string | {
42
+ value: string;
43
+ label?: string;
44
+ }>;
45
+ };
46
+ type TextareaConfigField = ConfigFieldBase & {
47
+ type: "textarea";
48
+ placeholder?: string;
49
+ rows?: number;
50
+ default?: string;
51
+ };
52
+ type NumberConfigField = ConfigFieldBase & {
53
+ type: "number";
54
+ min?: number;
55
+ max?: number;
56
+ step?: number;
57
+ default?: number;
58
+ };
59
+ type SelectConfigField = ConfigFieldBase & {
60
+ type: "select";
61
+ options: Array<{
62
+ value: string;
63
+ label: string;
64
+ }>;
65
+ default?: string;
66
+ };
67
+ type SwitchConfigField = ConfigFieldBase & {
68
+ type: "switch";
69
+ default?: boolean;
70
+ };
71
+ type JsonConfigField = ConfigFieldBase & {
72
+ type: "json";
73
+ language?: "json" | "yaml" | "javascript";
74
+ rows?: number;
75
+ default?: unknown;
76
+ };
77
+ type ExpressionConfigField = ConfigFieldBase & {
78
+ type: "expression";
79
+ /** A short example string shown as placeholder, e.g. "{{ $json.name }}". */
80
+ example?: string;
81
+ default?: string;
82
+ };
83
+ type CredentialConfigField = ConfigFieldBase & {
84
+ type: "credential";
85
+ /** Logical credential type. The host implements lookup / picker. */
86
+ credentialType: string;
87
+ };
88
+ /**
89
+ * Repeater — an editable list of objects, each row authored with its own
90
+ * sub-schema of ConfigFields. This is what a node kind should reach for
91
+ * instead of `type: "json"` whenever its config is list-shaped (form field
92
+ * lists, router routes, tool bindings). Keeping it declarative is what lets
93
+ * `NodeConfigPanel` stay the single authoring surface for humans AND keeps
94
+ * the shape introspectable for agents.
95
+ *
96
+ * Value shape: `Array<Record<string, unknown>>`.
97
+ */
98
+ type RepeaterConfigField = ConfigFieldBase & {
99
+ type: "repeater";
100
+ /** Schema applied to every row. */
101
+ fields: RepeaterRowField[];
102
+ /**
103
+ * Row field whose value titles the row in the editor. Falls back to the
104
+ * first field's value, then to "Item N".
105
+ */
106
+ titleKey?: string;
107
+ /** Label for the add button. Default: "Add". */
108
+ addLabel?: string;
109
+ /** Bounds enforced by validation + the add/remove controls. */
110
+ minItems?: number;
111
+ maxItems?: number;
112
+ default?: Array<Record<string, unknown>>;
113
+ };
114
+ /**
115
+ * Key/value map — an editable `Record<string, string>`. Use for filter maps,
116
+ * input bindings, header maps, and case→port tables.
117
+ *
118
+ * Value shape: `Record<string, string>`.
119
+ */
120
+ type KeyValueConfigField = ConfigFieldBase & {
121
+ type: "keyvalue";
122
+ keyLabel?: string;
123
+ valueLabel?: string;
124
+ keyPlaceholder?: string;
125
+ valuePlaceholder?: string;
126
+ /** Constrain values to a fixed set (e.g. the node's own port ids). */
127
+ valueOptions?: Array<{
128
+ value: string;
129
+ label: string;
130
+ }>;
131
+ addLabel?: string;
132
+ default?: Record<string, string>;
133
+ };
134
+ /**
135
+ * Document — an opaque rich/structured document stored in node config and
136
+ * edited by a HOST-SUPPLIED editor, wired through
137
+ * `NodeConfigPanel`'s `renderDocumentField`.
138
+ *
139
+ * fancy-flow deliberately does not know what a document *is* — same
140
+ * arrangement as `credential`. That keeps rich human-input surfaces (authored
141
+ * pages, required-reading steps, multi-section forms) possible without the
142
+ * editor taking a dependency on any particular document model or CMS.
143
+ */
144
+ type DocumentConfigField = ConfigFieldBase & {
145
+ type: "document";
146
+ /**
147
+ * Logical document format, passed through to the host renderer so one host
148
+ * can serve several (e.g. "stages", "markdown", "portable-text").
149
+ */
150
+ documentType?: string;
151
+ default?: unknown;
152
+ };
153
+ /**
154
+ * Port declaration — either a fixed list, or a function of the node's config
155
+ * for kinds whose branches ARE their config (switch cases, router routes).
156
+ *
157
+ * Declaring the function form keeps the canvas ports, the config, and the
158
+ * ports the runtime activates in lockstep automatically; a static list forces
159
+ * every consumer to hand-sync `data.outputs` from config in a custom renderer,
160
+ * and forgetting to breaks routing silently at execution time.
161
+ */
162
+ type PortSpec<TConfig = unknown> = PortDescriptor[] | ((config: TConfig) => PortDescriptor[]);
163
+ /**
164
+ * Context passed to a kind's optional `renderBody`. Hosts can read the
165
+ * resolved config + node id to render whatever fancy-* component they
166
+ * want inside the node card.
167
+ */
168
+ type RenderBodyContext<TConfig = unknown> = {
169
+ nodeId: string;
170
+ config: TConfig;
171
+ selected: boolean;
172
+ };
173
+ /**
174
+ * NodeKindDefinition — declares an authorable node type. Register one
175
+ * via `registerNodeKind()`. Hosts (and the agent bridge) introspect the
176
+ * registry to know what's authorable.
177
+ */
178
+ type NodeKindDefinition<TConfig = Record<string, unknown>, TIn = any, TOut = any> = {
179
+ /** Stable identifier — used as the xyflow node `type` and the schema export key. */
180
+ name: string;
181
+ /** Palette grouping. */
182
+ category: NodeCategory;
183
+ /** Display label. */
184
+ label: string;
185
+ /** One-line summary surfaced in the palette + agent bridge. */
186
+ description?: string;
187
+ /**
188
+ * Icon rendered in the node header and the palette row. A glyph string is
189
+ * fine; any ReactNode works, so a brand SVG can be dropped in directly.
190
+ */
191
+ icon?: ReactNode;
192
+ /** Hex / CSS color for the header bar. Falls back to a category default. */
193
+ accent?: string;
194
+ /** Declarative form schema for the config panel. */
195
+ configSchema?: ConfigField[];
196
+ /** Default config values used when a node of this kind is created. */
197
+ defaultConfig?: TConfig;
198
+ /** Input ports. Defaults vary by category. See `PortSpec`. */
199
+ inputs?: PortSpec<TConfig>;
200
+ /** Output ports. Defaults vary by category. See `PortSpec`. */
201
+ outputs?: PortSpec<TConfig>;
202
+ /** Optional custom body rendered inside the node card. */
203
+ renderBody?: (ctx: RenderBodyContext<TConfig>) => ReactNode;
204
+ /**
205
+ * Optional override for the config panel. Receives the current config and
206
+ * an onChange to update it. Defaults to the auto-generated form.
207
+ */
208
+ renderPanel?: (props: {
209
+ config: TConfig;
210
+ onChange: (next: TConfig) => void;
211
+ nodeId: string;
212
+ }) => ReactNode;
213
+ /**
214
+ * Executor — host-implemented function that runs at flow execution.
215
+ * Optional: built-in agentic kinds ship without one so the host wires
216
+ * the actual work (memory store backend, LLM client, HTTP fetcher, etc.).
217
+ */
218
+ executor?: NodeExecutor<TIn, TOut>;
219
+ };
220
+
221
+ export type { ConfigField as C, DocumentConfigField as D, ExpressionConfigField as E, JsonConfigField as J, KeyValueConfigField as K, NodeCategory as N, PortSpec as P, RenderBodyContext as R, SelectConfigField as S, TextConfigField as T, NodeKindDefinition as a, CredentialConfigField as b, NumberConfigField as c, RepeaterConfigField as d, RepeaterRowField as e, SwitchConfigField as f, TextareaConfigField as g };
package/dist/ux.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/registry/registry.ts","../src/ux.ts"],"names":["createEffectDispatcher","useMemo"],"mappings":"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","file":"ux.cjs","sourcesContent":["import type { ConfigField, NodeKindDefinition } from \"./types\";\n\nconst kinds = new Map<string, NodeKindDefinition<any, any, any>>();\nconst listeners = new Set<() => void>();\n\n/**\n * registerNodeKind — install a node kind in the global registry. Returns\n * an `unregister` function. Calling with the same name replaces the prior\n * registration (handy for HMR).\n */\nexport function registerNodeKind<TC = any, TI = any, TO = any>(\n definition: NodeKindDefinition<TC, TI, TO>,\n): () => void {\n kinds.set(definition.name, definition as NodeKindDefinition<any, any, any>);\n notify();\n return () => {\n if (kinds.get(definition.name) === (definition as any)) {\n kinds.delete(definition.name);\n notify();\n }\n };\n}\n\n/** Get a single kind by name, or null. */\nexport function getNodeKind(name: string): NodeKindDefinition | null {\n return (kinds.get(name) as NodeKindDefinition) ?? null;\n}\n\n/** List every registered kind, optionally filtered by category. */\nexport function listNodeKinds(category?: string): NodeKindDefinition[] {\n const all = Array.from(kinds.values()) as NodeKindDefinition[];\n return category ? all.filter((k) => k.category === category) : all;\n}\n\n/** Subscribe to registry changes. Returns an unsubscribe function. */\nexport function onNodeKindsChanged(listener: () => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n}\n\nfunction notify(): void {\n for (const l of listeners) l();\n}\n\n/** Fill in defaults from a kind's configSchema for newly-created nodes. */\nexport function defaultConfigFor(kind: NodeKindDefinition): Record<string, unknown> {\n const fromKind = kind.defaultConfig ? { ...(kind.defaultConfig as Record<string, unknown>) } : {};\n for (const field of kind.configSchema ?? []) {\n if (fromKind[field.key] !== undefined) continue;\n if (\"default\" in field && (field as any).default !== undefined) {\n fromKind[field.key] = (field as any).default;\n }\n }\n return fromKind;\n}\n\n/**\n * Validate a config object against a kind's schema. Returns an array of\n * issues (empty = valid). Validation is intentionally light — type\n * coercion + required-field checks. Hosts can layer Zod / Ajv on top.\n */\nexport function validateConfig(\n kind: NodeKindDefinition,\n config: Record<string, unknown>,\n): Array<{ key: string; message: string }> {\n const issues: Array<{ key: string; message: string }> = [];\n for (const field of kind.configSchema ?? []) {\n const value = config[field.key];\n if (field.required && (value === undefined || value === null || value === \"\")) {\n issues.push({ key: field.key, message: `${field.label} is required` });\n continue;\n }\n if (value === undefined || value === null) continue;\n const issue = validateField(field, value);\n if (issue) issues.push({ key: field.key, message: issue });\n }\n return issues;\n}\n\nfunction validateField(field: ConfigField, value: unknown): string | null {\n switch (field.type) {\n case \"text\":\n case \"textarea\":\n case \"expression\":\n case \"credential\":\n return typeof value === \"string\" ? null : `${field.label} must be a string`;\n case \"number\": {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return `${field.label} must be a number`;\n if (field.min !== undefined && value < field.min) return `${field.label} must be >= ${field.min}`;\n if (field.max !== undefined && value > field.max) return `${field.label} must be <= ${field.max}`;\n return null;\n }\n case \"switch\":\n return typeof value === \"boolean\" ? null : `${field.label} must be a boolean`;\n case \"select\": {\n const allowed = field.options.map((o) => o.value);\n return allowed.includes(String(value)) ? null : `${field.label} must be one of ${allowed.join(\", \")}`;\n }\n case \"json\":\n return null; // permissive — just JSON-shaped\n default:\n return null;\n }\n}\n\n/** Default accents per category. */\nexport function categoryAccent(category: string): string {\n switch (category) {\n case \"trigger\": return \"#10b981\";\n case \"logic\": return \"#f59e0b\";\n case \"data\": return \"#0ea5e9\";\n case \"ai\": return \"#8b5cf6\";\n case \"io\": return \"#3b82f6\";\n case \"human\": return \"#ec4899\";\n case \"output\": return \"#a855f7\";\n default: return \"#71717a\";\n }\n}\n","/**\n * FlowRunnerUx — the **flow-driven UX** bridge. The headless counterpart to\n * agent-integrations: where that wires an *agent* to host UI surfaces, this\n * wires a *running flow* to host UX. Both share the same primitives from\n * `@particle-academy/fancy-auto-common` (activity bus, effect dispatch).\n *\n * The host registers named UX effects (toast, navigate, confirm, …); this turns\n * each into a flow executor keyed by a node kind (`ux_<effect>` by default), so\n * dropping a `ux_toast` node into a `<FlowEditor>` and running it fires the\n * host's toast. Every dispatch broadcasts an `AutoActivityEvent` (source:\"flow\")\n * for presence / logging. Human-in-the-loop is free: an effect that returns a\n * Promise (e.g. an approval dialog) pauses the run until the user resolves it.\n *\n * const ux = createFlowRunnerUx({\n * effects: {\n * toast: ({ message }) => toast({ title: message }),\n * navigate:({ to }) => router.visit(to),\n * confirm: ({ prompt }) => new Promise(res => openDialog(prompt, res)), // pauses the run\n * },\n * });\n * ux.registerKinds(); // adds ux_toast / ux_navigate / ux_confirm to the palette\n * <FlowEditor initial={graph} executors={ux.executors} />\n */\nimport { useMemo } from \"react\";\nimport {\n createEffectDispatcher,\n type DispatchActor,\n type EffectRegistry,\n} from \"@particle-academy/fancy-auto-common\";\nimport { registerNodeKind } from \"./registry/registry\";\nimport type { ConfigField, NodeCategory } from \"./registry/types\";\nimport type { ExecutorRegistry, FlowNode } from \"./types\";\n\nexport type { AutoActivityEvent } from \"@particle-academy/fancy-auto-common\";\n\n/** Per-effect presentation for the palette node kind that drives it. */\nexport type UxEffectMeta = {\n /** Palette label. Default: the effect name. */\n label?: string;\n /** One-line palette description. */\n description?: string;\n /** Emoji / glyph for the node header. */\n icon?: string;\n /** Header accent color. */\n accent?: string;\n /** Palette grouping. Default \"output\". */\n category?: NodeCategory;\n /** Config form fields = the effect's params. */\n configSchema?: ConfigField[];\n};\n\nexport type FlowRunnerUxOptions = {\n /** Named host UX effects the flow can invoke. */\n effects: EffectRegistry;\n /** Optional per-effect palette metadata (used by `registerKinds`). */\n meta?: Record<string, UxEffectMeta>;\n /** Identifies the flow run in emitted activity. Default `{ id: \"flow\", source: \"flow\" }`. */\n actor?: DispatchActor;\n /** Map an effect name to its node-kind name. Default `ux_<effect>`. */\n kindFor?: (effectName: string) => string;\n};\n\nexport type FlowRunnerUx = {\n /** Executor registry to hand to `<FlowEditor executors>` or `runFlow`. */\n executors: ExecutorRegistry;\n /** Invoke an effect imperatively (also emits activity). */\n dispatch: <R = unknown>(name: string, params?: unknown) => Promise<R>;\n /** All effect names. */\n effectNames: () => string[];\n /** Register a palette node kind per effect (`ux_<effect>`). Idempotent. */\n registerKinds: () => void;\n};\n\nconst defaultKindFor = (name: string) => `ux_${name}`;\n\n/** Build a FlowRunnerUx from a set of host effects. Framework-agnostic. */\nexport function createFlowRunnerUx(options: FlowRunnerUxOptions): FlowRunnerUx {\n const {\n effects,\n meta = {},\n actor = { id: \"flow\", source: \"flow\" },\n kindFor = defaultKindFor,\n } = options;\n\n const dispatcher = createEffectDispatcher(effects, { actor, targetKind: \"ux\" });\n\n const executors: ExecutorRegistry = {};\n for (const name of Object.keys(effects)) {\n executors[kindFor(name)] = async ({ node }: { node: FlowNode }) => {\n const params = (node.data as { config?: Record<string, unknown> } | undefined)?.config ?? {};\n const result = await dispatcher.dispatch(name, params);\n // A UX effect can drive flow control: if it returns the decision sugar\n // (`{ branch }` or `{ __port }`) — e.g. an interactive \"choose\" effect that\n // awaits a human pick and returns the chosen port — pass it straight\n // through so runFlow routes on it. Otherwise wrap the result for the feed.\n if (result && typeof result === \"object\" && (\"branch\" in result || \"__port\" in result)) {\n return result;\n }\n return { effect: name, result };\n };\n }\n\n const registerKinds = () => {\n for (const name of Object.keys(effects)) {\n const m = meta[name] ?? {};\n registerNodeKind({\n name: kindFor(name),\n category: m.category ?? \"output\",\n label: m.label ?? name,\n description: m.description ?? `Flow-driven UX effect: ${name}.`,\n icon: m.icon ?? \"✨\",\n accent: m.accent ?? \"#8b5cf6\",\n inputs: [{ id: \"in\" }],\n outputs: [],\n configSchema: m.configSchema,\n });\n }\n };\n\n return {\n executors,\n dispatch: dispatcher.dispatch as FlowRunnerUx[\"dispatch\"],\n effectNames: dispatcher.names,\n registerKinds,\n };\n}\n\n/**\n * React hook form — memoizes on the effect-name set + actor id so the returned\n * executors keep a stable identity across renders.\n */\nexport function useFlowRunnerUx(options: FlowRunnerUxOptions): FlowRunnerUx {\n const key = `${Object.keys(options.effects).sort().join(\",\")}|${options.actor?.id ?? \"flow\"}`;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return useMemo(() => createFlowRunnerUx(options), [key]);\n}\n"]}
1
+ {"version":3,"sources":["../src/registry/registry.ts","../src/ux.ts"],"names":["createEffectDispatcher","useMemo"],"mappings":"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","file":"ux.cjs","sourcesContent":["import type { ConfigField, NodeKindDefinition } from \"./types\";\n\nconst kinds = new Map<string, NodeKindDefinition<any, any, any>>();\nconst listeners = new Set<() => void>();\n\n/**\n * registerNodeKind — install a node kind in the global registry. Returns\n * an `unregister` function. Calling with the same name replaces the prior\n * registration (handy for HMR).\n */\nexport function registerNodeKind<TC = any, TI = any, TO = any>(\n definition: NodeKindDefinition<TC, TI, TO>,\n): () => void {\n kinds.set(definition.name, definition as NodeKindDefinition<any, any, any>);\n notify();\n return () => {\n if (kinds.get(definition.name) === (definition as any)) {\n kinds.delete(definition.name);\n notify();\n }\n };\n}\n\n/** Get a single kind by name, or null. */\nexport function getNodeKind(name: string): NodeKindDefinition | null {\n return (kinds.get(name) as NodeKindDefinition) ?? null;\n}\n\n/** List every registered kind, optionally filtered by category. */\nexport function listNodeKinds(category?: string): NodeKindDefinition[] {\n const all = Array.from(kinds.values()) as NodeKindDefinition[];\n return category ? all.filter((k) => k.category === category) : all;\n}\n\n/** Subscribe to registry changes. Returns an unsubscribe function. */\nexport function onNodeKindsChanged(listener: () => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n}\n\nfunction notify(): void {\n for (const l of listeners) l();\n}\n\n/** Fill in defaults from a kind's configSchema for newly-created nodes. */\nexport function defaultConfigFor(kind: NodeKindDefinition): Record<string, unknown> {\n const fromKind = kind.defaultConfig ? { ...(kind.defaultConfig as Record<string, unknown>) } : {};\n for (const field of kind.configSchema ?? []) {\n if (fromKind[field.key] !== undefined) continue;\n if (\"default\" in field && (field as any).default !== undefined) {\n fromKind[field.key] = (field as any).default;\n }\n }\n return fromKind;\n}\n\n/**\n * Validate a config object against a kind's schema. Returns an array of\n * issues (empty = valid). Validation is intentionally light — type\n * coercion + required-field checks. Hosts can layer Zod / Ajv on top.\n */\nexport function validateConfig(\n kind: NodeKindDefinition,\n config: Record<string, unknown>,\n): Array<{ key: string; message: string }> {\n const issues: Array<{ key: string; message: string }> = [];\n for (const field of kind.configSchema ?? []) {\n const value = config[field.key];\n if (field.required && (value === undefined || value === null || value === \"\")) {\n issues.push({ key: field.key, message: `${field.label} is required` });\n continue;\n }\n if (value === undefined || value === null) continue;\n const issue = validateField(field, value);\n if (issue) issues.push({ key: field.key, message: issue });\n }\n return issues;\n}\n\nfunction validateField(field: ConfigField, value: unknown): string | null {\n switch (field.type) {\n case \"text\":\n case \"textarea\":\n case \"expression\":\n case \"credential\":\n return typeof value === \"string\" ? null : `${field.label} must be a string`;\n case \"number\": {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return `${field.label} must be a number`;\n if (field.min !== undefined && value < field.min) return `${field.label} must be >= ${field.min}`;\n if (field.max !== undefined && value > field.max) return `${field.label} must be <= ${field.max}`;\n return null;\n }\n case \"switch\":\n return typeof value === \"boolean\" ? null : `${field.label} must be a boolean`;\n case \"select\": {\n const allowed = field.options.map((o) => o.value);\n return allowed.includes(String(value)) ? null : `${field.label} must be one of ${allowed.join(\", \")}`;\n }\n case \"json\":\n return null; // permissive — just JSON-shaped\n case \"repeater\": {\n if (!Array.isArray(value)) return `${field.label} must be a list`;\n if (field.minItems !== undefined && value.length < field.minItems) {\n return `${field.label} needs at least ${field.minItems}`;\n }\n if (field.maxItems !== undefined && value.length > field.maxItems) {\n return `${field.label} allows at most ${field.maxItems}`;\n }\n // Surface the first offending row so the author knows WHICH one.\n for (let i = 0; i < value.length; i++) {\n const row = value[i];\n if (!row || typeof row !== \"object\" || Array.isArray(row)) {\n return `${field.label} item ${i + 1} must be an object`;\n }\n for (const sub of field.fields) {\n const cell = (row as Record<string, unknown>)[sub.key];\n if (sub.required && (cell === undefined || cell === null || cell === \"\")) {\n return `${field.label} item ${i + 1}: ${sub.label} is required`;\n }\n if (cell === undefined || cell === null) continue;\n const issue = validateField(sub, cell);\n if (issue) return `${field.label} item ${i + 1}: ${issue}`;\n }\n }\n return null;\n }\n case \"keyvalue\": {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return `${field.label} must be a key/value map`;\n }\n const allowed = field.valueOptions?.map((o) => o.value);\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (typeof v !== \"string\") return `${field.label}: \"${k}\" must be a string`;\n if (allowed && !allowed.includes(v)) {\n return `${field.label}: \"${k}\" must be one of ${allowed.join(\", \")}`;\n }\n }\n return null;\n }\n case \"document\":\n return null; // opaque to fancy-flow — the host's editor owns its shape\n default:\n return null;\n }\n}\n\n/** Default accents per category. */\nexport function categoryAccent(category: string): string {\n switch (category) {\n case \"trigger\": return \"#10b981\";\n case \"logic\": return \"#f59e0b\";\n case \"data\": return \"#0ea5e9\";\n case \"ai\": return \"#8b5cf6\";\n case \"io\": return \"#3b82f6\";\n case \"human\": return \"#ec4899\";\n case \"output\": return \"#a855f7\";\n default: return \"#71717a\";\n }\n}\n","/**\n * FlowRunnerUx — the **flow-driven UX** bridge. The headless counterpart to\n * agent-integrations: where that wires an *agent* to host UI surfaces, this\n * wires a *running flow* to host UX. Both share the same primitives from\n * `@particle-academy/fancy-auto-common` (activity bus, effect dispatch).\n *\n * The host registers named UX effects (toast, navigate, confirm, …); this turns\n * each into a flow executor keyed by a node kind (`ux_<effect>` by default), so\n * dropping a `ux_toast` node into a `<FlowEditor>` and running it fires the\n * host's toast. Every dispatch broadcasts an `AutoActivityEvent` (source:\"flow\")\n * for presence / logging. Human-in-the-loop is free: an effect that returns a\n * Promise (e.g. an approval dialog) pauses the run until the user resolves it.\n *\n * const ux = createFlowRunnerUx({\n * effects: {\n * toast: ({ message }) => toast({ title: message }),\n * navigate:({ to }) => router.visit(to),\n * confirm: ({ prompt }) => new Promise(res => openDialog(prompt, res)), // pauses the run\n * },\n * });\n * ux.registerKinds(); // adds ux_toast / ux_navigate / ux_confirm to the palette\n * <FlowEditor initial={graph} executors={ux.executors} />\n */\nimport { useMemo } from \"react\";\nimport {\n createEffectDispatcher,\n type DispatchActor,\n type EffectRegistry,\n} from \"@particle-academy/fancy-auto-common\";\nimport { registerNodeKind } from \"./registry/registry\";\nimport type { ConfigField, NodeCategory } from \"./registry/types\";\nimport type { ExecutorRegistry, FlowNode } from \"./types\";\n\nexport type { AutoActivityEvent } from \"@particle-academy/fancy-auto-common\";\n\n/** Per-effect presentation for the palette node kind that drives it. */\nexport type UxEffectMeta = {\n /** Palette label. Default: the effect name. */\n label?: string;\n /** One-line palette description. */\n description?: string;\n /** Emoji / glyph for the node header. */\n icon?: string;\n /** Header accent color. */\n accent?: string;\n /** Palette grouping. Default \"output\". */\n category?: NodeCategory;\n /** Config form fields = the effect's params. */\n configSchema?: ConfigField[];\n};\n\nexport type FlowRunnerUxOptions = {\n /** Named host UX effects the flow can invoke. */\n effects: EffectRegistry;\n /** Optional per-effect palette metadata (used by `registerKinds`). */\n meta?: Record<string, UxEffectMeta>;\n /** Identifies the flow run in emitted activity. Default `{ id: \"flow\", source: \"flow\" }`. */\n actor?: DispatchActor;\n /** Map an effect name to its node-kind name. Default `ux_<effect>`. */\n kindFor?: (effectName: string) => string;\n};\n\nexport type FlowRunnerUx = {\n /** Executor registry to hand to `<FlowEditor executors>` or `runFlow`. */\n executors: ExecutorRegistry;\n /** Invoke an effect imperatively (also emits activity). */\n dispatch: <R = unknown>(name: string, params?: unknown) => Promise<R>;\n /** All effect names. */\n effectNames: () => string[];\n /** Register a palette node kind per effect (`ux_<effect>`). Idempotent. */\n registerKinds: () => void;\n};\n\nconst defaultKindFor = (name: string) => `ux_${name}`;\n\n/** Build a FlowRunnerUx from a set of host effects. Framework-agnostic. */\nexport function createFlowRunnerUx(options: FlowRunnerUxOptions): FlowRunnerUx {\n const {\n effects,\n meta = {},\n actor = { id: \"flow\", source: \"flow\" },\n kindFor = defaultKindFor,\n } = options;\n\n const dispatcher = createEffectDispatcher(effects, { actor, targetKind: \"ux\" });\n\n const executors: ExecutorRegistry = {};\n for (const name of Object.keys(effects)) {\n executors[kindFor(name)] = async ({ node }: { node: FlowNode }) => {\n const params = (node.data as { config?: Record<string, unknown> } | undefined)?.config ?? {};\n const result = await dispatcher.dispatch(name, params);\n // A UX effect can drive flow control: if it returns the decision sugar\n // (`{ branch }` or `{ __port }`) — e.g. an interactive \"choose\" effect that\n // awaits a human pick and returns the chosen port — pass it straight\n // through so runFlow routes on it. Otherwise wrap the result for the feed.\n if (result && typeof result === \"object\" && (\"branch\" in result || \"__port\" in result)) {\n return result;\n }\n return { effect: name, result };\n };\n }\n\n const registerKinds = () => {\n for (const name of Object.keys(effects)) {\n const m = meta[name] ?? {};\n registerNodeKind({\n name: kindFor(name),\n category: m.category ?? \"output\",\n label: m.label ?? name,\n description: m.description ?? `Flow-driven UX effect: ${name}.`,\n icon: m.icon ?? \"✨\",\n accent: m.accent ?? \"#8b5cf6\",\n inputs: [{ id: \"in\" }],\n outputs: [],\n configSchema: m.configSchema,\n });\n }\n };\n\n return {\n executors,\n dispatch: dispatcher.dispatch as FlowRunnerUx[\"dispatch\"],\n effectNames: dispatcher.names,\n registerKinds,\n };\n}\n\n/**\n * React hook form — memoizes on the effect-name set + actor id so the returned\n * executors keep a stable identity across renders.\n */\nexport function useFlowRunnerUx(options: FlowRunnerUxOptions): FlowRunnerUx {\n const key = `${Object.keys(options.effects).sort().join(\",\")}|${options.actor?.id ?? \"flow\"}`;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return useMemo(() => createFlowRunnerUx(options), [key]);\n}\n"]}
package/dist/ux.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EffectRegistry, DispatchActor } from '@particle-academy/fancy-auto-common';
2
2
  export { AutoActivityEvent } from '@particle-academy/fancy-auto-common';
3
- import { N as NodeCategory, C as ConfigField } from './types-C0wdN6QX.cjs';
3
+ import { N as NodeCategory, C as ConfigField } from './types-DKqaUjF_.cjs';
4
4
  import { E as ExecutorRegistry } from './types-BS3Gwnkq.cjs';
5
5
  import 'react';
6
6
  import '@xyflow/react';
package/dist/ux.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EffectRegistry, DispatchActor } from '@particle-academy/fancy-auto-common';
2
2
  export { AutoActivityEvent } from '@particle-academy/fancy-auto-common';
3
- import { N as NodeCategory, C as ConfigField } from './types-DnMe9Vsf.js';
3
+ import { N as NodeCategory, C as ConfigField } from './types-BocBFh6l.js';
4
4
  import { E as ExecutorRegistry } from './types-BS3Gwnkq.js';
5
5
  import 'react';
6
6
  import '@xyflow/react';
package/dist/ux.js CHANGED
@@ -1,4 +1,4 @@
1
- import { registerNodeKind } from './chunk-WNVBXXOL.js';
1
+ import { registerNodeKind } from './chunk-CPSOC27D.js';
2
2
  import { useMemo } from 'react';
3
3
  import { createEffectDispatcher } from '@particle-academy/fancy-auto-common';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-flow",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Workflow editor + runner. Six built-in node kits (trigger / action / decision / output / note / subgraph), tokenized theme, topological execution with per-node status. React-flow bundled; consumers npm install fancy-flow and get nothing extra.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -81,13 +81,13 @@
81
81
  "react-dom": "^18.0.0 || ^19.0.0"
82
82
  },
83
83
  "dependencies": {
84
- "@particle-academy/fancy-auto-common": "^0.1.0",
85
- "@xyflow/react": "^12.4.0",
86
- "clsx": "^2.1.0"
84
+ "@particle-academy/fancy-auto-common": "^0.1.0"
87
85
  },
88
86
  "devDependencies": {
89
87
  "@types/react": "^19.0.0",
90
88
  "@types/react-dom": "^19.0.0",
89
+ "@xyflow/react": "^12.4.0",
90
+ "clsx": "^2.1.0",
91
91
  "react": "^19.0.0",
92
92
  "react-dom": "^19.0.0",
93
93
  "tsup": "^8.0.0",