Skip to content

Commit 90e728c

Browse files
serpentbladeclaude
andcommitted
feat(@rozie-ui/rete): FlowCanvas — cross-framework node-flow editor (Rete.js v2)
One FlowCanvas.rozie compiles to idiomatic React/Vue/Svelte/Angular/Solid/Lit node-flow editors, filling a real gap: no single library ships all six (xyflow is React+Svelte only, Vue Flow is separate, Solid has only an experiment, Lit nothing). Wraps the framework-agnostic Rete.js v2 engine (NodeEditor owns the graph; AreaPlugin owns pan/zoom/drag; ConnectionPlugin owns drag-to-connect) with a single VANILLA render layer — no per-framework render plugin. Surface: 13 props / two-way `zoom` model / 7 events / 12-verb `$expose` / reactive multi-instance `node` portal slot. Config-array `:nodes`/`:connections` reconciled live; sockets announced via `render` type 'socket' signals so the ConnectionPlugin + getDOMSocketPosition watcher wire up drag-to-connect; connection paths drawn with classicConnectionPath + the socket-position watcher. Package: @rozie-ui/rete + 6 pre-compiled leaves (codegen.mjs doc-automation, handle-manifest, README generation, ENFORCING docs props-table validation). No emitter/core change → dist-parity zero-drift. Verified: compile 6/6 zero-error; react/solid/lit leaf strict-tsc clean; 15 surface tests; behavioral VR 6/6 + screenshot VR 6/6 pixel-parity in the pinned Linux container (shared FlowCanvasScreenshot.png baseline, all 6 ≤2px). Docs: /guide/rete (showcase), /guide/rete-comparison, /guide/rete-demo (live @rozie-ui/rete-vue in VitePress) — build clean, 0 broken anchors. Two cross-target bugs found + fixed in the wrapper source: - socketWatcher.attach() needs a CHILD Scope of the area (parentScope walks up one level); attaching to the area itself throws. - slot-name == local-binding shadow: a `node` param shadowed Svelte's `$slots.node` (→ `const node = $derived(...)`), dropping default node chrome on Svelte only; renamed param → reteNode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b62c3dd commit 90e728c

59 files changed

Lines changed: 9615 additions & 3 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/.vitepress/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ export default defineConfig({
175175
{ text: 'PdfViewer — live demo', link: '/guide/pdf-demo' },
176176
],
177177
},
178+
{
179+
text: '@rozie-ui/rete',
180+
items: [
181+
{ text: 'FlowCanvas — showcase & API', link: '/guide/rete' },
182+
{ text: 'Node-flow editor comparison', link: '/guide/rete-comparison' },
183+
{ text: 'FlowCanvas — live demo', link: '/guide/rete-demo' },
184+
],
185+
},
178186
],
179187
'/compatibility': [
180188
{

docs/guide/rete-comparison.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Node-flow editor libraries comparison
2+
3+
How `@rozie-ui/rete` (`FlowCanvas`) compares to the existing per-framework node-flow / graph editor libraries. A node editor's hard parts — the graph model, viewport pan/zoom, node drag, and drag-to-connect — are inherently framework-agnostic; [Rete.js v2](https://retejs.org/) is the engine that owns all of them and delegates only *rendering* to a swappable layer. The per-framework editors each re-solve those hard parts from scratch, which is why the ecosystem is **siloed**: React and Svelte are well-served, Vue has a separate reimplementation, Angular has a couple of options, and **Solid has only an experiment while Lit has nothing**. Rozie ships one source to all six by wrapping the agnostic engine with a single vanilla render layer.
4+
5+
> Research snapshot: 2026-06-08. Versions and the landscape move; treat them as of that date. The full audit is in [`node-flow-editor-feasibility.md`](https://github.com/One-Learning-Community/rozie.js/blob/main/.planning/research/node-flow-editor-feasibility.md).
6+
7+
## The libraries at a glance
8+
9+
| Library | Package | Frameworks | Rendering | State model | Verdict |
10+
| --- | --- | --- | --- | --- | --- |
11+
| **React Flow** | `@xyflow/react` | **React only** | SVG edges + DOM nodes | internal Zustand store | mature, deep — the category leader on React |
12+
| **Svelte Flow** | `@xyflow/svelte` | **Svelte only** | SVG + DOM | Svelte 5 runes | mature; shares `@xyflow/system` core with React Flow |
13+
| **Vue Flow** | `@vue-flow/core` | **Vue only** | SVG + DOM | own Vue store | mature, but a **separate** codebase — not xyflow's shared core |
14+
| **Foblex Flow** | `@foblex/flow` | **Angular only** | DOM + SVG | Angular signals | active; Angular-only |
15+
| **ngx-graph** | `@swimlane/ngx-graph` | **Angular only** | SVG (D3 + dagre) | RxJS | graph-viz-first, less an interactive editor |
16+
| **solid-flow** | `solid-flow` | Solid | SVG + DOM | signals | **single-author experiment**, not production-grade |
17+
| **Lit** ||||| **no standalone library exists** |
18+
| **Rozie** | `@rozie-ui/rete-*` | **all 6** | DOM + SVG (vanilla render layer) | Rete `NodeEditor` (the engine owns it) | one source → React/Vue/Svelte/Angular/Solid/Lit |
19+
20+
The big-framework editors above are **excellent, mature libraries** — for a single-React app, React Flow is the obvious pick, and Rozie does not claim to out-feature it on its home framework. The wedge is breadth: **no single library ships all six**, and two targets are essentially unserved. xyflow — the strongest brand — publishes only `@xyflow/react` and `@xyflow/svelte` (its shared `@xyflow/system` core has **no** Vue/Solid/Angular/Lit wrapper); Vue Flow is a wholly separate project; **Solid** has only a single-author `solid-flow` experiment; and **Lit / web-components has nothing at all**. The one ecosystem that even approaches breadth is **Rete.js**, whose render plugins cover React/Vue/Angular/Svelte/Lit — five divergent codebases, and still no Solid. Rozie replaces those five plugins with **one `.rozie` source and one vanilla render layer**, and adds the missing Solid (and a far thinner Lit) for free.
21+
22+
## Why wrap Rete.js
23+
24+
A Rete render plugin's only job is to (a) fill each engine-created node element with DOM, (b) draw each connection's SVG path, and (c) tell the connection plugin where the sockets are. The official plugins do (a)+(b) with a framework's component tree — that *is* the per-framework coupling. `FlowCanvas` does all three with a **vanilla render pipe** (`area.addPipe`), emitting `render` socket signals the `ConnectionPlugin` + `getDOMSocketPosition` watcher consume, and drawing connection paths with `classicConnectionPath`. The engine (`NodeEditor` + `AreaPlugin` + `ConnectionPlugin`) owns graph state and all pointer interaction; the Rozie component is a thin view over it, so the same source behaves identically on every target.
25+
26+
## Feature matrix
27+
28+
Cell legend: **** = documented out-of-the-box · **** = not supported / not present · **⚠️** = partial / experimental / consumer-glue-required.
29+
30+
| Capability | React Flow | Vue Flow | Svelte Flow | Foblex (Angular) | solid-flow | Lit (none) | **`@rozie-ui/rete`** |
31+
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
32+
| Mount canvas ||||| ⚠️ | hand-roll ||
33+
| Pan / zoom viewport ||||| ⚠️ | hand-roll | ✅ (engine-owned) |
34+
| Node drag ||||| ⚠️ | hand-roll | ✅ (engine-owned) |
35+
| **Drag-to-connect** (sockets) ||||| ⚠️ | hand-roll | ✅ (socket render-signal bridge) |
36+
| **Custom node bodies** (framework component) | ✅ node types |||| ⚠️ ||`node` reactive portal slot (all 6) |
37+
| Two-way zoom binding | ⚠️ controlled | ⚠️ | ⚠️ | ⚠️ | ⚠️ | hand-roll |`r-model:zoom` (echo-guarded) |
38+
| Graph events (moved / connected / picked) ||||| ⚠️ | hand-roll | ✅ 7 structured events |
39+
| Imperative handle |`useReactFlow` |`useVueFlow` || ✅ service | ⚠️ | hand-roll | ✅ uniform 12-verb `$expose` |
40+
| Config-array graph (`:nodes` / `:connections`) ||||| ⚠️ || ✅ reconciled live, no remount |
41+
| MiniMap / Background / Controls |||| ⚠️ | ⚠️ || ⚠️ deferred (see below) |
42+
| TypeScript ||||| ⚠️ |||
43+
| One source → all 6 frameworks ||||||||
44+
45+
## Where Rozie wins today
46+
47+
- **One definition, six idiomatic packages** — including the two frameworks the ecosystem leaves out entirely: **Solid** (only a single-author experiment) and **Lit** (nothing). Those consumers get a real node editor they otherwise cannot have, from the same source that produces the four big-framework packages.
48+
- **Framework-native node bodies on all six** — the `node` **reactive multi-instance portal slot** renders a real framework fragment (any component, any reactivity) as a graph node, re-rendered in place as the node's data / selection changes, reconciled off the `nodes` data array.
49+
- **The engine owns interaction, so behavior is identical by construction** — pan/zoom transform, node drag, edge drawing, and connection-handle hit-testing all live in Rete's `AreaPlugin` + `ConnectionPlugin`. Rozie never re-implements pointer math per target, so there is no cross-framework drift in *how the editor feels*.
50+
- **A uniform 12-verb imperative handle** (`getEditor` / `getArea` / `addNode` / `removeNode` / `addConnection` / `removeConnection` / `clear` / `zoomToFit` / `zoomTo` / `getNodes` / `getConnections` / `getTransform`) grabbed with each framework's native ref — versus "however this library happens to expose its instance" (a hook, a service, a ref).
51+
- **`getEditor()` / `getArea()` are always one hop from the raw engine**, so the full Rete API (custom plugins, `rete-engine` dataflow, `rete-auto-arrange-plugin`, …) is reachable on any target when the curated surface doesn't cover something.
52+
53+
## What Rozie defers {#what-rozie-defers}
54+
55+
This page concedes where the standalone libraries are genuinely ahead — that's what keeps the comparison credible, and it doubles as Rozie's roadmap.
56+
57+
- **Declarative `<Node>` / `<Edge>` *children*.** React Flow et al. let you compose a graph as JSX/children with per-type node components registered up front. Rozie v1 takes a different authoring shape: the **`:nodes` / `:connections` config-array props** (reconciled into the live editor), with per-node bodies supplied through the `node` slot. It is the **same `addNode` / `addConnection` runtime** and reaches the same result, but the authoring model is a config array, not nested children. True declarative graph children — deeply-nested `<Node>`/`<Handle>` elements reading shared graph state (selection, viewport, neighbors) without prop-drilling — need a **cross-component context primitive** that Rozie deliberately defers (the same primitive MapLibre's `:sources` / `:layers` deferral is waiting on). The wrap-a-vanilla-engine strategy sidesteps it entirely: the **engine** owns the store, and node bodies reach it through portal scope.
58+
- **MiniMap / Background variants / NodeToolbar / NodeResizer.** React Flow ships these as first-class components. `FlowCanvas` v1 covers the canvas + nodes + sockets + connections + a dotted background; the second-tier chrome is on the roadmap (config-prop first, the MapLibre stance).
59+
- **Big-framework depth on the home framework.** React Flow (Zustand store, deep node/edge-type catalogs, helper hooks, layouting integrations) is a mature, multi-year library; on React it exposes more surface than Rozie's curated set. Rozie's value is **not** "more than React Flow on React" — it's the **same idiomatic editor on all six frameworks from one source**, with the unserved **Solid and Lit** finally covered.
60+
- **`@rozie-ui/rete` is `0.1.0`.** The surface (13 props / 7 events / 12-verb handle / `node` reactive slot) is stable and gate-verified (behavioral parity across all six targets), but it is younger than the incumbents.
61+
62+
## Try it
63+
64+
The [`@rozie-ui/rete` showcase + API reference](/guide/rete) documents the `@rozie-ui/rete-*` packages — one pre-compiled, per-framework install (`npm i @rozie-ui/rete-react rete rete-area-plugin rete-connection-plugin rete-render-utils`, etc.). Rete ships no stylesheet, so there is no engine CSS to import — all node / socket / connection chrome is styled by the component.
65+
66+
## Cross-references
67+
68+
- [FlowCanvas — showcase & API](/guide/rete) — the full `@rozie-ui/rete` surface, quick starts, and the `node` slot recipe.
69+
- [`FlowCanvas.rozie` source on GitHub](https://github.com/One-Learning-Community/rozie.js/blob/main/packages/ui/rete/src/FlowCanvas.rozie)
70+
- [The portal-slot primitive](/examples/portal-list) — the mechanism the `node` reactive slot builds on.

docs/guide/rete-demo.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
title: FlowCanvas — live demo
3+
---
4+
5+
<script setup lang="ts">
6+
import { ref } from 'vue';
7+
import FlowCanvas from '@rozie-ui/rete-vue';
8+
9+
// A small, fixed graph. `nodes` / `connections` are plain config arrays — the
10+
// same shape that compiles to all six frameworks. Each node declares its
11+
// input/output socket keys; connections reference nodes + socket keys by id.
12+
const INITIAL_NODES = [
13+
{ id: 'input', label: 'Input', x: 20, y: 40, data: { kind: 'in' }, outputs: [{ key: 'out', label: 'value' }] },
14+
{ id: 'filter', label: 'Transform', x: 280, y: 40, data: { kind: 'op' }, inputs: [{ key: 'in', label: 'in' }], outputs: [{ key: 'out', label: 'out' }] },
15+
{ id: 'output', label: 'Output', x: 540, y: 140, data: { kind: 'out' }, inputs: [{ key: 'in', label: 'result' }] },
16+
];
17+
const INITIAL_EDGES = [
18+
{ id: 'e1', source: 'input', sourceOutput: 'out', target: 'filter', targetInput: 'in' },
19+
{ id: 'e2', source: 'filter', sourceOutput: 'out', target: 'output', targetInput: 'in' },
20+
];
21+
22+
const flow = ref();
23+
const zoom = ref(1);
24+
const nodes = ref(INITIAL_NODES.map((n) => ({ ...n })));
25+
const connections = ref(INITIAL_EDGES.map((e) => ({ ...e })));
26+
let next = 1;
27+
28+
function addNode() {
29+
const id = 'n' + next++;
30+
nodes.value = [
31+
...nodes.value,
32+
{ id, label: 'Node ' + id, x: 120 + nodes.value.length * 24, y: 240, data: { kind: 'op' }, inputs: [{ key: 'in' }], outputs: [{ key: 'out' }] },
33+
];
34+
}
35+
function reset() {
36+
next = 1;
37+
nodes.value = INITIAL_NODES.map((n) => ({ ...n }));
38+
connections.value = INITIAL_EDGES.map((e) => ({ ...e }));
39+
zoom.value = 1;
40+
}
41+
</script>
42+
43+
# FlowCanvas — live demo
44+
45+
This is the **real `@rozie-ui/rete-vue` package** running on this page (VitePress is itself a Vue app) — driving an actual [Rete.js v2](https://retejs.org/) node editor. **Drag a node**, **drag from one socket to another to connect them**, scroll to zoom, or use the controls below. Everything is driven by the same `FlowCanvas.rozie` source that compiles to all six frameworks, through a **vanilla render layer** (no framework-specific Rete render plugin). Rete ships no stylesheet — every node, socket, and connection you see is styled by the component itself.
46+
47+
<ClientOnly>
48+
<div class="flow-live">
49+
<div class="flow-live__controls">
50+
<button @click="addNode">Add node +</button>
51+
<button @click="zoom = Math.min(Math.round((zoom + 0.25) * 100) / 100, 4)">Zoom in +</button>
52+
<button @click="zoom = Math.max(Math.round((zoom - 0.25) * 100) / 100, 0.2)">Zoom out -</button>
53+
<button @click="flow?.zoomToFit()">Fit ▣</button>
54+
<span class="flow-live__sep" />
55+
<button class="flow-live__primary" @click="reset">Reset ▸</button>
56+
</div>
57+
58+
<div class="flow-live__stage">
59+
<FlowCanvas
60+
ref="flow"
61+
:nodes="nodes"
62+
:connections="connections"
63+
v-model:zoom="zoom"
64+
style="width: 100%; height: 380px;"
65+
>
66+
<template #node="{ node, selected }">
67+
<div class="flow-live__node" :class="{ 'is-selected': selected }" :data-kind="node.data?.kind">
68+
<strong>{{ node.label }}</strong>
69+
</div>
70+
</template>
71+
</FlowCanvas>
72+
</div>
73+
74+
<div class="flow-live__readout">
75+
<code>{{ nodes.length }} nodes · {{ connections.length }} connections · zoom {{ zoom.toFixed(2) }}</code>
76+
</div>
77+
</div>
78+
</ClientOnly>
79+
80+
The graph is a pair of plain config arrays (`:nodes` / `:connections`); **Add node** pushes onto `nodes` and the wrapper reconciles it into the live editor with no remount. `zoom` is **two-way bound** with `v-model:zoom` — the readout tracks it as you scroll, and **Fit** drives the imperative handle (`zoomToFit()`), which echoes the new zoom back into the binding. Each node body is your own `<template #node>` fragment, rendered per node through the reactive `node` portal slot. See the [full API](/guide/rete) for the complete prop / event / handle surface.
81+
82+
## One source, six outputs
83+
84+
You author the component **once** as a `.rozie` file:
85+
86+
<<< ../../packages/ui/rete/src/FlowCanvas.rozie{html}[FlowCanvas.rozie — the single source]
87+
88+
…and Rozie compiles it to six idiomatic, framework-native components. Switch the tabs to see the **actual generated output** for each target (this is exactly what ships in `@rozie-ui/rete-{react,vue,svelte,angular,solid,lit}`):
89+
90+
::: code-group
91+
92+
<<< ../../packages/ui/rete/packages/react/src/FlowCanvas.tsx[React]
93+
<<< ../../packages/ui/rete/packages/vue/src/FlowCanvas.vue[Vue]
94+
<<< ../../packages/ui/rete/packages/svelte/src/FlowCanvas.svelte[Svelte]
95+
<<< ../../packages/ui/rete/packages/angular/src/FlowCanvas.ts[Angular]
96+
<<< ../../packages/ui/rete/packages/solid/src/FlowCanvas.tsx[Solid]
97+
<<< ../../packages/ui/rete/packages/lit/src/FlowCanvas.ts[Lit]
98+
99+
:::
100+
101+
Each is a real, idiomatic component for its framework — React `forwardRef` + hooks, Vue `<script setup>` + `defineModel`, Svelte 5 runes, an Angular standalone component with `model()` signals, a Solid component, and a Lit custom element. Same props, same 7 events, same 12-verb imperative handle, same reactive `node` portal slot, all from the one source above — and the engine (graph model, pan/zoom/drag, drag-to-connect) is Rete.js on every target.
102+
103+
## See also
104+
105+
- [FlowCanvas — showcase & API](/guide/rete) — install, quick starts for all six frameworks, the events, the two-way zoom binding, the imperative handle, and the `node` slot.
106+
- [Node-flow editor libraries comparison](/guide/rete-comparison) — how `@rozie-ui/rete` stacks up against React Flow / Vue Flow / Svelte Flow / Foblex (and the Solid / Lit gap it closes).
107+
108+
<style scoped>
109+
.flow-live {
110+
margin: 1.5rem 0;
111+
padding: 1rem;
112+
border: 1px solid var(--vp-c-divider);
113+
border-radius: 12px;
114+
background: var(--vp-c-bg-soft);
115+
}
116+
.flow-live__controls {
117+
display: flex;
118+
flex-wrap: wrap;
119+
align-items: center;
120+
gap: 0.4rem;
121+
margin-bottom: 0.85rem;
122+
}
123+
.flow-live__controls button {
124+
font: inherit;
125+
font-size: 0.82rem;
126+
padding: 0.3rem 0.7rem;
127+
border: 1px solid var(--vp-c-divider);
128+
border-radius: 7px;
129+
background: var(--vp-c-bg);
130+
color: var(--vp-c-text-1);
131+
cursor: pointer;
132+
transition: border-color 0.15s, background 0.15s;
133+
}
134+
.flow-live__controls button:hover {
135+
border-color: var(--vp-c-brand-1);
136+
color: var(--vp-c-brand-1);
137+
}
138+
.flow-live__controls button.flow-live__primary {
139+
background: var(--vp-c-brand-1);
140+
border-color: var(--vp-c-brand-1);
141+
color: #fff;
142+
font-weight: 600;
143+
}
144+
.flow-live__sep {
145+
width: 1px;
146+
align-self: stretch;
147+
margin: 0 0.3rem;
148+
background: var(--vp-c-divider);
149+
}
150+
.flow-live__stage {
151+
background: var(--vp-c-bg);
152+
border-radius: 8px;
153+
overflow: hidden;
154+
}
155+
.flow-live__node {
156+
padding: 0.5rem 0.75rem;
157+
font-size: 0.8125rem;
158+
color: var(--vp-c-text-1);
159+
}
160+
.flow-live__node[data-kind="in"] strong { color: #047857; }
161+
.flow-live__node[data-kind="out"] strong { color: #b91c1c; }
162+
.flow-live__node.is-selected { background: rgba(59, 130, 246, 0.1); }
163+
.flow-live__readout {
164+
min-height: 1.4rem;
165+
margin-top: 0.6rem;
166+
font-size: 0.82rem;
167+
color: var(--vp-c-text-2);
168+
}
169+
</style>

0 commit comments

Comments
 (0)