# Authoring vDesign documents A vDesign document is a tree of boxes whose geometry is **declarative**. A property holds either a number or an expression, and expressions are stored as written and resolved on read. `w=(100% - $gap * 2)` stays that formula in the file forever; change `$gap` and everything bound to it moves. That is the entire point of the format, and it is what you are being asked for. A document full of literal coordinates is a worse version of a Figma file. A document of expressions and tokens can be edited by changing one number. ## The output contract Emit **only DSL lines**. No prose before or after, no explanation, no JSON. A fenced block is tolerated but unnecessary. ## The grammar in one screen ```vd version 4 kind design title "Launch hero" $gap = 24 $accent = #5b5bd6 $paper = #ffffff $highlight = #ffd166 font display = Inter fallback "system-ui, sans-serif" frame page w=1280 h=720 background=paper rect hero.bg x=$gap y=24 w=(100% - $gap * 2) h=260 fill=accent radius=16 text hero.title x=(@hero.bg.left + 32) y=64 w=560 h=82 size=34 weight=700 fill=paper "An open substrate for the " "agent era" fill=highlight group cards rect card.1 x=$gap y=(@hero.bg.bottom + $gap) w=((100% - $gap * 4) / 3) h=200 ``` - **The first lines are the header.** `version 4` and `kind design`. Write both. `design` is the free-form canvas and is the only kind that exists today; a document that does not say which it is leaves every reader to guess from its contents. - **Then `title "…"`** — a short human name for what you are making, two to five words ("Coffee shop landing", not "Document"). It is what the tab shows, so write it before the first page: the name should appear while the design is still arriving. - **Two spaces of indent is one level.** Indentation is the only nesting. - A line is ` key=value key=value …`. - A **quoted line** under a `text` node is one run of its content. - A **boolean is a bare flag**: `italic`, never `italic=true`. - **There are no comments.** `#` already starts a colour. Anything trailing a value is an error rather than something to ignore, so do not annotate the document. ### The one rule worth memorising > A value is an **expression** if it starts with `$`, `@`, or `(`. Everything else is a > literal. So `x=$gap` and `x=@logo.right` need no parentheses, and everything else that is an expression does: `y=(prev.bottom + 12)`, `w=(50%)`, `y=(parent.h / 2)`. A quoted value is always a literal, which is how you write a string that starts with a sigil: `fill="$brand"`. ## Rules that decide whether the output is any good **1. Write relationships, not coordinates.** This is the difference between a vDesign document and a screenshot. Before writing a number, ask what it is derived from. ```text ✗ rect card.2 x=148 y=308 w=395 h=200 ✓ rect card.2 x=(@card.1.right + $gap) y=(prev.y) w=@card.1.w h=200 ``` **2. Put every repeated value in a token first.** A colour used twice, a spacing used three times, a type scale — all tokens. `$gap` used eleven times is the compression, and it is also the edit surface: "make it roomier" becomes one line. **3. Only reference ids you have already written.** References resolve against the document, and a consumer may be rendering your output as it streams. Write `logo` before the headline that sits beside it. **4. Give ids a role, not a number.** `hero.title`, `card.1`, `nav.logo` — an id should be guessable by someone who has not read the file. Never `node1`, `rect5`. **5. Text height is authored — estimate it.** Nothing measures glyphs for you. For a box of width `w` at font size `size`: ```text characters per line ≈ w / (size * 0.5) lines = ceil(length of the text / characters per line) h = ceil(lines * size * 1.2) ``` Round up. Too small is reported as a `text-overflow` problem; too large just leaves space. **6. `fill` takes a token name or a CSS colour, never an expression.** `fill=accent`, `fill=#5b5bd6`. Sizes and positions bind; colours do not. **7. A group has no geometry.** It holds children and nothing else — no `x`, `y`, `w`, `h`. Its children keep the coordinate space of whatever contains the group, and `parent`, `prev` and `next` see straight through it. **8. A page is a `frame`, and more than one page is more than one frame.** Start the document with a frame; you do not write a `canvas` line at all. Each frame is its own coordinate space, so `x=$gap` means `$gap` from _that page's_ left edge and `100%` means that page's width. `parent.cx` is that page's centre too, and `parent.left` is `0`, so the same line centres something on every page. Lay several out left to right with a gap between them — nothing stops one page being drawn over another, so do not overlap them. ```vd $gap = 24 frame page w=1280 h=720 background=paper rect hero.bg x=$gap y=$gap w=(100% - $gap * 2) h=320 fill=accent frame pricing x=1380 w=1280 h=720 background=paper text pricing.title x=$gap y=$gap w=600 h=48 size=34 weight=700 "Pricing" ``` **9. Centre a label by giving it the box it sits in, never an offset.** A button's label, a badge's number, a chip's text: same `x y w h` as the shape behind it, then `align=center valign=middle`. The renderer centres the line inside the box, so it stays centred at any size or font. A guessed `y=(@cta.top + 14)` is off by a few pixels and drifts the moment the button or the type changes. ```vd rect cta x=$gap y=400 w=180 h=48 fill=accent radius=10 text cta.label x=@cta.x y=@cta.y w=@cta.w h=@cta.h align=center valign=middle fill=paper "Get started" ``` ## Editing an existing document When given a document and asked to change it, reply with **statements**, not a new document. Five verbs, plus `title` to rename the document: ```vd-patch title "Pricing page" set $gap = 40 set hero.title size=($display * 1.2) weight=800 set hero.sub content="A shorter subhead." unset hero.sub opacity insert rect card.4 after card.3 x=(@card.3.right + $gap) y=(prev.y) w=@card.1.w h=200 insert frame pricing x=(@page.right + 100) w=@page.w h=@page.h background=paper move logo before hero.bg delete hero.tagline ``` An `insert` with no placement lands on the canvas, which in a multi-page document is _beside_ the pages rather than on one. Say `after ` or `in ` whenever the new node belongs to a page. Copy is changed with `set … content=`, or with an indented body when the marks matter. Never `delete` and `insert` a text node to reword it — it loses its place and every `@` reference to it. Prefer changing a **token** over changing the nodes that use it — that is usually what the request actually means. "More breathing room" is `set $gap = 40`, not eleven `set` statements. See `reference/edits.md` for placement, and for what happens when you delete something another node references. Two flags come from the person more often than from you: `hidden` (not drawn, export included) and `locked` (their pointer passes over it). Keep them as you find them. Hide with `set card.2 hidden` only when asked to hide — when asked to remove something, `delete` it, because a hidden node is still in the file for the next reader to wade through. `unset card.2 hidden` shows it again. A top-level frame is a slide when the document is played. For a backup or reference slide that should stay in the deck but out of the talk, write `skip` on its frame (`set appendix skip`) — don't hide it, which takes it off the canvas and out of every export too, and don't delete it. ## Motion Transitions between slides, parts of a slide appearing in order, and buttons that go to another slide are a `motion` section after the last page. Only Play reads it: the canvas and every export show each page as written, so write the page as it looks when finished and let motion say how the presenter gets there. ```vd motion intro enter=fade intro.title enter=rise intro.chart enter=fade step intro.cta enter=pop go=#pricing ``` Add it when the person is making something to present or click through, and keep it light. Never copy a page to animate between the copies. A web page taller than a screen gets `motion scroll`, so Play scrolls it instead of shrinking it to fit. `reference/motion.md` has the rest. ## Icons Never write an icon's path from memory: it is slow, and it comes out roughly right at best. Icons come from the Lucide set, about 1,900 line icons, by name. - **Over MCP**, find one with `vd_search_icon` (`"settings"`, `"notification"`) and place it with `vd_add_icon`, which writes the `vector` for you — the icon's path, Lucide's stroke, round caps. Its `x`, `y` and `size` take expressions, so place an icon by relationship like anything else: `x="(@cta.right - 32)"`. - **In the app**, find one with `search_icons` and get its line with `use_icon`, before you write any DSL; copy that `vector` line into your reply as it comes back. - **Where no icon tool is offered**, draw it as a `vector` (`reference/nodes.md`). - **Lucide has no brand logos** — GitHub, X, YouTube. A logo is an image the person supplies: leave it as a slot (see Images). A placed icon is an ordinary vector named `lucide:`. Recolour it with `set stroke=…`; resize it with `w` and `h`, and scale `strokeWidth` with them — 2 at 24. ## Images A picture is an `image` node that names an asset: `image hero.logo x=40 y=32 w=160 h=48 src=logo fit=contain`. The asset's registry line (`asset logo = image/png hash=…`) and its bytes are **never yours to write** — hashes come from the app. You only ever write `src=`. - **In the app**, images the person attached are offered by name at the top of the request ("Images you can put in the design"). Place one with `src=`; an edit uses `insert image in …`. Size it to the image's aspect ratio, and prefer `fit=contain` for a logo. - **Over MCP**, a picture at a public URL goes in with `vd_add_asset` first, then you place it. `vd_assets` lists what a document already has. - **A picture you cannot send** — one the person gave you in chat, or has on their machine — becomes a **slot**. You cannot pass on an image you can see, and no tool reads file paths, so do not try: write the image anyway, with a `src` nothing supplies yet and a `name` saying what goes there: ```vd image nav.logo x=40 y=24 w=120 h=40 src=logo fit=contain name="Your logo" ``` The canvas shows it as a labelled box the person drops the file into, whenever they like. Size and place it as if the picture were there, carry on with the design, and tell them where it is ("drop your logo onto the box in the top-left"). Every image with the same `src` fills together. `vd_assets` lists the slots still waiting. A `src` naming nothing reports `missing-asset`. That is what a slot is, so do not "fix" it by deleting the image; do fix a `src` you simply misspelled. ## Looking at your work, and exporting **Over MCP (desktop and CLI), `vd_export` is how you see what you made.** `vd_read` is text, and a layout that reads fine can still look wrong — text overflowing, two cards overlapping. After a build or a large edit, export the frame as `png` with `save: false` and look at the picture that comes back; fix what you see, then look again. When the person asks for a file — "export it as a PDF", "give me a PNG of the cover" — call `vd_export` with `save: true`. Formats: `png`, `jpg`, `webp`, `svg`, `pdf`, `ai` (Illustrator: one artboard of the whole canvas, or `artboards: "frames"` for a file per frame) and `html`. In the app the person chooses where it goes, and may cancel; if they do, do not try again unasked. Only save when asked — a look is `save: false`. ## Reference Each file is named by its path in this skill. Over MCP, `vd_skill` returns any of them by that path; in the flattened system prompt they follow below. - `reference/grammar.md` — the expression language in full: edges, percentages, relative references, precedence; plus tokens, paint (colours and gradients), fonts and the asset registry. - `reference/nodes.md` — every node type and every property it accepts: `frame`, `canvas`, `rect`, `ellipse`, `vector`, `image`, `text`, `group`. - `reference/edits.md` — the mutation dialect in full. - `reference/motion.md` — motion for Play: page transitions, reveals and buttons. - `examples/` — complete documents: `hero.vd`, `cards.vd`, `slide.vd`, and `deck.vd`, three pages with motion. --- ## The expression language Expressions are stored as written and resolved when the document is read. They form a one-way graph: a value may read other values, and nothing may read back. A cycle is reported as an error rather than solved. ### What an expression can contain | | | | ------------------- | -------------------------------------------------------------- | | `12`, `-4`, `1.5` | a number | | `50%` | a percentage of the containing box — see below | | `$gap` | a design token | | `@hero.title.right` | another node's edge, by id | | `prev.bottom` | a relative reference: `parent`, `prev`, `next`, `self`, `root` | | `+ - * /` | arithmetic, with the usual precedence | | `( )` | grouping | | unary `-` | `(-$gap)` | There are **no functions** — no `min`, `max`, `clamp`, `abs`. If you need one, the value is a literal or the layout needs rethinking. There are **no units** either: a value is a number, a percentage, or an expression — `h=480`, not `h=480px`; `w=(100%)`, not `w=100vw`. The one place a unit appears is inside a quoted gradient, which is CSS spelling: `"linear-gradient(135deg, …)"`. ### Edges Any reference names an edge of the box it points at: | edge | is | | ----------- | ----------------------- | | `x`, `left` | the left edge | | `y`, `top` | the top edge | | `w`, `h` | width, height | | `right` | `x + w` | | `bottom` | `y + h` | | `cx`, `cy` | the centre on each axis | ```text x=(@logo.right + $gap) beside the logo y=(@hero.title.bottom + 12) under the headline x=(parent.cx - self.w / 2) centred horizontally ``` ### Percentages A bare percentage measures against the **containing box** — the nearest enclosing frame, or the canvas when it has a size — on the axis the property lives on: - `x` and `w` measure against the parent's width - `y` and `h` measure against the parent's height - anywhere else — `opacity=(50%)` — it is simply `0.5` ```text w=(100% - $gap * 2) full width inside a gutter w=((100% - $gap * 4) / 3) one of three columns with gaps between ``` A group is transparent here: a percentage inside a group measures the frame around the group, not the group. A percentage on a frame itself, or on anything sitting directly on an unsized canvas, has no box to measure against and is reported as `percent-without-parent`. ### Relative references `parent`, `prev`, `next`, `self` and `root` need no `@` and take an edge the same way. ```text y=(prev.y) line up with the previous sibling y=(prev.bottom + $gap) stack under it w=(parent.w / 2) half the container ``` `prev` and `next` are siblings **in layout order**, which sees through groups: a group carries no geometry, so its children behave as though the group were not there. A frame is the opposite — it does carry geometry, so `parent` inside one is the frame. Every reference is read in the coordinates of the node that wrote it. Inside a frame, `parent.left` and `parent.top` are `0`, `parent.cx` is half the frame's width, and `parent.right` is its width — wherever the frame sits on the canvas. A reference to a node in another frame means the same point on the page, carried into this frame's coordinates. ### Which properties accept an expression Positions, sizes and numeric style properties: - `frame` — `x`, `y`, `w`, `h` - `canvas` — `w`, `h` - `rect` — `x`, `y`, `w`, `h`, `strokeWidth`, `radius`, `opacity` - `text` — `x`, `y`, `w`, `h`, `size`, `weight`, `lineHeight`, `letterSpacing`, `opacity` `fill`, `stroke`, `background` and `font` are **names, not numbers**. They take a token name or a literal value; an expression there is reported as `expression-not-supported`. Marks inside a text run — `weight=700` on one span — are always literals. There is no address for "run 2's size", so there is nothing an expression there could resolve against. ### Writing values | you mean | you write | | --------------------------------- | --------------------- | | the number 24 | `x=24` | | the token `gap` | `x=$gap` | | an expression | `x=(@a.right + $gap)` | | the string `accent` | `fill=accent` | | a string with a space | `name="Hero heading"` | | a string starting with `$` or `(` | `fill="$literal"` | | true | `italic` | | false | omit it | Quoting uses JSON's rules, so `\"` and `\n` mean what they do in JSON. ### Tokens ```vd $gap = 24 $accent = #5b5bd6 $display = 34 $label = "Section heading" ``` A bare numeric token is a number; quote it to keep it a string. A token may not hold an expression — it is a value, not a formula. ### Paint: colours and gradients `fill`, `stroke`, a frame's `background` and a run's `fill` take a **paint**: a colour, a token naming one, or a gradient in CSS spelling. Quote a gradient — it has spaces. ```vd $accent = #2563EB $glow = "linear-gradient(135deg, accent, #7C3AED 60%, transparent)" rect hero.bg x=0 y=0 w=(100%) h=480 fill=glow ellipse hero.orb x=600 y=40 w=240 h=240 fill="radial-gradient(circle at 30% 30%, #fff, accent)" text hero.title x=64 y=120 w=560 h=64 size=48 weight=700 fill="linear-gradient(90deg, #F43F5E, #F59E0B)" "Ship faster" ``` - `linear-gradient(, )` — the angle in `deg` (0 points up, 90 right) or `to right`, `to bottom left`…; omitted, it runs top to bottom. - `radial-gradient([circle|ellipse] [at X% Y%], )` — centred where asked, reaching the farthest corner. - A stop is a colour or a token name, with an optional `%`. `transparent` fades the neighbouring colour out, as in CSS. - A gradient spans the node's box; on text it sweeps across the whole box, not each run. - Not supported: `conic-gradient`, `repeating-*`, sizes such as `closest-side`, and a token holding a gradient used as a stop of another gradient. ### Fonts ```vd font display = Inter fallback "system-ui, -apple-system, sans-serif" face fonts/Inter-Regular.woff2 weight=400 face fonts/Inter-Bold.woff2 weight=700 style=italic ``` `font=display` on a node or a run names the entry. A name with no entry passes straight through as a CSS font stack, so `font="Georgia, serif"` works with no registry at all. `fallback` is what draws while a face loads, or if it never does. Each `face` is one font file, with the `weight` (a number) and `style` (`normal` or `italic`) it draws. ### Assets ```vd asset logo = image/png hash=sha256-9f2c4e8a… bytes=24567 asset hero = image/jpeg hash=sha256-1a77b0c3… bytes=412330 credit="Jane Doe / Unsplash" source="https://unsplash.com/photos/abc" license=Unsplash ``` One asset, one line, including its provenance. `hash` and `bytes` are required: they identify the block of bytes further down the file that carries the picture. `credit`, `source` and `license` are optional in the grammar but **not optional for an image you found** — the bytes end up inside a file someone re-shares. A `.vd` file is a **text head and a payload tail**, separated by a line reading `--- payload base64 ---`. Everything above it is the design, and is all you ever read or write. Everything below it is one line per asset: its hash, a space, and its bytes as base64. You never write that half by hand, and a tool reading a document for you will not show it to you. This is why the registry declares `bytes` and a media type up front: a reader knows what a document depends on, and how large each of those things is, before any of the bytes arrive. --- ## Node types Eight. Every line is ` key=value …`, and children are indented two spaces under their container. Every node also accepts `name="…"`, a human label with no effect on layout. Every node but the canvas also accepts two bare flags, written last on the line: - `hidden` — not drawn anywhere, export included. It still resolves, so `@badge.right` keeps working while `badge` is hidden. Hiding a container hides what is in it. - `locked` — the person's pointer passes over it on the canvas. It changes nothing for you: you can still edit it, and you may add or remove the flag when asked. ### `frame` — a page The node you almost always start with. A frame is a box with **its own coordinate space**: everything inside it is positioned from its top-left corner, and `%` inside it measures the frame rather than the document. A document may hold as many as you like, side by side — that is how it holds several pages, slides or artboards. Frames also nest, which is how you get a card with a boundary of its own. | property | type | | | ------------ | -------------------- | ------------------------------------ | | `x`, `y` | number or expression | where the page sits; both default 0 | | `w`, `h` | number or expression | the page size | | `background` | token or colour | drawn behind everything on this page | | `clip` | flag | hide what overflows; off by default | | `skip` | flag | left out of Play; top-level only | ```vd $gap = 24 frame page w=1280 h=720 background=paper rect hero.bg x=$gap y=$gap w=(100% - $gap * 2) h=320 fill=accent frame notes x=1380 w=1280 h=720 background=paper text agenda x=$gap y=$gap w=600 h=48 "What we agreed" ``` Lay several pages out **left to right with a gap between them**, as above: they must not overlap, because nothing stops one page being drawn on top of another. ### `canvas` — the surface The surface the frames sit on, and the outermost node. **You almost never write it.** A document that begins with frames gets one automatically, which is why the example above has no `canvas` line and no extra indent level. Write it only for a single-page document with shapes directly on it, where it is the page: ```vd $gap = 24 canvas page w=1280 h=720 background=paper rect hero.bg x=$gap y=24 w=(100% - $gap * 2) h=260 fill=accent ``` | property | type | | | ------------ | -------------------- | ---------------------------------- | | `w`, `h` | number or expression | optional; absent means unbounded | | `background` | token or colour | only meaningful when it has a size | A canvas must be the **only** top-level node. Writing one and then a frame beside it is an error; put the frames inside it, or leave it out entirely. ### `rect` — a box | property | type | default | | ------------- | -------------------- | -------------- | | `x`, `y` | number or expression | required | | `w`, `h` | number or expression | required | | `fill` | token or colour | `#CBD5E1` | | `stroke` | token or colour | none | | `strokeWidth` | number or expression | 1 when stroked | | `radius` | number or expression | 0 | | `opacity` | number or expression | 1 | ```vd rect hero.bg x=$gap y=24 w=(100% - $gap * 2) h=260 fill=accent radius=16 ``` ### `ellipse` — an oval, or a circle The ellipse inscribed in its box. A circle is `w` equal to `h`. | property | type | default | | ------------- | -------------------- | --------- | | `x`, `y` | number or expression | required | | `w`, `h` | number or expression | required | | `fill` | token or colour | `#CBD5E1` | | `stroke` | token or colour | none | | `strokeWidth` | number or expression | 0 | | `opacity` | number or expression | 1 | ```vd ellipse avatar.ring x=24 y=24 w=48 h=48 fill=none stroke=accent strokeWidth=2 ``` ### `vector` — any shape: icons, arrows, dividers, logos An SVG path, drawn into a box. | property | type | default | | ------------- | ---------------------------------- | --------- | | `x`, `y` | number or expression | required | | `w`, `h` | number or expression | required | | `d` | SVG path data, quoted | required | | `view` | `"minX minY width height"`, quoted | `0 0 w h` | | `fill` | token, colour or `none` | `#CBD5E1` | | `stroke` | token or colour | none | | `strokeWidth` | number or expression | 0 | | `opacity` | number or expression | 1 | | `rule` | `nonzero`, `evenodd` | `nonzero` | | `cap` | `butt`, `round`, `square` | `butt` | | `join` | `miter`, `round`, `bevel` | `miter` | ```vd vector nav.arrow x=320 y=20 w=24 h=24 view="0 0 24 24" d="M5 12h14M12 5l7 7-7 7" fill=none stroke=ink strokeWidth=2 cap=round join=round ``` `d` is exactly SVG's path syntax — `M L H V C S Q T A Z`, absolute or relative. `view` is the path's own coordinate space, spelled the way SVG spells a `viewBox`, and it is **stretched onto `x y w h`**: an icon drawn on a 24-unit grid keeps its numbers and is sized by `w` and `h` alone. - **Over MCP, place an icon with `vd_add_icon`** rather than writing one: it knows the Lucide set and writes this node for you. Otherwise, **copy the icon's `d` and its `viewBox` verbatim** into `d` and `view`. - The stroke is in page units and does not grow with `view`: an icon drawn with `strokeWidth=2` at 24 wants 4 at 48. A dot drawn as a tiny circle needs a stroke at least its diameter to read as solid. Do not rescale the numbers; change `w` and `h`. - A line icon wants `fill=none` and a `stroke`. Without `fill=none` the shape is filled grey by default. - Without `view`, the path draws at 1:1 inside its box: `vector rule x=0 y=100 w=400 h=0 d="M0 0 H400" stroke=ink strokeWidth=1` is a divider. - A path that cannot be read draws up to its first error and reports `bad-path`. To turn a `rect` or `ellipse` into a free-form shape, replace its line with a `vector` of the same id — same `x y w h`, `view="0 0 "`, and a `d` that traces it. The person can do the same with **Convert to vector** (⇧⌘O) and then drag its points. ### `image` — a picture | property | type | default | | --------- | -------------------------- | -------- | | `x`, `y` | number or expression | required | | `w`, `h` | number or expression | required | | `src` | a name from `asset` | required | | `fit` | `cover`, `contain`, `fill` | `fill` | | `radius` | number or expression | 0 | | `opacity` | number or expression | 1 | ```vd asset hero = image/jpeg hash=sha256-1a77b0c3… bytes=412330 image cover.photo x=0 y=0 w=(100%) h=480 src=hero fit=cover ``` `src` names an entry in the document's asset registry — **never a path or a URL**. `cover` fills the box and crops; `contain` fits the whole picture inside it; `fill` stretches. **You cannot invent an asset.** A registry line needs the content hash and byte count of bytes that actually exist in the file, so `asset` lines are written by whatever put the bytes there — a paste, an import, or an `add-image` tool. What you can do is refer to one that is already declared, move it, resize it, or change its `fit`. Referring to a name with no `asset` line reports `missing-asset` and, in the editor, draws a **slot** — a box labelled with the node's `name` that the person drops a picture into. That is how you leave room for a picture you cannot send; see "Images" in the overview. ### `text` — a box of text | property | type | default | | ------------------ | --------------------------------- | ------------------------------------------ | | `x`, `y`, `w`, `h` | number or expression | required | | `font` | registry name or CSS stack | `system-ui, …` | | `size` | number or expression | 16 | | `weight` | number or expression | 400 | | `lineHeight` | number or expression | 1.2 — a **multiple of size**, not a length | | `letterSpacing` | number or expression | 0 | | `align` | `left` `center` `right` `justify` | `left` | | `valign` | `top` `middle` `bottom` | `top` | | `italic` | bare flag | off | | `fill` | token or colour | `#0F172A` | | `opacity` | number or expression | 1 | **`h` is authored.** Nothing measures the glyphs for you, and text that does not fit is drawn overflowing and reported as `text-overflow`. Estimate: ```text characters per line ≈ w / (size * 0.5) lines = ceil(length / characters per line) h = ceil(lines * size * lineHeight) ``` #### Content One quoted line per run, indented under the node: ```vd text hero.title x=64 y=64 w=560 h=44 size=34 weight=700 "An open substrate for the " "agent era" fill=highlight ``` A single unformatted run is just one quoted line. Split into more runs only where the formatting actually changes — and mind the spaces, which belong to whichever run they follow. A run may carry `font`, `size`, `weight`, `italic`, `underline`, `strike`, `fill`, `letterSpacing`, `link`. All literals: a run is not addressable, so nothing there can be an expression. `link` makes the run a link: `link="https://…"`, `link="mailto:…"`, or `link="#pricing"` to jump to the frame `pricing` in the same document. Nothing else is followed — `http:`, `javascript:` and file paths are refused and drawn as plain text. Only link to a URL the person gave you; never invent one. A link is not underlined for you, so add `underline` when it should look like one: ```vd text cta.body x=64 y=480 w=400 h=24 "Read the " "docs" link="https://example.com/docs" underline " or see " "pricing" link="#pricing" underline ``` ### `group` — structure, and nothing else ```vd group cards rect card.1 x=$gap y=0 w=200 h=120 rect card.2 x=(@card.1.right + $gap) y=(prev.y) w=@card.1.w h=120 ``` A group has **no `x`, `y`, `w` or `h`**, and it is not a mistake that it does not. A frame derived from its children would close a loop — a child's `w=(100%)` would read the group's width, which would read the child's — so a group is transparent to layout. Its children keep the coordinate space of whatever contains the group, and `parent`, `prev`, `next` and `%` see straight through it. Group things because they belong together, and because grouping breaks no expression. ### Rotation `rect`, `ellipse`, `vector`, `image` and `text` take `rotation=` — degrees, clockwise, about the centre of the box. `x y w h` stay the **unrotated** box, so `@card.right` still means the right edge of the box as written; a rotated node is placed by its box and then turned. Frames and groups do not rotate. `rotation` is a number, so `rotation=$tilt` works. ```vd rect sticker x=520 y=60 w=160 h=48 radius=24 fill=accent rotation=-8 text sticker.label x=520 y=72 w=160 h=24 align=center fill=#fff rotation=-8 "New" ``` Give things that should turn together the same box centre, or the same rotation and matching boxes, as above. ### Effects: blend and shadow `frame`, `rect`, `ellipse`, `vector`, `image` and `text` also take: - `blend=` — how the node mixes with what is under it: `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`. Omit it for normal. A node blends only with the document beneath it. Groups take no blend. - `shadow=` — drop shadows in CSS `box-shadow` spelling, quoted: `x y blur spread colour`, several separated by commas, the first on top. The colour may be a token. `inset` is not supported. A shadow follows the shape — round for an ellipse, glyph-shaped for text — and never changes the box, so layout and `@` references ignore it. Put a shadow you reuse in a token. ```vd $lift = "0 1 2 0 #0F172A14, 0 8 24 -4 #0F172A29" image hero.photo x=0 y=0 w=(100%) h=480 src=city fit=cover rect hero.tint x=0 y=0 w=(100%) h=480 fill=accent blend=multiply opacity=0.8 rect card x=64 y=520 w=320 h=200 radius=16 fill=#FFFFFF shadow=lift ``` ### Ids An id is how everything else refers to a node, so make it guessable: `hero.title`, `card.1`, `nav.logo`, `footer.legal`. Dots are ordinary characters and read as structure. Ids must be unique across the document. --- ## Editing a document Given a document and a request to change it, reply with **statements**, not a new document. An edit is usually a handful of lines against hundreds, and each statement is applied as an operation — so it is undoable as one step and nothing else in the document is disturbed. Statements are never indented, except for the body of an `insert`. ### `set` ```vd-patch set $gap = 40 a token set hero.title size=34 weight=700 one or more properties set card.2 w=(@card.1.w * 2) an expression, same rules as anywhere ``` **Reach for the token first.** If four cards are laid out with `$gap`, "more breathing room" is `set $gap = 40` — one line, and the relationships survive. Writing four `set` statements with new coordinates destroys exactly what the document was for. #### Text `content` is the one thing a node line does not carry, so `set` reaches it two ways: ```vd-patch set hero.title content="A new headline" ``` ```vd-patch set hero.title "Open tooling for the " "agent era" fill=highlight ``` The first is for plain copy and is what you want almost always. The indented body is for when the runs and their marks matter — the same shape a `text` node declares in a document. Use one or the other, never both in a statement. Rewording is always `set`. Never `delete` and `insert` to change copy: the node loses its place in the document and every `@` reference to it breaks. ### `unset` Removes a property so its default comes back. ```vd-patch unset hero.sub opacity unset card.1 stroke strokeWidth ``` For a token, use `delete $name` — a token is a thing, not a property. ### `insert` ```vd-patch insert rect card.4 after card.3 x=(@card.3.right + $gap) y=(prev.y) w=@card.1.w h=200 insert text caption before footer x=$gap y=0 w=400 h=20 insert group cards in page rect inner x=0 y=0 w=10 h=10 text label x=0 y=12 w=40 h=10 "A caption" ``` Placement comes before the properties: | | | | -------------- | --------------------------------------------------------------------------------------------------------------------------- | | `after ` | next to that node, following it | | `before ` | next to that node, ahead of it | | `in ` | appended inside a frame, canvas or group | | `in at 0` | at a specific position inside it | | _omitted_ | appended to the canvas — **name the frame** in a multi-page document, or the node lands beside the pages rather than on one | Children and text runs go indented underneath, exactly as in a document. ### `move` Takes the same placements. ```vd-patch move logo before hero.bg move card.3 in cards at 0 ``` ### `delete` ```vd-patch delete hero.tagline delete $unused ``` ### `title` Renames the document. It is the same line a whole document opens with. ```vd-patch title "Pricing page" ``` ### `motion` Replaces one page's whole motion block with the lines under it; a bare `motion ` clears it. The format is in the motion reference. ```vd-patch motion intro enter=fade intro.title enter=rise intro.chart enter=fade step ``` ### Things worth knowing **Statements apply in order, against a document that grows as they do.** So this works: ```vd-patch insert rect a after card.2 x=0 y=0 w=10 h=10 insert rect b after a x=0 y=0 w=10 h=10 ``` **A bad statement fails alone.** The rest of the patch still lands, and the failure comes back naming the line. Fix that line; do not resend the whole patch. **Deleting leaves dangling references.** Nothing rewrites expressions for you, so removing `logo` while `hero.title` reads `x=(@logo.right + $gap)` produces an `unknown-reference` error on the next read. Before deleting, check what points at it, and either repoint those first or delete them too: ```vd-patch set hero.title x=$gap delete logo ``` **Moving a node changes what `prev` and `next` mean** for its neighbours. If siblings are chained with `y=(prev.y)`, reordering them is fine — that is the chain doing its job — but a `prev` reference on the node that used to be first now points somewhere new. **You cannot delete or move the canvas**, and you cannot give a group geometry. A frame is an ordinary node: insert, move and delete it like any other, and everything on it travels with it. --- ## Motion Motion is how Play gets to a page, never what the page is. Write every page as it looks when finished; motion says how a presenter arrives there. Only Play reads it: the canvas always shows the finished page, and no export carries motion — a PDF prints each page as drawn. Add motion when the person is making something to present or to click through, and keep it light. A transition per page and a reveal or two is a talk; everything flying in is noise. ### Where it goes After the last page, one block per page that has any: ```vd motion intro enter=fade intro.title enter=rise intro.chart enter=fade step intro.note enter=fade together intro.cta enter=pop go=#pricing motion pricing enter=push duration=600 pricing.back go=back ``` A header names a page — a top-level frame — and says how Play arrives on it. Each line under it names a node on that page and says what the node does. Never copy a page to animate between the copies: each page is written once. ### The header `motion [enter=…] [duration=…] [scroll]` | `enter` | Play arrives on the page by… | | ------- | ------------------------------------------------ | | `cut` | nothing at all — the default | | `fade` | fading in over the page before | | `push` | pushing the page before out, sideways | | `slide` | sliding in over the page before, which stays put | You never write a direction. Going forward runs the new page's `enter`; going back runs the leaving page's `enter` in reverse. #### `scroll`: a page taller than a screen Play fits each page whole into the window, which shrinks a long web page to a thin strip. `scroll` fits the page to the window's width instead, and the presenter scrolls it — wheel, trackpad, Space, Page Down, ↓ — the way a browser shows a page. At the bottom the same key goes on to the next page; → still goes there at once. A header with only `scroll` is a whole block: ```vd motion home scroll ``` Give it to a web page, a landing page or a long article taller than one screen. Leave it off slides, posters and phone screens, which are meant to be seen whole. A `go=#pricing` to a frame inside a page that scrolls lands with that frame at the top of the window. The canvas and every export are unchanged: they show the whole page. ### A line ` [enter=…|exit=…] [duration=…] [delay=…] [step|together] [go=…]` Effects, for `enter` and for `exit`: | effect | entering | leaving | | ------ | ------------------------------ | ---------------- | | `cut` | appears at once | vanishes at once | | `fade` | fades in | fades out | | `rise` | rises from a little below | rises away | | `drop` | drops from a little above | drops away | | `pop` | grows in from slightly smaller | shrinks away | A node that `enter`s is hidden when Play arrives on its page, until its turn; one that `exit`s is gone after its turn. A node may have one `enter` line and one `exit` line — two lines, never both on one. **Order.** Lines build in the order written, each starting when the one above finishes, unless it says: - `together` — start with the line above; - `step` — wait for the presenter to press → or click. What follows runs by itself up to the next `step`. `delay=300` waits that long after the line would otherwise start. **Time is milliseconds, as a plain number**: `duration=600`, never `600ms` or `0.6s`. The default is 400 and the most is 10000. ### Buttons: `go=` Any line can say where a click on its node goes in Play. A click anywhere inside the node counts, so put `go=` on the group or frame that _is_ the button, not on its label. - `go=#pricing` — that page. A frame inside a page goes to its page. - `go=back` — wherever the presenter came from. - `go=next` — the next page, whatever is left to reveal on this one. - `go=https://…` or `go=mailto:…` — only a link the person gave you. A button can animate as well: `intro.cta enter=pop go=#pricing`. A `go=` works only in Play. A link that should also work in an exported PDF is a text run's `link=` (see nodes). ### Editing motion A patch replaces a page's whole block: write `motion `, its header (`scroll` included), and every line as it should end up. A bare `motion ` clears it. ```vd-patch motion intro enter=fade intro.title enter=rise intro.chart enter=fade step ``` Deleting a node removes its lines, and moving it to another page takes them along. The `vd_read` outline ends with the pages that have motion; `mode: "full"` shows the lines. ### What is checked An **error** refuses the write: an effect that does not exist, a unit on a time, `enter` and `exit` on one line, `step` with `together`, a line that says nothing, a second `enter`, `exit` or `go` for one node, or a line naming the page itself. In a patch, naming a node that is not on the page is an error too. A **warning** keeps the line, and Play skips it: in a whole document, a node that is not there, a node on another page, a header that is not a page, or a `go=#` to no frame. --- # Examples ## cards.vd ```vd version 4 kind design title "Three feature cards" $gap = 24 $paper = #ffffff $muted = #eef2f7 $ink = #0f172a $heading = 20 $body = 14 frame page w=1040 h=520 background=paper text section.title x=$gap y=$gap w=400 h=24 size=$heading weight=700 fill=ink "Three ways in" group cards rect card.1 x=$gap y=(@section.title.bottom + $gap) w=((100% - $gap * 4) / 3) h=220 fill=muted radius=12 rect card.2 x=(@card.1.right + $gap) y=(prev.y) w=@card.1.w h=@card.1.h fill=muted radius=12 rect card.3 x=(@card.2.right + $gap) y=(prev.y) w=@card.1.w h=@card.1.h fill=muted radius=12 text card.1.label x=(@card.1.left + 16) y=(@card.1.top + 16) w=(@card.1.w - 32) h=17 size=$body weight=600 fill=ink "As a format" text card.2.label x=(@card.2.left + 16) y=(@card.1.label.top) w=(@card.1.label.w) h=17 size=$body weight=600 fill=ink "As an editor" text card.3.label x=(@card.3.left + 16) y=(@card.1.label.top) w=(@card.1.label.w) h=17 size=$body weight=600 fill=ink "As a protocol" ``` ## deck.vd ```vd version 4 kind design title "Launch plan" $ink = #101014 $paper = #fafafa $muted = #ececf1 $accent = #d64545 $gap = 64 frame intro x=0 y=0 w=1600 h=900 background=paper text intro.title x=$gap y=240 w=1100 h=116 size=96 weight=800 fill=ink "Launch plan" text intro.sub x=$gap y=(@intro.title.bottom + 24) w=900 h=39 size=32 fill=ink opacity=0.6 "What ships, when, and what it costs" group intro.cta rect intro.cta.bg x=$gap y=620 w=280 h=72 fill=accent radius=36 text intro.cta.label x=@intro.cta.bg.x y=@intro.cta.bg.y w=@intro.cta.bg.w h=@intro.cta.bg.h size=24 weight=700 align=center valign=middle fill=paper "See pricing" frame plan x=1700 y=0 w=1600 h=900 background=paper text plan.title x=$gap y=$gap w=1000 h=58 size=48 weight=800 fill=ink "Three steps" rect plan.one x=$gap y=240 w=((100% - $gap * 4) / 3) h=480 fill=muted radius=24 rect plan.two x=(@plan.one.right + $gap) y=(prev.y) w=@plan.one.w h=@plan.one.h fill=muted radius=24 rect plan.three x=(@plan.two.right + $gap) y=(prev.y) w=@plan.one.w h=@plan.one.h fill=muted radius=24 frame pricing x=3400 y=0 w=1600 h=900 background=ink text pricing.title x=$gap y=$gap w=1000 h=58 size=48 weight=800 fill=paper "Pricing" text pricing.price x=$gap y=300 w=800 h=154 size=128 weight=800 fill=accent "$49 a month" text pricing.back x=$gap y=780 w=200 h=34 size=28 fill=paper "← Back" motion intro enter=fade intro.title enter=rise intro.sub enter=fade delay=150 together intro.cta enter=pop go=#pricing motion plan enter=push plan.title enter=fade plan.one enter=rise step plan.two enter=rise step plan.three enter=rise step motion pricing enter=slide pricing.price enter=pop pricing.back go=back ``` ## hero.vd ```vd version 4 kind design title "Launch hero" $gap = 24 $paper = #ffffff $accent = #5b5bd6 $highlight = #ffd166 $display = 40 $body = 16 font display = Inter fallback "system-ui, -apple-system, Segoe UI, sans-serif" frame page w=1280 h=720 background=paper rect hero.bg x=$gap y=$gap w=(100% - $gap * 2) h=320 fill=accent radius=20 rect logo x=(@hero.bg.left + 40) y=(@hero.bg.top + 40) w=72 h=72 fill=paper radius=16 text hero.title x=(@logo.right + $gap) y=@logo.top w=680 h=96 font=display size=$display weight=700 fill=paper "An open document substrate for the " "agent era" fill=highlight text hero.sub x=@hero.title.left y=(@hero.title.bottom + 12) w=560 h=20 font=display size=$body fill=paper opacity=0.8 "The document is the artifact, not the code that made it." rect cta x=@hero.title.left y=(@hero.sub.bottom + $gap) w=180 h=48 fill=paper radius=10 text cta.label x=@cta.x y=@cta.y w=@cta.w h=@cta.h font=display size=$body weight=600 align=center valign=middle fill=accent "Read the spec" ``` ## slide.vd ```vd version 4 kind design title "Title slide" $ink = #101014 $paper = #fafafa $accent = #d64545 $title = 56 $sub = 22 frame slide w=1600 h=900 background=ink text slide.title x=(parent.cx - self.w / 2) y=300 w=1100 h=68 size=$title weight=700 align=center fill=paper "The document is the artifact" rect rule x=(parent.cx - self.w / 2) y=(@slide.title.bottom + 40) w=120 h=4 fill=accent radius=2 text slide.sub x=(parent.cx - self.w / 2) y=(@rule.bottom + 40) w=900 h=27 size=$sub align=center fill=paper opacity=0.7 "vDesign — an open substrate for the agent era" ``` ## Tool reference Every tool a vDesign MCP server can offer, read from the servers themselves. Which ones a server lists depends on where it runs: **desktop** is local MCP in the desktop app, **remote** is `https://mcp.getv.design/mcp`, and **CLI** is the `vdesign-mcp` command for developers. A tool a server does not offer is absent from its list, not present and failing. Picture search is switched off in the desktop app and on remote MCP for now, whatever keys are stored. A picture still reaches a document by URL, through `vd_add_asset`. | Tool | What it does | Offered | | ------------------------------------- | ------------------------------- | ----------------------------------------- | | [`vd_add_asset`](#vd_add_asset) | Add an image to a document | always | | [`vd_add_icon`](#vd_add_icon) | Place icons in a document | always | | [`vd_add_image`](#vd_add_image) | Add a found image to a document | CLI only, when an image key is configured | | [`vd_assets`](#vd_assets) | List a document’s images | always | | [`vd_create`](#vd_create) | Create a document | always | | [`vd_export`](#vd_export) | Export a document | desktop and CLI only | | [`vd_list`](#vd_list) | List documents | always | | [`vd_open`](#vd_open) | Open a document in vDesign | remote only | | [`vd_patch`](#vd_patch) | Edit a document | always | | [`vd_problems`](#vd_problems) | Check a document | always | | [`vd_read`](#vd_read) | Read a document | always | | [`vd_remove_asset`](#vd_remove_asset) | Remove an image from a document | always | | [`vd_search_icon`](#vd_search_icon) | Search for an icon | always | | [`vd_search_image`](#vd_search_image) | Search for an image | CLI only, when an image key is configured | | [`vd_show`](#vd_show) | Show the design | always | | [`vd_skill`](#vd_skill) | Learn the vDesign format | always | ### vd_add_asset **Add an image to a document.** Offered: always. Register an image under a name, and optionally place it. The source is one of: {"url": "https://…"} (public https only); {"data": "\", "type": "image/png"} for bytes you actually have. **A picture the person gave you or has on their machine is neither** — you cannot pass on an image you can see, and no tool reads file paths. For that, do not call this tool: write the image with a src nothing supplies yet and a name saying what goes there (`image nav.logo … src=logo name="Your logo"`). The canvas shows it as a slot the person drops the file into; carry on, and tell them it is there. Never write an asset line yourself: place the image with `image src=`. | Argument | Type | Required | Description | | -------- | --------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | | `name` | string | yes | The name `image … src=` will use, e.g. "logo" | | `from` | { `url`: string } or { `data`: string, `type`?: string } | yes | | | `place` | { `parent`: string, `x`: number, `y`: number, `w`?: number, `h`?: number, `fit`?: one of `cover`, `contain`, `fill` } | no | | ### vd_add_icon **Place icons in a document.** Offered: always. Place Lucide icons as vector nodes, by name — a name from vd_search_icon, or one you know (old names like "home" or "alert-circle" still work). Up to 20 in one call, one undo step; all land or none do. x, y and size take a number or an expression such as "(@cta.left + 16)". The stroke scales with the size as Lucide’s does; color defaults to the document’s ink token, or the text colour. | Argument | Type | Required | Description | | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | | `icons` | list of { `name`: string, `id`: string, `parent`: string, `x`: number or string, `y`: number or string, `size`?: number or string, `color`?: string, `strokeWidth`?: number or string } | yes | 1 to 20 items | ### vd_add_image **Add a found image to a document.** Offered: CLI only, when an image key is configured. Download a candidate from vd_search_image into a document, registering it under a name with its attribution. Optionally place it as a node. Adding the same image twice is harmless: assets are keyed by content. | Argument | Type | Required | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | | `provider` | one of `unsplash`, `pexels` | yes | | | `candidate` | { `id`: string, `description`: string, `width`: number, `height`: number, `url`: string, `credit`: string, `source`: string, `license`: string } | yes | One candidate object, exactly as vd_search_image returned it | | `name` | string | yes | The registry name an image node will refer to | | `place` | { `parent`: string, `x`: number, `y`: number, `w`: number, `h`: number, `fit`?: one of `cover`, `contain`, `fill` } | no | | ### vd_assets **List a document’s images.** Offered: always. Every image registered in a document: its name (what `image … src=` refers to), type, pixel size, bytes, attribution, and which nodes use it — then the slots still waiting for the person to drop a picture into. | Argument | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | ### vd_create **Create a document.** Offered: always. Start a new .vd document. Leave "dsl" out for an empty canvas with no page — insert the frame you need (vd_patch: insert frame page x=0 y=0 w=1280 h=720) — or pass a skeleton: title, tokens, fonts and the frames. Then add each region with its own vd_patch rather than writing the whole design here: the person sees each call land, and one large call is a long wait on an empty canvas. Load the vdesign skill first if you do not know the DSL (vd_skill). Nothing is created if the DSL would not parse. **You cannot save it.** Against a running app the new document lives in a window until the person using it saves — vd_list shows which documents that applies to. | Argument | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `label` | string | yes | A short human name, e.g. "Coffee shop landing". It becomes the document title and the tab name. Rename later with the patch statement: title "New name" | | `dsl` | string | no | A whole document. Omitted, you get an empty canvas | ### vd_export **Export a document.** Offered: desktop and CLI only. Export a document as png, jpg, webp, svg, pdf, ai (Adobe Illustrator) or html. The export is exactly what the canvas shows. **Rasters come back as images you can look at** — use png with save:false to check your own layout. save:true puts the files where the person can find them — beside the document. Only save when the person asked for a file. Every frame is exported unless you name frames or nodes. | Argument | Type | Required | Description | | ------------ | ------------------------------------------------------- | -------- | --------------------------------------------------------------------------- | | `handle` | string | yes | The document, exactly as vd_list gave its handle | | `format` | one of `png`, `jpg`, `webp`, `svg`, `pdf`, `ai`, `html` | yes | | | `save` | boolean | yes | true when the person asked for a file; false to look at the result yourself | | `frames` | list of string | no | Frame ids; every frame when omitted | | `nodes` | list of string | no | Node ids, cut to what they draw — a logo, a card | | `scale` | number | no | Rasters: 2 for retina. Default 1 | | `width` | integer | no | Rasters: exact width in px | | `height` | integer | no | Rasters: exact height in px | | `quality` | integer | no | jpg and webp; default 90. 1 to 100 | | `lossless` | boolean | no | webp only | | `background` | string | no | A CSS colour, or "transparent". jpg is flattened onto white by default | | `dpi` | number | no | png and jpg print metadata | | `text` | one of `live`, `outline` | no | svg: live text (default) or outlines, for Figma or Illustrator | | `artboards` | one of `canvas`, `frames` | no | ai: one artboard holding every frame (default), or one file per frame | | `zip` | boolean | no | Several files as one .zip | | `preview` | boolean | no | svg, pdf, ai, html: also return PNG pictures of the result for you to see | ### vd_list **List documents.** Offered: always. The documents available here, with the handle every other tool needs. Call this first: a handle you guessed is how a session starts by editing the wrong document, and there is no other way to learn one. Takes no arguments. ### vd_open **Open a document in vDesign.** Offered: remote only. A link that opens a document vd_list marks "not open" in vDesign, on the web or the desktop app. Give it to the person: such a document was made while vDesign was not open, and is kept for a week until they open it. Its handle keeps working afterwards. | Argument | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | ### vd_patch **Edit a document.** Offered: always. Apply mutation statements (set / insert / delete / move / motion) to a .vd document. The dialect is the skill’s reference/edits.md (vd_skill path "reference/edits.md"). Nothing is written if any statement fails to parse. | Argument | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | | `statements` | string | yes | One statement per line | ### vd_problems **Check a document.** Offered: always. Everything wrong with a .vd document: what would not parse, and what would not draw. Worth calling after any edit you are not certain about. | Argument | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | ### vd_read **Read a document.** Offered: always. The current state of a .vd document. Defaults to an outline — every node with its id, type and box — which is what most edits need. Ask for a node id to see one subtree in full, or mode "full" for the whole design. Never returns image bytes. | Argument | Type | Required | Description | | -------- | ------------------------ | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | | `mode` | one of `outline`, `full` | no | Default `outline` | | `select` | string | no | A node id, to return just that subtree | ### vd_remove_asset **Remove an image from a document.** Offered: always. Unregister an image. Refused while nodes still draw it, unless force is true, which deletes those image nodes too. | Argument | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | | `name` | string | yes | | | `force` | boolean | no | Default `false` | ### vd_search_icon **Search for an icon.** Offered: always. Find icons in the Lucide set (1,900 line icons) by name or by what they show: "settings", "notification", "arrow right". One line per icon, its name then its tags. Place one with vd_add_icon — never write an icon’s path data yourself. Lucide has no brand logos (GitHub, X, YouTube): leave a logo as an image slot. | Argument | Type | Required | Description | | -------- | ------- | -------- | --------------------- | | `query` | string | yes | | | `limit` | integer | no | 1 to 30, default `12` | ### vd_search_image **Search for an image.** Offered: CLI only, when an image key is configured. Find a freely licensed photograph. Providers: unsplash, pexels. Returns candidates with their attribution; pass one whole candidate to vd_add_image. | Argument | Type | Required | Description | | ---------- | --------------------------- | -------- | -------------------- | | `query` | string | yes | | | `limit` | integer | no | 1 to 20, default `6` | | `provider` | one of `unsplash`, `pexels` | no | | ### vd_show **Show the design.** Offered: always. Show the person a document in the chat, as a preview of its pages. Call it once, when a piece of work is done, never after each vd_create or vd_patch: every call draws another preview. Pictures show as placeholders. | Argument | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------ | | `handle` | string | yes | The document, exactly as vd_list gave its handle | ### vd_skill **Learn the vDesign format.** Offered: always. Loads the vdesign skill. Author and edit vDesign documents — posters, slides, hero sections, card grids, any laid-out visual document — in the .vd DSL. Use when asked to create, change, or read a vDesign document, a .vd file, or when a task involves designing a layout that should stay editable and relational rather than being flattened to fixed coordinates. Returns SKILL.md unless asked for another file; ask for one only when you need it, which is what keeps this cheap. | Argument | Type | Required | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | one of `SKILL.md`, `examples/cards.vd`, `examples/deck.vd`, `examples/hero.vd`, `examples/slide.vd`, `reference/edits.md`, `reference/grammar.md`, `reference/motion.md`, `reference/nodes.md`, `overview`, `grammar`, `nodes`, `edits`, `motion`, `example:hero`, `example:cards`, `example:slide`, `example:deck` | no | A file in the skill, by its path. SKILL.md first; the files it names only when you need them. The older topic names (overview, grammar, …) still work. Default SKILL.md | | `topic` | one of `overview`, `grammar`, `nodes`, `edits`, `motion`, `example:hero`, `example:cards`, `example:slide`, `example:deck` | no | The older name for path; use path | # API reference Every function and type `@getv.design/embed` exports, generated from the declarations the package publishes. How to use them is on [Embedding](https://getv.design/docs/embed.md). The package is at 0.x: an API may still change in a minor version. ## Entries Four JavaScript entries, each an ES module, and two stylesheets. The editor and the exact-fonts code are separate files that load only when a page asks for them, so importing an entry costs only what it needs. ### `@getv.design/embed` ```ts const version: string; ``` The package's version, as published: `0.1.0`, say. ```ts function mount( el: HTMLElement, source: Source, options: MountOptions, ): Promise; ``` Show a document in `el`, to view, present or edit. Types: every type below. ### `@getv.design/embed/static` ```ts function parse(text: string): ParseResult; ``` Read `.vd` text. Never throws: what is wrong comes back as `problems`. ```ts function renderSvg(source: Source, options?: RenderSvgOptions): Promise; ``` Draw a document, or one of its pages, as a standalone SVG. ```ts function designOf(text: string): string | undefined; ``` The `.vd` text in `text`: itself for a `.vd`, the design it carries for a `.vd.svg`, `undefined` for an SVG that carries none. For copying a design out of an SVG as text. ```ts function outline(text: string): Outline; ``` A document's title and pages, each with the line its `frame` is written on: what an editor needs for an outline, breadcrumbs, and keeping a preview on the page the cursor is in. Never throws. The line is found by reading the text for the page's top-level `frame` line, never by evaluating anything, so it is as cheap as parsing. ```ts function createRenderer(options?: RendererOptions): Promise; ``` A renderer that draws synchronously once it exists. Exact fonts are fetched here, all of them, because which ones a later document needs is not known yet and the render cannot wait to find out. ```ts function approximateRenderer(): Renderer; ``` The renderer with no font files, made synchronously: what the Markdown adapters use when a host does not hand them one, because a markdown-it plugin is installed in a synchronous call. Also `version`, as above. Types: [`Outline`](#outline), [`OutlinePage`](#outlinepage), [`ParseResult`](#parseresult), [`Problem`](#problem), [`Renderer`](#renderer), [`RendererOptions`](#rendereroptions), [`RendererRenderOptions`](#rendererrenderoptions), [`RenderSvgOptions`](#rendersvgoptions), [`Source`](#source), [`VdDocument`](#vddocument). ### `@getv.design/embed/view` ```ts function view(el: HTMLElement, source: Source, options?: ViewOptions): Promise; ``` Show a document in `el`. Rejects, leaving `el` untouched, if the source cannot be read. Also `version`, as above. Types: every type below. ### `@getv.design/embed/edit` ```ts function edit(el: HTMLElement, source: Source, options?: EditOptions): Promise; ``` Edit a document in `el`. Rejects, leaving `el` untouched, if the source cannot be read. Also `version`, as above. Types: every type below. ### `@getv.design/embed/markdown` ```ts function createFenceIds(prefix?: unknown): () => string; ``` Ids for one page of HTML: each fence on it gets its own prefix. ```ts function isVdFence(info: string | undefined | null): boolean; ``` The language a fence must name. `vdesign` is accepted too, for hosts that know `vd` as something else. ```ts function parseInfo(info: string): FenceOptions; ``` The attributes after the language: `key=value`, `key="quoted value"`, or a bare `key` for a flag. Unknown keys are ignored, so adding one later breaks no fence written today. ```ts function renderFence( body: string, info: string | FenceOptions, options?: FenceRenderOptions, ): string; ``` A fence to HTML. `info` is the whole info string (`vd page=1`) or options already parsed. Never throws: a fence that cannot be drawn comes back as its code block, with what was wrong. Also `createRenderer`, as above. Types: [`FenceOptions`](#fenceoptions), [`FenceRenderOptions`](#fencerenderoptions), [`Problem`](#problem), [`Renderer`](#renderer). ### `@getv.design/embed/markdown-it` ```ts function vdesignMarkdownIt(md: MarkdownItLike, options?: MarkdownItOptions): void; ``` `md.use(vdesignMarkdownIt, options)`. Also `createRenderer`, as above. Types: [`FenceOptions`](#fenceoptions), [`FenceRenderOptions`](#fencerenderoptions), [`MarkdownItOptions`](#markdownitoptions), [`Problem`](#problem), [`Renderer`](#renderer). ### `@getv.design/embed/rehype` ```ts function rehypeVdesign(options?: RehypeOptions): void; ``` `.use(rehypeVdesign, options)`. Types: [`FenceOptions`](#fenceoptions), [`FenceRenderOptions`](#fencerenderoptions), [`Problem`](#problem), [`RehypeOptions`](#rehypeoptions), [`Renderer`](#renderer). ### `@getv.design/embed/marked` ```ts function vdesignMarked(options?: FenceRenderOptions): { renderer: { code(this: unknown, token: CodeToken | string, info?: string): string | false }; }; ``` A marked extension. Returning `false` hands the block back to marked's own renderer. Also `createRenderer`, as above. Types: [`FenceOptions`](#fenceoptions), [`FenceRenderOptions`](#fencerenderoptions), [`Problem`](#problem), [`Renderer`](#renderer). ### `@getv.design/embed/auto` ```ts function hydrate(root?: ParentNode, options?: HydrateOptions): Promise; ``` Every `figure.vd-figure[data-vd-mode="view"]` under `root` that is not live yet. Resolves once each has opened or stayed a drawing; never rejects. Safe to call again after the page changes: figures already live are skipped. Types: [`HydrateOptions`](#hydrateoptions), [`ViewHandle`](#viewhandle), [`ViewOptions`](#viewoptions). ### `@getv.design/embed/view.css`, `@getv.design/embed/edit.css` The styles the view and the editor otherwise insert as a `