name: lumos-import-figma description: Build a page or fill in Lumos for Astro variables from a Figma file, especially a messy one missing global variables. Use when the user shares a Figma link or design and asks to implement it, translate it into Lumos, fill in the design tokens, or when the design's spacing, type and color are inconsistent and need reconciling against src/styles/base.css.
Building Lumos from a Figma file
A design file is a picture of an intention, not a source of truth. The job is to land the intention in the token system with as few new tokens as possible, and to be explicit about every guess.
The rule that outranks the rest: never invent a variable or a class to paper over an inconsistency in the design. Surface it and ask. Two paddings that differ by 6px are usually one padding drawn twice. Ask which it is before writing anything.
What Figma cannot say
Three conversions are always needed, because the file physically cannot hold the values this system uses.
| In Figma | In Lumos | Conversion |
| --- | --- | --- |
| 32px | 2rem | ÷ 16 |
| line height 70px on a 64px size | 1.094 | line height ÷ font size |
| letter spacing -2.4px on an 80px size | -0.03em | letter spacing ÷ font size, or % ÷ 100 |
| #FFFFFF at 60% opacity | color-mix(in lab, var(--light-100) 60%, transparent) | alpha becomes the mix percentage |
One refinement on the last row. If the faded hex is whatever a theme uses for
--text, the answer is currentcolor, not that swatch — otherwise the muted
label stays dark when the section flips to the dark theme. The script spots
this and says so.
The opacity one matters most. A designer who wants a muted label has no
color-mix, so they restate the base hex at lower opacity. That is not a new
color — it is the existing swatch, mixed. Adding --grey-400: #999 for it is
the mistake this skill exists to prevent.
A desktop frame is the maximum, not the value
Tokens here are fluid: --space-6-min: 32 at a 320px viewport, --space-6-max: 40
at 1440px. A measurement taken off a 1440px frame is therefore the token's
max. The min has to come from somewhere:
Check which frames exist before measuring anything — files differ, and the two paths produce different work:
- Both frames. Measure each. The mobile value is the min, the desktop value
is the max, and nothing is guessed. Say which frame widths you measured, since
a 375px frame and a 320px
--viewport-minare not the same thing — a value read at 375 is slightly larger than the token's true min. - Desktop only.
convert.mjsderives the min from the ratio of whichever existing token is closest in size, and marks it as a guess. Every derived min goes in the report; they are the values most likely to be wrong.
Never mix the two silently. If half the tokens are measured and half derived, the report has to say which is which.
Steps
-
Read the file. Use the Figma MCP tools —
get_variable_defsfor whatever variables do exist,get_design_contextfor the frame,get_screenshotto see what it should look like. Start with the variables: they tell you how much of the system the designer actually used. -
Inventory before converting. List every distinct spacing value, type size with its line height, and color with its opacity. Distinct values, not distinct layers — the same 24px appearing eleven times is one value.
-
Convert and match. Write the inventory to JSON and run the script:
node .claude/skills/lumos-import-figma/convert.mjs --json design.jsonThe script does the arithmetic because there is a lot of it and it is easy to get quietly wrong: forty values, each measured against twenty fluid token pairs, plus RGB distance for every colour. It reads the tokens out of
base.cssrather than carrying a copy, so it cannot drift from the system.{ "space": [{ "name": "stack gap", "px": 30 }], "type": [{ "name": "Section title", "sizePx": 64, "lineHeightPx": 70 }], "letter": [{ "name": "Hero tracking", "px": -2.4, "sizePx": 80 }], "radius": [{ "name": "Card corner", "px": 16 }], "weight": [{ "name": "Heading", "value": "Medium" }], "color": [{ "name": "Muted label", "hex": "#FFFFFF", "alpha": 0.6 }] }Add
"on": "#1F1D1E"and"sizePx"to a colour and the script also reports its WCAG contrast, using the large-text bar of 3:1 at 24px and above. These are flagged, never blocking — a decorative label may fail deliberately — but an unreadable body colour is usually the design being messy rather than a decision, so raise it with the other questions.lettertakes eitherpxwith itssizePx, orpct. Add"token": "name"to any entry to choose what a new variable would be called. Unknown keys are rejected rather than silently ignored, so a typo does not read as "nothing to convert".It reads the real tokens out of
src/styles/base.css, converts the units, snaps anything within 2px to the token it is drifting from, derives fluid mins, and prints anASK BEFORE WRITINGlist. Values off by more than 2px are decisions, not drift, and belong in that list.For one-off lookups:
--px 30,--lh 50/42,--color "#FFFFFF@60". -
Ask the questions. Put the whole
ASK BEFORE WRITINGlist to the user at once, each with the option to consolidate:The design uses 30px, 32px and 34px gaps in three places.
--space-5is 32px. Consolidate all three, or is one of them deliberate?Wait for answers. Do not write tokens for anything still in question.
-
Place the tokens yourself. The script prints what to add under
TO PLACE BY HAND; it does not touchbase.css. Where a token goes says what it means, and:rootis ordered by kind — put each one with its own:| Kind | Goes beside | | --- | --- | | spacing |
--space-8, before the section-space group | | section spacing |--section-space-large| | type size | theh1–h6/text-*block, in size order | | line height | the four--line-height-*values | | letter spacing | beside--letter-spacing-tight/-normal| | radius | the--radius-*group | | font weight | the--primary-*weights | | swatch | the swatch list at the top of:root| | themed color | every theme block —:root,.theme-dark,.theme-brand— or it breaks on one theme |A fluid token is three lines (
-min,-max, and theclamp()), and theclamp()the script prints already matches the formula the others use. Keep the scale in order: a--space-9of 120px belongs after--space-8, not wherever it was measured. -
Fill the gaps the design forgot. A messy file will be missing states nobody drew: hover and focus colors, the dark-theme counterpart of a button, disabled text. Derive them from what the file does show, following the existing pattern in
base.css— each theme block defines the same set of--button-*variables, so a missing dark-theme hover has an obvious shape to fill. Every one of these is a guess and goes in the report. -
Build with what exists, then build what doesn't. Compose from the library first —
Wrapper/SectionandWrapper/ContentWrapperfor layout,Wrapper/Gridfor columns,Item/Cardfor repeated blocks,Typography/*for text. A design that "needs" a new class usually needs an existing variant, and a one-off class is how a system stops being one.When something genuinely does not compose — a testimonial slider, a stats row — build it, following the new component checklist in
LUMOS.md. List every component you added in the report, with a sentence on why nothing existing covered it. That list is the one most worth arguing with: it is where the system grows, and growth is harder to undo than a token. -
Look at it. Tokens matching the table does not mean the page matches the design. Start the dev server, open the page, and compare it against the screenshot from step 1:
astro dev --backgroundScreenshot the built page at the same width as the frame you measured, and check the two side by side. Then check the width you did not measure — a design given only at 1440px still has to survive 375px, and that is where derived mins show up as wrong. Report what does not match rather than quietly adjusting tokens until it does: a mismatch is often the design being inconsistent, which is a question, not a bug.
The report
Close with four lists. Anything empty, say so.
- New variables — name, value, and what in the design asked for it.
- New components — what was built, and why nothing existing covered it.
- Guesses — derived fluid mins, invented states (dark-theme button hover, focus rings), anything the file did not actually specify.
- Snapped — values moved to an existing token, with the delta. These were applied without asking; the user may still want to reverse one.
- Contrast — any pair below its WCAG floor, with the ratio. Flagged, not fixed.
- Still open — inconsistencies the user has not ruled on yet.
Versions
This skill versions separately from the framework. A fix here does not need a Lumos release, and a Lumos release does not invalidate the skill.
-
Skill version —
SKILL_VERSIONinconvert.mjs. Bump it when the conversion rules or the workflow change. -
Lumos version —
package.jsonis the source of truth. Nothing here duplicates it; the script reads it and prints both on every run:lumos-import-figma 1.0.0 · Lumos 0.0.1If the running project is a different version than
TESTED_AGAINST, the script says so. That is a prompt to checkbase.cssstill looks the way this skill assumes — token names, theclamp()shape, the theme blocks — not a reason to stop.
Using this without Claude Code
Nothing here is Claude-specific except the loading. The workflow is this file
and the script is plain Node, so another assistant can be pointed at
.claude/skills/lumos-import-figma/SKILL.md and follow it, and anyone can run
node .claude/skills/lumos-import-figma/convert.mjs by hand. Only the automatic
triggering and /lumos-import-figma are Claude Code features.
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.