generate-uds-figma-component

star 1

UDS Component Factory. Drafts a token-bound UDS component set directly inside the UDS Components Figma file on a brand-new `๐ŸŸ  <id> {Cursor}{Ignore}` page. Use when the user says "generate a UDS component for X", "factory me an Avatar", "draft a new UDS component called Y", "build a UDS component for Z in Figma", or "use the component factory to start <Title>". Stops at Figma โ€” never writes to `uds-docs/uds/`. Docs landing is the existing `uds-updated` skill, run later by the designer.

sbajwa32 By sbajwa32 schedule Updated 6/12/2026

name: generate-uds-figma-component description: UDS Component Factory. Drafts a token-bound UDS component set directly inside the UDS Components Figma file on a brand-new ๐ŸŸ  <id> {Cursor}{Ignore} page. Use when the user says "generate a UDS component for X", "factory me an Avatar", "draft a new UDS component called Y", "build a UDS component for Z in Figma", or "use the component factory to start ". Stops at Figma โ€” never writes to <code>uds-docs/uds/</code>. Docs landing is the existing <code>uds-updated</code> skill, run later by the designer. lastUpdated: 2026-06-25T21:03:47Z</h2> <h2>UDS Component Factory โ€” Generate UDS Figma Component</h2> <p>This skill takes a short component brief and returns a strong, token-bound Figma component set on a brand-new <code>๐ŸŸ  <id> {Cursor}{Ignore}</code> page in the <a href="https://www.figma.com/file/1XJoUJgtNpw4R0IIT3VjoK"><code>UDS Components</code></a> file. The designer remains the design lead and the approval authority; the factory handles the repetitive construction, quality checks, and cleanup.</p> <p>The factory's job ends when the designer accepts the draft. Mainline rename, docs scaffold, status sync, and changelog are NOT in scope โ€” those happen later via the existing <a href="../uds-updated/SKILL.md"><code>uds-updated</code></a> skill, designer-initiated.</p> <h2>Locked decisions</h2> <p>These four decisions govern the factory's behavior. They are the contract; the phases below are the operational implementation.</p> <ol> <li><strong>Marker convention.</strong> The factory creates pages in <code>UDS Components</code> named <code>๐ŸŸ  <id> {Cursor}{Ignore}</code> โ€” orange/in-progress stoplight prefix + the lowercase kebab <code>componentId</code> (matching mainline UDS Components pages like <code>badge</code>, <code>data-table</code>, <code>icon-wrapper</code>), then the <code>{Cursor}</code> designer label + <code>{Ignore}</code> exclusion marker. The <code>{Ignore}</code> does the filtering, so every UDS automation already skips these pages โ€” no rule changes required. For a <strong>family</strong>, <code><id></code> is the family stem and the one page hosts every <code>udc-<stem>...</code> member set (see Inputs <code>familyMembers</code> and <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> ยง8).</li> <li><strong>Existing-page collision.</strong> If a <code><id> {Cursor}{Ignore}</code> page already exists, the factory inspects it to decide whether to resume/extend or rebuild. Because the <code>{Cursor}</code> tag is a standing write grant (decision #5), rebuilding does NOT require fresh permission โ€” but the factory reports what it found and its plan before writing.</li> <li><strong>Stoplight prefix while in review.</strong> The page is born with <code>๐ŸŸ </code> (in-progress). When the designer accepts and moves it to mainline, the rename drops <code>{Cursor}{Ignore}</code> and updates the stoplight โ€” <code>๐ŸŸ  avatar {Cursor}{Ignore}</code> becomes <code>๐ŸŸก avatar</code> (review) or <code>๐ŸŸข avatar</code> (production). The factory never performs that rename. For a <strong>family</strong>, that single page rename accepts every member set at once, and the family shares one status (the page prefix) โ€” a consequence the user accepted when choosing the one-page family model.</li> <li><strong>Write safety.</strong> The factory's only write scope is scope #4 in <a href="../../rules/uds-figma-write-safety.mdc"><code>uds-figma-write-safety.mdc</code></a> โ€” component drafts on a <code>{Cursor}{Ignore}</code> page in <code>UDS Components</code>. Any write to a page without that suffix requires explicit per-target user direction.</li> <li><strong>The <code>{Cursor}</code> tag = standing write grant.</strong> Any page whose name contains <code>{Cursor}</code> is Cursor's to create, modify, rebuild, or delete nodes on โ€” freely, without per-action permission โ€” until the user removes the <code>{Cursor}</code> tag. The factory never removes the <code>{Cursor}</code> / <code>{Ignore}</code> tags itself; that rename is the designer's ownership/acceptance gesture and revokes the grant.</li> </ol> <h2>Mandatory prerequisite skills</h2> <p>You MUST load these BEFORE any <code>use_figma</code> call. Loading order matters:</p> <ol> <li><code>figma-use</code> (load from the active Figma plugin skill path in the available skills list) โ€” Plugin API rules: page-context reset per call, return-pattern, ID return, font preload, color 0โ€“1 range, atomic-failure semantics. Required by every <code>use_figma</code> invocation.</li> <li><code>figma-generate-library</code> (load from the active Figma plugin skill path in the available skills list) โ€” supplies the state ledger (<code>setSharedPluginData('dsb','run_id', RUN_ID)</code>), the sequential-call rule, the library-discovery pattern (<code>get_libraries</code> + <code>search_design_system</code>), the validation pattern, and the Phase 3 component pattern. This factory inherits all of that and only defines UDS-specific deltas on top.</li> </ol> <p>The Figma plugin cache path can move between environments. Prefer the active paths from the available skills list, or the official Figma MCP skill loader by name (<code>figma-use</code>, <code>figma-generate-library</code>) when it is available. Do NOT proceed without both loaded; they are not optional.</p> <h2>Mandatory rules</h2> <p>These auto-attach via globs but are the contract this skill operates under. If any of them is unread in the current session, read them now:</p> <ul> <li><a href="../../rules/uds-figma-preflight.mdc"><code>uds-figma-preflight.mdc</code></a> โ€” read-only discovery first; mandatory preflight output before any Figma read or write; ignore <code>{Ignore}</code> pages.</li> <li><a href="../../rules/uds-figma-write-safety.mdc"><code>uds-figma-write-safety.mdc</code></a> โ€” Figma writes denied unless explicitly scoped. The factory writes ONLY into <strong>scope #4</strong> (component drafts on a page whose name contains <code>{Cursor}{Ignore}</code> in <code>UDS Components</code>). Every write must produce the standard before/after summary.</li> <li><a href="../../rules/uds-token-architecture.mdc"><code>uds-token-architecture.mdc</code></a> โ€” token vocabulary contract. Bind only via library variable keys; do not invent tokens.</li> <li><a href="../../rules/uds-source-of-truth.mdc"><code>uds-source-of-truth.mdc</code></a> โ€” the factory NEVER modifies anything under <code>uds-docs/uds/</code>.</li> <li><a href="../../rules/uds-rule-discipline.mdc"><code>uds-rule-discipline.mdc</code></a> โ€” any edit to this SKILL.md or to the write-safety rule must bump <code>lastUpdated:</code> and re-run <code>bash scripts/regenerate-toolchain.sh</code>.</li> <li><a href="../../rules/uds-master-preflight.mdc"><code>uds-master-preflight.mdc</code></a> Phase 5 โ€” round-trip checklist that applies if a follow-on edit touches anything under <code>uds-docs/</code>.</li> <li><a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> โ€” the design-system-level naming framework. Source of truth for every state name (sections 1, 6), variant axis name (sections 3, 4, 6), size step (section 2), region name (section 5), casing rule (section 7), and component / subcomponent name (sections 8, 9). The factory picks names from this framework rather than inheriting whatever the closest sibling happens to use.</li> <li><a href="../../rules/uds-component-checklist.mdc"><code>uds-component-checklist.mdc</code></a> โ€” the completeness rulebook. Defines the component class taxonomy and, per class, which states and which API surface (events, parts, slots) are REQUIRED vs. <code>notApplicable</code>. The factory uses this to decide what "complete" means for the component being drafted โ€” it is the spine of the Phase A model, not an afterthought. A draft that skips a class-required state or event is incomplete.</li> </ul> <h2>Inputs</h2> <p>Ask the user (via <code>AskQuestion</code>) for any of these that aren't clear from context:</p> <table> <thead> <tr> <th>Input</th> <th>Required</th> <th>Notes</th> </tr> </thead> <tbody><tr> <td><code>componentTitle</code></td> <td>Yes</td> <td>Title Case (e.g. <code>Avatar</code>, <code>Banner</code>, <code>Stepper</code>). The human-facing title for <code>spec.json</code> / docs โ€” NOT used for the Figma page or set name (those use <code>componentId</code>).</td> </tr> <tr> <td><code>componentId</code></td> <td>Yes</td> <td>kebab-case (e.g. <code>avatar</code>). Used for the Figma page name (<code>๐ŸŸ  <id> {Cursor}{Ignore}</code>, matching mainline UDS Components pages) and the component-set node name (<code>udc-<id></code>). Must NOT collide with an existing entry in <a href="../../../uds-docs/uds/components.json"><code>uds-docs/uds/components.json</code></a>. For a <strong>family</strong> (multiple public member sets on one page), this is the family <strong>stem</strong> / docs id; list the other members in <code>familyMembers</code>.</td> </tr> <tr> <td><code>familyMembers</code></td> <td>Optional</td> <td>For a multi-set <strong>family</strong>, the additional public member set ids to build on the same stem page beyond the base โ€” e.g. <code>["data-field-group"]</code> for the <code>data-field</code> family, or <code>["accordion-item", "accordion-group"]</code> when no bare <code>accordion</code> base exists. Each becomes a <code>udc-<member></code> set; all fold into the ONE docs component whose id is <code>componentId</code> (the stem), sharing its folder, status, and changelog. Primary member (the <code>figmaNodeId</code> target) = <code>udc-<componentId></code> if it is built, else the first listed member; the factory states which it chose. Subparts of any member stay <code>_udc-<member>_<sub></code>. See <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> ยง8.</td> </tr> <tr> <td><code>brief</code></td> <td>Yes</td> <td>Short prose describing purpose, when-to-use, when-not-to-use, and any non-obvious constraints. The factory expands this into the full model in Phase A.</td> </tr> <tr> <td><code>siblings</code></td> <td>Optional</td> <td>Up to 3 component IDs to use as anatomy/state/accessibility references. Default: factory picks the closest siblings from <code>components.json</code> based on the brief.</td> </tr> <tr> <td><code>pageBaseline</code></td> <td>Default <code>๐ŸŸ </code></td> <td>Stoplight prefix for the new page. Defaults to <code>๐ŸŸ </code> (in-progress) per locked decision #3 above.</td> </tr> </tbody></table> <p>The component target is always the <code>UDS Components</code> file, file key <code>1XJoUJgtNpw4R0IIT3VjoK</code>. The skill never writes to <code>UDS Tokens</code>.</p> <h2>Pre-flight (do these once at the start of the session)</h2> <ol> <li><strong>Confirm explicit user intent.</strong> Per <a href="../../rules/uds-figma-write-safety.mdc"><code>uds-figma-write-safety.mdc</code></a>, Figma writes are denied unless the user named the target. Confirm the user wants a draft component built for the named title, in the named file, on a <code>{Cursor}{Ignore}</code> page.</li> <li><strong>Run the preflight output block</strong> from <a href="../../rules/uds-figma-preflight.mdc"><code>uds-figma-preflight.mdc</code></a> ยง"Mandatory preflight output": Tokens version, Components version, site <code>UDS_VERSION</code>, mismatch yes/no, capability check pass/partial/fail, action.</li> <li><strong>Verify the UDS Tokens library is subscribed.</strong> Call <code>get_libraries({ fileKey: '1XJoUJgtNpw4R0IIT3VjoK' })</code>. If <code>libraries_added_to_file</code> does not include <code>UDS Tokens</code>, STOP and ask the user to subscribe it via Figma's library picker. Do not attempt to auto-subscribe. Same exit pattern as the <a href="../figma-component-card/SKILL.md"><code>figma-component-card</code></a> pre-flight.</li> <li><strong>Check id collision.</strong> Read <a href="../../../uds-docs/uds/components.json"><code>uds-docs/uds/components.json</code></a> and confirm the proposed <code>componentId</code> is not already taken. If it is, stop and ask whether the user wants to use a different id, sync the existing component instead (different workflow), or override.</li> <li><strong>Check existing-page collision.</strong> List the pages of <code>UDS Components</code> and check whether a page named <code><anyPrefix> <id> {Cursor}{Ignore}</code> already exists. If it does, inspect it and decide whether to resume/extend or rebuild (per locked decisions #2 and #5). The <code>{Cursor}</code> tag means you don't need fresh permission to rebuild โ€” but report what's there and your plan before writing.</li> <li><strong>Locate or create the resume-state file.</strong> State path is <code>.cursor/state/component-factory/<componentId>.md</code>. If it exists, read it and resume from where the prior session left off; do not overwrite the model. If it does not exist, you'll create it during Phase A. (<code>.cursor/state/</code> is gitignored โ€” proposals are runtime state, not committed history.)</li> <li><strong>Read the current factory build version.</strong> Open <a href="../../figma/state/factory-version.json"><code>.cursor/figma/state/factory-version.json</code></a> and hold its <code>version</code> (e.g. <code>2026.06.07.1</code>) and <code>fVersion</code> (the current F#, e.g. <code>12</code>). You stamp the DATE <code>version</code> onto the component set in Phase B (B.3.6) and write the matching <code>Factory-version:</code> line into the contract block (B.3.5) โ€” F# is NOT stamped; it's derived from the date for drift reports (see <a href="../../rules/uds-factory-versioning.mdc"><code>uds-factory-versioning.mdc</code></a> ยง"The F# short key"). This is the factory's vintage, not today's date.</li> <li><strong>If the target component ALREADY EXISTS, surface its factory-version drift BEFORE proposing any change.</strong> Two cases hit this: step 5 found an existing <code><id> {Cursor}{Ignore}</code> draft to resume/extend, OR you're entering maintenance mode on a live (accepted) component. In either, run the <a href="../../agents/figma-component-inspector.md"><code>figma-component-inspector</code></a> ยง3 drift pass and report its ยง"Preflight" drift block FIRST โ€” built F# + vintage, current F# + bar, behind-by-N, and the relevance-filtered list of what changed since that applies to THIS component (with the not-applicable items shown too). This is <strong>report-always</strong>: drift is surfaced every time you touch an existing component, draft or live โ€” not only when the user explicitly asks for an upgrade. Surfacing is automatic; ACTING on it is not โ€” it follows the mode's write rules (free scoped fixes on a <code>{Cursor}</code> draft per scope #4, gated per-change approval on a live component per scope #5). A brand-new component with no existing page skips this step (nothing to be behind).</li> </ol> <h2>Phase A โ€” Brief to model (no Figma writes)</h2> <p>Inputs the skill reads โ€” keep the cumulative payload under 30 KB; if sibling specs are larger, read only the sections you need (<code>anatomy</code>, <code>states</code>, <code>accessibility</code>, <code>props</code>):</p> <ul> <li>The user's <code>brief</code>.</li> <li><a href="../../../uds-docs/uds/components.json"><code>uds-docs/uds/components.json</code></a> โ€” full component list.</li> <li>The 2โ€“3 closest sibling components' <code>spec.json</code> files at <code>uds-docs/uds/components/<id>/spec.json</code>. Schema is <a href="../../../uds-docs/uds/schemas/spec.schema.json"><code>uds-docs/uds/schemas/spec.schema.json</code></a>.</li> <li><a href="../../../uds-docs/uds/tokens/semantic.css"><code>uds-docs/uds/tokens/semantic.css</code></a> โ€” available semantic surfaces, text, borders, and status treatments.</li> <li><a href="../../rules/uds-token-architecture.mdc"><code>uds-token-architecture.mdc</code></a> โ€” token-role contract (which token role binds to which CSS variable family).</li> <li><a href="../../rules/uds-component-checklist.mdc"><code>uds-component-checklist.mdc</code></a> โ€” the component class taxonomy and the per-class required-state / required-API baseline. Read this BEFORE drafting the model; it decides what a complete version of this component must include.</li> <li><a href="../../../uds-docs/uds/components/button/spec.json"><code>uds-docs/uds/components/button/spec.json</code></a> โ€” the bar for a fleshed-out contract (<code>props</code>, <code>events</code>, <code>slots</code>, <code>states</code>, full <code>accessibility</code>, contract-tied <code>acceptanceCriteria</code>). Use it as the completeness reference, not a thing to copy.</li> </ul> <h3>Discovery sweep (run before drafting the model)</h3> <p>Run <code>search_design_system</code> against the UDS Tokens library across <strong>both publishable artifact categories</strong> before writing the Token plan or any binding decision:</p> <ul> <li><strong>Variables</strong> (<code>includeVariables: true</code>) โ€” color, border, space, font, font-scale, etc. These fill the Token plan section.</li> <li><strong>Styles</strong> (<code>includeStyles: true</code>) โ€” covers BOTH Text Styles <em>and</em> Effect Styles, but you have to query each style family explicitly:<ul> <li>Text Styles โ€” <code>uds/text</code>, <code>label</code>, <code>heading</code>, <code>paragraph</code>, etc.</li> <li>Effect Styles โ€” <code>uds/effect</code>, <code>elevation</code>, <code>shadow</code>, <code>depth</code>, <code>blur</code>.</li> </ul> </li> </ul> <p>A typography-keyword sweep alone will NOT return Effect Styles, even though both flow through the same <code>includeStyles</code> flag. If the component's anatomy has any shadow, blur, or surface elevation, an Effect Style query is mandatory; missing it leads to hardcoded <code>node.effects = [...]</code> literals, which is the same anti-pattern as inventing token variables (prohibited by Phase B.3). Effect Styles bind via <code>await node.setEffectStyleIdAsync(style.id)</code>, the same way bundled Text Styles bind via <code>setTextStyleIdAsync</code>.</p> <p>Persist the proposed model to <code>.cursor/state/component-factory/<componentId>.md</code>. Sections:</p> <ul> <li><p><strong>Component class</strong> โ€” classify the component as exactly one of <code>layout</code>, <code>display</code>, <code>action</code>, <code>form</code>, <code>navigation</code>, <code>feedback</code>, or <code>data</code> per <a href="../../rules/uds-component-checklist.mdc"><code>uds-component-checklist.mdc</code></a> ยง"Component class". This is the FIRST decision in the model because it drives the required-state baseline and the required API surface (events, parts) for every section below. Completeness is judged against this class, not picked ad hoc.</p> </li> <li><p><strong>Purpose</strong> โ€” why this component exists.</p> </li> <li><p><strong>When to use</strong> / <strong>When not to use</strong> โ€” paired guidance.</p> </li> <li><p><strong>Anatomy, slots, and parts</strong> โ€” root, label, icon/content regions, helper text, action area, supporting parts. Formalize this into two lists the docs contract needs:</p> <ul> <li><strong>Slots</strong> โ€” every content region a consumer fills, named as a slot contract (<code>default</code> plus named regions like <code>leading</code>, <code>trailing</code>, <code>helper</code>). Maps to <code>spec.json</code> <code>slots[]</code>.</li> <li><strong>CSS parts</strong> โ€” internal regions worth exposing via <code>::part()</code> for external styling (e.g. <code>field</code>, <code>icon</code>, <code>helper</code>). Maps to the Web Component <code>parts</code> surface. If none apply, say so explicitly rather than omitting the list.</li> </ul> </li> <li><p><strong>Subcomponent classification</strong> โ€” for every distinct Figma component set the factory plans to build, declare exactly one of:</p> <ul> <li><strong>Main component</strong> โ€” the design unit a designer reaches for in the library picker. Named <code>udc-<id></code> (no underscore prefix). Visible in the asset picker. When the designer later runs <a href="../uds-updated/SKILL.md"><code>uds-updated</code></a>, gets its own <code>uds-docs/uds/components/<id>/</code> folder with <code>spec.json</code>, <code>status.json</code>, <code>changelog.json</code>, examples, etc., and its own entry in <code>uds-docs/uds/components.json</code>.</li> <li><strong>Subcomponent of <code><parent></code></strong> โ€” a building block of a parent component (<code>step</code> of Stepper, <code>card</code> of Carousel, <code>item</code> of Breadcrumb, <code>avatar</code> of Avatar Group, <code>tab</code> of Tabs, <code>crumb</code> of Breadcrumb, <code>option</code> of Segmented Control, ...). Named <code>_udc-<parentId>_<subName></code> with a <strong>leading underscore</strong> so Figma hides it from the asset picker (Figma's standard private- component convention). The <code>_</code> after the parent id is the parent/sub boundary and <code><subName></code> is hyphenated when multi-word (<code>_udc-stepper_step</code>, <code>_udc-data-field_example-longer</code>); see <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> ยง9. Its anatomy, props, states, and accessibility live inside the parent component's <code>spec.json</code>, NOT a separate component entry. No standalone <code>status.json</code> / <code>changelog.json</code> / Storybook story.</li> <li><strong>Family member set</strong> โ€” a public <code>udc-<stem>...</code> set that belongs to a multi-set <strong>family</strong>: a base plus a container/group that arranges instances of it (<code>udc-data-field</code> + <code>udc-data-field-group</code>; <code>udc-accordion-item</code> + <code>udc-accordion-group</code>). Visible in the asset picker like a main component, but UNLIKE a standalone main component the members do NOT each get their own docs entry โ€” the whole family folds into ONE docs component whose id is the family <strong>stem</strong> (<code>data-field</code>, <code>accordion</code>), sharing one <code>uds-docs/uds/components/<stem>/</code> folder, one <code>status.json</code>, one <code>changelog.json</code>, and one <code>uds-docs/uds/components.json</code> entry. One member is the <strong>primary</strong> โ€” the <code>figmaNodeId</code> target: the base <code>udc-<stem></code> if it exists, else a designated member. See <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> ยง8 "A family on one page."</li> </ul> <p> Default heuristic for "main vs subcomponent": if the component's variant axes only make sense in the context of a parent (e.g. <code>hasConnector</code> on a step is meaningless outside a stepper), or if the component is unlikely to be useful when dropped standalone into a layout (e.g. a single tab without its tab-list), it's a subcomponent. When in doubt, default to subcomponent and surface the question in the model for designer approval.</p> <p> Multi-set components are common, in two distinct shapes. A <strong>container-of-N</strong> is one main set (the container with <code>count</code> and the orchestration variants) plus a <code>_udc-</code> subcomponent (the per-item building block) โ€” the per-item piece is meaningless on its own, so it is a subcomponent. A <strong>family</strong> is two-or-more PUBLIC member sets โ€” a base plus its group/container, where the base is independently usable (<code>udc-data-field</code> + <code>udc-data-field-group</code>). Decide per set with the ยง9 litmus test: independently usable โ†’ family member set; only-inside-parent โ†’ subcomponent.</p> </li> <li><p><strong>Variant axes</strong> โ€” pick axis names and values from <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a>. Section 2 covers Size (<code>Small</code> / <code>Medium</code> / <code>Large</code>), section 3 covers Tone (<code>Info</code> / <code>Success</code> / <code>Warning</code> / <code>Error</code> / <code>Neutral</code>), section 4 covers Emphasis (either Primary / Secondary / Tertiary OR Bold / Default / Subtle depending on the component), section 6 covers when something is a variant vs a state vs a property, section 7 covers Title Case in Figma. Don't inherit a sibling's name if the sibling disagrees with the framework โ€” the framework wins. <strong>One concern per axis:</strong> <code>State</code> holds interaction/selection states only; disclosure (open vs closed), content presence (empty vs filled vs results), kind, size, and tone are each their own axis. A compound value like <code>Open-Empty</code> / <code>Open-Typed</code> is two concerns mushed into one โ€” split it (the global-search 2026-06-09 mush). See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง8.</p> </li> <li><p><strong>State matrix (class-driven)</strong> โ€” start from the component class's required-state baseline in <a href="../../rules/uds-component-checklist.mdc"><code>uds-component-checklist.mdc</code></a> ยง"State baseline", NOT from a generic list. That baseline is conditional on class: form controls require <code>error</code> / <code>required</code> / <code>disabled</code> (and <code>readonly</code> if text-entry); selectable controls require <code>checked</code> / <code>selected</code>; disclosure controls require <code>expanded</code> / <code>collapsed</code>; data / search surfaces require <code>empty</code>; pointer-interactive controls require <code>hover</code>; keyboard-focusable controls require <code>focus-visible</code>; actions/toggles/tabs require <code>active</code> / <code>pressed</code>. For EVERY state the class requires, mark it either supported (it WILL be built as a Figma variant in Phase B) or <code>notApplicable</code> with a one-line reason. Never silently omit a class-required state. <strong>When the component ALSO has a <code>Kind</code> / <code>Type</code> / <code>Mode</code> axis, apply this baseline to EACH interactive Kind โ€” reason per (Kind ร— State) cell, not once for the set.</strong> A Kind that can't take a state marks it <code>notApplicable</code> per kind (a static <code>Kind=Divider</code> takes none); a state present for one interactive Kind but silently missing for another is the gap โ€” the nav-header 2026-06-16 parents shipped without <code>Selected</code> / <code>Disabled</code> while leaves had them. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง"State coverage is per interactive Kind, not global". Pick the exact state NAMES from section 1 of <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> (including its Selected / Checked / Current reserved-word distinction). Maps to <code>spec.json</code> <code>states[]</code>. **Keep the <code>State</code> axis to genuine interaction/data states โ€” a value that changes the component's PURPOSE or MODE (an "add a metric" tile, a compose-vs-read mode) is a <code>Kind</code>/<code>Type</code> axis, not a state. The Metric Card originally carried <code>Add</code> as a <code>State</code> value; it's a distinct affordance and now lives on a <code>Kind = Metric | Add</code> axis. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง"State vs. kind."</p> </li> <li><p><strong>Accessibility plan</strong> โ€” keyboard, focus, screen reader, and disabled / loading / error behaviors, derived from the component class and node structure (never from screenshots). Maps to <code>spec.json</code> <code>accessibility.keyboard[]</code> / <code>screenReader[]</code> / <code>wcag[]</code>.</p> </li> <li><p><strong>Events</strong> โ€” every custom event the finished Web Component will dispatch: <code>name</code>, when it fires, and payload shape. Derive from the class โ€” <code>action</code> โ†’ <code>click</code>; <code>form</code> โ†’ <code>change</code> / <code>input</code>; <code>feedback</code> โ†’ <code>dismiss</code> / <code>open</code> / <code>close</code>; <code>navigation</code> โ†’ <code>select</code>; etc. Figma cannot draw an event, so this list lives only in the model and in the component-description contract written in Phase B (B.3.5) โ€” but it is REQUIRED for any class that dispatches events. If the class genuinely dispatches none, state that explicitly. Maps to <code>spec.json</code> <code>events[]</code>.</p> </li> <li><p><strong>Token plan</strong> โ€” explicit role-to-token map, e.g. <code>background.default = --uds-color-surface-interactive-default</code>. Every visual property the component will bind belongs in this map. If a needed token is missing from UDS Tokens, mark it <code>MISSING</code> and STOP the model there โ€” new tokens flow through the <a href="../import-figma-tokens/SKILL.md"><code>import-figma-tokens</code></a> skill, NOT through this factory.</p> </li> <li><p><strong>Typography binding strategy</strong> โ€” typography is one of the most common places ad-hoc font choices leak past the design system, so the factory has explicit defaults:</p> <ul> <li><strong>If the design system has bundled <code>TextStyle</code> objects</strong> (Figma's named text styles, e.g. <code>uds/text/label/base-medium</code>, <code>uds/text/heading/h2</code>, <code>uds/text/paragraph/xl</code>), they are the source of truth. Bind each text node via <code>textNode.textStyleId = style.id</code> after importing the style with <code>figma.importStyleByKeyAsync(STYLE_KEY)</code>. Bundled styles handle <code>fontFamily</code>, <code>fontSize</code>, <code>fontStyle</code> (weight), and <code>lineHeight</code> in one attachment, AND they automatically follow the design system's font-family modes (Inter / Poppins / Roboto / Lexend, etc.) and font-scale modes (smaller / default / larger). One binding, four properties tracked.</li> <li><strong>If the design system has only individual font variables</strong> (no bundled styles), bind each of the four font properties separately on every text node: <code>setBoundVariable('fontFamily', familyVar)</code>, <code>setBoundVariable('fontSize', sizeVar)</code>, <code>setBoundVariable('fontStyle', weightVar)</code>, and <code>setBoundVariable('lineHeight', lineHeightVar)</code>. Skipping any one means that property silently escapes the design system โ€” a designer flipping the font-family mode will see partial updates, not full theme switches.</li> <li><strong>Discover bundled styles via <code>search_design_system</code> with <code>includeStyles: true</code></strong> in Phase A. The Phase A model must enumerate either (a) the bundled style key per text role (<code>indicator-number โ†’ uds/text/label/base-medium</code>) OR (b) the four individual variable keys per text role, with rationale for which strategy applies.</li> <li><strong>Color is bound separately.</strong> Well-architected design system <code>TextStyle</code> objects do NOT include color โ€” color belongs to the semantic-tokens layer because it varies by state, surface, and theme. Bind text-color fills via the appropriate <code>text-*</code> color variable independently of the text style. If the design system's text styles happen to include color, surface that as a finding for the designer; apply the style anyway and bind a per-state color fill on top.</li> <li><strong>Font-loading prerequisite.</strong> Before applying a bundled text style or binding <code>fontFamily</code> / <code>fontStyle</code> to a multi-mode variable, preload every font family + every weight the styles and variables span (Inter Regular/Medium/Bold AND Poppins Regular/Medium/Bold AND Roboto AND Lexend, etc.). Per <code>figma-use</code> rule 8 + the <code>FONT_FAMILY</code> gotcha: missing fonts cause silent fallback or "missing font" placeholders.</li> </ul> </li> <li><p><strong>Inspector-editable properties</strong> โ€” enumerate every per-instance variation point the component should expose. The factory MUST default to exposing variation rather than burying it inside the variant hierarchy where a designer has to dive multiple levels to edit it. Four lists, every time, regardless of component type โ€” even if a list is empty, state that explicitly:</p> <ul> <li><p><strong>Variant axes (recap)</strong> โ€” the variant axes from above, restated here so the property surface for the component is in one place.</p> </li> <li><p><strong>TEXT properties</strong> โ€” every non-decorative text node a designer would reasonably override per-instance (labels, descriptions, titles, captions, counts, badge text, helper copy). Default rule: every text node not bound to an icon glyph gets a TEXT property. For each, list <code>propName</code>, <code>defaultValue</code>, and which node(s) the property links to via <code>componentPropertyReferences = { characters: propName }</code>. <strong>Non-skippable: the editable entry node of any text-entry component</strong> (the input/value text of an Input, Text Area, Combobox, Search, Date Picker, etc.) MUST get a TEXT property โ€” it is the component's primary editable surface, so it is never a candidate for omission. Differing baked-in display text across variants (placeholder vs. a selected value vs. a multi "Addโ€ฆ" hint) is NOT a reason to skip the property: bind the property anyway with a sensible default; per-variant display differences are demo cosmetics, not a contract. The earlier Combobox draft shipped without this property โ€” that was the miss this rule prevents.</p> </li> <li><p><strong>BOOLEAN properties</strong> โ€” every show/hide region (optional icons, optional descriptions, dividers, dismiss affordances, helper text, support indicators). For each, list <code>propName</code>, <code>defaultValue</code>, and which node(s) the property toggles via <code>componentPropertyReferences = { visible: propName }</code>.</p> </li> <li><p><strong>INSTANCE_SWAP properties</strong> โ€” every nested instance a designer would reasonably swap (icons, avatars, slot fillers, leading / trailing adornments). For a nested DS <em>wrapper</em> whose own purpose is to swap its content (e.g. <code>udc-icon-wrapper</code>'s glyph), prefer EXPOSING the nested instance (Phase B.2.6 / gotchas ยง12) so its own <code>Icon</code> / <code>Size</code> controls surface on the parent โ€” a whole-wrapper <code>INSTANCE_SWAP</code> only replaces the wrapper, not the glyph, which is rarely what the designer wants. When the design system or its subscribed libraries provide a wrapper or primitive for a category, the factory MUST nest that wrapper as the default INSTANCE_SWAP component. Do NOT fall back to:</p> <ul> <li>Unicode characters or font-glyph text nodes (<code>โœ“</code>, <code>!</code>, <code>โ˜…</code>, <code>โ†’</code>, etc.) for icons โ€” they don't follow size or color tokens, they break across font-family modes, and they're a code smell that signals the design system isn't being honored.</li> <li>Raw library components used directly when an established wrapper exists (e.g. a Material Icons component used directly when UDS has <code>udc-icon-wrapper</code> as the standardized icon container โ€” the wrapper handles size normalization and color binding; bypassing it leaks ad-hoc sizing into the file).</li> <li>Inventing a new local pattern for something a published primitive already covers. For each property, list <code>propName</code>, <code>defaultValue</code> (the appropriate wrapper / primitive โ€” wrapper when one exists in the design system, raw library component only when no wrapper applies), and which instance node the property links to via <code>componentPropertyReferences = { mainComponent: propName }</code>. The <code>defaultValue</code> FORMAT depends on where the target lives: in-file node ID for a local wrapper (<code>udc-icon-wrapper</code> is local), the published KEY only for a remote-library target โ€” see <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง2.</li> </ul> <p><strong>Per-variant defaults follow component anatomy and state semantics</strong>, not a single property-level default. For a Stepper, the <code>complete</code> variant's icon slot defaults to the wrapper containing <code>check</code>; the <code>error</code> variant defaults to the wrapper containing <code>priority_high</code>; the <code>upcoming</code> / <code>current</code> / <code>disabled</code> variants default to a hidden state or a neutral icon, depending on the model's anatomy decision. For a Notification, the <code>success</code> variant defaults to <code>check_circle</code>, the <code>error</code> to <code>error</code>, the <code>warning</code> to <code>warning_amber</code>, etc. State-specific defaults are baked into each variant via <code>setProperties</code> during the Phase B build, not hand-overridden by designers each time.</p> <p>For a component with a <code>Tone</code> axis, the same per-variant discipline applies to COLOR and to context: every tone-bearing adornment (status dot, secondary icon glyph, trend) defaults to that variant's tone family, and meaningful glyphs follow tone (a trend โ†’ <code>trending_up</code> for positive tones, <code>trending_down</code> for negative). Choose context-appropriate default glyphs over the wrapper's stock placeholder (a leads metric โ†’ <code>people_outline</code>, not <code>add_circle_outline</code>). See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง6.</p> </li> </ul> </li> <li><p><strong>Container count axis (if the component is a container of N repeating instances)</strong> โ€” if the component is a <em>container of N repeating instances of the same sub-component</em> (stepper, breadcrumb, tabs, pagination, carousel, avatar group, list, segmented control, ...), specify a <code>count</code> variant axis with a range appropriate to the component's UX. Derive the range from the component's nature plus comparable patterns in production design systems (Material, Polaris, Carbon, ...). Don't pick a global default โ€” choose per-component, justify the range in the model, and surface for designer approval. Indicative ranges (not contracts):</p> </li> </ul> <table> <thead> <tr> <th>Pattern</th> <th>Indicative range</th> <th>Why</th> </tr> </thead> <tbody><tr> <td>Stepper</td> <td>2โ€“7</td> <td><2 isn't a stepper; >7 is bad UX, use overflow</td> </tr> <tr> <td>Breadcrumb</td> <td>2โ€“5</td> <td>Deeper than 5 = use overflow menu</td> </tr> <tr> <td>Tabs (horizontal)</td> <td>2โ€“6</td> <td>More = vertical nav or overflow</td> </tr> <tr> <td>Avatar group</td> <td>2โ€“8</td> <td>Past 8, use "+N more" pill</td> </tr> <tr> <td>Carousel</td> <td>3โ€“10</td> <td><3 isn't really a carousel</td> </tr> <tr> <td>Pagination</td> <td>5โ€“10 visible page buttons</td> <td>Standard pattern</td> </tr> <tr> <td>Segmented control</td> <td>2โ€“5</td> <td>Past 5, use dropdown</td> </tr> <tr> <td>List (open-ended)</td> <td>not via variant โ€” instance duplication</td> <td>Don't fight the medium</td> </tr> </tbody></table> <p> Each <code>count</code> variant pre-builds the layout with N child instances and the terminal-position instance gets the correct terminal-state variant baked in (e.g. <code>hasConnector=false</code> on the last step) so the designer doesn't have to manually correct it after dropping in the stepper.</p> <ul> <li><strong>Sibling reuse โ€” reach for an existing DS component for EVERY standard affordance, never rebuild one from raw nodes.</strong> The factory nests existing UDS components as instances rather than re-drawing their anatomy. This is not just icons:<ul> <li><strong>Field label โ†’ nested <code>udc-label</code>, with its properties forwarded.</strong> A labeled field or control (text-input, dropdown, combobox, text-area, search, checkbox, radio, toggle, โ€ฆ) nests <code>udc-label</code> (NOT a raw text node + a separate required-dot). <code>udc-label</code> already owns the editable text, the required / optional indicators, the leading-icon and right-slot (badge) affordances, and the tone colors โ€” rebuilding it as raw text strands all of that. <strong>Nesting alone is not enough: forward the label's properties to the consumer</strong> โ€” set <code>isExposedInstance = true</code> on the nested instance (surfaces text / required / icon / right-slot in the host's right panel), or hoist the key ones (<code>text</code>, <code>required</code>) to top-level host properties. Drive the nested label's <code>tone</code> from host state (Error โ†’ <code>tone=Error</code>, Disabled โ†’ <code>tone=Disabled</code>). The 2026-06-08/09 combobox failed BOTH ways first โ€” nested-but-unforwarded (no editable label props in the panel), then host-owned raw text (threw the props away). The size drift (<code>paragraph/sm-medium</code> 12 px vs <code>label/base-medium</code> 14 px) was the draft <code>udc-label</code> binding the wrong text style, NOT a reason to abandon nesting โ€” fix it in the label component once and every field inherits it. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง9. The standalone <code>udc-label</code> also serves labels NOT wired to a built-in field (section headings, labels over custom controls).</li> <li><strong>Icons โ†’ <code>udc-icon-wrapper</code></strong> (per the INSTANCE_SWAP rules above).</li> <li><strong>Badges/counts โ†’ <code>udc-badge</code>; inline actions โ†’ <code>udc-button</code>; selectable tokens โ†’ <code>udc-chip</code>;</strong> etc. Before drawing any sub-part, check <code>uds-docs/uds/components.json</code> for an existing component that covers it and nest that instead. If the existing component is missing a property you need to drive from the parent (e.g. its label text isn't exposed), surface that as a finding โ€” the fix is to improve that component (a separate <code>{Cursor}</code> draft), not to bypass it with raw nodes. <strong>When you rebuild or improve an existing component as a successor draft, FIRST enumerate its full surface โ€” every entry in <code>componentPropertyDefinitions</code> (props, slots, INSTANCE_SWAPs, variant axes) and every named region โ€” and make the successor a SUPERSET. Never silently drop capability.</strong> A from-scratch rebuild that's missing features the original had (a leading-icon slot, a right-slot/badge, a tone) is a regression, not an improvement. The udc-label rebuild initially dropped its icon + badge slots; catch that by diffing the new component's property surface against the original before declaring it done. <strong>A restored/added slot's DEFAULT instance must be size- and emphasis-appropriate for the host.</strong> Reuse the original component's default slot variant (or pick a fitting small one) โ€” don't grab the first/largest variant of the swapped component. A label-sized host defaults to a 16px icon-wrapper and a SMALL, non-prominent badge, not a 24px icon and a full prominent badge (which dominates the label). And size the host layout so the text HUGS + wraps past a <code>maxWidth</code> rather than <code>FILL</code> โ€” <code>FILL</code> shoves a trailing slot to the far edge with an awkward gap. <strong>For multi-emphasis components (Button), pick the emphasis that matches the affordance's prominence โ€” secondary/utility affordances take the LOW-emphasis set.</strong> An overflow menu, a dismiss button, or an inline card action is a <code>udc-button-ghost</code> (or tertiary) icon-only instance, NOT <code>udc-button-primary</code> โ€” a filled accent button screams for attention a utility control shouldn't. Pair it with a context-appropriate default glyph (overflow โ†’ <code>more_vert</code>, dismiss โ†’ <code>close</code>), never the wrapper's stock <code>add_circle_outline</code> placeholder. The Metric Card overflow shipped as a primary button carrying <code>add_circle_outline</code> โ€” wrong emphasis AND wrong glyph. Common reuse set: <code>udc-label</code>, <code>udc-icon-wrapper</code>, <code>udc-button</code>, <code>udc-text-input</code>, <code>udc-chip</code>, <code>udc-badge</code>, <code>udc-card</code>, <code>udc-notification</code>. (Field labels nest <code>udc-label</code> AND forward its properties โ€” see the field-label bullet above.)</li> </ul> </li> <li><strong>Assumptions and acceptance criteria</strong> โ€” plain-language list the designer can scan in under a minute. Acceptance criteria must be contract-tied, not generic: name the <code><udc-<id>></code> tag, the variants that must render, each required state's distinct visual, the focus token, the events that must fire, and the key keyboard / screen-reader behavior โ€” the way a fleshed-out component's <code>acceptanceCriteria[]</code> reads (see <code>uds-docs/uds/components/button/spec.json</code> for the bar). Maps to <code>spec.json</code> <code>acceptanceCriteria[]</code>.</li> </ul> <h3>The drawable vs. non-drawable contract</h3> <p>Figma can draw variants, states, anatomy, slots, and nested instances. It CANNOT draw events, <code>::part()</code> exposure, keyboard / screen-reader behavior, or the acceptance checklist. Those non-drawable parts are still required for a complete component, so the factory handles them in two places:</p> <ol> <li>They live in the model file (sections above), so the designer reviews them at the approval gate.</li> <li>Phase B writes them into the Figma component's <strong>description</strong> as a delimited factory-contract block (step B.3.5). The <a href="../../agents/figma-component-inspector.md"><code>figma-component-inspector</code></a> reads the component description, so when the designer later runs <a href="../uds-updated/SKILL.md"><code>uds-updated</code></a> the contract round-trips into <code>spec.json</code> (<code>events[]</code>, <code>slots[]</code>, <code>accessibility</code>, <code>acceptanceCriteria[]</code>) instead of landing empty. This is the synergy: the factory authors the full web-component contract once, and the docs sync consumes it.</li> </ol> <h3>Approval gate (mandatory before Phase B)</h3> <p>After persisting the model, present it inline and pause. Per <code>figma-generate-library</code> ยง"Explicit phase approval", "looks good" / "fine" / "OK" do NOT count. Wait for the literal word <code>approved</code> (case-insensitive) before any Figma write.</p> <p><strong>Approval with changes.</strong> If the designer says "approved, but change X" or "approved with: <list>", apply the requested changes to the model (overwriting the persisted markdown), confirm the changes are captured, then proceed to Phase B. Do NOT require a second <code>approved</code>.</p> <p><strong>Pure rejection.</strong> "Not yet" / "rework this" returns to Phase A research. Do not proceed.</p> <h2>Phase B โ€” Draft page build (Figma writes, scope #4 only)</h2> <p>After the model is approved, the skill writes ONLY to a new <code>{Cursor}{Ignore}</code> page on the <code>UDS Components</code> file. Every <code>use_figma</code> call MUST:</p> <ul> <li>Re-import variables in that call. Do not rely on stale variable IDs from prior calls (variable IDs are not stable across <code>use_figma</code> invocations โ€” see the <a href="../figma-component-card/references/gotchas.md"><code>figma-component-card</code> gotchas</a>).</li> <li>Tag every created node immediately with <code>setSharedPluginData('dsb','run_id', RUN_ID)</code> plus a logical key, per the <code>figma-generate-library</code> state ledger. The <code>run_id</code> is what the future <code>purge-failed-factory-run</code> skill will use to clean up failed runs without name-guessing.</li> <li>Pass <code>skillNames: "generate-uds-figma-component, figma-generate-library, figma-use"</code> to <code>use_figma</code> so the call is logged correctly.</li> </ul> <p>The build is a small chain of sequential <code>use_figma</code> calls (never parallel โ€” <code>figma-generate-library</code> rule 13). Each call is its own atomic Figma transaction; if one throws, no nodes from that call land and you can re-run after fixing the cause.</p> <h3>B.1 โ€” Create the page</h3> <ul> <li>Page name: <code><pageBaseline> <componentId> {Cursor}{Ignore}</code> โ€” the lowercase kebab id, matching mainline UDS Components pages (<code>badge</code>, <code>data-table</code>, <code>icon-wrapper</code>). With the default baseline that's <code>๐ŸŸ  <id> {Cursor}{Ignore}</code> (e.g. <code>๐ŸŸ  metric-card {Cursor}{Ignore}</code>).</li> <li>The <code>{Ignore}</code> marker takes the page out of every UDS automation (<a href="../../agents/figma-inventory.md"><code>figma-inventory</code></a>, <a href="../sync-figma-component-status/SKILL.md"><code>sync-figma-component-status</code></a>, <a href="../uds-updated/SKILL.md"><code>uds-updated</code></a>, <a href="../../agents/figma-spec-gap.md"><code>figma-spec-gap</code></a>, <a href="../../agents/figma-component-inspector.md"><code>figma-component-inspector</code></a>) per <a href="../../rules/uds-figma-preflight.mdc"><code>uds-figma-preflight.mdc</code></a>. No rule changes are needed.</li> <li>The <code>{Cursor}</code> label tells designers it's a factory draft.</li> <li>Designer accepts later by renaming the page (drop <code>{Cursor}{Ignore}</code>, set the stoplight prefix to <code>๐ŸŸก</code> review or <code>๐ŸŸข</code> production).</li> </ul> <h3>B.2 โ€” Build the component set</h3> <ul> <li>Create each component set with the name agreed in the model:<ul> <li><strong>Main components:</strong> <code>udc-<id></code> (no underscore).</li> <li><strong>Subcomponents:</strong> <code>_udc-<parentId>_<subName></code> with a <strong>leading underscore</strong> and a single <code>_</code> parent/sub boundary (<code>_udc-stepper_step</code>, <code>_udc-data-field_example-longer</code>; the <code>_</code> boundary keeps the name parseable when the parent id has hyphens โ€” <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> ยง9). Figma's standard private-component convention keeps the subcomponent out of the asset picker so designers don't accidentally pick the building block instead of the container. Parent components reference subcomponents via component KEY (stable across files), so the underscore prefix has no functional effect on parent โ†’ child instance binding.</li> <li><strong>Family member sets:</strong> when the model declared a family (Phase A "Family member set"), build EACH public <code>udc-<stem>...</code> member set on the SAME stem page โ€” <code>udc-data-field</code> and <code>udc-data-field-group</code> both on the <code>data-field</code> page. Where the model says one member arranges the other (a group/container of the base), nest the base member as INSTANCES inside the container member and forward/expose its controls (<code>isExposedInstance = true</code> or hoisted host props, per B.2.6). Each member set is a real component set: it gets its own variant axes, its own factory contract block (B.3.5), and its own version stamp (B.3.6). Subparts of a member use <code>_udc-<member>_<sub></code>.</li> </ul> </li> <li>Variant property names and values follow <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> exactly โ€” Title Case in Figma (section 7), canonical state / variant / size / tone / emphasis names from sections 1-4. If the Phase A model picked a name that doesn't appear in the framework for a category the framework covers, fix it before this step rather than encoding the drift into Figma.</li> <li>Build variants and states with auto-layout. <strong>Build every state the Phase A model marked supported for the component's class</strong> โ€” not just the visually obvious ones. A <code>form</code> draft must include <code>error</code>, <code>required</code>, and (if supported) <code>readonly</code> as real variants; a selectable control must include <code>selected</code> / <code>checked</code>; a disclosure control must include <code>expanded</code> / <code>collapsed</code>; a data/search surface must include <code>empty</code>. States the model marked <code>notApplicable</code> are skipped (their reason rides in the B.3.5 contract block). Defaults: containers hug content unless a fixed dimension is part of the spec; <strong>every used spacing property is bound per side</strong> (see below); flat structure preferred unless nesting is required.</li> <li><strong>Per-side spacing binding (all five properties).</strong> The Plugin API has no <code>paddingHorizontal</code> / <code>paddingVertical</code> shorthand โ€” auto-layout exposes <code>paddingTop</code>, <code>paddingBottom</code>, <code>paddingLeft</code>, <code>paddingRight</code>, and <code>itemSpacing</code> as five independent bindable properties. Bind all five per <code>setBoundVariable('paddingTop', spaceVar)</code>, never assign <code>node.paddingTop = 8</code> directly. The requirement is <em>every side bound</em>, not <em>every side equal</em> โ€” a pill that legitimately wants <code>paddingLeft = uds-space-200</code> (16px) and <code>paddingRight = uds-space-150</code> (12px) is fine; what's not fine is pairing those bound horizontal values with raw <code>paddingTop = 8</code> and <code>paddingBottom = 8</code> literals. When the real component does want the same value on both sides of an axis, pass the same <code>uds-space-100</code> variable to both sides โ€” that's still five <code>setBoundVariable</code> calls. Helper functions that wrap padding configuration MUST take all five properties (or a <code>{ top, right, bottom, left, gap }</code> object); the two-axis helper signature <code>(h, v)</code> is an anti-pattern โ€” it binds whichever pair the helper takes and leaves the other pair as raw pixel literals. See <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง5 for the underlying API truth and the silent-failure mode.</li> <li><strong>Effect-style binding (never raw effects literals).</strong> For every drop shadow, blur, or other elevation visual the model specifies, bind via <code>await node.setEffectStyleIdAsync(effectStyle.id)</code> after <code>await figma.importStyleByKeyAsync(EFFECT_STYLE_KEY)</code>. Never write <code>node.effects = [{ type: 'DROP_SHADOW', ... }]</code> even if the literal values exactly match the design system's depth scale โ€” raw effect literals escape every "is this bound to a token?" check, and factory runs that copy depth values from inspection rather than binding the style routinely ship visually-shadowed nodes whose values don't match any depth step at all. See <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง6 for the wrong/correct contract and audit signature. To make a node intentionally FLAT (no elevation), set <code>node.effects = []</code> โ€” <code>setEffectStyleIdAsync('')</code> only detaches the style and leaves the prior shadow as a raw literal that fails the Phase C effect gate (gotchas ยง11).</li> <li>Bind fills, strokes, <strong>typography</strong>, spacing (per the per-side rule above), radius, <strong>and effects</strong> (per the effect-style rule above) to UDS Tokens via the role-to-token map from Phase A. For typography specifically: prefer bundled <code>textStyleId = style.id</code> when UDS has the matching bundled text style; fall back to four individual variable bindings (<code>fontFamily</code>, <code>fontSize</code>, <code>fontStyle</code>, <code>lineHeight</code>) only when bundled styles are absent. Never hardcode font names, sizes, weights, or line-heights as raw values.</li> <li>Use the actual library variable / style keys returned by <code>get_libraries</code> + <code>search_design_system</code> โ€” never hardcode hex values into bound paints.</li> <li><strong>Bake the resolved value into every bound paint's literal <code>color</code>.</strong> A paint from <code>setBoundVariableForPaint</code> keeps the literal fallback you passed, and component / variant ROOT fills render that literal (not the live-resolved token) โ€” and re-binding a variable a cloned node already had keeps the stale literal. Build the paint as <code>{ type:'SOLID', color: v.resolveForConsumer(node).value, boundVariables:{ color:{ type:'VARIABLE_ALIAS', id: v.id } } }</code>, or run a post-pass that re-bakes every bound fill/stroke from its resolved value. Skipping this is the "black variant roots" bug. See <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง9.</li> <li><strong>Re-bind every tone-bearing adornment per variant.</strong> When you clone a base variant across a <code>Tone</code> axis, retint not just the surface but EVERY color-carrying adornment โ€” status dot, secondary / leading icon glyph, trend icon + text, accent bar โ€” to that variant's tone family, and set meaningful glyph defaults per tone (trend โ†’ <code>trending_up</code> for positive tones, <code>trending_down</code> for negative). A clone-and-retint that misses one leaves it stuck on the base tone (the Metric Card trend shipped green on the Error card). See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง6.</li> <li><strong>Focus / ring states are an offset, gapped, resizing ring โ€” never a thickened border.</strong> Build a <code>focus-outline</code> child frame (absolute, <code>STRETCH</code> constraints, inset โˆ’(gap + ring), <code>fills = []</code>, stroke <code>outline-focus-visible</code>, <code>strokeAlign = 'INSIDE'</code>, concentric radius, parent <code>clipsContent = false</code>); <strong>2 px gap</strong> is the standard. Thickening the element's own border in the focus color is wrong (the Metric Card focus miss). See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง1 and <a href="../../rules/uds-design-language.mdc"><code>uds-design-language.mdc</code></a> ยง6.</li> <li><strong>Field / form-control sizing follows the field-metrics contract.</strong> When the component is a field or form control, size its alignment/touch container to the contract height (48 px for UDS) and vertically center the visible control โ€” fill it for a text field, center the smaller box for a checkbox / radio / toggle (never a 48 px-tall checkbox). Label = nested <code>udc-label</code> at the contract label style; helper = contract helper style; label/helper gaps = contract gap. <strong>Author at default scale and density</strong> โ€” font scale (<code>larger</code>/<code>default</code>/<code>smaller</code>) and density (<code>comfortable</code>) are runtime user settings, not per-control build decisions, and the contract's px (input 14, helper 10, label 14) are the default-scale resolutions โ€” don't bake in the larger-scale 16/12. Resolve token values through the alias chain + modes, never from a rendered size. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง11, <a href="../../rules/uds-design-language.mdc"><code>uds-design-language.mdc</code></a> ยง10 ("Design at default scale and density"), and <a href="../../rules/uds-token-architecture.mdc"><code>uds-token-architecture.mdc</code></a> ยง"Don't claim a token value โ€” resolve it first".</li> <li>Use existing UDS components as nested instances where the model says to (per Phase A "Sibling reuse").</li> <li><strong>For every icon slot the model enumerates, nest the appropriate UDS wrapper component as an INSTANCE</strong> (<code>udc-icon-wrapper</code> for icons, etc.) โ€” never a Unicode glyph in a TEXT node, never a raw Material Icons component referenced directly. For each variant, set the wrapper's swap property to the per-variant default the model specifies (e.g. <code>check</code> for the Stepper's <code>complete</code> variant indicator). Use <code>setProperties</code> on the variant's instance node with the wrapper's swap-property full name to bake in the per-state default.</li> <li>Use meaningful layer names that match the anatomy. Names like <code>Frame 12</code> are a layer-hygiene gate failure in Phase C.</li> </ul> <h3>B.2.5 โ€” Wire inspector properties</h3> <p>Every TEXT / BOOLEAN / INSTANCE_SWAP property the model enumerates in its "Inspector-editable properties" section MUST be registered on the component set and linked to the relevant nodes. Done after the component set is combined (so the property surface lives on the set, not on individual variants), and BEFORE the B.4 write summary.</p> <p>Plugin API recipe (per <code>figma-use</code> rule 15: re-capture node IDs from the state ledger, do not guess):</p> <pre><code class="language-js">// 1. Register each property on the component set. componentSet.addComponentProperty('label', 'TEXT', 'Step label'); componentSet.addComponentProperty('description', 'TEXT', 'Optional description'); componentSet.addComponentProperty('showDescription','BOOLEAN', true); // INSTANCE_SWAP default: local target = node ID (NOT the published key). // Recover via: (await someInstance.getMainComponentAsync()).id componentSet.addComponentProperty('leadingIcon', 'INSTANCE_SWAP', iconWrapperNodeId); // 2. Link the property to the node(s) it controls. The propName must // match the registration. For variant-scoped components, walk each // variant child of the set and link the equivalent node in each. for (const variant of componentSet.children) { const label = variant.findOne(n => n.name === 'label'); label.componentPropertyReferences = { characters: 'label' }; const description = variant.findOne(n => n.name === 'description'); description.componentPropertyReferences = { characters: 'description' }; const labelGroup = variant.findOne(n => n.name === 'label-group'); // BOOLEAN visibility lives on the section the property hides, NOT // on the parent containing it. The parent stays visible; the section // toggles. description.componentPropertyReferences = { ...description.componentPropertyReferences, visible: 'showDescription' }; const icon = variant.findOne(n => n.name === 'leading-icon' && n.type === 'INSTANCE'); if (icon) icon.componentPropertyReferences = { mainComponent: 'leadingIcon' }; } </code></pre> <p>Key rules:</p> <ul> <li><strong>Property surface lives on the set, not the variant.</strong> Variants inherit from the set; per-variant registration causes inspector panel noise and inconsistent property surfaces across variants.</li> <li><strong><code>propName</code> must be unique within the set.</strong> Figma silently prefixes the name with a hash on registration, but the <em>base name</em> you pass to <code>addComponentProperty</code> is what designers see.</li> <li><strong>Re-link in every variant.</strong> When a component set is combined from pre-existing variants, the child variants each carry their own node trees. The property reference must be applied to the matching node in each variant child, not just the first.</li> <li><strong>Text-node <code>characters</code> property links override the baked-in text.</strong> If a variant has special baked-in text (e.g. the <code>complete</code> state shows <code>โœ“</code> instead of a number), and you link a <code>stepNumber</code> TEXT property to that node, the property value overrides the baked-in <code>โœ“</code>. If you want a variant to keep baked-in text, either (a) skip the property reference on that variant's node, or (b) split the node into two โ€” one bound to the property, one with the baked-in glyph โ€” and use BOOLEAN visibility to toggle which one shows.</li> <li><strong>INSTANCE_SWAP default-value format depends on where the swap target lives</strong> (see <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง2). A <strong>local</strong> target (same file โ€” e.g. <code>udc-icon-wrapper</code>, which lives on a page in <code>UDS Components</code>) takes the in-file <strong>node ID</strong> (<code>5657:6767</code>), recovered via <code>await instance.getMainComponentAsync()</code> โ†’ <code>.id</code>. A <strong>remote</strong> (subscribed-library) target takes the published <strong>KEY</strong>. Passing a key for a local target throws <code>Property value is incompatible with component property type</code> โ€” that error means the format is mis-scoped, not that the property type is wrong.</li> </ul> <p>If the component is a container of repeating instances (per the "Container count axis" model section), build <strong>one variant per count value</strong>. Each variant pre-populates the correct number of child instances and the terminal-position instance(s) get the appropriate terminal-state variant set (e.g. <code>hasConnector=false</code> on the last step in a horizontal stepper). The designer should not have to manually fix anything about the terminal-position instance when they drop the parent component into a layout.</p> <h3>B.2.6 โ€” Expose nested DS-component instances</h3> <p>Nesting a UDS component (e.g. <code>udc-icon-wrapper</code>) does NOT surface its own properties on the parent. By default a designer selecting your component sees nothing of the wrapper's <code>Icon</code> glyph-swap or <code>Size</code>, and a top-level <code>INSTANCE_SWAP</code> (B.2.5) only swaps the <em>whole wrapper</em>, not the glyph inside it. For every nested DS instance whose own controls a designer should reach, set <code>isExposedInstance = true</code> on that instance in EVERY variant.</p> <p><strong>Enumerate ALL nested DS instances first, then expose each โ€” partial coverage is the trap.</strong> List every nested wrapper/button/adornment the component carries (leading icon, trend icon, overflow menu, dismiss button, โ€ฆ) and expose each one. Exposing the primary icon while forgetting the secondary instances (the overflow <code>menu</code> and the <code>trend</code> icon both shipped unexposed) was the Metric Card miss โ€” the designer could change the headline icon but not the menu glyph or the trend direction. The Phase C "nested-instance exposure coverage" gate makes this machine-checkable.</p> <p><strong>No sealed controls โ€” ask "what does a consumer need to set?" of EVERY nested instance, not just icons.</strong> This isn't only about icon wrappers. Any nested instance that carries its own controls โ€” a first-class <code>udc-*</code> component, or a <code>_udc-<id>_*</code> subpart with its own variant axis, editable text, meaningful boolean, or instance-swap โ€” must forward those controls (expose it, or hoist its key props to top-level host properties). A component whose nested controls are all sealed looks finished but can't be driven: the global-search 2026-06-09 build passed every other gate while shipping its <code>_udc-global-search_trigger</code> (<code>State</code>, <code>Filled</code>, placeholder, value) and <code>_udc-global-search_popover</code> (content variant) sealed, so a consumer could only toggle <code>Open</code>. As you nest, decide for each instance what a consumer would need to set and forward exactly that. The only exemption is purely decorative or structural nesting (a static divider, a fixed ornamental glyph) โ€” that reports as a soft review note in Phase C, not a failure. The proactive half of the exposure gate flags every sealed control-bearing instance even if the model never marked it reachable.</p> <pre><code class="language-js">for (const variant of componentSet.children) { for (const nm of ['icon', 'trend-icon']) { const inst = variant.findOne(n => n.name === nm && n.type === 'INSTANCE'); if (inst) inst.isExposedInstance = true; // surfaces its Icon + Size on the parent } } </code></pre> <p>Verify: instantiate the set and confirm <code>instance.exposedInstances</code> lists each wrapper's <code>componentProperties</code> (<code>Icon#โ€ฆ</code>, <code>Size</code>). If B.2.5 registered a whole-wrapper <code>INSTANCE_SWAP</code> for the same instance, drop it (<code>componentSet.deleteComponentProperty(name)</code>) unless replacing the entire wrapper is a real use case โ€” exposing the glyph/size control is what designers want, and keeping both clutters the panel. This was the Metric Card <code>trendIcon</code> miss. See <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง12.</p> <h3>B.3 โ€” Token discipline</h3> <ul> <li>If a needed token is missing from UDS Tokens, STOP and ask. New tokens flow through the UDS Tokens Figma file, then the <a href="../import-figma-tokens/SKILL.md"><code>import-figma-tokens</code></a> skill โ€” never via this factory's <code>use_figma</code> calls. Document the missing token in the model's "Token plan" section as <code>MISSING</code> and surface it to the user.</li> <li>Never write raw hex into a bound paint's <code>color</code> field unless you also bind it to a variable. The literal is the design-time fallback; the binding is what survives mode flips.</li> </ul> <h3>B.3.5 โ€” Author the factory contract into the component description</h3> <p>Write the non-drawable contract (the parts Figma can't represent visually) into the component set's <code>descriptionMarkdown</code> field so it round-trips into <code>spec.json</code> when the designer later runs <a href="../uds-updated/SKILL.md"><code>uds-updated</code></a>. Setting the description of a component that lives on the <code>{Cursor}{Ignore}</code> draft page is covered by scope #4 โ€” it's part of building the draft, not a separate write target. Use a delimited block so the inspector can find and parse it and the designer can edit or delete it freely:</p> <pre><code class="language-text"><<UDS-FACTORY-CONTRACT v1>> Factory-version: <YYYY.MM.DD.N> Component: <Title> (<id>) Class: <layout|display|action|form|navigation|feedback|data> Summary: <1โ€“2 plain-language sentences: what the component is and what it does> Depends on: - <udc-foo> โ€” <relationship: nested | opens in popover | composed in slot> (or: none) Variant axes: - <Axis> โ€” <Value> | <Value> | โ€ฆ (verbatim from the component's variant options) Exposed properties (forwarded to the consumer panel): - <nested instance> โ†’ <prop> (<type>) โ€” <what it controls> (or: none โ€” component forwards nothing beyond its own top-level props) Props (behavioral, non-drawable): - <name> (<type>, default <value>) โ€” <behavior; which states/events it gates> (or: none) Events: - <name> โ€” <when it fires> โ€” payload: <shape> (or: none (class does not dispatch)) Slots: - <name> โ€” <what it holds> Parts: - <name> โ€” <region exposed for ::part() styling> (or: none) States (variant baseline): - <state> โ€” supported | notApplicable: <reason> (verbatim from the State axis options) States (behavioral, non-drawable): - <state> โ€” supported | notApplicable: <reason> (states NOT on the State axis: driven by another axis like Open/Content, or applied at runtime like loading) (or: none) Keyboard: - <key> โ€” <action> Screen reader: - <trigger> โ€” <announcement> Acceptance criteria: - <contract-tied item> Designer: edit or remove this block freely โ€” it's a factory draft. <<END-UDS-FACTORY-CONTRACT>> </code></pre> <p>Rules:</p> <ul> <li><strong>Write via <code>descriptionMarkdown</code>, NOT <code>description</code>.</strong> The plain <code>description</code> setter HTML-escapes <code><</code>/<code>></code> (<code><<</code> becomes <code>&lt;&lt;</code>), which breaks the <code><<UDS-FACTORY-CONTRACT v1>></code> delimiter the inspector matches and lands the <code>spec.json</code> fields empty. <code>descriptionMarkdown</code> stores the literal brackets โ€” after writing, verify <code>node.descriptionMarkdown.indexOf('<<UDS-FACTORY-CONTRACT v1>>') >= 0</code>. See <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง10.</li> <li><strong>Human-readable.</strong> A designer sees this in Figma's asset panel. Keep it as the plain-text block above, not a JSON dump.</li> <li><strong>Behavioral props go in the <code>Props (behavioral, non-drawable)</code> section, NOT as no-op Figma BOOLEANs.</strong> A prop that changes runtime behavior but has no visual node to toggle โ€” <code>selectable</code>, <code>href</code>, <code>loading</code> as an aria-state, etc. โ€” has no honest Figma component property to bind to. Registering it as an unlinked BOOLEAN fails the Phase C property-wiring gate (a BOOLEAN with no <code>visible</code> reference). Record it here instead; this is the only round-trip source for props that aren't drawable component properties. Drawable props (<code>showIcon</code>, <code>label</code>, <code>leadingIcon</code>) stay as real Figma TEXT/BOOLEAN/INSTANCE_SWAP properties and are NOT duplicated here. This was the Metric Card <code>selectable</code> gap.</li> <li><strong>No silent omission.</strong> Every section appears. If a section doesn't apply, write <code>none</code> or <code>none (class does not dispatch)</code> rather than dropping the heading โ€” the inspector relies on the section being present.</li> <li><strong>Summary is plain-language and sits right under the identity lines (<code>Component</code> / <code>Class</code>).</strong> <code>Factory-version:</code> is now the FIRST line of the block so a stale draft is obvious at a glance; the identity lines and then the Summary follow. One or two sentences a designer reads first: what the component is and what it does. Not a feature list, not implementation. (e.g. "A compact dashboard card showing one labeled metric in five tones; optionally selectable to act as a filter.")</li> <li><strong><code>Depends on:</code> lists the other UDS components this one relies on, with the relationship.</strong> Cover both <em>structural</em> dependencies (a <code>udc-*</code> nested as an instance โ€” <code>udc-icon-wrapper โ€” nested</code>) and <em>behavioral</em> ones (a component this one opens or reveals โ€” <code>udc-calendar โ€” opens in popover</code>). The nested-instance half is machine-checkable (see the currency rule below); the "opens / reveals" half is author-supplied because Figma can't infer runtime behavior. Internal <code>_udc-<id>_*</code> subparts of THIS component are not dependencies โ€” only other first-class <code>udc-*</code> components are. <code>none</code> if it composes nothing.</li> <li><strong><code>Exposed properties:</code> lists what a consumer can set on the top-level instance โ€” the API made visible.</strong> Beyond the component's own top-level <code>componentPropertyDefinitions</code> (the <code>Variant axes</code> and drawable Props), record every control forwarded from a nested instance marked <code>isExposedInstance = true</code>, as <code><nested instance> โ†’ <prop></code>. This is machine-checked against the live exposure (ยง2 check 12): a control-bearing nested instance that's sealed must NOT be claimed here, and an exposed one must NOT be omitted. The section exists so "this component ships sealed" is visible on the page instead of discovered when a designer tries to use it โ€” the global-search 2026-06-09 miss (only <code>Open</code> was reachable). Write <code>none</code> only when the component genuinely forwards nothing beyond its top-level props.</li> <li><strong>Regenerate the WHOLE block on ANY touch โ€” never patch one line.</strong> The block is a derived view of the live component; every section can go stale, not just states. Whenever you touch the component โ€” rename a variant value, add/remove a state, change props/slots/events, swap a nested component, or revise behavior โ€” rewrite the entire block from the component's current state and re-stamp (<code>built_at</code> + the bar's <code>factory_version</code>). The metric-card 2026-06-09 drift (variants renamed to <code>Hovered</code>/<code>Focused</code>, contract still reading <code>Hover</code>/<code>Focus</code>) is exactly the failure this prevents. <code>Variant axes</code> and <code>States</code> are copied VERBATIM from <code>componentPropertyDefinitions</code>; <code>Depends on</code> (nested half) from the actual nested <code>udc-*</code> instances. See <a href="../../rules/uds-factory-versioning.mdc"><code>uds-factory-versioning.mdc</code></a> "Touching a component regenerates its contract."</li> <li><strong>Internal <code>_udc-<id>*</code> subparts don't carry their own block.</strong> A subcomponent that exists only inside one parent (the <code>_udc-calendar_day</code> cell set inside <code>udc-calendar</code>) is documented INSIDE the parent's contract โ€” its variant axes/states go in the parent's <code>Parts</code> + <code>States</code> sections, as the calendar block does for the day cell. The subpart still carries the <code>factory_version</code> plugin-data stamp (so drift detection works on it), but it is exempt from the "contract block present" gate. Only first-class <code>udc-*</code> components (the ones that round-trip to <code>spec.json</code> via <code>uds-updated</code>) require their own block.</li> <li><strong>Single round-trip source.</strong> This block is the only place the inspector can read events, parts, keyboard, and acceptance for a brand-new component (there's no Web Component source yet). If it's missing or malformed, those <code>spec.json</code> fields land empty on first sync.</li> <li><strong>The <code>Factory-version:</code> line records the build vintage as <code>F# (date)</code></strong> โ€” e.g. <code>Factory-version: F12 (2026.06.24.1)</code>. The date is <code>version</code> from <code>.cursor/figma/state/factory-version.json</code>, and the <code>F#</code> is the current <code>fVersion</code> with an <code>F</code> prefix (both read in pre-flight step 7). The DATE MUST match the <code>factory_version</code> plugin-data stamp written in B.3.6 (the date is the machine key โ€” two places); the <code>F#</code> is the human-readable display index and is NOT separately stamped. See <a href="../../rules/uds-factory-versioning.mdc"><code>uds-factory-versioning.mdc</code></a> ยง"The F# short key".</li> </ul> <h3>B.3.6 โ€” Stamp the build version</h3> <p>After authoring the contract block, stamp the factory build version onto the <strong>main component-set node</strong> (<code>udc-<id></code>) so drift detection and the docs round-trip can read it later. Two plugin-data writes, same <code>dsb</code> namespace <code>run_id</code> already uses:</p> <pre><code class="language-js">const fv = '<YYYY.MM.DD.N>'; // from factory-version.json, pre-flight step 7 componentSet.setSharedPluginData('dsb', 'factory_version', fv); componentSet.setSharedPluginData('dsb', 'built_at', '<YYYY-MM-DD>'); // today: date -u +%Y-%m-%d // run_id is already set per the figma-generate-library state ledger </code></pre> <p>The <code>Factory-version:</code> line in the B.3.5 contract block shows the same vintage for humans as <code>F# (date)</code> โ€” <code>F<fVersion> (<fv>)</code>, e.g. <code>F12 (2026.06.24.1)</code>. Only the date (<code>fv</code>) is stamped in plugin data; the F# is derived. The stamp lives on the NODE, so it survives the designer's acceptance rename. <code>built_at</code> is the date of the factory write that produced the current stamped state โ€” NOT the same as <code>factory_version</code> (the factory's vintage; a component built months after the factory last changed still carries the older vintage).</p> <p><strong>On a stamp-only catch-up (re-emit/upgrade), not a fresh build:</strong> set <code>factory_version</code> to the current bar, set <code>built_at</code> to the upgrade date (today โ€” the date of this factory write), and <strong>keep the original <code>run_id</code></strong> unchanged. <code>run_id</code> records the creation run and is what cleanup tooling maps to; do not mint a new one for an in-place upgrade. The three fields then read coherently: <code>run_id</code> = birth run, <code>built_at</code> = last factory write, <code>factory_version</code> = bar met. (Metric Card example: <code>run_id</code> stayed <code>metric-card-2026-06-04</code>, <code>built_at</code> became <code>2026-06-07</code>, <code>factory_version</code> became <code>2026.06.07.1</code>.) See <a href="../../rules/uds-factory-versioning.mdc"><code>uds-factory-versioning.mdc</code></a>.</p> <h3>B.4 โ€” Required write summary</h3> <p>After the build, emit ONE Figma write summary per the <a href="../../rules/uds-figma-write-safety.mdc"><code>uds-figma-write-safety.mdc</code></a> ยง"Required before/after report" template, scoped to the new page:</p> <pre><code class="language-markdown">## Figma write summary - File: UDS Components - File key: 1XJoUJgtNpw4R0IIT3VjoK - Page: ๐ŸŸ  <id> {Cursor}{Ignore} (newly created) - Node/frame: udc-<componentId> (component set) - Operation: create - Old value: (page did not exist) - New value: <list of variant property names + values + node IDs> - Rollback note: delete the page or run purge-failed-factory-run with run_id=<RUN_ID> </code></pre> <p>If the build fails partway through, stop and report the exact last successful operation. Do NOT continue with more writes until the user reviews the state. Re-run from the failed step after fixing the cause.</p> <h3>B.5 โ€” Post-build verification (mandatory before Phase C)</h3> <p>Per <code>figma-generate-library</code> rule 12 ("Validate before proceeding") and the <code>figma-use</code> "always read IDs from the state ledger" rule:</p> <ol> <li><p>Call <code>get_metadata</code> on the new page node to confirm structure: page child count, component-set children, variant-property names match the approved model.</p> </li> <li><p>Call <code>get_screenshot</code> on the page node to capture a visual record.</p> </li> <li><p>Cross-check that every created node ID returned by Phase B is present in the metadata response. Any missing or duplicate ID STOPS the run for investigation โ€” Phase C does not run on unverified work.</p> </li> <li><p><strong>Run the deterministic gate harness โ€” do NOT hand-roll which gates to run.</strong> Paste <a href="./references/phase-c-gate-check.js"><code>references/phase-c-gate-check.js</code></a> into a <code>use_figma</code> call with <code>SET_ID</code> set to the component set. It returns <code>pass</code> / <code>failedHardGates</code> plus per-gate detail for token bindings, per-side spacing, effect-style bindings, typography, text-wrap, the canonical-naming gate, <strong>property-wiring liveness</strong> (every registered property referenced by โ‰ฅ1 node in every variant โ€” catches dead toggles), layer hygiene, the contract-block delimiters, the version stamp, and a proactive sealed-control scan. <code>pass: false</code> STOPS the run. The harness output populates the Phase C "Tool-emitted gates" section directly โ€” that section is no longer hand-assembled from memory. For a <strong>family</strong>, run the harness once per public <code>udc-<stem>...</code> member set (set <code>SET_ID</code> to each in turn): each member set has its own variant matrix, contract block, and version stamp, so each must clear the gate independently.</p> <p><strong>The harness is necessary, not sufficient.</strong> It clears only the mechanical, model-independent gates. It does NOT clear the judgment / model-dependent gates (variant matrix vs the approved model, per-variant INSTANCE_SWAP defaults, tone-bearing adornment role-split, the model's designer-reachable exposure list, visual correctness). Complete <code>report.NOT_CHECKED_run_these_manually</code> as a SEPARATE pass โ€” a green harness is not a ship signal on its own.</p> <p>Tripwire: a hand-rolled Phase C has now silently dropped a NON-SKIPPABLE gate twice โ€” the <code>State = โ€ฆ | Editing</code> naming miss (data-field 2026-06-10) and the dead <code>showExpand</code> BOOLEAN (rich-text-editor 2026-06-11). The harness exists so a skipped gate is a missing key in the report, not a silent pass.</p> </li> </ol> <h2>Phase C โ€” Quality-gate report</h2> <p>The first draft is not production-ready by default. Goal: a high-quality starting point + a clear report of what still needs review. Emit a structured report with two sections.</p> <h3>Tool-emitted gates (deterministic โ€” counts, not opinions)</h3> <p>These are produced by running <a href="./references/phase-c-gate-check.js"><code>references/phase-c-gate-check.js</code></a> in B.5 step 4 โ€” <strong>run the harness, don't re-derive these by hand.</strong> The script is the EXECUTABLE definition of the deterministic gates; the descriptions below explain what each means and must be reconciled with the script when either changes. The harness covers the model-INDEPENDENT gates: token bindings, per-side spacing, effect-style bindings, typography, text-wrap, canonical-naming, property-wiring liveness, layer hygiene, contract delimiters + version stamp, and the proactive sealed-control scan. The gates that need the approved Phase A model (variant matrix match, per-variant INSTANCE_SWAP defaults, tone-bearing adornment role-split, the model's designer-reachable exposure list) are NOT in the harness and stay a manual pass.</p> <ul> <li><strong>Token bindings.</strong> Raw color/fill/stroke values found: N. Unbound corner radii: N at <code><nodeIds></code>.</li> <li><strong>Build version stamped.</strong> The main component set's <code>factory_version</code> plugin data equals the current <code>.cursor/figma/state/factory-version.json</code> <code>version</code> (date), and the contract block's <code>Factory-version: F# (date)</code> line shows that same date (plus the current <code>fVersion</code> as <code>F#</code>). Missing or date-mismatched: fail. The F# is the derived display index, not separately stamped. (<code>built_at</code> plugin data is also set to the build date.)</li> <li><strong>Per-side spacing bindings.</strong> Every auto-layout frame the factory created MUST have all five spacing properties (<code>paddingTop</code>, <code>paddingBottom</code>, <code>paddingLeft</code>, <code>paddingRight</code>, <code>itemSpacing</code>) bound to a <code>uds-space</code> variable when that property has a non-zero value. Per-side counts:</li> <li>Frames with unbound <code>paddingTop</code>: N at <code><nodeIds></code>.</li> <li>Frames with unbound <code>paddingBottom</code>: N at <code><nodeIds></code>.</li> <li>Frames with unbound <code>paddingLeft</code>: N at <code><nodeIds></code>.</li> <li>Frames with unbound <code>paddingRight</code>: N at <code><nodeIds></code>.</li> <li>Frames with unbound <code>itemSpacing</code> (gap > 0): N at <code><nodeIds></code>.</li> </ul> <p> The five must be reported separately. A combined "unbound spacing" count hides the most common asymmetry โ€” horizontal pair bound, vertical pair stranded as raw pixels โ€” which renders correctly in the default density mode and only breaks under <code>[data-density="comfortable"]</code>. See <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง5.</p> <ul> <li><p><strong>Effect-style bindings.</strong> Every node where <code>effects.length > 0</code> MUST have <code>effectStyleId !== ''</code>. Nodes with raw <code>effects = [...]</code> literals and no effect style attached: N at <code><nodeIds></code>. This is a separate gate from "Token bindings" because the visual output looks right even when the binding is missing โ€” the gate must check <code>effectStyleId</code> directly, not infer from <code>effects.length</code>. See <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง6.</p> </li> <li><p><strong>Typography binding.</strong> Every text node MUST have either a <code>textStyleId</code> (bundled UDS text style) OR a complete set of four font variable bindings (<code>fontFamily</code>, <code>fontSize</code>, <code>fontStyle</code>, <code>lineHeight</code>). Text nodes with neither: N. Text nodes with partial individual bindings (some but not all four): N. The Phase A model decides which strategy applies; the file must follow it consistently. Color binding is checked separately under "Token bindings" since color is not part of typography styles.</p> </li> <li><p><strong>Text wrap.</strong> Every copy-bearing text node (helper/error text, descriptions, option supporting text, the value/input line) MUST be <code>textAutoResize='HEIGHT'</code> + <code>layoutSizingHorizontal='FILL'</code> inside a width-bound parent โ€” never the default <code>WIDTH_AND_HEIGHT</code> auto-width, which overflows the component on long content instead of wrapping. Auto-width copy nodes: N at <code><nodeIds></code>. Fixed short labels are exempt. See <a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง8.</p> </li> <li><p><strong>Variant matrix.</strong> Generated variant axes and values vs. the approved model. Match / mismatch report. For container-of-N components, this includes the <code>count</code> variant axis โ€” each enumerated count value MUST be present as a variant.</p> </li> <li><p><strong>Naming-convention gate (canonical axis values).</strong> Walk the set's <code>componentPropertyDefinitions</code> and check every <code>State</code>-axis (and any selection-axis) value against the canonical set from <a href="../../rules/uds-naming-conventions.mdc"><code>uds-naming-conventions.mdc</code></a> ยง1: <code>Default</code> ยท <code>Hovered</code> ยท <code>Focused</code> ยท <code>Pressed</code> ยท <code>Selected</code> ยท <code>Disabled</code> ยท <code>Loading</code> ยท <code>Error</code> ยท <code>Empty</code> ยท <code>Read-only</code> ยท <code>Dragged</code> ยท <code>Indeterminate</code> ยท <code>Checked</code> ยท <code>Current</code>. A <code>State</code> value outside this set is a HARD fail โ€” and NOT only the hyphenated compounds the ยง8 smell test catches (<code>Open-Empty</code>): a clean-looking SINGLE word that names a different concern (<code>Editing</code> = a mode, <code>Open</code> / <code>Expanded</code> = disclosure, <code>Selected</code> on a toggle on-state) fails by membership too, because it is a mode / disclosure / kind wearing a state's clothes and belongs on its own axis (ยง8). Non-canonical State values: N at <code><variantIds></code>. A rename classifies <code>potentially-breaking</code> (ask-user), never silent. This gate is a NON-SKIPPABLE line item, not an optional walk: the data-field 2026-06-10 build shipped <code>State = โ€ฆ | Editing</code> and passed a hand-rolled Phase C that simply omitted this check โ€” that omission is why it is enumerated here, mirroring <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง2 (naming-convention gate) and ยง8.</p> </li> <li><p><strong>Property wiring.</strong> Component properties registered on the set vs. the approved model's "Inspector-editable properties" lists. Match / mismatch per list (TEXT / BOOLEAN / INSTANCE_SWAP). For each registered TEXT property, every variant in the set MUST have at least one descendant node linking via <code>componentPropertyReferences.characters</code>. Same for BOOLEAN (<code>visible</code>) and INSTANCE_SWAP (<code>mainComponent</code>). Heuristic gap detection: text nodes whose name suggests editable copy (<code>label</code>, <code>description</code>, <code>title</code>, <code>caption</code>, <code>count</code>, <code>body</code>, <code>helper</code>, etc.) lacking a <code>characters</code> reference are flagged as <em>candidate</em> gaps โ€” designer judges whether each is intentionally decorative. <strong>Hard failure (not a candidate): the editable entry node of a text-entry component</strong> (<code>value</code>, <code>input</code>, <code>value-text</code>, search/entry field) without a <code>characters</code> TEXT-property reference fails this gate outright, per the non-skippable rule in Phase A. The harness reports this as <code>propertyWiring.dead</code> (property referenced by zero variants) and <code>propertyWiring.partial</code> (wired in some variants only). Tripwire: the rich-text-editor 2026-06-11 build shipped a dead <code>showExpand</code> BOOLEAN (registered, wired to no node) because its hand-rolled Phase C skipped this gate โ€” run the harness, don't eyeball it.</p> </li> <li><p><strong>Per-variant INSTANCE_SWAP defaults.</strong> For every component property of type <code>INSTANCE_SWAP</code> registered on the set, walk every variant and compare its current swap target against the per-variant default the Phase A model specified for that variant. Variants still holding the factory's universal default (when the model said otherwise): N at <code><variantIds></code>. This catches "every icon shipped as the same placeholder" โ€” the technical wiring (property exists, real wrapper default) is correct but the per-variant differentiation (<code>complete โ†’ check</code>, <code>error โ†’ priority_high</code>, etc.) was never baked in via <code>setProperties</code>. The model file under <code>.cursor/state/component-factory/<componentId>.md</code> is the source of truth for "what each variant should default to."</p> </li> <li><p><strong>Tone-bearing adornment coverage (content vs control split).</strong> For a component with a <code>Tone</code> / status / state axis, walk each variant and check color-carrying adornments by role. <strong>Content / status adornments</strong> (status dot, leading / secondary icon glyph, trend icon + text, accent bar) MUST resolve to that variant's tone family (Neutral โ†’ <code>*-secondary</code>, Disabled โ†’ <code>*-disabled</code>); content icons NOT following tone in Error / Disabled / Read-only: N at <code><variantIds></code>. <strong>Control affordances</strong> (chevron, clear / dismiss, stepper caret) follow usability not validity โ€” they MUST be <code>icon-disabled</code> on Disabled, but stay neutral / <code>icon-interactive</code> on Error (a red chevron is the bug, not the fix; do NOT flag it). Control affordances still <code>icon-interactive</code> on a Disabled variant: N at <code><variantIds></code> (the combobox 2026-06-08 miss โ€” every icon pinned to <code>icon-interactive</code> across all states). See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง6 and ยง2 check 6 (extends the per-variant INSTANCE_SWAP gate to color).</p> </li> <li><p><strong>Nested-instance exposure coverage.</strong> Every nested DS instance the model marked as designer-reachable (B.2.6) MUST have <code>isExposedInstance = true</code> in EVERY variant. Instantiate the set and confirm <code>instance.exposedInstances</code> lists each one. Designer-reachable nested instances missing exposure: N at <code><variantIds/names></code>. Catches the partial-coverage trap โ€” the primary icon exposed but the overflow menu / trend icon left unexposed (the Metric Card miss). <strong>Proactive half โ€” sealed controls (don't trust the model).</strong> Also walk every nested instance independently and flag any <strong>control-bearing</strong> one (a first-class <code>udc-*</code>, or a <code>_udc-<id>_*</code> subpart with its own variant axis / editable TEXT / meaningful BOOLEAN / INSTANCE_SWAP) that is NEITHER <code>isExposedInstance=true</code> NOR has its props hoisted to the host: sealed control-bearing instances: N at <code><nodeIds></code>. The global-search 2026-06-09 build passed every gate while its trigger (State/Filled/placeholder/value) and popover (content) were sealed โ€” only <code>Open</code> was reachable. Soft carve-out: a purely decorative / structural nested instance is a review note, not a fail. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง2 check 7.</p> </li> <li><p><strong>Behavioral props captured.</strong> Every behavioral prop the model lists (props that change runtime behavior but have no drawable Figma property โ€” <code>selectable</code>, <code>href</code>, etc.) MUST appear in the contract block's <code>Props (behavioral, non-drawable)</code> section. A modelled behavioral prop absent from the contract block: fail. Conversely, a no-op Figma BOOLEAN registered for a behavioral prop (a BOOLEAN with no <code>visible</code> reference) fails the property-wiring gate above โ€” record it in the contract instead.</p> </li> <li><p><strong>Focus / ring construction.</strong> Any Focus (or ring-style) variant MUST contain an offset <code>focus-outline</code> ring (absolute, gapped, <code>outline-focus-visible</code> stroke), not merely a thickened border on the element root. Focus variants whose only change from Default is a heavier / recolored root border: N. <strong>Find the ring candidate by its focus-bound stroke or a <code>focus</code>-prefixed empty-fill stroked frame โ€” NOT by requiring <code>ABSOLUTE</code> position</strong>, or a mis-built in-layout ring reads as "0 rings" and the variant falsely passes (the toggle 2026-06-09 <code>focus-wrapper</code> was <code>AUTO</code>-positioned and slipped an absolute-only finder). <strong>Then verify the ring is built to contract โ€” "resizing" and "unclipped" are properties to check, not words to assert</strong> (the combobox 2026-06-08 build shipped a <code>MIN</code>/<code>MIN</code>, clipped ring that passed the old presence-only check):</p> <ul> <li><code>constraints={horizontal:'STRETCH', vertical:'STRETCH'}</code> always (the resize guarantee). Rings pinned <code>MIN</code>/<code>CENTER</code>/<code>MAX</code> (don't resize with the box): N at <code><ringIds></code>. <code>layoutPositioning='ABSOLUTE'</code> only when the ring's parent is an auto-layout frame; a ring in a plain-frame box (e.g. the toggle track) is correctly <code>AUTO</code>+STRETCH โ€” don't flag it.</li> <li>Negative inset (<code>x<0</code>, <code>y<0</code>, size โ‰ˆ box + 2ยท|inset|): rings that add to the box footprint instead of overlaying outside it: N.</li> <li>Unclipped ancestor chain โ€” walk ring โ†’ element box โ†’ variant โ†’ component set; any ancestor with <code>clipsContent=true</code> shaves the ring: N at <code><nodeIds></code>.</li> <li>Ring parented to the focused element box (bordered field/trigger), not the variant wrapper: violations: N.</li> </ul> <p>See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง1 and ยง2 (check 8) and <a href="../../rules/uds-design-language.mdc"><code>uds-design-language.mdc</code></a> ยง6.</p> </li> <li><p><strong>Layer hygiene.</strong> Unnamed nodes: N. Generic names (<code>Frame N</code>, <code>Rectangle N</code>): N. Orphan top-level nodes on the page: N.</p> </li> <li><p><strong>Auto-layout coverage.</strong> Frames without auto-layout: N at <code><nodeIds></code>.</p> </li> <li><p><strong>Subcomponent visibility.</strong> Every component set the model classified as a subcomponent must be named with a leading <code>_</code>. Subcomponents named without <code>_</code>: N. Inversely, main components named with a leading <code>_</code> (would be hidden from the picker by mistake): N. The model's "Subcomponent classification" entry is the source of truth โ€” if the file disagrees, that's a gate failure.</p> </li> <li><p><strong>Library reuse.</strong> For every icon / avatar / swappable adornment the model declared in its INSTANCE_SWAP list, the file must contain an INSTANCE node of the agreed wrapper (or library primitive) at the documented anatomy location. Heuristic gap detection: text nodes whose <code>characters</code> is a single non-ASCII glyph (Unicode code point > U+0080, or any single character that isn't a digit / letter / common punctuation) when a wrapper component for that category exists in the subscribed libraries are flagged as candidate gaps. Designer judges whether each is intentional decoration vs. a missed icon-wrapper instance. <strong>Also flag raw rebuilds of an existing DS component:</strong> a field label drawn as a raw text node (+ a sibling required-dot) where <code>udc-label</code> exists, an inline button drawn as a raw frame where <code>udc-button</code> exists, etc. Any sub-part a published UDS component already covers MUST be a nested instance of that component, not raw nodes โ€” per the Phase A "Sibling reuse" rule. For field labels, nesting is necessary but not sufficient โ€” the nested <code>udc-label</code> must also forward its properties (caught by the Label-forwarding gate below).</p> </li> <li><p><strong>Label nesting + forwarding (form/labeled components).</strong> For any component whose class is a labeled field or control, the field label MUST be a nested <code>udc-label</code> instance whose properties are forwarded to the consumer. Three findings: (1) field labels drawn as raw TEXT (or text + required-dot) where <code>udc-label</code> exists: N. (2) nested <code>udc-label</code> instances that are NEITHER <code>isExposedInstance=true</code> NOR have their key props (<code>text</code>, <code>required</code>, leading-icon, right-slot) hoisted to the host set โ€” so the consumer can't edit them: N at <code><list></code>. (3) nested labels whose <code>tone</code> doesn't track host state (Error variant not driving <code>tone=Error</code>, Disabled not <code>tone=Disabled</code>): N. Exempt the standalone <code>udc-label</code> set itself. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง9. This is the gate that catches BOTH the original combobox (nested-but-unforwarded โ€” no editable label props) and the host-owned overcorrection (raw text that threw the props away).</p> </li> <li><p><strong>Field popover overlay (field-with-menu components).</strong> For any component whose class opens a menu / listbox / popover (combobox, select, dropdown, autocomplete, date-picker), every open variant's open surface MUST be an overlay anchored to the field, not an in-flow sibling after the helper row. Open menus that are in-flow auto-layout children (<code>layoutPositioning !== 'ABSOLUTE'</code>) sitting after the helper, or anchored with a gap below the field's bottom edge: N at <code><variantIds></code>. The combobox 2026-06-08 open state wedged the helper between the field and its menu โ€” it failed this. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง10 and ยง2 check 10.</p> </li> <li><p><strong>Field-metrics conformance (field/control components).</strong> For any component whose class is a field or form control, verify it matches the design-language field-metrics contract: alignment/touch container = contract height (48 px for UDS) with the visible control centered. Controls whose container is shorter than the contract (the ~24 px checkbox / ~32 px toggle โ€” breaks single-line alignment + the 44 px touch floor), or whose visible box is stretched to the container height (a giant checkbox): N at <code><variantIds></code>. Label not at the contract label style, helper not at the contract helper style, or label/helper gaps off the contract gap: N. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง11 and ยง2 check 11, and <a href="../../rules/uds-design-language.mdc"><code>uds-design-language.mdc</code></a> ยง10.</p> </li> <li><p><strong>Action-pair order + alignment (Cancel + primary pairs).</strong> For any component that nests a confirm / dismiss button pair (Cancel + Save / Apply / Confirm โ€” e.g. an inline editor, a dialog footer), check two things. (a) Order: the primary (Save / Apply / Confirm) is the right-most button, the dismiss (Cancel, ghost / secondary) to its left. Pairs with the primary left of the dismiss: N at <code><nodeIds></code>. (b) Alignment: when the pair's row sits under a full-width control, the row MUST <code>layoutSizingHorizontal='FILL'</code> the control's width and right-align (<code>primaryAxisAlignItems='MAX'</code>) so the primary's right edge meets the control edge. Pair rows left in <code>HUG</code> + <code>MIN</code> under a wider sibling (primary floats mid-width): N at <code><nodeIds></code>. The data-field 2026-06-10 inline editor shipped both faults (Save on the left; a 248px hug row under a 260px input) โ€” caught by eye, not a gate, which is why this is now enumerated. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง12.</p> </li> <li><p><strong>Class-required state coverage.</strong> Every state the component's class requires (per <a href="../../rules/uds-component-checklist.mdc"><code>uds-component-checklist.mdc</code></a> ยง"State baseline") MUST be present as a Figma variant OR marked <code>notApplicable</code> with a reason in the contract block. Required states with neither a variant nor a <code>notApplicable</code> reason: N at <code><list></code>. This is the gate that stops a <code>form</code> draft shipping without <code>error</code> / <code>required</code>, or a disclosure control without <code>expanded</code> / <code>collapsed</code>. <strong>When a <code>Kind</code> / <code>Type</code> / <code>Mode</code> axis is present, reason per (Kind ร— State) cell, not globally:</strong> a required state satisfied for one interactive Kind but missing for another interactive Kind FAILS, the same as if it were missing entirely. Report per Kind: <code><Kind></code> missing <code><state></code>. This is what catches the nav-header 2026-06-16 gap (parents missing <code>Selected</code> / <code>Disabled</code> while leaves carried the full set) โ€” "the State axis contains it somewhere" is not coverage. See <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง"State coverage is per interactive Kind, not global".</p> </li> <li><p><strong>Events planned.</strong> For any class that dispatches events (<code>action</code>, <code>form</code>, <code>feedback</code>, <code>navigation</code>, โ€ฆ), the contract block's <code>Events</code> section MUST list at least one event or the explicit <code>none (class does not dispatch)</code>. An empty <code>Events</code> section on a class that should have them: fail.</p> </li> <li><p><strong>Contract block present and well-formed.</strong> The component <code>descriptionMarkdown</code> MUST contain a <code><<UDS-FACTORY-CONTRACT v1>> โ€ฆ <<END-UDS-FACTORY-CONTRACT>></code> block with all sections present (Summary, Depends on, Variant axes, Exposed properties, Class, Props, Events, Slots, Parts, States, Keyboard, Screen reader, Acceptance) โ€” each either filled or explicitly <code>none</code> / <code>notApplicable</code>. Missing or malformed: fail. A block missing <code>Summary</code> or <code>Depends on</code> (added in 2026.06.09.7) or <code>Exposed properties</code> (added in 2026.06.09.8) fails. The <code>spec.json</code> round-trip via <code>uds-updated</code> depends on this block. First-class components only โ€” internal <code>_udc-<id>*</code> subparts are exempt (documented inside the parent's block; see the contract rules above).</p> </li> <li><p><strong>Slots and parts enumerated.</strong> The contract block's <code>Slots</code> and <code>Parts</code> sections must each be filled or explicitly <code>none</code>. An empty (not <code>none</code>) section is a gap.</p> </li> <li><p><strong>Contract currency (block matches the live component).</strong> The machine-checkable sections MUST agree with the component's actual anatomy โ€” see <a href="../../rules/uds-figma-factory-quality.mdc"><code>uds-figma-factory-quality.mdc</code></a> ยง2 check 12. <code>Variant axes</code> lines must match <code>componentPropertyDefinitions</code> (every axis, every value โ€” the metric-card <code>Hover</code> vs <code>Hovered</code> drift fails here); <code>States</code> baseline must cover the State axis options; <code>Parts</code> must name real regions; <code>Depends on</code> (nested half) must match the nested <code>udc-*</code> instances; the date in the <code>Factory-version: F# (date)</code> line must equal the <code>factory_version</code> plugin-data stamp (F# is the derived display index). Any mismatch = stale contract = fail. The prose sections (Summary, behavioral Props, Events, Keyboard, Screen reader, Acceptance) can't be auto-verified โ€” their currency rides on the "Regenerate the WHOLE block on ANY touch" rule in the contract section and <a href="../../rules/uds-factory-versioning.mdc"><code>uds-factory-versioning.mdc</code></a>.</p> </li> </ul> <p>If any tool-emitted gate fires with non-zero findings, the skill reports the issue and proposes a fix. Design-changing, destructive, or token-creating fixes require explicit approval before applying.</p> <h3>Human-judged gates (Cursor flags; designer decides)</h3> <ul> <li><strong>State coverage.</strong> Are all states present and visually distinguishable?</li> <li><strong>Accessibility plan.</strong> Is the documented keyboard / focus / SR behavior plausible and complete?</li> <li><strong>Visual direction.</strong> Does the draft match the intended UDS feel?</li> </ul> <h3>Review-ready definition</h3> <p>The factory job is complete when:</p> <ul> <li>Every tool-emitted gate above reports zero findings, AND</li> <li>The human-judged gates have been written into a designer-facing prompt block (one paragraph per gate; no decision required to finish โ€” the designer reads them as part of acceptance).</li> </ul> <p>Production-ready is a higher bar that happens later, after designer rename + the eventual <code>uds-updated</code> run. The factory does not chase it.</p> <h2>Phase D โ€” Designer hand-off + enrollment in maintenance</h2> <p>When the designer accepts the draft (renames the page to drop <code>{Cursor}{Ignore}</code>), the factory's DRAFT job is done โ€” and the component is now a live library member, enrolled in review-gated maintenance (see "Maintenance mode" below). What happens next is NOT the draft skill's responsibility:</p> <ul> <li>The designer renames the page in <code>UDS Components</code> to drop <code>{Cursor}{Ignore}</code> and update the stoplight prefix to whatever status they want (<code>๐ŸŸ </code> in-progress, <code>๐ŸŸก</code> review, <code>๐ŸŸข</code> production).</li> <li>When the designer is ready, they run <a href="../uds-updated/SKILL.md"><code>uds-updated</code></a> (or equivalent prompt: "UDS updated", "Figma updated", "sync UDS from Figma"). That workflow handles <code>new-component</code> scaffold, <code>sync-figma-component-spec</code>, <code>link-figma-nodes</code>, status sync, changelog, commit, and push. (Cloudflare and Next handle cache freshness post-migration โ€” there's no cache-bust step.)</li> <li>Optionally, the designer may run <a href="../figma-component-card/SKILL.md"><code>figma-component-card</code></a> to build the seven-section page layout in Figma. Independent of this factory.</li> </ul> <p><strong>Docs-side hint about subcomponents.</strong> When the page contains a main component plus one or more <code>_</code>-prefixed subcomponents, the docs-side scaffold (<code>new-component</code> + <code>components.json</code> aggregator) should land <strong>only</strong> the main component โ€” the subcomponent's anatomy, props, states, and accessibility live inside the parent's <code>spec.json</code>. The subcomponent does not get its own <code>uds-docs/uds/components/<sub>/</code> folder, its own <code>components.json</code> entry, its own <code>status.json</code>, its own <code>changelog.json</code>, or its own Storybook story. If <code>uds-updated</code> behaves as if a subcomponent were a peer, surface that as a finding; fix is in <code>uds-updated</code> / <code>new-component</code>, not here.</p> <p>The model proposal at <code>.cursor/state/component-factory/<componentId>.md</code> can be deleted once the component has landed in docs, or left in place โ€” cleanup is optional, since <code>.cursor/state/</code> is gitignored.</p> <h2>Maintenance mode โ€” keeping live components current</h2> <p>Once a component is live (accepted: no <code>{Cursor}</code>, no <code>{Ignore}</code>), the factory can help keep it current as the factory's output bar moves. This is the consumer side of <a href="../../rules/uds-factory-versioning.mdc"><code>uds-factory-versioning.mdc</code></a>.</p> <p>Separate two things that get conflated: <strong>surfacing</strong> drift vs <strong>applying</strong> an upgrade.</p> <ul> <li><strong>Surfacing is automatic.</strong> Any time you inspect or are asked to work on an existing component โ€” this maintenance mode, OR resuming a <code>{Cursor}</code> draft in the default mode โ€” report its drift verdict first (pre-flight step 8): built vintage, current bar, behind-by-N, the relevance-filtered list of what applies. You don't wait to be asked "is it stale?" โ€” you say so up front, every time.</li> <li><strong>Applying is deliberate and gated.</strong> The UPGRADE itself is invoked on purpose โ€” e.g. "upgrade metric-card to the current factory bar", or as a follow-up to a <a href="../../agents/figma-inventory.md"><code>figma-inventory</code></a> factory-version drift report โ€” and nothing is written to a live component without per-change approval. Surface always; apply only on the mode's terms.</li> </ul> <p>The loop reuses the existing inspect โ†’ classify โ†’ propose โ†’ approve pattern, pointed in the Figma-write direction:</p> <ol> <li><strong>Inspect</strong> the live component with <a href="../../agents/figma-component-inspector.md"><code>figma-component-inspector</code></a>: current structure, token bindings, contract block, and the <code>factory_version</code> stamp.</li> <li><strong>Diff</strong> the stamp against <code>.cursor/figma/state/factory-version.json</code>: pull the newer changelog entries whose <code>affects[]</code> labels match the component's anatomy. Each is a concrete, scoped change.</li> <li><strong>Propose every change for approval โ€” nothing on a live component auto-applies.</strong> A live page is gated (write-safety <strong>scope #5</strong>): the factory writes nothing without explicit per-change approval. The <code>breaking</code> flag only changes how a change is presented โ€” additive (<code>breaking:false</code>) is a plain "apply this?"; <code>breaking:true</code> stops and flags the risk it could invalidate hand-edits. Both wait for approval. Where the live component diverges from what the factory would produce (a designer hand-edit), surface it; never overwrite it.</li> <li><strong>Apply on approval</strong>, writing only the approved scoped change to the live page โ€” NOT a free rebuild. Emit the standard write summary (write-safety ยง"Required before/after report").</li> </ol> <h3>Stamp-only catch-up for a pre-versioning draft</h3> <p>The most common drift case is NOT a behind-the-bar rebuild โ€” it's an <strong>unstamped <code>{Cursor}</code> draft</strong> built before the stamping machinery existed (no <code>factory_version</code> plugin data, no <code>Factory-version:</code> contract line) that already meets the current bar in substance. Forcing a from-scratch teardown there is wasteful and risks dropping capability (<a href="../../rules/uds-source-of-truth.mdc"><code>uds-source-of-truth.mdc</code></a> โ€” never silently rewrite a build that's already correct).</p> <p>For such a draft, run the maintenance audit (inspect โ†’ diff) and pick the lightest sufficient action:</p> <ul> <li><strong>Already compliant</strong> โ€” every applicable bar item passes (focus-ring construction, per-variant tone rebinding, nested-instance exposure, secondary-affordance emphasis/glyph, state-vs-kind hygiene, behavioral-prop contract): do a <strong>targeted stamp-and-verify</strong>. Write the <code>factory_version</code> + <code>built_at</code> plugin data (B.3.6) and insert the <code>Factory-version:</code> line into the contract block (B.3.5), then re-run B.5 verification. No geometry, token, or variant changes.</li> <li><strong>Partially behind</strong> โ€” apply only the scoped fixes for the failing items, then stamp.</li> </ul> <p>Both are <code>{Cursor}</code> scratch writes (scope #4, free) โ€” surgical, not a rebuild. Only fall back to a full regenerate when the audit shows the draft is structurally behind or the designer explicitly asks for one. Run the audit with the host-vs-nested attribution and structural ring-detection guards (<a href="../../rules/uds-figma-plugin-api-gotchas.mdc"><code>uds-figma-plugin-api-gotchas.mdc</code></a> ยง14โ€“ยง15) so a nested component's own parts aren't misread as draft cruft.</p> <p><strong>Excluded:</strong> a <code>{Frozen}</code> / <code>{NoFactory}</code> page is reported as behind but never written โ€” the designer has marked it hands-off (<code>uds-figma-preflight.mdc</code>). An un-accepted <code>{Cursor}</code> draft uses the free regenerate-for-review path instead (it's scratch, not gated). The <code>{Cursor}</code> free-write grant (scope #4) remains scratch-only; maintenance writes to live pages are the gated scope #5.</p> <h2>Procedure (single-component draft, the default mode)</h2> <ol> <li><strong>Pre-flight</strong> (above).</li> <li><strong>Phase A</strong> โ€” read brief + siblings + tokens, persist model to <code>.cursor/state/component-factory/<componentId>.md</code>, present inline, wait for <code>approved</code>.</li> <li><strong>Phase B</strong> โ€” sequential <code>use_figma</code> calls: B.1 page, B.2 component set, B.4 write summary, B.5 verification.</li> <li><strong>Phase C</strong> โ€” emit the quality-gate report.</li> <li><strong>Phase D</strong> โ€” hand off to the designer; do NOT rename the page; do NOT run any docs-side skill; do NOT touch <code>uds-docs/uds/</code>.</li> </ol> <p>If the designer says "iterate" / "revise" after Phase C, return to Phase B with the requested changes and re-run B.5 + Phase C. Do not rebuild the page from scratch unless the designer explicitly asks (per locked decision #2 above).</p> <h2>Output principles</h2> <ul> <li><strong>Single source of truth for the design model is the persisted <code>.cursor/state/component-factory/<componentId>.md</code> file.</strong> If conversation context is truncated, re-read the file before resuming.</li> <li><strong>Variables MUST be re-imported every <code>use_figma</code> call.</strong> See the <a href="../figma-component-card/references/gotchas.md"><code>figma-component-card</code> gotchas</a>.</li> <li><strong>Never write raw hex into bound paints' <code>color</code> field unless you also bind it.</strong></li> <li><strong>Never modify any file under <code>uds-docs/uds/</code>.</strong> The factory's output is Figma-only.</li> <li><strong>Every Figma write must be paired with a write-summary report.</strong> See <a href="../../rules/uds-figma-write-safety.mdc"><code>uds-figma-write-safety.mdc</code></a>.</li> </ul> <h2>DO NOT</h2> <ul> <li><strong>Don't write to <code>UDS Components</code></strong> outside the new <code>{Cursor}{Ignore}</code> page. Other pages are not in scope #4.</li> <li><strong>Don't write to <code>UDS Tokens</code></strong> at all. Token additions flow through the UDS Tokens Figma file (designer-side) and <a href="../import-figma-tokens/SKILL.md"><code>import-figma-tokens</code></a>.</li> <li><strong>Don't auto-rename the page</strong> to drop <code>{Cursor}{Ignore}</code> or to change the stoplight. That's the designer's acceptance gesture.</li> <li><strong>Don't ship a draft with an incomplete behavior contract for a class that requires one.</strong> A <code>form</code> component with no <code>error</code> / <code>required</code> states, an <code>action</code> / <code>feedback</code> component with no events, or any draft missing the B.3.5 contract block is incomplete โ€” fix it before Phase D, or record an explicit <code>notApplicable</code> reason in the contract.</li> <li><strong>Don't run <a href="../new-component/SKILL.md"><code>new-component</code></a>, <a href="../sync-figma-component-spec/SKILL.md"><code>sync-figma-component-spec</code></a>, <a href="../link-figma-nodes/SKILL.md"><code>link-figma-nodes</code></a>, or any <code>uds-docs/uds/</code> writer skill</strong> as part of this workflow. Docs landing is the designer's separate <code>uds-updated</code> invocation.</li> <li><strong>Don't proceed past Phase A without the literal <code>approved</code></strong> (or "approved with: โ€ฆ"). "Looks good" is not approval.</li> <li><strong>Don't parallelize <code>use_figma</code> calls.</strong> Sequential only, per <code>figma-generate-library</code> rule 13.</li> <li><strong>Don't invent tokens.</strong> If the model's token plan needs something that isn't in UDS Tokens, STOP and ask the user to add it via the UDS Tokens Figma file + <code>import-figma-tokens</code>.</li> <li><strong>Don't remove the <code>{Cursor}</code> or <code>{Ignore}</code> tags from a page.</strong> That rename is the designer's acceptance gesture and revokes Cursor's standing write grant (locked decision #5). A pre-existing <code>{Cursor}{Ignore}</code> page may be rebuilt without asking โ€” inspect it and report your plan first.</li> <li><strong>Don't skip the Phase B.5 verification.</strong> The quality-gate report in Phase C does not run on unverified work.</li> </ul> <h2>See also</h2> <ul> <li><code>figma-use</code> (active Figma plugin skill) โ€” Plugin API rules.</li> <li><code>figma-generate-library</code> (active Figma plugin skill) โ€” state ledger, sequential rule, Phase 3 component pattern.</li> <li><a href="../figma-component-card/SKILL.md"><code>figma-component-card</code></a> โ€” sibling Figma writer skill (writes the page-layout cards). Pairs with this factory after the docs scaffold exists.</li> <li><a href="../uds-updated/SKILL.md"><code>uds-updated</code></a> โ€” designer-initiated follow-on that lands the accepted draft into the docs site.</li> <li><a href="../new-component/SKILL.md"><code>new-component</code></a> โ€” docs-side scaffold the <code>uds-updated</code> workflow calls. Not invoked by this factory.</li> <li><a href="../../rules/uds-figma-preflight.mdc"><code>uds-figma-preflight.mdc</code></a> โ€” preflight discovery requirements.</li> <li><a href="../../rules/uds-figma-write-safety.mdc"><code>uds-figma-write-safety.mdc</code></a> โ€” write scopes (this factory's writes are scope #4).</li> <li><a href="../../rules/uds-token-architecture.mdc"><code>uds-token-architecture.mdc</code></a> โ€” token vocabulary contract.</li> <li><a href="../../rules/uds-source-of-truth.mdc"><code>uds-source-of-truth.mdc</code></a> โ€” why the factory stops at Figma.</li> <li><a href="../../rules/uds-rule-discipline.mdc"><code>uds-rule-discipline.mdc</code></a> โ€” bookkeeping when this skill itself is edited.</li> </ul> </article> </div> <!-- Right: Metadata & Command Sidebar --> <div class="w-full lg:w-80 shrink-0 flex flex-col gap-6" data-astro-cid-7zzsworf> <!-- Install Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-mono" data-astro-cid-7zzsworf>Install via CLI</span> <div class="flex flex-col gap-2" data-astro-cid-7zzsworf> <div id="detail-install-cmd" class="font-mono text-[11px] p-3 rounded-lg bg-black/40 border border-border select-all break-all text-primary font-bold leading-relaxed" data-astro-cid-7zzsworf> npx skills add https://github.com/sbajwa32/uds-docs --skill generate-uds-figma-component </div> <button id="detail-copy-btn" class="w-full py-2.5 rounded-lg bg-primary hover:bg-primary-hover text-on-primary font-sans font-bold text-sm shadow transition-all active:scale-95 flex items-center justify-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px]" data-astro-cid-7zzsworf>content_copy</span> <span data-astro-cid-7zzsworf>Copy Command</span> </button> </div> </div> <!-- Details & Stats Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm text-on-surface" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>Repository Details</span> <div class="flex flex-col gap-3.5" data-astro-cid-7zzsworf> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>star</span> Stars </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>1</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>call_split</span> Forks </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>0</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>navigation</span> Branch </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant" data-astro-cid-7zzsworf>main</span> </div> <div class="flex justify-between items-start text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5 mt-0.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>article</span> Path </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant truncate max-w-[150px]" title="SKILL.md" data-astro-cid-7zzsworf>SKILL.md</span> </div> </div> </div> <!-- Occupations Tag Card --> <!-- Related Creators Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-3 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>More from Creator</span> <div class="flex items-center gap-2" data-astro-cid-7zzsworf> <img class="w-8 h-8 rounded-full border border-border" src="https://avatars.githubusercontent.com/u/170023165?u=faecfb7a54004ea5ed1ceab526452817ccd63c81&v=4" alt="sbajwa32" onerror="this.src='https://avatars.githubusercontent.com/u/9919?v=4'" data-astro-cid-7zzsworf> <div class="flex flex-col min-w-0" data-astro-cid-7zzsworf> <span class="font-bold text-sm truncate text-on-surface" data-astro-cid-7zzsworf>sbajwa32</span> <a href="/?creator=sbajwa32" class="text-xs text-primary hover:underline font-semibold transition-all" data-astro-cid-7zzsworf>Explore all skills →</a> </div> </div> </div> </div> </div> </div> </div> <script> const copyBtn = document.getElementById("detail-copy-btn"); const installCmd = document.getElementById("detail-install-cmd"); if (copyBtn && installCmd) { copyBtn.addEventListener("click", () => { const cmd = installCmd.textContent.trim(); navigator.clipboard.writeText(cmd).then(() => { const originalText = copyBtn.innerHTML; copyBtn.innerHTML = ` <span class="material-symbols-outlined text-[16px]">check</span> <span>Copied!</span> `; copyBtn.style.background = "#10b981"; copyBtn.style.borderColor = "#10b981"; setTimeout(() => { copyBtn.innerHTML = originalText; copyBtn.style.background = ""; copyBtn.style.borderColor = ""; }, 1500); }); }); } </script> </div> <!-- Footer --> <footer class="border-t border-border bg-surface-container-low text-on-surface-variant py-8 px-gutter mt-16 rounded-xl"> <div class="max-w-container-max mx-auto flex flex-col md:flex-row justify-between items-center gap-6"> <div class="flex items-center gap-2"> <div class="w-6 h-6 rounded bg-primary bg-opacity-20 flex items-center justify-center"> <span class="material-symbols-outlined text-primary text-sm">code_blocks</span> </div> <span class="font-bold text-on-surface text-sm">SkillMD</span> </div> <div class="flex flex-wrap justify-center gap-6 text-sm"> <a href="/about" class="hover:text-primary transition-colors">About Us</a> <a href="/contact" class="hover:text-primary transition-colors">Contact Us</a> <a href="/privacy" class="hover:text-primary transition-colors">Privacy Policy</a> <a href="/terms" class="hover:text-primary transition-colors">Terms of Service</a> <a href="/support" class="hover:text-primary transition-colors">Support</a> </div> <div class="text-xs text-on-surface-variant/80"> © 2026 SkillMD. All rights reserved. </div> </div> </footer> </main> <!-- Script for Theme Toggle, Mobile Menu, and Sidebar Filter Redirection --> <script> // Theme setup const savedTheme = localStorage.getItem("theme") || "dark"; function applyTheme(theme) { document.documentElement.classList.remove("dark", "green", "dracula", "nord"); if (theme === "dark") { document.documentElement.classList.add("dark"); } else if (theme === "green") { document.documentElement.classList.add("dark", "green"); } else if (theme === "dracula") { document.documentElement.classList.add("dark", "dracula"); } else if (theme === "nord") { document.documentElement.classList.add("dark", "nord"); } document.documentElement.setAttribute("data-theme", theme); const themeMoon = document.getElementById("theme-moon"); const themeSun = document.getElementById("theme-sun"); const themeLeaf = document.getElementById("theme-leaf"); const themeDracula = document.getElementById("theme-dracula"); const themeNord = document.getElementById("theme-nord"); if (themeMoon && themeSun && themeLeaf && themeDracula && themeNord) { themeMoon.style.display = theme === "dark" ? "inline" : "none"; themeSun.style.display = theme === "light" ? "inline" : "none"; themeLeaf.style.display = theme === "green" ? "inline" : "none"; themeDracula.style.display = theme === "dracula" ? "inline" : "none"; themeNord.style.display = theme === "nord" ? "inline" : "none"; } } applyTheme(savedTheme); const themeToggleBtn = document.getElementById("theme-toggle-btn"); if (themeToggleBtn) { themeToggleBtn.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme") || "dark"; let newTheme = "dark"; if (currentTheme === "dark") { newTheme = "light"; } else if (currentTheme === "light") { newTheme = "green"; } else if (currentTheme === "green") { newTheme = "dracula"; } else if (currentTheme === "dracula") { newTheme = "nord"; } else { newTheme = "dark"; } applyTheme(newTheme); localStorage.setItem("theme", newTheme); }); } // Mobile menu toggle and sidebar logic const mobileMenuToggle = document.getElementById("mobile-menu-toggle"); const sidebarMenu = document.getElementById("sidebar-menu"); const sidebarOverlay = document.getElementById("sidebar-overlay"); function isMobile() { return window.innerWidth < 768; // 768px is the 'md' breakpoint in Tailwind } function openSidebar() { if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.remove("hidden"); } } function closeSidebar() { if (sidebarMenu && isMobile()) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } if (mobileMenuToggle && sidebarMenu) { mobileMenuToggle.addEventListener("click", (e) => { e.stopPropagation(); if (isMobile()) { const isClosed = sidebarMenu.classList.contains("-translate-x-full"); if (isClosed) { openSidebar(); } else { closeSidebar(); } } }); document.addEventListener("click", (e) => { if (isMobile()) { if (!sidebarMenu.contains(e.target) && !mobileMenuToggle.contains(e.target)) { closeSidebar(); } } }); if (sidebarOverlay) { sidebarOverlay.addEventListener("click", () => { if (isMobile()) { closeSidebar(); } }); } // Collapse sidebar when clicking a filter button, creator button, or nav item inside it sidebarMenu.addEventListener("click", (e) => { if (isMobile()) { const clickTarget = e.target.closest("button, a"); if (clickTarget) { closeSidebar(); } } }); // Sync sidebar state on window resize window.addEventListener("resize", () => { if (!isMobile()) { // Desktop: sidebar should be visible, no overlay if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } else { // Mobile: start collapsed if (sidebarMenu) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } }); } // If not on homepage, redirect on sidebar filter click const isHomepage = window.location.pathname === "/"; document.querySelectorAll("#occupation-filters .filter-btn").forEach(btn => { btn.addEventListener("click", (e) => { const occ = e.currentTarget.getAttribute("data-occupation"); if (!isHomepage) { window.location.href = occ ? `/?occupation=${encodeURIComponent(occ)}` : "/"; } }); }); document.querySelectorAll("#creator-filters .creator-btn").forEach(btn => { btn.addEventListener("click", (e) => { const creator = e.currentTarget.getAttribute("data-creator"); if (!isHomepage) { window.location.href = `/?creator=${encodeURIComponent(creator)}`; } }); }); </script> </body> </html>