1. Title + How it works
This skill takes an existing application artifact — JSX/TSX, HTML, Vue/Svelte templates, CSS/Tailwind, or a styled-components tree — and reconstructs it as an editable, system-bound Penpot screen. Every mutation goes through execute_code; validate visually with export_shape; read structure with penpotUtils.shapeStructure (full tool surface: shared/penpot-mcp-tool-reference.md). The core move is discover first (Phase 0 enumerates the existing tokens and components you must map onto), then transform the code incrementally, section by section, mapping each CSS/style value to a semantic token and each repeated UI part to an existing library component — never hardcoding hex or off-grid spacing, never reinventing a component as a raw box.
2. The One Rule That Matters Most
Never one-shot a screen, and never hardcode a value that a token already expresses. Build the screen one section at a time (one logical execute_code step per call), and before you write any fill/spacing/radius/type, resolve it against the discovered token set. A code-derived #0066FF is not a fill — it is a lookup into the design system. If no token matches, you propose one and stop; you do not invent it. Translating an entire page in a single call, or pasting raw computed CSS onto shapes, is the failure mode this skill exists to prevent.
3. Penpot MCP Tool Reference
Full surface: shared/penpot-mcp-tool-reference.md. The calls this skill leans on:
- Discovery (read-only):
penpotUtils.tokenOverview(),penpot.library.local(components, tokens),penpotUtils.shapeStructure(shape, depth),penpotUtils.findShape/findShapes. - Screen scaffold:
penpot.createBoard(),board.addFlexLayout(),penpotUtils.addFlexLayout(container, dir). - Section build:
penpot.createRectangle/createText/createEllipse/createPath,component.instance(),container.appendChild(child),child.layoutChild. - Token binding:
shape.applyToken(token, properties[]),token.resolvedValue,penpotUtils.findTokenByName. - Validation:
export_shape(separate tool call) and ashapeStructureread.
4. Plugin API Essentials
The gotchas that bite this skill, as one-liners — full text in shared/plugin-api-gotchas.md:
- #1 Immutable style arrays — assign a brand-new
fills/strokesarray; never mutatefills[0]. - #2 Token application is async (~100 ms) — apply in one
execute_codecall, verifyresolvedValuein a later call. - #3
resize()forces textgrowType: 'fixed'— setgrowType = 'auto-width'/'auto-height'after resizing text you want to flow. - #4 Flex/grid overrides child x/y — position children by append order + gap/padding/align/justify; opt out only with
child.layoutChild.absolute = true. - #5
width/heightandparentX/parentYare read-only — useshape.resize(w, h)andpenpotUtils.setParentXY(shape, x, y). - #6 Detach before mutating an instance's internals —
detach()first and report it. - #7 Append or it isn't on the canvas —
container.appendChild(child)(orpenpot.currentPage.root). - #8 Padding tokens — padding tokens bind on ≥ 2.17;
createScreenWrapper.jsfalls back to mirrored numbers + a ledger exception on older instances. - #9
switchVariant(pos, value)—posis the index intovariants.properties(seestorage.run.ds.componentsByRole[role].axes), never the axis name. - #13b Exact font match —
penpot.fonts.all.find(f => f.name === "…");findByNameis a substring search. - #15
layoutChildafter append — setlayoutChild.*only once the child is inside a layout board;openPageis async. - #16 flipX after fill sizing — run the
clearFlipsweep after every section that uses fill sizing (shared/visual-effects.md). - #17 Layout geometry —
await board.waitForLayoutUpdate()before reading geometry (replaces the sleep-100ms idiom). - Verify any unfamiliar signature with
penpot_api_info(type, member)before using it.
5. Token-Aware Brief Contract
Before any mutation, restate the request as this contract. Act as a senior design-engineer who treats the design system as the single source of truth and never hardcodes a value when a token exists.
- Context — what product/surface is this code from, who uses it, what page/view does it represent (e.g. "settings page of the B2B admin console, desktop").
- Objective — single and specific: "rebuild the
<SettingsPage>component as an editable Penpot board bound to the existingsemantictoken set and reusing theButton,InputField, andCardcomponents." - Inputs — the actual code (component files, CSS/Tailwind config, design tokens export if any), the target Penpot page, the discovered token sets, the discovered component library.
- Constraints — forbidden moves (no raw hex, no off-4px-grid spacing, no reinvented components, no
detach()without approval), inviolable rules (semantic tokens not primitives; existing components over new shapes), viewport width. - Acceptance Criteria — quantitative: ≥95% of color/spacing/radius/type values bound to tokens (validated by
validateScreen.js); zero orphan raw values flagged; every repeated UI part that has a matching library component is an instance, not a shape; all spacing on the 4px grid; layer names semantic (noRectangle 12).
6. Mandatory Workflow
Visual self-review (mandatory): before every ✋ checkpoint that shows visual work, run the export → look → fix loop from
shared/visual-self-review.md— export the unit you just built, inspect the image yourself against the checklist, fix visible defects (max 2 iterations), and present that same export with any remaining defects named.
Phase 0 — Discovery (read-only)
Goal: map the existing Penpot design system so code values have somewhere to land. No mutation.
high_level_overview(once).execute_code: runscripts/inspectDesignSystem.js→ enumerate token sets, every token (name,type,resolvedValue), and every library component (name,id, variant axes). Cache intostorage.run.ds.execute_code:penpotUtils.tokenOverview()to confirm the active theme/sets.- Parse the input code into a section outline (header, nav, hero, form, card grid, footer…) and a style inventory (every distinct color/spacing/radius/font value the code uses).
Exit criterion: you have (a) a token map keyed by resolved value, (b) a component catalog keyed by role, (c) a section outline, (d) a style inventory with each value pre-matched to a token or flagged
UNMAPPED. ✋ Checkpoint: present the token/component coverage and the UNMAPPED list. Ask which UNMAPPED values should become proposed new tokens vs. accepted as documented exceptions. Seereferences/01-discovery.md.
Phase 1 — Screen wrapper
Goal: create the screen Board with the right flex container and a RUN_ID ledger entry.
execute_code: runscripts/createScreenWrapper.js(idempotent by board name) → Board sized to the viewport,addFlexLayout('column'), gap + padding bound to spacing tokens (padding mirrored +padding-mirrors-tokenexception on 2.16.x). Exit criterion: one empty, correctly-sized, flex column Board exists; its id is in the ledger. ✋ Checkpoint:export_shapethe empty board; confirm dimensions/orientation before filling.
Phase 2 — Incremental section build (repeat per section)
Goal: build exactly one section from its code fragment, on-system. See references/02-component-assembly.md, references/03-token-binding.md, references/04-incremental-transform.md.
execute_code: runscripts/buildSection.jsfor that one section — prefercomponent.instance()for any part with a matching catalog component; create raw shapes only for genuinely bespoke parts; bind every fill/spacing/radius/type to a token viaapplyToken.execute_code(separate call, after ~100 ms gap from the agent's turn): read backtoken.resolvedValue/shapeStructureto confirm bindings applied.export_shapethe section. Exit criterion: the section renders, every value is token-bound or an approved exception, components are instances. ✋ Checkpoint: show the section; "looks good" approves only this section. Name the next section before continuing.
Phase 3 — Validation & report
Goal: prove the screen is on-system.
execute_code: runscripts/validateScreen.js→ token coverage %, list of orphan raw values, structure sanity (flex set, no auto-named layers, instances vs. shapes).- Produce the structured report (section 12 of AGENTS.md). Exit criterion: acceptance criteria met or every gap explicitly listed for review. ✋ Checkpoint: present coverage + remaining gaps; ask for sign-off.
7. Critical Rules
- Always
high_level_overviewfirst; always Phase 0 discovery before any write. - One section per
execute_codemutation call. Never build multiple sections in one call. - Every color/spacing/radius/border/type value MUST resolve to a token before it is applied. No raw hex/rgb/px on a shape when a matching token exists.
- All spacing/padding/gap MUST be a multiple of 4px (round code values to the nearest grid token; document any exception).
- Prefer semantic-tier tokens over primitives so theming works (
tokens-schema.json). - Any UI part with a matching library component MUST be an instance, not a hand-built shape group.
- Never invent a token. If a code value has no match, propose it (name/value/tier) and stop for approval.
- Token application is async — verify
resolvedValuein a laterexecute_codecall, never the same one. - Never
detach()an instance without explicit approval; report it if you do. - Semantic, kebab-case layer names from the code's role (
nav,header,card-container) — neverRectangle 12. - Write the
RUN_IDledger after each phase; make every create idempotent (existence-check by name).
8. Domain Architecture
The skill produces a screen tree and consumes a design-system map.
- Design-system map (Phase 0 output, cached in
storage.run.ds):tokensByValue: { "#0066FF": "color.action.primary.bg", "16": "spacing.4", ... }— reverse index for fast code-value → token lookup.tokensByName: { "color.action.primary.bg": {type, resolvedValue} }.componentsByRole: { button: {id, variants}, input: {...}, card: {...} }.
- Section outline (from code): ordered list
[{ role, codeFragment, childrenRoles }]. - Style inventory:
[{ codeValue, cssProp, matchedToken | "UNMAPPED" }]. - Screen tree (Penpot output):
Board(flex column) → section Boards(flex) → component instances + bound shapes. - Ledger (
RUN_ID):{ runId, phase, created:[{kind,name,id}], proposedTokens:[...], exceptions:[...], pendingReview:[...] }.
9. Modes & Policies
Default review (Apply-with-review): build each section, then stop at the checkpoint with an export_shape + summary before the next. Link: shared/modes-and-policies.md.
- Safe-set (may auto-apply): semantic layer renaming; replacing a raw value that is exactly equal to a token's resolved value with that token (loss-less swap); adding plugin-data/metadata.
- Always require review: creating the screen Board and any geometry; instantiating/restructuring components; any proposed new token;
detach(); any token match that is a judgement call rather than an exact equality.
10. State Management
Link: shared/state-management.md. Ledger namespace penpot-ai, keyed by RUN_ID (e.g. bfc-2026-06-05-a).
storage.run.ds— cached design-system map (tokens, components) from Phase 0.setSharedPluginData('penpot-ai', '<RUN_ID>.phase', n)— current phase.setSharedPluginData('penpot-ai', '<RUN_ID>.ledger', JSON)—{boardId, sectionsBuilt:[role], proposedTokens, exceptions}.- Resume: re-read the ledger, re-run
inspectDesignSystem.jsto re-derive the DS map,shapeStructure(board)to see which sections exist, continue fromledger.phase. Idempotent steps make re-runs safe.
11. User Checkpoints
| After phase | Artifacts shown | What we ask |
|---|---|---|
| 0 Discovery | Token/component coverage table; UNMAPPED style values | "Which UNMAPPED values become proposed tokens vs. documented exceptions?" |
| 1 Wrapper | export_shape of empty Board + dimensions |
"Correct viewport size and column orientation?" |
| 2 Section (each) | export_shape of the section + token-binding summary |
"Approve THIS section? (next section is named explicitly)" |
| 3 Validation | Coverage %, orphan-value list, structure sanity | "Sign off, or address listed gaps first?" |
12. Naming Conventions
Link: shared/naming-conventions.md. Domain specifics:
- Screen Board:
screen-<view-name>(kebab), e.g.screen-settings. - Section Boards: the semantic HTML role —
header,nav,main,section,footer, or kebab role names likecard-grid,field-row. - Component instances keep the catalog component name.
- Proposed tokens follow dot-notation tiers from
tokens-schema.json; prefer semantic names that describe intent derived from the code's usage (color.action.primary.bg, notcolor.blue.500).
13. Anti-Rationalization Table
| Excuse the LLM makes | Why it's wrong | Deterministic countermeasure that halts the flow |
|---|---|---|
"The CSS says #0066FF, I'll just set that as the fill." |
Hardcoded hex is off-system; it won't theme and fails governance. | Look it up in storage.run.ds.tokensByValue. If matched → applyToken. If not → propose a token and stop for approval. Never set raw hex. |
"Tailwind p-5 is 20px, close enough to set 20." |
A literal bypasses the token scale and may be off-grid. | Round to the nearest 4px-grid spacing token and applyToken. Document any exception only with approval. |
"There's a Button component but it's faster to draw a rounded rect with text." |
Reinventing a system part breaks updates, variants, and handoff parity. | If storage.run.ds.componentsByRole.button exists, component.instance() it and set props/variant. Raw shapes only for genuinely bespoke parts. |
"I'll build the whole page in one execute_code to save calls." |
One-shotting hides failures and produces unbound, unverified output. | Build one section per call; verify token bindings in a later call; export_shape and checkpoint before the next. |
"This code value has no token; I'll add color.misc.x myself to keep moving." |
Silently inventing tokens pollutes the system and skips human governance. | Add it to ledger.proposedTokens with name/value/tier and halt at the discovery checkpoint. Do not create it unilaterally. |
| "Token didn't read back; I'll just hardcode the value too as a fallback." | A hardcoded fallback is exactly the orphan value this skill forbids; the read-back is async. | Re-read resolvedValue in a later execute_code call. If it truly failed, fix the binding — never add a raw fallback. |
14. Helper Code Snippets
// Reverse-index lookup: code value -> token (Phase 2, inside buildSection)
const ds = storage.run.ds;
const tokenName = ds.tokensByValue[String(codeValue).toLowerCase()]; // e.g. "#0066ff" -> "color.action.primary.bg"
if (tokenName) {
const token = penpotUtils.findTokenByName(tokenName);
shape.applyToken(token, ["fill"]); // verify resolvedValue in a LATER call
} else {
ledger.proposedTokens.push({ from: codeValue, suggest: "propose a semantic token", tier: "semantic" });
}
return { applied: !!tokenName, codeValue };
// Prefer an existing component over a raw shape
const role = "button";
const cat = storage.run.ds.componentsByRole[role];
let node;
if (cat) {
const comp = penpot.library.local.components.find(c => c.id === cat.id);
node = comp.instance(); // instance, not a redrawn box
} else {
node = penpot.createRectangle(); // bespoke only
}
container.appendChild(node);
return { role, usedComponent: !!cat, id: node.id };
15. Reference Resources
penpot_api_info("LibraryComponent"),penpot_api_info("Shape", "applyToken"),penpot_api_info("Board", "addFlexLayout"),penpot_api_info("Token", "resolvedValue")— verify before use.shared/penpot-mcp-tool-reference.md(tool surface),shared/plugin-api-gotchas.md(traps),shared/tokens-schema.json(token types/tiers),shared/naming-conventions.md,shared/state-management.md.- Related skills:
penpot-build-screen(brief-driven generate flow),penpot-foundations(when the system lacks tokens to map onto — its infer mode),penpot-audit-accessibility(post-build contrast check).
16. Supporting Files
references/
| File | Loaded during | Purpose |
|---|---|---|
references/01-discovery.md |
Phase 0 | Enumerate tokens/components; build the reverse value→token index and component-by-role catalog; parse code into a section outline + style inventory. |
references/02-component-assembly.md |
Phase 2 | Decide instance-vs-shape; instantiate library components, set variants/props; assemble bespoke parts with flex. |
references/03-token-binding.md |
Phase 2 | Map code styles to semantic tokens; the applyToken property names; async verification; proposing new tokens. |
references/04-incremental-transform.md |
Phase 2 | The section-by-section loop, ordering, ledger updates, resumability. |
references/05-anti-rationalization.md |
All phases | Expanded countermeasures with concrete code-translation traps. |
references/06-error-recovery.md |
On failure | Diagnosing failed bindings, orphan values, flex/x-y conflicts, detach mishaps. |
scripts/
| File | Phase | Purpose |
|---|---|---|
scripts/inspectDesignSystem.js |
0 | Enumerate token sets/tokens and library components; build the value→token reverse index + component-by-role catalog; cache in storage. |
scripts/createScreenWrapper.js |
1 | Idempotently create the screen Board with flex column and token-bound padding/gap. |
scripts/buildSection.js |
2 | Build one section from a code fragment: instance components, create bespoke shapes, bind tokens. |
scripts/validateScreen.js |
3 | Compute token coverage, list orphan raw values, check structure sanity (flex, names, instances). |
Doctrine paths. shared/… and policies/… resolve inside this bundle in native installs (vendored by the installer); in a Claude Code plugin install they live at the plugin root — ${CLAUDE_PLUGIN_ROOT}/shared/…, two directories up from this file.