@particle-academy/fancy-flow 0.5.0 → 0.5.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @particle-academy/fancy-flow
2
2
 
3
+ [![Fancified](art/fancified.svg)](https://particle.academy)
4
+
3
5
  Workflow editor + runner with six built-in node kits, tokenized theme, and topological execution with per-node status events. React Flow is bundled — consumers `npm install fancy-flow` and get nothing extra.
4
6
 
5
7
  ## Install
package/dist/ux.cjs CHANGED
@@ -37,6 +37,9 @@ function createFlowRunnerUx(options) {
37
37
  executors[kindFor(name)] = async ({ node }) => {
38
38
  const params = node.data?.config ?? {};
39
39
  const result = await dispatcher.dispatch(name, params);
40
+ if (result && typeof result === "object" && ("branch" in result || "__port" in result)) {
41
+ return result;
42
+ }
40
43
  return { effect: name, result };
41
44
  };
42
45
  }
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 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 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.js CHANGED
@@ -16,6 +16,9 @@ function createFlowRunnerUx(options) {
16
16
  executors[kindFor(name)] = async ({ node }) => {
17
17
  const params = node.data?.config ?? {};
18
18
  const result = await dispatcher.dispatch(name, params);
19
+ if (result && typeof result === "object" && ("branch" in result || "__port" in result)) {
20
+ return result;
21
+ }
19
22
  return { effect: name, result };
20
23
  };
21
24
  }
package/dist/ux.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ux.ts"],"names":[],"mappings":";;;;AAyEA,IAAM,cAAA,GAAiB,CAAC,IAAA,KAAiB,CAAA,GAAA,EAAM,IAAI,CAAA,CAAA;AAG5C,SAAS,mBAAmB,OAAA,EAA4C;AAC7E,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,OAAO,EAAC;AAAA,IACR,KAAA,GAAQ,EAAE,EAAA,EAAI,MAAA,EAAQ,QAAQ,MAAA,EAAO;AAAA,IACrC,OAAA,GAAU;AAAA,GACZ,GAAI,OAAA;AAEJ,EAAA,MAAM,aAAa,sBAAA,CAAuB,OAAA,EAAS,EAAE,KAAA,EAAO,UAAA,EAAY,MAAM,CAAA;AAE9E,EAAA,MAAM,YAA8B,EAAC;AACrC,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,EAAG;AACvC,IAAA,SAAA,CAAU,QAAQ,IAAI,CAAC,IAAI,OAAO,EAAE,MAAK,KAA0B;AACjE,MAAA,MAAM,MAAA,GAAU,IAAA,CAAK,IAAA,EAA2D,MAAA,IAAU,EAAC;AAC3F,MAAA,MAAM,MAAA,GAAS,MAAM,UAAA,CAAW,QAAA,CAAS,MAAM,MAAM,CAAA;AACrD,MAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IAChC,CAAA;AAAA,EACF;AAEA,EAAA,MAAM,gBAAgB,MAAM;AAC1B,IAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,EAAG;AACvC,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,IAAI,CAAA,IAAK,EAAC;AACzB,MAAA,gBAAA,CAAiB;AAAA,QACf,IAAA,EAAM,QAAQ,IAAI,CAAA;AAAA,QAClB,QAAA,EAAU,EAAE,QAAA,IAAY,QAAA;AAAA,QACxB,KAAA,EAAO,EAAE,KAAA,IAAS,IAAA;AAAA,QAClB,WAAA,EAAa,CAAA,CAAE,WAAA,IAAe,CAAA,uBAAA,EAA0B,IAAI,CAAA,CAAA,CAAA;AAAA,QAC5D,IAAA,EAAM,EAAE,IAAA,IAAQ,QAAA;AAAA,QAChB,MAAA,EAAQ,EAAE,MAAA,IAAU,SAAA;AAAA,QACpB,MAAA,EAAQ,CAAC,EAAE,EAAA,EAAI,MAAM,CAAA;AAAA,QACrB,SAAS,EAAC;AAAA,QACV,cAAc,CAAA,CAAE;AAAA,OACjB,CAAA;AAAA,IACH;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,UAAU,UAAA,CAAW,QAAA;AAAA,IACrB,aAAa,UAAA,CAAW,KAAA;AAAA,IACxB;AAAA,GACF;AACF;AAMO,SAAS,gBAAgB,OAAA,EAA4C;AAC1E,EAAA,MAAM,MAAM,CAAA,EAAG,MAAA,CAAO,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,CAAE,IAAA,EAAK,CAAE,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAA,EAAO,MAAM,MAAM,CAAA,CAAA;AAE3F,EAAA,OAAO,QAAQ,MAAM,kBAAA,CAAmB,OAAO,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AACzD","file":"ux.js","sourcesContent":["/**\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 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/ux.ts"],"names":[],"mappings":";;;;AAyEA,IAAM,cAAA,GAAiB,CAAC,IAAA,KAAiB,CAAA,GAAA,EAAM,IAAI,CAAA,CAAA;AAG5C,SAAS,mBAAmB,OAAA,EAA4C;AAC7E,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,OAAO,EAAC;AAAA,IACR,KAAA,GAAQ,EAAE,EAAA,EAAI,MAAA,EAAQ,QAAQ,MAAA,EAAO;AAAA,IACrC,OAAA,GAAU;AAAA,GACZ,GAAI,OAAA;AAEJ,EAAA,MAAM,aAAa,sBAAA,CAAuB,OAAA,EAAS,EAAE,KAAA,EAAO,UAAA,EAAY,MAAM,CAAA;AAE9E,EAAA,MAAM,YAA8B,EAAC;AACrC,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,EAAG;AACvC,IAAA,SAAA,CAAU,QAAQ,IAAI,CAAC,IAAI,OAAO,EAAE,MAAK,KAA0B;AACjE,MAAA,MAAM,MAAA,GAAU,IAAA,CAAK,IAAA,EAA2D,MAAA,IAAU,EAAC;AAC3F,MAAA,MAAM,MAAA,GAAS,MAAM,UAAA,CAAW,QAAA,CAAS,MAAM,MAAM,CAAA;AAKrD,MAAA,IAAI,UAAU,OAAO,MAAA,KAAW,aAAa,QAAA,IAAY,MAAA,IAAU,YAAY,MAAA,CAAA,EAAS;AACtF,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IAChC,CAAA;AAAA,EACF;AAEA,EAAA,MAAM,gBAAgB,MAAM;AAC1B,IAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,EAAG;AACvC,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,IAAI,CAAA,IAAK,EAAC;AACzB,MAAA,gBAAA,CAAiB;AAAA,QACf,IAAA,EAAM,QAAQ,IAAI,CAAA;AAAA,QAClB,QAAA,EAAU,EAAE,QAAA,IAAY,QAAA;AAAA,QACxB,KAAA,EAAO,EAAE,KAAA,IAAS,IAAA;AAAA,QAClB,WAAA,EAAa,CAAA,CAAE,WAAA,IAAe,CAAA,uBAAA,EAA0B,IAAI,CAAA,CAAA,CAAA;AAAA,QAC5D,IAAA,EAAM,EAAE,IAAA,IAAQ,QAAA;AAAA,QAChB,MAAA,EAAQ,EAAE,MAAA,IAAU,SAAA;AAAA,QACpB,MAAA,EAAQ,CAAC,EAAE,EAAA,EAAI,MAAM,CAAA;AAAA,QACrB,SAAS,EAAC;AAAA,QACV,cAAc,CAAA,CAAE;AAAA,OACjB,CAAA;AAAA,IACH;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,UAAU,UAAA,CAAW,QAAA;AAAA,IACrB,aAAa,UAAA,CAAW,KAAA;AAAA,IACxB;AAAA,GACF;AACF;AAMO,SAAS,gBAAgB,OAAA,EAA4C;AAC1E,EAAA,MAAM,MAAM,CAAA,EAAG,MAAA,CAAO,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,CAAE,IAAA,EAAK,CAAE,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAA,EAAO,MAAM,MAAM,CAAA,CAAA;AAE3F,EAAA,OAAO,QAAQ,MAAM,kBAAA,CAAmB,OAAO,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AACzD","file":"ux.js","sourcesContent":["/**\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-flow",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",