npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@liteforge/flow

v4.0.0

Published

Signals-based node editor — foundation for pipeline and visual programming editors.

Downloads

873

Readme

@liteforge/flow

npm version license: MIT tests passing

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 any in 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-empty

Edges 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 storage

SSR-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 (from node.data.label), aria-selected
  • Roving tabindex — Tab / Shift+Tab navigate between nodes
  • Arrow keys move focused node 10 px (50 px with Shift)
  • Enter selects; Escape deselects all; Delete / Backspace remove selection
  • Input focus guard — shortcuts are suppressed when focus is inside a text field

Installation

pnpm add @liteforge/flow @liteforge/core

Import 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


License

MIT © SchildW3rk (René), Salzburg, Austria