@dschz/solid-flow 0.3.0-next.4 → 1.0.0-next.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
+ | `1.x` | `solid-js` 2.x | Active development (`next` tag) |
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 1.x line is built for SolidJS 2.0 and its deferred, fine-grained reactive graph; the stable 1.0.0 ships alongside SolidJS 2.0 stable (until then, install with the `next` tag). 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,271 @@ 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 { SolidFlowEdge, SolidFlowNode } 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 SolidFlowNode<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
+ **Prefer letting the flow own the data?** Pass `defaultNodes` / `defaultEdges` instead of `nodes` / `edges` for an **uncontrolled** flow (React Flow parity): the arrays seed the flow once (later values are ignored), and membership belongs to the flow — commands like `addNodes` and completed connections persist with no adoption step. Read live state through `useSolidFlow()`'s `flow.nodes` / `flow.edges`. The two axes are independent, so you can control edges while leaving nodes uncontrolled (or vice versa); supplying both props on one axis is a mistake (`nodes` wins, with a dev warning).
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
+ ## The flow API
193
156
 
194
- #### Coming from React Flow?
157
+ `useSolidFlow()` returns `{ flow, commands }` (with `commands` also spread at the top level for React/Svelte Flow familiarity). Both are stable identities — destructuring is safe.
195
158
 
196
- Some React Flow hooks intentionally have no Solid Flow equivalent, because
197
- Solid's fine-grained reactivity makes them unnecessary:
159
+ ```tsx
160
+ import { SolidFlowProvider, useSolidFlow } from "@dschz/solid-flow";
198
161
 
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.
162
+ const Toolbar = () => {
163
+ const { flow, commands } = useSolidFlow();
206
164
 
207
- ### Utility Functions
165
+ // flow.* reads are reactive: use them in JSX or tracked scopes.
166
+ // flow.nodes, flow.edges, flow.viewport, flow.selection,
167
+ // flow.nodesInitialized, flow.viewportInitialized, ...
208
168
 
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
- }),
169
+ return (
170
+ <div>
171
+ <span>{flow.selection.nodes.length} selected</span>
172
+ <button onClick={() => commands.fitView()}>Fit</button>
173
+ <button onClick={() => commands.zoomIn()}>+</button>
174
+ </div>
175
+ );
176
+ };
177
+
178
+ // Hooks used outside <SolidFlow> children need a provider:
179
+ export const App = () => (
180
+ <SolidFlowProvider>
181
+ <Toolbar />
182
+ <Flow />
183
+ </SolidFlowProvider>
221
184
  );
185
+ ```
222
186
 
223
- // Add new connections
224
- setEdges(addEdge(connection, edges));
187
+ 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
188
 
226
- // Coordinate transformations (via useSolidFlow)
227
- const { screenToFlowPosition, flowToScreenPosition } = useSolidFlow();
189
+ ## Hooks
228
190
 
229
- // Node/edge utilities
230
- getNodesBounds(nodes);
231
- getIntersectingNodes(node, nodes);
191
+ ```tsx
192
+ const solidFlow = useSolidFlow(); // { flow, commands } — the main API
193
+ const nodes = useNodes(); // reactive readonly node array
194
+ const edges = useEdges(); // reactive readonly edge array
195
+ const viewport = useViewport(); // reactive viewport accessor
196
+ const connection = useConnection(); // in-progress connection state
197
+
198
+ // Hook parameters that feed reactive reads are accessors, so they can't
199
+ // silently go stale when derived from props:
200
+ const nodeData = useNodesData(() => ["node-1", "node-2"]);
201
+ const connections = useNodeConnections(() => ({ id: "node-1" }));
202
+ const internal = useInternalNode(() => props.nodeId); // measured/internal record
203
+
204
+ const updateInternals = useUpdateNodeInternals(); // re-measure after handle changes
205
+
206
+ // Which node/edge a nested component is rendered inside — for composable
207
+ // custom nodes and edge labels without prop drilling:
208
+ const nodeId = useNodeId();
209
+ const edgeId = useEdgeId();
232
210
  ```
233
211
 
234
- ## Advanced Features
212
+ ### Coming from React Flow?
235
213
 
236
- ### Custom Nodes and Edges
214
+ Some React Flow hooks intentionally have no Solid Flow equivalent, because Solid's fine-grained reactivity makes them unnecessary:
237
215
 
238
- Create fully customized components with multiple handles:
216
+ - `useOnSelectionChange` / `useOnViewportChange` reading `flow.selection` or `flow.viewport` in a tracked scope IS the subscription; wrap side effects in `createEffect` over those reads.
217
+ - `useNodesState` / `useEdgesState` — use `createNodeStore` / `createEdgeStore` instead.
218
+ - `useStore` — the reactive `flow` struct is the supported read surface; there is no public escape hatch into internals.
219
+
220
+ ## Custom edges and reconnection
239
221
 
240
222
  ```tsx
241
- import { Handle, type NodeProps } from "@dschz/solid-flow";
223
+ import { BaseEdge, EdgeReconnectAnchor, type EdgeProps, getBezierPath } from "@dschz/solid-flow";
224
+
225
+ const ReconnectableEdge = (props: EdgeProps) => {
226
+ const path = () =>
227
+ getBezierPath({
228
+ sourceX: props.sourceX,
229
+ sourceY: props.sourceY,
230
+ sourcePosition: props.sourcePosition,
231
+ targetX: props.targetX,
232
+ targetY: props.targetY,
233
+ targetPosition: props.targetPosition,
234
+ })[0];
242
235
 
243
- // Type-safe custom node component
244
- function CustomNode(props: NodeProps<{ label: string }, "custom">) {
245
236
  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>
237
+ <>
238
+ <BaseEdge path={path()} markerEnd={props.markerEnd} />
239
+ {/* drag either end of a selected edge onto another handle */}
240
+ <EdgeReconnectAnchor type="source" position={{ x: props.sourceX, y: props.sourceY }} />
241
+ <EdgeReconnectAnchor type="target" position={{ x: props.targetX, y: props.targetY }} />
242
+ </>
252
243
  );
253
- }
244
+ };
245
+ ```
254
246
 
255
- // Create type-safe node types
256
- const nodeTypes = {
257
- custom: CustomNode,
258
- } satisfies NodeTypes;
247
+ Reconnection lifecycle callbacks (`onReconnectStart`, `onReconnect`, `onReconnectEnd`, `onBeforeReconnect`) are available on `<SolidFlow>`.
259
248
 
260
- // Use with typed store
261
- const [nodes] = createNodeStore<typeof nodeTypes>([...]);
249
+ ## Built-in components
262
250
 
263
- <SolidFlow nodeTypes={nodeTypes} nodes={nodes} ... />
264
- ```
251
+ **Node types:** `input` (source handle only), `output` (target handle only), `default` (both), `group` (container for subflows)
265
252
 
266
- ### Connection Validation
253
+ **Edge types:** `default` (bezier), `straight`, `step`, `smoothstep`
267
254
 
268
- ```tsx
269
- import { type Connection } from "@dschz/solid-flow";
255
+ **Plugins:**
270
256
 
271
- const isValidConnection = (connection: Connection) => {
272
- // Custom validation logic
273
- return connection.source !== connection.target;
274
- };
257
+ - **Background** dots, lines, and cross patterns
258
+ - **Controls** zoom in/out, fit view, lock interactions
259
+ - **MiniMap** interactive overview with viewport indicator, custom node rendering via `nodeComponent`, and `onClick` / `onNodeClick` handlers
260
+ - **NodeToolbar** — context-sensitive toolbars for nodes
261
+ - **NodeResizer** — interactive node resizing with handles
275
262
 
276
- const onConnect = (connection: Connection) => {
277
- console.log("New connection:", connection);
278
- setEdges(addEdge(connection, edges));
279
- };
263
+ ## Performance
280
264
 
281
- <SolidFlow
282
- isValidConnection={isValidConnection}
283
- onConnect={onConnect}
284
- ...
285
- />
286
- ```
265
+ Two complementary culling tiers keep large graphs fast:
287
266
 
288
- ### Event Handling
267
+ - **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.
268
+ - **`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
269
 
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
- ```
270
+ The MiniMap always renders the full graph in either mode — it reads the data graph, not the DOM.
271
+
272
+ ## Server-side rendering
273
+
274
+ `<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
275
 
300
276
  ## Accessibility
301
277
 
302
- Solid Flow includes comprehensive accessibility features:
278
+ - Full keyboard navigation (arrow-key node movement, focus traversal, Escape to deselect)
279
+ - Screen reader support with ARIA labels and live announcements
280
+ - Focus management, including under viewport culling
281
+ - Color mode support (`light` / `dark` / `system`)
303
282
 
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
283
+ ## Migrating from 0.2.x
309
284
 
310
- ## Performance
285
+ The 1.x 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
286
 
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
287
+ **1. Upgrade the peer dependencies.** `solid-js` and `@solidjs/web` move to matching 2.0 versions.
316
288
 
317
- ## Examples
289
+ **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.
290
+
291
+ ```tsx
292
+ // 0.2.x (SolidJS 1.x) — path syntax
293
+ setNodes(0, "position", "x", (x) => x + 20);
294
+ setEdges((edge) => edge.id === "e1", "animated", true);
295
+
296
+ // 1.x (SolidJS 2.0) — draft callback
297
+ setNodes((nodes) => {
298
+ nodes[0]!.position.x += 20;
299
+ });
300
+ setEdges((edges) => {
301
+ const edge = edges.find((e) => e.id === "e1");
302
+ if (edge) edge.animated = true;
303
+ });
304
+
305
+ // wholesale replacement (re-seeds the flow; rows keyed by id are reused)
306
+ setNodes(() => nextNodes);
307
+ ```
308
+
309
+ **3. `useSolidFlow` reads moved to the reactive `flow` struct.** The flat getters (`getNodes()`, `getEdges()`, `getNode(id)`, `getEdge(id)`, `getInternalNode(id)`, `getViewport()`, `getZoom()`) are removed:
310
+
311
+ ```tsx
312
+ // 0.2.x // 1.x
313
+ solidFlow.getNodes();
314
+ flow.nodes;
315
+ solidFlow.getViewport();
316
+ flow.viewport;
317
+ solidFlow.getZoom();
318
+ flow.viewport.zoom;
319
+ solidFlow.getNode("a");
320
+ flow.nodes.find((n) => n.id === "a");
321
+ solidFlow.getInternalNode("a");
322
+ useInternalNode(() => "a");
323
+ ```
318
324
 
319
- The repository includes a comprehensive playground with 25+ examples:
325
+ `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
326
 
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
327
+ **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 the 1.x 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
328
 
330
- Run the examples locally:
329
+ **5. `onlyRenderVisibleElements` now does what it says.** In 0.2.x the prop was accepted but inert. In 1.x 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.
330
+
331
+ **6. Smaller signature changes.** `useNodes()` / `useEdges()` return `readonly` arrays; `useHandleEdgeSelect` is removed (it was internal plumbing — select edges through `commands`).
332
+
333
+ ## Examples
334
+
335
+ 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
336
 
332
337
  ```bash
338
+ bun install
333
339
  bun start
334
340
  ```
335
341
 
@@ -359,6 +365,14 @@ bun install
359
365
  bun start
360
366
  ```
361
367
 
368
+ ### Testing
369
+
370
+ ```bash
371
+ bun run test # unit + component tests (vitest, jsdom)
372
+ bun run test:ssr # SSR lane (node environment, server builds)
373
+ bun run test:e2e # browser gesture harness (Playwright)
374
+ ```
375
+
362
376
  ### Linting & Formatting
363
377
 
364
378
  ```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"
@@ -612,6 +627,22 @@ type SolidFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
612
627
  * ]);
613
628
  */
614
629
  readonly edges?: Store<EdgeType[]>;
630
+ /**
631
+ * Initial nodes for an UNCONTROLLED flow. When `nodes` is not supplied,
632
+ * the flow owns element state: this array seeds it once (later values
633
+ * are ignored), and membership belongs to the flow — commands like
634
+ * `addNodes`/`deleteElements` and completed connections write through
635
+ * and persist, with no adoption step. Mutually exclusive with `nodes`
636
+ * (which wins, with a dev warning). Mode is fixed at mount, per axis:
637
+ * nodes and edges can each be controlled or uncontrolled independently.
638
+ */
639
+ readonly defaultNodes?: readonly NodeType[];
640
+ /**
641
+ * Initial edges for an UNCONTROLLED flow — the edge-axis counterpart of
642
+ * `defaultNodes`: seeds once, flow owns membership, completed
643
+ * connections are kept automatically. Mutually exclusive with `edges`.
644
+ */
645
+ readonly defaultEdges?: readonly EdgeType[];
615
646
  /**
616
647
  * Custom node types to be available in a flow.
617
648
  * Solid Flow matches a node's type to a component in the nodeTypes object.
@@ -866,7 +897,10 @@ type SolidFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
866
897
  * Component-LOCAL state does not survive — signals created inside a
867
898
  * custom node, uncontrolled inputs, scroll positions of inner elements.
868
899
  * Keep state you care about in `node.data`. Never unmounted: selected
869
- * elements, unmeasured nodes, and the node holding DOM focus.
900
+ * elements, unmeasured nodes, and the node holding DOM focus. Elements
901
+ * whose component-local state (or side effects) must keep running while
902
+ * off-screen can opt out entirely with `cullable: false` on the node or
903
+ * edge — they are then exempt from both tiers.
870
904
  *
871
905
  * Independent of this prop, off-viewport elements are always CSS-culled
872
906
  * (`visibility: hidden` + `pointer-events: none`, everything stays
@@ -1065,11 +1099,11 @@ type AllEdgeTypes<TUserEdgeTypes extends EdgeTypes> = TUserEdgeTypes extends Rec
1065
1099
  * ```typescript
1066
1100
  * const initialEdges = [
1067
1101
  * { id: "e1", source: "1", target: "2", type: "labeled", data: { label: "hi" } },
1068
- * ] satisfies EdgesFor<typeof edgeTypes>[];
1102
+ * ] satisfies SolidFlowEdge<typeof edgeTypes>[];
1069
1103
  * ```
1070
1104
  */
1071
- type EdgesFor<TUserEdgeTypes extends EdgeTypes = Record<string, never>> = { [K in keyof AllEdgeTypes<TUserEdgeTypes>]: Edge<EdgeDataOf<AllEdgeTypes<TUserEdgeTypes>[K]>, K & string>; }[keyof AllEdgeTypes<TUserEdgeTypes>];
1072
- type EdgesInput<TUserEdgeTypes extends EdgeTypes> = EdgesFor<TUserEdgeTypes>;
1105
+ type SolidFlowEdge<TUserEdgeTypes extends EdgeTypes = Record<string, never>> = { [K in keyof AllEdgeTypes<TUserEdgeTypes>]: Edge<EdgeDataOf<AllEdgeTypes<TUserEdgeTypes>[K]>, K & string>; }[keyof AllEdgeTypes<TUserEdgeTypes>];
1106
+ type EdgesInput<TUserEdgeTypes extends EdgeTypes> = SolidFlowEdge<TUserEdgeTypes>;
1073
1107
  /**
1074
1108
  * Creates a type-safe reactive store of edges for use in Solid Flow.
1075
1109
  *
@@ -1314,11 +1348,11 @@ type AllNodeTypes<TUserNodeTypes extends NodeTypes> = TUserNodeTypes extends Rec
1314
1348
  * ```typescript
1315
1349
  * const initialNodes = [
1316
1350
  * { id: "1", type: "custom", position: { x: 0, y: 0 }, data: { value: 1 } },
1317
- * ] satisfies NodesFor<typeof nodeTypes>[];
1351
+ * ] satisfies SolidFlowNode<typeof nodeTypes>[];
1318
1352
  * ```
1319
1353
  */
1320
- type NodesFor<TUserNodeTypes extends NodeTypes = Record<string, never>> = { [K in keyof AllNodeTypes<TUserNodeTypes>]: Node<NodeDataOf<AllNodeTypes<TUserNodeTypes>[K]>, K & string>; }[keyof AllNodeTypes<TUserNodeTypes>];
1321
- type NodesInput<TUserNodeTypes extends NodeTypes> = NodesFor<TUserNodeTypes>;
1354
+ type SolidFlowNode<TUserNodeTypes extends NodeTypes = Record<string, never>> = { [K in keyof AllNodeTypes<TUserNodeTypes>]: Node<NodeDataOf<AllNodeTypes<TUserNodeTypes>[K]>, K & string>; }[keyof AllNodeTypes<TUserNodeTypes>];
1355
+ type NodesInput<TUserNodeTypes extends NodeTypes> = SolidFlowNode<TUserNodeTypes>;
1322
1356
  /**
1323
1357
  * Creates a type-safe reactive store of nodes for use in Solid Flow.
1324
1358
  *
@@ -1911,4 +1945,4 @@ declare const getEdgeCenter: typeof getEdgeCenter$1;
1911
1945
  /** Returns the label center and offsets for a bezier edge. */
1912
1946
  declare const getBezierEdgeCenter: typeof getBezierEdgeCenter$1;
1913
1947
  //#endregion
1914
- export { Align, AriaLabelConfig, Background, type BackgroundProps, type BackgroundVariant, BaseEdge, BezierEdge, BezierEdgeInternal, type BezierEdgeProps, BezierPathOptions, Box, type BuiltInEdge, type BuiltInNode, type BuiltInNodeTypes, ColorMode, ColorModeClass, type Connection, ConnectionData, ConnectionLine, ConnectionLineComponentProps, ConnectionLineType, ConnectionMode, type ConnectionsRecord, ControlButton, type ControlLinePosition, type ControlPosition, Controls, type CoordinateExtent, type DefaultEdgeOptions, DefaultNode, DeleteEvents, Dimensions, type Edge, EdgeConnection, EdgeEvents, EdgeLabel, EdgeLabelRenderer, type EdgeMarker, EdgeMarkerType, type EdgeProps, EdgeReconnectAnchor, EdgeReconnectEvents, EdgeRenderer, EdgeToolbar, EdgeToolbarProps, type EdgeTypes, EdgeWrapper, type EdgesFor, type FitBounds, FitBoundsOptions, FitViewOptions, type FlowCommands, type FlowSelection, type FlowState, GetBezierPathParams, type GetMiniMapNodeAttribute, GetSmoothStepPathParams, GetStraightPathParams, GroupNode, Handle, type HandleConnection, InputNode, type InternalNode, IsValidConnection, KeyDefinition, KeyDefinitionObject, KeyModifier, Marker, MarkerDefinition, MarkerType, MiniMap, MiniMapNode, type MiniMapNodeProps, type MiniMapProps, type Node, type NodeConnection, NodeEvents, NodeGraph, type NodeOrigin, type NodeProps, NodeRenderer, NodeResizer, NodeSelection, NodeSelectionEvents, NodeToolbar, NodeToolbarProps, type NodeTypes, NodeWrapper, type NodesFor, OnBeforeDelete, OnBeforeEdgeConnect, OnBeforeReconnect, OnConnect, OnConnectEnd, OnConnectStart, OnConnectStartParams, OnDelete, OnEdgeConnect, OnEdgeCreate, OnError, type OnMove, OnMoveEnd, OnMoveStart, OnReconnect, OnReconnectEnd, OnReconnectStart, OnResize, OnResizeEnd, OnResizeStart, OnSelectionChange, OnSelectionDrag, OutputNode, PanOnScrollMode, Pane, PaneEvents, Panel, type PanelPosition, Position, ProOptions, Rect, ResizeControl, ResizeControlVariant, ResizeDragEvent, ResizeParams, ResizeParamsWithDirection, Selection, SelectionMode, SelectionRect, type SetCenter, SetCenterOptions, type SetViewport, ShortcutModifier, ShortcutModifierDefinition, type ShouldResize, SmoothStepEdge, SmoothStepEdgeInternal, type SmoothStepEdgeProps, SmoothStepPathOptions, SnapGrid, SolidFlow, type SolidFlowInitialProps, type SolidFlowProps, SolidFlowProvider, StepEdge, StepEdgeInternal, type StepEdgeProps, StraightEdge, StraightEdgeInternal, type StraightEdgeProps, Transform, UseSolidFlowReturn, Viewport, ViewportHelperFunctionOptions, ViewportPortal, type XYPosition, XYZPosition, Zoom, addEdge, connectionKey, createEdgeStore, createNodeStore, getBezierEdgeCenter, getBezierPath, getConnectedEdges, getEdgeCenter, getIncomers, getNodesBounds, getOutgoers, getSmoothStepPath, getStraightPath, getViewportForBounds, useColorMode, useConnection, useEdgeId, useEdges, useInternalNode, useNodeConnections, useNodeId, useNodes, useNodesData, useNodesInitialized, useSolidFlow, useUpdateNodeInternals, useViewport, useViewportInitialized };
1948
+ export { Align, AriaLabelConfig, Background, type BackgroundProps, type BackgroundVariant, BaseEdge, BezierEdge, BezierEdgeInternal, type BezierEdgeProps, BezierPathOptions, Box, type BuiltInEdge, type BuiltInNode, type BuiltInNodeTypes, ColorMode, ColorModeClass, type Connection, ConnectionData, ConnectionLine, ConnectionLineComponentProps, ConnectionLineType, ConnectionMode, type ConnectionsRecord, ControlButton, type ControlLinePosition, type ControlPosition, Controls, type CoordinateExtent, type DefaultEdgeOptions, DefaultNode, DeleteEvents, Dimensions, type Edge, EdgeConnection, EdgeEvents, EdgeLabel, EdgeLabelRenderer, type EdgeMarker, EdgeMarkerType, type EdgeProps, EdgeReconnectAnchor, EdgeReconnectEvents, EdgeRenderer, EdgeToolbar, EdgeToolbarProps, type EdgeTypes, EdgeWrapper, type FitBounds, FitBoundsOptions, FitViewOptions, type FlowCommands, type FlowSelection, type FlowState, GetBezierPathParams, type GetMiniMapNodeAttribute, GetSmoothStepPathParams, GetStraightPathParams, GroupNode, Handle, type HandleConnection, InputNode, type InternalNode, IsValidConnection, KeyDefinition, KeyDefinitionObject, KeyModifier, Marker, MarkerDefinition, MarkerType, MiniMap, MiniMapNode, type MiniMapNodeProps, type MiniMapProps, type Node, type NodeConnection, NodeEvents, NodeGraph, type NodeOrigin, type NodeProps, NodeRenderer, NodeResizer, NodeSelection, NodeSelectionEvents, NodeToolbar, NodeToolbarProps, type NodeTypes, NodeWrapper, OnBeforeDelete, OnBeforeEdgeConnect, OnBeforeReconnect, OnConnect, OnConnectEnd, OnConnectStart, OnConnectStartParams, OnDelete, OnEdgeConnect, OnEdgeCreate, OnError, type OnMove, OnMoveEnd, OnMoveStart, OnReconnect, OnReconnectEnd, OnReconnectStart, OnResize, OnResizeEnd, OnResizeStart, OnSelectionChange, OnSelectionDrag, OutputNode, PanOnScrollMode, Pane, PaneEvents, Panel, type PanelPosition, Position, ProOptions, Rect, ResizeControl, ResizeControlVariant, ResizeDragEvent, ResizeParams, ResizeParamsWithDirection, Selection, SelectionMode, SelectionRect, type SetCenter, SetCenterOptions, type SetViewport, ShortcutModifier, ShortcutModifierDefinition, type ShouldResize, SmoothStepEdge, SmoothStepEdgeInternal, type SmoothStepEdgeProps, SmoothStepPathOptions, SnapGrid, SolidFlow, type SolidFlowEdge, type SolidFlowInitialProps, type SolidFlowNode, type SolidFlowProps, SolidFlowProvider, StepEdge, StepEdgeInternal, type StepEdgeProps, StraightEdge, StraightEdgeInternal, type StraightEdgeProps, Transform, UseSolidFlowReturn, Viewport, ViewportHelperFunctionOptions, ViewportPortal, type XYPosition, XYZPosition, Zoom, addEdge, connectionKey, createEdgeStore, createNodeStore, getBezierEdgeCenter, getBezierPath, getConnectedEdges, getEdgeCenter, getIncomers, getNodesBounds, getOutgoers, getSmoothStepPath, getStraightPath, getViewportForBounds, useColorMode, useConnection, useEdgeId, useEdges, useInternalNode, useNodeConnections, useNodeId, useNodes, useNodesData, useNodesInitialized, useSolidFlow, useUpdateNodeInternals, useViewport, useViewportInitialized };
@@ -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,
@@ -451,8 +451,6 @@ const A11yDescriptions = () => {
451
451
  }, cullingViewport);
452
452
  }, getDefaultFlowStateProps = () => ({
453
453
  id: "1",
454
- nodes: [],
455
- edges: [],
456
454
  nodeOrigin: [0, 0],
457
455
  nodeExtent: infiniteExtent,
458
456
  defaultEdgeOptions: {},
@@ -753,7 +751,9 @@ const createLayoutedEdges = (source) => {
753
751
  zoom: 1
754
752
  };
755
753
  }, createFlowState = (props, injections = {}) => {
756
- let _props = merge(getDefaultFlowStateProps(), props), initialNodeTypes = injections.initialNodeTypes ?? {}, initialEdgeTypes = injections.initialEdgeTypes ?? {}, prefersDark = injections.prefersDark ?? (() => _props.colorModeSSR === "dark"), [config, setConfig] = createSignal(_props), [ariaLabelConfig, setAriaLabelConfig] = createSignal(() => mergeAriaLabelConfig(config().ariaLabelConfig)), [ariaLiveMessage, setAriaLiveMessage] = createSignal(() => config().ariaLiveMessage), [clickConnectStartHandle, setClickConnectStartHandle] = createSignal(void 0), [connection, setConnection] = createSignal(initialConnection), [domNode, setDomNode] = createSignal(null), [dragging, setDragging] = createSignal(!1), [elementsSelectable, setElementsSelectable] = createSignal(() => config().elementsSelectable), [height, setHeight] = createSignal(() => config().height), [minZoom, _setMinZoom] = createSignal(() => config().minZoom), [maxZoom, _setMaxZoom] = createSignal(() => config().maxZoom), [nodesConnectable, setNodesConnectable] = createSignal(() => config().nodesDraggable), [nodesDraggable, setNodesDraggable] = createSignal(() => config().nodesDraggable), [panZoom, setPanZoom] = createSignal(null), [selectionRect, setSelectionRect] = createSignal(), [selectionRectMode, setSelectionRectMode] = createSignal(), [snapGrid, setSnapGrid] = createSignal(() => config().snapGrid), [translateExtent, _setTranslateExtent] = createSignal(() => config().translateExtent ?? infiniteExtent), [width, setWidth] = createSignal(() => config().width), [selectionKeyPressed, setSelectionKeyPressed] = createSignal(!1), [multiselectionKeyPressed, setMultiselectionKeyPressed] = createSignal(!1), [deleteKeyPressed, setDeleteKeyPressed] = createSignal(!1), [panActivationKeyPressed, setPanActivationKeyPressed] = createSignal(!1), [zoomActivationKeyPressed, setZoomActivationKeyPressed] = createSignal(!1), [nodesStore, setNodesStore] = createStore(_props.nodes), [edgesStore, setEdgesStore] = createStore(_props.edges), [measurementsStore, setMeasurementsStore] = createStore({}), internalNodes = createInternalNodes({
754
+ let _props = merge(getDefaultFlowStateProps(), props), initialNodeTypes = injections.initialNodeTypes ?? {}, initialEdgeTypes = injections.initialEdgeTypes ?? {}, prefersDark = injections.prefersDark ?? (() => _props.colorModeSSR === "dark"), [config, setConfig] = createSignal(_props), [ariaLabelConfig, setAriaLabelConfig] = createSignal(() => mergeAriaLabelConfig(config().ariaLabelConfig)), [ariaLiveMessage, setAriaLiveMessage] = createSignal(() => config().ariaLiveMessage), [clickConnectStartHandle, setClickConnectStartHandle] = createSignal(void 0), [connection, setConnection] = createSignal(initialConnection), [domNode, setDomNode] = createSignal(null), [dragging, setDragging] = createSignal(!1), [elementsSelectable, setElementsSelectable] = createSignal(() => config().elementsSelectable), [height, setHeight] = createSignal(() => config().height), [minZoom, _setMinZoom] = createSignal(() => config().minZoom), [maxZoom, _setMaxZoom] = createSignal(() => config().maxZoom), [nodesConnectable, setNodesConnectable] = createSignal(() => config().nodesDraggable), [nodesDraggable, setNodesDraggable] = createSignal(() => config().nodesDraggable), [panZoom, setPanZoom] = createSignal(null), [selectionRect, setSelectionRect] = createSignal(), [selectionRectMode, setSelectionRectMode] = createSignal(), [snapGrid, setSnapGrid] = createSignal(() => config().snapGrid), [translateExtent, _setTranslateExtent] = createSignal(() => config().translateExtent ?? infiniteExtent), [width, setWidth] = createSignal(() => config().width), [selectionKeyPressed, setSelectionKeyPressed] = createSignal(!1), [multiselectionKeyPressed, setMultiselectionKeyPressed] = createSignal(!1), [deleteKeyPressed, setDeleteKeyPressed] = createSignal(!1), [panActivationKeyPressed, setPanActivationKeyPressed] = createSignal(!1), [zoomActivationKeyPressed, setZoomActivationKeyPressed] = createSignal(!1);
755
+ props.nodes !== void 0 && props.defaultNodes, props.edges !== void 0 && props.defaultEdges;
756
+ let [nodesStore, setNodesStore] = createStore(props.nodes ?? [...props.defaultNodes ?? []]), [edgesStore, setEdgesStore] = createStore(props.edges ?? [...props.defaultEdges ?? []]), nodeSeedAdopted = props.nodes !== void 0 || props.defaultNodes !== void 0, edgeSeedAdopted = props.edges !== void 0 || props.defaultEdges !== void 0, [measurementsStore, setMeasurementsStore] = createStore({}), internalNodes = createInternalNodes({
757
757
  get nodes() {
758
758
  return nodesStore;
759
759
  },
@@ -775,16 +775,21 @@ const createLayoutedEdges = (source) => {
775
775
  }), nodeLookup = new RecordMapFacade(internalNodes), initialViewport = getInitialViewport(_props.fitView, _props.initialViewport, _props.width ?? 0, _props.height ?? 0, nodeLookup), [viewportStore, setViewportStore] = createStore(_props.viewport ?? initialViewport);
776
776
  createEffect(() => {
777
777
  let next = config().nodes;
778
- for (let node of next);
778
+ if (next) for (let node of next);
779
779
  return { next };
780
780
  }, ({ next }) => {
781
- setNodesStore(() => next);
781
+ next && (nodeSeedAdopted = !0, setNodesStore(() => next));
782
782
  }, { defer: !0 }), createEffect(() => {
783
783
  let next = config().edges;
784
- for (let edge of next);
784
+ if (next) for (let edge of next);
785
785
  return { next };
786
786
  }, ({ next }) => {
787
- setEdgesStore(() => next);
787
+ next && (edgeSeedAdopted = !0, setEdgesStore(() => next));
788
+ }, { defer: !0 }), createEffect(() => ({
789
+ nodes: config().defaultNodes,
790
+ edges: config().defaultEdges
791
+ }), ({ nodes: defaultNodes, edges: defaultEdges }) => {
792
+ defaultNodes && !nodeSeedAdopted && config().nodes === void 0 && (nodeSeedAdopted = !0, setNodesStore(() => [...defaultNodes])), defaultEdges && !edgeSeedAdopted && config().edges === void 0 && (edgeSeedAdopted = !0, setEdgesStore(() => [...defaultEdges]));
788
793
  }, { defer: !0 }), createEffect(() => config().viewport, (next) => {
789
794
  next && setViewportStore(() => next);
790
795
  }, { defer: !0 });
@@ -3278,6 +3283,34 @@ function matchesKey(event, keyDef) {
3278
3283
  function matchesKeyArray(event, keyDefs) {
3279
3284
  return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
3280
3285
  }
3286
+ const MODIFIER_FLAG = {
3287
+ alt: "altKey",
3288
+ control: "ctrlKey",
3289
+ ctrl: "ctrlKey",
3290
+ meta: "metaKey",
3291
+ shift: "shiftKey"
3292
+ };
3293
+ /**
3294
+ * Whether an event's modifier flags PROVE this key definition cannot be held
3295
+ * right now: the key itself is a modifier whose flag is off, or one of its
3296
+ * required modifiers is off. Definitions built on non-modifier keys are never
3297
+ * contradicted (their state isn't derivable from flags).
3298
+ */
3299
+ function isContradicted(flags, keyDef) {
3300
+ let keyFlag = MODIFIER_FLAG[getKeyString(keyDef).toLowerCase()];
3301
+ if (keyFlag && !flags[keyFlag]) return !0;
3302
+ let modifiers = getModifier(keyDef);
3303
+ return (Array.isArray(modifiers) ? modifiers.flatMap((mod) => mod) : [modifiers]).some((mod) => {
3304
+ let modFlag = MODIFIER_FLAG[mod.toLowerCase()];
3305
+ return !!modFlag && !flags[modFlag];
3306
+ });
3307
+ }
3308
+ /** True only when EVERY definition for this key state is contradicted. */
3309
+ function allContradicted(flags, keyDefs) {
3310
+ if (!keyDefs) return !1;
3311
+ let keys = Array.isArray(keyDefs) ? keyDefs : [keyDefs];
3312
+ return keys.length > 0 && keys.every((keyDef) => isContradicted(flags, keyDef));
3313
+ }
3281
3314
  const KeyHandler = (props) => {
3282
3315
  let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
3283
3316
  get selectionKey() {
@@ -3297,6 +3330,20 @@ const KeyHandler = (props) => {
3297
3330
  }
3298
3331
  }, resetKeysAndSelection = () => {
3299
3332
  actions.setSelectionRect(void 0), actions.setSelectionKeyPressed(!1), actions.setMultiselectionKeyPressed(!1), actions.setDeleteKeyPressed(!1), actions.setPanActivationKeyPressed(!1), actions.setZoomActivationKeyPressed(!1);
3333
+ }, reconcileModifiers = (event) => {
3334
+ let changed = !1;
3335
+ 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();
3336
+ }, cancelPointerGestures = () => {
3337
+ let release = (Ctor, type) => {
3338
+ try {
3339
+ window.dispatchEvent(new Ctor(type, { view: window }));
3340
+ } catch {
3341
+ window.dispatchEvent(new Ctor(type));
3342
+ }
3343
+ };
3344
+ release(MouseEvent, "mouseup"), typeof PointerEvent < "u" && release(PointerEvent, "pointerup");
3345
+ }, handleWindowBlur = () => {
3346
+ resetKeysAndSelection(), cancelPointerGestures();
3300
3347
  }, handleDelete = async () => {
3301
3348
  let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
3302
3349
  nodes: selectedNodes,
@@ -3307,16 +3354,22 @@ const KeyHandler = (props) => {
3307
3354
  edges: deletedEdges
3308
3355
  });
3309
3356
  };
3310
- return isServer || createEventListenerMap(window, {
3357
+ return isServer || (createEventListenerMap(window, {
3311
3358
  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();
3359
+ 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
3360
  },
3314
3361
  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();
3362
+ 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
3363
  },
3317
- blur: resetKeysAndSelection,
3364
+ blur: handleWindowBlur,
3318
3365
  contextmenu: resetKeysAndSelection
3319
- }), null;
3366
+ }), createEventListenerMap(window, {
3367
+ pointerdown: reconcileModifiers,
3368
+ wheel: reconcileModifiers
3369
+ }, {
3370
+ capture: !0,
3371
+ passive: !0
3372
+ })), null;
3320
3373
  };
3321
3374
  //#endregion
3322
3375
  //#region src/components/SolidFlow.tsx
@@ -3349,7 +3402,7 @@ const SolidFlow = (props) => {
3349
3402
  zoomOnPinch: !0,
3350
3403
  zoomOnDoubleClick: !0,
3351
3404
  zoomOnScroll: !0
3352
- }, props), htmlProps = omit(_props, "nodes", "edges", "nodeTypes", "edgeTypes", "width", "height", "fitView", "fitViewOptions", "nodeOrigin", "nodeDragThreshold", "paneClickDistance", "nodeClickDistance", "minZoom", "maxZoom", "zIndexMode", "initialViewport", "viewport", "translateExtent", "nodeExtent", "selectionKey", "panActivationKey", "deleteKey", "multiSelectionKey", "zoomActivationKey", "panOnDrag", "panOnScroll", "panOnScrollMode", "panOnScrollSpeed", "selectionOnDrag", "selectNodesOnDrag", "preventScrolling", "zoomOnScroll", "zoomOnDoubleClick", "zoomOnPinch", "onlyRenderVisibleElements", "autoPanOnConnect", "autoPanOnNodeDrag", "autoPanOnNodeFocus", "autoPanOnSelection", "autoPanSpeed", "connectionRadius", "connectionMode", "connectionLineType", "connectionLineComponent", "connectionLineStyle", "connectionLineContainerStyle", "connectionDragThreshold", "isValidConnection", "clickConnect", "reconnectRadius", "selectionMode", "elementsSelectable", "nodesDraggable", "nodesConnectable", "nodesFocusable", "edgesFocusable", "disableKeyboardA11y", "ariaLabelConfig", "ariaLiveMessage", "colorMode", "colorModeSSR", "class", "style", "snapGrid", "defaultMarkerColor", "defaultEdgeOptions", "elevateNodesOnSelect", "elevateEdgesOnSelect", "noDragClass", "noPanClass", "noWheelClass", "attributionPosition", "proOptions", "onInit", "onMoveStart", "onMove", "onMoveEnd", "onFlowError", "onNodeClick", "onNodeContextMenu", "onNodeDrag", "onNodeDragStart", "onNodeDragStop", "onNodePointerEnter", "onNodePointerMove", "onNodePointerLeave", "onEdgeClick", "onEdgeContextMenu", "onEdgePointerEnter", "onEdgePointerLeave", "onPaneClick", "onPaneContextMenu", "onSelectionChange", "onSelectionClick", "onSelectionContextMenu", "onSelectionDrag", "onSelectionDragStart", "onSelectionDragStop", "onSelectionStart", "onSelectionEnd", "onConnect", "onConnectStart", "onConnectEnd", "onReconnect", "onReconnectStart", "onReconnectEnd", "onClickConnectStart", "onClickConnectEnd", "onBeforeConnect", "onBeforeReconnect", "onDelete", "onBeforeDelete", "deleteKeyCode", "selectionKeyCode", "panActivationKeyCode", "multiSelectionKeyCode", "zoomActivationKeyCode", "children"), TypedSolidFlowContext = SolidFlowContext, solidFlow = useContext(TypedSolidFlowContext) ?? createSolidFlow(_props), { store, actions } = solidFlow;
3405
+ }, props), htmlProps = omit(_props, "nodes", "edges", "defaultNodes", "defaultEdges", "nodeTypes", "edgeTypes", "width", "height", "fitView", "fitViewOptions", "nodeOrigin", "nodeDragThreshold", "paneClickDistance", "nodeClickDistance", "minZoom", "maxZoom", "zIndexMode", "initialViewport", "viewport", "translateExtent", "nodeExtent", "selectionKey", "panActivationKey", "deleteKey", "multiSelectionKey", "zoomActivationKey", "panOnDrag", "panOnScroll", "panOnScrollMode", "panOnScrollSpeed", "selectionOnDrag", "selectNodesOnDrag", "preventScrolling", "zoomOnScroll", "zoomOnDoubleClick", "zoomOnPinch", "onlyRenderVisibleElements", "autoPanOnConnect", "autoPanOnNodeDrag", "autoPanOnNodeFocus", "autoPanOnSelection", "autoPanSpeed", "connectionRadius", "connectionMode", "connectionLineType", "connectionLineComponent", "connectionLineStyle", "connectionLineContainerStyle", "connectionDragThreshold", "isValidConnection", "clickConnect", "reconnectRadius", "selectionMode", "elementsSelectable", "nodesDraggable", "nodesConnectable", "nodesFocusable", "edgesFocusable", "disableKeyboardA11y", "ariaLabelConfig", "ariaLiveMessage", "colorMode", "colorModeSSR", "class", "style", "snapGrid", "defaultMarkerColor", "defaultEdgeOptions", "elevateNodesOnSelect", "elevateEdgesOnSelect", "noDragClass", "noPanClass", "noWheelClass", "attributionPosition", "proOptions", "onInit", "onMoveStart", "onMove", "onMoveEnd", "onFlowError", "onNodeClick", "onNodeContextMenu", "onNodeDrag", "onNodeDragStart", "onNodeDragStop", "onNodePointerEnter", "onNodePointerMove", "onNodePointerLeave", "onEdgeClick", "onEdgeContextMenu", "onEdgePointerEnter", "onEdgePointerLeave", "onPaneClick", "onPaneContextMenu", "onSelectionChange", "onSelectionClick", "onSelectionContextMenu", "onSelectionDrag", "onSelectionDragStart", "onSelectionDragStop", "onSelectionStart", "onSelectionEnd", "onConnect", "onConnectStart", "onConnectEnd", "onReconnect", "onReconnectStart", "onReconnectEnd", "onClickConnectStart", "onClickConnectEnd", "onBeforeConnect", "onBeforeReconnect", "onDelete", "onBeforeDelete", "deleteKeyCode", "selectionKeyCode", "panActivationKeyCode", "multiSelectionKeyCode", "zoomActivationKeyCode", "children"), TypedSolidFlowContext = SolidFlowContext, solidFlow = useContext(TypedSolidFlowContext) ?? createSolidFlow(_props), { store, actions } = solidFlow;
3353
3406
  onSettled(() => (actions.applyInitialFitView(_props.fitView), actions.setConfig(_props), actions.setDomNode(domNode), () => {
3354
3407
  actions.reset();
3355
3408
  })), createEffect(() => domNodeRef(), (el) => {
@@ -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,
@@ -271,8 +271,6 @@ const getEdgeId = (connection) => {
271
271
  }, cullingViewport);
272
272
  }, getDefaultFlowStateProps = () => ({
273
273
  id: "1",
274
- nodes: [],
275
- edges: [],
276
274
  nodeOrigin: [0, 0],
277
275
  nodeExtent: infiniteExtent,
278
276
  defaultEdgeOptions: {},
@@ -573,7 +571,9 @@ const createLayoutedEdges = (source) => {
573
571
  zoom: 1
574
572
  };
575
573
  }, createFlowState = (props, injections = {}) => {
576
- let _props = merge(getDefaultFlowStateProps(), props), initialNodeTypes = injections.initialNodeTypes ?? {}, initialEdgeTypes = injections.initialEdgeTypes ?? {}, prefersDark = injections.prefersDark ?? (() => _props.colorModeSSR === "dark"), [config, setConfig] = createSignal(_props), [ariaLabelConfig, setAriaLabelConfig] = createSignal(() => mergeAriaLabelConfig(config().ariaLabelConfig)), [ariaLiveMessage, setAriaLiveMessage] = createSignal(() => config().ariaLiveMessage), [clickConnectStartHandle, setClickConnectStartHandle] = createSignal(void 0), [connection, setConnection] = createSignal(initialConnection), [domNode, setDomNode] = createSignal(null), [dragging, setDragging] = createSignal(!1), [elementsSelectable, setElementsSelectable] = createSignal(() => config().elementsSelectable), [height, setHeight] = createSignal(() => config().height), [minZoom, _setMinZoom] = createSignal(() => config().minZoom), [maxZoom, _setMaxZoom] = createSignal(() => config().maxZoom), [nodesConnectable, setNodesConnectable] = createSignal(() => config().nodesDraggable), [nodesDraggable, setNodesDraggable] = createSignal(() => config().nodesDraggable), [panZoom, setPanZoom] = createSignal(null), [selectionRect, setSelectionRect] = createSignal(), [selectionRectMode, setSelectionRectMode] = createSignal(), [snapGrid, setSnapGrid] = createSignal(() => config().snapGrid), [translateExtent, _setTranslateExtent] = createSignal(() => config().translateExtent ?? infiniteExtent), [width, setWidth] = createSignal(() => config().width), [selectionKeyPressed, setSelectionKeyPressed] = createSignal(!1), [multiselectionKeyPressed, setMultiselectionKeyPressed] = createSignal(!1), [deleteKeyPressed, setDeleteKeyPressed] = createSignal(!1), [panActivationKeyPressed, setPanActivationKeyPressed] = createSignal(!1), [zoomActivationKeyPressed, setZoomActivationKeyPressed] = createSignal(!1), [nodesStore, setNodesStore] = createStore(_props.nodes), [edgesStore, setEdgesStore] = createStore(_props.edges), [measurementsStore, setMeasurementsStore] = createStore({}), internalNodes = createInternalNodes({
574
+ let _props = merge(getDefaultFlowStateProps(), props), initialNodeTypes = injections.initialNodeTypes ?? {}, initialEdgeTypes = injections.initialEdgeTypes ?? {}, prefersDark = injections.prefersDark ?? (() => _props.colorModeSSR === "dark"), [config, setConfig] = createSignal(_props), [ariaLabelConfig, setAriaLabelConfig] = createSignal(() => mergeAriaLabelConfig(config().ariaLabelConfig)), [ariaLiveMessage, setAriaLiveMessage] = createSignal(() => config().ariaLiveMessage), [clickConnectStartHandle, setClickConnectStartHandle] = createSignal(void 0), [connection, setConnection] = createSignal(initialConnection), [domNode, setDomNode] = createSignal(null), [dragging, setDragging] = createSignal(!1), [elementsSelectable, setElementsSelectable] = createSignal(() => config().elementsSelectable), [height, setHeight] = createSignal(() => config().height), [minZoom, _setMinZoom] = createSignal(() => config().minZoom), [maxZoom, _setMaxZoom] = createSignal(() => config().maxZoom), [nodesConnectable, setNodesConnectable] = createSignal(() => config().nodesDraggable), [nodesDraggable, setNodesDraggable] = createSignal(() => config().nodesDraggable), [panZoom, setPanZoom] = createSignal(null), [selectionRect, setSelectionRect] = createSignal(), [selectionRectMode, setSelectionRectMode] = createSignal(), [snapGrid, setSnapGrid] = createSignal(() => config().snapGrid), [translateExtent, _setTranslateExtent] = createSignal(() => config().translateExtent ?? infiniteExtent), [width, setWidth] = createSignal(() => config().width), [selectionKeyPressed, setSelectionKeyPressed] = createSignal(!1), [multiselectionKeyPressed, setMultiselectionKeyPressed] = createSignal(!1), [deleteKeyPressed, setDeleteKeyPressed] = createSignal(!1), [panActivationKeyPressed, setPanActivationKeyPressed] = createSignal(!1), [zoomActivationKeyPressed, setZoomActivationKeyPressed] = createSignal(!1);
575
+ props.nodes !== void 0 && props.defaultNodes, props.edges !== void 0 && props.defaultEdges;
576
+ let [nodesStore, setNodesStore] = createStore(props.nodes ?? [...props.defaultNodes ?? []]), [edgesStore, setEdgesStore] = createStore(props.edges ?? [...props.defaultEdges ?? []]), nodeSeedAdopted = props.nodes !== void 0 || props.defaultNodes !== void 0, edgeSeedAdopted = props.edges !== void 0 || props.defaultEdges !== void 0, [measurementsStore, setMeasurementsStore] = createStore({}), internalNodes = createInternalNodes({
577
577
  get nodes() {
578
578
  return nodesStore;
579
579
  },
@@ -595,16 +595,21 @@ const createLayoutedEdges = (source) => {
595
595
  }), nodeLookup = new RecordMapFacade(internalNodes), initialViewport = getInitialViewport(_props.fitView, _props.initialViewport, _props.width ?? 0, _props.height ?? 0, nodeLookup), [viewportStore, setViewportStore] = createStore(_props.viewport ?? initialViewport);
596
596
  createEffect(() => {
597
597
  let next = config().nodes;
598
- for (let node of next);
598
+ if (next) for (let node of next);
599
599
  return { next };
600
600
  }, ({ next }) => {
601
- setNodesStore(() => next);
601
+ next && (nodeSeedAdopted = !0, setNodesStore(() => next));
602
602
  }, { defer: !0 }), createEffect(() => {
603
603
  let next = config().edges;
604
- for (let edge of next);
604
+ if (next) for (let edge of next);
605
605
  return { next };
606
606
  }, ({ next }) => {
607
- setEdgesStore(() => next);
607
+ next && (edgeSeedAdopted = !0, setEdgesStore(() => next));
608
+ }, { defer: !0 }), createEffect(() => ({
609
+ nodes: config().defaultNodes,
610
+ edges: config().defaultEdges
611
+ }), ({ nodes: defaultNodes, edges: defaultEdges }) => {
612
+ defaultNodes && !nodeSeedAdopted && config().nodes === void 0 && (nodeSeedAdopted = !0, setNodesStore(() => [...defaultNodes])), defaultEdges && !edgeSeedAdopted && config().edges === void 0 && (edgeSeedAdopted = !0, setEdgesStore(() => [...defaultEdges]));
608
613
  }, { defer: !0 }), createEffect(() => config().viewport, (next) => {
609
614
  next && setViewportStore(() => next);
610
615
  }, { defer: !0 });
@@ -2385,6 +2390,34 @@ function matchesKey(event, keyDef) {
2385
2390
  function matchesKeyArray(event, keyDefs) {
2386
2391
  return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
2387
2392
  }
2393
+ const MODIFIER_FLAG = {
2394
+ alt: "altKey",
2395
+ control: "ctrlKey",
2396
+ ctrl: "ctrlKey",
2397
+ meta: "metaKey",
2398
+ shift: "shiftKey"
2399
+ };
2400
+ /**
2401
+ * Whether an event's modifier flags PROVE this key definition cannot be held
2402
+ * right now: the key itself is a modifier whose flag is off, or one of its
2403
+ * required modifiers is off. Definitions built on non-modifier keys are never
2404
+ * contradicted (their state isn't derivable from flags).
2405
+ */
2406
+ function isContradicted(flags, keyDef) {
2407
+ let keyFlag = MODIFIER_FLAG[getKeyString(keyDef).toLowerCase()];
2408
+ if (keyFlag && !flags[keyFlag]) return !0;
2409
+ let modifiers = getModifier(keyDef);
2410
+ return (Array.isArray(modifiers) ? modifiers.flatMap((mod) => mod) : [modifiers]).some((mod) => {
2411
+ let modFlag = MODIFIER_FLAG[mod.toLowerCase()];
2412
+ return !!modFlag && !flags[modFlag];
2413
+ });
2414
+ }
2415
+ /** True only when EVERY definition for this key state is contradicted. */
2416
+ function allContradicted(flags, keyDefs) {
2417
+ if (!keyDefs) return !1;
2418
+ let keys = Array.isArray(keyDefs) ? keyDefs : [keyDefs];
2419
+ return keys.length > 0 && keys.every((keyDef) => isContradicted(flags, keyDef));
2420
+ }
2388
2421
  const KeyHandler = (props) => {
2389
2422
  let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
2390
2423
  get selectionKey() {
@@ -2404,6 +2437,20 @@ const KeyHandler = (props) => {
2404
2437
  }
2405
2438
  }, resetKeysAndSelection = () => {
2406
2439
  actions.setSelectionRect(void 0), actions.setSelectionKeyPressed(!1), actions.setMultiselectionKeyPressed(!1), actions.setDeleteKeyPressed(!1), actions.setPanActivationKeyPressed(!1), actions.setZoomActivationKeyPressed(!1);
2440
+ }, reconcileModifiers = (event) => {
2441
+ let changed = !1;
2442
+ 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();
2443
+ }, cancelPointerGestures = () => {
2444
+ let release = (Ctor, type) => {
2445
+ try {
2446
+ window.dispatchEvent(new Ctor(type, { view: window }));
2447
+ } catch {
2448
+ window.dispatchEvent(new Ctor(type));
2449
+ }
2450
+ };
2451
+ release(MouseEvent, "mouseup"), typeof PointerEvent < "u" && release(PointerEvent, "pointerup");
2452
+ }, handleWindowBlur = () => {
2453
+ resetKeysAndSelection(), cancelPointerGestures();
2407
2454
  }, handleDelete = async () => {
2408
2455
  let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
2409
2456
  nodes: selectedNodes,
@@ -2414,16 +2461,22 @@ const KeyHandler = (props) => {
2414
2461
  edges: deletedEdges
2415
2462
  });
2416
2463
  };
2417
- return isServer || createEventListenerMap(window, {
2464
+ return isServer || (createEventListenerMap(window, {
2418
2465
  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();
2466
+ 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
2467
  },
2421
2468
  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();
2469
+ 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
2470
  },
2424
- blur: resetKeysAndSelection,
2471
+ blur: handleWindowBlur,
2425
2472
  contextmenu: resetKeysAndSelection
2426
- }), null;
2473
+ }), createEventListenerMap(window, {
2474
+ pointerdown: reconcileModifiers,
2475
+ wheel: reconcileModifiers
2476
+ }, {
2477
+ capture: !0,
2478
+ passive: !0
2479
+ })), null;
2427
2480
  }, SolidFlow = (props) => {
2428
2481
  let [domNodeRef, setDomNodeRef] = createSignal(), domNode, _props = merge({
2429
2482
  ...getDefaultFlowStateProps(),
@@ -2451,7 +2504,7 @@ const KeyHandler = (props) => {
2451
2504
  zoomOnPinch: !0,
2452
2505
  zoomOnDoubleClick: !0,
2453
2506
  zoomOnScroll: !0
2454
- }, props), htmlProps = omit(_props, "nodes", "edges", "nodeTypes", "edgeTypes", "width", "height", "fitView", "fitViewOptions", "nodeOrigin", "nodeDragThreshold", "paneClickDistance", "nodeClickDistance", "minZoom", "maxZoom", "zIndexMode", "initialViewport", "viewport", "translateExtent", "nodeExtent", "selectionKey", "panActivationKey", "deleteKey", "multiSelectionKey", "zoomActivationKey", "panOnDrag", "panOnScroll", "panOnScrollMode", "panOnScrollSpeed", "selectionOnDrag", "selectNodesOnDrag", "preventScrolling", "zoomOnScroll", "zoomOnDoubleClick", "zoomOnPinch", "onlyRenderVisibleElements", "autoPanOnConnect", "autoPanOnNodeDrag", "autoPanOnNodeFocus", "autoPanOnSelection", "autoPanSpeed", "connectionRadius", "connectionMode", "connectionLineType", "connectionLineComponent", "connectionLineStyle", "connectionLineContainerStyle", "connectionDragThreshold", "isValidConnection", "clickConnect", "reconnectRadius", "selectionMode", "elementsSelectable", "nodesDraggable", "nodesConnectable", "nodesFocusable", "edgesFocusable", "disableKeyboardA11y", "ariaLabelConfig", "ariaLiveMessage", "colorMode", "colorModeSSR", "class", "style", "snapGrid", "defaultMarkerColor", "defaultEdgeOptions", "elevateNodesOnSelect", "elevateEdgesOnSelect", "noDragClass", "noPanClass", "noWheelClass", "attributionPosition", "proOptions", "onInit", "onMoveStart", "onMove", "onMoveEnd", "onFlowError", "onNodeClick", "onNodeContextMenu", "onNodeDrag", "onNodeDragStart", "onNodeDragStop", "onNodePointerEnter", "onNodePointerMove", "onNodePointerLeave", "onEdgeClick", "onEdgeContextMenu", "onEdgePointerEnter", "onEdgePointerLeave", "onPaneClick", "onPaneContextMenu", "onSelectionChange", "onSelectionClick", "onSelectionContextMenu", "onSelectionDrag", "onSelectionDragStart", "onSelectionDragStop", "onSelectionStart", "onSelectionEnd", "onConnect", "onConnectStart", "onConnectEnd", "onReconnect", "onReconnectStart", "onReconnectEnd", "onClickConnectStart", "onClickConnectEnd", "onBeforeConnect", "onBeforeReconnect", "onDelete", "onBeforeDelete", "deleteKeyCode", "selectionKeyCode", "panActivationKeyCode", "multiSelectionKeyCode", "zoomActivationKeyCode", "children"), TypedSolidFlowContext = SolidFlowContext, solidFlow = useContext(TypedSolidFlowContext) ?? createSolidFlow(_props), { store, actions } = solidFlow;
2507
+ }, props), htmlProps = omit(_props, "nodes", "edges", "defaultNodes", "defaultEdges", "nodeTypes", "edgeTypes", "width", "height", "fitView", "fitViewOptions", "nodeOrigin", "nodeDragThreshold", "paneClickDistance", "nodeClickDistance", "minZoom", "maxZoom", "zIndexMode", "initialViewport", "viewport", "translateExtent", "nodeExtent", "selectionKey", "panActivationKey", "deleteKey", "multiSelectionKey", "zoomActivationKey", "panOnDrag", "panOnScroll", "panOnScrollMode", "panOnScrollSpeed", "selectionOnDrag", "selectNodesOnDrag", "preventScrolling", "zoomOnScroll", "zoomOnDoubleClick", "zoomOnPinch", "onlyRenderVisibleElements", "autoPanOnConnect", "autoPanOnNodeDrag", "autoPanOnNodeFocus", "autoPanOnSelection", "autoPanSpeed", "connectionRadius", "connectionMode", "connectionLineType", "connectionLineComponent", "connectionLineStyle", "connectionLineContainerStyle", "connectionDragThreshold", "isValidConnection", "clickConnect", "reconnectRadius", "selectionMode", "elementsSelectable", "nodesDraggable", "nodesConnectable", "nodesFocusable", "edgesFocusable", "disableKeyboardA11y", "ariaLabelConfig", "ariaLiveMessage", "colorMode", "colorModeSSR", "class", "style", "snapGrid", "defaultMarkerColor", "defaultEdgeOptions", "elevateNodesOnSelect", "elevateEdgesOnSelect", "noDragClass", "noPanClass", "noWheelClass", "attributionPosition", "proOptions", "onInit", "onMoveStart", "onMove", "onMoveEnd", "onFlowError", "onNodeClick", "onNodeContextMenu", "onNodeDrag", "onNodeDragStart", "onNodeDragStop", "onNodePointerEnter", "onNodePointerMove", "onNodePointerLeave", "onEdgeClick", "onEdgeContextMenu", "onEdgePointerEnter", "onEdgePointerLeave", "onPaneClick", "onPaneContextMenu", "onSelectionChange", "onSelectionClick", "onSelectionContextMenu", "onSelectionDrag", "onSelectionDragStart", "onSelectionDragStop", "onSelectionStart", "onSelectionEnd", "onConnect", "onConnectStart", "onConnectEnd", "onReconnect", "onReconnectStart", "onReconnectEnd", "onClickConnectStart", "onClickConnectEnd", "onBeforeConnect", "onBeforeReconnect", "onDelete", "onBeforeDelete", "deleteKeyCode", "selectionKeyCode", "panActivationKeyCode", "multiSelectionKeyCode", "zoomActivationKeyCode", "children"), TypedSolidFlowContext = SolidFlowContext, solidFlow = useContext(TypedSolidFlowContext) ?? createSolidFlow(_props), { store, actions } = solidFlow;
2455
2508
  onSettled(() => (actions.applyInitialFitView(_props.fitView), actions.setConfig(_props), actions.setDomNode(domNode), () => {
2456
2509
  actions.reset();
2457
2510
  })), createEffect(() => domNodeRef(), (el) => {
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": "1.0.0-next.6",
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
  },
@@ -98,8 +99,9 @@
98
99
  },
99
100
  "devDependencies": {
100
101
  "@changesets/cli": "2.31.1",
101
- "@dagrejs/dagre": "^1.1.5",
102
+ "@dagrejs/dagre": "3.1.1",
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",