Imported from d2c-ai/d2c (
skills/d2c-build-flow/SKILL.md). Install upstream withnpx skills add d2c-ai/d2c --skill d2c-build-flow. Copyright stays with the author.
Figma to Design — Build Flow
You are a flow-aware code generator. You take a natural-language prompt listing ordered Figma frames and produce a connected, production-ready flow: per-page components, a shared layout shell, routes wired up, optional shared state, and a navigation smoke test. You reuse the existing /d2c-build machinery for per-page IR + codegen + pixel-diff and add only a thin flow layer on top.
This skill ships a parallel pipeline to /d2c-build. It does not replace it. When the user hands you a single Figma URL, redirect them to /d2c-build.
Non-negotiables
These rules hold across every phase of this skill. No exceptions.
- Design tokens MUST be loaded before any decision. Read
.claude/d2c/design-tokens.json. If it is missing, unreadable, or hasd2c_schema_version < 1, STOP AND ASK the user to run/d2c-init(or/d2c-init --forceif outdated). - NEVER use a library outside
preferred_libraries.<category>.selected. The user explicitly chose which library to use for each capability. NEVER substitute an installed-but-not-selected library. If the design requires a capability not covered bypreferred_libraries, STOP AND ASK. - NEVER hardcode color, spacing, typography, shadow, or radius values. Every visual value MUST reference a design token from
design-tokens.json. No raw hex, no magic numbers, no exceptions. - MUST reuse existing components when an existing component can serve the need. Check the
componentsarray indesign-tokens.jsonbefore creating anything new. If an existing component can do the job, MUST use it. - MUST follow project conventions when
confidence > 0.6andvalue ≠ "mixed". Project conventions (declaration style, export style, type definitions, import ordering, file naming, CSS wrapper, barrel exports, props pattern) override framework defaults. - NEVER re-decide a locked component or token. Read
decisions.lock.jsonfrom the IR run directory at the start of every phase after Phase 2. Only nodes withstatus: "failed"may have their component choice or token mapping changed. If a locked decision must change, STOP AND ASK.
When any rule is ambiguous, STOP AND ASK — do not guess.
Flow-specific rules (in addition to the Non-negotiables)
- The parsed step list is authoritative. The user's prompt (step numbers, URLs, per-step routes) wins over any Figma prototype metadata. Prototype edges only feed shell detection and the
F-FLOW-PROTOTYPE-CONTRADICTS-ORDERwarning. - Flow IR freezes in Phase 2a; page IR freezes per page in Phase 2. Neither may re-decide the other without user input. Phase 3 reads both IRs as frozen; if a fix would require changing the flow graph, STOP AND ASK.
- Never auto-generate Next buttons that weren't drawn in Figma. If no interactive component in a page carries a
link_target, emit a TODO comment and flag the edge asinferred: truein the report. Do not invent chrome to make the pixel-diff or the nav test "pass." - The report must echo the parsed step list. Users verify intent by diffing "what I wrote" against "what you understood."
- One argument controls flow shape:
mode:. It has four values —auto(default),routes,stepper,hybrid. When the user omitsmode:, Phase 2a auto-detects from Figma per the 5-signal procedure in §Phase 2a step 3a and logs the chosen mode + per-signal reasons. Explicitmode:always wins. Auto-detection confidence below 0.55 aborts Phase 2a with a structured error listing signal scores — never silently picks a wrong shape. - Shared components are immutable. Before modifying any file listed in
design-tokens.components[], read itsimport_count(the usage-count signal emitted by/d2c-init).import_count ≤ 1→ the component is local to this flow and MAY be mutated to satisfy a Figma frame.import_count > 1(or the flow-levelshared_component_thresholdoverride) → the component is a shared dependency and is STRICTLY IMMUTABLE — no prop additions, no JSX edits, no style edits. Enforced in Phase 3 §"Component mutation boundary" and §5b F1-Flow. Violating a shared component fires F-FLOW-SHARED-COMPONENT-MUTATION. - Composition over modification. When a shared component (rule 12) needs a per-step visual variation, create a step-scoped wrapper under
app/<route>/_components/<Step><Original>.tsx(or the framework equivalent) that imports the original and applies local CSS / layout around it. NEVER append step-conditional props (isStep2,hideAvatarOnCheckout,variant="checkout") to the shared component's prop signature — that's prop soup. Attempting to add a per-step boolean / enum to an immutable component fires F-FLOW-WRAPPER-PROP-SOUP. - Blast-radius reverification. A flow-emitted file (shared layout, state context, orchestrator, shared wrapper created under rule 13) that was touched after a prior host already passed pixel-diff MUST trigger a re-diff of every earlier passing host that depends on that file. If the re-diff drops any earlier host below
--threshold, HALT the auto-fix loop (no further rounds on the current host) and fire F-FLOW-BLAST-RADIUS-REGRESSION so the developer arbitrates — never enter a break-and-fix oscillation where step 3 keeps breaking step 1. Enforced in §Phase 4a.5.
Arguments
Parse $ARGUMENTS for optional flags (in addition to the flow prompt):
--threshold <number>(default: 95) — per-page pixel-diff threshold; forwarded to each page's/d2c-buildPhase 4. Clamped to[50, 100].--max-rounds <number>(default: 4) — per-page max auto-fix rounds; forwarded to each page's Phase 4. Clamped to[1, 10].--shared-component-threshold <number>(default: 2) —import_countcutoff at which a component entry indesign-tokens.components[]flips from mutable to immutable for this flow (rule 12). Any entry withimport_count >= thresholdis treated as shared. Clamped to[2, 50]. Lowering to2is the safe default; raising it is only useful in monorepos where an internal design system sits in the samedesign-tokens.jsonand every token-layer component naturally hasimport_count ≥ 3.--yes— skip the Phase 1 confirm-or-edit gate; proceed immediately once parsing succeeds. Use for scripted/CI runs.
Unknown flags are ignored with a one-line warning.
Pre-flight Check
Before anything else:
- Confirm
.claude/d2c/design-tokens.jsonexists. If not, trigger F-FLOW-TOKENS-MISSING. - Read the prompt (
$ARGUMENTSafter the slash command plus any message body the user provided). - Do NOT start any phase until the prompt has been parsed (Phase 1).
Phase 1 — Prompt Parsing
Goal: turn the user's natural-language prompt into a deterministic, validated list of steps with resolved routes.
Tooling
Use the parser at skills/d2c-build-flow/scripts/parse-flow-prompt.js. Invoke it from Bash:
node skills/d2c-build-flow/scripts/parse-flow-prompt.js <prompt-file>
Or call the exported parseFlowPrompt(text) function directly from another Node script. The parser is pure and deterministic; it does not touch Figma, the filesystem, or the network.
Invocation grammar
The parser recognises two canonical forms. Both are legal; teach users Form A in examples unless they need to route outside a common parent.
Form A — base route, derived per-step routes
/d2c-build-flow
In these following pages we need to build the following flow, this is the route /onboarding
These are the steps:
Step 1: <figma-frame-url>
Step 2: <figma-frame-url>
Step 3: <figma-frame-url>
Every step without an explicit route: is resolved to <base_route>/step-<N> (e.g. /onboarding/step-1).
Form B — explicit per-step routes
/d2c-build-flow
In these following pages we need to build the following flow.
These are the steps:
Step 1: <figma-frame-url> route: /signup
Step 2: <figma-frame-url> route: /signup/verify
Step 3: <figma-frame-url> route: /signup/complete
Mixed form (base route plus some explicit per-step routes) is legal. Explicit routes that leave the base_route subtree trigger F-FLOW-ROUTE-ESCAPES-BASE as a warning.
Form C — auto-discover from an entry frame
/d2c-build-flow
Build the onboarding flow, this is the route /onboarding
Step: <figma-frame-url>
Exactly one Step: line (no number). The parser returns auto_discovered: true and a single-entry steps[]. Phase 2a then BFS-walks Figma's prototype connections starting at entry_node_id to enumerate the rest of the flow; each discovered edge lands in flow-graph.edges[] with inferred: false and a real source_component_node_id. Routes are derived as <base_route>/step-<N> in BFS order; if no base_route is given, the model asks for one before freezing the graph.
Mixing Form C with Form A/B (or listing multiple Step: lines) → F-FLOW-PARSE-AMBIGUOUS.
Mode directive (any form) — mode: auto | routes | stepper | hybrid
Declare the flow shape in the preamble (typically trailing the route line):
this is the route /onboarding, mode: stepper
Values:
auto(default when omitted) — Phase 2a evaluates the 5-signal mode-detection procedure (§Phase 2a step 3a) against the Figma frames to pickroutes/stepper/hybrid.routes— every step is its own URL (today's behaviour). ForbidsStepper groupblocks.stepper— all steps share one URL and swap in place. If noStepper groupblocks are present, Phase 2a wraps the entiresteps[]into a single implicit group named after the flow.hybrid— one or more explicitStepper groupblocks mixed with bareStep:lines. Requires at least one block.
Unknown values → F-FLOW-MODE-UNKNOWN with the allowed set shown.
Stepper group blocks (mode: stepper or hybrid)
Stepper group "intake" at /signup:
Step 1: <figma-frame-url> title: "Name"
Step 2: <figma-frame-url> title: "Email" validate: form
- Header:
Stepper group "<name>" at <route>:— quoted name (single or double), followed by the single route the group is mounted at. - Steps under the header are group-internal: their
step_numbermust be 1-based contiguous within the group and their URLs are rendered as swappable bodies sharing the group's route. - Per-step directives legal in groups:
title:(stepper label),optional: true|false(Skip button),validate: none|form(Next gate),state:(shared form fields). - Groups are closed when a line de-indents to ≤ header indent or a new group starts. Empty groups (<2 steps) → F-FLOW-STEPPER-GROUP-EMPTY.
- Two groups with the same name → F-FLOW-STEPPER-GROUP-DUP.
A full hybrid prompt:
/d2c-build-flow
Build the signup, this is the route /signup, mode: hybrid
Stepper group "intake" at /signup:
Step 1: <url> title: "Name"
Step 2: <url> title: "Email"
Step 1: <url> route: /signup/verify
Step 2: <url> route: /signup/welcome
Grammar rules (also documented in references/failure-modes.md)
- Preamble: any free text before the first candidate step line. Capture
base_routeas the first match of/\broute\s+(\/\S+)/iin the preamble; strip trailing punctuation. - Step candidate: any line matching
^\s*Step\s+\d+\b.*$(case-insensitive). - Strict step grammar:
^\s*Step\s+(\d+)\s*[:\-]\s*(\S+)(?:\s+route:\s*(\/\S+))?\s*$— URL must be one whitespace-free token. - Minimum: 2 step lines. Fewer → F-FLOW-TOO-FEW-STEPS.
- Step numbers: contiguous
[1, 2, …, N]. Gaps or duplicates → F-FLOW-STEP-GAP. - URL validation: must contain
?node-id=or&node-id=. Bare file URLs → F-FLOW-FILE-URL (offer a frame pick-list viaget_metadata). - Cross-file flows (B-FLOW-CROSS-FILE). Steps may come from different Figma files; the parser extracts the file key from each URL (
/design/<key>/…or/file/<key>/…) and setscross_file: truewhen more than one unique key appears. Phase 2a MUST fetch metadata per uniquefile_keyand the Phase 6 report MUST echo the full dependency list so the user knows which files gate the flow. - Route resolution: explicit per-step
route:wins; else<base_route>/step-<N>; else F-FLOW-NO-ROUTE (ask for a base route once and apply to all unrouted steps). - Optional
state:directive (per step, after URL and optionalroute:, order-independent):state: <name>:<type>[, <name>:<type> …]. Declares which fields this page writes into shared state. Types MUST bestring | number | boolean— anything else fires F-FLOW-STATE-TYPE-UNSUPPORTED. Example:Step 1: <url> state: email:string, age:number. - Optional
mobile:directive (per step, B-FLOW-MOBILE-VARIANT):mobile: <figma-frame-url>pairs a mobile viewport with the desktop frame. Phase 4 verifies both viewports; Phase 3 emits responsive CSS (tokens-aware) rather than two components. The mobile URL must also carry anode-id— bare file URLs fire F-FLOW-FILE-URL. Example:Step 1: <desktop-url> mobile: <mobile-url>. - Anything else → F-FLOW-PARSE-AMBIGUOUS with the failing line quoted and a canonical example in the prompt.
Output of Phase 1
{
ok,
failures[],
base_route,
flow_name,
auto_discovered,
mode: "auto" | "routes" | "stepper" | "hybrid",
mode_source: "explicit" | "default",
steps: [{ step_number, figma_url, node_id, route, route_source }],
stepper_groups: [{ name, raw_name, route, header_line, validation_enabled, steps: [...] }]
}
auto_discovered === true signals Phase 2a to run Figma-prototype BFS instead of trusting the user's step list for page enumeration. In Form A/B the flag is false and the flow is exactly as the user listed.
mode carries the user's declared flow shape; when "auto", Phase 2a's mode-detection procedure (§step 3a) resolves it to one of the three terminal values before writing flow-graph.json. stepper_groups[] holds any explicit Stepper group blocks; additional groups may be synthesised in Phase 2a (either by wrapping all steps when mode: stepper is declared with no blocks, or by the partitioning step in mode: auto).
node_id is extracted from the URL's node-id query parameter and normalised to colon form (e.g. 1-2 → 1:2) to match Figma's MCP node id format.
Batched error reporting
When multiple Phase-1 failures fire together (common when a user mistypes), present them in a single grouped STOP AND ASK message per the meta-rule in failure-modes.md. Do not iterate one by one.
When F-FLOW-FILE-URL fires (step URL missing ?node-id=), follow the Runtime procedure in failure-modes.md: call mcp__figma__get_metadata on the file, then render the response as a numbered pick-list. Use this format:
The URL "<failing-url>" is a Figma file URL — I need a frame URL.
Pick a frame from <file-name>:
1. <Frame Name 1> (node-id=1-2, 1440×900)
2. <Frame Name 2> (node-id=3-4, 390×844)
...
Reply with the number, or paste a corrected URL.
Show only top-level FRAME nodes. Sort by Figma's documentationLinks first (if present), then by document order. Truncate to 50 entries with ... and N more (paste a URL to pick from outside this list) when the file has more frames.
Pass criteria
Phase 1 passes when ok === true (only warning-severity failures allowed). Warnings are surfaced to the user but do not block.
Confirm-or-edit gate
Before entering Phase 2a, echo the parsed step list and wait for confirmation. This catches grammar misunderstandings before any Figma fetch runs.
Format:
Parsed steps:
Step 1 → /onboarding/step-1 → https://www.figma.com/design/abc/Flow?node-id=1-2
Step 2 → /onboarding/step-2 → https://www.figma.com/design/abc/Flow?node-id=3-4
Step 3 → /onboarding/step-3 → https://www.figma.com/design/abc/Flow?node-id=5-6
Proceed? [y = proceed / e = edit prompt / n = abort]
Rules:
- Skip this gate when
--yesis present in$ARGUMENTS. y→ continue to Phase 2a.e→ wait for an updated prompt, re-run Phase 1 from the top.n→ stop the flow with "aborted at confirm gate" and exit cleanly.- Any other input → re-show the prompt unchanged.
Phase 1.5 — Flow-level Intake
Goal: ask the standard /d2c-build intake questions ONCE upfront and bundle the answers into flow_intake so every per-page/per-step /d2c-build dispatch can read them. This closes the gap where running /d2c-build standalone asks 6 questions but /d2c-build-flow silently fell back to the structured-input defaults at /d2c-build/SKILL.md §1.0. Also adds a flow-only Q7 that collects all mobile Figma URLs upfront in one pass.
Runs after the Phase 1 confirm-or-edit gate, before Phase 1b. Skipped entirely when --yes is present in $ARGUMENTS (in which case the standard /d2c-build defaults apply per-dispatch and flow_intake is omitted from flow-graph.json).
1.5a — Flow Complexity Classification
For each declared step (every entry in the parsed step list, including stepper-group steps), call mcp__Figma__get_metadata to fetch the node tree only — no images, no full design context. Count descendant layers per step using the same rules as standalone /d2c-build §1.2a (FRAME, INSTANCE, COMPONENT, COMPONENT_SET, TEXT, RECTANGLE, ELLIPSE, LINE, VECTOR, GROUP, BOOLEAN_OPERATION, STAR, REGULAR_POLYGON, excluding the root).
Resolve the dominant flow complexity:
- Simple flow — every step is Simple-classified (Figma node name matches a Simple keyword AND ≤20 layers). Rare for flows; usually a misuse (a flow of icons or chips). Skip Q4 (viewports) + Q6 (API) + Q7 (mobile).
- Medium flow — highest step is Medium (Medium keyword + ≤50 layers) and no step is Complex. AND the flow has fewer than 4 pages AND no
shared_state[]declaration AND novalidate: formon any stepper step. Skip Q6 only. - Complex flow — any step is Complex, OR the flow has 4+ pages, OR
shared_state[]was declared, OR any stepper step carriesvalidate: form. Ask all questions.
Surface to the user before asking:
"Classified as Complex flow (5 pages, max 78 layers). Asking all questions including mobile."
or
"Classified as Medium flow (2 pages, max 35 layers). Skipping API question — defaulting to no API."
Persist the classification on flow_intake.complexity and the list of skipped questions on flow_intake.skipped_questions[] so reruns can echo the same rationale.
1.5b — Ask Intake Questions
Ask the applicable questions in a single message, applying the skip rules from §1.5a. The questions mirror standalone /d2c-build §1.2b with the following flow-level deltas:
-
What is this? (skipped by default for flows — defaults to
pagesince every step is route-bound) — Only ask when the user's prompt explicitly mixes section-level frames into the flow (e.g. a step labelled "the header section of /dashboard"). Default =page. -
Where should it live? — Skip. Routes are already declared in the prompt and resolved by Phase 2a.
-
Functional or visual-only? — Always ask. Drives shared_state inference, form-validation generation in stepper Next handlers, and API plumbing across pages.
-
Viewports? (skipped on Simple flows — defaults to
desktop-only) —desktop-onlyormultiple. Note: do NOT ask the user for per-step Figma URLs here — Q7 below collects them in one pass. -
Components to reuse? — Always ask. Free-text answer; "use what makes sense" is the most common response.
-
Does this design connect to any APIs? (skipped on Simple flows — defaults to
no) — Same follow-up structure as standalone/d2c-buildQ6 (number of calls, then per-call name + sample schema). Stored inflow_intake.api_calls[]. -
Mobile designs? (NEW — skipped on Simple flows) — "Do you have mobile Figma designs for this flow? (yes / no)". If
yes: prompt the user to paste one mobile Figma URL per declared step in order as a numbered list:"Paste the mobile Figma URL for each step, one per line, in declared order:
- /onboarding/step-1:
- /onboarding/step-2:
- /onboarding/step-3: "
The user may write
skipon any line — that step ships desktop-only. Validate every supplied URL carries a?node-id=segment (else fire F-FLOW-FILE-URL with the offending URL quoted). Validate each URL points to a real Figma frame (the samemcp__Figma__get_metadatacheck used on desktop URLs); a 404 fires F-FLOW-MOBILE-FRAME-MISSING with the step number quoted.
Wait for answers before proceeding. Do not assume defaults beyond the auto-fills declared by the complexity classifier.
1.5c — Bundle Answers into flow_intake
Write the gathered answers to flow_intake on the in-memory run state, then persist on flow-graph.json (Phase 2a). Schema at skills/d2c-build-flow/schemas/flow-graph.schema.json#/definitions/flow_intake. Required fields:
what,mode,viewports,components_to_reuse,has_api_calls,mobile,complexity,skipped_questions.api_calls[]only whenhas_api_calls === "yes".mobile.urls_by_step_indexis a sparse object keyed by 0-based step index across pages[] + stepper_groups[].steps[] in declared order. Empty object whenmobile.enabled === falseor every step wasskip-ed.
1.5d — Propagate to Per-Page /d2c-build Dispatches
Every Phase 2 (per-page IR + per-variant), Phase 3 (per-step body codegen — see §"Stepper groups" delegation in Phase 3), and Phase 4 (per-variant pixel-diff) dispatch into /d2c-build MUST include the flow_intake answers in the structured-input payload, replacing the hardcoded defaults at /d2c-build/SKILL.md §1.0. Mapping:
flow_intake field |
structured-input payload field |
|---|---|
what |
what |
mode |
mode |
viewports |
viewports |
components_to_reuse |
components_to_reuse |
has_api_calls |
has_api_calls |
api_calls[] |
passed through as a top-level api_calls array (parser already accepts it) |
The mobile URLs are NOT propagated as payload fields — instead, Phase 2a attaches mobile_variant: { figma_url, node_id, file_key } directly to each matching page / stepper_step IR (the existing mobile_variant field). The flow's existing mobile_variant codegen path (framework-react-next.md §"Mobile variants") then handles dual-viewport pixel-diff and responsive emission without any further payload plumbing.
When --yes is present and flow_intake is omitted, every dispatch falls back to the existing structured-input defaults — preserving today's silent behaviour for scripted invocations.
Failure modes
- F-FLOW-INTAKE-METADATA-FAILED (stop-and-ask) —
mcp__Figma__get_metadatafailed for one or more steps during §1.5a. Show the failing step numbers and ask whether to (a) retry, (b) skip classification and treat the flow as Complex (ask all questions), or (c) abort. - F-FLOW-MOBILE-FRAME-MISSING (stop-and-ask) — a mobile URL supplied in §1.5b Q7 returned 404 from
mcp__Figma__get_metadata. Show the step number and URL; ask the user to re-supply orskipthat step. - F-FLOW-MOBILE-COUNT-MISMATCH (stop-and-ask) — the user pasted a different number of mobile URLs than declared steps (excluding
skiplines). Show both counts and the parsed list; ask the user to re-supply.
Phase 1b — State variant extraction
Goal: from the raw prompt text, recognise (figma_url, state_keyword, trigger, step_ref) quadruples so Phase 2a can attach state_variants blocks to the correct pages / stepper steps. This phase is prose-based, not grammar-based — the skill does not extend the Phase 1 parser. Instead it instructs the executing model to read the prompt and produce a deterministic extraction table.
When the user's prompt mentions only primary frames (no loading/empty/error language), this phase emits an empty extraction and every page keeps its pre-state-variants shape. Identity guarantee: a loaded-only prompt produces a loaded-only IR, byte-identical to the pre-state-variants pipeline.
Canonical state-keyword vocabulary
Map the user's phrasing to one of five canonical keywords. Matching is case-insensitive, whole-phrase-preferred; fall back to the longest matching substring.
| Canonical | Recognised phrasings |
|---|---|
loaded |
loaded, normal, default, populated, happy path, data view, primary, main |
loading |
loading, skeleton, fetching, pending, placeholder, shimmer, spinner view |
empty |
empty, no data, zero state, null state, blank state, nothing to show |
error |
error, failed, failure, broken, crashed, fallback, something went wrong |
initial |
initial, idle, pre-fetch, pristine, untouched, not started, before action, before search, waiting for input |
A frame mentioned without any state keyword is treated as loaded by default — this is the identity case. loaded is always inferred when absent, so the user never has to spell it out.
The initial slot captures the pre-fetch / pre-action render — the moment a user lands on the page before any data request has been initiated or any user input has been given (e.g. a search page before the query is typed, a checkout step before Pay is clicked). It is distinct from loading (fetch in flight) and from empty (fetch completed, zero results). It is NOT a trigger-carrying state — the "when" is structural, not contextual.
Extraction algorithm
Apply these rules in order:
-
Segment the prompt by host. A "host" is either a route (e.g.
/dashboard) or a step reference (e.g.step 2inside a stepper group). Walk the prompt top-to-bottom; every sentence or list item belongs to the host most recently introduced. The identifying phrase for a host may be/route,"<route>", orstep N. -
Within each host segment, collect (state_keyword, figma_url) pairs by proximity. For each Figma URL in the segment, scan backwards within the same clause for a state keyword. Stop at sentence boundaries. If no keyword is found in the clause, the URL maps to
loadedby default. -
Trigger capture. For every
loadinganderrorpair, scan the same clause (and the following sentence if needed) for trigger phrasing — "while ", "on ", "when ", "during ", "if ". Store the trigger verbatim. If no trigger is found in the local context, mark the trigger asMISSINGand defer to the clarification phase. Theloaded,empty, andinitialslots skip trigger capture — their "when" is structural (identity / zero-length data / pre-fetch). -
Error stub detection. An error mention with no Figma URL emits a stub entry:
{ stub: true, trigger: <captured or MISSING> }. Only theerrorslot may be a stub —loaded/loading/empty/initialwithout a URL are parse failures.Recognised phrasings (non-exhaustive; match case-insensitively, treat
—/-/:as separators):- "error state but no design yet" / "no design for error yet" / "error design TBD"
- "error: TBD" / "error: placeholder" / "error: WIP"
- "error state exists (placeholder for now)" / "error state exists — placeholder"
- "has an error state" / "we also need an error state" — when no URL appears within the same sentence or the immediately following one
- "error handled separately" / "error lives elsewhere" — when no URL is attached
Trigger capture still applies to stub entries. The stub MUST carry a trigger describing when the error fires; if no trigger phrasing is in the local context, mark the trigger
MISSINGand ask in the clarification phase (the phrasing "no design yet" does not itself count as a trigger).When a stub is emitted, note it in the confirmation table with
stub: truein the row so the user sees the contract explicitly before Phase 3 emits the dashed placeholder. -
Collision rules. If the same host ends up with two URLs claiming the same state (e.g. two different
loadingURLs for/dashboard), abort with a clear message listing both URLs and the host. The extractor MUST NOT silently pick one. If two different hosts share the same URL for the same state, allow it — frames legitimately get reused. -
Mode inference. After all hosts are extracted, infer the flow mode from the extraction shape:
- Only bare route hosts →
routes. step Nreferences inside a declared stepper group →stepper.- Both →
hybrid. - A user-declared
mode:directive from Phase 1 always wins — the inferred mode is only used when the parser left it asauto.
- Only bare route hosts →
-
Form C rejection. If Phase 1 returned
auto_discovered === trueand the extraction produced any state_variants, abort with F-FLOW-VARIANTS-FORMC-UNSUPPORTED (deferred to P3.1). Do not continue.
Extraction output
Emit a normalised table the user sees in the confirmation gate (Phase 2a step 2d) and that Phase 2a step 2 uses to populate state_variants[]:
{host, step_ref?, state, figma_url?, trigger, stub?}
host— route string (e.g./dashboard) or<group-name> step N.step_ref— 1-based step index inside the stepper group; null for bare routes.state— one ofempty | error | initial | loaded | loading.figma_url— the supplied URL; absent for error stubs.trigger— captured trigger text, or the literal stringMISSING(clarified later). Required forloadinganderror; ignored forloaded,empty, andinitial.stub—trueonly on error entries with no URL.
Rows are serialised alphabetically by state within each host, for diff stability (empty, error, initial, loaded, loading).
Examples the skill must handle
Mixed routes-mode prompt (all triggers inline):
Build /dashboard — the normal view is https://figma.com/.../a,
loading skeleton is https://figma.com/.../b while fetching the user's data,
error view is https://figma.com/.../c if the fetch returns 5xx.
Also /settings from https://figma.com/.../d.
Yields rows for /dashboard (loaded, loading, error — all with triggers) and /settings (loaded only).
Stepper-mode prompt:
Three-step onboarding at /onboarding.
Step 1: loaded .../a, loading .../b while validating email.
Step 2: loaded .../c, loading .../d on password submit, error .../e on password mismatch.
Step 3: loaded .../f.
Yields step 1 (loaded + loading), step 2 (loaded + loading + error), step 3 (loaded only).
Trigger missing → deferred to clarification:
Loading state for /dashboard is https://figma.com/.../b
Yields one row {host: "/dashboard", state: "loading", figma_url: ".../b", trigger: "MISSING"}. The clarification phase will ask "When does the loading state show for /dashboard?".
Error stub declaration:
/dashboard: loaded is https://figma.com/.../a, plus an error state
(no design yet) for when the fetch fails.
Yields /dashboard loaded (with URL) plus error as {stub: true, trigger: "when the fetch fails"}.
Initial-state declaration (pre-fetch render):
/search — initial view is https://figma.com/.../a (before the user types),
loaded is https://figma.com/.../b, loading .../c while querying Algolia,
empty .../d when no results.
Yields /search with four rows in alphabetical order: empty (with URL, no trigger), initial (with URL, no trigger), loaded (with URL, no trigger), loading (with URL, trigger "while querying Algolia"). The initial row never enters trigger clarification.
Hybrid-mode prompt (standalone route + stepper group, both with full variant coverage):
/dashboard with loaded https://figma.com/.../a,
loading https://figma.com/.../b while fetching dashboard data,
empty https://figma.com/.../c when the user has zero items,
and error https://figma.com/.../d if the fetch returns 5xx.
Plus a multi-step /checkout.
Step 1: loaded https://figma.com/.../e,
loading https://figma.com/.../f while confirming cart totals,
empty https://figma.com/.../g when the cart is empty,
error https://figma.com/.../h on payment provider failure.
Step 2: loaded https://figma.com/.../i.
Yields two hosts: /dashboard (route, 4 rows — empty, error, loaded, loading with triggers where required) and /checkout (stepper group, step 1 with 4 rows + step 2 with loaded only). Mode inference reads step N + bare route → hybrid. Phase 2a step 6a attaches the /dashboard block to pages[dashboard].state_variants and the /checkout step 1 block to stepper_groups[checkout].steps[0].state_variants — the two hosts never share slots and never cross-contaminate.
Failure modes
- F-FLOW-VARIANTS-FORMC-UNSUPPORTED — state variants declared alongside
Step: <entry-url>(Form C). MVP scope; deferred to P3.1. STOP AND ASK the user to either (a) drop the state variant language and let the flow ship as loaded-only, or (b) switch to explicitStep N:form. - F-FLOW-VARIANTS-COLLISION — two URLs for the same
(host, state)pair. STOP AND ASK, showing both URLs. - F-FLOW-VARIANTS-ORPHAN-URL — a URL was mentioned with a state keyword but no recognisable host. Likely the prompt lacks a route / step anchor. STOP AND ASK with the failing sentence quoted.
- F-FLOW-VARIANTS-STUB-NON-ERROR — the user declared a state without a URL for
loaded,loading,empty, orinitial(onlyerrormay be a stub). STOP AND ASK.
Fallback: sibling-name detection
When prompt extraction finds a loaded URL for a host but no URL for one or more of loading / empty / error / initial, opportunistically scan the primary frame's Figma parent for siblings whose names match the canonical vocabulary (Dashboard — Loaded / Dashboard — Skeleton / Dashboard — Empty State / Dashboard — Error / Dashboard — Idle). This is a secondary path — it never overrides an explicit URL from the prompt.
When to fire:
- Phase 1b extraction produced a
loadedentry for the host, AND - One or more of
loading/empty/error/initialis absent from the host's extracted rows, AND - The user did NOT pass
mode: no-fallback(a per-flow opt-out directive).
How to fire:
- Identify the primary frame's
parent_node_id. This is NOT in the Phase 2a fixture (which carries onlytop_level_children), so a live Figma call is required:mcp__Figma__get_design_context(parent_node_id). The response must carry achildren[]array where each entry has at minimum{node_id, name}. - Walk
children[]and classify each sibling against the canonical state vocabulary below. For each sibling whose name matches a state keyword (case-insensitive, whitespace-and-punctuation-tolerant — soHome — Empty State,home_empty_state, andHomeEmptyStateall classify the same way):- Pick the longest matching keyword phrase when multiple match (so
"empty state"beats"empty"). - Whole-word matches outrank substring matches (so
"loading"matchesHome — Loadingbut notHome — Reloading). - Skip siblings whose
file_keydiffers from the primary's (cross-file rejection — BFS-over-one-parent always shares the file key, so a mismatch is structural). - Build a result map
found[slot] = { node_id, figma_url, file_key }per matched sibling. When two siblings match the same slot, the LATER one wins and the earlier becomes acollisionentry — record both for §step 5 below. - Build
unmatched_siblings[] = [{node_id, name, candidate_slot?}]for siblings whose names contained a state-like word but didn't fully match (e.g."Skeleton View"partially matchesloadingkeywords;candidate_slot: "loading"so the user can confirm).
- Pick the longest matching keyword phrase when multiple match (so
- Merge
result.found[slot]into the host's IRstate_variants[slot]ONLY for slots that were absent from prompt extraction. Prompt-derived entries always win. - Surface
result.unmatched_siblings[]in the §"Clarification phase" (Phase 2a step 2d): "I also saw these sibling frames but couldn't classify them — tell me which, if any, belong to a state variant: ". Collision entries (two siblings matching the same slot) are shown with both URLs so the user picks one. - Stage audit warnings (P2.4). After the clarification phase resolves, record the leftover ambiguities on
flow_graph._pending_audit_warnings[]so Phase 4 can persist them intoaudit.json.warnings[](see §"Warnings surface (P2.4)"):- For every entry in
result.unmatched_siblings[]the user did NOT attach to a slot, push{ kind: "fallback_unmatched_sibling", route: "<host route>", node_id: "<sibling node_id>", recommendation: "Rename the Figma frame to match a state keyword (e.g. 'Skeleton', 'Empty State', 'Error') or attach it explicitly in the prompt, then re-run /d2c-build-flow.", details: { name: "<sibling name>", candidate_slot: "<detector's best-guess slot, if any>" } }. Omitslotbecause the siblings were NOT assigned. - For every collision entry (a sibling whose detected slot was already filled by another sibling or by prompt extraction), push
{ kind: "fallback_collision", route: "<host route>", slot: "<colliding slot>", node_id: "<losing sibling node_id>", recommendation: "Two frames match the <slot> slot for <route> — disambiguate by renaming one (or removing it from the parent section) before the next run.", details: { name: "<losing sibling name>", other_node_id: "<winning node_id>" } }. These entries drain intoaudit.json.warnings[]during Phase 4's audit-seeding pass (see §"Warnings surface" rule 2)._pending_audit_warningsis in-memory only and never serialised intoflow-graph.json; when Phase 4 is skipped (rare — e.g.--plan-only), Phase 4 itself stages the list to the sidecar<run-dir>/flow/pending-audit-warnings.jsonso the next Phase 4 run can drain it.
- For every entry in
What the detector matches (mirrors Phase 1b vocabulary — loaded keywords are NOT matched because the primary is always the loaded frame):
loading: loading, skeleton, fetching, pending, placeholder, shimmer, spinnerempty: empty, no data, zero state, null state, blank state, nothing to showerror: error, failed, failure, broken, crashed, fallback, something went wronginitial: initial, idle, pre-fetch, pristine, untouched, not started
Whole-word matches outrank substring matches; longest phrase wins when multiple fire. Case-insensitive, whitespace- and punctuation-tolerant (so Home — Empty State and home_empty_state both classify the same way).
Cross-file rejection: siblings whose file_key differs from the primary's are dropped silently — BFS-over-one-parent always shares the primary's file key, so a mismatch is a structural error rather than a naming ambiguity.
Limitations:
- The detector does NOT guess triggers — every slot it populates still needs a trigger when required (only
loadinganderror), so §"Clarification phase" still asks "When does<state>show for<host>?" for every newly-populatedloading/errorrow.emptyandinitialrows are trigger-free and never enter clarification. - False positives are possible — "PendingTasksCard" would substring-match the
loading→pendingkeyword, and "InitialPageTitle" could substring-match theinitialslot. The confirmation gate (Phase 2a step 2d) surfaces every detected entry for explicit approval before dispatch, so false positives are user-visible and correctable, not silent.
Phase 2a — Flow Planning
Goal: produce a validated, frozen flow-graph.json from the parsed step list plus Figma metadata.
Run directory
Create .claude/d2c/runs/<YYYY-MM-DDTHHMMSS>/flow/ containing flow-graph.json. Individual per-page runs land at .claude/d2c/runs/<ts>/pages/<node_id>/ (Phase 2 per page).
Steps
-
Inherit
framework,meta_framework,conventions,components,preferred_libraries,apifromdesign-tokens.json. Phase 2a builds the flow graph for any framework — it is codegen (Phase 3) that branches. Supported framework/meta_framework pairs for Phase 3:react+next(App Router),react+nextwith Pages Router,vue+nuxt,svelte+sveltekit,angular+angular,solid+solidstart,astro+astro. Other pairs still produce a validflow-graph.jsonduring Phase 2a but abort at Phase 3 preconditions with a clear message. -
Enumerate pages. Two modes, picked by
auto_discovered:Mode 1 — declared steps (
auto_discovered === false, Form A/B): iterate the user'ssteps[]in order. For each step, call Figma MCP (in parallel when possible):get_design_context(nodeId)→ metadata + screenshot.- Capture the frame
titlefor the report and for future IR layers. - Inspect prototype interactions for overlay / conditional actions:
- Overlay triggers → add to
not_supported_detected[]withkind: "overlay"and fire F-FLOW-OVERLAY-AS-PAGE. - Conditional actions →
kind: "conditional", fire F-FLOW-CONDITIONAL.
- Overlay triggers → add to
Mode 2 — auto-discover (
auto_discovered === true, Form C): the parser handed us a single entry frame. BFS the prototype graph starting atentry_node_idto enumerate every downstream page:- Call
get_design_context(entry_node_id)first; readprototype.connections[](the outgoing prototype edges). If empty → fire F-FLOW-DISCOVERY-EMPTY. - Enqueue each connection's destination node id; repeat
get_design_contextper visited frame (parallelise where the MCP allows). De-duplicate onnode_id. - If a connection closes a cycle (revisits a frame already visited on the current branch) → fire F-FLOW-DISCOVERY-CYCLE (inform), record the back-edge in
not_supported_detected[]withkind: "loop", and skip that edge. - After BFS terminates, check whether the file's prototype metadata lists additional entry frames reachable only from elsewhere. When multiple disconnected subtrees exist → fire F-FLOW-DISCOVERY-DISCONNECTED (stop-and-ask).
- Assign
pages[]in BFS order (entry first). Assign routes:- If
base_routeis set →route = <base_route>/step-<N>where N is the 1-based BFS index. - If
base_routeis null → STOP AND ASK the user for one before continuing (reuse the F-FLOW-NO-ROUTE wording).
- If
- Emit edges from the BFS tree: each non-back-edge becomes one
edges[]entry withinferred: false,source_component_node_idset to the prototype connection's source component node id, andtriggerderived from the connection (ON_CLICK→"onClick", etc.). - Same overlay / conditional checks apply per visited frame.
-
Shell detection. Compare top-level children across every page's design context. Use the same component-match scoring pass that Phase 2 uses for candidate identification. A shared shell is identified when ≥ 75% of pages contain the same top-level component instance. Otherwise fire F-FLOW-SHELL-DIVERGENT (inform; fall back to no layouts).
- When identified, add a single
layouts[]entry with PascalCasename(derived from the shared component's Figma name, e.g.OnboardingShell),figma_node_idpointing at the shared component, andapplies_tolisting every page that contains it. - Procedure: for each page, list its
top_level_childrenfromget_design_context. Build a per-component-instance frequency map keyed oncomponentId(ormainComponentIdfor instances). The component(s) with frequency ≥ ⌈0.75 × page_count⌉ are shared shells; those below are page-specific. When the highest frequency is below the threshold, setlayouts: []anddivergent: true(the signal to fireF-FLOW-SHELL-DIVERGENT). - Stepper-indicator sub-detection. Within the identified shared shell, scan each top-level child's
descendants[]for a repeated component instance whosevariant(orproperties.step/index/current) differs per page in an ordered way. When found, attach astepper_indicatorobject to the layout entry capturingcomponent_id,node_ids_per_page,variants_per_page, andordered: true|false(true when the variants form a monotonic sequence likestep=1, step=2, step=3). This feeds mode detection and stepper codegen.
- When identified, add a single
3a. Mode resolution. When Phase 1 returned mode !== "auto", carry the declared value through to flow-graph.mode and set mode_source = "explicit". When mode === "auto", run the 5-signal mode-detection procedure below to resolve to one of routes / stepper / hybrid.
Inputs you'll need:
pages[]— each entry carriesnode_id,figma_url,frame_sizefromget_design_context, optionaldifferential_region(derived by subtracting the shared-shell bbox from the frame bbox and reporting the leftover area ratio + bbox), andprototype_edges[]from the prototype metadata.shell_result— the §step 3 output, re-evaluated at the stricter threshold of 0.9 for mode detection (stepper coverage bar is higher than the layout-detection default).explicit_groups— anyStepper groupblocks already parsed.
The 5 signals — score each in [0, 1] independently. The final mode_confidence is a weighted sum (weights below); detected_mode is the leading classification each signal points at, picked by majority weighted vote.
| # | Signal | What it measures | Score formula | Stepper-leaning when |
|---|---|---|---|---|
| 1 | frame_size_uniformity |
All step frames have nearly identical width × height | 1 - stddev(sizes) / mean(sizes) clamped to [0,1] |
uniform sizes (>0.95) |
| 2 | shared_shell_coverage |
Fraction of pages containing the shell at threshold 0.9 | pages_with_shell / total_pages |
≥0.9 |
| 3 | stepper_indicator_instance |
Layout has a stepper_indicator with ordered: true and 1 variant per page |
1.0 if present and ordered, else 0.0 |
present + ordered |
| 4 | differential_region_geometry |
The leftover (non-shell) area is the same bbox across pages | iou(bbox_i, bbox_j) averaged across all pairs |
≥0.85 |
| 5 | prototype_semantics |
Prototype connections form a linear chain from one frame to the next, all with the same trigger | chain_length / (pages-1) (1.0 when fully chained) |
≥0.9 |
Weights: signals 2 and 3 weight 0.30 each (shell coverage + stepper indicator are the strongest tells); signals 1, 4, 5 weight 0.13–0.14 each. Sum to mode_confidence ∈ [0, 1].
Mode pick:
mode_confidence ≥ 0.55AND signals 2+3 both lean stepper →detected_mode = "stepper".mode_confidence ≥ 0.55AND only some pages lean stepper (partition) →detected_mode = "hybrid"with the partitioned runs.mode_confidence ≥ 0.55AND no stepper indicators →detected_mode = "routes".mode_confidence < 0.55→aborted = true. Fire F-FLOW-MODE-UNDECIDABLE and STOP AND ASK the user to passmode:explicitly. Include the per-signal scores verbatim.
Persist on the IR: mode, mode_source: "auto", mode_confidence, mode_detection_reasons[] (one entry per signal: {signal, score, weight, contributed}).
Bands for handling:
band: "silent"(mode_confidence ≥ 0.80): write the result into the IR, log a single-line notice, proceed.band: "advisory"(0.55 ≤ mode_confidence < 0.80): write the result, print a prominent warning with the signal breakdown, proceed.band: "abort"(mode_confidence < 0.55): as above — fire F-FLOW-MODE-UNDECIDABLE.
When mode resolves to stepper with no explicit Stepper group blocks, synthesise a single implicit group named after the flow (PascalCase of flow_name) containing all top-level steps. When mode is hybrid, keep explicit groups as-is; when the detector returns additional stepper runs beyond what the user declared, merge them into stepper_groups[] with detected_mode_run: true for observability.
Finally, for every stepper group (explicit or detected), insert a single virtual pages[] entry with page_type: "stepper_group", node_id: "stepper:<hash>", route equal to the group's route, stepper_group_ref equal to the group's name, and drop the group-internal steps from pages[] — those steps live only in stepper_groups[*].steps[].
-
Edges. Behaviour depends on
auto_discovered, branch suffixes, and stepper groups:- Linear declared steps (
auto_discovered === false, no branches, no stepper groups): emit one linear edge per consecutive pair of pages:from_node_id = pages[i].node_id,to_node_id = pages[i+1].node_id.trigger = "onClick",source_component_node_id = null,inferred = true,condition = null.- v1 does not populate
source_component_node_idin this mode — identifying the Next button is deferred to the per-page Phase 2 (where it lands oncomponent-match.link_targetinstead).
- Stepper-group internal edges (mode: stepper or hybrid): do NOT add entries to
flow-graph.edges[]. Inside a stepper group the "next step" action is represented at the page level viastepper_groups[i].steps[]order, and at the button level vialink_target.edge_kind = "step_delta"set by Phase 2b's link-target prose (§Phase 2b step 3). The stepper-group virtual page may still have outgoing edges into the next top-level page (route-mode exit), and those ARE recorded inedges[]as normal. - Branching declared steps (at least one
Step Na:/Step Nb:in the prompt, B-FLOW-MULTI-BRANCH): for each pair of consecutive unique step numbers, emit the edge(s) into the next group's pages:- A group of size 1 → one edge to the next group's only page (linear).
- A group of size 2+ → N edges, one per sibling, all
from_node_idsharing the previous page'snode_id. - For every outgoing branch, identify the Figma component that triggers it (typically the button whose prototype connection targets the branch's entry frame). Populate
source_component_node_idwith its node id and setinferred = false. If any branch can't be wired to an identifiable component → fire F-FLOW-BRANCH-UNWIRED. The validator refuses to freeze a branching graph with nullsource_component_node_idon any outgoing edge. - After a branch group, subsequent linear steps re-merge — emit one edge from each branch's last page to the merge target.
- Mode 2 (auto-discover): edges are emitted directly from the Figma prototype connections visited during BFS. Every edge MUST have
inferred = falseand a non-nullsource_component_node_id; the validator enforces this invariant wheneverauto_discovered === true. Trigger is taken from the prototype connection type. Prototype-discovered branching is fully supported here — a page with multiple outgoing prototype connections becomes a multi-branch page automatically.
- Linear declared steps (
-
Shared state.
- From
state:directives: when any parsed step carried astate:directive, carry the field list onto that page aspages[].state_writes. Then auto-create a singleshared_state[]entry named<flow_name>Data(camelCase, e.g.onboardingData) whosepages[]is the set of nodes that appear as writers or readers and whosepersistencedefaults to"memory"(user can request"session"or"local"during the confirm-or-edit gate). The field types feed the generated TypeScript interface. - Persistence override. When the user asks for
"local"persistence, accept an optionalttl_seconds(positive integer). Write both intoshared_state[i]. When the user asks for"local"without a TTL, setttl_seconds: null— the provider keeps the data untilreset()is called. - No
state:directives, no user ask: leaveshared_state: []. The skill does not auto-infer from Figma in v1. - User asked for shared state (e.g. "this flow carries user data across steps") but no form elements exist across pages → fire F-FLOW-MISSING-STATE.
- From
-
Prototype vs declared order. If prototype metadata exists and implies an order that differs from the declared steps, fire F-FLOW-PROTOTYPE-CONTRADICTS-ORDER (inform only; user's list wins).
6a. Attach state variants. Consume the extraction table from Phase 1b and populate state_variants on the corresponding pages / stepper steps. Rules:
- Pair every extraction row to its host by (route) for routes-mode rows, or by (stepper_groups[].name, step_ref) for stepper/hybrid rows. An unmatched row → fire F-FLOW-VARIANTS-UNMATCHED-HOST and STOP AND ASK with the row quoted.
- For the
loadedslot: reuse the host's ownnode_id+figma_url(do not re-parse — identity with the host is enforced by the validator). - For
loading,empty,error: parsefile_key,node_idfrom the supplied URL (same extraction as step 2's URL parsing). Carrytriggerverbatim. - For error stubs: emit
{ stub: true, trigger }and leavenode_id/figma_urlunset (validator enforces the mutex). - If a host ends up with only a
loadedrow, omit thestate_variantskey entirely from that page/step. This keeps loaded-only flows byte-identical with the pre-state-variants IR (identity gate, P0.8). - Serialize each
state_variantsobject with keys in alphabetical order (empty,error,loaded,loading) — P2.3 hardens this via the validator's normaliser; step 6a relies on the producer emitting them in order in MVP.
6b. Project convention detection. When at least one page/step carries a state_variants block, scan the project root for the three conventions Phase 3 needs to know about. Skip this step entirely when no host declared state_variants — loaded-only flows do not need convention data and the validator forbids the block in that case.
component_type — "server" | "client" | "mixed":
- Count files under
app/**andsrc/app/**that contain'use client'at the top vs files without it. - Mostly-server (≥80% no
'use client') →"server". Mostly-client (≥80% with'use client') →"client". Otherwise →"mixed".
error_boundary.kind — "next-file-convention" | "react-error-boundary" | "custom-class" | "none" plus optional import_path:
- Glob for
app/**/error.tsxorapp/**/error.jsx(orsrc/app/**/error.{tsx,jsx}). If any exist →{kind: "next-file-convention", import_path: null}. - Else grep
package.json.dependenciesforreact-error-boundary. If present →{kind: "react-error-boundary", import_path: "react-error-boundary"}. - Else grep src files for a class component extending a name like
*ErrorBoundary*. If present →{kind: "custom-class", import_path: "<resolved import path>"}. - Else →
{kind: "none", import_path: null}.
data_fetching.kind — "server-component-fetch" | "react-query" | "swr" | "custom-hook" | "none" plus optional example_import:
- Grep
package.json.dependencies:@tanstack/react-query→{kind: "react-query", example_import: "@tanstack/react-query"}.swr→{kind: "swr", example_import: "swr"}. - Else grep src files for
async functionserver components that callfetch(. If common (≥3 hits) →{kind: "server-component-fetch", example_import: null}. - Else grep for repeated
useFetch/useApi/useGet*patterns. If found →{kind: "custom-hook", example_import: "<resolved import>"}. - Else →
{kind: "none", example_import: null}.
Write the resolved block verbatim into flow_graph.project_conventions.
6c. Clarification phase. After extraction + convention detection, resolve the unknowns:
- For every extraction row with
trigger === "MISSING", ask: "When does the<state>state show for<host>? (e.g. during initial data fetch, during form submission, on a specific action, other.)" — one question per missing trigger, serialised top-to-bottom. Write the user's answer back into the row'striggerfield. - When
project_conventions.component_type === "mixed"AND the user prompt did not specify'use client'preference, ask: "The project mixes Server and Client Components. Which should the generated pages be? (a) Server Components (async, data fetched on the server), (b) Client Components ('use client', data fetched in hooks)." — normalise the answer toserverorclientand overwritecomponent_type. - When
project_conventions.error_boundary.kind === "none"AND at least one page declares a non-stuberrorvariant, ask: "No error boundary was detected in the project. Options: (a) addreact-error-boundaryas a dependency and wire it in, (b) use the Next.jserror.tsxfile convention, (c) skip error-boundary wiring and render the error variant unconditionally at the data branch." — overwriteerror_boundarywith the user's choice (react-error-boundary→ install on first generation;next-file-convention→ rely on file system;none→ keep but record the user opted out so Phase 3 doesn't add imports). - When
project_conventions.data_fetching.kind === "none"AND at least one page declaresloadingorerror, ask: "No data-fetching library was detected. Options: (a) plainfetchinside async Server Components (default for Next.js), (b)@tanstack/react-query, (c)swr, (d) use a project-specific hook (paste the import)." — overwritedata_fetchingwith the chosen kind + example_import. - Confirmation table. Print the final resolved plan and STOP AND ASK
y = proceed / e = edit prompt / n = abort. Skip when--yesis in$ARGUMENTS.
Format:
State variants:
/dashboard loaded https://figma.com/.../a —
/dashboard loading https://figma.com/.../b while fetching user dashboard data
/dashboard error https://figma.com/.../c when the fetch returns 5xx
/settings loaded https://figma.com/.../d —
Project conventions (detected):
component_type: server
error_boundary: next-file-convention
data_fetching: server-component-fetch
Proceed? [y / e / n]
Rules:
y→ continue to step 7.e→ return control to the user for prompt edits; on resume, re-run Phase 1 + 1b + 2a from the top.n→ stop cleanly with "aborted at variant-confirm gate".--yesshort-circuits toybut still prints the table for audit.
6d. Attach mobile variants from Phase 1.5. When flow_intake.mobile.enabled === true, walk the flow_intake.mobile.urls_by_step_index map and attach each entry to the matching host's mobile_variant:
- The 0-based step index keys this map. Resolve the index to a host by walking the declared step order: every entry in
pages[](filtered topage_type === "page") followed by every step in eachstepper_groups[*].steps[]in declared order. Index 0 is the first declared step, regardless of whether it's a route page or a stepper step. - For each
(index, mobile_url)pair, parsenode_idandfile_keyfrom the URL (same extraction as step 2's URL parsing). Constructmobile_variant: { figma_url: mobile_url, node_id, file_key }. - Write the block onto the matching host (
pages[i].mobile_variantfor route pages and overlays;stepper_groups[g].steps[s].mobile_variantfor stepper steps). The schema acceptsmobile_varianton both shapes. - Skip indices are absent from the map (the user wrote
skipon that line in §1.5b Q7). Hosts at those indices ship desktop-only — nomobile_variantwritten. - When
flow_intakeis absent (--yeswas passed) ORflow_intake.mobile.enabled === false, this step is a no-op. Pre-existing per-stepmobile:directives from Phase 1 (the inl
Truncated - read the full file at https://github.com/d2c-ai/d2c/blob/43e3f64d10c52459457a15141a83d7cf0da7f28d/skills/d2c-build-flow/SKILL.md.