Build and maintain Penpot components with COMPLETE variant matrices — sizes, hierarchies, and all interactive states (default/hover/pressed/focus/disabled) — fully tokenized and correctly named. Use to create a new component with variants, fill in missing states, or normalize an existing component. Triggers: 'create a Button component with variants', 'build component variants', 'add hover/pressed/disabled states', 'make a variant matrix', 'turn this into a component with sizes', 'normalize this component'.
Instrucciones de origen · Vista previa de solo lectura
name
penpot-component-factory
description
Build and maintain Penpot components with COMPLETE variant matrices — sizes, hierarchies, and all interactive states (default/hover/pressed/focus/disabled) — fully tokenized and correctly named. Use to create a new component with variants, fill in missing states, or normalize an existing component. Triggers: 'create a Button component with variants', 'build component variants', 'add hover/pressed/disabled states', 'make a variant matrix', 'turn this into a component with sizes', 'normalize this component'.
penpot-component-factory builds components and their full variant matrices; 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).
It builds a base component as a Board with flex layout (every value tokenized via penpot-foundations
tokens), generates variants across axes, combines them into a variant container
(penpot.createVariantFromComponents(boards)), and verifies completeness.
2. The One Rule That Matters Most
Every interactive component ships every required state, and every value is a token. No
hardcoded fills/spacing; no missing Hover/Pressed/Focus/Disabled unless the system explicitly
says otherwise. Build one variant at a time; checkpoint before combining.
3. Penpot MCP Tool Reference
Full surface: shared/penpot-mcp-tool-reference.md. Key calls:
Call
Why
penpot.createBoard() + addFlexLayout()
base component container
penpot.library.local.createComponent(shapes)
turn the base into a component
shape.clone()
derive variants from the base
penpot.createVariantFromComponents(mainInstances)
group component main-instances into a variant container
instance.switchVariant(pos, value)
demonstrate/switch variants
export_shape
visual checkpoint of the matrix
4. Plugin API Essentials
Gotcha numbers refer to shared/plugin-api-gotchas.md.
Build the base as a Board (createBoard, NOT createFrame) with addFlexLayout(); set dir, gaps, padding, horizontalSizing/.
verticalSizing
#4 flex/grid overrides child x/y — order children by append; use layoutChild for per-child sizing/margins.
clone() duplicates a shape with all properties — the basis for the variant matrix.
#9 variant API — penpot.createVariantFromComponents(mainInstances) (no combineAsVariants); switch with instance.switchVariant(pos, value).
#6 detach before mutating an instance's internals — and NEVER on a variant instance (see #12).
#12 variant MUTATION corrupts the file — this skill's critical failure mode.setVariantProperty(pos, value) updates the variant properties but not the variant root's internal :variant-name, so the file fails backend referential-integrity validation: from then on every component-touching mutation is rejected with an HTTP 400 the plugin never surfaces — calls hang ~30 s, the session dies, unflushed mutations roll back. The same poison applies to addVariant() + rename, comp.instance() + detach() on a variant, createComponent on a detached variant instance, and shape.remove() on a variant board; recovery is manual. Reading variants and instancing a specific variant are safe. Safe strategy: build each state as a standalone Board and createComponent([board])one at a time, named Component / State; Phase 3 (§6) carries the duplicate-file / verify-saves / fallback procedure.
Verify unfamiliar signatures with penpot_api_info('VariantContainer') / penpot_api_info('Variants') first.
5. Token-Aware Brief Contract
Context — which design system / token sets are active; component purpose.
Objective — single component + its variant axes.
Inputs — base layout, the token set (penpot-foundations), required axes.
Constraints — all values tokenized; all required states present; naming Property=Value.
Acceptance Criteria — matrix complete; zero hardcoded values; every state legible & AA-contrast; names follow convention.
Act as a senior component engineer.
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.high_level_overview; read tokens (tokenOverview()) and existing components. Decide axes (references/01-variant-axes.md). ✋ Checkpoint: confirm the axis matrix.
Phase 1 — Base. Build the base Board with flex layout, tokenized (scripts/buildComponentBase.js), then createComponent. ✋ Checkpoint: review base (export_shape).
Phase 2 — Variants as components. For each matrix cell, clone the base, retokenize per state, and
createComponent it — createVariantFromComponents needs a component per variant. Use
scripts/createVariants.js (collects each cell's main-instance id). ✋ Checkpoint: review the cells.
Phase 3 — Group, name axes, organize. ⚠️ HIGH-RISK PHASE — read shared/plugin-api-gotchas.md
#12 first. Mutating variant components (setVariantProperty, axis renames, detaching/removing
variants) has corrupted files and hung all subsequent saves in live sessions (the backend rejects
every save with an unsurfaced HTTP 400). Before this phase: ✋ Checkpoint — ask the user to duplicate
the file (or confirm they accept the risk on a scratch file). Then apply one variant mutation,
verify the file still saves (a later read-only call must succeed and persist), and only then continue.
If any call hangs ~30 s, stop immediately and switch to the fallback below.
The variant flow: scripts/createVariantGroup.js calls
penpot.createVariantFromComponents(mainInstances), then renames the auto-created property to your
first axis, adds the remaining axes (variants.addProperty() + renameProperty), and sets each
component's value via setVariantProperty(pos, value). Finally it gives the variant container a flex
layout — createVariantFromComponents stacks the variants at the same spot, so always apply a flex
(container.flex || container.addFlexLayout(); row, gaps, padding, wrap) so they arrange and the
container reflows to fit. ✋ Checkpoint.
Fallback (always safe): skip the variant container entirely. Keep the per-cell components from
Phase 2 as standalone components named Component / Axis=Value, … (e.g. Button / Size=Medium, State=Hover), created one at a time with a flush pause between calls, arranged in a labeled grid.
Penpot groups them by the / prefix and the user can convert them to real variants in the UI later.
Phase 4 — Validate.scripts/validateComponent.js — every required state present, all values tokenized, naming correct. Optional scripts/switchVariantDemo.js to prove switching. Report.
7. Critical Rules
All interactive components include Default/Hover/Pressed/Focus/Disabled unless the system says otherwise.
Zero hardcoded values — bind to semantic (or component) tokens.
Build as a flex Board; never absolute-position children unless layoutChild.absolute.
Idempotent: don't duplicate an existing variant.
Detach only when necessary, and report it — never detach a variant instance (gotchas #12).
After grouping, give the variant container a flex layout — variants stack at the same position otherwise.
Variant mutation is a known file-corruption risk (gotchas #12): duplicate the file before Phase 3,
verify saves after the first mutation, and fall back to standalone Component / State components
if anything hangs.
The matrix is the Cartesian product you need — keep it as small as the system allows (don't generate
combinations the design language doesn't use). Each variant is a cloned base with state-specific
token bindings.
9. Modes & Policies
Default review. Variant/component restructuring and detach() are never auto-applied
(shared/modes-and-policies.md).
10. State Management
Ledger keys under RUN_ID: phase, baseComponentId, created:[{variant:'Size=Medium,State=Hover', id}]. Re-derive with a component read after truncation.