Skip to content
Repository components

Documentation

Velloo documentation

Markdown for LLMs ↗

Repository components

Your app's own components — from its dependencies or its own source — discovered, rendered for real on the canvas, and emitted with their exact imports.

The components in a Velloo screen don’t have to come from a library Velloo adapts. They can be your app’s own: the Mantine or NextUI package it depends on, a private design system, or the components/ directory you wrote last week. Velloo finds them by reading what the app renders, draws them on the canvas from the app’s own installed copy, and emits them with their real import paths.

These are repository components, and they sit alongside the folder’s provider rather than replacing it. A shadcn folder still has shadcn’s Button; an app that also ships its own StatCard gets that too, on its own shelf.

Why this exists

A design tool that only knows five libraries is wrong about every app that isn’t one of them. The old answer — approximate the component with primitives and remember its real name for code emission — kept implementation intent but produced a canvas that didn’t look like the product.

Repository components remove the approximation. The component that renders in the frame is the component that ships.

Discovery

Velloo starts at the app’s entries and routes — Vite’s index.html and src/main.*, Next’s app/**/page and pages/** — follows local imports and barrel files, and records every JSX element it finds whose binding resolves to a direct dependency or to the app’s own source.

Three properties are worth internalizing, because they explain most of what you’ll see:

  • It reads text; it never runs your code. Discovery is a source scan. Nothing from the repository executes during it.
  • It never walks node_modules. A package contributes the components your app actually renders from it, not its entire export surface. Install Mantine and use four components, and four components appear.
  • It leaves an adapter’s own library alone. MUI’s components belong to the MUI adapter, and shadcn’s ui/ directory to the shadcn one, so they aren’t cataloged twice.

Server-only modules are skipped, since they can’t run in a browser. To bound the scan explicitly, hostApp.components.include adds component roots and hostApp.components.exclude drops specifiers or paths; see the config reference.

What Velloo extracts

For each component it finds, Velloo reads the host’s own declarations — a package’s .d.ts, a local .tsx — for a <Name>Props type, literal unions, @default values, and JSDoc. Compound parts (Tabs.List, Tabs.Tab) come from staticComponents and from how the app uses them. Preview states come from Storybook args and from the app’s real call sites, so the Library shows the component with plausible props rather than empty.

Each entry also carries provenance — where in the app it’s used — and, when it reads data for itself, the hooks it calls. That last one matters: a component that pulls from a store or a context won’t be filled by props alone, and the catalog says so instead of leaving you to discover it in a blank frame.

The catalog also says what a component is:

  • What it is for comes from the doc comment its author wrote above it. list_components shows it in the component’s notes, which makes those comments the best evidence for a design’s written rules.
  • Icons are counted, not listed. An app that uses sixty of a pack’s icons has sixty entries and none of them is a building block, so the catalog reports each pack with a count. Any icon the pack exports can be placed by name.
  • A flat-named part belongs to its root. PanelBody, exported beside Panel from the same module, is filed as a part of Panel, the way Tabs.List is a part of Tabs.
  • One module is one component, however call sites spell its path. A component imported as both @/components/panel and ./src/components/panel is listed once, and a design saved under either spelling is the same component.

Inference isn’t always right. repo-components.json in the design folder is the checked-in correction layer:

{
  "components": {
    "@mantine/core#Button": {
      "description": "Primary action. Use `variant=\"filled\"` for the main CTA."
    },
    "./src/components/stat-card#StatCard": {
      "states": {
        "Loading": { "value": null, "loading": true }
      }
    },
    "./src/components/internal-debug#DebugPanel": { "exclude": true }
  }
}

Overrides refine what discovery inferred; they never replace it. A key that no longer matches anything discovered is reported as stale rather than silently ignored.

How a node stores one

A repository component is an ordinary component node carrying a $repo identity:

{
  "$ref": "Tabs",
  "$repo": { "importPath": "@mantine/core", "exportName": "Tabs" },
  "props": { "defaultValue": "overview" },
  "children": [
    {
      "$ref": "Tabs.List",
      "$repo": { "importPath": "@mantine/core", "exportName": "Tabs", "member": "List" }
    }
  ]
}

$ref stays the JSX name; $repo carries importPath, exportName, an optional member for a compound part, an optional app in a monorepo, and an optional proxy naming a snippet to draw when the real component can’t render.

Because identity lives on the node, which component renders is never a question of registry order. Mantine’s Button and the provider’s Button can coexist; when two catalog ids would collide, the repo one is qualified (Mantine.Button), and list_components says why. A bare name means Velloo’s component, unless the composed source imports it: a Stack imported from "@mantine/core" is Mantine’s, when the catalog has that module and export. When a write gives bare Text props only the app’s Text takes, and exactly one app component of that name takes them, compose writes the app’s component and reports it under appComponents.read. When more than one could be meant, the write is refused with ShadowedComponent, naming the qualified tag to use. This is the usual result of pasting emit_code output back into compose: emitted JSX is app code, where the bare names are the app’s.

Compose with them by id, exactly like any other tag:

{
  "screenId": "settings",
  "mode": "append",
  "jsx": "<Tabs defaultValue=\"overview\"><Tabs.List><Tabs.Tab value=\"overview\">Overview</Tabs.Tab></Tabs.List></Tabs>"
}

The preview entry

Your components need the context your app gives them: a MantineProvider, a theme, a query client, a global stylesheet. The preview entry supplies it — preview.tsx in the design folder (or preview.<app>.tsx for a named host app in a monorepo).

It’s a normal module whose default export receives { children, colorScheme, theme, recipeTheme } and returns the wrappers your components need, with the app’s global CSS imported at the top:

import "../src/styles/globals.css";
import { MantineProvider, createTheme } from "@mantine/core";
import "@mantine/core/styles.css";

export default function Preview({ children, colorScheme, recipeTheme }) {
  return (
    <MantineProvider theme={createTheme(recipeTheme ?? {})} forceColorScheme={colorScheme}>
      {children}
    </MantineProvider>
  );
}

When an imported stylesheet uses Tailwind directives (@tailwind, @apply, @theme, @config), Velloo expands it with its embedded Tailwind against the app’s tailwind.config before the canvas loads it. A stylesheet it can’t expand loads as written, and preview_status names it.

Agents set this up once per folder with two operations:

  • preview_status reports whether an entry is in place and proves it by mounting a real component in a headless browser. It returns state (absent, valid, incomplete, or failing), the wrappers and stylesheets the app’s own entry uses, and a suggestedPreviewEntry to adapt.
  • set_preview_entry writes the file and re-runs that check, so the answer is a verified mount rather than a guess. With no source, it writes the suggested entry: the app’s own entry rewritten for a canvas, with its providers in their nesting order, an in-memory router, a query client, and auth providers left out.

A third checks the result across the whole app:

  • check_components mounts every component the app gives the design in one render, at the props its call sites pass, and returns contact sheets to look at. A component with a variant prop is mounted twice, differing in that prop alone; two mounts that paint the same are flagged variantsIdentical, which is how a stylesheet or theme colour that never reached the canvas shows up. check_components →

Keep the preview entry free of network calls and credentials. It runs in your local browser during design; treat it as a fixture, not a bootstrap of the real app.

Recipes

For a popular library, Velloo can supply the preview entry itself. A recipe is the small built-in form: the default wrapper and stylesheet imports resolved against your app’s own install, a mapping from Velloo’s tokens to the library’s native theme, design-time adaptations that keep overlays inside the frame, the style props its components accept, and a stylesheet probe.

Mantine is the first recipe. A Mantine app gets a working canvas with no preview.tsx at all; write one when you want the app’s own theme, router, or data providers instead.

Which recipe applies depends on what each component’s own app resolves, not on the folder’s adapter. A Mantine app in an MUI or shadcn folder gets Mantine’s theme mapping and adaptations, and emit_theme writes a native theme module for each component source.

A recipe is deliberately not an adapter. An adapter owns styling, theme projection, and codegen idiom for a library Velloo knows deeply; a recipe just supplies context so the app’s own copies render. Most libraries need the second and not the first.

Fidelity is reported, never assumed

Each repository component falls back on its own. One component that can’t render leaves the rest of the screen mounted and real — it alone becomes its proxy snippet or a labelled frame.

component_status reports each one with a status and a stable code:

Status Meaning
exact The app’s own component, rendered from its own install
adapted Rendered with a design-time adaptation (an overlay kept inside the frame)
unstyled It rendered, but its stylesheet wasn’t loaded — so the result can’t be called exact
proxy It couldn’t render; the snippet named in $repo.proxy stands in
unavailable It couldn’t render and has no proxy; a labelled frame stands in

A status nothing has observed yet says so: an exact that only passed the build check carries observed: false until a mounted frame confirms it. The canvas shows the same verdict as a fidelity badge on the component in the library and the inspector.

When something isn’t exact, a code says why — missing-provider, render-threw, missing-export, compile-failed, server-only, resolve-failed, unstyled — and a remedy says what to do about it. missing-provider almost always means the preview entry needs another wrapper.

The unstyled case is worth calling out: a component that renders without its CSS looks like it worked. A recipe’s stylesheet probe is a DOM check that fails when the library’s CSS is absent, which is what turns a false exact into an honest unstyled.

Emit

Codegen prints a repository component as itself: its authored props, its children, and its exact import via repoImports. Nothing is translated into another styling system, because nothing needs to be — the component in the design is already the component in the app.

When to reach for something else

  • A snippet is still the right tool for a composition you want to reuse across screens and edit in one place. It’s a Velloo-side abstraction, not an app component.
  • An extension remains the opt-in path for a dynamic leaf that can’t render as a normal node — a chart wired to a live data layer — where you want a bundled visual island rather than a tree of nodes.
  • $emitAs still loads on existing folders, and the port-a-page flow still uses it where a preview is deliberately approximate. For an app component that can simply render, prefer the repository catalog.

Trust boundary

Repository code runs only in your local browser, inside the canvas mount. Publishing mounts each screen that uses your app’s components there and uploads the markup it drew, beside your app’s stylesheet and the preview PNGs. Velloo Cloud shows that markup and never executes your application code, so a share is the same picture as your canvas. Nothing in the uploaded markup runs.

Freezing a screen needs the headless browser (velloo browser install). Without it at publish time, a share falls back to proxies and labelled frames for those components, and velloo publish warns.

Next