@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.
@@ -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
  };