Skip to content

Design Import API Reference

Technical reference for the Pulp design import pipeline. For a getting-started guide, see Importing Designs.

CLI Commands

import-design

Import a design from an external tool into Pulp JS, DesignIR, or baked C++ artifacts.

pulp import-design --file <design> [--from <source>] [options]

Sources: fig, figma, figma-plugin, stitch, v0, pencil, claude, html, designmd, jsx

Runnable .html/.htm files are detected automatically. Claude bundle fingerprints retain claude provenance; ordinary pages use the distinct html source identity. Both use the same isolated Chromium evaluator.

fig decodes a local Figma .fig save file offline into the same envelope consumed by the figma-plugin path. figma-plugin is the "Design for Pulp" Figma plugin export (a semantic audio-widget envelope; see the Figma plugin guide). It accepts a .pulp.zip directly. figma is the raw Figma REST/file import.

Flag Description Default
--from <source> Design source. Optional for runnable HTML, where the CLI detects claude versus generic html. auto for HTML
--file <path> Input file path. Repeatable with --from figma-plugin — pass one faithful-vector envelope per state to capture a multi-state design into one DesignFrameView; the order given is the frame index a swap <n> layer targets (the first --file is frame 0).
--url <url> URL that serves design JSON/HTML directly (e.g. a v0 share link). Fetched unauthenticated — a figma.com file URL does not work; see Importing from Figma
--frame <name> Frame/artboard to import. For --from fig, pass a frame guid or name; required unless using --outline. first frame for figma
--page <name> Restrict local .fig frame lookup to one page
--outline List pages/frames of a local .fig file and exit
--screen <name> Screen to import (Stitch) first screen
--output <path> Destination file for the primary artifact ui.js
--emit {js\|ir-json\|cpp\|swiftui} Primary artifact kind. js, ir-json, cpp, and swiftui are implemented; cpp and swiftui require --mode baked. swiftui emits a baked native SwiftUI view (ImportedPulpView.swift + a per-view <RootView>Theme.swift + binding manifest). js built-in, or import_design.default_emit
--mode {live\|baked} Runtime model. live is the built-in default. baked emits canonical IR or baked C++ via --emit ir-json\|cpp. live built-in, or import_design.default_mode
--snapshot-semantics {fail\|warn\|accept} JSX baked snapshot policy. fail rejects dynamic APIs by default, warn proceeds with diagnostics, and accept proceeds silently. fail
--allow-network-fetch Allow DesignIR asset-manifest HTTP(S) fetches at import time. off
--browser <path> Explicit Chromium/Chrome executable for browser-solved HTML import; overrides path env, mode, managed, and system selection.
--browser-interactions <json> Apply a versioned bounded click/type/wait plan before browser evidence capture. initial state
--offline Explicitly use the lower-fidelity static HTML parser instead of Chromium. off
--allow-browser-network Permit only the source document's declared public HTTPS origins during browser evaluation; local/private destinations remain blocked and fetched content is recorded in capture provenance. off
--asset-cache <path> Asset cache directory for HTTP(S) imports. PULP_IMPORT_ASSET_CACHE or user cache
--asset-timeout-ms <ms> Per-request network asset timeout. 30000
--asset-hash <uri=sha256> Expected content hash for an asset URI; may be repeated.
--tokens <path> Output token file tokens.json (theme.css for css-variables)
--format {w3c\|css-variables\|tailwind\|json-tailwind\|css-tailwind} Token export format. w3c (DTCG JSON) is the default; css-variables emits CSS custom properties (base → :root, .dark-suffixed modes → @media (prefers-color-scheme: dark)); the tailwind variants currently require --from designmd. w3c
--dry-run Show generated code without writing
--no-tokens Skip token extraction
--no-comments Omit comments from generated code
--web-compat Use DOM API instead of native Pulp API
--validate Render generated JS and validate layout. Browser-backed HTML always runs its required browser-to-DesignIR A/B validation; this flag additionally publishes convenience render/diff files beside the primary output.
--screenshot-backend {skia\|coregraphics} Validation render backend; browser-backed HTML uses it for the automatic A/B gate. Skia (default) composites file-backed images; CoreGraphics does not (it draws an image's filename placeholder — not faithful). skia
--reference <png> Compare render against reference screenshot
--diff <png> Save visual diff image
--render-size WxH Render dimensions 340x280
--preview Minimal widget styling for design comparison
--debug Output JSON report with metrics
--debug-output <path> Save debug JSON to file stdout
--detect-only Detect source, format-version, and parser-version without parsing; see imports/ for detection details.
--directory <path> Path to directory export (alternative to --file)
--compat <path> Override compat.json discovery walk up from input
--report-new-format Emit a fingerprint-diff JSON for a new format-version. Implies --detect-only

Either --file or --url is required (or --directory for --detect-only). When --url is provided without --file, the URL is fetched through an argv-safe curl invocation into a unique temporary file. Literal --file paths are read directly and may contain normal filesystem punctuation; --url still rejects shell metacharacters before fetching.

For a secondary prototype screen, pass --browser-interactions <plan.json>. The versioned pulp-browser-interactions-v1 document accepts only bounded click, type, wait-for, and wait-ms actions. Selectors and action results are saved in the capture evidence; typed text is represented only by its length, with neither plaintext nor a per-action text hash in the interaction report. Typed text remains rendered page state and may appear in screenshots, DOM/semantic evidence, or tokens, so plans must never contain secrets. There is deliberately no arbitrary JavaScript action, and popup pages are rejected. Per-action selector timeouts remain bounded by the capture-wide timeout and cannot extend it. A wait-for action checks the live rendered element state, so hidden or inert DOM content does not count merely because its strings exist in the document. A source may expose globalThis.__pulpInteractionReady for a distinct post-action asynchronous completion boundary; the initial __pulpCaptureReady contract remains one-shot.

{
  "schema": "pulp-browser-interactions-v1",
  "version": 1,
  "actions": [
    { "action": "click", "selector": "[data-view='patch']" },
    {
      "action": "wait-for",
      "selector": "[data-screen='patch-composer']",
      "state": "visible"
    }
  ]
}

--url fetches unauthenticated and sends no credential of any kind. It is therefore only useful for URLs that serve the design data itself — a v0 share link, or any host that returns design JSON/HTML directly. It is not a Figma import path; see below.

Importing from Figma

A figma.com file URL cannot be imported with --url. The CLI has no Figma credential (there is no token flag, and the fetch is a bare unauthenticated curl), so a private file returns HTTP 403 and a public one returns the Figma web app's HTML shell rather than design data. The CLI rejects such URLs up front and points at the lanes below. The only authenticated Figma REST client in the repo is tools/import-design/figma_rest_export.py.

Use these instead, local-first:

Lane Command When
Figma desktop MCP get_design_context / get_metadata / get_screenshot Inspecting or verifying a design; no rate limit
"Design for Pulp" desktop plugin pulp import-design --from figma-plugin --file <export>.pulp.zip Exporting a scene for import — see the Figma plugin guide
Local .fig file pulp import-design --from fig --file design.fig --outline Fully offline, from a .fig save file
Figma REST (headless/CI) tools/import-design/figma_rest_export.py --token <pat> Only when neither desktop lane is available; subject to REST rate limits (HTTP 429)

--from figma --file design.json remains valid for IR-format JSON you already have on disk (for example, translated from Figma MCP output).

The shipped default is live runtime import: --mode live --emit js. In that mode Pulp keeps the generated JS/runtime artifact, which is the right default for iteration, hot reload, dynamic React behavior, and design-tool workflows. Users who strongly prefer a different default can persist it in ~/.pulp/config.toml:

pulp config set import_design.default_mode baked
pulp config set import_design.default_emit ir-json

For baked C++ by default, set import_design.default_emit cpp as well. If only import_design.default_mode baked is set, Pulp chooses ir-json as the baked artifact default. PULP_IMPORT_DESIGN_DEFAULT_MODE and PULP_IMPORT_DESIGN_DEFAULT_EMIT override those config keys for one environment/session, and direct --mode / --emit flags override the matching preference. pulp status reports the effective import-design defaults.

Browser selection uses --browser > PULP_DESIGN_BROWSER > PULP_DESIGN_BROWSER_MODE / import_design.browser > an explicitly installed managed Chrome for Testing > system Chrome/Chromium. auto is the default; installing the managed option is explicit and never happens during import:

pulp tool install chrome-for-testing
pulp config set import_design.browser auto

Mental model:

Flow What ships Runtime behavior
Live/runtime import The generated JS or precompiled React bundle Runs the app at launch through the JS/React/runtime bridge
Baked DesignIR A serialized snapshot of the materialized UI tree No React program remains; the IR is an inspectable UI blueprint
Baked C++ Native C++ generated from DesignIR Constructs native views directly without a JS engine
Baked SwiftUI Native SwiftUI generated from DesignIR A compiled declarative Apple view; no Pulp JS runtime

Live/runtime import means "run the original app." Baked DesignIR means "run the app once and save the resulting UI structure." Baked C++ means "compile that saved structure into native code." You can move from live iteration to a baked snapshot and then to baked C++; you cannot reconstruct the original live React program from baked IR because loops, hooks, closures, and arbitrary JS logic are not preserved in the snapshot.

--emit ir-json writes a canonical DesignIR v1 envelope. Asset collection runs before serialization. Local files and data URIs are recorded by default; HTTP(S) asset fetches require explicit --allow-network-fetch consent and are cached by content hash. For --url imports, relative asset references resolve against the source URL while the authored relative value remains in the manifest as original_uri.

The IR envelope also records document-level provenance (capture_method, settle_rounds, fallback_reason, source_adapter, source_version, imported_at) plus structured diagnostics. All source adapters return the shared normalized form: interactive frame nodes are promoted through the library normalization pass before code generation or IR serialization.

--emit swiftui (baked-only, macOS 13 / iOS 16 floor) lowers the same DesignIR to native SwiftUI — a fourth target alongside DOM/native-JS/baked-C++. It writes ImportedPulpView.swift (a View generic over the PulpParameterResolving protocol from PulpSwift), a sibling per-view theme file <RootView>Theme.swift, and a .bindings.json manifest. Frames lower to VStack/HStack, text to Text, fixed frame/padding/background modifiers apply, and knob/slider/toggle bind to PulpKnob/PulpSlider/PulpToggle. Tokens lower to a code-first <RootView>Theme enum (named per-view so two imports don't collide in one Swift target) whose .dark-suffixed entries become light/dark dynamic colors. Binding resolves a generated key by exact PulpParameter.name match (Pulp has no stable string param key); missing or duplicate names render a visible placeholder rather than silently binding the wrong parameter. The full visual style set is emitted (opacity, corner radius, border, box-shadow, linear gradient, transform, mix-blend-mode, and mixed-style text), and rgb()/rgba() colours are supported alongside hex. Because SwiftUI stacks are not Yoga, flex layout is approximate: cross-axis alignment maps to the stack's alignment:, and space-between/around is approximated with Spacer()s. CSS grid lowers to a LazyVGrid (column count from grid-template-columns; exact track sizing approximated). Anything a SwiftUI stack cannot reproduce — flex-wrap, justify distribution, align:stretch, position:absolute (approximated with .offset), skew/matrix transforms, per-side borders, and multi-/inset shadows — is reported as a fidelity: warning; the ones that genuinely render wrong fail the import under --strict-fidelity (exit 4). The audio meter/xy_pad/waveform/spectrum widgets map to native PulpMeter/PulpXYPad/PulpWaveform/PulpSpectrum views, text buttons to a SwiftUI Button, and images to Image("<asset_id>") (bundled, by asset-catalog id) or AsyncImage (remote); svg/canvas vectors remain deferred. A templates/swiftui-design-host/ scaffold mounts the generated root view in a standalone app for Simulator/macOS preview.

For --from jsx --mode live --emit js, Pulp writes the precompiled bundle verbatim for runtime import. That pass-through path does not parse or render the bundle, so --validate, --reference, --diff, and --debug are rejected; use baked IR or baked C++ when an import report or native snapshot validation is needed. For JSX baked IR/C++ snapshots, Pulp first runs the runtime harness and walks the materialized DOM; when a live/native bundle routes React through @pulp/react and leaves no expanded DOM, the harness freezes the native WidgetBridge tree instead (capture_method: runtime_native_snapshot, snapshotSource: native-view). Pulp scans the precompiled bundle for dynamic APIs that make a frozen snapshot non-deterministic (setInterval, setTimeout, requestAnimationFrame, Date.now, new Date, performance.now, Math.random, and fetch). Comments and string literals are ignored. The default --snapshot-semantics fail exits with code 2. warn emits the IR and records a snapshot-dynamic-api diagnostic; accept emits the IR or C++ with provenance recording the accepted policy.

Generated baked/native artifacts can link the core view target, pulp::view-core, when they only construct View trees and do not evaluate JS. Live import, ScriptEngine, WidgetBridge, and scripted UI consumers should link pulp::view-script or the full compatibility target, pulp::view.

export-tokens

Export a Pulp theme as W3C Design Tokens JSON (default) or another --format.

pulp export-tokens [options]
pulp export-tokens --format css-variables          # built-in dark theme → theme.css
pulp export-tokens --file theme.json --format css-variables --tokens theme.css
Flag Description Default
--file <path> Input theme JSON built-in dark theme
--format <fmt> Token export format (w3c, css-variables) w3c
--tokens <path> Output file tokens.json (theme.css for css-variables)
--dry-run Print to stdout

Intermediate Representation (IR)

All source adapters produce a normalized JSON IR before code generation. You can also write IR by hand. The canonical schema is DesignIR v1; the summary below covers the common node fields.

IRNode

{
  "type": "frame|text|image|button|input|slider|knob|fader|meter",
  "name": "ElementName",
  "content": "Text content (for text nodes)",
  "style": { ... },
  "layout": { ... },
  "label": "Audio widget label",
  "min": 0, "max": 1, "default": 0.5,
  "children": [ ... ]
}

IRStyle

CSS-like visual properties on each node:

Property Type Description
backgroundColor string #hex color
backgroundGradient string linear-gradient(...)
color string Text color
opacity float 0.0–1.0
borderRadius float Corner radius in px
border string e.g. "1px solid #333"
boxShadow string CSS box-shadow
filter string e.g. "blur(4px)"
fontFamily string Font name
fontSize float In px
fontWeight int 100–900
fontStyle string normal, italic
textAlign string left, center, right
letterSpacing float In px
lineHeight float Multiplier
textTransform string uppercase, etc.
overflow string hidden, scroll, auto
position string absolute, relative
top, left, right, bottom float Position offsets in px
zIndex int Stacking order
transform string CSS transform
width, height float In px
minWidth, minHeight float In px
maxWidth, maxHeight float In px

For importer and backend planning, classify visual properties with the rendering reference's effect/mask/composite split: filters and backdrop filters are effects, clip-path and mask properties constrain visibility, and mix-blend-mode / Canvas globalCompositeOperation control how a completed subtree composites back into its parent. Keeping those categories distinct prevents a design import gap from becoming the wrong runtime abstraction.

IRLayout

Flexbox layout properties for container nodes:

Property Type Default Description
direction string "column" "row" or "column"
gap float 0 Gap between children in px
padding float 0 Uniform padding (or use per-side)
paddingTop/Right/Bottom/Left float 0 Per-side padding
justify string "flex-start" Main axis alignment
align string "stretch" Cross axis alignment
wrap bool false Enable flex wrap
widthMode string "fixed" "fixed", "hug", "fill"
heightMode string "fixed" "fixed", "hug", "fill"

IRTokens

Design tokens extracted from the source:

{
  "tokens": {
    "colors": { "bg.primary": "#1a1a2e", "accent": "#e94560" },
    "dimensions": { "spacing.md": 16, "radius.md": 8 },
    "strings": { "font.family": "Inter" }
  }
}

Audio Widget Detection

Widgets are detected from node names (case-insensitive substring match):

Pattern Widget Type Pulp Function
knob, dial knob createKnob()
fader, slider fader createFader()
meter, level, vu meter createMeter()
xypad, xy_pad, xy pad xy_pad createXYPad()
waveform, oscilloscope waveform createWaveform()
spectrum, analyzer, analyser spectrum createSpectrum()

Audio widgets get additional IR properties: label, min, max, default.

Code Generation Modes

Native mode (default)

Uses Pulp's native widget bridge API: createCol(), createRow(), createKnob(), setFlex(), etc. Encodes Yoga layout constraints:

  • Every container has explicit height or flex_grow
  • Labels get minimum height (14px)
  • Knobs: min 56x56px
  • Faders: min 40px wide, 80px tall
  • Meters: min 20px wide, 80px tall

Web-compat mode (--web-compat)

Uses document.createElement() + element.style.* for compatibility with the web-compat JS layer.

W3C Design Tokens

Format

Follows the DTCG Design Tokens specification:

{
  "color": {
    "$type": "color",
    "primary": { "$value": "#89B4FA" },
    "bg": { "$value": "#1E1E2E" }
  }
}

Features

  • Group $type inheritance — children inherit $type from parent group
  • Aliases{ "$value": "{color.primary}" } with chained resolution (max 10 levels)
  • Cycle detection — circular aliases are detected and safely terminated
  • Math expressions"8 * 2", "{spacing.base} * 2" evaluated at parse time
  • Composite tokenstypography, shadow, border decomposed into individual tokens
  • Unit strippingpx, rem, em suffixes stripped for numeric evaluation

CSS custom properties (--format css-variables)

Emits the captured theme as CSS custom properties — the external, themeable output sink for the multi-mode tokens the importers capture. Base (light/default) tokens land under :root; tokens whose name carries the .dark multi-mode suffix (the convention the Figma plugin and DESIGN.md body parser use for dark-mode values) become overrides under @media (prefers-color-scheme: dark).

Token names map to custom-property ids by replacing . with - (color.bg--color-bg); colors become hex, dimensions get a px unit, and string tokens are emitted verbatim.

/* Generated by pulp import-design --format css-variables */
:root {
  --color-bg: #ffffff;
  --spacing-sm: 4px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #000000;
  }
}

This is an external artifact consumable by web tooling and Style-Dictionary pipelines. Pulp's runtime already resolves var(--x), but a loader that applies a themed @media CSS file through the theme/setProperty APIs is a separate, later step — the css-variables exporter does not claim runtime consumption. The [data-theme="dark"] selector strategy is intentionally not emitted; only @media is, until a real host-toggle consumer exists.

Token Sync

Figma Variables

// Parse Figma Variables (from MCP get_variable_defs)
Theme theme = parse_figma_variables(json);

// Export as Figma Variables format
std::string json = export_figma_variables(theme);

Figma uses slash-separated paths (color/primary) which are converted to dot-separated (color.primary) for Pulp themes.

Stitch Design Systems

// Parse Stitch Design System (from MCP list_design_systems)
Theme theme = parse_stitch_design_system(json);

// Export as Stitch format
std::string json = export_stitch_design_system(theme);

Maps Stitch-specific properties: colors, fonts, roundness (none/small/medium/large/full), spacing.

Source Adapter Details

Figma (parse_figma_json)

Accepts IR-format JSON. In Claude Code flows, the Figma MCP (get_design_context) provides the design data, which is translated to IR format using the Figma-to-Pulp mapping rules.

Figma plugin (parse_figma_plugin_json)

The "Design for Pulp" Figma plugin export envelope. Recognized Pulp library components (Knob / Fader / Meter / Toggle / XYPad / Waveform / Spectrum) carry semantic audio_widget metadata + parameter bindings, so they materialize as native, bound widgets rather than generic frames; everything else imports as layout/visual. Accepts a .pulp.zip (assets bundled) directly — the importer unpacks it and resolves assets from the manifest — or a bare .pulp.json. See the Figma plugin guide for the full workflow.

Stitch (parse_stitch_html)

Dual parsing: tries JSON IR first (from MCP get_screen), falls back to HTML tag extraction for basic structure.

v0 (parse_v0_tsx)

Dual parsing: tries JSON IR first (pre-processed by AI), falls back to Tailwind className extraction (flex-row, flex-col, gap-*, bg-*).

Pencil (parse_pencil_json)

Accepts IR-format JSON from MCP batch_get. Pencil uses Yoga layout internally, so layout translation is nearly 1:1. Frames default to horizontal (row) when layout is unspecified.

Validation

The --validate flag renders the generated JS headlessly and optionally compares against a reference screenshot:

# Validate layout renders correctly
pulp import-design --from figma --file design.json --validate

# Compare against source design screenshot
pulp import-design --from figma --file design.json \
    --validate --reference source.png --diff diff.png

Runnable browser-backed HTML is the exception: its browser-to-DesignIR A/B validation is a required adoption gate and therefore runs even without --validate. The proof remains under the durable browser-capture evidence directory. Passing --validate additionally publishes convenient render and diff copies beside the primary output.

Similarity threshold: 85% (PASS) / below 85% (NEEDS REVIEW).

The printed verdict is advisory--validate exits 0 at any similarity so existing callers keep working. To enforce a bar in CI, add --fail-below <pct>, which exits 5 when the similarity falls under it:

# Fails the build if the render drifts below 85% of the reference
pulp import-design --from figma --file design.json \
    --validate --reference source.png --fail-below 85

<pct> is a percentage from 0 to 100 (85, not 0.85) — matching the Similarity: NN% line it gates on. A fraction is rejected rather than read as a sub-1% threshold that would never fire.

Project Templates

Create a new project pre-configured for design import:

pulp create "My Plugin" --template from-figma
pulp create "My Plugin" --template from-v0

These generate placeholder UIs with import workflow instructions in the JS file.