@liteforge/flow
v4.0.0
Published
Signals-based node editor — foundation for pipeline and visual programming editors.
Downloads
873
Maintainers
Readme
@liteforge/flow
A fully-featured node editor for the LiteForge framework. Signals-based reactivity, zero Virtual DOM, zero external runtime dependencies.
What it is
@liteforge/flow is a node-graph library for building visual editors, pipeline builders, workflow designers, and any other application that represents relationships as connected nodes. It is the LiteForge equivalent of ReactFlow — same concepts, same fully-controlled model, but built on LiteForge signals instead of React state. The result is fine-grained DOM updates: only the parts of the graph that actually changed are touched on each interaction.
The library is fully controlled: you own the nodes and edges arrays. The canvas tells you what the user wants to change via onNodesChange / onEdgesChange, and you decide whether to accept it. This makes undo/redo, validation, and server sync straightforward.
Features
Graph model
- ✓ Fully-controlled graph model — you own the state
- ✓ Signals-based reactivity — zero Virtual DOM, fine-grained DOM updates
- ✓ Custom node types (plain DOM renderers, no framework lock-in)
- ✓ Custom edge types (bezier, step, straight, or your own path function)
- ✓ Node groups / parent–child nesting (
parentId) - ✓ Zero external runtime dependencies
Interaction
- ✓ Pan & zoom — mouse wheel, trackpad, touch pinch, middle-mouse drag
- ✓ Node drag with optional snap-to-grid
- ✓ Multi-node group drag (drag selection as a unit)
- ✓ Marquee selection (drag canvas to select a region)
- ✓ Keyboard shortcuts — Delete / Backspace to remove selected elements
- ✓ Keyboard accessibility — WCAG 2.1 AA, roving tabindex, ARIA roles
- ✓ Edge reconnect — drag either endpoint to a new handle
Edges
- ✓ Animated edges (CSS stroke-dashoffset, GPU-composited)
- ✓ Edge labels
- ✓ Arrow markers (open and filled)
- ✓ Connection validation — built-in self-loop guard + composable helpers
- ✓ Ghost edge preview turns red when connection is invalid
UI components
- ✓ MiniMap
- ✓ Controls (zoom in / zoom out / fit view)
- ✓ Node resize handles (
createNodeResizer, 8 directions) - ✓ Node toolbar (
createNodeToolbar, floating, 4 positions × 3 alignments) - ✓ Context menus — node, edge, and pane (
createContextMenu)
Imperative API
- ✓
fitView+fitBounds - ✓
zoomTo,zoomIn,zoomOut,setViewport,getViewport - ✓
getNode,getEdge,getIntersectingNodes,isNodeIntersecting - ✓ Mouse events —
onNodeMouseEnter/Leave,onEdgeMouseEnter/Leave,onViewportChange
Composables
- ✓ Undo/redo —
createFlowHistory - ✓ Copy/paste —
createFlowClipboard - ✓ Auto-layout —
createAutoLayout(Sugiyama-inspired, 4 directions) - ✓ Viewport persistence —
createViewportPersistence(localStorage, SSR-safe)
Quality
- ✓ TypeScript-first, strict mode, no
anyin public APIs - ✓ SSR-safe — all browser API access is guarded
- ✓ Performance-optimized: batched edge effects, viewport culling (details below)
Quick Start
import { createFlow, FlowCanvas, applyNodeChanges, applyEdgeChanges } from '@liteforge/flow'
import { signal } from '@liteforge/core'
import '@liteforge/flow/styles'
// --- Node renderer ---
function DefaultNode(node) {
const el = document.createElement('div')
el.className = 'my-node'
el.textContent = node.data.label
return el
}
// --- State (you own it) ---
const nodes = signal([
{ id: '1', type: 'default', position: { x: 100, y: 100 }, data: { label: 'Input' } },
{ id: '2', type: 'default', position: { x: 350, y: 100 }, data: { label: 'Process' } },
{ id: '3', type: 'default', position: { x: 600, y: 100 }, data: { label: 'Output' } },
])
const edges = signal([
{ id: 'e1-2', source: '1', sourceHandle: 'out', target: '2', targetHandle: 'in' },
{ id: 'e2-3', source: '2', sourceHandle: 'out', target: '3', targetHandle: 'in' },
])
// --- Flow handle ---
const flow = createFlow({ nodeTypes: { default: DefaultNode } })
// --- Mount ---
const canvas = FlowCanvas({
flow,
nodes: () => nodes(),
edges: () => edges(),
fitView: true,
onNodesChange: (changes) => nodes.set(applyNodeChanges(nodes(), changes)),
onEdgesChange: (changes) => edges.set(applyEdgeChanges(edges(), changes)),
onConnect: (conn) => edges.update(es => [...es, { id: `e${Date.now()}`, ...conn }]),
onNodeMouseEnter: (node) => console.log('hover:', node.id),
})
document.getElementById('app')!.appendChild(canvas)Composables
createFlowHistory — Undo / Redo
import { createFlowHistory } from '@liteforge/flow'
const history = createFlowHistory(nodes, edges, { maxHistory: 50 })
history.attachKeyboard() // Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z
// Wire to FlowCanvas instead of your own handlers:
FlowCanvas({ flow, nodes: () => nodes(), edges: () => edges(),
onNodesChange: history.onNodesChange,
onEdgesChange: history.onEdgesChange,
onConnect: history.onConnect,
})
history.undo() // restore previous state
history.redo() // reapply undone state
history.canUndo() // Signal<boolean>
history.canRedo() // Signal<boolean>createFlowClipboard — Copy / Paste
import { createFlowClipboard } from '@liteforge/flow'
const clipboard = createFlowClipboard(nodes, edges, { pasteOffset: { x: 20, y: 20 } })
clipboard.attachKeyboard() // Ctrl+C / Ctrl+V
clipboard.copy() // copies selected nodes + edges between them
clipboard.paste() // pastes with fresh IDs and offset positions
clipboard.hasContent // boolean — true when clipboard is non-emptyEdges whose source and target are both in the selection are copied too. Edges to nodes outside the selection are dropped.
createAutoLayout — Automatic Layout
import { createAutoLayout, applyNodeChanges } from '@liteforge/flow'
const autoLayout = createAutoLayout({
direction: 'LR', // 'LR' | 'RL' | 'TB' | 'BT'
rankSpacing: 80,
nodeSpacing: 40,
})
const changes = autoLayout.layout(nodes(), edges())
nodes.set(applyNodeChanges(nodes(), changes))createAutoLayout computes synchronously using a Sugiyama-inspired layered layout. Call it before mounting for large graphs to avoid a visible position jump.
createViewportPersistence — Viewport Persistence
import { createViewportPersistence } from '@liteforge/flow'
const persist = createViewportPersistence('my-flow', flow, { debounce: 500 })
FlowCanvas({
flow,
nodes: () => nodes(),
edges: () => edges(),
defaultViewport: persist.savedViewport, // restores last position on reload
onViewportChange: persist.onViewportChange, // auto-saves on pan/zoom (debounced)
...
})
// Manual control
persist.save() // write immediately
persist.clear() // remove from storageSSR-safe: savedViewport is undefined on the server and all methods are no-ops.
Connection Validation
import { combineValidators, isNoSelfConnection, isNoDuplicateEdge } from '@liteforge/flow'
const flow = createFlow({
nodeTypes,
isValidConnection: combineValidators(
isNoSelfConnection,
isNoDuplicateEdge(() => edges()),
(conn) => conn.sourceHandle !== conn.targetHandle, // custom rule
),
})Self-connections (source === target) are always blocked regardless of isValidConnection. This guard runs first and cannot be bypassed.
Performance
Edge Batching
Before 0.3.0, each edge maintained three reactive subscriptions on the viewport transform signal. A graph with 400 edges fired 1200 effect executions per pan event. Since 0.3.0, a single batched effect handles all edges in one loop:
| Edges | Effects before | Effects after | |-------|---------------|---------------| | 50 | 150 | 1 | | 400 | 1200 | 1 | | 1000 | 3000 | 1 |
No configuration required. The change is transparent to all public APIs.
Viewport Culling
Nodes whose bounding box is entirely outside the visible viewport (plus a 100 px margin) are hidden with display: none. They remain in the DOM — the MiniMap, fitView, and getIntersectingNodes all read the full node list. Selected nodes and actively dragged nodes are never culled.
No configuration required.
Accessibility
@liteforge/flow implements the ARIA APG composite widget pattern for WCAG 2.1 AA conformance:
- Canvas root:
role="application",aria-label="Flow canvas" - Node wrappers:
role="button",aria-label(fromnode.data.label),aria-selected - Roving tabindex —
Tab/Shift+Tabnavigate between nodes Arrow keysmove focused node 10 px (50 px withShift)Enterselects;Escapedeselects all;Delete/Backspaceremove selection- Input focus guard — shortcuts are suppressed when focus is inside a text field
Installation
pnpm add @liteforge/flow @liteforge/coreImport the default styles once in your app entry point:
import '@liteforge/flow/css'Or use the unstyled: true option on FlowCanvas and bring your own CSS.
Docs
Full documentation: liteforge.dev/flow
- 01 — Create Flow
- 02 — Flow Canvas
- 03 — Nodes
- 04 — Edges
- 05 — Context Menu
- 06 — Flow History
- 07 — Auto Layout
- 08 — Imperative API
- 09 — Node Groups
- 10 — Connection Validation
- 11 — Viewport Persistence
- 12 — Keyboard Accessibility
- 13 — Performance
- 14 — Flow Clipboard
- 15 — Node Resizer
- 16 — Node Toolbar
License
MIT © SchildW3rk (René), Salzburg, Austria
