Skip to content
From design to code

Documentation

Velloo documentation

Markdown for LLMs ↗

From design to code

emit_code returns JSX or native HTML as agent-consumed IR; your agent reads it alongside your app's conventions and writes the real file.

Velloo’s designs are made of your real components, so the last mile is short. But it’s still a mile: your app has routing, an import alias, a formatter, maybe a data layer. Velloo doesn’t guess at any of that.

The philosophy: IR, not paste

emit_code returns a structured intermediate representation: JSX-shaped for React providers or native HTML for HTML/htmx, with integration into your app still handled by the agent. No imports, no function wrapper, no formatting pass. Your agent reads the IR alongside your app’s existing code and conventions, then writes the real file in your style: the right import paths, the right page wrapper, your prettier config, your lint rules.

This is a feature, not a limitation. Paste-ready output has to guess at your conventions and gets them wrong; an IR plus an agent that has read your codebase gets them right.

{ "tool": "emit_code", "args": { "screenId": "pricing" } }

What the IR contains

The result is pure data; nothing was written to disk. It arrives as two blocks: a JSON description, then the JSX as a text block of its own, verbatim, so there is no escaped string to undo before typing it. Shown together, a shadcn screen looks like this:

{
  "screen": { "id": "pricing", "name": "Pricing" },
  "jsx": "<div className=\"flex flex-col gap-16 bg-background px-6 py-24\">…</div>",
  "componentsUsed": ["Badge", "Box", "Button", "Card", "Heading", "Text"],
  "iconsUsed": ["Check", "Sparkles"],
  "snippetsUsed": [
    {
      "id": "pricing-tier",
      "componentName": "PricingTier",
      "params": [
        { "name": "title", "type": "string" },
        { "name": "highlighted", "type": "boolean", "default": "false" }
      ],
      "jsx": "<Card className=\"…\">…</Card>",
      "componentsToInstall": ["card", "badge"],
      "helpersToMaterialize": [],
      "warnings": []
    }
  ],
  "componentsToInstall": ["badge", "button", "card"],
  "helpersToMaterialize": [],
  "warnings": []
}
  • jsx is the screen body, using the library’s own identifiers and the framework’s native styling idiom: Tailwind classes verbatim for a shadcn folder, sx objects importing from @mui/material for a MUI folder. Tailwind classes come pre-consolidated: no duplicates, deterministic merge order.
  • componentsUsed / iconsUsed: what to import. Library ids map to your components alias; icons import from lucide-react.
  • snippetsUsed lists every snippet the screen references, each with its own IR: a PascalCase componentName, typed params, and a JSX body. The agent decides whether to materialize each as a real component file or inline the subtree.
  • classesUsed: the unique Tailwind classes in the output, for verifying your app’s Tailwind setup covers everything. It is returned only when you pass classesUsed: true, because every class is already in the code, on the element it styles.
  • componentsToInstall: shadcn primitives the screen uses, as kebab names ready for npx shadcn@latest add <names>. Compare against your app before installing; list_components reports installedInApp per component.
  • helpersToMaterialize lists the Velloo composition helpers that carry runtime logic and need authoring in your app: Gradient, SVG, Image, Layer, Divider. Box, Stack, Container, Heading, Text, Prose, and Placeholder lower to plain HTML elements, and Icon to a lucide-react import, so they never appear here.
  • warnings: non-fatal caveats where something couldn’t be expressed faithfully in JSX and needs agent attention.
  • tailwindV3Compat is present only when the host app is on Tailwind v3: v4 classes from the design (screen and snippet bodies) to rename or rework while writing the file, such as v4 shadow-sm ⇒ v3 shadow.
  • diagnostics is present when the full-screen check finds something: invalid Tailwind utilities, undefined CSS variables, v3 incompatibilities, raw colors that won’t theme-flip, or a component that threw while rendering, each with a node path. Fix them on the canvas before writing the file.

On a MUI, Chakra, or Ant Design folder, or a no-framework folder with inline styles, componentsToInstall and helpersToMaterialize are always empty (there is nothing to shadcn add) and tailwindV3Compat never appears.

Two things never emit: board layout (frame positions, sizes, groups, and flow arrows are canvas-only data) and application logic (handlers are yours to write). HTML output preserves declarative hx-* attributes, while the canvas leaves them inert.

Lists come out as lists

A design tree stores every repeated element written out: four stat cards are four subtrees. The code you would write for them is one template over an array, so that is what emit_code returns. A run of three or more look-alike siblings is emitted once, as a .map over the values that differ:

{[
  { className: "bg-card", text: "Active users", text2: "8,420" },
  { className: "bg-card", text: "Revenue", text2: "$48.2k" },
  { className: "bg-destructive/10", text: "Churn", text2: "1.9%" },
].map((item, index) => (
  <div key={index} className={`rounded-lg border p-5 ${item.className}`}>
    <p className="text-sm text-muted-foreground">{item.text}</p>
    <span className="text-3xl font-bold">{item.text2}</span>
  </div>
))}

Values that never vary stay in the template, and nothing is folded where the result would not be shorter. The rows are the design’s sample content; your agent replaces them with the app’s data. Pass fold: false to get every sibling written out.

An icon the design never sized is written with size={16}. The canvas draws icons at 16px and lucide-react defaults to 24, so the explicit size keeps the code matching the canvas.

Implementing a flow

When a board is drawn as a flow, the arrows are the spec for what emit_code leaves out. Nothing is emitted for them. The agent reads the board, emits each screen, and writes the behaviour itself:

  1. get_board with mode: "flow" returns the steps, each step’s states, the transitions between them, and where the flow starts and ends.
  2. emit_code for each step’s screen, and for each state’s.
  3. The agent writes the routing, requests, and state handling in your app’s idiom: a route per step, using the step’s route where it has one, and the step’s states inside that route’s component. A state with after follows that state rather than the default: loading, then the error.

A gaps entry in the flow means the design is missing a state, such as a step that loads but has no error state. Settle it on the canvas before writing the code.

HTML and htmx output

For an HTML screen, emit_code returns format: "html", html, classesUsed, hostRoutes, and warnings, alongside the screen identity and any diagnostics. Native attributes and hx-* requests remain in the markup. Your agent integrates it into the server’s templates, replaces sample content with data, and implements the required routes. HTML snippets use $name markers for the agent to translate into template variables.

velloo emit contacts --to contacts.html  # native markup
velloo emit contacts --to contacts.json  # full HTML IR

See Design an HTML or htmx app for stored CSS, assets, and capture setup.

Snippets as components

emit_snippet is the same idea scoped to one snippet, useful when you want a design-system piece as its own file without emitting a whole screen:

{ "tool": "emit_snippet", "args": { "snippetId": "pricing-tier" } }

Returns the snippet’s id, PascalCase componentName, typed params (optional params emit as name?), the JSX body, and its own componentsToInstall, helpersToMaterialize, and warnings, plus tailwindV3Compat and diagnostics when they apply. Snippets emit with a typed className?: string prop, so instances can keep their per-instance class overrides.

The import alias

The IR mentions an import prefix for snippet identifiers, @/components/ui by default. Set it once in the design folder’s config (codegen.componentsAlias in .design/config.json, see Config) or override per call:

{ "tool": "emit_code", "args": { "screenId": "pricing", "componentsAlias": "~/components/ui" } }

It’s a hint. The agent picks the actual import path from your app’s conventions; that’s the whole point of the IR model.

CLI equivalents

The same paths exist without an agent in the loop:

velloo emit pricing                      # print header comments + the JSX body to stdout
velloo emit pricing --to pricing-ir.json # write the full IR as JSON
velloo render pricing --to pricing.png   # headless render — .html or .png

velloo emit takes a screen id or a path to a screen JSON file; with neither, it picks one interactively. --design selects the design, and --components-alias mirrors the tool argument. The stdout header lists the screen, components, lucide icons, snippets, and any Tailwind v3 renames; --to writes the IR as JSON, including tailwindV3Compat when it applies. The CLI does not run the diagnostics pass; use emit_code for that. velloo render renders a screen outside the canvas entirely: --to picks .html (no browser needed) or .png (headless Chromium), and --w/--h set the viewport (default 1440×900).

End to end

A realistic pass, with your agent driving:

  1. Design the screen on the canvas (the compose-and-verify loop) until the compare screenshot looks right in both modes.
  2. The agent calls emit_code for the screen (and emit_snippet for any snippet it wants as its own file). For a flow, it reads the board first and emits every step and state.
  3. It reads the IR next to your app: route structure, import alias, existing components. It runs npx shadcn@latest add for whatever componentsToInstall names that the app is missing, and applies any tailwindV3Compat renames.
  4. It writes app/pricing/page.tsx: real imports, your page conventions, snippets materialized as components where that earns its keep.
  5. It runs your app’s own lint and format commands over the new file. Velloo has no prettier pass on purpose; your formatter is the source of truth.

The design stays in the folder. When the page changes, edit the design and emit again: the screen is the artifact you iterate, the file is a projection of it.

Next