How Figma Dev Mode shows real @prepared911/ui snippets, where the templates live, and the check that keeps them honest.
@prepared911/ui component and props. The reference file is the Axon 911 Design System in Figma. A Figma component set maps to a code component only when its properties mirror the code props: same names, same values, same defaults.
Each template sits beside the component it maps and is owned by the same team.
| Unit | Template |
|---|---|
| Component | packages/ui/src/components/<id>/<id>.figma.ts |
| Block | packages/ui/src/blocks/<id>/<id>.figma.ts |
figma.code template. It starts with three header lines and ends with a default export that has an id:
pnpm --filter @prepared911/ui code-connect:check runs in ci:gates and check. It fails when:
scripts/code-connect/no-figma-counterpart.json and is not in scripts/code-connect/unmapped-baseline.json;source or component does not resolve, a header line is missing, or the url is outside the Design System file;Contents, Portal, VisuallyHidden), has no Figma counterpart to map. It goes in scripts/code-connect/no-figma-counterpart.json, one id per entry with a written reason, and never in the baseline.
One rule per kind of axis, so every template reads the same way. Read each Figma property once, at the top of the template, with the getter that matches its Figma type.
| Figma property | Template |
|---|---|
BOOLEAN | getBoolean("Loading") |
VARIANT whose options are False and True (boolean as a variant: Disabled, Icon only) | getEnum("Disabled", { False: false, True: true }). getBoolean is for BOOLEAN properties only; the drift check flags it on a variant. The axis is named for the code prop in sentence case. When there is no such prop, the axis picks a branch of a union (Icon only picks icon plus label instead of children) and the template says so in a constant. |
VARIANT with a third option (Toggle: None, Off, On) | getEnum to lowercase words ("none", "off", "on"), then derive the props from the words. |
VARIANT that is an enum in code (Intent, Variant, Size) | getEnum with one entry per Figma option, mapped to the enum member ("ButtonIntent.Accent"). |
TEXT, SLOT | getString, getSlot. Copy is a copy("...") placeholder, never the Figma text (ADR 0009). |
State, Scene, Story) exist so Figma can draw a look the browser makes by itself. Never invent a prop for one. Apply the first rule that fits:
State=Pressed emits active.// line at the top of the snippet that names the pseudo-class, and no prop. Button State=Hover and State=Focus do this.Scene): do not read it, and list it in the header as // story-only=Scene.// retired=Show label,Pressed. The drift check then stops asking for a mapping and reminds you to remove it from Figma. Anything that a snippet cannot express is written down in the component's table below.
2072:3) has these properties: Intent, Variant, Size, Disabled, State, Icon only, Toggle, Loading, Show leading, Show trailing, Label, Leading and Trailing. Show label and Pressed are retired: Icon only and Toggle replace them.
| Figma | Snippet |
|---|---|
Icon only=True | icon={<Leading />} and label={copy(...)}, no children, no leading or trailing |
Icon only=False | children, plus leading and trailing when Show leading and Show trailing are on |
Toggle=On | pressed and onPressedChange={handlePressedChange} |
Toggle=Off | pressed={false} and onPressedChange={handlePressedChange} |
Toggle=None | no toggle props |
State=Rest | nothing |
State=Pressed | active (data-pressed, no aria-pressed), except on a toggle, where active is not defined |
State=Hover | a comment: the browser paints :hover, Button has no prop that forces it |
State=Focus | a comment: the browser paints :focus-visible, Button has no prop that forces it |
Disabled=True | disabled |
Loading on | loading and loadingLabel={copy(...)}, except on a toggle, where loading is not defined |
tooltip, type, pressedIcon, defaultPressed, preventFocusOnPress, className and native attributes have no Figma property. Hover and Focus cannot be set in code at all. handlePressedChange and the copy keys are placeholders the developer fills in.
code-connect:check reads only the repository, so it cannot see Figma change under a template. code-connect:drift closes that gap:
getBoolean on a variant);getEnum map that skips an option the set gained, or names one it lost;// story-only= or // retired=.code-connect-drift workflow runs it every night and on demand with a Figma token read from the FIGMA_ACCESS_TOKEN repository secret, or the Figma repository secret when that one is empty, and a failed run is the alert. When neither secret is set the run skips with a warning that names both. It is not a pull request check and not a required check.
publish --dry-run does need one: it lists the files it would publish, then exits 1 with "Couldn't find a Figma access token". Publishing is a separate, manual step; see the Figma Code Connect decision record in packages/ui/docs/decisions/0030-figma-code-connect.md.