diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts
index 7cd3eb13..1d68c858 100644
--- a/examples/react/src/App/routes.ts
+++ b/examples/react/src/App/routes.ts
@@ -50,6 +50,7 @@ import UseNodesInitialized from '../examples/UseNodesInit';
import UseNodesData from '../examples/UseNodesData';
import UseHandleConnections from '../examples/UseHandleConnections';
import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop';
+import DevTools from '../examples/DevTools';
export interface IRoute {
name: string;
@@ -113,6 +114,11 @@ const routes: IRoute[] = [
path: 'default-nodes',
component: DefaultNodes,
},
+ {
+ name: 'DevTools',
+ path: 'devtools',
+ component: DevTools,
+ },
{
name: 'Drag Handle',
path: 'draghandle',
diff --git a/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx b/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx
new file mode 100644
index 00000000..d4fffcda
--- /dev/null
+++ b/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx
@@ -0,0 +1,74 @@
+import { useEffect, useRef, useState } from 'react';
+import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react';
+
+type ChangeLoggerProps = {
+ color?: string;
+ limit?: number;
+};
+
+type ChangeInfoProps = {
+ change: NodeChange;
+};
+
+function ChangeInfo({ change }: ChangeInfoProps) {
+ const id = 'id' in change ? change.id : '-';
+ const { type } = change;
+
+ return (
+
+
node id: {id}
+
+ {type === 'add' ? JSON.stringify(change.item, null, 2) : null}
+ {type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null}
+ {type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null}
+ {type === 'remove' ? 'remove' : null}
+ {type === 'replace' ? JSON.stringify(change.item, null, 2) : null}
+ {type === 'select' ? (change.selected ? 'select' : 'unselect') : null}
+
+
+ );
+}
+
+export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) {
+ const [changes, setChanges] = useState([]);
+ const onNodesChangeIntercepted = useRef(false);
+ const onNodesChange = useStore((s) => s.onNodesChange);
+ const store = useStoreApi();
+
+ useEffect(() => {
+ if (!onNodesChange || onNodesChangeIntercepted.current) {
+ return;
+ }
+
+ onNodesChangeIntercepted.current = true;
+ const userOnNodesChange = onNodesChange;
+
+ const onNodesChangeLogger: OnNodesChange = (changes) => {
+ userOnNodesChange(changes);
+
+ setChanges((c) => {
+ changes.forEach((change) => {
+ if (c.length >= limit) {
+ c.pop();
+ }
+
+ c = [change, ...c];
+ });
+ return c;
+ });
+ };
+
+ store.setState({ onNodesChange: onNodesChangeLogger });
+ }, [onNodesChange]);
+
+ return (
+
+
Change Logger
+ {changes.length === 0 ? (
+ <>no changes triggered>
+ ) : (
+ changes.map((change, index) =>
)
+ )}
+
+ );
+}
diff --git a/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx
new file mode 100644
index 00000000..5d467514
--- /dev/null
+++ b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx
@@ -0,0 +1,68 @@
+import { useNodes, ViewportPortal } from '@xyflow/react';
+
+type NodeInfoProps = {
+ id: string;
+ type: string;
+ x: number;
+ y: number;
+ width?: number;
+ height?: number;
+ data: any;
+};
+
+function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) {
+ if (!width || !height) {
+ return null;
+ }
+
+ return (
+
+
id: {id}
+
type: {type}
+
+ position: {x.toFixed(1)}, {y.toFixed(1)}
+
+
+ dimensions: {width} × {height}
+
+
data: {JSON.stringify(data, null, 2)}
+
+ );
+}
+
+export default function NodeInspector() {
+ const nodes = useNodes();
+
+ return (
+
+
+ {nodes.map((node) => {
+ const x = node.computed?.positionAbsolute?.x || 0;
+ const y = node.computed?.positionAbsolute?.y || 0;
+ const width = node.computed?.width || 0;
+ const height = node.computed?.height || 0;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/examples/react/src/examples/DevTools/DevTools/index.tsx b/examples/react/src/examples/DevTools/DevTools/index.tsx
new file mode 100644
index 00000000..107d0078
--- /dev/null
+++ b/examples/react/src/examples/DevTools/DevTools/index.tsx
@@ -0,0 +1,42 @@
+import { useState, type Dispatch, type SetStateAction, type ReactNode, HTMLAttributes } from 'react';
+import { Panel } from '@xyflow/react';
+
+import NodeInspector from './NodeInspector';
+import ChangeLogger from './ChangeLogger';
+
+export default function ReactFlowDevTools() {
+ const [nodeInspectorActive, setNodeInspectorActive] = useState(false);
+ const [changeLoggerActive, setChangeLoggerActive] = useState(false);
+
+ return (
+
+
+
+ Node Inspector
+
+
+ Change Logger
+
+
+ {changeLoggerActive &&
}
+ {nodeInspectorActive &&
}
+
+ );
+}
+
+function DevToolButton({
+ active,
+ setActive,
+ children,
+ ...rest
+}: {
+ active: boolean;
+ setActive: Dispatch>;
+ children: ReactNode;
+} & HTMLAttributes) {
+ return (
+
+ );
+}
diff --git a/examples/react/src/examples/DevTools/DevTools/style.css b/examples/react/src/examples/DevTools/DevTools/style.css
new file mode 100644
index 00000000..39c65969
--- /dev/null
+++ b/examples/react/src/examples/DevTools/DevTools/style.css
@@ -0,0 +1,63 @@
+.react-flow__devtools {
+ --border-radius: 4px;
+ --highlight-color: rgba(238, 58, 115, 1);
+ --font: monospace, sans-serif;
+
+ border-radius: var(--border-radius);
+ font-size: 11px;
+ font-family: var(--font);
+}
+
+.react-flow__devtools button {
+ background: white;
+ border: none;
+ padding: 5px 15px;
+ color: #222;
+ font-weight: bold;
+ font-size: 12px;
+ cursor: pointer;
+ font-family: var(--font);
+ background-color: #f4f4f4;
+}
+
+.react-flow__devtools button:hover {
+ background: var(--highlight-color);
+ color: white;
+}
+
+.react-flow__devtools button.active {
+ background: var(--highlight-color);
+ color: white;
+}
+
+.react-flow__devtools button:first-child {
+ border-radius: var(--border-radius) 0 0 var(--border-radius);
+ border-right: 1px solid #ddd;
+}
+
+.react-flow__devtools button:last-child {
+ border-radius: 0 var(--border-radius) var(--border-radius) 0;
+}
+
+.react-flow__devtools-changelogger {
+ pointer-events: none;
+ position: relative;
+ top: 50px;
+ left: 20px;
+ font-family: var(--font);
+}
+
+.react-flow__devtools-title {
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+.react-flow__devtools-nodeinspector {
+ pointer-events: none;
+ font-family: monospace, sans-serif;
+ font-size: 10px;
+}
+
+.react-flow__devtools-nodeinfo {
+ top: 5px;
+}
diff --git a/examples/react/src/examples/DevTools/index.tsx b/examples/react/src/examples/DevTools/index.tsx
new file mode 100644
index 00000000..5303dea6
--- /dev/null
+++ b/examples/react/src/examples/DevTools/index.tsx
@@ -0,0 +1,56 @@
+import { useCallback } from 'react';
+import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react';
+
+import DevTools from './DevTools';
+import './Devtools/style.css';
+
+const initNodes: Node[] = [
+ {
+ id: '1a',
+ type: 'input',
+ data: { label: 'Node 1' },
+ position: { x: 250, y: 5 },
+ },
+ {
+ id: '2a',
+ data: { label: 'Node 2' },
+ position: { x: 100, y: 100 },
+ },
+ {
+ id: '3a',
+ data: { label: 'Node 3' },
+ position: { x: 400, y: 100 },
+ },
+ {
+ id: '4a',
+ data: { label: 'Node 4' },
+ position: { x: 400, y: 200 },
+ },
+];
+
+const initEdges: Edge[] = [
+ { id: 'e1-2', source: '1a', target: '2a' },
+ { id: 'e1-3', source: '1a', target: '3a' },
+];
+
+const BasicFlow = () => {
+ const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
+
+ const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
+
+ return (
+
+
+
+ );
+};
+
+export default BasicFlow;