| name | figma-from-code-build-screens |
| description | Subagent for figma-from-code Phase 4. Builds full-page screen frames in Figma by composing built component instances into 1440×900 layouts. Validates each screen against app screenshots and iterates up to 3 fix passes. |
| model | claude-sonnet-4-5 |
Skill: Build Screens (Phase 4)
Builds full-page screen frames in Figma by composing built component instances into screenBodySize layouts (default 1440x900). Each screen assembles navigation, list panels, and detail/form panels using instances of components already created in Phase 3, then validates them visually against an app screenshot — iterating up to 3 fix passes to converge on visual fidelity.
All Figma MCP tools (use_figma, get_screenshot, etc.) are available. This skill runs its entire workflow — analyze, build, screenshot, compare, fix — inline when dispatched by the orchestrator.
When to Use
- When
figma-from-code reaches Phase 4
- Standalone to rebuild screen layouts after component changes
- To add new screens after adding pages to the app
Required Inputs
| Input | Description | Source |
|---|
screenName | PascalCase name (e.g., CasesPage, CreateCasePage) | Route → PascalCase conversion |
route | URL path on the dev server (e.g., /cases, /cases/new) | component-map.json → routes |
pageSourceFile | Path to the page's .tsx source | Component source discovery |
fileKey | Figma file key | State ledger or caller |
screensFrameId | Node ID of the Screens container to append into | state.json → figmaNodes |
appScreenshot | Path to the app screenshot PNG | .temp/figma-from-code/screenshots/screens/{name}/app.png |
textContent | Extracted text JSON from the live page | .temp/figma-from-code/screenshots/screens/{name}/text.json |
keyComponents | Top-level components rendered on the route + their descendants | component-map.json → tree |
builtComponents | Map of {componentName: nodeId} for all components built in Phase 3 | state.json → builtComponents |
preExistingScreens | Immutable snapshot of screen frames that existed in Figma BEFORE this orchestrator run started | state.json → preExistingScreens (Phase 0a snapshot) |
screenshotDir | Directory for saving Figma screenshots and diff artifacts | .temp/figma-from-code/screenshots/screens/{name}/ |
Optional Inputs
| Input | Description |
|---|
computedStyles | Resolved CSS values from computed-styles.json (produced by inspect-styles.js against the page root). Authoritative for colors, spacing, typography on screen chrome (the page's own elements, not its component children) |
screenBodySize | Body dimensions read from state.json → screenBodySize if the project uses a non-default screen size (default 1440x900) |
Config placeholders like {pagesRoot} resolve from state.json → config (fields: devServerUrl, devServerStart, sourceDir, componentsRoot, pagesRoot, cssPath, tailwindConfigPath, iconLibrary, skillRoot).
Pre-Existing Screens Rule
Before doing any work that resolves to a node ID in preExistingScreens, stop. That node existed in Figma before this run; modifying it (rebuild, resize, delete + recreate, restructure) requires explicit user authorization.
Concretely:
- If
screenName itself maps to a node in preExistingScreens and the caller's intent is to rebuild that node: write a result file with "status": "needs_authorization" and "preExistingTouched": ["<name>"], then return. Do not call use_figma to delete, replace, resize, or restyle the existing node. Building a fresh screen with the same name into the same screensFrameId is also a modification (creates a duplicate the orchestrator must reconcile) — don't do it without authorization.
- Instancing a component that is itself in
preExistingComponents is fine — that's reuse, not modification.
- The fix-loop in Step 5 must never edit a node in
preExistingScreens. If the comparison says you need to, escalate to the orchestrator instead.
This rule overrides Steps 2–5 of the workflow when in conflict.
Execution Mode
The orchestrator dispatches one subagent per screen. Each subagent runs the entire workflow (Steps 1–7) independently — analyze, build via use_figma, screenshot via get_screenshot, compare, fix loop, and return results.
The subagent writes its result to .temp/figma-from-code/build-results/screens/{screenName}.json. The orchestrator collects results after all subagents complete using collect-screen-results.js.
Screens can run in parallel — they only instantiate (not modify) already-built Phase 3 components.
Agent Prompt Template
The orchestrator dispatches one agent per screen. Send all agents in a single message so they run in parallel.
Build a Figma screen frame by composing built component instances, then validate visually.
Follow this skill's workflow (all 7 steps):
0. Verify all referenced components exist in builtComponents
1. FIRST study the app screenshot to understand what the default view shows,
THEN analyze the page source — only include components visible in the screenshot.
Exclude components behind conditional branches (ternaries, state guards, URL params)
and portal-rendered overlays (Dialog, Sheet, etc.) that aren't visible by default.
2. Build the screen in Figma via use_figma (`screenBodySize` frame, default 1440x900)
3. Screenshot the result via get_screenshot
4. Structural check (content must match), then sizing check, then pixel diff
5. If mismatch, diagnose and fix (up to 3 iterations)
6. Write figma-screen.json tracking file
7. Write results to .temp/figma-from-code/build-results/screens/{screenName}.json
Inputs:
- Screen name: {screenName}
- Route: {route}
- Page source file: {pageSourceFile}
- Figma file key: {fileKey}
- Screens frame node ID: {screensFrameId}
- App screenshot: .temp/figma-from-code/screenshots/screens/{screenName}/app.png
- Text content: .temp/figma-from-code/screenshots/screens/{screenName}/text.json
- Screenshot dir: .temp/figma-from-code/screenshots/screens/{screenName}/
- Built components (for instance reuse): .temp/figma-from-code/builtComponents.json
- Pre-existing screens (DO NOT MODIFY): {JSON.stringify(preExistingScreens)}
Read plugins/figma-from-code/skills/figma-from-code/8-build-screens/SKILL.md for the full workflow,
fixSizing() function, variant resolution, and common pitfalls.
Workflow
Step 0 Prereqs Verify all referenced components exist in builtComponents
Step 1 Analyze Study appScreenshot first, then read page source; filter out non-default conditional branches
Step 2 Build Create the screen frame in Figma via use_figma
Step 3 Screenshot Capture the Figma result via get_screenshot
Step 4 Compare Structural check + sizing sanity check + pixel diff against the app screenshot
Step 5 Fix Loop If mismatch, diagnose and fix (up to 3 iterations)
Step 6 Track Write figma-screen.json into the page folder
Step 7 Return Report result with node ID, match score, and any remaining issues
Step 0: Verify all components exist (prerequisite gate)
Before building any screen, verify that every component referenced by the screen exists in builtComponents from state.json.
Identify the screen's key components from component-map.json → tree (the top-level components on that route and all their descendants). Check each one against builtComponents. Also check every icon imported by the page source (e.g., Icon/Check).
If any component or icon is missing from builtComponents:
STOP — do not proceed to Step 1. Return immediately with a rejection result:
{
"screenName": "CasesPage",
"status": "rejected",
"reason": "missing_components",
"missingComponents": ["CaseDetails", "MenuList"],
"missingIcons": ["Icon/Trash"],
"availableComponents": ["AppHeader", "Sidebar", "Button"]
}
Write this to .temp/figma-from-code/build-results/screens/{screenName}.json so the orchestrator can see what's missing.
Standalone (no orchestrator) — if the caller is the user directly, surface the rejection in the conversation and ask how to proceed. Don't fall back to inlining the missing components, building stubs, or downgrading the build into "best effort" — those produce a different artifact than the skill is supposed to produce. The right options are: (a) build the missing components first via plugins/figma-from-code/skills/figma-from-code/7-build-component/SKILL.md, (b) abandon the screen, or (c) get explicit user authorization to deviate.
Only proceed to Step 1 if every required component and icon is confirmed present.
Step 1: Analyze the Screen
Before writing any use_figma code, analyze all inputs to plan the screen structure.
Pre-flight: dev server is required for Step 1f (live inspection). Step 1f — inspecting the rendered page in a browser via inspect-styles.js — is the authoritative source for colors, spacing, and layout on the page chrome (the elements the page itself renders, outside of component children). Do not silently skip it. If you don't already have a dev server URL (from the orchestrator state ledger, project memory, or the caller's arguments), pause and ask the user for one before proceeding past Step 1. Only skip Step 1f if the user explicitly says no dev server is available.
1-pre. Screenshot-first composition (MANDATORY)
Before reading source code, look at appScreenshot (app.png). This screenshot shows the ACTUAL default view of the page as rendered in the browser with no user interaction. It is the ground truth for what the screen should look like.
Study the screenshot and identify:
- Which regions are visible (sidebar, main content area, detail panels, headers, footers)
- Whether the main content area shows populated data or an empty/placeholder state (e.g., "No case selected", "Select an item")
- Which component instances are actually rendered vs hidden by conditional logic
- Whether any overlays, dialogs, or menus are open (they should not be in the default state)
The source code analysis in Steps 1a–1b must MATCH what the screenshot shows, not the full component tree from the JSX. If the source code contains components behind conditional branches that are not visible in the screenshot, those components must be excluded from composition.
1a. Identify the page composition
Read the page source file and determine:
- Layout direction: Is the page root a vertical stack (
flex-col), horizontal row (flex, flex-row), or a grid (grid)?
- Sizing: Screens are always fixed at
screenBodySize (default 1440x900; read from state.json → screenBodySize). The outermost screen frame is primaryAxisSizingMode='FIXED' and counterAxisSizingMode='FIXED'. Capture this explicitly — Step 4a verifies it.
- Container children: What is the top-level region structure? Typical pages have: top nav (full width), sidebar (fixed width), main content (fill), or a hero + sections stack. Identify each region and which built component instance lives there.
- Spacing:
gap-* classes on the page root map to itemSpacing. p-*, px-*, py-* map to padding.
- Background:
bg-* class on the page root. Resolve through CSS variables if needed (see Step 1g of plugins/figma-from-code/skills/figma-from-code/7-build-component/step-1-analyze.md for the full chain).
1b. Identify component instances (default state only)
Walk the page source JSX and list component references, but only include components visible in the default state (as shown in appScreenshot from Step 1-pre).
For each included component:
- Map it to
builtComponents[name] — that's the node ID to instantiate
- Note the variant props passed in code (e.g.,
<Button variant="primary" size="lg">) — these resolve to a specific variant inside the component set
- Note any sizing classes applied at the call site (
className="w-full", className="flex-1") — these translate to layoutSizingHorizontal='FILL' etc. on the instance
Conditional rendering analysis
When walking the JSX, identify and handle conditional patterns:
-
Ternary expressions (condition ? <A /> : <B />): Determine which branch is visible when the page first loads with no user interaction and no URL params beyond the base route. Cross-reference with appScreenshot — only include the branch that matches.
-
URL-param-gated components: For components guarded by useParams() values (e.g., id ? <Detail /> : <Fallback />), use the param-absent branch as default since the screen captures the base route.
-
State-hook-gated components: Components behind useState(false) or useState(null) guards are hidden by default — exclude them.
-
Logical AND guards ({flag && <C />}): Exclude unless the guard's initial value is truthy.
DO NOT compose components from non-default conditional branches. Those belong in separate screen variants if the pipeline supports them.
Portal-rendered overlay exclusion
Portal-rendered overlays — Dialog, Sheet, ConfirmationDialog, DropdownMenu, Popover, Drawer, AlertDialog, Tooltip, etc. — are NEVER visible in the default resting state. Exclude them from screen composition unless the appScreenshot explicitly shows them open.
These components are already handled by the component-level State axis in Phase 3 (see 7-build-component/step-1-analyze.md → "Conditional rendering of overlays"). Do not duplicate them in the screen frame.
1c. Identify icon usage on the page chrome
If the page renders any Lucide icons directly (not via a child component), map each to its builtComponents entry (Icon/{Name}) and size from the className. See Step 1c of plugins/figma-from-code/skills/figma-from-code/7-build-component/step-1-analyze.md for the size mapping table.
1d. Plan text content
Use textContent (from text.json) for any text the page itself renders (page titles, section headings, empty states). Never use generic placeholders. Text rendered inside a component instance is handled by that component's own master — don't try to override it from the screen.
1e. Identify pre-existing screen conflicts
Check whether screenName is in preExistingScreens. If yes — apply the Pre-Existing Screens Rule above and stop.
1f. Inspect the live page in Playwright
Before building, inspect the actual rendered page in the browser to capture computed styles on the page chrome. This provides ground-truth values for the page-level background, padding, and layout — more reliable than inferring from Tailwind classes alone.
Run inspect-styles.js against the page root selector on the dev server:
node {skillRoot}/scripts/inspect-styles.js \
"{devServerUrl}/{route}" \
--selector "[data-page='{ScreenName}'], main, #root > div" \
--output ".temp/figma-from-code/screenshots/screens/{ScreenName}/"
This produces:
| File | Contents |
|---|
computed-styles.json | Resolved CSS properties on the page root (background, padding, layout direction, gap), the element's class list, and layoutContext (viewport, offsetWidth/Height) |
How to use the outputs:
Use exact padding and gap values to set Figma properties — these are the authoritative values for the page chrome. For colors, the computed RGB values are authoritative for what the color is, but not for how to apply it: reverse-match each one against the variable index first (node {skillRoot}/scripts/resolve-color.js 'rgb(...)' --context fill) and bind the matched variable (see §2e). Hardcode the RGB only when the match is "none".
Dev server is required — don't silently skip this step.
See Step 1g of plugins/figma-from-code/skills/figma-from-code/7-build-component/step-1-analyze.md for the full decision tree: orchestrator-dispatched vs standalone vs auto/non-interactive, how to derive selectors, and when escalation to the user is required.
Step 2: Build the Screen in Figma
2a. Create the screen frame
const screensFrame = figma.getNodeById('{screensFrameId}');
screensFrame.layoutWrap = 'WRAP';
screensFrame.counterAxisSpacing = 80;
const screen = figma.createFrame();
screen.name = '{screenName}';
screen.resize(1440, 900);
screen.layoutMode = '{VERTICAL or HORIZONTAL}';
screen.primaryAxisSizingMode = 'FIXED';
screen.counterAxisSizingMode = 'FIXED';
screen.itemSpacing = { gapValue };
screen.paddingTop = { pt };
screen.paddingBottom = { pb };
screen.paddingLeft = { pl };
screen.paddingRight = { pr };
screen.fills = [{ type: 'SOLID', color: { ...pageBackground } }];
screen.clipsContent = true;
screensFrame.appendChild(screen);
return JSON.stringify({ name: screen.name, id: screen.id });
2b. Add component instances
For each component identified in Step 1b:
const comp = figma.getNodeById(builtComponents['{ComponentName}']);
let master = comp;
if (comp.type === 'COMPONENT_SET') {
const targetProps = { Variant: 'primary', Size: 'regular' };
master =
comp.children.find((child) =>
Object.entries(targetProps).every(
([k, v]) => child.variantProperties?.[k]?.toLowerCase() === v.toLowerCase()
)
) ?? comp.children[0];
}
const instance = master.createInstance();
if (callSiteHasWFull) instance.layoutSizingHorizontal = 'FILL';
if (callSiteHasFlex1) instance.layoutSizingHorizontal = 'FILL';
if (callSiteHasHFull) instance.layoutSizingVertical = 'FILL';
parent.appendChild(instance);
2c. Add region frames for nested layout
When the page source nests multiple components inside a layout container (e.g., a sidebar + main content row), create a region frame:
const row = figma.createFrame();
row.layoutMode = 'HORIZONTAL';
row.primaryAxisSizingMode = 'FIXED';
row.counterAxisSizingMode = 'FIXED';
row.layoutSizingHorizontal = 'FILL';
row.layoutSizingVertical = 'FILL';
row.itemSpacing = 0;
row.fills = [];
screen.appendChild(row);
2d. Tailwind-to-Figma mapping
Read {skillRoot}/7-build-component/figma-utils.md for the canonical fixSizing() definition and the Tailwind→Figma mapping table.
2e. Resolving page background colors
When the page root uses semantic colors (bg-background, bg-muted), resolve and bind the Figma variable — page chrome colors are exactly the colors that should stay coupled to tokens. The reverse-match-before-hardcode rule from 7-build-component/step-2-build.md §2e Step 0 applies to screens too:
- Resolve the class or the computed RGB from
computed-styles.json (Step 1f) via the lookup CLI:
node {skillRoot}/scripts/resolve-color.js 'bg-background' --context fill
node {skillRoot}/scripts/resolve-color.js 'rgb(255, 255, 255)' --context fill
- On
match: "exact" or "tolerance", set the fill then bind: screen.setBoundVariable('fills', 0, await figma.variables.getVariableByIdAsync('{id}')).
- Only on
match: "none", hardcode the RGB from computed-styles.json.
(If tailwindConfigPath is null in state, class-based lookups still work — the CLI falls back to stripping known prefixes and matching CSS variable names directly.)
Step 3: Screenshot the Figma Result
get_screenshot(fileKey, screenFrameId)
Save to {screenshotDir}/figma.png:
curl -sL "{image_url}" -o "{screenshotDir}/figma.png"
Step 4: Compare Against App Screenshot
4-pre. Structural match check (run BEFORE sizing or pixel checks)
Before running any automated comparison, visually inspect both app.png and figma.png side by side. Check whether the two images show the same content structure:
- Do both show the same regions filled with the same type of content (list, detail, empty state)?
- Does one show an empty state ("No case selected") while the other shows a populated detail view?
- Are there overlays, dialogs, or menus visible in the Figma screenshot that don't appear in the app screenshot?
- Are there entire sections present in one but missing in the other?
If the two images show fundamentally different content — not just styling differences, but different components or states entirely — flag as structural_mismatch. This indicates the composition in Step 1b included wrong conditional branches and the screen must be rebuilt, not patched.
A structural_mismatch overrides any pixel score. Do not accept a high match percentage as valid when the content is visibly different — structural similarity in shared chrome (headers, sidebars) can inflate pixel scores even when the main content area is completely wrong.
"comparison": {
"structuralCheck": {
"verdict": "pass" | "structural_mismatch",
"issues": ["Figma shows case detail view but app shows empty state 'No case selected'"]
}
}
If structural_mismatch: enter Step 5 fix loop, but the fix is to re-run Step 1b with stricter screenshot-informed filtering, not to patch colors or spacing.
4a. Sizing sanity check (run BEFORE the pixel compare)
A pixel diff against app.png can pass even when the screen is built much smaller than 1440x900 — most commonly when the outermost frame collapsed to hug content. Run this check first; it is independent of the screenshot.
Inspect the built screen (use_figma) and read its top-level frame:
const node = figma.getNodeById('{screenNodeId}');
const built = {
w: Math.round(node.width),
h: Math.round(node.height),
primaryAxisSizingMode: node.primaryAxisSizingMode,
counterAxisSizingMode: node.counterAxisSizingMode,
layoutMode: node.layoutMode,
};
Compare against the expected screen body size (default 1440x900, or screenBodySize from state):
| Check | Pass criteria | Flag if … |
|---|
| Width | built.w === expectedW ± 2px | Off by more than 2px |
| Height | built.h === expectedH ± 2px | Off by more than 2px |
| Sizing modes | primaryAxisSizingMode === 'FIXED' AND counterAxisSizingMode === 'FIXED' | Either is 'AUTO' |
| Layout mode | Set ('VERTICAL' or 'HORIZONTAL') | 'NONE' — children won't auto-layout |
| Top-level region count | Matches the page source structure (e.g., 2 for nav + body, 3 for header + sidebar row + footer) | Region count differs from source |
| Fill children | Any child whose call-site has flex-1 / w-full has layoutSizingHorizontal='FILL' | Source says fill, built says hug |
If any check fails, treat this as a size_mismatch discrepancy and feed it into Step 5 alongside (or before) the pixel diff results. Do not declare a match based on pixel score alone if the sizing check failed — pixel match against a too-small app.png is a false positive.
Record the sizing check result in the eventual result file:
"comparison": {
"sizingCheck": {
"verdict": "pass" | "fail",
"issues": ["counterAxisSizingMode='AUTO' (expected FIXED)", "built height 412 (expected 900)"],
"builtSize": {"w": 1440, "h": 412},
"expectedSize": {"w": 1440, "h": 900}
},
"matchPct": 95.26,
...
}
4b. Pixel diff comparison
Run the pixel diff comparison:
node {skillRoot}/scripts/compare.js \
"{screenshotDir}/app.png" \
"{screenshotDir}/figma.png" \
"{screenshotDir}/"
This produces:
diff.png — red pixels mark differences, matching pixels dimmed
comparison.json — { matchPct, borderMatchPct, verdict, borderVerdict }
Verdict thresholds (combined with Step 4a result):
- 4a passed AND
matchPct >= 88% AND borderMatchPct >= 80% → match (done)
- 4a failed (regardless of pixel score) → size_mismatch (needs fixing — fix sizing first, then re-screenshot, then re-run 4a + 4b)
- 4a passed AND
matchPct 72-88% or borderMatchPct < 80% → minor_diff (needs fixing)
- 4a passed AND
matchPct < 72% → mismatch (needs fixing)
Screen thresholds are slightly more lenient than component thresholds because screens contain many instances whose internal pixels are already validated at the component level — a small per-instance drift compounds across the page.
A passing pixel verdict alone is NOT enough — 4a must also pass. Otherwise the build is silently wrong-sized and the validation phase will reject it later.
If no app screenshot exists (appScreenshot is null), skip pixel comparison — but still run Step 4a. Report no_app_reference only if 4a also passes; otherwise report size_mismatch.
Step 5: Fix Loop (Up to 3 Iterations)
If the verdict is minor_diff, mismatch, or size_mismatch, enter the fix loop.
Per-iteration process
5a. Diagnose the discrepancy
Use all five inputs together to identify specific differences:
- Step 4a sizing check result — if it failed, address sizing FIRST. A wrong-sized screen will mask everything else and will re-fail validation later.
- Read
diff.png — red regions show exactly where pixels differ
- Read
app.png — what the screen should look like
- Read
figma.png — what was actually built
- Read page source
.tsx — Tailwind classes reveal intended values
Cross-reference to identify the exact Figma properties that need correction. Common screen-level discrepancy patterns:
| Symptom | Likely Cause | Fix |
|---|
| 4a failed (frame collapsed to hug) | Outermost frame has *SizingMode='AUTO' | node.primaryAxisSizingMode='FIXED'; node.counterAxisSizingMode='FIXED'; node.resizeWithoutConstraints(1440, 900). Order matters — modes before resize. |
| 4a failed (region didn't fill width) | Child region missing layoutSizingHorizontal='FILL' | Set child.layoutSizingHorizontal='FILL' (and Vertical if appropriate) |
| Sidebar / header in wrong position | Layout direction wrong, or x/y manually set inside auto-layout | Set screen.layoutMode correctly; remove any manual x/y assignments |
| Component instance shows wrong variant | Targeted the wrong variant during Step 2b | instance.setProperties({ Variant: 'secondary' }) or recreate from correct master |
| Whole page shifted by ~24px | Wrong padding on the screen frame | Adjust paddingTop/Bottom/Left/Right |
| Background color wrong | Wrong fill on the screen frame | Adjust screen.fills — prefer computed-styles.json resolved RGB |
| Two components touching where source has gap | Wrong itemSpacing on the parent region | Set region.itemSpacing to match source gap-* class |
| Missing region (e.g., footer absent) | Region frame not created during Step 2c | Add the missing region with its children |
| Component appears tiny in the corner | Instance added before auto-layout was set, or appended to wrong parent | Re-parent the instance; verify screen.layoutMode is set before appending children |
| Screen positioned at wrong x/y inside Screens frame | Manual x/y set despite screensFrame.layoutWrap='WRAP' | Remove x/y assignments — let the wrap layout position it |
5b. Apply the fix via use_figma
Write a targeted fix — change only the properties identified in diagnosis:
const node = figma.getNodeById('{nodeId}');
node.primaryAxisSizingMode = 'FIXED';
node.counterAxisSizingMode = 'FIXED';
node.resizeWithoutConstraints(1440, 900);
fixSizing(node, { exemptRoot: true });
return 'fixed';
5c. Re-screenshot and re-compare
get_screenshot(fileKey, screenFrameId)
Save to {screenshotDir}/figma.png (overwrite previous).
node {skillRoot}/scripts/compare.js \
"{screenshotDir}/app.png" \
"{screenshotDir}/figma.png" \
"{screenshotDir}/"
5d. Evaluate and continue or stop
- If verdict is now
match → exit loop, report as fixed
- If verdict improved but still
minor_diff, mismatch, or size_mismatch → continue to next iteration
- If iteration count reaches 3 → exit loop, report remaining issues
Structural audit (run before first comparison if issues suspected)
function auditScreen(node) {
const issues = [];
if (node.layoutMode === 'NONE') {
issues.push({ type: 'no_layout_mode', node: node.id, name: node.name });
}
if (node.primaryAxisSizingMode === 'AUTO' || node.counterAxisSizingMode === 'AUTO') {
issues.push({ type: 'screen_not_fixed', node: node.id });
}
for (const child of node.children) {
if (child.x !== 0 && child.parent?.layoutMode && child.parent.layoutMode !== 'NONE') {
issues.push({ type: 'manual_xy_in_autolayout', node: child.id });
}
}
return issues;
}
const node = figma.getNodeById('{screenNodeId}');
return JSON.stringify(auditScreen(node));
Fix structural issues before screenshotting — manual x/y inside an auto-layout parent causes silent layout drift, and missing layoutMode collapses all children to (0,0).
Step 6: Write figma-screen.json tracking file
Write a tracking record to the page's source folder so the codebase has a durable link back to the Figma screen node.
Path: Resolve from pageSourceFile. If the page is {pagesRoot}/CasesPage.tsx, write to {pagesRoot}/CasesPage.figma-screen.json. If pages live in a folder ({pagesRoot}/CasesPage/index.tsx), write to {pagesRoot}/CasesPage/figma-screen.json.
Schema:
{
"fileKey": "{figmaFileKey}",
"nodeId": "{screenFrameId}",
"url": "https://figma.com/design/{fileKey}?node-id={nodeIdWithDashes}",
"screenName": "CasesPage",
"route": "/cases",
"createdAt": "2026-05-15T14:32:00Z",
"updatedAt": "2026-05-15T14:32:00Z"
}
Read-then-write semantics:
- If
figma-screen.json already exists at the target path: parse it, preserve the existing createdAt, and refresh nodeId, url, updatedAt (and screenName/route if they changed) with current values.
- If it does not exist: write a fresh file with
createdAt and updatedAt both set to the current ISO 8601 UTC timestamp.
Failure handling: if the write fails (permission, missing parent path that can't be created), log the failure and continue — do not fail the build. Surface the failure in the Step 7 return result under a trackingFile field with { written: false, error: "..." } so the orchestrator can report it.
Step 7: Return Result
Return a structured result for the caller:
{
"screenName": "CasesPage",
"nodeId": "600:1",
"route": "/cases",
"comparison": {
"matchPct": 92.4,
"borderMatchPct": 86.0,
"verdict": "match",
"iterations": 1,
"fixes": ["counterAxisSizingMode AUTO -> FIXED, resize to 1440x900"],
"sizingCheck": {
"verdict": "pass",
"builtSize": { "w": 1440, "h": 900 },
"expectedSize": { "w": 1440, "h": 900 }
}
},
"figmaScreenshot": ".temp/figma-from-code/screenshots/screens/CasesPage/figma.png",
"trackingFile": {
"written": true,
"path": "{pagesRoot}/CasesPage.figma-screen.json"
}
}
If no app screenshot was available:
{
"screenName": "EmptyStatePage",
"nodeId": "600:9",
"route": "/empty",
"comparison": {
"verdict": "no_app_reference",
"matchPct": null,
"iterations": 0,
"sizingCheck": {
"verdict": "pass",
"builtSize": { "w": 1440, "h": 900 },
"expectedSize": { "w": 1440, "h": 900 }
}
}
}
If rejected for missing components:
{
"screenName": "CasesPage",
"status": "rejected",
"reason": "missing_components",
"missingComponents": ["CaseDetails"]
}
Aggregate output
Across all screens, write .temp/figma-from-code/build-screens.json:
{
"screens": [
{
"name": "CasesPage",
"nodeId": "600:1",
"verdict": "match",
"matchPct": 92.4,
"iterations": 1
}
],
"failed": [],
"rejected": []
}
fixSizing() — for descendants, not the screen root
Read {skillRoot}/7-build-component/figma-utils.md for the canonical fixSizing() definition and the Tailwind→Figma mapping table.
The screen frame itself must stay FIXED on both axes (screenBodySize, default 1440x900). Descendant frames may need fixSizing() to release height locks introduced by resize() calls. Call it with { exemptRoot: true } to preserve the root frame's FIXED sizing while releasing descendants.
Call fixSizing(screen, { exemptRoot: true }) after composition — the root stays at FIXED screenBodySize (default 1440x900) while descendants are released to grow with content.
Common Pitfalls
| Pitfall | Prevention |
|---|
| Screen frame collapses to hug content | Set primaryAxisSizingMode='FIXED' and counterAxisSizingMode='FIXED' BEFORE resize(1440, 900) |
| Children stacked at (0,0) | screen.layoutMode not set — children need an auto-layout parent to position |
| Screen positioned at hardcoded x/y inside Screens frame | Use screensFrame.layoutWrap='WRAP' + appendChild; never set x/y |
| Wrong component variant rendered | Resolve COMPONENT_SET to the specific variant matching source props before createInstance() |
| Sidebar appears as a thin strip | Forgot layoutSizingVertical='FILL' on the sidebar instance |
| Page background missing | Set screen.fills to the resolved page-root background, or [] if transparent |
| Manual padding inside an auto-layout child | Use parent itemSpacing for gaps, child padding* for insets — never manual x offsets |
fixSizing() collapsed the screen to hug | Always pass { exemptRoot: true } when calling fixSizing on the screen frame |
| Modifying a pre-existing screen without authorization | Check preExistingScreens in Step 1e before building |
| Wrong-text inside component instance | Don't override component instance text from the screen — the component master owns its text |
| Screen shows non-default conditional branch | Study appScreenshot in Step 1-pre; exclude components behind ternaries/guards that aren't visible |
| Portal overlay (Dialog, Sheet, etc.) visible on screen | Portals are never visible by default — exclude unless appScreenshot explicitly shows them open |
| High pixel match but fundamentally different content | Run Step 4-pre structural check; don't trust pixel score when content structure differs |
Error Handling
| Scenario | Action |
|---|
Component missing from builtComponents | Reject the entire build — return status: "rejected" with the missing components list (Step 0) |
Icon missing from builtComponents | Reject — return status: "rejected" with the missing icon in missingIcons (Step 0) |
use_figma fails | Diagnose error, fix script, retry once. If it fails again, return screen as failed |
use_figma incremental limit | Split the build across multiple use_figma calls. Create the screen frame and regions first, then append instances in follow-up calls |
get_screenshot fails | Retry once. If still failing, return screen as built but unvalidated |
compare.js fails | Report comparison error, return the screen with nodeId but no match score |
| App screenshot missing | Build from source code alone, run Step 4a sizing check, report no_app_reference if 4a passes |
| Pre-existing screen targeted | Return status: "needs_authorization" with preExistingTouched — do not modify |
| Dev server unavailable for Step 1f | Ask the user (standalone) or flag liveInspection: "skipped_no_dev_server" (auto mode) |
Never fail silently. Every error or skip must appear in the returned result.