Velloo documentation
Markdown for LLMs ↗Port an existing page
Code-to-design. Seed the theme from your app's stylesheet, rebuild the structure on the canvas, and verify fidelity against the live URL.
The reverse direction: your app already has pages, and you want them on the canvas, so redesigns happen in Velloo instead of in scattered JSX edits, and the design becomes the source going forward.
The guiding rule is re-express, don’t clone. A ported screen strips handlers, state, and data fetching, inlines representative copy as literals, and keeps the visual structure. Designs are static by construction; a data-heavy dashboard ports as its structure plus fixture copy.
Set Velloo up for the app first
Once per design, before the first screen, have your agent set the design up for the app. It is what makes everything after it trustworthy, and it is cheapest while nothing is drawn yet. In Claude Code that is /velloo:setup; in any other agent, say “set Velloo up for my app”. Four steps:
- Theme and fonts.
import_themethe app’s stylesheet and work through itsnotImportedlist.velloo initalready ran the same import, so on a fresh design the dry run mostly confirms it. Step 1 below has the details. - The preview entry.
preview_statussays whether your app’s own components have the providers and stylesheets they need. When the state is notvalid,set_preview_entry {}writes the suggested entry: the app’s own entry rewritten for a canvas, with an in-memory router in place of the real one. - Every component, in one render.
check_componentsmounts each component the app gives the design at the props its call sites pass, and returns contact sheets to look at. Fix what the problems have in common first: several components whose variants all draw the same is usually one stylesheet the preview entry doesn’t load, or colours the theme lacks. - The design language.
set_design_languagerecords what the design is going for and the rules the code visibly holds to. You review and edit them on the canvas. See Theme your design.
An app with no components of its own, only pages and a router, skips steps 2 and 3. The agent finishes by telling you what is exact, what is adapted or stands in, and what it could not verify.
1. Seed the theme
Match the palette before composing anything, or every color decision you make will be against the wrong background. import_theme reads the app’s stylesheet. Note that globals.css lives outside the design folder, so pass an absolute path or one relative to the host app root (css takes the stylesheet text instead):
{ "tool": "import_theme", "args": { "cssPath": "app/globals.css" } }
What it parses:
- shadcn-convention
:root/.darkcustom properties: raw HSL triplets or any CSS color - Tailwind v4
@theme--color-*variables, withvar()indirection resolved, plus--radiusand--font-*roles - The stylesheets it imports: given a
cssPath, relative@imports inside the app are followed, so an entry sheet that only imports./theme.cssyields that file’s tokens.importedStylesheetslists what was read. Packages and URLs are never followed tailwind.configtheme.extend(found near thecssPath): brandcolorsland in the theme’spalette, namedspacingbecomes spacing tokens (sow-icon-railresolves),boxShadowbecomes shadows,fontFamilybecomes font roles,keyframes+animationbecome--animate-*
Beyond the semantic slots, every other custom color var (numeric scales like --primary-600, extra roles like --success-500, bare brand names like --ink) is captured into the palette.* passthrough namespace, so app classes like bg-primary-600 or bg-ink render literally on the canvas instead of falling back. The result also reports the app’s Tailwind container config as container.suggestedClasses. Centered/padded/capped layout doesn’t transfer as a theme, so wrap page content in a Box with those classes.
It dry-runs by default; review the changes list, then re-run with apply: true. Read coverage as well as the change count: an app whose variables follow its own naming rather than shadcn’s maps zero semantic slots, parks every color in palette.*, and leaves colors.background, primary, and the rest on the starter palette. Map those yourself with set_theme { tokens } before composing. Details in the theme guide.
Then read notImported: what the app states that did not reach the theme, each with its reason. A font the import could not trace, a body font the app never assigns to sans, or a dark palette the stylesheet does not define all appear there. An empty list means the import left nothing it saw behind.
Match the app’s real fonts as well as its colors. Fonts that next/font sets at runtime are in no stylesheet, so the import follows each one to its loader call and sets the role, family, and weights; fonts in the result names them. Set any it could not trace with set_theme { fonts }. When the repo has a design-system DESIGN.md, read its rules and optionally layer its tokens over the stylesheet with import_theme { designMdPath }. Theme guide →
Pages behind a login
When the page sits behind a login, on staging, or on a site you don’t run, capture it first. start_capture_session opens a real browser window that you drive:
{ "tool": "start_capture_session", "args": { "url": "https://staging.example.com/dashboard" } }
It returns a sessionId immediately and does not wait. Log in, press Capture page in the Velloo toolbar on each page you need, then Done; the agent polls list_captures meanwhile rather than calling start_capture_session again. From a terminal, velloo capture https://staging.example.com/dashboard opens the same kind of session (velloo capture list browses stored captures, velloo capture delete <id> removes one).
get_capture { captureId } then returns the evidence: absolute files paths including page.png (the authoritative reference), a structural outline with each node’s rect and HIDDEN / OFFSCREEN markers, fonts, downloaded image assets, and themeCss, the page’s real custom properties. Pass that CSS to import_theme as css before composing. The session cookies never reach the agent, and captures live outside the design folder.
2. Rebuild the structure
Now compose the screen, the same compose-and-verify loop, with the page’s source open next to list_components.
compose reads the JSX your app already holds, so a page does not have to be flattened by hand first. Point it at the file:
{ "tool": "compose", "args": { "screenId": "dashboard", "mode": "replace", "file": "app/dashboard/page.tsx" } }
The file is read as data and nothing in it executes. const data, .map, cond && <X />, components defined in the same file, and data imported from the app’s own files are written out as plain elements. Event handlers are dropped and reported in sourceNotes. A value nothing holds, such as a fetch or a package’s data, is refused by name, with its line and column. A Next app-router page is composed inside the layout files above it, as the route shows it. compose →
A few porting-specific rules:
- Build into scanned screens. If
velloo initscanned the app’s routes, it already created one placeholder screen per route (id = route slug). Rebuild it in place withcomposeinmode: "replace", which swaps the placeholder tree for yours in one call;add_screenfor a scanned route returnsScreenIdConflict. - Keep Tailwind classes verbatim on a shadcn app. It shares Velloo’s component vocabulary, so most refs map one-to-one and the classes travel unchanged.
- Use the app’s own component before rebuilding it. The page you’re porting is built from components Velloo has probably already found: check
list_componentsfor the Repo shelves first, and compose the realFeatureCardrather than reassembling it from primitives. A repository component renders from your own installed copy and emits its exact import, which is the shortest path to a faithful port. Runpreview_statusonce before you start so the mount is known-good. - Keep the page editable. Compose the app’s sections, cards, and controls rather than placing the entire page as one opaque component. The
screen/opaquediagnostic flags this shortcut even when the screenshot matches. - Then snippet, then extension. A presentational pattern you want to reuse and edit in one place becomes a snippet with typed params; snippets render for real. A dynamic leaf that can’t render as a tree of nodes — a chart wired to your data layer — becomes an extension registered with its real import path, optionally with live rendering so Velloo bundles your actual component.
- Replace
dark:variant patterns with semantic tokens. Velloo’s dark mode swaps token values, not a class, sodark:variants are inert on the canvas:bg-white dark:bg-backgroundbecomesbg-background.
3. Check fidelity against the live URL
compare_to_url renders your screen and captures the running app page at the same viewport, then pixel-diffs. What to diff against goes in source; the render size goes in viewport (default: the folder’s Desktop preset):
{
"tool": "compare_to_url",
"args": {
"screenId": "dashboard",
"source": { "url": "http://localhost:3000/dashboard" },
"viewport": { "w": 1440, "h": 900 }
}
}
The result carries similarity, changedRatio, heightDelta, and contentHeight, plus contentSimilarity when the heights differ. topMismatches ranks the worst diff regions and names the node responsible for each (so “the sidebar is off” arrives as a node reference, not a guess), and styleDiff, when both sides could be measured, lists the computed properties behind those regions (design versus page). A side-by-side PNG comes back too (URL on the left, Velloo on the right; pass image: false for metrics only).
Useful knobs:
mode: "dark"renders the Velloo side dark and best-effort drives the target page dark (prefers-color-scheme,.dark/data-themeon the html element,localStorage.theme), so dark fidelity checks against the app’s real dark theme.source.settleTimeoutMs(default 8000) caps the network-quiet wait before capture. Raise it for data-heavy pages that paint a spinner first.source.auth:storageStatePath(a Playwright storage-state JSON),cookies, orlocalStorageget you past a login wall to the real page.source.cache:freeze: truecaptures a dynamic page (feed, dashboard, per-user content) once and diffs every later call against that frozen reference, so drifting content doesn’t makesimilarityjitter.ttlMsbounds staleness (default five minutes);refresh: truere-samples after you change the app.
For a page you captured in a browser session, diff against the stored capture instead of the live URL. It is already past the login and frozen, so it can’t bounce to a login page or drift between calls; the viewport comes from the capture:
{ "tool": "compare_to_url", "args": { "screenId": "dashboard", "source": { "captureId": "<id from list_captures>" } } }
Two cases are not treated as a problem. A sign-in, sign-up, or password-reset page is not called an auth wall, and neither is any page whose design has a password field of its own. A design that is itself a loading state, with an aria-busy region or a “Loading…” label, is compared against the page as it loads instead of waiting for it to finish.
4. Iterate, then flip the source of truth
Fix what topMismatches points at, in order, re-compare, repeat. Calibration:
- 0.85+ similarity is a faithful structural port.
- Don’t chase 1.0. Fonts and imagery legitimately differ; the headless render substitutes some emoji glyphs and doesn’t fetch remote images, so a hotlinked avatar shows a fallback box. Use
upload_assetplus a localImageorPlaceholderfor art that needs to be pixel-faithful, and let the rest go.
Once the port is faithful, the screen is the design source going forward: redesign on the canvas, verify with screenshots, and push changes back with emit_code.