Skip to main content
Audience: AI coding agents (and teams running them). This page is part of the deterministic agent workflow for building and verifying Velt UI customizations. Customizing by hand? Start with the Approaches and Feature Guides.
The fidelity-critical inputs (spacing, padding, sizing, radius, typography, colours, and the design’s own icon SVGs) are a data-extraction problem, not a judgment one. Never approximate them from a screenshot. Produce a designSpec first, then apply those EXACT numbers and verify rendered output against them.
Use the same output contract whether extraction is automated or manual: exact measurements, exported SVG assets, frame-relative boxes, and a per-state checklist.

Producing the designSpec

  • REST path (preferred): With a Figma token, fetch the node JSON from api.figma.com and emit designSpec.json plus exported icon SVGs under assets/.
  • MCP fallback (no token): Save Figma MCP outputs (get_variable_defs, and a node tree from get_metadata/get_design_context) to a dump and convert it into the same designSpec schema. This has lower fidelity because it is limited to what the MCP exposes; prefer REST when available.
Each designSpec node carries cssDecls: CSS-ready, exact declarations: a box (x/y/w/h), and a frameId (the top-level block-frame it belongs to). Apply the cssDecls to the real classes from Manifest; then diff rendered computed styles and layout boxes against them. Boxes should be emitted frame-relative (boxSpace: "frame-relative"): subtract each frame’s own origin from its subtree, so box.x/y are relative to that frame’s top-left and directly comparable to both getBoundingClientRect − surface-root measurements and the per-block frame PNG.
  • Why per-frame (not one root origin): a multi-state design is one section of many frames laid out across the canvas (e.g. node 1:3398 = 16 sidebar frames), and each frame is exported as its own PNG at 0,0. Subtracting a single section origin left every node ~1500px off, so visual-diff --mask-text-from mislocated every mask and the visual gate went blind (passing wrong icons / structure / spacing as “matched”). Per-frame normalization fixes that. The spec also lists frames: [{id,name,type}].
  • Per-block consumption: when you extract a section once and diff one block, filter to the block’s frame id so only that block’s (already frame-relative) nodes are used. Omit this filtering only when the spec is a single-frame extraction.

The mapping rules (auto-layout → CSS, from FigmaToCode)

  • layoutMode HORIZONTAL/VERTICALdisplay:flex + flex-direction:row|column.
  • primaryAxisAlignItemsjustify-content (MIN→flex-start, CENTER→center, MAX→flex-end, SPACE_BETWEEN→space-between).
  • counterAxisAlignItemsalign-items (MIN/CENTER/MAX/BASELINE).
  • itemSpacinggap: suppressed when SPACE_BETWEEN (the browser distributes the space; an explicit gap would be wrong).
  • paddingT/R/B/Lpadding, collapsed to the concise form (14px 16px).
  • Sizing is axis-dependent: the rule most converters get wrong (and we did): for a child of an auto-layout parent, FILL on the parent’s primary axis → flex:1 1 0; FILL on the counter axis → align-self:stretch; HUG → emit nothing (content-driven); FIXED → literal px. Getting this wrong is what squeezed the dialog.
  • cornerRadiusborder-radius; strokes+strokeWeightborder; first solid fillsbackground (or color on a TEXT node); stylefont-family/size/weight/line-height/letter-spacing.

Icons: export the design’s real SVGs (R17) + assign them to slots

Flag icon-candidate nodes (vectors, or small ≤64px vector-only subtrees), export each as an SVG (id-suffixed filename so generic names like “Icon”/“Vector” don’t collide), and assign each to a slot using the manifest’s iconHint, in confidence layers (stop at the first that matches; never guess):
  1. nearText (reliable): the icon’s adjacent label: a menu row is [icon, "Edit"], the reply affordance is [icon, "Reply"]. Auto-assign (de-duped: one SVG → one slot). Output: designSpec.iconAssignments[reactPath] = { file, by, glyph }.
  2. name / component-signal (S3): an icon-only control is frequently a named Figma icon component (an iconButton/Icon instance) or a node named for its glyph (filterIcon, more, checkCircle). The resolver matches the slot’s glyph (+ synonyms: filter-lines→filter/funnel, kebab→more/ellipsis/dots, check-circle→check/resolve/tick, …) against the icon’s own name + ancestry, preferring a single name hit or a single named component. This is the layer that fixes the M2a filter + kebab misses (named components with no adjacent label).
  3. ancestryKeyword (weak): only accepted when it resolves to exactly one free icon; a broad keyword (e.g. “comment”) matches many → left unassigned, never guessed.
  4. unassignedIcons → render-and-recognize: anything still unmatched is reported here with { slot, hint:{glyph}, renderRecognize:true, candidates:[{file,name,isComponent,box}] }: a shortlist of the free exported SVGs (name-hits first, else the icon components). Rasterize each candidate SVG, identify the glyph visually by opening the assets/*.svg file, then wire the one matching the glyph into the slot. Explicit recognition: never a Velt default, never hand-drawn, never a blind guess.
Distinct slots need distinct glyphs. Never reuse one icon for another slot. In particular, resolve ≠ unresolve: the ResolveButton is a check-circle; the UnresolveButton is a counter-clockwise reopen/undo arrow (synonyms: reopen / undo / restore / rotate-ccw / refresh) and it appears only in the resolved-state frame as an icon-only control. Resolve it via render-and-recognize like any icon-only control (layers 2-4); do NOT fill UnresolveButton with the resolve check, because that conflation is a recurring miss. Same rule for any visually-similar pair (filter vs sort, edit vs copy): a mustSupply slot gets the glyph for that slot, never the nearest already-resolved one. Wire the assigned + recognized SVGs into the mustSupply icon slots as a small icons/ file of React SVG components. Never leave a Velt default icon and never hand-draw one.

Token handling

The REST path needs a Figma personal-access token; it is optional (no token → MCP fallback, never a halt). Resolution: FIGMA_TOKEN env var first, then the OS secure store: never the target repo’s .env.
  • Check token presence without printing the value.
  • Read tokens from STDIN or the OS secure store, never argv/history.
  • Remove stored tokens when the extraction workflow is done. The token is sent only to api.figma.com over HTTPS, never logged in full, never written to a tracked file. A read-only / file-content-scoped PAT is recommended.