Example · flow-studio
React Flow + Tailwind + shadcn/ui, served from NanoVM
node · host JS engine
A heavier front-end stack: React Flow for the node graph, Tailwind CSS v4 through its Vite plugin, and shadcn/ui-style components (class-variance-authority, clsx, tailwind-merge, lucide-react). The diagram illustrates the RISC-V runner's pipeline; its counters are simulated animation data, labelled as such in the app.
As with react-studio, Vite builds the app ahead of time from a pinned lockfile and a Node.js static server serves dist/ from NanoVM's filesystem into the preview.
Run it
Serve the React Flow build
Seeds the built dist/ into /work, then runs this static server.
javascript
// A dependency-free static file server for the pre-built Vite dist/. Runs inside // NanoVM; serves the React Flow app to the preview iframe via the SW bridge. const http = require("http"); const fs = require("fs"); const path = require("path"); // dist/ relative to the working directory (`cd <demo> && node server.js`). const ROOT = path.resolve("dist"); const TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".map": "application/json", }; const server = http.createServer(function (req, res) { let url = req.url.split("?")[0]; if (url === "/" || url === "") url = "/index.html"; const file = path.join(ROOT, url); fs.readFile(file, function (err, data) { if (err) { // SPA fallback → index.html fs.readFile(path.join(ROOT, "index.html"), function (e2, html) { if (e2) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "Content-Type": TYPES[".html"] }); res.end(html); }); return; } res.writeHead(200, { "Content-Type": TYPES[path.extname(file)] || "application/octet-stream" }); res.end(data); }); }); server.listen(3000, function () { console.log("flow-studio listening on 3000"); });
Expected output
animated illustration
How it runs
- The built
dist/(HTML, one JS and one CSS file) is written to/work/dist/. server.jsserves it on port 3000 from the host-engine Node tier.- The preview iframe loads it through the service-worker bridge.
Source
From demos/flow-studio/.
src/App.jsx
jsx
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactFlow, {
Background,
BackgroundVariant,
Controls,
Handle,
Position,
ReactFlowProvider,
} from "reactflow";
import {
Activity,
Cpu,
FileCode2,
Gauge,
HardDrive,
Layers,
Pause,
Play,
StepForward,
Terminal,
Zap,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/card";
import { Button } from "./components/ui/button";
import { Badge } from "./components/ui/badge";
import { cn } from "./lib/utils";
// ---- the RISC-V execution pipeline (mirrors the landing-page architecture) ----
const STAGES = [
{ id: "elf", kicker: "INPUT", title: "RISC-V ELF", note: "segments · argv / envp", icon: FileCode2, tone: "outline", pos: { x: 95, y: 0 } },
{ id: "decode", kicker: "DECODE", title: "Fetch & decode", note: "field extraction", icon: Cpu, tone: "outline", pos: { x: 95, y: 132 } },
{ id: "exec", kicker: "CORE", title: "exec() loop", note: "br_table dispatch · #![no_std]", icon: Zap, tone: "default", pos: { x: 95, y: 286 } },
{ id: "syscall", kicker: "KERNEL", title: "Syscall dispatch", note: "~80 Linux syscalls", icon: Layers, tone: "default", pos: { x: 95, y: 440 } },
{ id: "host", kicker: "HOST", title: "MemFS / host", note: "shared-memory FS", icon: HardDrive, tone: "accent", pos: { x: 95, y: 594 } },
];
// Syscalls the simulated guest "issues", with a plausible strace-style line.
const SYSCALLS = [
{ name: "write", fs: true, line: (n) => `→ write(1, buf, ${n}) = ${n}` },
{ name: "read", fs: true, line: () => `→ read(3, buf, 4096) = ${256 + ((Math.random() * 3840) | 0)}` },
{ name: "openat", fs: true, line: () => `→ openat(AT_FDCWD, "/lib/libc.so") = 3` },
{ name: "fstat", fs: true, line: () => `→ fstat(3, {mode=0644, …}) = 0` },
{ name: "mmap", fs: false, line: () => `→ mmap(NULL, 65536, RW) = 0x${(0x40000000 + ((Math.random() * 0xfffff) | 0)).toString(16)}` },
{ name: "clock_gettime", fs: false, line: () => `→ clock_gettime(MONOTONIC) = 0` },
{ name: "brk", fs: false, line: () => `→ brk(0x${(0x60000 + ((Math.random() * 0xffff) | 0)).toString(16)}) = ok` },
{ name: "epoll_wait", fs: false, line: () => `→ epoll_wait(4, …, 64) = 1` },
{ name: "futex", fs: false, line: () => `→ futex(addr, WAIT, 0) = 0` },
];
const fmtCount = (n) =>
n >= 1e9 ? `${(n / 1e9).toFixed(2)} B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)} M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)} K` : `${n}`;
const fmtIps = (n) =>
n >= 1e6 ? `${(n / 1e6).toFixed(1)} Mips` : n >= 1e3 ? `${(n / 1e3).toFixed(0)} Kips` : `${n | 0} ips`;
const hex = (n) => "0x" + (n >>> 0).toString(16).padStart(8, "0");
const fmtClock = (s) => `${String((s / 60) | 0).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
// ---- custom nodes ----
// Vertical spine: flow enters at the top, leaves at the bottom; the syscall
// return loop re-enters exec from the left.
const HANDLES = (
<>
<Handle id="t" type="target" position={Position.Top} />
<Handle id="b" type="source" position={Position.Bottom} />
<Handle id="lt" type="target" position={Position.Left} style={{ top: "62%" }} />
<Handle id="ls" type="source" position={Position.Left} style={{ top: "38%" }} />
</>
);
const StageNode = memo(function StageNode({ data }) {
const Icon = data.icon;
return (
<div className={cn("w-[196px] rounded-xl", data.active && "pulse-ring")}>
<Card className={cn("transition-colors duration-150", data.active ? "border-primary/70" : "border-border")}>
<CardHeader className="gap-2 p-3">
<div className="flex items-center justify-between">
<Badge variant={data.tone}>{data.kicker}</Badge>
<Icon className={cn("size-4", data.active ? "text-primary" : "text-muted-foreground")} />
</div>
<CardTitle className="text-[13px]">{data.title}</CardTitle>
<div className="text-[10.5px] leading-snug text-muted-foreground">{data.note}</div>
</CardHeader>
<CardContent className="p-3 pt-0">
<div className="rounded-md bg-secondary/60 px-2 py-1 text-[11px] tabular-nums text-foreground/90">
{data.metric}
</div>
</CardContent>
</Card>
{HANDLES}
</div>
);
});
const OutputNode = memo(function OutputNode({ data }) {
return (
<div className="w-[260px]">
<Card>
<CardHeader className="flex-row items-center justify-between p-3 pb-1.5">
<Badge variant="accent">
<Terminal className="size-3" /> stdout
</Badge>
<span className="text-[10px] text-muted-foreground tabular-nums">{data.bytes} B</span>
</CardHeader>
<CardContent className="p-3 pt-0">
<div className="h-[104px] overflow-hidden rounded-md bg-black/50 p-2 text-[10.5px] leading-relaxed">
{data.lines.map((l, i) => (
<div key={i} className={cn("truncate", i === data.lines.length - 1 ? "text-foreground" : "text-muted-foreground/70")}>
{l}
</div>
))}
</div>
</CardContent>
</Card>
<Handle id="t" type="target" position={Position.Top} style={{ left: "50%" }} />
</div>
);
});
const nodeTypes = { stage: StageNode, output: OutputNode };
const BASE_EDGES = [
{ id: "e1", source: "elf", target: "decode", sourceHandle: "b", targetHandle: "t", animated: true },
{ id: "e2", source: "decode", target: "exec", sourceHandle: "b", targetHandle: "t", animated: true },
{ id: "e3", source: "exec", target: "syscall", sourceHandle: "b", targetHandle: "t", animated: true, label: "ecall" },
{ id: "e4", source: "syscall", target: "host", sourceHandle: "b", targetHandle: "t", animated: true },
{ id: "e5", source: "host", target: "stdout", sourceHandle: "b", targetHandle: "t", animated: true, label: "write()" },
{ id: "e6", source: "host", target: "exec", sourceHandle: "ls", targetHandle: "lt", animated: true, label: "iret" },
];
const STAGE_EDGE = { decode: "e1", exec: "e2", syscall: "e3", host: "e4" };
function Flow() {
const [running, setRunning] = useState(true);
const [speed, setSpeed] = useState(1); // 0.5 / 1 / 2 ×
const [sim, setSim] = useState({
instructions: 0,
syscalls: 0,
fsOps: 0,
ips: 0,
pc: 0x10000,
active: "elf",
hot: "e1",
warm: [],
syscall: "—",
bytes: 0,
lines: ["booting RV64GC guest …", "exec() loop entered"],
uptime: 0,
});
const ema = useRef(0);
const step = useCallback((dtMs) => {
setSim((s) => {
const batch = 30000 + ((Math.random() * 90000) | 0);
ema.current = ema.current ? ema.current * 0.8 + (batch / (dtMs / 1000)) * 0.2 : batch / (dtMs / 1000);
const r = Math.random();
let active = "exec";
let hot = Math.random() < 0.5 ? "e2" : "e6";
let warm = [];
let syscall = s.syscall;
let syscalls = s.syscalls;
let fsOps = s.fsOps;
let lines = s.lines;
let bytes = s.bytes;
if (r < 0.17) {
const sc = SYSCALLS[(Math.random() * SYSCALLS.length) | 0];
active = "syscall";
hot = "e3";
warm = ["e4", "e6"];
syscall = sc.name;
syscalls += 1;
if (sc.fs) fsOps += 1;
const line = sc.line(64 + ((Math.random() * 192) | 0));
if (sc.name === "write") {
warm.push("e5");
bytes += 64;
}
lines = [...s.lines, line].slice(-7);
} else if (r < 0.36) {
active = "decode";
hot = "e1";
} else if (r < 0.42) {
active = "host";
hot = "e4";
}
return {
instructions: s.instructions + batch,
syscalls,
fsOps,
ips: ema.current,
pc: 0x10000 + ((s.instructions + batch) * 2) % 0x7fff0000,
active,
hot,
warm,
syscall,
bytes,
lines,
uptime: s.uptime,
};
});
}, []);
// Drive the simulation; cadence scales with the speed control.
useEffect(() => {
if (!running) return;
const dt = 130 / speed;
const id = setInterval(() => step(dt), dt);
return () => clearInterval(id);
}, [running, speed, step]);
// Uptime ticks independently (only while running).
useEffect(() => {
if (!running) return;
const id = setInterval(() => setSim((s) => ({ ...s, uptime: s.uptime + 1 })), 1000);
return () => clearInterval(id);
}, [running]);
const metricFor = (id) => {
switch (id) {
case "elf": return "/usr/bin/node · 3 PT_LOAD";
case "decode": return `pc ${hex(sim.pc)}`;
case "exec": return `${fmtIps(sim.ips)} · ${fmtCount(sim.instructions)} insn`;
case "syscall": return `${sim.syscall} · ${sim.syscalls} total`;
case "host": return `${fmtCount(sim.fsOps)} fs ops`;
default: return "";
}
};
const nodes = useMemo(() => {
const stages = STAGES.map((st) => ({
id: st.id,
type: "stage",
position: st.pos,
data: { ...st, active: sim.active === st.id, metric: metricFor(st.id) },
draggable: false,
}));
stages.push({
id: "stdout",
type: "output",
position: { x: 63, y: 748 },
data: { lines: sim.lines, bytes: sim.bytes },
draggable: false,
});
return stages;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sim]);
const edges = useMemo(
() =>
BASE_EDGES.map((e) => ({
...e,
className: e.id === sim.hot ? "is-hot" : sim.warm.includes(e.id) ? "is-warm" : "",
})),
[sim.hot, sim.warm],
);
return (
<div className="flex h-full flex-col">
{/* header */}
<header className="flex items-center gap-3 border-b border-border px-4 py-2.5">
<Activity className="size-4 text-primary" />
<div className="text-[13px] font-semibold">
NanoVM pipeline <span className="text-muted-foreground">· animated illustration (simulated numbers)</span>
</div>
<Badge variant={running ? "ok" : "outline"} className={running ? "pulse-ring" : ""}>
{running ? "● animating" : "‖ paused"}
</Badge>
<div className="ml-auto flex items-center gap-4 text-[11px] tabular-nums text-muted-foreground">
<span><span className="text-foreground">{fmtCount(sim.instructions)}</span> insn</span>
<span><span className="text-foreground">{fmtIps(sim.ips)}</span></span>
<span><span className="text-foreground">{sim.syscalls}</span> syscalls</span>
<span><span className="text-foreground">{fmtClock(sim.uptime)}</span> up</span>
</div>
</header>
{/* graph */}
<div className="relative flex-1">
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.18 }}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
proOptions={{ hideAttribution: true }}
minZoom={0.4}
maxZoom={1.5}
>
<Background variant={BackgroundVariant.Dots} gap={22} size={1} color="#2a2a31" />
<Controls showInteractive={false} />
</ReactFlow>
</div>
{/* controls */}
<footer className="flex items-center gap-2 border-t border-border px-4 py-2.5">
<Button onClick={() => setRunning((v) => !v)} size="sm">
{running ? <Pause /> : <Play />}
{running ? "Pause" : "Resume"}
</Button>
<Button variant="outline" size="sm" onClick={() => step(130 / speed)} disabled={running}>
<StepForward /> Step
</Button>
<div className="mx-1 h-5 w-px bg-border" />
<Gauge className="size-3.5 text-muted-foreground" />
{[0.5, 1, 2].map((x) => (
<Button key={x} variant={speed === x ? "default" : "ghost"} size="sm" onClick={() => setSpeed(x)}>
{x}×
</Button>
))}
<div className="ml-auto text-[11px] text-muted-foreground">
A live visualization — the real loop runs millions of these per second.
</div>
</footer>
</div>
);
}
export default function App() {
return (
<ReactFlowProvider>
<Flow />
</ReactFlowProvider>
);
}src/main.jsx
jsx
import React from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import "reactflow/dist/style.css";
import "./index.css";
import App from "./App.jsx";
// Render the first frame synchronously, so the page is complete by the time
// its load event fires (the live sample checks the preview's text on load).
const root = createRoot(document.getElementById("root"));
flushSync(() =>
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
),
);src/index.css
css
@import "tailwindcss";
/* shadcn-style design tokens, mapped into Tailwind v4 utilities via @theme inline.
Dark-only, tuned to the userland.run palette (violet primary, cyan accent). */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--card);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-border: var(--border);
--color-ring: var(--ring);
--color-ok: var(--ok);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
:root {
--radius: 0.6rem;
--background: #0b0b0e;
--foreground: #ededf3;
--card: #141419;
--card-foreground: #ededf3;
--primary: #a984f5;
--primary-foreground: #160a2e;
--secondary: #1c1c23;
--secondary-foreground: #d6d6df;
--muted: #1a1a20;
--muted-foreground: #8f8f9c;
--accent: #5fd6d1;
--accent-foreground: #062a28;
--border: #2a2a31;
--ring: #a984f5;
--ok: #74d493;
}
* {
box-sizing: border-box;
border-color: var(--border);
}
html,
body,
#root {
height: 100%;
margin: 0;
}
body {
background:
radial-gradient(900px 500px at 75% -10%, rgba(169, 132, 245, 0.12), transparent 70%),
var(--background);
color: var(--foreground);
font-family: var(--font-mono);
-webkit-font-smoothing: antialiased;
}
/* ---- React Flow dark surface ---- */
.react-flow__attribution {
display: none;
}
.react-flow__background {
background: transparent;
}
.react-flow__edge-path {
stroke: var(--border);
stroke-width: 1.5;
}
.react-flow__edge.is-hot .react-flow__edge-path {
stroke: var(--primary);
stroke-width: 2.5;
filter: drop-shadow(0 0 6px rgba(169, 132, 245, 0.7));
}
.react-flow__edge.is-warm .react-flow__edge-path {
stroke: var(--accent);
stroke-width: 2;
}
.react-flow__edge .react-flow__edge-text {
fill: var(--muted-foreground);
font-family: var(--font-mono);
font-size: 10px;
}
.react-flow__edge .react-flow__edge-textbg {
fill: var(--background);
}
.react-flow__handle {
border: none;
background: transparent;
width: 1px;
height: 1px;
min-width: 1px;
min-height: 1px;
}
.react-flow__controls-button {
background: var(--card);
border-color: var(--border);
color: var(--muted-foreground);
}
.react-flow__controls-button:hover {
background: var(--secondary);
}
@keyframes pulse-ring {
0% {
box-shadow: 0 0 0 0 rgba(169, 132, 245, 0.5);
}
70% {
box-shadow: 0 0 0 7px rgba(169, 132, 245, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(169, 132, 245, 0);
}
}
.pulse-ring {
animation: pulse-ring 1.1s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.pulse-ring {
animation: none;
}
}src/components/ui/card.jsx
jsx
import { cn } from "../../lib/utils";
// shadcn/ui Card (and friends), trimmed to what this demo uses.
export function Card({ className, ...props }) {
return (
<div
className={cn(
"rounded-xl border border-border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
);
}
export function CardHeader({ className, ...props }) {
return <div className={cn("flex flex-col gap-1 p-4", className)} {...props} />;
}
export function CardTitle({ className, ...props }) {
return <div className={cn("text-sm font-semibold leading-none tracking-tight", className)} {...props} />;
}
export function CardContent({ className, ...props }) {
return <div className={cn("p-4 pt-0", className)} {...props} />;
}src/components/ui/button.jsx
jsx
import { cva } from "class-variance-authority";
import { cn } from "../../lib/utils";
// shadcn/ui Button — cva variants over the design tokens.
const buttonVariants = cva(
"inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-3.5 cursor-pointer select-none",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border border-border bg-transparent text-foreground hover:bg-secondary",
ghost: "text-muted-foreground hover:bg-secondary hover:text-foreground",
},
size: {
default: "h-8 px-3",
sm: "h-7 px-2.5",
icon: "h-8 w-8",
},
},
defaultVariants: { variant: "default", size: "default" },
},
);
export function Button({ className, variant, size, ...props }) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}
export { buttonVariants };src/components/ui/badge.jsx
jsx
import { cva } from "class-variance-authority";
import { cn } from "../../lib/utils";
// shadcn/ui Badge — cva variants over the design tokens.
const badgeVariants = cva(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium tracking-wide uppercase transition-colors",
{
variants: {
variant: {
default: "border-transparent bg-primary/15 text-primary",
accent: "border-transparent bg-accent/15 text-accent",
ok: "border-transparent bg-[var(--ok)]/15 text-[var(--ok)]",
outline: "border-border text-muted-foreground",
},
},
defaultVariants: { variant: "default" },
},
);
export function Badge({ className, variant, ...props }) {
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
}src/lib/utils.js
javascript
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
/** shadcn's class combiner: merge conditional classes, de-dupe Tailwind conflicts. */
export function cn(...inputs) {
return twMerge(clsx(inputs));
}server.js
javascript
// A dependency-free static file server for the pre-built Vite dist/. Runs inside // NanoVM; serves the React Flow app to the preview iframe via the SW bridge. const http = require("http"); const fs = require("fs"); const path = require("path"); // dist/ relative to the working directory (`cd <demo> && node server.js`). const ROOT = path.resolve("dist"); const TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".map": "application/json", }; const server = http.createServer(function (req, res) { let url = req.url.split("?")[0]; if (url === "/" || url === "") url = "/index.html"; const file = path.join(ROOT, url); fs.readFile(file, function (err, data) { if (err) { // SPA fallback → index.html fs.readFile(path.join(ROOT, "index.html"), function (e2, html) { if (e2) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "Content-Type": TYPES[".html"] }); res.end(html); }); return; } res.writeHead(200, { "Content-Type": TYPES[path.extname(file)] || "application/octet-stream" }); res.end(data); }); }); server.listen(3000, function () { console.log("flow-studio listening on 3000"); });
vite.config.js
javascript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
// base: "./" → relative asset URLs, so the build works when served under the
// preview path (/sw/<port>/) instead of the origin root. Inline binary assets so
// the dist is just index.html + one JS + one CSS (served by the tiny node server).
export default defineConfig({
base: "./",
plugins: [react(), tailwindcss()],
build: { assetsInlineLimit: 100000000 },
});package.json
json
{
"name": "nano-flow-studio",
"private": true,
"type": "module",
"scripts": { "build": "vite build" },
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"reactflow": "^11.11.4",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@vitejs/plugin-react": "^4.3.4",
"tailwindcss": "^4.0.0",
"vite": "^6.0.7"
}
}