@mindlogic-ai/logician-ui 4.0.0-alpha.36 → 4.0.0-alpha.37

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 (64) hide show
  1. package/dist/components/Form/FormSection/CollapsibleFormSection.js +1 -0
  2. package/dist/components/Form/FormSection/CollapsibleFormSection.js.map +1 -1
  3. package/dist/components/Form/FormSection/CollapsibleFormSection.mjs +1 -0
  4. package/dist/components/Form/FormSection/CollapsibleFormSection.mjs.map +1 -1
  5. package/dist/components/Workflow/Workflow.d.ts +1 -1
  6. package/dist/components/Workflow/Workflow.d.ts.map +1 -1
  7. package/dist/components/Workflow/Workflow.js +2 -2
  8. package/dist/components/Workflow/Workflow.js.map +1 -1
  9. package/dist/components/Workflow/Workflow.mjs +2 -2
  10. package/dist/components/Workflow/Workflow.mjs.map +1 -1
  11. package/dist/components/Workflow/Workflow.types.d.ts +9 -0
  12. package/dist/components/Workflow/Workflow.types.d.ts.map +1 -1
  13. package/dist/components/Workflow/Workflow.types.js.map +1 -1
  14. package/dist/components/Workflow/Workflow.types.mjs.map +1 -1
  15. package/dist/components/Workflow/canvas/Canvas/Canvas.d.ts +6 -2
  16. package/dist/components/Workflow/canvas/Canvas/Canvas.d.ts.map +1 -1
  17. package/dist/components/Workflow/canvas/Canvas/Canvas.js +29 -23
  18. package/dist/components/Workflow/canvas/Canvas/Canvas.js.map +1 -1
  19. package/dist/components/Workflow/canvas/Canvas/Canvas.mjs +31 -25
  20. package/dist/components/Workflow/canvas/Canvas/Canvas.mjs.map +1 -1
  21. package/dist/components/Workflow/createNode.d.ts +25 -1
  22. package/dist/components/Workflow/createNode.d.ts.map +1 -1
  23. package/dist/components/Workflow/createNode.js +39 -1
  24. package/dist/components/Workflow/createNode.js.map +1 -1
  25. package/dist/components/Workflow/createNode.mjs +38 -2
  26. package/dist/components/Workflow/createNode.mjs.map +1 -1
  27. package/dist/components/Workflow/index.d.ts +2 -0
  28. package/dist/components/Workflow/index.d.ts.map +1 -1
  29. package/dist/components/Workflow/layout/autoLayout.d.ts +10 -0
  30. package/dist/components/Workflow/layout/autoLayout.d.ts.map +1 -1
  31. package/dist/components/Workflow/layout/autoLayout.js +4 -0
  32. package/dist/components/Workflow/layout/autoLayout.js.map +1 -1
  33. package/dist/components/Workflow/layout/autoLayout.mjs +1 -1
  34. package/dist/components/Workflow/layout/autoLayout.mjs.map +1 -1
  35. package/dist/components/Workflow/placement.d.ts +65 -0
  36. package/dist/components/Workflow/placement.d.ts.map +1 -0
  37. package/dist/components/Workflow/placement.js +100 -0
  38. package/dist/components/Workflow/placement.js.map +1 -0
  39. package/dist/components/Workflow/placement.mjs +96 -0
  40. package/dist/components/Workflow/placement.mjs.map +1 -0
  41. package/dist/components/Workflow/useWorkflowActions.d.ts +54 -0
  42. package/dist/components/Workflow/useWorkflowActions.d.ts.map +1 -0
  43. package/dist/components/Workflow/useWorkflowActions.js +105 -0
  44. package/dist/components/Workflow/useWorkflowActions.js.map +1 -0
  45. package/dist/components/Workflow/useWorkflowActions.mjs +103 -0
  46. package/dist/components/Workflow/useWorkflowActions.mjs.map +1 -0
  47. package/dist/index.js +2 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +1 -0
  50. package/dist/index.mjs.map +1 -1
  51. package/package.json +1 -1
  52. package/src/components/Workflow/README.md +64 -0
  53. package/src/components/Workflow/Workflow.tsx +2 -0
  54. package/src/components/Workflow/Workflow.types.ts +10 -0
  55. package/src/components/Workflow/canvas/Canvas/Canvas.tsx +57 -53
  56. package/src/components/Workflow/createNode.test.ts +40 -2
  57. package/src/components/Workflow/createNode.ts +70 -2
  58. package/src/components/Workflow/index.ts +8 -0
  59. package/src/components/Workflow/layout/autoLayout.ts +4 -4
  60. package/src/components/Workflow/placement.test.ts +156 -0
  61. package/src/components/Workflow/placement.ts +172 -0
  62. package/src/components/Workflow/stories/Workflow.stories.tsx +79 -0
  63. package/src/components/Workflow/useWorkflowActions.test.tsx +345 -0
  64. package/src/components/Workflow/useWorkflowActions.ts +196 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWorkflowActions.d.ts","sourceRoot":"","sources":["../../../src/components/Workflow/useWorkflowActions.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,aAAa,CAAC;AAGrB,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,iFAAiF;AACjF,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;CACzC,CAAC;AAEF,qEAAqE;AACrE,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;;OAMG;IACH,OAAO,EAAE,CACP,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,EACrB,OAAO,CAAC,EAAE,cAAc,KACrB,aAAa,GAAG,IAAI,CAAC;IAC1B;;;;;;OAMG;IACH,iBAAiB,EAAE,CACjB,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAC9B,cAAc,GAAG,IAAI,CAAC;IAC3B,4CAA4C;IAC5C,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,OAAO,CAAC;CAC7D,CAAC;AAKF;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAuHpD"}
@@ -0,0 +1,105 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var React = require('react');
5
+ var react = require('@xyflow/react');
6
+ var createNode = require('./createNode.js');
7
+ var placement = require('./placement.js');
8
+ var WorkflowContext = require('./WorkflowContext/WorkflowContext.js');
9
+
10
+ /** Pan duration when `addNode` brings the new node into view. */
11
+ const PAN_MS = 300;
12
+ /**
13
+ * Imperative editor actions for content rendered inside `<Workflow>` (its
14
+ * `children` and render slots sit inside the canvas's React Flow provider).
15
+ * Hosts use this instead of re-implementing node creation and placement — the
16
+ * tap-to-add palette on touch devices being the first caller.
17
+ *
18
+ * Must be called from a component rendered inside `<Workflow>`.
19
+ */
20
+ function useWorkflowActions() {
21
+ const { graph, dispatch, getNodeType, translate, hostBridge, readOnly, editor: { drawerTarget }, setSelectedNodeId, setSelectedEdgeId, setDrawerTarget, } = WorkflowContext.useWorkflow();
22
+ const { getNode, getViewport, setCenter } = react.useReactFlow();
23
+ const store = react.useStoreApi();
24
+ const getAddAfterSource = React.useCallback((nodeId) => {
25
+ const exit = placement.findFreeExit(graph, getNodeType, nodeId);
26
+ if (!exit)
27
+ return null;
28
+ const def = getNodeType(exit.node.kind);
29
+ const label = def?.getInstanceTitle?.(exit.node.config) || def?.label || exit.node.id;
30
+ return { nodeId: exit.node.id, sourceHandle: exit.sourceHandle, label };
31
+ }, [graph, getNodeType]);
32
+ const canAddAfter = React.useCallback((nodeId) => getAddAfterSource(nodeId) !== null, [getAddAfterSource]);
33
+ const addNode = React.useCallback((kind, target, { inspector = 'follow' } = {}) => {
34
+ if (readOnly)
35
+ return null;
36
+ const def = getNodeType(kind);
37
+ if (!def)
38
+ return null;
39
+ const draft = createNode.createNodeFromType(def, {
40
+ position: { x: 0, y: 0 },
41
+ nodes: graph.nodes,
42
+ translate: createNode.toDefaultsTranslate(translate),
43
+ hostBridge,
44
+ });
45
+ // `nextNodeId` returns a fixed id for singletons (`start`); a second one
46
+ // would be dropped by the reducer's dedupe, so report it instead.
47
+ if (graph.nodes.some((n) => n.id === draft.id))
48
+ return null;
49
+ // Centre of the visible pane, in flow coordinates.
50
+ const { width, height } = store.getState();
51
+ const viewport = getViewport();
52
+ const viewportCenter = {
53
+ x: (width / 2 - viewport.x) / viewport.zoom,
54
+ y: (height / 2 - viewport.y) / viewport.zoom,
55
+ };
56
+ const { position, edge } = placement.planNodePlacement({
57
+ graph,
58
+ getNodeType,
59
+ node: draft,
60
+ target,
61
+ viewportCenter,
62
+ edgeId: createNode.genId('e'),
63
+ getNodeSize: (id) => {
64
+ const { width: w, height: h } = getNode(id)?.measured ?? {};
65
+ return w && h ? { width: w, height: h } : undefined;
66
+ },
67
+ });
68
+ const node = { ...draft, position };
69
+ dispatch({
70
+ type: 'addNodeWithEdge',
71
+ node,
72
+ autoConnectFrom: edge ? { edge } : null,
73
+ });
74
+ // Select the new node so a follow-up `{ after: selected }` chains on.
75
+ setSelectedNodeId(node.id);
76
+ setSelectedEdgeId(null);
77
+ if (inspector === 'open' || (inspector === 'follow' && drawerTarget)) {
78
+ setDrawerTarget({ type: 'node', id: node.id });
79
+ }
80
+ else if (inspector === 'close') {
81
+ setDrawerTarget(null);
82
+ }
83
+ void setCenter(position.x + placement.FALLBACK_NODE_SIZE.width / 2, position.y + placement.FALLBACK_NODE_SIZE.height / 2, { zoom: viewport.zoom, duration: PAN_MS });
84
+ return { nodeId: node.id, edgeId: edge?.id ?? null };
85
+ }, [
86
+ readOnly,
87
+ getNodeType,
88
+ graph,
89
+ translate,
90
+ hostBridge,
91
+ store,
92
+ getViewport,
93
+ getNode,
94
+ dispatch,
95
+ setSelectedNodeId,
96
+ setSelectedEdgeId,
97
+ drawerTarget,
98
+ setDrawerTarget,
99
+ setCenter,
100
+ ]);
101
+ return React.useMemo(() => ({ addNode, getAddAfterSource, canAddAfter }), [addNode, getAddAfterSource, canAddAfter]);
102
+ }
103
+
104
+ exports.useWorkflowActions = useWorkflowActions;
105
+ //# sourceMappingURL=useWorkflowActions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWorkflowActions.js","sources":["../../../src/components/Workflow/useWorkflowActions.ts"],"sourcesContent":[null],"names":["useWorkflow","useReactFlow","useStoreApi","useCallback","findFreeExit","createNodeFromType","toDefaultsTranslate","planNodePlacement","genId","FALLBACK_NODE_SIZE","useMemo"],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { useCallback, useMemo } from 'react';
3
+ import { useReactFlow, useStoreApi } from '@xyflow/react';
4
+ import { createNodeFromType, genId, toDefaultsTranslate } from './createNode.mjs';
5
+ import { findFreeExit, planNodePlacement, FALLBACK_NODE_SIZE } from './placement.mjs';
6
+ import { useWorkflow } from './WorkflowContext/WorkflowContext.mjs';
7
+
8
+ /** Pan duration when `addNode` brings the new node into view. */
9
+ const PAN_MS = 300;
10
+ /**
11
+ * Imperative editor actions for content rendered inside `<Workflow>` (its
12
+ * `children` and render slots sit inside the canvas's React Flow provider).
13
+ * Hosts use this instead of re-implementing node creation and placement — the
14
+ * tap-to-add palette on touch devices being the first caller.
15
+ *
16
+ * Must be called from a component rendered inside `<Workflow>`.
17
+ */
18
+ function useWorkflowActions() {
19
+ const { graph, dispatch, getNodeType, translate, hostBridge, readOnly, editor: { drawerTarget }, setSelectedNodeId, setSelectedEdgeId, setDrawerTarget, } = useWorkflow();
20
+ const { getNode, getViewport, setCenter } = useReactFlow();
21
+ const store = useStoreApi();
22
+ const getAddAfterSource = useCallback((nodeId) => {
23
+ const exit = findFreeExit(graph, getNodeType, nodeId);
24
+ if (!exit)
25
+ return null;
26
+ const def = getNodeType(exit.node.kind);
27
+ const label = def?.getInstanceTitle?.(exit.node.config) || def?.label || exit.node.id;
28
+ return { nodeId: exit.node.id, sourceHandle: exit.sourceHandle, label };
29
+ }, [graph, getNodeType]);
30
+ const canAddAfter = useCallback((nodeId) => getAddAfterSource(nodeId) !== null, [getAddAfterSource]);
31
+ const addNode = useCallback((kind, target, { inspector = 'follow' } = {}) => {
32
+ if (readOnly)
33
+ return null;
34
+ const def = getNodeType(kind);
35
+ if (!def)
36
+ return null;
37
+ const draft = createNodeFromType(def, {
38
+ position: { x: 0, y: 0 },
39
+ nodes: graph.nodes,
40
+ translate: toDefaultsTranslate(translate),
41
+ hostBridge,
42
+ });
43
+ // `nextNodeId` returns a fixed id for singletons (`start`); a second one
44
+ // would be dropped by the reducer's dedupe, so report it instead.
45
+ if (graph.nodes.some((n) => n.id === draft.id))
46
+ return null;
47
+ // Centre of the visible pane, in flow coordinates.
48
+ const { width, height } = store.getState();
49
+ const viewport = getViewport();
50
+ const viewportCenter = {
51
+ x: (width / 2 - viewport.x) / viewport.zoom,
52
+ y: (height / 2 - viewport.y) / viewport.zoom,
53
+ };
54
+ const { position, edge } = planNodePlacement({
55
+ graph,
56
+ getNodeType,
57
+ node: draft,
58
+ target,
59
+ viewportCenter,
60
+ edgeId: genId('e'),
61
+ getNodeSize: (id) => {
62
+ const { width: w, height: h } = getNode(id)?.measured ?? {};
63
+ return w && h ? { width: w, height: h } : undefined;
64
+ },
65
+ });
66
+ const node = { ...draft, position };
67
+ dispatch({
68
+ type: 'addNodeWithEdge',
69
+ node,
70
+ autoConnectFrom: edge ? { edge } : null,
71
+ });
72
+ // Select the new node so a follow-up `{ after: selected }` chains on.
73
+ setSelectedNodeId(node.id);
74
+ setSelectedEdgeId(null);
75
+ if (inspector === 'open' || (inspector === 'follow' && drawerTarget)) {
76
+ setDrawerTarget({ type: 'node', id: node.id });
77
+ }
78
+ else if (inspector === 'close') {
79
+ setDrawerTarget(null);
80
+ }
81
+ void setCenter(position.x + FALLBACK_NODE_SIZE.width / 2, position.y + FALLBACK_NODE_SIZE.height / 2, { zoom: viewport.zoom, duration: PAN_MS });
82
+ return { nodeId: node.id, edgeId: edge?.id ?? null };
83
+ }, [
84
+ readOnly,
85
+ getNodeType,
86
+ graph,
87
+ translate,
88
+ hostBridge,
89
+ store,
90
+ getViewport,
91
+ getNode,
92
+ dispatch,
93
+ setSelectedNodeId,
94
+ setSelectedEdgeId,
95
+ drawerTarget,
96
+ setDrawerTarget,
97
+ setCenter,
98
+ ]);
99
+ return useMemo(() => ({ addNode, getAddAfterSource, canAddAfter }), [addNode, getAddAfterSource, canAddAfter]);
100
+ }
101
+
102
+ export { useWorkflowActions };
103
+ //# sourceMappingURL=useWorkflowActions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWorkflowActions.mjs","sources":["../../../src/components/Workflow/useWorkflowActions.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -173,6 +173,7 @@ var NodeShell_styles = require('./components/Workflow/canvas/NodeShell/NodeShell
173
173
  var workflowLabelProps = require('./components/Workflow/canvas/workflowLabelProps.js');
174
174
  var graphObserver = require('./components/Workflow/graphObserver.js');
175
175
  var graphReducer = require('./components/Workflow/graphReducer.js');
176
+ var useWorkflowActions = require('./components/Workflow/useWorkflowActions.js');
176
177
  var Workflow = require('./components/Workflow/Workflow.js');
177
178
  var Workflow_types = require('./components/Workflow/Workflow.types.js');
178
179
  var WorkflowContext = require('./components/Workflow/WorkflowContext/WorkflowContext.js');
@@ -424,6 +425,7 @@ exports.WorkflowGraphObserverContext = graphObserver.WorkflowGraphObserverContex
424
425
  exports.useWorkflowGraphObserver = graphObserver.useWorkflowGraphObserver;
425
426
  exports.emptyGraph = graphReducer.emptyGraph;
426
427
  exports.graphReducer = graphReducer.graphReducer;
428
+ exports.useWorkflowActions = useWorkflowActions.useWorkflowActions;
427
429
  exports.Workflow = Workflow.Workflow;
428
430
  exports.defineNodeType = Workflow_types.defineNodeType;
429
431
  exports.resolveDefaultConfig = Workflow_types.resolveDefaultConfig;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.mjs CHANGED
@@ -171,6 +171,7 @@ export { CARD_WIDTH, MAX_META_CHIPS, PORT_STYLE, metaChipStyles } from './compon
171
171
  export { workflowLabelProps } from './components/Workflow/canvas/workflowLabelProps.mjs';
172
172
  export { WorkflowGraphObserverContext, useWorkflowGraphObserver } from './components/Workflow/graphObserver.mjs';
173
173
  export { emptyGraph, graphReducer } from './components/Workflow/graphReducer.mjs';
174
+ export { useWorkflowActions } from './components/Workflow/useWorkflowActions.mjs';
174
175
  export { Workflow } from './components/Workflow/Workflow.mjs';
175
176
  export { defineNodeType, resolveDefaultConfig } from './components/Workflow/Workflow.types.mjs';
176
177
  export { WorkflowProvider, useWorkflow, useWorkflowTranslate } from './components/Workflow/WorkflowContext/WorkflowContext.mjs';
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindlogic-ai/logician-ui",
3
- "version": "4.0.0-alpha.36",
3
+ "version": "4.0.0-alpha.37",
4
4
  "description": "A comprehensive React design system built on Chakra UI",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -182,6 +182,69 @@ to keep the built-in drawer but also highlight the selection elsewhere.
182
182
 
183
183
  ---
184
184
 
185
+ ## Adding nodes from your own UI (`useWorkflowActions`)
186
+
187
+ The palette is drag-and-drop. For anything else — a tap-to-add sheet on touch
188
+ devices, a "+" beside a node, a command menu — call `useWorkflowActions()` from a
189
+ component rendered inside `<Workflow>` (its `children` or
190
+ `renderSelectedNodeToolbar`). It uses the same node builder as a palette drop
191
+ (`{kind}_{N}` id; `defaultConfig` → `localizeDefaults` → `hostDefaults`), so you
192
+ never rebuild ids, configs or placement, and you never import `@xyflow/react`.
193
+
194
+ ```tsx
195
+ import {
196
+ useWorkflow,
197
+ useWorkflowActions,
198
+ Workflow,
199
+ } from '@mindlogic-ai/logician-ui';
200
+
201
+ function AddAgentButton() {
202
+ const { editor } = useWorkflow();
203
+ const { addNode, getAddAfterSource } = useWorkflowActions();
204
+ const after = getAddAfterSource(editor.selectedNodeId);
205
+ return (
206
+ <button
207
+ onClick={() =>
208
+ addNode(
209
+ 'agent',
210
+ after ? { after: after.nodeId } : { at: 'viewportCenter' }
211
+ )
212
+ }
213
+ >
214
+ {after ? `Add after ${after.label}` : 'Add at centre'}
215
+ </button>
216
+ );
217
+ }
218
+
219
+ <Workflow
220
+ nodeTypes={NODE_TYPES}
221
+ renderSelectedNodeToolbar={(node) => <PlusAfter nodeId={node.id} />}
222
+ >
223
+ <AddAgentButton />
224
+ </Workflow>;
225
+ ```
226
+
227
+ - `addNode(kind, target, { inspector? })` → `{ nodeId, edgeId | null } | null`.
228
+ - `{ after: nodeId }` places the node to that node's right and wires it from the
229
+ first free exit. No edge when the new kind has no entry (Note) or a
230
+ `canConnect` hook / connection rule rejects it. A node with no free exit (End,
231
+ Note, every exit taken) falls back to the viewport centre.
232
+ - `{ at: 'viewportCenter' }` places it unconnected in the middle of the view.
233
+ - Either way the node moves down past anything already there, becomes the
234
+ selection, and the view pans to it. Node + edge are **one undo step**.
235
+ - `inspector`: `'follow'` (default — an open inspector moves to the new node),
236
+ `'open'`, or `'close'`.
237
+ - Returns `null` without changing anything when read-only, the kind is not
238
+ registered, or its id is taken (a second `start`).
239
+ - `getAddAfterSource(nodeId)` → `{ nodeId, sourceHandle, label } | null` — the
240
+ exit an add-after would use; `null` means the add would land at the centre.
241
+ `canAddAfter(nodeId)` is the boolean form.
242
+ - `renderSelectedNodeToolbar(node)` (a `<Workflow>` prop) renders your content
243
+ beside the selected node's exit side, in screen space (constant size at any
244
+ zoom). Return `null` to show nothing.
245
+
246
+ ---
247
+
185
248
  ## Controlled vs uncontrolled graph
186
249
 
187
250
  - **Controlled:** pass `graph` and `onGraphChange`. You own the source of truth.
@@ -335,6 +398,7 @@ surfaces.
335
398
  | `onArrange` | `(g: Graph) => void` | After one-click auto-arrange. |
336
399
  | `onHistoryNavigate` | `(g: Graph) => void` | After undo/redo. |
337
400
  | `children` | `ReactNode` | Canvas overlay slot (mount `<NodeInspector>` here). |
401
+ | `renderSelectedNodeToolbar` | `(node) => ReactNode` | Content beside the selected node (e.g. a "+"). |
338
402
  | `footer` | `ReactNode` | Slot below the canvas. |
339
403
  | `categoryTokens` | `CategoryTokenMap` | Override category → color tokens. |
340
404
  | `hostBridge` | `unknown` | Opaque host data for inspectors. |
@@ -36,6 +36,7 @@ export function Workflow({
36
36
  onEdgeClick,
37
37
  onSelectionChange,
38
38
  onIssuesChange,
39
+ renderSelectedNodeToolbar,
39
40
  footer,
40
41
  children,
41
42
  categoryTokens,
@@ -169,6 +170,7 @@ export function Workflow({
169
170
  showPalette={showPalette && !readOnly}
170
171
  onNodeClick={onNodeClick}
171
172
  onEdgeClick={onEdgeClick}
173
+ renderSelectedNodeToolbar={renderSelectedNodeToolbar}
172
174
  >
173
175
  {children}
174
176
  </Canvas>
@@ -443,6 +443,16 @@ export type WorkflowProps = {
443
443
  */
444
444
  children?: ReactNode;
445
445
 
446
+ /**
447
+ * Content rendered beside the selected node, on its exit (right) side — e.g.
448
+ * a "+" that adds a node after it via `useWorkflowActions().addNode`. Called
449
+ * with the selected node whenever one is selected; return `null` to show
450
+ * nothing. The library positions it in screen space (it keeps its size at
451
+ * any zoom) and renders it inside the editor, so the content may call
452
+ * `useWorkflow` / `useWorkflowActions`. Hosts never touch React Flow.
453
+ */
454
+ renderSelectedNodeToolbar?: (node: GraphNode) => ReactNode;
455
+
446
456
  /**
447
457
  * Notified whenever the issues array identity changes. Mostly a Storybook
448
458
  * convenience now that validation is owned entirely by the backend — prod
@@ -21,7 +21,9 @@ import {
21
21
  IsValidConnection,
22
22
  MiniMap,
23
23
  NodeChange,
24
+ NodeToolbar,
24
25
  NodeTypes,
26
+ Position as HandleSide,
25
27
  ReactFlow,
26
28
  ReactFlowProvider,
27
29
  useReactFlow,
@@ -34,7 +36,11 @@ import {
34
36
  isValidConnection as isValidWorkflowConnection,
35
37
  soleEdgeToHandle,
36
38
  } from '../../connectionRules';
37
- import { genId } from '../../createNode';
39
+ import {
40
+ createNodeFromType,
41
+ genId,
42
+ toDefaultsTranslate,
43
+ } from '../../createNode';
38
44
  import {
39
45
  autoLayout,
40
46
  estimateLabelOverhang,
@@ -47,7 +53,6 @@ import type {
47
53
  GraphNode,
48
54
  Position,
49
55
  } from '../../Workflow.types';
50
- import { resolveDefaultConfig } from '../../Workflow.types';
51
56
  import { useWorkflow, useWorkflowTranslate } from '../../WorkflowContext';
52
57
  import type { WorkflowReactFlowNode } from '../GenericNode';
53
58
  import { GenericNode } from '../GenericNode';
@@ -107,17 +112,22 @@ function graphToReactFlow(
107
112
  };
108
113
  }
109
114
 
115
+ type CanvasProps = {
116
+ showPalette?: boolean;
117
+ onNodeClick?: (node: GraphNode) => void;
118
+ onEdgeClick?: (edge: GraphEdge) => void;
119
+ /** See `WorkflowProps.renderSelectedNodeToolbar`. */
120
+ renderSelectedNodeToolbar?: (node: GraphNode) => ReactNode;
121
+ children?: ReactNode;
122
+ };
123
+
110
124
  function CanvasInner({
111
- showPalette,
125
+ showPalette = true,
112
126
  onNodeClick: onNodeClickHost,
113
127
  onEdgeClick: onEdgeClickHost,
128
+ renderSelectedNodeToolbar,
114
129
  children,
115
- }: {
116
- showPalette: boolean;
117
- onNodeClick?: (node: GraphNode) => void;
118
- onEdgeClick?: (edge: GraphEdge) => void;
119
- children?: ReactNode;
120
- }) {
130
+ }: CanvasProps) {
121
131
  const {
122
132
  graph,
123
133
  dispatch,
@@ -144,13 +154,6 @@ function CanvasInner({
144
154
  const [isPaletteOpen, setIsPaletteOpen] = useState(true);
145
155
 
146
156
  const translate = useWorkflowTranslate();
147
- // Tear off only the string lookup so `localizeDefaults` doesn't depend
148
- // on the React-fragment interpolation path.
149
- const t = useCallback(
150
- (key: string, params?: Record<string, string>) =>
151
- translate(key, params) as string,
152
- [translate]
153
- );
154
157
 
155
158
  useWorkflowKeyboard();
156
159
 
@@ -606,32 +609,37 @@ function CanvasInner({
606
609
  if (!kind) return;
607
610
  const def = getNodeType(kind);
608
611
  if (!def) return;
609
- const position = screenToFlowPosition({ x: e.clientX, y: e.clientY });
610
- const base = resolveDefaultConfig(def) as Record<string, unknown>;
611
- const overlay = def.localizeDefaults?.(t) as
612
- | Record<string, unknown>
613
- | undefined;
614
- // Host-data overlay (e.g. a starting LLM picked from the tenant's live
615
- // model list) — applied last so it wins over any static placeholder in
616
- // `defaultConfig`. Keyed on the opaque `hostBridge`; the node type narrows
617
- // it. Absent on a generic host, leaving the static defaults in place.
618
- const bridged = def.hostDefaults?.(hostBridge) as
619
- | Record<string, unknown>
620
- | undefined;
621
- const node = {
622
- id: genId(kind),
623
- kind,
624
- position,
625
- config: { ...base, ...overlay, ...bridged },
626
- };
612
+ // Same builder `useWorkflowActions().addNode` uses, so a drop and a
613
+ // programmatic add get the same `{kind}_{N}` id and starting config.
614
+ const node = createNodeFromType(def, {
615
+ position: screenToFlowPosition({ x: e.clientX, y: e.clientY }),
616
+ nodes: graph.nodes,
617
+ translate: toDefaultsTranslate(translate),
618
+ hostBridge,
619
+ });
627
620
 
628
621
  // Dropped nodes land unconnected — authors wire edges explicitly by
629
622
  // dragging from an exit point.
630
623
  dispatch({ type: 'addNode', node });
631
624
  },
632
- [dispatch, getNodeType, screenToFlowPosition, t, hostBridge]
625
+ [
626
+ dispatch,
627
+ getNodeType,
628
+ screenToFlowPosition,
629
+ graph.nodes,
630
+ translate,
631
+ hostBridge,
632
+ ]
633
633
  );
634
634
 
635
+ const selectedNode = selectedNodeId
636
+ ? graph.nodes.find((n) => n.id === selectedNodeId)
637
+ : undefined;
638
+ const selectedNodeToolbar =
639
+ selectedNode && renderSelectedNodeToolbar
640
+ ? renderSelectedNodeToolbar(selectedNode)
641
+ : null;
642
+
635
643
  // Does the canvas top-left corner actually hold overlay chrome the zoom
636
644
  // controls should tuck under? The palette toggle docks there when the palette
637
645
  // is collapsed, and the error banner when the (editable) graph has issues. If
@@ -716,6 +724,18 @@ function CanvasInner({
716
724
  proOptions={{ hideAttribution: true }}
717
725
  >
718
726
  <Background />
727
+ {selectedNode && selectedNodeToolbar ? (
728
+ // Positioned in screen space beside the node's exit side, so the
729
+ // host's content keeps its size at any zoom.
730
+ <NodeToolbar
731
+ nodeId={selectedNode.id}
732
+ isVisible
733
+ position={HandleSide.Right}
734
+ offset={4}
735
+ >
736
+ {selectedNodeToolbar}
737
+ </NodeToolbar>
738
+ ) : null}
719
739
  <CanvasControls
720
740
  readOnly={readOnly}
721
741
  onAutoArrange={onAutoArrange}
@@ -748,26 +768,10 @@ function CanvasInner({
748
768
  );
749
769
  }
750
770
 
751
- export function Canvas({
752
- showPalette = true,
753
- onNodeClick,
754
- onEdgeClick,
755
- children,
756
- }: {
757
- showPalette?: boolean;
758
- onNodeClick?: (node: GraphNode) => void;
759
- onEdgeClick?: (edge: GraphEdge) => void;
760
- children?: ReactNode;
761
- }) {
771
+ export function Canvas(props: CanvasProps) {
762
772
  return (
763
773
  <ReactFlowProvider>
764
- <CanvasInner
765
- showPalette={showPalette}
766
- onNodeClick={onNodeClick}
767
- onEdgeClick={onEdgeClick}
768
- >
769
- {children}
770
- </CanvasInner>
774
+ <CanvasInner {...props} />
771
775
  </ReactFlowProvider>
772
776
  );
773
777
  }
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { nextNodeId } from './createNode';
4
- import type { GraphNode } from './Workflow.types';
3
+ import { createNodeFromType, nextNodeId } from './createNode';
4
+ import type { GraphNode, NodeTypeDef } from './Workflow.types';
5
5
 
6
6
  const node = (id: string, kind: string): GraphNode => ({
7
7
  id,
@@ -59,3 +59,41 @@ describe('nextNodeId', () => {
59
59
  expect(nextNodeId('agent', nodes)).toBe('agent_4');
60
60
  });
61
61
  });
62
+
63
+ describe('nextNodeId with namespaced kinds', () => {
64
+ it('treats the dot in a kind literally', () => {
65
+ // Unescaped, `math.add` would match `mathXadd_7` and jump to _8.
66
+ const nodes = [
67
+ node('mathXadd_7', 'math.add'),
68
+ node('math.add_2', 'math.add'),
69
+ ];
70
+ expect(nextNodeId('math.add', nodes)).toBe('math.add_3');
71
+ });
72
+ });
73
+
74
+ describe('createNodeFromType', () => {
75
+ const def = {
76
+ kind: 'agent',
77
+ defaultConfig: () => ({ name: 'Agent', model: 'static', temp: 1 }),
78
+ localizeDefaults: (t: (k: string) => string) => ({ name: t('agent_name') }),
79
+ hostDefaults: (bridge: unknown) => ({
80
+ model: (bridge as { model: string }).model,
81
+ }),
82
+ handles: () => ({ inputs: [], outputs: [] }),
83
+ } as unknown as NodeTypeDef;
84
+
85
+ it('layers defaultConfig, then localizeDefaults, then hostDefaults', () => {
86
+ const created = createNodeFromType(def, {
87
+ position: { x: 1, y: 2 },
88
+ nodes: [node('agent_4', 'agent')],
89
+ translate: (key) => `t:${key}`,
90
+ hostBridge: { model: 'live' },
91
+ });
92
+ expect(created).toEqual({
93
+ id: 'agent_5',
94
+ kind: 'agent',
95
+ position: { x: 1, y: 2 },
96
+ config: { name: 't:agent_name', model: 'live', temp: 1 },
97
+ });
98
+ });
99
+ });