@dschz/solid-flow 0.3.0-next.4 → 0.3.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,100 +1,69 @@
1
1
  <p align="center">
2
- <img src="https://assets.solidjs.com/banner?project=solid-flow&type=Ecosystem&background=tiles" alt="@dschz/solid-flow banner" />
2
+ <!-- the %20%20 suffix works around a banner-generator bug that clips a trailing W -->
3
+ <img src="https://assets.solidjs.com/banner?project=solid-flow%20%20&type=Ecosystem&background=tiles" alt="@dschz/solid-flow banner" />
3
4
  </p>
4
5
 
5
6
  # Solid Flow
6
7
 
7
8
  [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
8
9
  [![npm](https://img.shields.io/npm/v/@dschz/solid-flow?color=blue)](https://www.npmjs.com/package/@dschz/solid-flow)
10
+ [![JSR](https://jsr.io/badges/@dschz/solid-flow)](https://jsr.io/@dschz/solid-flow)
9
11
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@dschz/solid-flow)](https://bundlephobia.com/package/@dschz/solid-flow)
10
12
  [![CI](https://github.com/dsnchz/solid-flow/actions/workflows/ci.yaml/badge.svg)](https://github.com/dsnchz/solid-flow/actions/workflows/ci.yaml)
11
13
 
12
- > Solid Flow is a port of [React Flow](https://reactflow.dev/) and [Svelte Flow](https://svelteflow.dev/) for SolidJS.
14
+ A SolidJS port of [React Flow](https://reactflow.dev/) and [Svelte Flow](https://svelteflow.dev/): build node-based editors, diagrams, and interactive graphs with the same battle-tested gesture system ([@xyflow/system](https://github.com/xyflow/xyflow)) and an API designed around Solid's fine-grained reactivity.
13
15
 
14
- ☣️ **Solid Flow is alpha and currently under development. The API intends to follow React/Svelte Flow closely but some things might change for the sake of SolidJS.** ☣️
16
+ ## Version pairing
15
17
 
16
- ## Current Unsupported Features:
18
+ | Solid Flow | SolidJS | Status |
19
+ | ---------- | --------------- | ------------------- |
20
+ | `0.3.x` | `solid-js` 2.x | Active development |
21
+ | `0.2.x` | `solid-js` 1.9+ | Maintenance (fixes) |
17
22
 
18
- - [onlyRenderVisibleElements](https://github.com/dsnchz/solid-flow/issues/15): render only visible elements.
19
- - [Custom MiniMap Nodes](https://github.com/dsnchz/solid-flow/issues/12): define custom minimap jsx node elements.
20
- - [Edge Reconnect Anchors](https://github.com/dsnchz/solid-flow/issues/13): the ability to re-connect already connected edges.
23
+ The 0.3 line is built for SolidJS 2.0 and its deferred, fine-grained reactive graph. Keep `solid-js` and `@solidjs/web` on matching 2.0 versions — mixing them breaks at import time. Upgrading from 0.2.x? See [Migrating from 0.2.x](#migrating-from-02x).
21
24
 
22
25
  ## Key Features
23
26
 
24
- - **Easy to use:** Seamless zooming and panning, single- and multi selection of graph elements and keyboard shortcuts are supported out of the box
25
- - **Customizable:** Different node types (Input, Output, Default, Group) and edge types (Bezier, Straight, Step, SmoothStep) with full support for custom nodes and edges
26
- - **Fast rendering:** Only nodes that have changed are re-rendered using SolidJS's fine-grained reactivity
27
- - **Rich Plugin Ecosystem:** Background patterns, Interactive MiniMap, Zoom Controls, Node Toolbar, and Node Resizer components
28
- - **Powerful Hooks:** Comprehensive set of reactive hooks for nodes, edges, viewport, connections, and data management
29
- - **Full Accessibility:** Complete keyboard navigation, screen reader support, ARIA labels, and focus management
30
- - **Drag & Drop:** Built-in dragging for nodes, external drag-and-drop support, and customizable drag handles
31
- - **Advanced Features:** Node grouping, intersection detection, connection validation, and subflow support
32
- - **TypeScript First:** Fully typed API with generic type support and IntelliSense integration
27
+ - **Easy to use:** Zooming and panning, single- and multi-selection of graph elements, and keyboard shortcuts out of the box
28
+ - **Customizable:** Built-in node types (Input, Output, Default, Group) and edge types (Bezier, Straight, Step, SmoothStep) with full support for custom nodes and edges — including edge reconnection via `EdgeReconnectAnchor`
29
+ - **Fine-grained by construction:** A node drag is a handful of DOM attribute writes — no virtual DOM, no component re-renders, no memoization ceremony
30
+ - **Scales to large graphs:** Off-screen elements are viewport-culled by default, and an opt-in `onlyRenderVisibleElements` mode only mounts what's visible — stress-tested at 10,000 nodes
31
+ - **Type-guided data:** Your custom components are the schema node/edge stores narrow each element's `data` by its `type` field, with autocomplete for type names
32
+ - **Rich plugins:** Background patterns, interactive MiniMap (custom minimap nodes, click handlers), zoom Controls, Node Toolbar, and Node Resizer
33
+ - **Accessible:** Keyboard navigation, screen reader support, ARIA labels, and focus management
34
+ - **SSR-ready:** Renders on the server (a dedicated SSR test lane keeps it that way)
33
35
 
34
36
  ## Installation
35
37
 
36
- The easiest way to get the latest version of Solid Flow is to install it via npm, yarn or pnpm:
37
-
38
38
  ```sh
39
- npm install @dschz/solid-flow
40
- pnpm install @dschz/solid-flow
41
- yarn install @dschz/solid-flow
42
- bun install @dschz/solid-flow
39
+ npm install @dschz/solid-flow
40
+ pnpm add @dschz/solid-flow
41
+ yarn add @dschz/solid-flow
42
+ bun add @dschz/solid-flow
43
43
  ```
44
44
 
45
- ## Quick Start
45
+ `solid-js` is a peer dependency. Also available on [JSR](https://jsr.io/@dschz/solid-flow).
46
46
 
47
- This is a basic example to get you started. For more advanced examples and full API documentation, explore the playground examples included in this repository.
47
+ ## Quick Start
48
48
 
49
49
  ```tsx
50
50
  import {
51
- SolidFlow,
52
- SolidFlowProvider,
53
- Controls,
54
51
  Background,
55
- MiniMap,
56
- addEdge,
57
- type EdgeConnection,
52
+ Controls,
58
53
  createEdgeStore,
59
54
  createNodeStore,
60
- type Viewport,
55
+ type EdgeConnection,
56
+ MiniMap,
57
+ Panel,
58
+ SolidFlow,
61
59
  } from "@dschz/solid-flow";
62
- import "@dschz/solid-flow/styles"; // Required styles
63
-
64
- import { createStore, produce } from "solid-js/store";
65
-
66
- export default function Page() {
67
- return (
68
- <SolidFlowProvider>
69
- <Flow />
70
- </SolidFlowProvider>
71
- )
72
- }
73
-
74
- function Flow() {
75
- // Can invoke useSolidFlow due to parent Page + SolidFlowProvider wrapper. Contains all helper APIs
76
- const { .. } = useSolidFlow();
60
+ import "@dschz/solid-flow/styles"; // required styles, import once
77
61
 
78
- // Use createNodeStore and createEdgeStore for reactive state management
62
+ export const Flow = () => {
79
63
  const [nodes, setNodes] = createNodeStore([
80
- {
81
- id: "1",
82
- type: "input",
83
- data: { label: "Input Node" },
84
- position: { x: 250, y: 0 },
85
- },
86
- {
87
- id: "2",
88
- type: "default",
89
- data: { label: "Default Node" },
90
- position: { x: 100, y: 100 },
91
- },
92
- {
93
- id: "3",
94
- type: "output",
95
- data: { label: "Output Node" },
96
- position: { x: 250, y: 200 },
97
- },
64
+ { id: "1", type: "input", data: { label: "Input" }, position: { x: 250, y: 0 } },
65
+ { id: "2", type: "default", data: { label: "Default" }, position: { x: 100, y: 100 } },
66
+ { id: "3", type: "output", data: { label: "Output" }, position: { x: 250, y: 200 } },
98
67
  ]);
99
68
 
100
69
  const [edges, setEdges] = createEdgeStore([
@@ -102,234 +71,269 @@ function Flow() {
102
71
  { id: "e2-3", source: "2", target: "3" },
103
72
  ]);
104
73
 
105
- const [viewport, setViewport] = createStore<Viewport>({
106
- x: 100,
107
- y: 100,
108
- zoom: 5,
109
- });
110
-
111
- const updateViewport = () => {
112
- setViewport("x", (prev) => prev + 10);
74
+ // Your stores own which elements exist — adopt new connections into
75
+ // your edge store (see "Who owns the data" below).
76
+ const onConnect = (connection: EdgeConnection) => {
77
+ setEdges((edges) => {
78
+ edges.push(connection);
79
+ });
113
80
  };
114
81
 
115
- const onConnect = (connection: EdgeConnection) => {
116
- /**
117
- * Solid Flow updates the node/edge stores internally. The user-land edge store will have the connection inserted by the time onConnect fires so we can just go ahead and update the state of it
118
- */
119
- setEdges(
120
- (edge) => edge.id === connection.id,
121
- produce((edge) => {
122
- edge.animated = true;
123
- }),
124
- );
82
+ const nudge = () => {
83
+ // SolidJS 2.0 store writes are drafts: mutate what changed.
84
+ setNodes((nodes) => {
85
+ nodes[0]!.position.x += 20;
86
+ });
125
87
  };
126
88
 
127
89
  return (
128
90
  <SolidFlow nodes={nodes} edges={edges} onConnect={onConnect} fitView>
129
91
  <Controls />
130
92
  <MiniMap />
93
+ <Background variant="dots" />
131
94
  <Panel position="top-left">
132
- <button onClick={updateViewport}>Update viewport</button>
95
+ <button onClick={nudge}>Nudge first node</button>
133
96
  </Panel>
134
- <Background variant="dots" />
135
97
  </SolidFlow>
136
98
  );
137
- }
99
+ };
138
100
  ```
139
101
 
140
- ## Core Components
141
-
142
- ### Built-in Node Types
102
+ ## Your components are the schema
143
103
 
144
- - **InputNode** - Nodes with source handles only (starting points)
145
- - **OutputNode** - Nodes with target handles only (ending points)
146
- - **DefaultNode** - Standard nodes with both source and target handles
147
- - **GroupNode** - Container nodes for organizing other nodes
104
+ `createNodeStore` / `createEdgeStore` derive each element's `data` type from the component registered for its `type` — the renderer map is the single source of truth, and the stores narrow against it:
148
105
 
149
- ### Built-in Edge Types
106
+ ```tsx
107
+ import {
108
+ createNodeStore,
109
+ Handle,
110
+ type NodeProps,
111
+ type NodeTypes,
112
+ Position,
113
+ } from "@dschz/solid-flow";
150
114
 
151
- - **BezierEdge** - Smooth curved connections (default)
152
- - **StraightEdge** - Direct straight line connections
153
- - **StepEdge** - Right-angle step connections
154
- - **SmoothStepEdge** - Rounded step connections
115
+ const CounterNode = (props: NodeProps<{ count: number }, "counter">) => (
116
+ <div style={{ padding: "10px", background: "white", border: "1px solid #333" }}>
117
+ <Handle type="target" position={Position.Top} />
118
+ <div>count: {props.data.count}</div>
119
+ <Handle type="source" position={Position.Bottom} />
120
+ </div>
121
+ );
155
122
 
156
- ### Plugin Components
123
+ const nodeTypes = { counter: CounterNode } satisfies NodeTypes;
157
124
 
158
- - **Background** - Customizable canvas backgrounds (dots, lines, cross patterns)
159
- - **Controls** - Zoom in/out, fit view, lock/unlock interactions
160
- - **MiniMap** - Interactive overview with viewport indicator
161
- - **NodeToolbar** - Context-sensitive toolbars for nodes
162
- - **NodeResizer** - Real-time node resizing with handles
125
+ const [nodes] = createNodeStore<typeof nodeTypes>([
126
+ // "counter" narrows data to { count: number }; built-ins still work
127
+ { id: "a", type: "counter", data: { count: 1 }, position: { x: 0, y: 0 } },
128
+ { id: "b", type: "default", data: { label: "hi" }, position: { x: 0, y: 100 } },
129
+ // { id: "c", type: "countr", ... } <-- compile error: unknown type name
130
+ ]);
163
131
 
164
- ## Hooks & Utilities
132
+ <SolidFlow nodes={nodes} nodeTypes={nodeTypes} />;
133
+ ```
165
134
 
166
- ### Essential Hooks
135
+ The same guided unions are exported as standalone types, so plain arrays, props, and vanilla stores get identical narrowing:
167
136
 
168
137
  ```tsx
169
- // Main flow instance with full API
170
- const solidFlow = useSolidFlow();
138
+ import type { EdgesFor, NodesFor } from "@dschz/solid-flow";
171
139
 
172
- // Reactive access to nodes and edges
173
- const nodes = useNodes();
174
- const edges = useEdges();
140
+ const initialNodes = [
141
+ { id: "a", type: "counter", data: { count: 1 }, position: { x: 0, y: 0 } },
142
+ ] satisfies NodesFor<typeof nodeTypes>[];
143
+ ```
175
144
 
176
- // Viewport control and monitoring
177
- const viewport = useViewport();
145
+ ## Who owns the data
178
146
 
179
- // Connection state during drag operations
180
- const connection = useConnection();
147
+ The stores you pass as `nodes` / `edges` props are **controlled** — a deliberate contract:
181
148
 
182
- // Reactive access to node data
183
- const nodeData = useNodesData(["node-1", "node-2"]);
149
+ - **Your store owns membership.** Which nodes and edges exist is decided by your store. Any write form works: draft mutations update in place (`O(changed)`), and wholesale replacement (`setNodes(() => next)`) re-seeds the flow — rows are keyed by `id`, so surviving elements keep their runtime state.
150
+ - **The flow writes runtime fields onto your rows.** Dragging updates `position`, selection updates `selected`, measurement fills `measured` — on the same objects you provided, so reading your store is always live.
151
+ - **Imperative commands don't write membership back.** `commands.addNodes(...)` and friends update the flow, not your store. To keep an element across a store replacement, adopt it — like the `onConnect` handler in the Quick Start pushing the new connection into the edge store.
184
152
 
185
- // Node connection information
186
- const connections = useNodeConnections("node-1");
153
+ ## The flow API
187
154
 
188
- // Which node/edge a nested component is rendered inside (for building
189
- // composable custom nodes and edge labels without prop drilling)
190
- const nodeId = useNodeId();
191
- const edgeId = useEdgeId();
192
- ```
155
+ `useSolidFlow()` returns `{ flow, commands }` (with `commands` also spread at the top level for React/Svelte Flow familiarity). Both are stable identities — destructuring is safe.
193
156
 
194
- #### Coming from React Flow?
157
+ ```tsx
158
+ import { SolidFlowProvider, useSolidFlow } from "@dschz/solid-flow";
195
159
 
196
- Some React Flow hooks intentionally have no Solid Flow equivalent, because
197
- Solid's fine-grained reactivity makes them unnecessary:
160
+ const Toolbar = () => {
161
+ const { flow, commands } = useSolidFlow();
198
162
 
199
- - `useOnSelectionChange` / `useOnViewportChange` reading `flow.selection`
200
- or `flow.viewport` (from `useSolidFlow()`) in a tracked scope IS the
201
- subscription; wrap side effects in `createEffect` over those reads.
202
- - `useNodesState` / `useEdgesState` — use `createNodeStore` /
203
- `createEdgeStore` instead.
204
- - `useStore` — the reactive `flow` struct is the supported read surface;
205
- there is no public escape hatch into internals.
163
+ // flow.* reads are reactive: use them in JSX or tracked scopes.
164
+ // flow.nodes, flow.edges, flow.viewport, flow.selection,
165
+ // flow.nodesInitialized, flow.viewportInitialized, ...
206
166
 
207
- ### Utility Functions
167
+ return (
168
+ <div>
169
+ <span>{flow.selection.nodes.length} selected</span>
170
+ <button onClick={() => commands.fitView()}>Fit</button>
171
+ <button onClick={() => commands.zoomIn()}>+</button>
172
+ </div>
173
+ );
174
+ };
208
175
 
209
- ```tsx
210
- // Create reactive stores (replaces signals)
211
- const [nodes, setNodes] = createNodeStore(initialNodes);
212
- const [edges, setEdges] = createEdgeStore(initialEdges);
213
-
214
- // Update stores with SolidJS patterns
215
- import { produce } from "solid-js/store";
216
- setNodes(
217
- (node) => node.id === "1",
218
- produce((node) => {
219
- node.position.x += 20;
220
- }),
176
+ // Hooks used outside <SolidFlow> children need a provider:
177
+ export const App = () => (
178
+ <SolidFlowProvider>
179
+ <Toolbar />
180
+ <Flow />
181
+ </SolidFlowProvider>
221
182
  );
183
+ ```
222
184
 
223
- // Add new connections
224
- setEdges(addEdge(connection, edges));
185
+ Commands include viewport control (`fitView`, `fitBounds`, `zoomIn`/`zoomOut`, `setZoom`, `setCenter`, `setViewport`, `panBy`), element updates (`updateNode`, `updateNodeData`, `updateEdge`, `addNodes`, `addEdges`, `deleteElements`), coordinate conversion (`screenToFlowPosition`, `flowToScreenPosition`), and geometry queries (`isNodeIntersecting`, `getIntersectingNodes`).
225
186
 
226
- // Coordinate transformations (via useSolidFlow)
227
- const { screenToFlowPosition, flowToScreenPosition } = useSolidFlow();
187
+ ## Hooks
228
188
 
229
- // Node/edge utilities
230
- getNodesBounds(nodes);
231
- getIntersectingNodes(node, nodes);
189
+ ```tsx
190
+ const solidFlow = useSolidFlow(); // { flow, commands } — the main API
191
+ const nodes = useNodes(); // reactive readonly node array
192
+ const edges = useEdges(); // reactive readonly edge array
193
+ const viewport = useViewport(); // reactive viewport accessor
194
+ const connection = useConnection(); // in-progress connection state
195
+
196
+ // Hook parameters that feed reactive reads are accessors, so they can't
197
+ // silently go stale when derived from props:
198
+ const nodeData = useNodesData(() => ["node-1", "node-2"]);
199
+ const connections = useNodeConnections(() => ({ id: "node-1" }));
200
+ const internal = useInternalNode(() => props.nodeId); // measured/internal record
201
+
202
+ const updateInternals = useUpdateNodeInternals(); // re-measure after handle changes
203
+
204
+ // Which node/edge a nested component is rendered inside — for composable
205
+ // custom nodes and edge labels without prop drilling:
206
+ const nodeId = useNodeId();
207
+ const edgeId = useEdgeId();
232
208
  ```
233
209
 
234
- ## Advanced Features
210
+ ### Coming from React Flow?
235
211
 
236
- ### Custom Nodes and Edges
212
+ Some React Flow hooks intentionally have no Solid Flow equivalent, because Solid's fine-grained reactivity makes them unnecessary:
237
213
 
238
- Create fully customized components with multiple handles:
214
+ - `useOnSelectionChange` / `useOnViewportChange` reading `flow.selection` or `flow.viewport` in a tracked scope IS the subscription; wrap side effects in `createEffect` over those reads.
215
+ - `useNodesState` / `useEdgesState` — use `createNodeStore` / `createEdgeStore` instead.
216
+ - `useStore` — the reactive `flow` struct is the supported read surface; there is no public escape hatch into internals.
217
+
218
+ ## Custom edges and reconnection
239
219
 
240
220
  ```tsx
241
- import { Handle, type NodeProps } from "@dschz/solid-flow";
221
+ import { BaseEdge, EdgeReconnectAnchor, type EdgeProps, getBezierPath } from "@dschz/solid-flow";
222
+
223
+ const ReconnectableEdge = (props: EdgeProps) => {
224
+ const path = () =>
225
+ getBezierPath({
226
+ sourceX: props.sourceX,
227
+ sourceY: props.sourceY,
228
+ sourcePosition: props.sourcePosition,
229
+ targetX: props.targetX,
230
+ targetY: props.targetY,
231
+ targetPosition: props.targetPosition,
232
+ })[0];
242
233
 
243
- // Type-safe custom node component
244
- function CustomNode(props: NodeProps<{ label: string }, "custom">) {
245
234
  return (
246
- <div class="custom-node" style={{ padding: "10px", background: "white" }}>
247
- <Handle type="target" position="top" />
248
- <div>{props.data.label}</div>
249
- <Handle type="source" position="bottom" id="output-a" />
250
- <Handle type="source" position="bottom" id="output-b" style={{ left: "80%" }} />
251
- </div>
235
+ <>
236
+ <BaseEdge path={path()} markerEnd={props.markerEnd} />
237
+ {/* drag either end of a selected edge onto another handle */}
238
+ <EdgeReconnectAnchor type="source" position={{ x: props.sourceX, y: props.sourceY }} />
239
+ <EdgeReconnectAnchor type="target" position={{ x: props.targetX, y: props.targetY }} />
240
+ </>
252
241
  );
253
- }
242
+ };
243
+ ```
254
244
 
255
- // Create type-safe node types
256
- const nodeTypes = {
257
- custom: CustomNode,
258
- } satisfies NodeTypes;
245
+ Reconnection lifecycle callbacks (`onReconnectStart`, `onReconnect`, `onReconnectEnd`, `onBeforeReconnect`) are available on `<SolidFlow>`.
259
246
 
260
- // Use with typed store
261
- const [nodes] = createNodeStore<typeof nodeTypes>([...]);
247
+ ## Built-in components
262
248
 
263
- <SolidFlow nodeTypes={nodeTypes} nodes={nodes} ... />
264
- ```
249
+ **Node types:** `input` (source handle only), `output` (target handle only), `default` (both), `group` (container for subflows)
265
250
 
266
- ### Connection Validation
251
+ **Edge types:** `default` (bezier), `straight`, `step`, `smoothstep`
267
252
 
268
- ```tsx
269
- import { type Connection } from "@dschz/solid-flow";
253
+ **Plugins:**
270
254
 
271
- const isValidConnection = (connection: Connection) => {
272
- // Custom validation logic
273
- return connection.source !== connection.target;
274
- };
255
+ - **Background** dots, lines, and cross patterns
256
+ - **Controls** zoom in/out, fit view, lock interactions
257
+ - **MiniMap** interactive overview with viewport indicator, custom node rendering via `nodeComponent`, and `onClick` / `onNodeClick` handlers
258
+ - **NodeToolbar** — context-sensitive toolbars for nodes
259
+ - **NodeResizer** — interactive node resizing with handles
275
260
 
276
- const onConnect = (connection: Connection) => {
277
- console.log("New connection:", connection);
278
- setEdges(addEdge(connection, edges));
279
- };
261
+ ## Performance
280
262
 
281
- <SolidFlow
282
- isValidConnection={isValidConnection}
283
- onConnect={onConnect}
284
- ...
285
- />
286
- ```
263
+ Two complementary culling tiers keep large graphs fast:
287
264
 
288
- ### Event Handling
265
+ - **CSS culling (always on):** Elements outside the (overscanned) viewport are hidden with `visibility: hidden` + `pointer-events: none`. Everything stays mounted, so component state, measurement, and accessibility semantics are untouched — this tier has no userland contract at all.
266
+ - **`onlyRenderVisibleElements` (opt-in):** Off-screen elements are **unmounted entirely** and remount as the viewport reaches them. At 10,000 nodes this cuts the DOM ~16x, roughly halves memory, and makes node drags several times faster. Positions, selection, and cached measurements live in the flow's data graph — outside your components — so elements come back exactly as they left. Component-_local_ state does not survive unmounting: keep state you care about in `node.data`. Selected elements, unmeasured nodes, and the node holding focus are never unmounted — and an element whose content must keep running off-screen (media, timers, embeds) can opt out of culling entirely with `cullable: false` on the node or edge.
289
267
 
290
- ```tsx
291
- <SolidFlow
292
- onNodeClick={(event, node) => console.log("Node clicked:", node)}
293
- onNodeDrag={(event, node) => console.log("Node dragged:", node)}
294
- onEdgeClick={(event, edge) => console.log("Edge clicked:", edge)}
295
- onPaneClick={(event) => console.log("Pane clicked")}
296
- onSelectionChange={(params) => console.log("Selection changed:", params)}
297
- />
298
- ```
268
+ The MiniMap always renders the full graph in either mode — it reads the data graph, not the DOM.
269
+
270
+ ## Server-side rendering
271
+
272
+ `<SolidFlow>` renders with `renderToString` and hydrates cleanly. For meaningful server-rendered layout, give nodes explicit `width` / `height` (there is no DOM to measure on the server) — the same guidance as React/Svelte Flow.
299
273
 
300
274
  ## Accessibility
301
275
 
302
- Solid Flow includes comprehensive accessibility features:
276
+ - Full keyboard navigation (arrow-key node movement, focus traversal, Escape to deselect)
277
+ - Screen reader support with ARIA labels and live announcements
278
+ - Focus management, including under viewport culling
279
+ - Color mode support (`light` / `dark` / `system`)
303
280
 
304
- - Full keyboard navigation support
305
- - Screen reader compatibility with ARIA labels
306
- - Focus management and visual indicators
307
- - High contrast and color mode support
308
- - Customizable keyboard shortcuts
281
+ ## Migrating from 0.2.x
309
282
 
310
- ## Performance
283
+ The 0.3 line targets SolidJS 2.0, which changes how you write to stores, and reworks the read API. The gestures, components, plugins, and commands are otherwise the same.
311
284
 
312
- - **Reactive Updates**: Only re-renders components when their specific data changes
313
- - **Viewport Optimization**: Option to render only visible elements (coming soon)
314
- - **Memory Efficient**: Optimized data structures for large graphs
315
- - **Stress Tested**: Handles hundreds of nodes smoothly
285
+ **1. Upgrade the peer dependencies.** `solid-js` and `@solidjs/web` move to matching 2.0 versions.
316
286
 
317
- ## Examples
287
+ **2. Store writes: path setters are gone (SolidJS 2.0).** Every `setNodes` / `setEdges` call site using 1.x path syntax becomes a draft callback — the callback's argument is a mutable draft, so mutation is the API. Returning a value instead replaces wholesale.
288
+
289
+ ```tsx
290
+ // 0.2.x (SolidJS 1.x) — path syntax
291
+ setNodes(0, "position", "x", (x) => x + 20);
292
+ setEdges((edge) => edge.id === "e1", "animated", true);
293
+
294
+ // 0.3.x (SolidJS 2.0) — draft callback
295
+ setNodes((nodes) => {
296
+ nodes[0]!.position.x += 20;
297
+ });
298
+ setEdges((edges) => {
299
+ const edge = edges.find((e) => e.id === "e1");
300
+ if (edge) edge.animated = true;
301
+ });
302
+
303
+ // wholesale replacement (re-seeds the flow; rows keyed by id are reused)
304
+ setNodes(() => nextNodes);
305
+ ```
306
+
307
+ **3. `useSolidFlow` reads moved to the reactive `flow` struct.** The flat getters (`getNodes()`, `getEdges()`, `getNode(id)`, `getEdge(id)`, `getInternalNode(id)`, `getViewport()`, `getZoom()`) are removed:
308
+
309
+ ```tsx
310
+ // 0.2.x // 0.3.x
311
+ solidFlow.getNodes();
312
+ flow.nodes;
313
+ solidFlow.getViewport();
314
+ flow.viewport;
315
+ solidFlow.getZoom();
316
+ flow.viewport.zoom;
317
+ solidFlow.getNode("a");
318
+ flow.nodes.find((n) => n.id === "a");
319
+ solidFlow.getInternalNode("a");
320
+ useInternalNode(() => "a");
321
+ ```
318
322
 
319
- The repository includes a comprehensive playground with 25+ examples:
323
+ `flow.*` reads are reactive — using them in JSX or a tracked scope subscribes. Commands (`fitView`, `setViewport`, `updateNode`, `deleteElements`, ...) are unchanged and now also available namespaced under `commands`.
320
324
 
321
- - **Basic Usage** - Simple flows and interactions
322
- - **Custom Nodes** - Creating specialized node types
323
- - **Edge Types** - Different connection styles
324
- - **Drag & Drop** - External elements and node creation
325
- - **Validation** - Connection rules and constraints
326
- - **Subflows** - Hierarchical node organization
327
- - **Performance** - Large dataset handling
328
- - **Accessibility** - Keyboard navigation and screen readers
325
+ **4. New connections are no longer written into your edge store.** In 0.2.x the flow inserted the connected edge into your store before `onConnect` fired. Under 0.3's ownership contract your store owns membership: adopt the connection yourself (see the Quick Start's `onConnect`). Unadopted connections still render, but won't survive a wholesale store replacement.
329
326
 
330
- Run the examples locally:
327
+ **5. `onlyRenderVisibleElements` now does what it says.** In 0.2.x the prop was accepted but inert. In 0.3 it opts into unmount culling (off-screen elements are not mounted at all — see [Performance](#performance)), while the CSS culling tier is always on and needs no prop.
328
+
329
+ **6. Smaller signature changes.** `useNodes()` / `useEdges()` return `readonly` arrays; `useHandleEdgeSelect` is removed (it was internal plumbing — select edges through `commands`).
330
+
331
+ ## Examples
332
+
333
+ The repository ships a playground with 25+ runnable examples — custom nodes and edges, edge reconnection, drag & drop, subflows, validation, minimap customization, a 10k-node stress test, accessibility, and more:
331
334
 
332
335
  ```bash
336
+ bun install
333
337
  bun start
334
338
  ```
335
339
 
@@ -359,6 +363,14 @@ bun install
359
363
  bun start
360
364
  ```
361
365
 
366
+ ### Testing
367
+
368
+ ```bash
369
+ bun run test # unit + component tests (vitest, jsdom)
370
+ bun run test:ssr # SSR lane (node environment, server builds)
371
+ bun run test:e2e # browser gesture harness (Playwright)
372
+ ```
373
+
362
374
  ### Linting & Formatting
363
375
 
364
376
  ```bash
@@ -21,6 +21,14 @@ type Node<NodeData extends UnknownStruct = UnknownStruct, NodeType extends strin
21
21
  class?: string;
22
22
  style?: JSX.CSSProperties;
23
23
  focusable?: boolean;
24
+ /**
25
+ * When `false`, the node is exempt from viewport culling on both tiers:
26
+ * the always-on CSS tier never hides it and `onlyRenderVisibleElements`
27
+ * never unmounts it. Use for nodes whose content must keep running while
28
+ * off-screen (media playback, timers, third-party embeds).
29
+ * @default true
30
+ */
31
+ cullable?: boolean;
24
32
  /**
25
33
  * The ARIA role attribute for the node element, used for accessibility.
26
34
  * @default "group"
@@ -72,6 +80,13 @@ type Edge<EdgeData extends UnknownStruct = UnknownStruct, EdgeType extends strin
72
80
  style?: JSX.CSSProperties;
73
81
  class?: string;
74
82
  focusable?: boolean;
83
+ /**
84
+ * When `false`, the edge is exempt from viewport culling on both tiers:
85
+ * the always-on CSS tier never hides it and `onlyRenderVisibleElements`
86
+ * never unmounts it.
87
+ * @default true
88
+ */
89
+ cullable?: boolean;
75
90
  /**
76
91
  * The ARIA role attribute for the edge, used for accessibility.
77
92
  * @default "group"
@@ -866,7 +881,10 @@ type SolidFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
866
881
  * Component-LOCAL state does not survive — signals created inside a
867
882
  * custom node, uncontrolled inputs, scroll positions of inner elements.
868
883
  * Keep state you care about in `node.data`. Never unmounted: selected
869
- * elements, unmeasured nodes, and the node holding DOM focus.
884
+ * elements, unmeasured nodes, and the node holding DOM focus. Elements
885
+ * whose component-local state (or side effects) must keep running while
886
+ * off-screen can opt out entirely with `cullable: false` on the node or
887
+ * edge — they are then exempt from both tiers.
870
888
  *
871
889
  * Independent of this prop, off-viewport elements are always CSS-culled
872
890
  * (`visibility: hidden` + `pointer-events: none`, everything stays
@@ -430,9 +430,9 @@ const A11yDescriptions = () => {
430
430
  height: 2 * halfHeight
431
431
  };
432
432
  }, { equals: rectsEqual }), isNodeCulled = (node, cullingViewport) => {
433
- if (!cullingViewport || node.selected) return !1;
433
+ if (!cullingViewport || node.selected || node.cullable === !1) return !1;
434
434
  let { width, height } = node.measured;
435
- if (!width || !height) return !1;
435
+ if (!width || !height || !node.internals.handleBounds) return !1;
436
436
  let { x, y } = node.internals.positionAbsolute;
437
437
  return !rectsOverlap({
438
438
  x,
@@ -441,7 +441,7 @@ const A11yDescriptions = () => {
441
441
  height
442
442
  }, cullingViewport);
443
443
  }, isEdgeCulled = (row, cullingViewport) => {
444
- if (!cullingViewport || row.selected) return !1;
444
+ if (!cullingViewport || row.selected || row.cullable === !1) return !1;
445
445
  let x = Math.min(row.sourceX, row.targetX), y = Math.min(row.sourceY, row.targetY);
446
446
  return !rectsOverlap({
447
447
  x,
@@ -3278,6 +3278,34 @@ function matchesKey(event, keyDef) {
3278
3278
  function matchesKeyArray(event, keyDefs) {
3279
3279
  return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
3280
3280
  }
3281
+ const MODIFIER_FLAG = {
3282
+ alt: "altKey",
3283
+ control: "ctrlKey",
3284
+ ctrl: "ctrlKey",
3285
+ meta: "metaKey",
3286
+ shift: "shiftKey"
3287
+ };
3288
+ /**
3289
+ * Whether an event's modifier flags PROVE this key definition cannot be held
3290
+ * right now: the key itself is a modifier whose flag is off, or one of its
3291
+ * required modifiers is off. Definitions built on non-modifier keys are never
3292
+ * contradicted (their state isn't derivable from flags).
3293
+ */
3294
+ function isContradicted(flags, keyDef) {
3295
+ let keyFlag = MODIFIER_FLAG[getKeyString(keyDef).toLowerCase()];
3296
+ if (keyFlag && !flags[keyFlag]) return !0;
3297
+ let modifiers = getModifier(keyDef);
3298
+ return (Array.isArray(modifiers) ? modifiers.flatMap((mod) => mod) : [modifiers]).some((mod) => {
3299
+ let modFlag = MODIFIER_FLAG[mod.toLowerCase()];
3300
+ return !!modFlag && !flags[modFlag];
3301
+ });
3302
+ }
3303
+ /** True only when EVERY definition for this key state is contradicted. */
3304
+ function allContradicted(flags, keyDefs) {
3305
+ if (!keyDefs) return !1;
3306
+ let keys = Array.isArray(keyDefs) ? keyDefs : [keyDefs];
3307
+ return keys.length > 0 && keys.every((keyDef) => isContradicted(flags, keyDef));
3308
+ }
3281
3309
  const KeyHandler = (props) => {
3282
3310
  let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
3283
3311
  get selectionKey() {
@@ -3297,6 +3325,20 @@ const KeyHandler = (props) => {
3297
3325
  }
3298
3326
  }, resetKeysAndSelection = () => {
3299
3327
  actions.setSelectionRect(void 0), actions.setSelectionKeyPressed(!1), actions.setMultiselectionKeyPressed(!1), actions.setDeleteKeyPressed(!1), actions.setPanActivationKeyPressed(!1), actions.setZoomActivationKeyPressed(!1);
3328
+ }, reconcileModifiers = (event) => {
3329
+ let changed = !1;
3330
+ store.selectionKeyPressed && allContradicted(event, _props.selectionKey) && (actions.setSelectionKeyPressed(!1), changed = !0), store.multiselectionKeyPressed && allContradicted(event, _props.multiSelectionKey) && (actions.setMultiselectionKeyPressed(!1), changed = !0), store.panActivationKeyPressed && allContradicted(event, _props.panActivationKey) && (actions.setPanActivationKeyPressed(!1), changed = !0), store.zoomActivationKeyPressed && allContradicted(event, _props.zoomActivationKey) && (actions.setZoomActivationKeyPressed(!1), changed = !0), changed && flush();
3331
+ }, cancelPointerGestures = () => {
3332
+ let release = (Ctor, type) => {
3333
+ try {
3334
+ window.dispatchEvent(new Ctor(type, { view: window }));
3335
+ } catch {
3336
+ window.dispatchEvent(new Ctor(type));
3337
+ }
3338
+ };
3339
+ release(MouseEvent, "mouseup"), typeof PointerEvent < "u" && release(PointerEvent, "pointerup");
3340
+ }, handleWindowBlur = () => {
3341
+ resetKeysAndSelection(), cancelPointerGestures();
3300
3342
  }, handleDelete = async () => {
3301
3343
  let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
3302
3344
  nodes: selectedNodes,
@@ -3307,16 +3349,22 @@ const KeyHandler = (props) => {
3307
3349
  edges: deletedEdges
3308
3350
  });
3309
3351
  };
3310
- return isServer || createEventListenerMap(window, {
3352
+ return isServer || (createEventListenerMap(window, {
3311
3353
  keydown: (event) => {
3312
- matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!0), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!0), matchesKeyArray(event, _props.deleteKey) && !isInputDOMNode(event) && (event.ctrlKey || event.metaKey || event.shiftKey || (actions.setDeleteKeyPressed(!0), handleDelete())), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!0), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!0), flush();
3354
+ reconcileModifiers(event), matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!0), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!0), matchesKeyArray(event, _props.deleteKey) && !isInputDOMNode(event) && (event.ctrlKey || event.metaKey || event.shiftKey || (actions.setDeleteKeyPressed(!0), handleDelete())), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!0), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!0), flush();
3313
3355
  },
3314
3356
  keyup: (event) => {
3315
- matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!1), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!1), matchesKeyArray(event, _props.deleteKey) && actions.setDeleteKeyPressed(!1), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!1), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!1), flush();
3357
+ reconcileModifiers(event), matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!1), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!1), matchesKeyArray(event, _props.deleteKey) && actions.setDeleteKeyPressed(!1), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!1), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!1), flush();
3316
3358
  },
3317
- blur: resetKeysAndSelection,
3359
+ blur: handleWindowBlur,
3318
3360
  contextmenu: resetKeysAndSelection
3319
- }), null;
3361
+ }), createEventListenerMap(window, {
3362
+ pointerdown: reconcileModifiers,
3363
+ wheel: reconcileModifiers
3364
+ }, {
3365
+ capture: !0,
3366
+ passive: !0
3367
+ })), null;
3320
3368
  };
3321
3369
  //#endregion
3322
3370
  //#region src/components/SolidFlow.tsx
@@ -250,9 +250,9 @@ const getEdgeId = (connection) => {
250
250
  height: 2 * halfHeight
251
251
  };
252
252
  }, { equals: rectsEqual }), isNodeCulled = (node, cullingViewport) => {
253
- if (!cullingViewport || node.selected) return !1;
253
+ if (!cullingViewport || node.selected || node.cullable === !1) return !1;
254
254
  let { width, height } = node.measured;
255
- if (!width || !height) return !1;
255
+ if (!width || !height || !node.internals.handleBounds) return !1;
256
256
  let { x, y } = node.internals.positionAbsolute;
257
257
  return !rectsOverlap({
258
258
  x,
@@ -261,7 +261,7 @@ const getEdgeId = (connection) => {
261
261
  height
262
262
  }, cullingViewport);
263
263
  }, isEdgeCulled = (row, cullingViewport) => {
264
- if (!cullingViewport || row.selected) return !1;
264
+ if (!cullingViewport || row.selected || row.cullable === !1) return !1;
265
265
  let x = Math.min(row.sourceX, row.targetX), y = Math.min(row.sourceY, row.targetY);
266
266
  return !rectsOverlap({
267
267
  x,
@@ -2385,6 +2385,34 @@ function matchesKey(event, keyDef) {
2385
2385
  function matchesKeyArray(event, keyDefs) {
2386
2386
  return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
2387
2387
  }
2388
+ const MODIFIER_FLAG = {
2389
+ alt: "altKey",
2390
+ control: "ctrlKey",
2391
+ ctrl: "ctrlKey",
2392
+ meta: "metaKey",
2393
+ shift: "shiftKey"
2394
+ };
2395
+ /**
2396
+ * Whether an event's modifier flags PROVE this key definition cannot be held
2397
+ * right now: the key itself is a modifier whose flag is off, or one of its
2398
+ * required modifiers is off. Definitions built on non-modifier keys are never
2399
+ * contradicted (their state isn't derivable from flags).
2400
+ */
2401
+ function isContradicted(flags, keyDef) {
2402
+ let keyFlag = MODIFIER_FLAG[getKeyString(keyDef).toLowerCase()];
2403
+ if (keyFlag && !flags[keyFlag]) return !0;
2404
+ let modifiers = getModifier(keyDef);
2405
+ return (Array.isArray(modifiers) ? modifiers.flatMap((mod) => mod) : [modifiers]).some((mod) => {
2406
+ let modFlag = MODIFIER_FLAG[mod.toLowerCase()];
2407
+ return !!modFlag && !flags[modFlag];
2408
+ });
2409
+ }
2410
+ /** True only when EVERY definition for this key state is contradicted. */
2411
+ function allContradicted(flags, keyDefs) {
2412
+ if (!keyDefs) return !1;
2413
+ let keys = Array.isArray(keyDefs) ? keyDefs : [keyDefs];
2414
+ return keys.length > 0 && keys.every((keyDef) => isContradicted(flags, keyDef));
2415
+ }
2388
2416
  const KeyHandler = (props) => {
2389
2417
  let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
2390
2418
  get selectionKey() {
@@ -2404,6 +2432,20 @@ const KeyHandler = (props) => {
2404
2432
  }
2405
2433
  }, resetKeysAndSelection = () => {
2406
2434
  actions.setSelectionRect(void 0), actions.setSelectionKeyPressed(!1), actions.setMultiselectionKeyPressed(!1), actions.setDeleteKeyPressed(!1), actions.setPanActivationKeyPressed(!1), actions.setZoomActivationKeyPressed(!1);
2435
+ }, reconcileModifiers = (event) => {
2436
+ let changed = !1;
2437
+ store.selectionKeyPressed && allContradicted(event, _props.selectionKey) && (actions.setSelectionKeyPressed(!1), changed = !0), store.multiselectionKeyPressed && allContradicted(event, _props.multiSelectionKey) && (actions.setMultiselectionKeyPressed(!1), changed = !0), store.panActivationKeyPressed && allContradicted(event, _props.panActivationKey) && (actions.setPanActivationKeyPressed(!1), changed = !0), store.zoomActivationKeyPressed && allContradicted(event, _props.zoomActivationKey) && (actions.setZoomActivationKeyPressed(!1), changed = !0), changed && flush();
2438
+ }, cancelPointerGestures = () => {
2439
+ let release = (Ctor, type) => {
2440
+ try {
2441
+ window.dispatchEvent(new Ctor(type, { view: window }));
2442
+ } catch {
2443
+ window.dispatchEvent(new Ctor(type));
2444
+ }
2445
+ };
2446
+ release(MouseEvent, "mouseup"), typeof PointerEvent < "u" && release(PointerEvent, "pointerup");
2447
+ }, handleWindowBlur = () => {
2448
+ resetKeysAndSelection(), cancelPointerGestures();
2407
2449
  }, handleDelete = async () => {
2408
2450
  let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
2409
2451
  nodes: selectedNodes,
@@ -2414,16 +2456,22 @@ const KeyHandler = (props) => {
2414
2456
  edges: deletedEdges
2415
2457
  });
2416
2458
  };
2417
- return isServer || createEventListenerMap(window, {
2459
+ return isServer || (createEventListenerMap(window, {
2418
2460
  keydown: (event) => {
2419
- matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!0), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!0), matchesKeyArray(event, _props.deleteKey) && !isInputDOMNode(event) && (event.ctrlKey || event.metaKey || event.shiftKey || (actions.setDeleteKeyPressed(!0), handleDelete())), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!0), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!0), flush();
2461
+ reconcileModifiers(event), matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!0), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!0), matchesKeyArray(event, _props.deleteKey) && !isInputDOMNode(event) && (event.ctrlKey || event.metaKey || event.shiftKey || (actions.setDeleteKeyPressed(!0), handleDelete())), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!0), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!0), flush();
2420
2462
  },
2421
2463
  keyup: (event) => {
2422
- matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!1), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!1), matchesKeyArray(event, _props.deleteKey) && actions.setDeleteKeyPressed(!1), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!1), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!1), flush();
2464
+ reconcileModifiers(event), matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!1), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!1), matchesKeyArray(event, _props.deleteKey) && actions.setDeleteKeyPressed(!1), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!1), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!1), flush();
2423
2465
  },
2424
- blur: resetKeysAndSelection,
2466
+ blur: handleWindowBlur,
2425
2467
  contextmenu: resetKeysAndSelection
2426
- }), null;
2468
+ }), createEventListenerMap(window, {
2469
+ pointerdown: reconcileModifiers,
2470
+ wheel: reconcileModifiers
2471
+ }, {
2472
+ capture: !0,
2473
+ passive: !0
2474
+ })), null;
2427
2475
  }, SolidFlow = (props) => {
2428
2476
  let [domNodeRef, setDomNodeRef] = createSignal(), domNode, _props = merge({
2429
2477
  ...getDefaultFlowStateProps(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dschz/solid-flow",
3
- "version": "0.3.0-next.4",
3
+ "version": "0.3.0-next.5",
4
4
  "description": "Solid Flow - A highly customizable Solid library for building node-based editors, workflow systems, diagrams and more.",
5
5
  "keywords": [
6
6
  "ai workflows",
@@ -87,6 +87,7 @@
87
87
  "start": "vite",
88
88
  "test": "vitest run",
89
89
  "test:cov": "vitest run --coverage",
90
+ "test:e2e": "playwright test",
90
91
  "typecheck": "tsc --noEmit",
91
92
  "test:ssr": "vitest run --config vite.config.ssr.ts"
92
93
  },
@@ -100,6 +101,7 @@
100
101
  "@changesets/cli": "2.31.1",
101
102
  "@dagrejs/dagre": "^1.1.5",
102
103
  "@dom-expressions/compiler": "0.50.0-next.43",
104
+ "@playwright/test": "1.62.1",
103
105
  "@solidjs/testing-library": "1.0.0-beta.2",
104
106
  "@solidjs/vite-plugin": "3.0.0-next.28",
105
107
  "@testing-library/jest-dom": "7.0.0",