---
name: vetta-ui-design
description: "Build and edit design documents (.vetd) on the Vetta design canvas — app screens, landing pages, slides, posters, infographics. Use when the user asks for a UI design, mockup, screen, deck, or poster, or attaches a design frame/element from the canvas. Frames are real React (TSX) routes, not pictures. Do NOT use for ordinary front-end work in the user's own codebase — building, styling or reworking pages of a real app is that repo's job, and none of the vetd_* tools or canvas rules here apply to it."
---

# Vetta UI Design

A design document is ONE directory — `login-app.vetd/` — holding everything:

```text
login-app.vetd/
  design.json           ← canvas manifest. GENERATED — see below.
  frames/               ← one TSX file = one canvas frame = one route
    index.tsx           ← the site root "/"
    login.tsx           ← "/login"
    _layout.tsx         ← optional shared shell; `_` files are NOT frames
  components/           ← shared React components
  assets/               ← images, imported relatively
  theme.css             ← color/radius/shadow tokens (Tailwind v4 @theme)
  package.json          ← this design's npm dependencies; vetd_install writes it
  DESIGN.md             ← optional spec; OVERRIDES defaults in this skill
```

You write the sources; the plugin owns the manifest and reconciles
automatically. Every save hot-reloads the canvas. Two consequences worth
internalising before you start:

- **The file name IS the route and the frame id.** Create a screen by writing
  `frames/<kebab-id>.tsx`, delete one by deleting the file. There is no
  registration step anywhere.
- **Editing `design.json` is pointless, not just forbidden.** It is
  regenerated from your tsx declarations on every save, so any hand-edit is
  overwritten seconds later. If a frame is not showing up the way you expect,
  the answer is always in the tsx.

## It is a project, not a picture

The output is a running front-end app the user can click through in preview, built
from sources meant to ship. Decide structure BEFORE writing screens:

1. **Routes** — `frames/login.tsx` is `/login`, `index.tsx` is `/`.
2. **Navigation** — `import { Link, useNavigate } from "react-router"`.
3. **Shared chrome written once** — nav bar / sidebar / tab bar goes in
   `components/`, or in `frames/_layout.tsx` rendering `<Outlet />` when it must
   survive navigation. Never paste it into each frame.
4. **Shared vocabulary** — colors/radii/shadows from `theme.css` tokens
   (`bg-primary`, not `#0d99ff`); repeated blocks become components.

If changing the nav bar means editing more than one file, the structure is
wrong. Posters, slides and infographics are standalone artwork — steps 1-3 do
not apply to them.

## The one rule you have to hold in your head

**Frame meta is the FIRST statement**, one flat line (regex-parsed):

```tsx
export const frame = { width: 390, height: 844, title: "Login" };
```

Every frame declares its own. Omit it and the frame is rendered at a size
inferred from the rest of the design — it still reaches the canvas, but nothing
about that size is yours, and `issues` will keep asking until you declare one.
Sizes come from the product type below; when the design already has frames,
`vetd_status` lists their sizes and the design's `defaultFrameSize` — match them
unless the new screen is a different product type.

It gets its own section because it is the only convention here that needs
judgment from you. Everything else is either caught automatically or visible in
the render.

### Caught for you — copy the templates and these never come up

A checker parses every source you write; violations come back as `issues` on
`vetd_screenshot` and `vetd_status`, naming the file and line. A file that does
not parse, or that imports a package the engine does not have, comes back the
same way. The templates below are already correct on all of it:

- `h-full`, never `h-screen`/`min-h-screen` — a frame is a fixed-size canvas,
  not a viewport.
- Icons are Iconify classes — a `<span>` with a class, NOT a component:
  `<span className="icon-[lucide--search] size-4" />`. There is no icon package
  to import: `import { Search } from "lucide-react"` fails to build, and so does
  every other icon library. Never your own icon component, never inline SVG
  paths. Offline sets, and only these: `lucide`, `tabler`, `mdi`,
  `simple-icons` — naming any other set fails to build too.
- `<Link to>` from react-router for internal screens, never `<a href="/…">`, and
  never a locally redefined `Link`/`useNavigate`/`useLocation`.
- No remote image URLs (`<img>`, `background-image`) — they break screenshots.
  Import from `assets/`, or use a gradient/token color/Iconify glyph.
- Normal formatting, one element per line for nested markup. Everything on one
  line destroys element→source mapping, and the user's "Ask Vetta" edits then
  point every element at the same line.
- One default export per frame, rendering edge-to-edge — no page margins.
- react, react-router, Tailwind v4 and Iconify are always there. Anything else
  has to be installed into the design first (see below) — importing a package
  that is not installed fails the build. No remote font links (Google Fonts
  `<link>`/`@import url(...)`) — typefaces are installed packages, see below.

### Dependencies are yours to choose — and yours to justify

A design is a real npm project: `vetd_install` adds packages to its own
`package.json`, and they travel with the design. `vetd_status` lists what is
already installed — read that before adding anything.

Install when the job is a solved domain problem where a hand-rolled version is
visibly worse: chart geometry and axes, Markdown or rich-text rendering, date
math and calendars, virtualised long lists, gesture/physics animation.

Build it yourself when Tailwind and React state already do it well — cards,
tabs, modals, dropdowns, toggles, steppers, progress bars, a simple bar or donut
drawn with divs or inline SVG, and layout of every kind. A UI kit pulled in for a
rounded card costs more than it gives: it arrives with its own design language
and spends the rest of the design fighting `theme.css`.

Never install an icon package (icons are Iconify classes), a CSS framework
(Tailwind v4 is here) or a router (react-router is here).

Install everything you need in ONE call, then import normally.

### Typefaces are packages too

System fonts alone cannot give a display face and a body face that differ, and
that sameness is a large part of why generated UI looks generated. A typeface is
an npm package here like any other library:

1. `vetd_install` the faces — `@fontsource-variable/<slug>` when the family has
   a variable build (one file covers every weight), else `@fontsource/<slug>`,
   whose bare import is weight 400 only — import each other weight you use
   (`@import "@fontsource/<slug>/700.css";`) or the browser fakes the bold.
2. Import them at the TOP of `theme.css`, above `@theme`. Install first: theme.css
   feeds every frame, so importing a package that is not there yet breaks the
   styling of the whole canvas, not one frame.
3. Declare them as `--font-*` tokens and use the matching `font-*` class.

```css
@import "@fontsource-variable/fraunces";
@import "@fontsource-variable/geist";

@theme {
	--font-display: "Fraunces Variable", "Songti SC", "SimSun", ui-serif, serif;
	--font-sans: "Geist Variable", "PingFang SC", "Microsoft YaHei", ui-sans-serif, system-ui, sans-serif;
}
```

The family name must be the one the package registers: the `-variable` packages
append ` Variable`, the static ones do not. A mismatch silently falls back —
`issues` reports it, as it reports an installed face nobody imports.

Latin faces only. CJK packages are megabytes per weight; Chinese and Japanese
copy uses the platform faces listed after the Latin one in the stack above
(serif: Songti SC / SimSun; sans: PingFang SC / Microsoft YaHei), so every stack
names both.

### The one thing nothing catches: a token that does not exist

Theme tokens in `className` (`bg-primary`, `text-muted`) instead of hex — but
**the token must exist in `theme.css` `@theme`**. Tailwind generates nothing for
a class it cannot resolve, so `bg-brand` without `--color-brand` leaves the
element with no background at all. Same mechanism as a blank icon: the source
reads perfectly, and nothing renders. Add the token first, then use it.

## Structure first, lines second

Reach for whitespace, alignment and surface tone before you reach for a border:
a gap twice the size of the gaps inside a group already reads as a separation,
and a shared gutter already reads as a column. Then, for every line, divider or
card wrapper you are about to draw, answer one question — **what does it do that
space could not?**

The question does not presume the answer is no. A dense table, an object the
user acts on, a layer that overlaps scrolling content and a field that has to be
findable all answer it in a word; a wireframe, a report, a terminal UI or an
engineering console answers it for most of their lines. What it catches is the
line drawn out of habit — the box around every section, the rule under every
heading — which is a generated-UI tell rather than neutral styling.
`references/taste/whitespace.md` has the substitutions, the per-product-type
notes, and the cases where lines are the design.

## Pick the product type

This is the first decision, not an afterthought: `vetd_create` requires it
(`product`, or `frameSize` in pixels for anything the enum cannot express), and
it becomes the design's default size. Read it off the user's own words, in
whatever language they wrote them — a phone app is `mobile`, an admin console or
dashboard is `desktop`. An explicit user instruction always wins. Do not default to a phone frame; a dashboard at 390 wide is the
most common failure.

| Product | `product` | Default size | What matters |
| --- | --- | --- | --- |
| Mobile screen | `mobile` | 390x844 | One primary action; thumb reach; full state coverage |
| Desktop app / dashboard | `desktop` | 1440x900 | Real density; aligned grid; no empty middle |
| Landing page | `landing` | 1440x2400 | hero → proof → CTA rhythm |
| Slide | `slide` | 1920x1080 | One idea; large type; generous margins |
| Poster / social | `poster` | 1080x1440 | One focal point; strong size contrast |
| Square social post | — | `frameSize` 1080x1080 | One focal point; strong size contrast |
| Infographic / chart | — | `frameSize`, free | Honest data, labeled axes, legible legend |
| Print (A4 portrait) | — | `frameSize` 794x1123 @96dpi, 2480x3508 @300dpi | Convert mm→px yourself; 300dpi only when the user asks for print |

The design default only covers frames that forgot to declare a size. A document
can mix product types — a poster next to three phone screens — as long as each
frame declares its own.

## Templates

Start from these three. They are the shape the checker expects; deviating from
them is how the mechanical `issues` above get triggered.

Their copy is English only because this file is; it says nothing about which
language to design in. Frame titles and every label inside a frame go in the
language the USER is writing to you in — same rule as your own replies.

```tsx
// frames/products.tsx — a screen is composition, nothing structural
export const frame = { width: 1440, height: 900, title: "Products" };

import { StatCard } from "../components/StatCard";

export default function Products() {
	return (
		<div className="flex h-full flex-col gap-6 bg-surface p-8 text-surface-foreground">
			<h1 className="text-xl font-semibold">Products</h1>
			<div className="grid grid-cols-4 gap-4">
				<StatCard label="In stock" value="1,284" />
			</div>
		</div>
	);
}
```

```tsx
// frames/_layout.tsx — the shell, mounted once, renders <Outlet /> itself.
// `h-full` here too: this wraps the frame, it is not a page.
import { Outlet, useLocation } from "react-router";
import { NavBar } from "../components/NavBar";

export default function Layout() {
	const { pathname } = useLocation();
	// Screens outside the app shell (login, onboarding) opt out here.
	if (pathname === "/login") return <Outlet />;
	return (
		<div className="flex h-full flex-col bg-surface">
			<NavBar />
			<div className="min-h-0 flex-1 overflow-auto">
				<Outlet />
			</div>
		</div>
	);
}
```

```tsx
// components/NavBar.tsx — the ONLY definition of this chrome
import { Link, useLocation } from "react-router";

const items = [
	{ to: "/", label: "Overview", icon: "icon-[lucide--layout-dashboard]" },
	{ to: "/products", label: "Products", icon: "icon-[lucide--package]" },
];

export function NavBar() {
	const { pathname } = useLocation();
	return (
		<nav className="flex items-center gap-1 px-4 py-3">
			{items.map((item) => (
				<Link
					key={item.to}
					to={item.to}
					className={`flex items-center gap-2 rounded-md px-3 py-1.5 text-sm ${
						pathname === item.to ? "bg-primary/10 text-primary" : "text-muted"
					}`}
				>
					<span className={`${item.icon} size-4`} />
					{item.label}
				</Link>
			))}
		</nav>
	);
}
```

## Workflow

**New document**: pick the product type → `vetd_create` with it → set the
direction → start building. Do NOT offer the user a menu of styles: design
systems are a thing the user opts into from the Design sidebar, not something you
propose. Just design well for what they asked for, deriving the palette and type
scale from the product itself.

If the **project root** has a `design-resources/<slug>/` directory, the user
started this project from that style in the Design sidebar; it is the style
reference pack for this project and following it is the default:

- Read its `INDEX.md` (the file inventory), then its `DESIGN.md` — that spec
  is the style contract for what you are about to design.
- After `vetd_create`, write the pack's `theme.css` content into the design's
  own `theme.css` before building frames.
- **Before your first frame, Read the pack's demo HTML and screenshots too**
  (`INDEX.md` marks which is which). The demo is the spec applied to a full
  page — it answers what `DESIGN.md` alone cannot: real density, spacing
  rhythm, component shapes, how the palette is actually distributed. Skipping
  it is how a design ends up on-token but off-style. They are
  **visual reference only**: study layout and mood, never copy their markup or
  code into frames — frames are React + the tokens you just copied, not HTML.
- The user outranks the pack: if they ask for a different style, follow the
  user. Several packs side by side and no user pick: ask which one first.

The pack stays the contract in later sessions too: adding screens to a design
that grew out of one, re-read its `DESIGN.md` (and glance at the demo) before
designing anything new.

If `DESIGN.md` exists in the design document, the user has already applied a
system — read it first and follow it, and do NOT rewrite it or `theme.css`.

**Set the direction** when neither a pack nor `DESIGN.md` sets the style. Left to
defaults, every design converges on the same thing — the scaffold's indigo and
slate, one system sans at two sizes, centred cards, a three-column feature row —
and users recognise it instantly as generated. Before the first frame:

1. Read `references/taste/directions.md` and `references/taste/whitespace.md`,
   then the file for the product type:
   `references/taste/landing-structures.md` for a landing page,
   `references/taste/app-density.md` for app screens and dashboards.
2. Decide tone, anchor hue, type pairing and structure, and state them in one
   line of your reply — `Direction: technical · cobalt (256°) · Space Grotesk +
   Geist · dense workbench`. The user redirects from that line; no menu.
3. Rewrite every value in the scaffold `theme.css` with the direction's palette
   (keep the token names), `vetd_install` the two faces, and import them at the
   top of `theme.css` — all before the first frame, so no frame is ever built on
   the placeholder palette.

Read `references/taste/color.md`, `typography.md` and `copy.md` when a decision
in them is in front of you, not up front. With a pack or `DESIGN.md` in charge,
skip the direction and still screen every capture against
`references/taste/anti-patterns.md` — no style asks for those.

**Existing document**: `vetd_status` ONCE first. It returns the frame ids/sizes,
`sharedShell` (existing `_layout.tsx` + `components/` — reuse them) and
`issues`. Read `theme.css` and reuse what is there. You do not need to call it
again afterwards — `vetd_screenshot` returns `issues` per frame.

Then, for a multi-screen product, write in two passes:

1. **Skeleton pass.** Write the shared parts (`_layout.tsx`, `components/`),
   then EVERY frame as a short file: the `frame` meta line plus the page's
   structural blocks — header, the grid, section placeholders. A few dozen lines
   each. The whole set lands on the canvas within seconds, so the user sees the
   screen inventory and the layout immediately, and a wrong size or a wrong
   route shows up before you have written any detail.
2. **Detail pass in coherent batches.** Fill in real content for a small group
   of related frames (or the whole set when it is simple), then call
   `vetd_screenshot` once with `frames: [...]`. Read the returned contact sheet
   once and compare the screens together. Use the single `frame` form only for
   a targeted recheck after a fix. Each save still hot-reloads, so the user sees
   progress while one model/tool round verifies several screens.

Single screens, posters and slides skip the skeleton pass — just write the file.

**Going back**: every design keeps its own version history — one version per turn
that changed it, saved automatically, titled with what the user asked for. So
"undo that", "go back to before the nav bar moved", "restore the previous
version" is `vetd_history` then `vetd_restore`, NOT you editing files until they
resemble the old version from memory. Restoring is safe in both directions: it
is saved as a new version too, so a wrong pick is one more `vetd_restore` away
from being corrected — the response tells you where the pre-restore state went.
The user can also do it themselves from the canvas's history panel, where the
versions carry thumbnails.

**Verify visually**: call `vetd_screenshot` with all frames you created or
changed (`frames: [...]`, or `all: true` when the whole design is in scope), then
Read the returned overview image once. You are looking for rendering defects
the code cannot show you — run the checklist in `references/quality.md`, which
starts with the three that account for most of them: **misalignment**,
**unintended text wrapping**, and **classes that resolve to nothing** (a blank
icon, or a surface whose theme token was never defined). A frame that does not
parse is listed as a failed member without discarding the other captures — fix
the reported line and recheck that frame only.

Every successful frame also carries a `verification` observation. If
`stalled: true`, two consecutive checks produced the same image and the same
issues. Do not make a third blind edit: inspect whether the checker assumption
fits the visible result, and report the stalled finding when it is a false
positive. `vetd_status.issues: []` is source-only and is never visual proof;
`renderVerification` tells you whether prior captures are current or stale.

Nothing will remind you. Writing a file tells you it was written, not that it
renders — the checker only speaks through `vetd_screenshot` and `vetd_status`,
and neither runs by itself. A frame you wrote but never included in a screenshot
batch may be
sitting on the canvas as a build error or at a size you never chose, and the
turn will end that way: the user is looking at the canvas, so they see it and
you do not. Batch verification keeps that safety without spending one model
round and one image Read per frame.

**Check the structure**: for a UI product with more than one screen, or with
chrome that repeats across screens, answer these four before reporting back.
They are the ones no checker can see; skip them for posters, slides and
single-screen designs.

1. **Chrome defined once?** Grep for the nav bar's markup — it must appear in
   exactly one file. Two frames both containing the sidebar means extract it.
2. **Shell survives navigation?** Persistent chrome belongs in
   `frames/_layout.tsx`. Per-frame `AppShell` is acceptable only combined with
   `<Link>` navigation, otherwise nothing persists at all.
3. **Props actually match?** Nothing typechecks these sources at runtime, so a
   renamed prop fails silently — a component taking `name` but always called
   with `icon` renders one fallback everywhere, and the source reads fine.
4. **Every touched frame included in the latest batch, and its overview actually Read?**

**User notes on the canvas**: the user pins Figma-style notes onto frames (or
onto empty canvas) — each is a request addressed to you. `vetd_notes` lists the
pending ones: every note carries its thread, a source anchor re-resolved at read
time (`element.source` = `frames/x.tsx:LINE` — the authoritative edit target
unless `anchorStale`), and per-frame screenshots where numbered pins mark the
exact spots. Take them **one at a time**: edit for one note, verify it with
`vetd_screenshot`, then immediately `resolve` that one note before you start the
next — never hold the replies back until every note is done. The user is watching
the canvas, where each bubble flips to resolved the moment you reply, and a turn
that dies halfway has to leave the finished ones already marked. That reply is
what marks a note handled; never touch `.notes.json` yourself.

Each `resolve` response tells you what is still pending — when notes remain it
lists them in full, anchors and annotated screenshots included, so you can go
straight into the next one without another lookup. Treat `pendingRemaining` as
your loop condition: keep handling and resolving until a resolve comes back with
`pendingRemaining: 0`. Anything that appears there but was not in your original
list is something the user pinned while you were working. Notes reach you three ways: the
user sends an explicit "handle my notes" prompt, `vetd_status` reports a
`pendingNotes` count, and `vetd_screenshot` flags pending notes on the frame it
just shot.

Notes are also how the user talks to you **while you are working**: rather than
interrupt your turn with a new message, they pin the request onto the canvas and
let you pick it up. So a note can appear at any moment, including after your last
screenshot — which is exactly why the Done check below runs `vetd_notes` even when
nothing this turn mentioned notes, instead of waiting for some tool response to
name one. That check is the only way those notes ever reach you: nothing will send
you a follow-up message about them, so a note you skip just sits there unanswered.

Every instruction the user gives from the canvas becomes a note, so a message
asking you to handle canvas notes never carries the request text itself — the
text lives in the notes, and `vetd_notes` is where you read it. You finish each
one by replying through `vetd_notes`'s `resolve`, which is what puts your answer
back on the canvas where the user asked and clears the pending badge.

**Everything from the canvas arrives as a note.** Whether the user pinned it with
the note tool or typed it into the badge on a selected frame or element, it lands
in `.notes.json` and reaches you through `vetd_notes` — there is no separate
attachment format to learn. A note anchored to an element carries the exact
`frames/xxx.tsx:LINE` in `element.source` (re-resolved at read time, authoritative
unless `anchorStale`) plus its DOM path, classes and text, which is your edit
target. If that location points into `components/`, the change hits every frame
using it — say so in your reply.

**Done** means: every frame you touched is represented in the latest screenshot
batch and its overview was Read, each current frame capture is free of the three
screenshot defects and of the generated-UI tells in `references/quality.md` § 5,
its per-frame `issues` came back empty (or a repeated false positive is
explicitly reported as stalled), and no user note is left pending.

That last one has a hard rule, because nothing outside this turn will catch a
miss: **when you touched a `.vetd` design this turn, the final action — after the
work, right before you write your summary — is one more `vetd_notes` call.** Run
it even when nothing this turn mentioned notes. Anything still pending is work the
user asked for while you were busy: do it now, `resolve` it, then check again, and
only report once a check comes back clean. Do not report first and leave it for
"next turn" — the user deliberately did not interrupt you with a message, precisely
because you were going to look here, and nothing will nudge you afterwards.

The condition is the whole rule, so read it literally: **a turn in which you wrote
or edited no file inside a `.vetd/` directory ends with no `vetd_notes` call at
all.** Canvas notes only exist for designs on the canvas, so there is nothing to
check when the work was anywhere else — editing pages in the user's own repo,
answering a question, running tests, writing code that has no design attached.
Calling it there is not a harmless extra check: it fails, and it tells the user
you mistook their codebase for a design document. Same for every other `vetd_*`
tool — none of them apply to work outside a `.vetd`.

Not one of those three is checked for you at the end of the turn — if you
report back without them, whatever is broken simply stays broken. So before you
report, name the frames you touched and confirm each one cleared all three.

At that point stop and report — do not keep polishing. If something is still off
but you have already revised it twice, say what it is instead of attempting a
third pass.

## Exporting deliverables

When the user asks to export, download or hand off the design ("导出 PDF",
"export all frames as images"), call `vetd_export`. It writes into the current
working directory and each file holds the frame's FULL content, including what
scrolls below the frame's viewport:

- `format: "pdf"` — one `<design>-frames.pdf`, one page per frame.
- `format: "images"` — one PNG per frame in a `<design>-frames/` folder.
- Omit `frames` for every frame in canvas order; pass `frames: [...]` when the
  user named specific screens.

**One format per request — the one the user asked for.** "Export PDF" means
`pdf` only; "export images" means `images` only. Never export both: every
format re-renders every frame, so doing both doubles a slow export for a file
nobody asked for. If the user did not say which format, ask.

**Export is not verification.** For checking a design, call `vetd_screenshot`.
`vetd_export` runs no checks and is only for when the user asks for exported
files — do not screenshot or re-verify before exporting, and do not Read the
exported files afterwards. Just tell the user where they are.

## Do not rework

Rewriting your own output is the single largest waste in this workflow, and it
buys nothing: a revision made without looking at the render is a guess.

- **Never re-read a file you just wrote.** Its contents are already in context.
- **Fix with `edit`, never by rewriting the whole file with `write`.** Both the
  `issues` and the build errors you get back name a file and a line; go there.
  A full rewrite costs many times more and usually introduces a new defect.
- **Screenshot before revising.** Once a coherent frame batch is written, the
  next thing you do to it is one `vetd_screenshot` batch. Do not polish it first
  — you cannot see what is wrong yet. One evidence-based revision pass per
  capture; recheck only the frames you changed.
- **Do not restate the plan between steps.** A todo list is worth it only when
  the design has more screens than you can hold at once. Create it once and
  update it at phase boundaries; never spend one tool round marking every frame
  complete immediately after its write.

## References (read on demand)

Resolve against `$SKILL_DIR`. Do not read them all up front.

| File | Read it when |
| --- | --- |
| `references/interaction.md` | Wiring clicks, `_layout.tsx`, cross-screen flows |
| `references/quality.md` | Reviewing a screenshot; before declaring any frame done |
| `references/taste/directions.md` | Before the first frame of a new design with no pack or `DESIGN.md` |
| `references/taste/whitespace.md` | Weighing whether a line, divider or card wrapper earns its place; a capture looks boxed-in |
| `references/taste/landing-structures.md` | Product type is a landing page |
| `references/taste/app-density.md` | Product type is app screens or a dashboard |
| `references/taste/color.md` | Building or changing the palette, chart or status colors |
| `references/taste/typography.md` | Choosing faces, the type scale, CJK font stacks |
| `references/taste/copy.md` | Writing headlines, labels, empty and error states |
| `references/taste/anti-patterns.md` | A capture looks generic, or a tell from quality.md § 5 needs its fix |
