penpot-component-factory
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesepenpot-component-factory — complete, tokenized variants
penpot-component-factory —— 完整的令牌化变体
1. Title + How it works
1. 标题与工作原理
penpot-component-factoryexecute_codeexport_shapepenpotUtils.shapeStructureshared/penpot-mcp-tool-reference.mdpenpot-foundationspenpotUtils.createVariantContainerpenpot.createVariantFromComponentspenpot-component-factoryexecute_codeexport_shapepenpotUtils.shapeStructureshared/penpot-mcp-tool-reference.mdpenpot-foundationspenpotUtils.createVariantContainerpenpot.createVariantFromComponents2. The One Rule That Matters Most
2. 最重要的核心规则
Every interactive component ships every required state, and every value is a token. No
hardcoded fills/spacing; no missing /// unless the system explicitly
says otherwise. Build one variant at a time; checkpoint before combining.
HoverPressedFocusDisabled每个交互组件都必须包含所有必填状态,且所有值都必须是令牌。 不允许硬编码填充/间距;除非系统明确说明,否则不得缺失 /// 状态。每次构建一个变体,组合前需设置检查点。
HoverPressedFocusDisabled3. Penpot MCP Tool Reference
3. Penpot MCP 工具参考
Full surface: . Key calls:
shared/penpot-mcp-tool-reference.md| Call | Why |
|---|---|
| base component container |
| turn the base into a component |
| derive variants from the base |
| preferred (≥ 2.17): container + axes + values in one call — behind the duplicate-file gate |
| low-level fallback when the helper is absent |
| demonstrate/switch variants ( |
| visual checkpoint of the matrix |
完整接口参考:。核心调用:
shared/penpot-mcp-tool-reference.md| 调用 | 用途 |
|---|---|
| 基础组件容器 |
| 将基础元素转换为组件 |
| 从基础组件衍生变体 |
| 推荐方式(≥2.17版本):单次调用完成容器+维度+值的创建——需在文件复制确认后使用 |
| 无辅助工具时的低层级降级方案 |
| 演示/切换变体( |
| 变体矩阵的视觉检查点 |
4. Plugin API Essentials
4. 插件 API 核心要点
Gotcha numbers refer to .
shared/plugin-api-gotchas.md- Build the base as a Board (, NOT
createBoard) withcreateFrame; setaddFlexLayout(), gaps, padding,dir/horizontalSizing.verticalSizing - #4 flex/grid overrides child x/y — order children by append; use for per-child sizing/margins.
layoutChild - duplicates a shape with all properties — the basis for the variant matrix.
clone() - #9 variant API — prefer (≥ 2.17) behind the duplicate-file gate; the low-level
penpotUtils.createVariantContainer([{ shape: mainInstance, properties: { Size: "Medium", State: "Default" } }])+penpot.createVariantFromComponents(mainInstances)/renameProperty/addPropertypath only as fallback. Switch withsetVariantProperty—instance.switchVariant(pos, value)is the index inpos, not the axis name.variants.propertiesis listed byBoard.combineAsVariants(ids)on 2.17 but unverified — do not use.penpot_api_info - #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. updates the variant properties but not the variant root's internal
setVariantProperty(pos, value), 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:variant-name+ rename,addVariant()+comp.instance()on a variant,detach()on a detached variant instance, andcreateComponenton a variant board; recovery is manual. Reading variants and instancing a specific variant are safe. Safe strategy: build each state as a standalone Board andshape.remove()one at a time, namedcreateComponent([board]); Phase 3 (§6) carries the duplicate-file / verify-saves / fallback procedure.Component / State - Verify unfamiliar signatures with /
penpot_api_info('VariantContainer')first.penpot_api_info('Variants')
注意事项编号对应 中的条目。
shared/plugin-api-gotchas.md- 将基础组件构建为带有 的 Board(使用
addFlexLayout(),而非createBoard);设置createFrame、间距、内边距、dir/horizontalSizing。verticalSizing - #4 flex/grid 布局会覆盖子元素的 x/y 坐标 —— 通过追加顺序控制子元素排列;使用 为单个子元素设置尺寸/外边距。
layoutChild - 会复制形状的所有属性——是构建变体矩阵的基础。
clone() - #9 变体 API —— 优先使用 (≥2.17版本),且需在文件复制确认后使用;仅作为降级方案时,使用低层级的
penpotUtils.createVariantContainer([{ shape: mainInstance, properties: { Size: "Medium", State: "Default" } }])+penpot.createVariantFromComponents(mainInstances)/renameProperty/addProperty路径。使用setVariantProperty切换变体——instance.switchVariant(pos, value)是pos中的索引,而非维度名称。2.17版本的variants.properties中列出了penpot_api_info,但尚未验证——请勿使用。Board.combineAsVariants(ids) - 修改实例内部属性前需执行 #6 detach(分离) 操作——但绝对不要对变体实例执行分离操作(见#12)。
- #12 变体修改会损坏文件——这是本技能的严重故障模式。 会更新变体属性,但不会更新变体根节点内部的
setVariantProperty(pos, value),导致文件无法通过后端的引用完整性校验:自此之后所有涉及组件的修改都会被后端以HTTP 400拒绝,且插件不会提示该错误——调用会挂起约30秒,会话失效,未刷新的修改会回滚。同样的问题也会出现在:variant-name+ 重命名、对变体执行addVariant()+comp.instance()、对已分离的变体实例执行detach()、对变体Board执行createComponent等操作中;恢复只能手动进行。读取变体和实例化特定变体是安全的。安全策略: 将每个状态构建为独立的Board,每次一个地执行shape.remove(),命名格式为createComponent([board]);第3阶段(§6)包含文件复制/保存验证/降级方案的流程。Component / State - 遇到不熟悉的签名时,先通过 /
penpot_api_info('VariantContainer')验证。penpot_api_info('Variants')
5. Token-Aware Brief Contract
5. 令牌感知需求契约
- Context — which design system / token sets are active; component purpose.
- Objective — single component + its variant axes.
- Inputs — base layout, the token set (), required axes.
penpot-foundations - 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.
- 上下文 —— 当前使用的设计系统/令牌集;组件用途。
- 目标 —— 单个组件及其变体维度。
- 输入 —— 基础布局、令牌集()、必填维度。
penpot-foundations - 约束 —— 所有值令牌化;所有必填状态齐全;命名格式为 。
Property=Value - 验收标准 —— 变体矩阵完整;零硬编码值;所有状态清晰可读且对比度达AA级;命名符合规范。
请以高级组件工程师的身份执行操作。
6. Mandatory Workflow
6. 强制工作流
Visual self-review (mandatory): before every ✋ checkpoint that shows visual work, run the export → look → fix loop from— 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.shared/visual-self-review.md
Phase 0 — Discovery. ; read tokens () and existing components. Decide axes (). ✋ Checkpoint: confirm the axis matrix.
high_level_overviewtokenOverview()references/01-variant-axes.mdPhase 1 — Base. Build the base Board with flex layout, tokenized (), then . ✋ Checkpoint: review base ().
scripts/buildComponentBase.jscreateComponentexport_shapePhase 2 — Variants as components. For each matrix cell, clone the base, retokenize per state, and
it — needs a component per variant. Use
(collects each cell's main-instance id). ✋ Checkpoint: review the cells.
createComponentcreateVariantFromComponentsscripts/createVariants.jsPhase 3 — Group, name axes, organize. ⚠️ HIGH-RISK PHASE — read
#12 first. Mutating variant components (, 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.
shared/plugin-api-gotchas.mdsetVariantPropertyThe variant flow: is gated — it returns unless
(set it in a prior call only after the user duplicated the
file and confirmed saves work). It then prefers
(path , ≥ 2.17: axes + values in one call), falling back to
scripts/createVariantGroup.js{ halted: true }storage.cf.fileDuplicatedConfirmed === truepenpotUtils.createVariantContainer(records.map(r => ({ shape: main, properties: r.properties })))"helper"penpot.createVariantFromComponents- /
renameProperty/addProperty(pathsetVariantProperty, kept verbatim in the script). Either way it clears the container fill (#11), gives the container a flex layout (variants stack at the same spot otherwise:"legacy"; row, gaps, padding,container.flex || container.addFlexLayout()) and returnswrap. Save verification: after the call, make a trivial change in the Penpot UI and confirm it persists before any further mutation — a ~30 s hang means the file is poisoned; discard the duplicate and switch to the fallback below. ✋ Checkpoint.{ containerId, path, properties, components, next }
Fallback (always safe): skip the variant container entirely. Keep the per-cell components from
Phase 2 as standalone components named (e.g. ), 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.
Component / Axis=Value, …Button / Size=Medium, State=Hover/Phase 4 — Validate. — every required state present, all values tokenized, naming correct. Optional to prove switching. Report.
scripts/validateComponent.jsscripts/switchVariantDemo.js视觉自查(强制要求): 在每个展示视觉成果的✋检查点之前, 需执行中规定的导出→检查→修复循环——导出你刚构建的单元,对照检查清单自行检查图片,修复可见缺陷(最多2轮迭代),然后提交该导出结果,并说明所有剩余缺陷。shared/visual-self-review.md
阶段0 — 调研。 执行 ;读取令牌()和现有组件。确定变体维度(参考 )。✋ 检查点:确认维度矩阵。
high_level_overviewtokenOverview()references/01-variant-axes.md阶段1 — 基础组件。 构建带有弹性布局的基础Board,完成令牌化(参考 ),然后执行 。✋ 检查点:评审基础组件(通过 导出)。
scripts/buildComponentBase.jscreateComponentexport_shape阶段2 — 变体组件化。 对矩阵中的每个单元格,克隆基础组件,根据状态重新令牌化,然后执行 ——因为 需要每个变体对应一个组件。使用 (收集每个单元格的主实例ID)。✋ 检查点:评审所有变体单元格。
createComponentcreateVariantFromComponentsscripts/createVariants.js阶段3 — 分组、命名维度、整理。 ⚠️ 高风险阶段——请先阅读 的#12条目。 修改变体组件(、维度重命名、分离/删除变体)在实际会话中曾导致文件损坏、后续所有保存操作挂起(后端会以未提示的HTTP 400拒绝所有保存请求)。本阶段开始前:✋ 检查点——要求用户复制文件(或确认他们愿意在临时文件上承担风险)。然后执行一次变体修改,验证文件仍可正常保存(后续的只读调用必须成功且数据持久化),之后再继续操作。如果任何调用挂起约30秒,立即停止并切换到下方的降级方案。
shared/plugin-api-gotchas.mdsetVariantProperty变体流程: 是门控的——除非 (仅当用户复制文件并确认保存正常后,再通过之前的调用设置该值),否则它会返回 。之后它会优先使用 (路径为 ,≥2.17版本:单次调用完成维度+值的创建),降级方案为 + //(路径为 ,脚本中按原样保留)。无论哪种方式,它都会清除容器的填充(#11),为容器添加弹性布局(否则变体会堆叠在同一位置:;设置为行方向、间距、内边距、自动换行),并返回 。保存验证: 调用完成后,在Penpot UI中做一个微小修改,确认修改已持久化后再进行后续修改——如果出现约30秒的挂起,说明文件已损坏;请丢弃该复制文件,切换到下方的降级方案。✋ 检查点。
scripts/createVariantGroup.jsstorage.cf.fileDuplicatedConfirmed === true{ halted: true }penpotUtils.createVariantContainer(records.map(r => ({ shape: main, properties: r.properties })))"helper"penpot.createVariantFromComponentsrenamePropertyaddPropertysetVariantProperty"legacy"container.flex || container.addFlexLayout(){ containerId, path, properties, components, next }降级方案(始终安全): 完全跳改变体容器。将阶段2中每个单元格的组件保留为独立组件,命名格式为 (例如 ),每次创建一个,每次调用之间留出刷新暂停时间,按带标签的网格排列。Penpot会按 前缀对它们进行分组,用户之后可以在UI中将其转换为真实变体。
组件名 / 维度=值, …Button / Size=Medium, State=Hover/阶段4 — 验证。 运行 ——检查所有必填状态是否齐全、所有值是否已令牌化、命名是否正确。可选运行 来演示变体切换功能。最后生成报告。
scripts/validateComponent.jsscripts/switchVariantDemo.js7. Critical Rules
7. 关键规则
- All interactive components include unless the system says otherwise.
Default/Hover/Pressed/Focus/Disabled - Zero hardcoded values — bind to semantic (or component) tokens.
- Variant property/value names: , PascalCase (
Property=Value,Size=Medium).State=Hover - 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 components if anything hangs.
Component / State
- 除非系统另有说明,否则所有交互组件都必须包含 状态。
Default/Hover/Pressed/Focus/Disabled - 零硬编码值——绑定到语义(或组件)令牌。
- 变体属性/值命名格式:,使用大驼峰命名法(PascalCase),例如
Property=Value、Size=Medium。State=Hover - 基于弹性布局的Board构建;除非使用 ,否则绝不对子元素使用绝对定位。
layoutChild.absolute - 幂等性:不要重复创建已有的变体。
- 仅在必要时执行分离(detach)操作,并进行报告——绝对不要对变体实例执行分离操作(见注意事项#12)。
- 分组后,为变体容器添加弹性布局——否则变体会堆叠在同一位置。
- 变体修改是已知的文件损坏风险(见注意事项#12):阶段3前复制文件,第一次修改后验证保存是否正常,如果出现任何挂起,降级为独立的 组件。
组件 / 状态
8. Domain Architecture
8. 领域架构
Standard axes:
Size = Small | Medium | LargeHierarchy = Primary | Secondary | TertiaryState = Default | Hover | Pressed | Focus | DisabledIcon = None | Left | Right
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.
标准维度:
Size = Small | Medium | LargeHierarchy = Primary | Secondary | TertiaryState = Default | Hover | Pressed | Focus | DisabledIcon = None | Left | Right
变体矩阵是所需的笛卡尔积——请保持矩阵尽可能小,仅包含系统允许的组合(不要生成设计语言未使用的组合)。每个变体都是克隆的基础组件,并绑定了对应状态的令牌。
9. Modes & Policies
9. 模式与策略
Default review. Variant/component restructuring and are never auto-applied
().
detach()shared/modes-and-policies.md默认模式为评审。变体/组件重构和 操作绝不自动执行(参考 )。
detach()shared/modes-and-policies.md10. State Management
10. 状态管理
Ledger keys under : , , . Re-derive with a component read after truncation.
RUN_IDphasebaseComponentIdcreated:[{variant:'Size=Medium,State=Hover', id}]RUN_IDphasebaseComponentIdcreated:[{variant:'Size=Medium,State=Hover', id}]11. User Checkpoints
11. 用户检查点
| After phase | Artifacts shown | What we ask |
|---|---|---|
| 0 | proposed axis matrix | Approve axes? |
| 1 | base | Approve base? |
| 2 | variant grid | Approve variants? |
| 3 | variant container | Approve combine? |
| 阶段后 | 展示产物 | 需确认内容 |
|---|---|---|
| 0 | 拟定的维度矩阵 | 是否批准维度? |
| 1 | 基础组件的 | 是否批准基础组件? |
| 2 | 变体网格的 | 是否批准变体? |
| 3 | 变体容器 | 是否批准组合? |
12. Naming Conventions
12. 命名规范
shared/naming-conventions.mdButtonProperty=Valuelabeliconbutton参考 :组件使用大驼峰命名(PascalCase,例如 );变体格式为 ;内部图层使用语义化命名(例如 、、)。
shared/naming-conventions.mdButtonProperty=Valuelabeliconbutton13. Anti-Rationalization Table
13. 反合理化对照表
| Excuse | Why it's wrong | Countermeasure (halt) |
|---|---|---|
| "I'll add hover/disabled later." | Incomplete components ship broken states. | All required states are acceptance gates; build them now. |
| "A plain rectangle is fine for this state." | Hardcoded shapes drift from the system. | Clone the base and bind state tokens. |
| "I'll hardcode the hover color." | Breaks theming and governance. | Use/propose a semantic token ( |
| "Absolute-position the icon, it's easier." | Fights flex; breaks resizing. | Use flex order + |
| "I'll assume the variant-creation method/args." | Variant API is easy to get wrong. | Prefer |
| "Variant mutation worked once — I'll batch the rest in one call." | One bad | One mutation → verify saves persist → continue. On any ~30 s hang, stop and use the standalone-components fallback. |
| 借口 | 错误原因 | 应对措施(停止操作) |
|---|---|---|
| "我之后再加hover/disabled状态。" | 不完整的组件会交付有问题的状态。 | 所有必填状态都是验收门槛;必须现在构建。 |
| "这个状态用普通矩形就行。" | 硬编码的形状会偏离设计系统。 | 克隆基础组件并绑定状态令牌。 |
| "我硬编码hover颜色就好。" | 会破坏主题化和治理规则。 | 使用/提议一个语义令牌(例如 |
| "图标用绝对定位更简单。" | 会与flex布局冲突;破坏缩放能力。 | 使用flex排序 + |
| "我就猜一下变体创建的方法/参数。" | 变体API很容易用错。 | 优先使用 |
| "变体修改成功过一次——我把剩下的批量一次调用完。" | 一个错误的 | 一次修改 → 验证保存成功 → 再继续。出现任何约30秒的挂起,立即停止并使用独立组件降级方案。 |
14. Helper Code Snippets
14. 辅助代码片段
js
// Base as a flex Board (Phase 1) — gaps/fill/padding bound to tokens (padding binds on ≥ 2.17; mirror + exception on 2.16.x, gotchas #8)
const board = penpot.createBoard();
board.name = "Button";
const flex = board.addFlexLayout();
flex.dir = "row";
flex.horizontalSizing = "auto"; flex.verticalSizing = "auto";
// gap binds to a token; padding binds on Penpot ≥ 2.17 — try applyToken per side, fall back to the RESOLVED value + a padding-mirrors-token exception
const gapTok = penpotUtils.findTokenByName("spacing.inset.sm"); // or propose it at the checkpoint
if (gapTok) board.applyToken(gapTok, ["columnGap"]);
const padY = penpotUtils.findTokenByName("spacing.8");
const padX = penpotUtils.findTokenByName("spacing.16");
penpot.currentPage.root.appendChild(board); // append before binding layout props
const exceptions = [];
for (const [tok, sides, fallback] of [[padY, ["paddingTop", "paddingBottom"], 8], [padX, ["paddingLeft", "paddingRight"], 16]]) {
for (const side of sides) {
try { if (tok) board.applyToken(tok, [side]); else throw new Error("token missing"); }
catch { flex[side.replace("padding", "").toLowerCase() + "Padding"] = tok ? Number(tok.resolvedValue) : fallback; exceptions.push({ kind: "padding-mirrors-token", token: tok && tok.name, side }); }
}
}
// FILL POLICY: a button is a SURFACE — bind its bg token (never a literal, never the default white)
board.fills = [];
const bgTok = penpotUtils.findTokenByName("color.action.primary.bg");
if (bgTok) board.applyToken(bgTok, ["fill"]);
const label = penpot.createText("Button");
board.appendChild(label);
const comp = penpot.library.local.createComponent([board]);
return { component: comp.name, id: comp.mainInstance().id, exceptions };js
// Base as a flex Board (Phase 1) — gaps/fill/padding bound to tokens (padding binds on ≥ 2.17; mirror + exception on 2.16.x, gotchas #8)
const board = penpot.createBoard();
board.name = "Button";
const flex = board.addFlexLayout();
flex.dir = "row";
flex.horizontalSizing = "auto"; flex.verticalSizing = "auto";
// gap binds to a token; padding binds on Penpot ≥ 2.17 — try applyToken per side, fall back to the RESOLVED value + a padding-mirrors-token exception
const gapTok = penpotUtils.findTokenByName("spacing.inset.sm"); // or propose it at the checkpoint
if (gapTok) board.applyToken(gapTok, ["columnGap"]);
const padY = penpotUtils.findTokenByName("spacing.8");
const padX = penpotUtils.findTokenByName("spacing.16");
penpot.currentPage.root.appendChild(board); // append before binding layout props
const exceptions = [];
for (const [tok, sides, fallback] of [[padY, ["paddingTop", "paddingBottom"], 8], [padX, ["paddingLeft", "paddingRight"], 16]]) {
for (const side of sides) {
try { if (tok) board.applyToken(tok, [side]); else throw new Error("token missing"); }
catch { flex[side.replace("padding", "").toLowerCase() + "Padding"] = tok ? Number(tok.resolvedValue) : fallback; exceptions.push({ kind: "padding-mirrors-token", token: tok && tok.name, side }); }
}
}
// FILL POLICY: a button is a SURFACE — bind its bg token (never a literal, never the default white)
board.fills = [];
const bgTok = penpotUtils.findTokenByName("color.action.primary.bg");
if (bgTok) board.applyToken(bgTok, ["fill"]);
const label = penpot.createText("Button");
board.appendChild(label);
const comp = penpot.library.local.createComponent([board]);
return { component: comp.name, id: comp.mainInstance().id, exceptions };15. Reference Resources
15. 参考资源
- ,
penpot_api_info('Board'),penpot_api_info('VariantContainer'),penpot_api_info('Variants').penpot_api_info('LibraryComponent')
- 、
penpot_api_info('Board')、penpot_api_info('VariantContainer')、penpot_api_info('Variants')。penpot_api_info('LibraryComponent')
16. Supporting Files
16. 支持文件
references/: , , , , , .
scripts/: , (one component per cell), (gated; helper ≥ 2.17 or legacy path), , .
01-variant-axes.md02-flex-grid-layout.md03-variant-api.md04-instance-swap-detach.md05-anti-rationalization.md06-error-recovery.mdbuildComponentBase.jscreateVariants.jscreateVariantGroup.jsswitchVariantDemo.jsvalidateComponent.jsDoctrine paths. and resolve inside this bundle in native installs (vendored by the installer); in a Claude Code plugin install they live at the plugin root — , two directories up from this file.
shared/…policies/…${CLAUDE_PLUGIN_ROOT}/shared/…references/ 目录:、、、、、。
scripts/ 目录:、(每个单元格一个组件)、(门控;≥2.17版本使用辅助工具,否则使用旧路径)、、。
01-variant-axes.md02-flex-grid-layout.md03-variant-api.md04-instance-swap-detach.md05-anti-rationalization.md06-error-recovery.mdbuildComponentBase.jscreateVariants.jscreateVariantGroup.jsswitchVariantDemo.jsvalidateComponent.js规则路径说明。 原生安装时, 和 路径在此包内解析(由安装程序打包);在Claude Code插件安装中,它们位于插件根目录——,即本文件的上两级目录。
shared/…policies/…${CLAUDE_PLUGIN_ROOT}/shared/…