Skip to content

Figma vs the repo

Written by the doc-generator on 2026-10-16, alongside the intent records for the six components that read Completed in the registry — Button, Badge, Card, Switch, Progress and Empty State.

Each entry gives both sides and says which is which. None of them is resolved here. Resolving a disagreement between the design file and the code is a designer’s or an engineer’s decision, and this agent makes neither.

Figma, live over the MCP connection: the component description on each of the six component sets, and get_variable_defs on each node — 26:1665 (button), 17:1139 (Badge), 65:230 (card), 17:1201 (Toggle), 69:1151 (progress), 94:1862 (empty-state). All six sets resolve to the published library Lumo.Web.Component.V1.0.Complete.

The repo: src/components/<name>/, its stories, and the build report in docs/<name>.md.

Where the two agree there is no entry here. They agree on a great deal — most importantly, all six Figma component descriptions are quoted verbatim in the repo, five of them in both the component source and the build report.


A. The six “don’t use this” sentences point at components that do not exist

Section titled “A. The six “don’t use this” sentences point at components that do not exist”

The strongest pattern across all six, and it is one disagreement repeated. Every Figma description closes with a substitution — use X instead — and Figma has every one of those X’s as a published component set. The repo has none of them.

Component Figma says to use instead In Figma In src/components/
Badge Chip ✅ chip ❌
Switch Checkbox ✅ Checkbox ❌
Progress Spinner ✅ spinner ❌
Button (nothing named) — —
Card (nothing named — “don’t nest”) — —
Empty State (nothing named — “always give the action”) — —

Three of the six route a developer to a component this library cannot give them. spinner in turn routes on to skeleton, which is also unbuilt.

Button is the sharper case of the same thing. Its description says “Don’t use it for opening external link” and names no replacement — and there is no Link component in Lumo.Web either, only an Additional links set with no description at all. So for Button the question “then what?” is unanswered in both sources, not just in one.

Which is which: Figma is complete and the repo is not. This is a build backlog, not a contradiction — but it makes three shipped components’ documented advice unfollowable today.


B. Card: Figma documents six sub-components, the repo built three

Section titled “B. Card: Figma documents six sub-components, the repo built three”

The largest single divergence.

Figma component Built?
card/header ✅ cardHeader.jsx
card/footer ✅ cardFooter.jsx
card/layout/vertical ✅ cardLayoutVertical.jsx
card/media ❌
card/layout/horizontal ❌
card/layout/metric ❌

Each of the three missing ones has a written description in Figma saying what it is for. card/layout/horizontal is referenced by card/layout/vertical’s own description (“Don’t use it when the card leads with an image or chart; use card/layout/horizontal”), so the built component points at an unbuilt one.

docs/card.md lists the four files it built and does not record the three it did not.

Which is which: Figma has six, the repo has three, and the repo’s own build report does not note the absence.


C. Composition documented in Figma and not demonstrated in the repo

Section titled “C. Composition documented in Figma and not demonstrated in the repo”

Figma’s card/header is “an Action slot for a button, badge or avatar”, and card/footer is “a slot for the card’s own actions or meta”. The repo’s card.stories.jsx imports only Card and CardLayoutVertical and fills both slots with a dashed placeholder — it never puts a real Button or Badge there.

The same pattern holds for Badge: Figma documents five host components for a badge (card/header, card/layout/metric, table/cell, kanban/card, marketing/pricing-card), and badge.stories.jsx imports nothing but Badge. Four of those five hosts have no code at all.

The counter-example, and it is the good one. Empty State is the one place where both sources say the same thing and the repo proves it: emptyState.stories.jsx imports the real Button into the ↳Action slot, which is exactly what Figma’s empty slot and its “always give the action” ask for.

Which is which: Figma documents the pairings; the repo demonstrates one of them.


The Figma component set 17:1201 is named Toggle. The registry row, the repo folder, the exported identifier and this site all say Switch.

The repo’s reasons are recorded in docs/naming-conflicts.md: switch is a reserved word in JavaScript, and the folder follows the registry row.

Which is which: Figma says Toggle, everything else says Switch. Both are deliberate and they have not been reconciled.

A related trap, worth stating so nobody falls into it. Searching Figma for “Toggle” returns much longer, much more prescriptive Toggle documentation — DO / DON’T / ACCESSIBILITY / SPEC, label placement rules, four supported states, a 50×26px track with a 22px thumb. That belongs to other design systems in the same Figma account (Design System.Component In Progress, Component Library), not to Lumo.Web. Its geometry contradicts Lumo’s. None of it has been used in Lumo’s intent record.


E. Switch: Figma draws two variants, the repo ships six states

Section titled “E. Switch: Figma draws two variants, the repo ships six states”

Figma’s 17:1201 has one variant property, On, and nothing else. No hovered, pressed, focused, disabled, error or loading variant exists anywhere in the set.

The repo implemented hovered, pressed, focused and disabled as behaviour, bound to semantic tokens that already exist. docs/switch.md gap 3 states the reasoning and does not hide it: a control whose setting “takes effect immediately” with no focus ring and no disabled affordance is a defect, and CLAUDE.md asks every component to cover the states the product uses. No token was invented — but the mapping of state to token is the build’s reading of the token names, not a designer’s decision.

One sub-gap inside it: there is no color/border/primary/pressed in the export, so the off track has nothing to darken to while held and keeps its hovered colour.

Which is which: Figma draws two states, the repo ships six, and the four extra ones are the engineer’s reading awaiting a designer’s confirmation.

Note the contrast with the other two. Badge and Progress also have no drawn states, and in both cases the repo added none — because a badge is read rather than clicked and progress is an output rather than a control. The three components were treated differently on purpose, and each build report says why.


F. Three token bindings where the node and the code name different tokens

Section titled “F. Three token bindings where the node and the code name different tokens”

All three resolve to the same rendered value, so nothing looks wrong. All three mean the code and the design file are not saying the same thing.

Component Figma binds Repo writes Both resolve to
Button color/text/accent/aqua --color-text-accent-aqualight #16bee3
Button color/border/disabled --color-border-primary-disabled #dadce7
Card color/border/disabled --color-border-primary-disabled #dadce7

The aqua one has a known cause and it is still live. docs/button.md gap 3 records that the library holds both color/text/accent/aqua (#176e82) and color/text/accent/aquaLight (#16bee3), carrying identical descriptions, and that the Figma MCP renders both under the single name --lumo-semantic-color-text-accent-aqua. Reading 26:1665 live today still returns one collapsed name at the aquaLight value. The descriptions need to be made distinct.

The disabled-border one is unexplained in both directions. get_variable_defs returns color/border/disabled on both nodes, while docs/card.md gap 4 reports the opposite shape of problem — that color/border/primary/disabled is described in Figma with the older path lumo.semantic.color.border.disabled. Whether the nodes bind two different variables, or one variable whose description is stale, is not settled by either source and is not settled here.


G. motion/duration/fast is in the code and not on the node

Section titled “G. motion/duration/fast is in the code and not on the node”

Button and Card both transition their state changes with --motion-duration-fast, and docs/card.md lists motion/duration/fast as a Figma variable in its token map. get_variable_defs on 26:1665 and on 65:230 returns no motion variable at all.

Which is which: either the binding sits on a child the node query does not reach, or the transition is the repo’s addition and the build report recorded a binding that is not there. Neither source settles it.


H. The node’s own fill contradicts the node’s own value text (Progress)

Section titled “H. The node’s own fill contradicts the node’s own value text (Progress)”

Every variant of 69:1151 draws the Indicator at a fixed 158px while the value text reads 72%. The track in that layout is about 246px wide, so the drawn fill is about 64%. The two cannot both be right.

The repo drives the fill from percent, so 72% renders at 72%. The coded component will therefore not match a pixel measurement of the node’s indicator, and that is correct.

Which is which: this is a contradiction inside Figma, not between the two sources. docs/progress.md reads it as unfillable rather than as a bug — a static frame has no way to express “the fill follows the number” — and records it because QA will measure it.


I. The Empty State action slot is 100px of height that will never exist

Section titled “I. The Empty State action slot is 100px of height that will never exist”

94:14 and 94:22 are both empty slots measuring exactly 100×100. That square is most of the node’s published height: sm is 360×256 and md is 520×284, and a real Button is 36px tall. So the node’s own sizes are about 100px taller than any empty state with a real button in it.

The repo sizes the slot by its contents — an sm empty state with one button is about 192px, not 256px.

Which is which: Figma’s number is 100×100 and the repo’s is hug. docs/emptyState.md declines to decide it, and says why: rendering a fixed square would put a 36px button in the top-left of a large empty box, which is plainly not what the design means — but “plainly” is a judgement and the number is in the node. A designer should either fill the slot in Figma with the Button component or shrink it to hug.


J. Geometry that binds no token, by component

Section titled “J. Geometry that binds no token, by component”

Not Figma-versus-repo disagreements so much as places the design file is silent and the code had to choose. Each is reported in the component’s build report and each was confirmed live today.

Component What binds nothing What the repo did
Switch track 40×22, knob 16, inset 3 — and no token equal to 22 or 3 exists in any family or mode borrowed size/control/md, size/icon/sm, spacing/padding/xs; derived height and travel
Switch track and knob radius borrowed radius/chip and radius/avatar, both of which resolve to radius/full
Progress the two track heights, 6 and 8 — the only thing Size changes composed from core spacing primitives exactly
Progress track radius binds core/border/radius/full, skipping the semantic layer reproduced the core binding as drawn
Empty State medallion radius binds core/border/radius/full reproduced the core binding as drawn
Empty State medallion sizes 56 and 72 used core size/56 and size/72
Empty State frame widths 360/520 and description insets 20/50 made the component fluid; the caller sizes it

Two of these have a consequence worth stating. The switch is not 40×22 even in web — it is 40×24, because the borrowed inset rounds 3 up to 4 — and it is a different size in each platform mode (40/4 web, 32/2 admin, 44/4 mobile) because both borrowed tokens are mode-dependent, while the node draws one fixed size.

And the Empty State medallion is the subtlest: Switch borrowed a semantic alias where the node bound nothing, but here the node binds core explicitly. Substituting a semantic alias would be deciding which meaning the medallion has. A designer should add semantic.border.radius.medallion, or point the node at radius/avatar.


K. Intent nobody has written, in either source

Section titled “K. Intent nobody has written, in either source”

These are not disagreements. They are the fields where both sources are silent, collected because a reader of the intent records will notice the same sentence six times and should know it is deliberate.

No component in this set documents what its variants are for. Every one of the six names its variant properties and values; not one says which value to reach for when. No variant inside any of the six component sets carries a description of its own, and the repo describes mechanics only — “Type picks the skin”, “tone picks the tonal surface and the label colour”, “status picks the indicator colour”.

Component Undecided
Button which Type a primary, secondary or destructive action takes
Badge which Tone a given status takes
Card which Padding step to use when
Progress what each Status means; when sm rather than md
Empty State when sm rather than md; how the four causes named in use_when map onto anything
Switch (nothing — On=true / On=false need no explanation)

Placement is unstated by five of the six. Only Empty State’s own description names where it goes. For the rest, the placement evidence that exists belongs to other components’ documentation — card/header saying it has an Action slot, table/cell saying what can be dropped into it, learning/video-player saying “Don’t put the progress bar inside it”. Those are statements by those components about themselves, and the intent records list them as evidence rather than promoting them into rules.

The one place where a rule exists, in the wrong library

Section titled “The one place where a rule exists, in the wrong library”

Lumo.Web’s progress does not say what Status means. Two sibling platform libraries do, and they disagree with each other:

Library What its progress description states
Lumo.Mobile “Tone: primary default, positive on track/complete, warning slipping, negative at risk.”
Lumo.AdminSystem “Tone=danger above 90% utilisation or when blocked; warning when at risk.”

Three things differ. They call the property Tone where the web set calls it Status. Mobile names the fourth value negative and Admin names it danger — and within Lumo.Web itself, Badge uses Danger while Progress uses negative, so both spellings are live. And the two descriptions disagree about what “at risk” means: Mobile says it is negative, Admin says it is warning.

None of this has been adopted into the web component’s intent record. It is recorded here because it is the closest thing to a decision that exists, and because whoever makes the web decision should know these two already exist and conflict.


L. Repo-internal: three tokens used in code and missing from the build report

Section titled “L. Repo-internal: three tokens used in code and missing from the build report”

Not a Figma/repo disagreement — the node and the code agree. The build report is what is incomplete.

get_variable_defs on 65:230 returns --lumo-semantic-color-bg-surfacePrimary (#f5f6fa), and src/components/card/ uses --color-bg-surfaceprimary. The token map in docs/card.md does not list it. The same applies to --body-body3-intense and --spacing-gap-lg, both used in the card stylesheets and both absent from that map.


M. One live reading neither source explains

Section titled “M. One live reading neither source explains”

get_variable_defs on 17:1139 returns, alongside the sixteen Lumo semantic variables Badge uses, a bare color/primary: #5b4cf5.

That is not a Lumo token. #5b4cf5 appears nowhere in the component, the build report, or the generated CSS in build/css/. 17:1139 is a wrapper frame around the five Tone variants, so it most likely belongs to the frame rather than to any badge — but neither source says what it is or where it came from.

Reported, not resolved.

N. border/radius/card resolves to two different values in one file

Section titled “N. border/radius/card resolves to two different values in one file”

Added 2026-10-17, from the QA rows that put Toast and Accordion into To be fixed. Both failed on the same thing, and neither is a component defect — both rows say so, and both are right.

The same variable, read live on three nodes in the same file:

Node var(--lumo-semantic-border-radius-card)
Card 65:230 12
Accordion 208:4912 16
Toast 151:162 16

The committed export agrees with Card. tokens/lumo.semantic.border.value.tokens.json aliases border-radius-card to {border-radius-lg}, and --border-radius-lg is 12px. --border-radius-xl is already 16px, and border-radius-modal is what points at it.

So Toast and Accordion render a 12px corner while the nodes they were built from read 16, and the 4px gap is not a rendering artefact — 16px exists in the export under a different name.

Why a token re-export does not fix this. Both QA rows suggested routing it to the Token Runner for a re-export. A re-export would re-read the variable and get 12 — the value Card’s node resolves and the value already committed. Nothing would move. The disagreement is not between Figma and the export; it is inside the Figma file, between nodes using one variable name.

Two readings, and choosing between them is a designer’s call:

  • Those nodes sit under a mode that resolves radius/card to 16, and the export captures only one mode. CLAUDE.md is explicit that a token which exists in one mode and not another is a design gap to report rather than fill in.
  • Or the 16 is a detached override still carrying the variable’s name, in which case the nodes should be re-bound and the export is already correct.

The second is the likelier reading on the evidence here: Card’s own node and the export agree at 12, and Card’s 28 test rows recorded 12px as expected and passed. It is not resolved here, because resolving it means deciding what the card corner is, and that decision belongs on the canvas.

What must not happen in code: re-pointing toast.css or accordion.css at --border-radius-xl to make the number match. Both components bind the token their node names. Changing the binding to chase a value would manufacture agreement between the file and the code — the failure CLAUDE.md calls out by name.

Verified independently of the QA rows: all three Figma readings taken live over the MCP connection, and the export read from the committed tokens/ source and the generated build/css/web/tokens.css.



The Astro Link registry cell. The doc-generator owns it and it has deliberately not been written. The contract says that cell is a link somebody opened and watched render; the site has not deployed successfully yet, because the Vercel project is pointed at main and main has no docs-site/. Writing the link from a local build would make the cell a lie, and the rest of the crew acts on these cells.

Release Verdict. All six components currently read Blocked, and the first reason given in all six release reviews is that no reference page existed. These pages are that reference page — but whether that unblocks anything is the Release agent’s call and then a human’s, not this agent’s. Documenting a component is not reviewing it.