userland.run70Try the demo
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

dev server

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");
});
loading…
Expected output
animated illustration

How it runs

  1. The built dist/ (HTML, one JS and one CSS file) is written to /work/dist/.
  2. server.js serves it on port 3000 from the host-engine Node tier.
  3. 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"
  }
}

Further reading

Other examples