Skip to content

Velloo documentation

Markdown for LLMs ↗

From design to code

emit_code returns an agent-consumed IR, not paste-ready JSX; 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, but deliberately not a finished file. 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. 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": []
    }
  ],
  "classesUsed": ["bg-background", "text-muted-foreground", "gap-16"],
  "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.
  • 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 are canvas-only data) and interactivity (designs are static by construction; handlers are yours to write).

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).
  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