@og-mcp/reactflow-mcp 1.0.4 → 1.0.6
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/LICENSE +21 -21
- package/README.md +74 -74
- package/dist/data/api-types.js +69 -69
- package/dist/data/components.js +217 -217
- package/dist/data/hooks.js +106 -106
- package/dist/data/migration.js +44 -44
- package/dist/data/patterns.js +582 -582
- package/dist/data/templates.js +126 -126
- package/dist/data/utilities.js +29 -29
- package/dist/index.js +0 -0
- package/dist/tools/cheatsheet.js +84 -84
- package/dist/tools/generate-flow.js +89 -89
- package/package.json +54 -54
package/dist/data/patterns.js
CHANGED
|
@@ -2,603 +2,603 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PATTERNS = void 0;
|
|
4
4
|
exports.PATTERNS = {
|
|
5
|
-
"zustand-store": `# Zustand Store Architecture for React Flow
|
|
6
|
-
|
|
7
|
-
\`\`\`ts
|
|
8
|
-
import { create } from 'zustand';
|
|
9
|
-
import {
|
|
10
|
-
type Node, type Edge, type OnNodesChange, type OnEdgesChange, type OnConnect,
|
|
11
|
-
applyNodeChanges, applyEdgeChanges, addEdge,
|
|
12
|
-
} from '@xyflow/react';
|
|
13
|
-
|
|
14
|
-
type FlowState = {
|
|
15
|
-
nodes: Node[];
|
|
16
|
-
edges: Edge[];
|
|
17
|
-
onNodesChange: OnNodesChange;
|
|
18
|
-
onEdgesChange: OnEdgesChange;
|
|
19
|
-
onConnect: OnConnect;
|
|
20
|
-
setNodes: (nodes: Node[]) => void;
|
|
21
|
-
setEdges: (edges: Edge[]) => void;
|
|
22
|
-
addNode: (node: Node) => void;
|
|
23
|
-
removeNode: (id: string) => void;
|
|
24
|
-
updateNodeData: (id: string, data: Partial<Record<string, unknown>>) => void;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const useFlowStore = create<FlowState>((set, get) => ({
|
|
28
|
-
nodes: [],
|
|
29
|
-
edges: [],
|
|
30
|
-
onNodesChange: (changes) => set({ nodes: applyNodeChanges(changes, get().nodes) }),
|
|
31
|
-
onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }),
|
|
32
|
-
onConnect: (connection) => set({ edges: addEdge(connection, get().edges) }),
|
|
33
|
-
setNodes: (nodes) => set({ nodes }),
|
|
34
|
-
setEdges: (edges) => set({ edges }),
|
|
35
|
-
addNode: (node) => set({ nodes: [...get().nodes, node] }),
|
|
36
|
-
removeNode: (id) => set({
|
|
37
|
-
nodes: get().nodes.filter((n) => n.id !== id),
|
|
38
|
-
edges: get().edges.filter((e) => e.source !== id && e.target !== id),
|
|
39
|
-
}),
|
|
40
|
-
updateNodeData: (id, data) => set({
|
|
41
|
-
nodes: get().nodes.map((n) => n.id === id ? { ...n, data: { ...n.data, ...data } } : n),
|
|
42
|
-
}),
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
export default useFlowStore;
|
|
46
|
-
\`\`\`
|
|
47
|
-
|
|
48
|
-
**Usage with stable selectors (prevents re-renders):**
|
|
49
|
-
\`\`\`tsx
|
|
50
|
-
const selector = (s: FlowState) => ({
|
|
51
|
-
nodes: s.nodes, edges: s.edges,
|
|
52
|
-
onNodesChange: s.onNodesChange,
|
|
53
|
-
onEdgesChange: s.onEdgesChange,
|
|
54
|
-
onConnect: s.onConnect,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
function Flow() {
|
|
58
|
-
const store = useFlowStore(selector);
|
|
59
|
-
return <ReactFlow {...store} fitView />;
|
|
60
|
-
}
|
|
5
|
+
"zustand-store": `# Zustand Store Architecture for React Flow
|
|
6
|
+
|
|
7
|
+
\`\`\`ts
|
|
8
|
+
import { create } from 'zustand';
|
|
9
|
+
import {
|
|
10
|
+
type Node, type Edge, type OnNodesChange, type OnEdgesChange, type OnConnect,
|
|
11
|
+
applyNodeChanges, applyEdgeChanges, addEdge,
|
|
12
|
+
} from '@xyflow/react';
|
|
13
|
+
|
|
14
|
+
type FlowState = {
|
|
15
|
+
nodes: Node[];
|
|
16
|
+
edges: Edge[];
|
|
17
|
+
onNodesChange: OnNodesChange;
|
|
18
|
+
onEdgesChange: OnEdgesChange;
|
|
19
|
+
onConnect: OnConnect;
|
|
20
|
+
setNodes: (nodes: Node[]) => void;
|
|
21
|
+
setEdges: (edges: Edge[]) => void;
|
|
22
|
+
addNode: (node: Node) => void;
|
|
23
|
+
removeNode: (id: string) => void;
|
|
24
|
+
updateNodeData: (id: string, data: Partial<Record<string, unknown>>) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const useFlowStore = create<FlowState>((set, get) => ({
|
|
28
|
+
nodes: [],
|
|
29
|
+
edges: [],
|
|
30
|
+
onNodesChange: (changes) => set({ nodes: applyNodeChanges(changes, get().nodes) }),
|
|
31
|
+
onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }),
|
|
32
|
+
onConnect: (connection) => set({ edges: addEdge(connection, get().edges) }),
|
|
33
|
+
setNodes: (nodes) => set({ nodes }),
|
|
34
|
+
setEdges: (edges) => set({ edges }),
|
|
35
|
+
addNode: (node) => set({ nodes: [...get().nodes, node] }),
|
|
36
|
+
removeNode: (id) => set({
|
|
37
|
+
nodes: get().nodes.filter((n) => n.id !== id),
|
|
38
|
+
edges: get().edges.filter((e) => e.source !== id && e.target !== id),
|
|
39
|
+
}),
|
|
40
|
+
updateNodeData: (id, data) => set({
|
|
41
|
+
nodes: get().nodes.map((n) => n.id === id ? { ...n, data: { ...n.data, ...data } } : n),
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
export default useFlowStore;
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
**Usage with stable selectors (prevents re-renders):**
|
|
49
|
+
\`\`\`tsx
|
|
50
|
+
const selector = (s: FlowState) => ({
|
|
51
|
+
nodes: s.nodes, edges: s.edges,
|
|
52
|
+
onNodesChange: s.onNodesChange,
|
|
53
|
+
onEdgesChange: s.onEdgesChange,
|
|
54
|
+
onConnect: s.onConnect,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function Flow() {
|
|
58
|
+
const store = useFlowStore(selector);
|
|
59
|
+
return <ReactFlow {...store} fitView />;
|
|
60
|
+
}
|
|
61
61
|
\`\`\``,
|
|
62
|
-
"undo-redo": `# Undo / Redo with Zundo
|
|
63
|
-
|
|
64
|
-
\`\`\`bash
|
|
65
|
-
npm install zundo
|
|
66
|
-
\`\`\`
|
|
67
|
-
|
|
68
|
-
\`\`\`ts
|
|
69
|
-
import { create } from 'zustand';
|
|
70
|
-
import { temporal } from 'zundo';
|
|
71
|
-
|
|
72
|
-
const useFlowStore = create<FlowState>()(
|
|
73
|
-
temporal(
|
|
74
|
-
(set, get) => ({
|
|
75
|
-
nodes: [],
|
|
76
|
-
edges: [],
|
|
77
|
-
onNodesChange: (changes) => set({ nodes: applyNodeChanges(changes, get().nodes) }),
|
|
78
|
-
onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }),
|
|
79
|
-
onConnect: (connection) => set({ edges: addEdge(connection, get().edges) }),
|
|
80
|
-
}),
|
|
81
|
-
{
|
|
82
|
-
// Only track meaningful changes, not every drag pixel
|
|
83
|
-
equality: (past, current) =>
|
|
84
|
-
JSON.stringify(past.nodes.map(n => ({ id: n.id, position: n.position, data: n.data }))) ===
|
|
85
|
-
JSON.stringify(current.nodes.map(n => ({ id: n.id, position: n.position, data: n.data }))),
|
|
86
|
-
limit: 50,
|
|
87
|
-
}
|
|
88
|
-
)
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
// Hook for undo/redo
|
|
92
|
-
export function useFlowHistory() {
|
|
93
|
-
return useFlowStore.temporal.getState();
|
|
94
|
-
}
|
|
95
|
-
\`\`\`
|
|
96
|
-
|
|
97
|
-
**Usage:**
|
|
98
|
-
\`\`\`tsx
|
|
99
|
-
function UndoRedoControls() {
|
|
100
|
-
const { undo, redo, pastStates, futureStates } = useFlowHistory();
|
|
101
|
-
return (
|
|
102
|
-
<Panel position="top-right">
|
|
103
|
-
<button onClick={() => undo()} disabled={pastStates.length === 0}>Undo</button>
|
|
104
|
-
<button onClick={() => redo()} disabled={futureStates.length === 0}>Redo</button>
|
|
105
|
-
</Panel>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
62
|
+
"undo-redo": `# Undo / Redo with Zundo
|
|
63
|
+
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
npm install zundo
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
\`\`\`ts
|
|
69
|
+
import { create } from 'zustand';
|
|
70
|
+
import { temporal } from 'zundo';
|
|
71
|
+
|
|
72
|
+
const useFlowStore = create<FlowState>()(
|
|
73
|
+
temporal(
|
|
74
|
+
(set, get) => ({
|
|
75
|
+
nodes: [],
|
|
76
|
+
edges: [],
|
|
77
|
+
onNodesChange: (changes) => set({ nodes: applyNodeChanges(changes, get().nodes) }),
|
|
78
|
+
onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }),
|
|
79
|
+
onConnect: (connection) => set({ edges: addEdge(connection, get().edges) }),
|
|
80
|
+
}),
|
|
81
|
+
{
|
|
82
|
+
// Only track meaningful changes, not every drag pixel
|
|
83
|
+
equality: (past, current) =>
|
|
84
|
+
JSON.stringify(past.nodes.map(n => ({ id: n.id, position: n.position, data: n.data }))) ===
|
|
85
|
+
JSON.stringify(current.nodes.map(n => ({ id: n.id, position: n.position, data: n.data }))),
|
|
86
|
+
limit: 50,
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Hook for undo/redo
|
|
92
|
+
export function useFlowHistory() {
|
|
93
|
+
return useFlowStore.temporal.getState();
|
|
94
|
+
}
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
**Usage:**
|
|
98
|
+
\`\`\`tsx
|
|
99
|
+
function UndoRedoControls() {
|
|
100
|
+
const { undo, redo, pastStates, futureStates } = useFlowHistory();
|
|
101
|
+
return (
|
|
102
|
+
<Panel position="top-right">
|
|
103
|
+
<button onClick={() => undo()} disabled={pastStates.length === 0}>Undo</button>
|
|
104
|
+
<button onClick={() => redo()} disabled={futureStates.length === 0}>Redo</button>
|
|
105
|
+
</Panel>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
108
|
\`\`\``,
|
|
109
|
-
"drag-and-drop": `# Drag & Drop from Sidebar
|
|
110
|
-
|
|
111
|
-
\`\`\`tsx
|
|
112
|
-
function Sidebar() {
|
|
113
|
-
const onDragStart = (event: DragEvent, nodeType: string) => {
|
|
114
|
-
event.dataTransfer.setData('application/reactflow', nodeType);
|
|
115
|
-
event.dataTransfer.effectAllowed = 'move';
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<aside>
|
|
120
|
-
<div draggable onDragStart={(e) => onDragStart(e, 'customNode')}>
|
|
121
|
-
Custom Node
|
|
122
|
-
</div>
|
|
123
|
-
</aside>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function Flow() {
|
|
128
|
-
const { screenToFlowPosition, addNodes } = useReactFlow();
|
|
129
|
-
|
|
130
|
-
const onDragOver = useCallback((event: DragEvent) => {
|
|
131
|
-
event.preventDefault();
|
|
132
|
-
event.dataTransfer.dropEffect = 'move';
|
|
133
|
-
}, []);
|
|
134
|
-
|
|
135
|
-
const onDrop = useCallback((event: DragEvent) => {
|
|
136
|
-
event.preventDefault();
|
|
137
|
-
const type = event.dataTransfer.getData('application/reactflow');
|
|
138
|
-
if (!type) return;
|
|
139
|
-
|
|
140
|
-
const position = screenToFlowPosition({
|
|
141
|
-
x: event.clientX,
|
|
142
|
-
y: event.clientY,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
addNodes({
|
|
146
|
-
id: crypto.randomUUID(),
|
|
147
|
-
type,
|
|
148
|
-
position,
|
|
149
|
-
data: { label: \`New \${type}\` },
|
|
150
|
-
});
|
|
151
|
-
}, [screenToFlowPosition, addNodes]);
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<ReactFlow onDragOver={onDragOver} onDrop={onDrop} ... />
|
|
155
|
-
);
|
|
156
|
-
}
|
|
109
|
+
"drag-and-drop": `# Drag & Drop from Sidebar
|
|
110
|
+
|
|
111
|
+
\`\`\`tsx
|
|
112
|
+
function Sidebar() {
|
|
113
|
+
const onDragStart = (event: DragEvent, nodeType: string) => {
|
|
114
|
+
event.dataTransfer.setData('application/reactflow', nodeType);
|
|
115
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<aside>
|
|
120
|
+
<div draggable onDragStart={(e) => onDragStart(e, 'customNode')}>
|
|
121
|
+
Custom Node
|
|
122
|
+
</div>
|
|
123
|
+
</aside>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function Flow() {
|
|
128
|
+
const { screenToFlowPosition, addNodes } = useReactFlow();
|
|
129
|
+
|
|
130
|
+
const onDragOver = useCallback((event: DragEvent) => {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
event.dataTransfer.dropEffect = 'move';
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const onDrop = useCallback((event: DragEvent) => {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
const type = event.dataTransfer.getData('application/reactflow');
|
|
138
|
+
if (!type) return;
|
|
139
|
+
|
|
140
|
+
const position = screenToFlowPosition({
|
|
141
|
+
x: event.clientX,
|
|
142
|
+
y: event.clientY,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
addNodes({
|
|
146
|
+
id: crypto.randomUUID(),
|
|
147
|
+
type,
|
|
148
|
+
position,
|
|
149
|
+
data: { label: \`New \${type}\` },
|
|
150
|
+
});
|
|
151
|
+
}, [screenToFlowPosition, addNodes]);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<ReactFlow onDragOver={onDragOver} onDrop={onDrop} ... />
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
157
|
\`\`\``,
|
|
158
|
-
"auto-layout-dagre": `# Auto Layout with Dagre
|
|
159
|
-
|
|
160
|
-
\`\`\`bash
|
|
161
|
-
npm install @dagrejs/dagre
|
|
162
|
-
\`\`\`
|
|
163
|
-
|
|
164
|
-
\`\`\`tsx
|
|
165
|
-
import Dagre from '@dagrejs/dagre';
|
|
166
|
-
|
|
167
|
-
function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
|
|
168
|
-
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
169
|
-
g.setGraph({ rankdir: direction, nodesep: 50, ranksep: 80 });
|
|
170
|
-
|
|
171
|
-
nodes.forEach((node) => {
|
|
172
|
-
g.setNode(node.id, {
|
|
173
|
-
width: node.measured?.width ?? 172,
|
|
174
|
-
height: node.measured?.height ?? 36,
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
edges.forEach((edge) => {
|
|
179
|
-
g.setEdge(edge.source, edge.target);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
Dagre.layout(g);
|
|
183
|
-
|
|
184
|
-
const layoutedNodes = nodes.map((node) => {
|
|
185
|
-
const pos = g.node(node.id);
|
|
186
|
-
return {
|
|
187
|
-
...node,
|
|
188
|
-
position: {
|
|
189
|
-
x: pos.x - (node.measured?.width ?? 172) / 2,
|
|
190
|
-
y: pos.y - (node.measured?.height ?? 36) / 2,
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
return { nodes: layoutedNodes, edges };
|
|
196
|
-
}
|
|
158
|
+
"auto-layout-dagre": `# Auto Layout with Dagre
|
|
159
|
+
|
|
160
|
+
\`\`\`bash
|
|
161
|
+
npm install @dagrejs/dagre
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
\`\`\`tsx
|
|
165
|
+
import Dagre from '@dagrejs/dagre';
|
|
166
|
+
|
|
167
|
+
function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
|
|
168
|
+
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
169
|
+
g.setGraph({ rankdir: direction, nodesep: 50, ranksep: 80 });
|
|
170
|
+
|
|
171
|
+
nodes.forEach((node) => {
|
|
172
|
+
g.setNode(node.id, {
|
|
173
|
+
width: node.measured?.width ?? 172,
|
|
174
|
+
height: node.measured?.height ?? 36,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
edges.forEach((edge) => {
|
|
179
|
+
g.setEdge(edge.source, edge.target);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
Dagre.layout(g);
|
|
183
|
+
|
|
184
|
+
const layoutedNodes = nodes.map((node) => {
|
|
185
|
+
const pos = g.node(node.id);
|
|
186
|
+
return {
|
|
187
|
+
...node,
|
|
188
|
+
position: {
|
|
189
|
+
x: pos.x - (node.measured?.width ?? 172) / 2,
|
|
190
|
+
y: pos.y - (node.measured?.height ?? 36) / 2,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return { nodes: layoutedNodes, edges };
|
|
196
|
+
}
|
|
197
197
|
\`\`\``,
|
|
198
|
-
"auto-layout-elk": `# Auto Layout with ELK
|
|
199
|
-
|
|
200
|
-
\`\`\`bash
|
|
201
|
-
npm install elkjs
|
|
202
|
-
\`\`\`
|
|
203
|
-
|
|
204
|
-
\`\`\`tsx
|
|
205
|
-
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
206
|
-
|
|
207
|
-
const elk = new ELK();
|
|
208
|
-
|
|
209
|
-
async function getLayoutedElements(nodes: Node[], edges: Edge[]) {
|
|
210
|
-
const graph = {
|
|
211
|
-
id: 'root',
|
|
212
|
-
layoutOptions: {
|
|
213
|
-
'elk.algorithm': 'layered',
|
|
214
|
-
'elk.direction': 'DOWN',
|
|
215
|
-
'elk.spacing.nodeNode': '50',
|
|
216
|
-
'elk.layered.spacing.nodeNodeBetweenLayers': '80',
|
|
217
|
-
},
|
|
218
|
-
children: nodes.map((n) => ({
|
|
219
|
-
id: n.id,
|
|
220
|
-
width: n.measured?.width ?? 172,
|
|
221
|
-
height: n.measured?.height ?? 36,
|
|
222
|
-
})),
|
|
223
|
-
edges: edges.map((e) => ({ id: e.id, sources: [e.source], targets: [e.target] })),
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const layout = await elk.layout(graph);
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
nodes: nodes.map((node) => {
|
|
230
|
-
const elkNode = layout.children?.find((n) => n.id === node.id);
|
|
231
|
-
return { ...node, position: { x: elkNode?.x ?? 0, y: elkNode?.y ?? 0 } };
|
|
232
|
-
}),
|
|
233
|
-
edges,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
198
|
+
"auto-layout-elk": `# Auto Layout with ELK
|
|
199
|
+
|
|
200
|
+
\`\`\`bash
|
|
201
|
+
npm install elkjs
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
\`\`\`tsx
|
|
205
|
+
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
206
|
+
|
|
207
|
+
const elk = new ELK();
|
|
208
|
+
|
|
209
|
+
async function getLayoutedElements(nodes: Node[], edges: Edge[]) {
|
|
210
|
+
const graph = {
|
|
211
|
+
id: 'root',
|
|
212
|
+
layoutOptions: {
|
|
213
|
+
'elk.algorithm': 'layered',
|
|
214
|
+
'elk.direction': 'DOWN',
|
|
215
|
+
'elk.spacing.nodeNode': '50',
|
|
216
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '80',
|
|
217
|
+
},
|
|
218
|
+
children: nodes.map((n) => ({
|
|
219
|
+
id: n.id,
|
|
220
|
+
width: n.measured?.width ?? 172,
|
|
221
|
+
height: n.measured?.height ?? 36,
|
|
222
|
+
})),
|
|
223
|
+
edges: edges.map((e) => ({ id: e.id, sources: [e.source], targets: [e.target] })),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const layout = await elk.layout(graph);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
nodes: nodes.map((node) => {
|
|
230
|
+
const elkNode = layout.children?.find((n) => n.id === node.id);
|
|
231
|
+
return { ...node, position: { x: elkNode?.x ?? 0, y: elkNode?.y ?? 0 } };
|
|
232
|
+
}),
|
|
233
|
+
edges,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
236
|
\`\`\``,
|
|
237
|
-
"context-menu": `# Context Menu
|
|
238
|
-
|
|
239
|
-
\`\`\`tsx
|
|
240
|
-
function Flow() {
|
|
241
|
-
const [menu, setMenu] = useState<{ x: number; y: number; nodeId?: string } | null>(null);
|
|
242
|
-
const { deleteElements, getNode } = useReactFlow();
|
|
243
|
-
|
|
244
|
-
const onPaneContextMenu = useCallback((event: React.MouseEvent) => {
|
|
245
|
-
event.preventDefault();
|
|
246
|
-
setMenu({ x: event.clientX, y: event.clientY });
|
|
247
|
-
}, []);
|
|
248
|
-
|
|
249
|
-
const onNodeContextMenu = useCallback((event: React.MouseEvent, node: Node) => {
|
|
250
|
-
event.preventDefault();
|
|
251
|
-
setMenu({ x: event.clientX, y: event.clientY, nodeId: node.id });
|
|
252
|
-
}, []);
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<>
|
|
256
|
-
<ReactFlow
|
|
257
|
-
onPaneContextMenu={onPaneContextMenu}
|
|
258
|
-
onNodeContextMenu={onNodeContextMenu}
|
|
259
|
-
onPaneClick={() => setMenu(null)}
|
|
260
|
-
/>
|
|
261
|
-
{menu && (
|
|
262
|
-
<div style={{ position: 'fixed', left: menu.x, top: menu.y }} className="bg-white shadow rounded p-2">
|
|
263
|
-
{menu.nodeId && (
|
|
264
|
-
<button onClick={() => { deleteElements({ nodes: [{ id: menu.nodeId! }] }); setMenu(null); }}>
|
|
265
|
-
Delete Node
|
|
266
|
-
</button>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
269
|
-
)}
|
|
270
|
-
</>
|
|
271
|
-
);
|
|
272
|
-
}
|
|
237
|
+
"context-menu": `# Context Menu
|
|
238
|
+
|
|
239
|
+
\`\`\`tsx
|
|
240
|
+
function Flow() {
|
|
241
|
+
const [menu, setMenu] = useState<{ x: number; y: number; nodeId?: string } | null>(null);
|
|
242
|
+
const { deleteElements, getNode } = useReactFlow();
|
|
243
|
+
|
|
244
|
+
const onPaneContextMenu = useCallback((event: React.MouseEvent) => {
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
setMenu({ x: event.clientX, y: event.clientY });
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
const onNodeContextMenu = useCallback((event: React.MouseEvent, node: Node) => {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
setMenu({ x: event.clientX, y: event.clientY, nodeId: node.id });
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<>
|
|
256
|
+
<ReactFlow
|
|
257
|
+
onPaneContextMenu={onPaneContextMenu}
|
|
258
|
+
onNodeContextMenu={onNodeContextMenu}
|
|
259
|
+
onPaneClick={() => setMenu(null)}
|
|
260
|
+
/>
|
|
261
|
+
{menu && (
|
|
262
|
+
<div style={{ position: 'fixed', left: menu.x, top: menu.y }} className="bg-white shadow rounded p-2">
|
|
263
|
+
{menu.nodeId && (
|
|
264
|
+
<button onClick={() => { deleteElements({ nodes: [{ id: menu.nodeId! }] }); setMenu(null); }}>
|
|
265
|
+
Delete Node
|
|
266
|
+
</button>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
273
|
\`\`\``,
|
|
274
|
-
"copy-paste": `# Copy & Paste Nodes
|
|
275
|
-
|
|
276
|
-
\`\`\`tsx
|
|
277
|
-
function useCopyPaste() {
|
|
278
|
-
const { getNodes, getEdges, addNodes, addEdges, screenToFlowPosition } = useReactFlow();
|
|
279
|
-
const clipboard = useRef<{ nodes: Node[]; edges: Edge[] }>({ nodes: [], edges: [] });
|
|
280
|
-
|
|
281
|
-
const copy = useCallback(() => {
|
|
282
|
-
const selected = getNodes().filter((n) => n.selected);
|
|
283
|
-
const selectedIds = new Set(selected.map((n) => n.id));
|
|
284
|
-
const connectedEdges = getEdges().filter(
|
|
285
|
-
(e) => selectedIds.has(e.source) && selectedIds.has(e.target)
|
|
286
|
-
);
|
|
287
|
-
clipboard.current = { nodes: selected, edges: connectedEdges };
|
|
288
|
-
}, [getNodes, getEdges]);
|
|
289
|
-
|
|
290
|
-
const paste = useCallback(() => {
|
|
291
|
-
const { nodes: copiedNodes, edges: copiedEdges } = clipboard.current;
|
|
292
|
-
if (copiedNodes.length === 0) return;
|
|
293
|
-
|
|
294
|
-
const idMap = new Map<string, string>();
|
|
295
|
-
const newNodes = copiedNodes.map((n) => {
|
|
296
|
-
const newId = crypto.randomUUID();
|
|
297
|
-
idMap.set(n.id, newId);
|
|
298
|
-
return { ...n, id: newId, position: { x: n.position.x + 50, y: n.position.y + 50 }, selected: true };
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
const newEdges = copiedEdges.map((e) => ({
|
|
302
|
-
...e,
|
|
303
|
-
id: crypto.randomUUID(),
|
|
304
|
-
source: idMap.get(e.source) ?? e.source,
|
|
305
|
-
target: idMap.get(e.target) ?? e.target,
|
|
306
|
-
}));
|
|
307
|
-
|
|
308
|
-
addNodes(newNodes);
|
|
309
|
-
addEdges(newEdges);
|
|
310
|
-
}, [addNodes, addEdges]);
|
|
311
|
-
|
|
312
|
-
return { copy, paste };
|
|
313
|
-
}
|
|
274
|
+
"copy-paste": `# Copy & Paste Nodes
|
|
275
|
+
|
|
276
|
+
\`\`\`tsx
|
|
277
|
+
function useCopyPaste() {
|
|
278
|
+
const { getNodes, getEdges, addNodes, addEdges, screenToFlowPosition } = useReactFlow();
|
|
279
|
+
const clipboard = useRef<{ nodes: Node[]; edges: Edge[] }>({ nodes: [], edges: [] });
|
|
280
|
+
|
|
281
|
+
const copy = useCallback(() => {
|
|
282
|
+
const selected = getNodes().filter((n) => n.selected);
|
|
283
|
+
const selectedIds = new Set(selected.map((n) => n.id));
|
|
284
|
+
const connectedEdges = getEdges().filter(
|
|
285
|
+
(e) => selectedIds.has(e.source) && selectedIds.has(e.target)
|
|
286
|
+
);
|
|
287
|
+
clipboard.current = { nodes: selected, edges: connectedEdges };
|
|
288
|
+
}, [getNodes, getEdges]);
|
|
289
|
+
|
|
290
|
+
const paste = useCallback(() => {
|
|
291
|
+
const { nodes: copiedNodes, edges: copiedEdges } = clipboard.current;
|
|
292
|
+
if (copiedNodes.length === 0) return;
|
|
293
|
+
|
|
294
|
+
const idMap = new Map<string, string>();
|
|
295
|
+
const newNodes = copiedNodes.map((n) => {
|
|
296
|
+
const newId = crypto.randomUUID();
|
|
297
|
+
idMap.set(n.id, newId);
|
|
298
|
+
return { ...n, id: newId, position: { x: n.position.x + 50, y: n.position.y + 50 }, selected: true };
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const newEdges = copiedEdges.map((e) => ({
|
|
302
|
+
...e,
|
|
303
|
+
id: crypto.randomUUID(),
|
|
304
|
+
source: idMap.get(e.source) ?? e.source,
|
|
305
|
+
target: idMap.get(e.target) ?? e.target,
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
addNodes(newNodes);
|
|
309
|
+
addEdges(newEdges);
|
|
310
|
+
}, [addNodes, addEdges]);
|
|
311
|
+
|
|
312
|
+
return { copy, paste };
|
|
313
|
+
}
|
|
314
314
|
\`\`\``,
|
|
315
|
-
"save-restore": `# Save & Restore Flow
|
|
316
|
-
|
|
317
|
-
\`\`\`tsx
|
|
318
|
-
function SaveRestore() {
|
|
319
|
-
const { toObject, setNodes, setEdges, setViewport } = useReactFlow();
|
|
320
|
-
|
|
321
|
-
const onSave = useCallback(() => {
|
|
322
|
-
const flow = toObject();
|
|
323
|
-
localStorage.setItem('flow', JSON.stringify(flow));
|
|
324
|
-
}, [toObject]);
|
|
325
|
-
|
|
326
|
-
const onRestore = useCallback(() => {
|
|
327
|
-
const json = localStorage.getItem('flow');
|
|
328
|
-
if (!json) return;
|
|
329
|
-
const flow = JSON.parse(json);
|
|
330
|
-
setNodes(flow.nodes || []);
|
|
331
|
-
setEdges(flow.edges || []);
|
|
332
|
-
if (flow.viewport) {
|
|
333
|
-
setViewport(flow.viewport);
|
|
334
|
-
}
|
|
335
|
-
}, [setNodes, setEdges, setViewport]);
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
<Panel position="top-right">
|
|
339
|
-
<button onClick={onSave}>Save</button>
|
|
340
|
-
<button onClick={onRestore}>Restore</button>
|
|
341
|
-
</Panel>
|
|
342
|
-
);
|
|
343
|
-
}
|
|
315
|
+
"save-restore": `# Save & Restore Flow
|
|
316
|
+
|
|
317
|
+
\`\`\`tsx
|
|
318
|
+
function SaveRestore() {
|
|
319
|
+
const { toObject, setNodes, setEdges, setViewport } = useReactFlow();
|
|
320
|
+
|
|
321
|
+
const onSave = useCallback(() => {
|
|
322
|
+
const flow = toObject();
|
|
323
|
+
localStorage.setItem('flow', JSON.stringify(flow));
|
|
324
|
+
}, [toObject]);
|
|
325
|
+
|
|
326
|
+
const onRestore = useCallback(() => {
|
|
327
|
+
const json = localStorage.getItem('flow');
|
|
328
|
+
if (!json) return;
|
|
329
|
+
const flow = JSON.parse(json);
|
|
330
|
+
setNodes(flow.nodes || []);
|
|
331
|
+
setEdges(flow.edges || []);
|
|
332
|
+
if (flow.viewport) {
|
|
333
|
+
setViewport(flow.viewport);
|
|
334
|
+
}
|
|
335
|
+
}, [setNodes, setEdges, setViewport]);
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<Panel position="top-right">
|
|
339
|
+
<button onClick={onSave}>Save</button>
|
|
340
|
+
<button onClick={onRestore}>Restore</button>
|
|
341
|
+
</Panel>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
344
|
\`\`\``,
|
|
345
|
-
"prevent-cycles": `# Prevent Cycles (DAG Validation)
|
|
346
|
-
|
|
347
|
-
\`\`\`tsx
|
|
348
|
-
import { getOutgoers } from '@xyflow/react';
|
|
349
|
-
|
|
350
|
-
function hasCycle(node: Node, target: Node, nodes: Node[], edges: Edge[], visited = new Set<string>()): boolean {
|
|
351
|
-
if (visited.has(node.id)) return false;
|
|
352
|
-
visited.add(node.id);
|
|
353
|
-
if (node.id === target.id) return true;
|
|
354
|
-
|
|
355
|
-
for (const outgoer of getOutgoers(node, nodes, edges)) {
|
|
356
|
-
if (hasCycle(outgoer, target, nodes, edges, visited)) return true;
|
|
357
|
-
}
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Use as isValidConnection:
|
|
362
|
-
<ReactFlow
|
|
363
|
-
isValidConnection={(connection) => {
|
|
364
|
-
const nodes = getNodes();
|
|
365
|
-
const edges = getEdges();
|
|
366
|
-
const target = nodes.find((n) => n.id === connection.target);
|
|
367
|
-
const source = nodes.find((n) => n.id === connection.source);
|
|
368
|
-
if (!target || !source) return false;
|
|
369
|
-
return !hasCycle(target, source, nodes, edges);
|
|
370
|
-
}}
|
|
371
|
-
/>
|
|
345
|
+
"prevent-cycles": `# Prevent Cycles (DAG Validation)
|
|
346
|
+
|
|
347
|
+
\`\`\`tsx
|
|
348
|
+
import { getOutgoers } from '@xyflow/react';
|
|
349
|
+
|
|
350
|
+
function hasCycle(node: Node, target: Node, nodes: Node[], edges: Edge[], visited = new Set<string>()): boolean {
|
|
351
|
+
if (visited.has(node.id)) return false;
|
|
352
|
+
visited.add(node.id);
|
|
353
|
+
if (node.id === target.id) return true;
|
|
354
|
+
|
|
355
|
+
for (const outgoer of getOutgoers(node, nodes, edges)) {
|
|
356
|
+
if (hasCycle(outgoer, target, nodes, edges, visited)) return true;
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Use as isValidConnection:
|
|
362
|
+
<ReactFlow
|
|
363
|
+
isValidConnection={(connection) => {
|
|
364
|
+
const nodes = getNodes();
|
|
365
|
+
const edges = getEdges();
|
|
366
|
+
const target = nodes.find((n) => n.id === connection.target);
|
|
367
|
+
const source = nodes.find((n) => n.id === connection.source);
|
|
368
|
+
if (!target || !source) return false;
|
|
369
|
+
return !hasCycle(target, source, nodes, edges);
|
|
370
|
+
}}
|
|
371
|
+
/>
|
|
372
372
|
\`\`\``,
|
|
373
|
-
"keyboard-shortcuts": `# Keyboard Shortcuts
|
|
374
|
-
|
|
375
|
-
\`\`\`tsx
|
|
376
|
-
function KeyboardShortcuts() {
|
|
377
|
-
const { undo, redo } = useFlowHistory();
|
|
378
|
-
const { copy, paste } = useCopyPaste();
|
|
379
|
-
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
|
380
|
-
|
|
381
|
-
useEffect(() => {
|
|
382
|
-
const handler = (e: KeyboardEvent) => {
|
|
383
|
-
const mod = e.metaKey || e.ctrlKey;
|
|
384
|
-
if (mod && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
|
|
385
|
-
if (mod && e.key === 'z' && e.shiftKey) { e.preventDefault(); redo(); }
|
|
386
|
-
if (mod && e.key === 'c') { copy(); }
|
|
387
|
-
if (mod && e.key === 'v') { paste(); }
|
|
388
|
-
if (mod && e.key === '=') { e.preventDefault(); zoomIn(); }
|
|
389
|
-
if (mod && e.key === '-') { e.preventDefault(); zoomOut(); }
|
|
390
|
-
if (mod && e.key === '0') { e.preventDefault(); fitView({ duration: 300 }); }
|
|
391
|
-
};
|
|
392
|
-
window.addEventListener('keydown', handler);
|
|
393
|
-
return () => window.removeEventListener('keydown', handler);
|
|
394
|
-
}, [undo, redo, copy, paste, fitView, zoomIn, zoomOut]);
|
|
395
|
-
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
373
|
+
"keyboard-shortcuts": `# Keyboard Shortcuts
|
|
374
|
+
|
|
375
|
+
\`\`\`tsx
|
|
376
|
+
function KeyboardShortcuts() {
|
|
377
|
+
const { undo, redo } = useFlowHistory();
|
|
378
|
+
const { copy, paste } = useCopyPaste();
|
|
379
|
+
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
|
380
|
+
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
const handler = (e: KeyboardEvent) => {
|
|
383
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
384
|
+
if (mod && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
|
|
385
|
+
if (mod && e.key === 'z' && e.shiftKey) { e.preventDefault(); redo(); }
|
|
386
|
+
if (mod && e.key === 'c') { copy(); }
|
|
387
|
+
if (mod && e.key === 'v') { paste(); }
|
|
388
|
+
if (mod && e.key === '=') { e.preventDefault(); zoomIn(); }
|
|
389
|
+
if (mod && e.key === '-') { e.preventDefault(); zoomOut(); }
|
|
390
|
+
if (mod && e.key === '0') { e.preventDefault(); fitView({ duration: 300 }); }
|
|
391
|
+
};
|
|
392
|
+
window.addEventListener('keydown', handler);
|
|
393
|
+
return () => window.removeEventListener('keydown', handler);
|
|
394
|
+
}, [undo, redo, copy, paste, fitView, zoomIn, zoomOut]);
|
|
395
|
+
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
398
|
\`\`\``,
|
|
399
|
-
"performance": `# Performance Optimization
|
|
400
|
-
|
|
401
|
-
## Rules
|
|
402
|
-
1. **Define nodeTypes/edgeTypes outside the component** or useMemo — never inline.
|
|
403
|
-
2. **Use stable selectors** with Zustand to prevent unnecessary re-renders.
|
|
404
|
-
3. **Avoid useNodes/useEdges** in components that don't need the full array — use useNodesData(ids) instead.
|
|
405
|
-
4. **Enable onlyRenderVisibleElements** for large graphs (1000+ nodes).
|
|
406
|
-
5. **Use useReactFlow().getNodes()** for on-demand access instead of subscribing.
|
|
407
|
-
|
|
408
|
-
\`\`\`tsx
|
|
409
|
-
// BAD: re-renders on every node change
|
|
410
|
-
const nodes = useNodes();
|
|
411
|
-
|
|
412
|
-
// GOOD: only re-renders when specific node data changes
|
|
413
|
-
const nodeData = useNodesData('node-1');
|
|
414
|
-
|
|
415
|
-
// GOOD: on-demand access, no re-renders
|
|
416
|
-
const { getNodes } = useReactFlow();
|
|
417
|
-
const handleClick = () => {
|
|
418
|
-
const nodes = getNodes();
|
|
419
|
-
};
|
|
420
|
-
\`\`\`
|
|
421
|
-
|
|
422
|
-
## Large graph settings
|
|
423
|
-
\`\`\`tsx
|
|
424
|
-
<ReactFlow
|
|
425
|
-
onlyRenderVisibleElements
|
|
426
|
-
minZoom={0.1}
|
|
427
|
-
maxZoom={4}
|
|
428
|
-
elevateNodesOnSelect={false}
|
|
429
|
-
elevateEdgesOnSelect={false}
|
|
430
|
-
/>
|
|
399
|
+
"performance": `# Performance Optimization
|
|
400
|
+
|
|
401
|
+
## Rules
|
|
402
|
+
1. **Define nodeTypes/edgeTypes outside the component** or useMemo — never inline.
|
|
403
|
+
2. **Use stable selectors** with Zustand to prevent unnecessary re-renders.
|
|
404
|
+
3. **Avoid useNodes/useEdges** in components that don't need the full array — use useNodesData(ids) instead.
|
|
405
|
+
4. **Enable onlyRenderVisibleElements** for large graphs (1000+ nodes).
|
|
406
|
+
5. **Use useReactFlow().getNodes()** for on-demand access instead of subscribing.
|
|
407
|
+
|
|
408
|
+
\`\`\`tsx
|
|
409
|
+
// BAD: re-renders on every node change
|
|
410
|
+
const nodes = useNodes();
|
|
411
|
+
|
|
412
|
+
// GOOD: only re-renders when specific node data changes
|
|
413
|
+
const nodeData = useNodesData('node-1');
|
|
414
|
+
|
|
415
|
+
// GOOD: on-demand access, no re-renders
|
|
416
|
+
const { getNodes } = useReactFlow();
|
|
417
|
+
const handleClick = () => {
|
|
418
|
+
const nodes = getNodes();
|
|
419
|
+
};
|
|
420
|
+
\`\`\`
|
|
421
|
+
|
|
422
|
+
## Large graph settings
|
|
423
|
+
\`\`\`tsx
|
|
424
|
+
<ReactFlow
|
|
425
|
+
onlyRenderVisibleElements
|
|
426
|
+
minZoom={0.1}
|
|
427
|
+
maxZoom={4}
|
|
428
|
+
elevateNodesOnSelect={false}
|
|
429
|
+
elevateEdgesOnSelect={false}
|
|
430
|
+
/>
|
|
431
431
|
\`\`\``,
|
|
432
|
-
"dark-mode": `# Dark Mode with Tailwind
|
|
433
|
-
|
|
434
|
-
React Flow v12 supports \`colorMode\` prop:
|
|
435
|
-
|
|
436
|
-
\`\`\`tsx
|
|
437
|
-
<ReactFlow colorMode="dark" ... />
|
|
438
|
-
// or follow system:
|
|
439
|
-
<ReactFlow colorMode="system" ... />
|
|
440
|
-
\`\`\`
|
|
441
|
-
|
|
442
|
-
For Tailwind + shadcn, map CSS variables:
|
|
443
|
-
\`\`\`css
|
|
444
|
-
.react-flow.dark {
|
|
445
|
-
--xy-background-color: hsl(var(--background));
|
|
446
|
-
--xy-node-background-color: hsl(var(--card));
|
|
447
|
-
--xy-node-border-color: hsl(var(--border));
|
|
448
|
-
--xy-node-color: hsl(var(--card-foreground));
|
|
449
|
-
--xy-edge-stroke: hsl(var(--muted-foreground));
|
|
450
|
-
--xy-minimap-background: hsl(var(--card));
|
|
451
|
-
--xy-controls-button-background: hsl(var(--card));
|
|
452
|
-
--xy-controls-button-color: hsl(var(--card-foreground));
|
|
453
|
-
}
|
|
432
|
+
"dark-mode": `# Dark Mode with Tailwind
|
|
433
|
+
|
|
434
|
+
React Flow v12 supports \`colorMode\` prop:
|
|
435
|
+
|
|
436
|
+
\`\`\`tsx
|
|
437
|
+
<ReactFlow colorMode="dark" ... />
|
|
438
|
+
// or follow system:
|
|
439
|
+
<ReactFlow colorMode="system" ... />
|
|
440
|
+
\`\`\`
|
|
441
|
+
|
|
442
|
+
For Tailwind + shadcn, map CSS variables:
|
|
443
|
+
\`\`\`css
|
|
444
|
+
.react-flow.dark {
|
|
445
|
+
--xy-background-color: hsl(var(--background));
|
|
446
|
+
--xy-node-background-color: hsl(var(--card));
|
|
447
|
+
--xy-node-border-color: hsl(var(--border));
|
|
448
|
+
--xy-node-color: hsl(var(--card-foreground));
|
|
449
|
+
--xy-edge-stroke: hsl(var(--muted-foreground));
|
|
450
|
+
--xy-minimap-background: hsl(var(--card));
|
|
451
|
+
--xy-controls-button-background: hsl(var(--card));
|
|
452
|
+
--xy-controls-button-color: hsl(var(--card-foreground));
|
|
453
|
+
}
|
|
454
454
|
\`\`\``,
|
|
455
|
-
ssr: `# SSR / SSG Setup
|
|
456
|
-
|
|
457
|
-
React Flow requires the DOM for measurement. For Next.js or other SSR frameworks:
|
|
458
|
-
|
|
459
|
-
\`\`\`tsx
|
|
460
|
-
'use client'; // Next.js app dir
|
|
461
|
-
|
|
462
|
-
import dynamic from 'next/dynamic';
|
|
463
|
-
|
|
464
|
-
const Flow = dynamic(() => import('./Flow'), { ssr: false });
|
|
465
|
-
|
|
466
|
-
export default function Page() {
|
|
467
|
-
return (
|
|
468
|
-
<div style={{ width: '100%', height: '100vh' }}>
|
|
469
|
-
<Flow />
|
|
470
|
-
</div>
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
\`\`\`
|
|
474
|
-
|
|
475
|
-
Or with React.lazy:
|
|
476
|
-
\`\`\`tsx
|
|
477
|
-
import { Suspense, lazy } from 'react';
|
|
478
|
-
const Flow = lazy(() => import('./Flow'));
|
|
479
|
-
|
|
480
|
-
export default function Page() {
|
|
481
|
-
return (
|
|
482
|
-
<Suspense fallback={<div>Loading flow...</div>}>
|
|
483
|
-
<Flow />
|
|
484
|
-
</Suspense>
|
|
485
|
-
);
|
|
486
|
-
}
|
|
455
|
+
ssr: `# SSR / SSG Setup
|
|
456
|
+
|
|
457
|
+
React Flow requires the DOM for measurement. For Next.js or other SSR frameworks:
|
|
458
|
+
|
|
459
|
+
\`\`\`tsx
|
|
460
|
+
'use client'; // Next.js app dir
|
|
461
|
+
|
|
462
|
+
import dynamic from 'next/dynamic';
|
|
463
|
+
|
|
464
|
+
const Flow = dynamic(() => import('./Flow'), { ssr: false });
|
|
465
|
+
|
|
466
|
+
export default function Page() {
|
|
467
|
+
return (
|
|
468
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
469
|
+
<Flow />
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
\`\`\`
|
|
474
|
+
|
|
475
|
+
Or with React.lazy:
|
|
476
|
+
\`\`\`tsx
|
|
477
|
+
import { Suspense, lazy } from 'react';
|
|
478
|
+
const Flow = lazy(() => import('./Flow'));
|
|
479
|
+
|
|
480
|
+
export default function Page() {
|
|
481
|
+
return (
|
|
482
|
+
<Suspense fallback={<div>Loading flow...</div>}>
|
|
483
|
+
<Flow />
|
|
484
|
+
</Suspense>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
487
|
\`\`\``,
|
|
488
|
-
subflows: `# SubFlows (Parent/Child Nodes)
|
|
489
|
-
|
|
490
|
-
\`\`\`tsx
|
|
491
|
-
const nodes = [
|
|
492
|
-
{
|
|
493
|
-
id: 'group-1',
|
|
494
|
-
type: 'group',
|
|
495
|
-
position: { x: 0, y: 0 },
|
|
496
|
-
style: { width: 400, height: 300 },
|
|
497
|
-
data: {},
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
id: 'child-1',
|
|
501
|
-
parentId: 'group-1',
|
|
502
|
-
extent: 'parent' as const, // constrain to parent bounds
|
|
503
|
-
expandParent: true, // auto-expand parent if needed
|
|
504
|
-
position: { x: 20, y: 40 }, // relative to parent
|
|
505
|
-
data: { label: 'Child 1' },
|
|
506
|
-
},
|
|
507
|
-
{
|
|
508
|
-
id: 'child-2',
|
|
509
|
-
parentId: 'group-1',
|
|
510
|
-
extent: 'parent' as const,
|
|
511
|
-
position: { x: 200, y: 40 },
|
|
512
|
-
data: { label: 'Child 2' },
|
|
513
|
-
},
|
|
514
|
-
];
|
|
515
|
-
\`\`\`
|
|
516
|
-
|
|
517
|
-
**Rules:**
|
|
518
|
-
- Parent nodes must appear before children in the nodes array.
|
|
519
|
-
- Child positions are relative to the parent.
|
|
520
|
-
- Use \`extent: 'parent'\` to keep children inside the parent bounds.
|
|
521
|
-
- Use \`expandParent: true\` for auto-expanding group.
|
|
488
|
+
subflows: `# SubFlows (Parent/Child Nodes)
|
|
489
|
+
|
|
490
|
+
\`\`\`tsx
|
|
491
|
+
const nodes = [
|
|
492
|
+
{
|
|
493
|
+
id: 'group-1',
|
|
494
|
+
type: 'group',
|
|
495
|
+
position: { x: 0, y: 0 },
|
|
496
|
+
style: { width: 400, height: 300 },
|
|
497
|
+
data: {},
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: 'child-1',
|
|
501
|
+
parentId: 'group-1',
|
|
502
|
+
extent: 'parent' as const, // constrain to parent bounds
|
|
503
|
+
expandParent: true, // auto-expand parent if needed
|
|
504
|
+
position: { x: 20, y: 40 }, // relative to parent
|
|
505
|
+
data: { label: 'Child 1' },
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: 'child-2',
|
|
509
|
+
parentId: 'group-1',
|
|
510
|
+
extent: 'parent' as const,
|
|
511
|
+
position: { x: 200, y: 40 },
|
|
512
|
+
data: { label: 'Child 2' },
|
|
513
|
+
},
|
|
514
|
+
];
|
|
515
|
+
\`\`\`
|
|
516
|
+
|
|
517
|
+
**Rules:**
|
|
518
|
+
- Parent nodes must appear before children in the nodes array.
|
|
519
|
+
- Child positions are relative to the parent.
|
|
520
|
+
- Use \`extent: 'parent'\` to keep children inside the parent bounds.
|
|
521
|
+
- Use \`expandParent: true\` for auto-expanding group.
|
|
522
522
|
- Set \`zIndexMode="auto"\` on ReactFlow for proper z-ordering in sub-flows.`,
|
|
523
|
-
"edge-reconnection": `# Edge Reconnection
|
|
524
|
-
|
|
525
|
-
\`\`\`tsx
|
|
526
|
-
import { reconnectEdge } from '@xyflow/react';
|
|
527
|
-
|
|
528
|
-
function Flow() {
|
|
529
|
-
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
530
|
-
|
|
531
|
-
const onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => {
|
|
532
|
-
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
|
|
533
|
-
}, [setEdges]);
|
|
534
|
-
|
|
535
|
-
return (
|
|
536
|
-
<ReactFlow
|
|
537
|
-
edges={edges}
|
|
538
|
-
onEdgesChange={onEdgesChange}
|
|
539
|
-
edgesReconnectable
|
|
540
|
-
onReconnect={onReconnect}
|
|
541
|
-
onReconnectStart={(_, edge, handleType) => console.log('reconnect start', edge.id, handleType)}
|
|
542
|
-
onReconnectEnd={(_, edge, handleType) => console.log('reconnect end', edge.id, handleType)}
|
|
543
|
-
/>
|
|
544
|
-
);
|
|
545
|
-
}
|
|
523
|
+
"edge-reconnection": `# Edge Reconnection
|
|
524
|
+
|
|
525
|
+
\`\`\`tsx
|
|
526
|
+
import { reconnectEdge } from '@xyflow/react';
|
|
527
|
+
|
|
528
|
+
function Flow() {
|
|
529
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
530
|
+
|
|
531
|
+
const onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => {
|
|
532
|
+
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
|
|
533
|
+
}, [setEdges]);
|
|
534
|
+
|
|
535
|
+
return (
|
|
536
|
+
<ReactFlow
|
|
537
|
+
edges={edges}
|
|
538
|
+
onEdgesChange={onEdgesChange}
|
|
539
|
+
edgesReconnectable
|
|
540
|
+
onReconnect={onReconnect}
|
|
541
|
+
onReconnectStart={(_, edge, handleType) => console.log('reconnect start', edge.id, handleType)}
|
|
542
|
+
onReconnectEnd={(_, edge, handleType) => console.log('reconnect end', edge.id, handleType)}
|
|
543
|
+
/>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
546
|
\`\`\``,
|
|
547
|
-
"custom-connection-line": `# Custom Connection Line
|
|
548
|
-
|
|
549
|
-
\`\`\`tsx
|
|
550
|
-
import type { ConnectionLineComponentProps } from '@xyflow/react';
|
|
551
|
-
|
|
552
|
-
function CustomConnectionLine({
|
|
553
|
-
fromX, fromY, toX, toY, connectionStatus,
|
|
554
|
-
}: ConnectionLineComponentProps) {
|
|
555
|
-
return (
|
|
556
|
-
<g>
|
|
557
|
-
<path
|
|
558
|
-
fill="none"
|
|
559
|
-
stroke={connectionStatus === 'valid' ? '#22c55e' : '#ef4444'}
|
|
560
|
-
strokeWidth={2}
|
|
561
|
-
d={\`M\${fromX},\${fromY} C \${fromX} \${toY} \${fromX} \${toY} \${toX},\${toY}\`}
|
|
562
|
-
/>
|
|
563
|
-
<circle cx={toX} cy={toY} r={4} fill={connectionStatus === 'valid' ? '#22c55e' : '#ef4444'} />
|
|
564
|
-
</g>
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Usage:
|
|
569
|
-
<ReactFlow connectionLineComponent={CustomConnectionLine} />
|
|
570
|
-
\`\`\`
|
|
571
|
-
|
|
547
|
+
"custom-connection-line": `# Custom Connection Line
|
|
548
|
+
|
|
549
|
+
\`\`\`tsx
|
|
550
|
+
import type { ConnectionLineComponentProps } from '@xyflow/react';
|
|
551
|
+
|
|
552
|
+
function CustomConnectionLine({
|
|
553
|
+
fromX, fromY, toX, toY, connectionStatus,
|
|
554
|
+
}: ConnectionLineComponentProps) {
|
|
555
|
+
return (
|
|
556
|
+
<g>
|
|
557
|
+
<path
|
|
558
|
+
fill="none"
|
|
559
|
+
stroke={connectionStatus === 'valid' ? '#22c55e' : '#ef4444'}
|
|
560
|
+
strokeWidth={2}
|
|
561
|
+
d={\`M\${fromX},\${fromY} C \${fromX} \${toY} \${fromX} \${toY} \${toX},\${toY}\`}
|
|
562
|
+
/>
|
|
563
|
+
<circle cx={toX} cy={toY} r={4} fill={connectionStatus === 'valid' ? '#22c55e' : '#ef4444'} />
|
|
564
|
+
</g>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Usage:
|
|
569
|
+
<ReactFlow connectionLineComponent={CustomConnectionLine} />
|
|
570
|
+
\`\`\`
|
|
571
|
+
|
|
572
572
|
The \`connectionStatus\` is 'valid' when hovering over a compatible handle.`,
|
|
573
|
-
"auto-layout-on-mount": `# Auto Layout on Mount
|
|
574
|
-
|
|
575
|
-
Use \`useNodesInitialized\` to wait for all nodes to be measured before running layout:
|
|
576
|
-
|
|
577
|
-
\`\`\`tsx
|
|
578
|
-
function LayoutFlow({ initialNodes, initialEdges }) {
|
|
579
|
-
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
580
|
-
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
581
|
-
const { fitView } = useReactFlow();
|
|
582
|
-
const initialized = useNodesInitialized();
|
|
583
|
-
|
|
584
|
-
useEffect(() => {
|
|
585
|
-
if (!initialized) return;
|
|
586
|
-
|
|
587
|
-
// Run layout (e.g., Dagre)
|
|
588
|
-
const { nodes: layouted } = getLayoutedElements(nodes, edges, 'TB');
|
|
589
|
-
setNodes(layouted);
|
|
590
|
-
|
|
591
|
-
// Fit after layout settles
|
|
592
|
-
requestAnimationFrame(() => fitView({ duration: 300 }));
|
|
593
|
-
}, [initialized]);
|
|
594
|
-
|
|
595
|
-
return (
|
|
596
|
-
<ReactFlow
|
|
597
|
-
nodes={nodes} edges={edges}
|
|
598
|
-
onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
|
|
599
|
-
fitView
|
|
600
|
-
/>
|
|
601
|
-
);
|
|
602
|
-
}
|
|
573
|
+
"auto-layout-on-mount": `# Auto Layout on Mount
|
|
574
|
+
|
|
575
|
+
Use \`useNodesInitialized\` to wait for all nodes to be measured before running layout:
|
|
576
|
+
|
|
577
|
+
\`\`\`tsx
|
|
578
|
+
function LayoutFlow({ initialNodes, initialEdges }) {
|
|
579
|
+
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
580
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
581
|
+
const { fitView } = useReactFlow();
|
|
582
|
+
const initialized = useNodesInitialized();
|
|
583
|
+
|
|
584
|
+
useEffect(() => {
|
|
585
|
+
if (!initialized) return;
|
|
586
|
+
|
|
587
|
+
// Run layout (e.g., Dagre)
|
|
588
|
+
const { nodes: layouted } = getLayoutedElements(nodes, edges, 'TB');
|
|
589
|
+
setNodes(layouted);
|
|
590
|
+
|
|
591
|
+
// Fit after layout settles
|
|
592
|
+
requestAnimationFrame(() => fitView({ duration: 300 }));
|
|
593
|
+
}, [initialized]);
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<ReactFlow
|
|
597
|
+
nodes={nodes} edges={edges}
|
|
598
|
+
onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
|
|
599
|
+
fitView
|
|
600
|
+
/>
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
603
|
\`\`\``,
|
|
604
604
|
};
|