关于 penpot-component-factory
面向 Penpot 的技能,用于构建设计系统组件及完整的令牌化变体集。它引导设计师和组件工程师分阶段创建与验证,并提示变体分组可能损坏文件。
主要功能
- 01构建弹性布局组件画板
- 02从组件基础生成变体
- 03将属性绑定到设计令牌
- 04验证变体完整性与视觉效果
- 05以独立组件作为安全后备方案
适用场景
- 01为按钮创建必需的交互状态
- 02构建尺寸与层级变体矩阵
- 03分组前检查组件输出
MCP 相关
使用 execute_code、export_shape 和 shapeStructure 等 Penpot MCP 工具创建、检查与验证组件。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ penpot/ penpot-ai-kit --skill penpot-component-factory安装后在 Claude Code 中输入/penpot-component-factory确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/01-variant-axes.md02-flex-grid-layout.md03-variant-api.md04-instance-swap-detach.md05-anti-rationalization.md06-error-recovery.mdscripts/buildComponentBase.jscreateVariantGroup.jscreateVariants.jsswitchVariantDemo.jsvalidateComponent.js详细介绍
penpot-component-factory — 完整的令牌化变体
1. 标题 + 工作原理
penpot-component-factory 用于构建组件及其完整的变体矩阵;所有修改都通过
execute_code 执行;使用 export_shape 进行视觉验证;使用
penpotUtils.shapeStructure 读取结构(完整工具列表:shared/penpot-mcp-tool-reference.md)。
它会将基础组件构建为采用弹性布局的画板(所有值都通过 penpot-foundations
令牌化),沿多个轴生成变体,将它们组合成变体容器
(在 ≥ 2.17 版本中使用 penpotUtils.createVariantContainer;低层级备用方案为
penpot.createVariantFromComponents),并验证完整性。
2. 最重要的一条规则
每个交互式组件都必须包含所有必需状态,并且每个值都必须是令牌。 不得硬编码填充或间距;除非设计系统明确另有规定,否则不得缺少 Hover/Pressed/Focus/Disabled 状态。一次构建一个变体;组合前先创建检查点。
3. Penpot MCP 工具参考
完整工具列表:shared/penpot-mcp-tool-reference.md。关键调用:
| 调用 | 用途 |
|---|---|
penpot.createBoard() + addFlexLayout() | 基础组件容器 |
penpot.library.local.createComponent(shapes) | 将基础形状转换为组件 |
shape.clone() | 从基础形状派生变体 |
penpotUtils.createVariantContainer([{ shape, properties }]) | 首选(≥ 2.17):一次调用创建容器、轴和值 — 需要通过重复文件门控 |
penpot.createVariantFromComponents(mainInstances) | 缺少辅助函数时使用的低层级备用方案 |
instance.switchVariant(pos, value) | 展示/切换变体(pos = variants.properties 中的索引) |
export_shape | 矩阵的视觉检查点 |
4. Plugin API 基础知识
注意事项编号参见 shared/plugin-api-gotchas.md。
- 将基础组件构建为画板(使用
createBoard,而非createFrame),并通过addFlexLayout()添加弹性布局;设置dir、间距、内边距、horizontalSizing/verticalSizing。 - #4 弹性/网格布局会覆盖子项的 x/y 坐标 — 按追加顺序排列子项;使用
layoutChild设置每个子项的尺寸/边距。 clone()会复制形状的所有属性 — 这是构建变体矩阵的基础。- #9 变体 API — 优先在重复文件门控下使用
penpotUtils.createVariantContainer([{ shape: mainInstance, properties: { Size: "Medium", State: "Default" } }])(≥ 2.17);仅在备用情况下使用低层级的penpot.createVariantFromComponents(mainInstances)+renameProperty/addProperty/setVariantProperty路径。使用instance.switchVariant(pos, value)切换 —pos是variants.properties中的索引,不是轴名称。Board.combineAsVariants(ids)虽由 2.17 版本的penpot_api_info列出,但尚未验证 — 请勿使用。 - #6 在修改实例内部结构前先分离实例 — 但绝不要对变体实例执行此操作(参见 #12)。
- #12 变体修改会损坏文件 — 这是本技能的关键故障模式。
setVariantProperty(pos, value)会更新变体属性,但不会更新变体根节点内部的:variant-name,因此文件无法通过后端引用完整性验证:此后所有涉及组件的修改都会被拒绝,并返回插件无法显示的 HTTP 400 错误 — 调用会挂起约 30 秒,会话会终止,尚未刷新的修改会回滚。相同的损坏也会由addVariant()+ 重命名、对变体执行comp.instance()+detach()、对已分离的变体实例执行createComponent,以及对变体画板执行shape.remove()引发;需要手动恢复。读取变体和创建特定变体的实例是安全的。安全策略: 将每个状态构建为独立画板,并逐个调用createComponent([board]),命名为Component / State;第 3 阶段(§6)说明了重复文件、验证保存和备用方案的流程。 - 使用
penpot_api_info('VariantContainer')/penpot_api_info('Variants')验证不熟悉的签名。
5. Token 感知型简报规范
- 上下文 — 当前启用的设计系统/Token 集;组件用途。
- 目标 — 单个组件及其变体维度。
- 输入 — 基础布局、Token 集(
penpot-foundations)、必需维度。 - 约束 — 所有值均使用 Token;包含所有必需状态;命名格式为
Property=Value。 - 验收标准 — 矩阵完整;不存在硬编码值;每个状态均清晰易读且符合 AA 对比度要求;名称遵循约定。
以资深组件工程师的身份行动。
6. 强制工作流
**视觉自检(强制):**在每个展示视觉成果的 ✋ 检查点之前, 执行
shared/visual-self-review.md中的导出 → 查看 → 修复循环——导出刚构建的单元,按照检查清单自行检查图像,修复可见缺陷(最多 2 轮),并展示同一份导出,同时说明任何仍存在的缺陷。
阶段 0 — 探索。 high_level_overview;读取 Token(tokenOverview())和现有组件。根据 references/01-variant-axes.md 确定维度。✋ 检查点:确认维度矩阵。
阶段 1 — 基础组件。 使用弹性布局构建基础 Board,并应用 Token(scripts/buildComponentBase.js),然后执行 createComponent。✋ 检查点:检查基础组件(export_shape)。
阶段 2 — 将变体创建为组件。 对矩阵中的每个单元格,克隆基础组件,根据状态重新应用 Token,然后对其执行 createComponent——createVariantFromComponents 需要每个变体各自对应一个组件。使用 scripts/createVariants.js(收集每个单元格主实例的 ID)。✋ 检查点:检查各单元格。
阶段 3 — 分组、命名维度并整理。 ⚠️ 高风险阶段——先阅读 shared/plugin-api-gotchas.md
中的 #12。 在实时会话中,变体组件变更(setVariantProperty、重命名维度、分离/移除变体)曾导致文件损坏,并使之后的所有保存操作卡住(后端会拒绝每次保存,并返回未显示的 HTTP 400 错误)。开始此阶段前:✋ 检查点——请用户复制文件(或确认接受在临时文件上承担风险)。然后只应用一项变体变更,确认文件仍能保存(之后的只读调用必须成功并持久化),再继续。如果任何调用卡住约 30 秒,立即停止并改用下方的回退方案。
变体流程:scripts/createVariantGroup.js 受门控——除非 storage.cf.fileDuplicatedConfirmed === true(仅在用户复制文件并确认保存正常后,才在之前的调用中设置该值),否则会返回 { halted: true }。之后优先使用
penpotUtils.createVariantContainer(records.map(r => ({ shape: main, properties: r.properties })))
(路径 "helper",≥ 2.17:一次调用即可设置维度和取值);若不可用,则回退到 penpot.createVariantFromComponents
renameProperty/addProperty/setVariantProperty(路径"legacy",在脚本中原样保留)。无论哪种方式,都会清除容器填充(#11),为容器设置弹性布局(否则变体会堆叠在同一位置:container.flex || container.addFlexLayout();行、间距、内边距、wrap),并返回{ containerId, path, properties, components, next }。**保存验证:**调用完成后,在进行任何进一步变更之前,在 Penpot UI 中进行一项无关紧要的更改,并确认其已持久保存——卡住约 30 秒意味着文件已损坏;丢弃副本并改用下方的回退方案。✋ 检查点。
**回退方案(始终安全):**完全跳过变体容器。将阶段 2 中各单元格的组件保留为独立组件,命名为 Component / Axis=Value, …(例如 Button / Size=Medium, State=Hover),每次只创建一个组件,调用之间短暂停顿,并将组件排列成带标签的网格。Penpot 会按 / 前缀将它们分组,之后用户可在 UI 中将其转换为真正的变体。
阶段 4 — 验证。 运行 scripts/validateComponent.js——确认所有必需状态均已包含、所有值均已使用 Token、命名正确。可选运行 scripts/switchVariantDemo.js 演示切换。报告结果。
7. 关键规则
- 所有交互组件都必须包含
Default/Hover/Pressed/Focus/Disabled,除非系统另有规定。 - 不得使用硬编码值——绑定到语义令牌(或组件令牌)。
- 变体属性/值名称格式:
Property=Value,采用 PascalCase(Size=Medium、State=Hover)。 - 使用 flex Board 构建;除非设置
layoutChild.absolute,否则绝不对子元素使用绝对定位。 - 操作须幂等:不要重复创建已有的变体。
- 仅在必要时分离,并报告分离情况——绝不分离变体实例(注意事项 #12)。
- 分组后,为变体容器设置 flex 布局——否则变体会叠放在同一位置。
- 变体修改存在已知的文件损坏风险(注意事项 #12):在第 3 阶段前复制文件,首次修改后验证保存结果;如果出现卡顿,则回退为独立的
Component / State组件。
8. 领域架构
标准轴:
Size = Small | Medium | LargeHierarchy = Primary | Secondary | TertiaryState = Default | Hover | Pressed | Focus | DisabledIcon = None | Left | Right
所需矩阵是这些轴的笛卡尔积——将其控制在系统允许的最小范围内(不要生成设计语言中未使用的组合)。每个变体都是克隆的基础组件,并绑定特定状态的令牌。
9. 模式与策略
默认使用审查模式。变体/组件重构和 detach() 绝不自动应用(shared/modes-and-policies.md)。
10. 状态管理
RUN_ID 下的记录键:phase、baseComponentId、created:[{variant:'Size=Medium,State=Hover', id}]。截断后,使用组件读取结果重新推导。
11. 用户检查点
| 阶段后 | 展示的产物 | 要询问的内容 |
|---|---|---|
| 0 | 拟定的轴矩阵 | 批准这些轴? |
| 1 | 基础组件 export_shape | 批准基础组件? |
| 2 | 变体网格 export_shape | 批准这些变体? |
| 3 | 变体容器 | 批准合并? |
12. 命名约定
shared/naming-conventions.md:组件使用 PascalCase(Button);变体使用 Property=Value;内部图层采用语义名称(label、icon、button)。
13. 反合理化对照表
| 借口 | 错误原因 | 应对措施(停止) |
|---|---|---|
| “我稍后再添加悬停/禁用状态。” | 组件状态不完整,上线后会出现问题。 | 所有必需状态都是验收门槛;现在就构建它们。 |
| “这个状态用普通矩形就行。” | 硬编码形状会偏离系统规范。 | 克隆基础组件并绑定状态令牌。 |
| “我直接硬编码悬停颜色。” | 这会破坏主题和治理规范。 | 使用/提议语义令牌(color.action.primary.hover.bg)。 |
| “把图标设为绝对定位,这样更简单。” | 这与 flex 冲突,也会破坏尺寸调整。 | 使用 flex 顺序 + layoutChild;仅在有充分理由时,才使用 layoutChild.absolute 设置绝对定位。 |
| “我假设变体创建方法/参数就可以。” | 变体 API 很容易用错。 | 优先使用 penpotUtils.createVariantContainer;仅在缺少该辅助函数时,才使用底层的 createVariantFromComponents;通过 penpot_api_info('Variants') 验证。绝不使用 combineAsVariants(未经验证)。 |
| “变体修改成功过一次——我这次就一次调用批量处理剩下的。” | 一次出错的 setVariantProperty 就会悄无声息地损坏文件;之后每次保存都会卡住(注意事项 #12)。 | 一次修改 → 验证保存结果已持久化 → 继续。任何操作卡顿约 30 秒时,都应停止并回退为独立组件。 |
14. 辅助代码片段
// 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. 参考资源
penpot_api_info('Board')、penpot_api_info('VariantContainer')、penpot_api_info('Variants')、penpot_api_info('LibraryComponent')。
16. 支持文件
references/:01-variant-axes.md、02-flex-grid-layout.md、03-variant-api.md、04-instance-swap-detach.md、05-anti-rationalization.md、06-error-recovery.md。
scripts/:buildComponentBase.js、createVariants.js(每个单元格一个组件)、createVariantGroup.js(有门槛限制;helper ≥ 2.17 或使用旧版路径)、switchVariantDemo.js、validateComponent.js。
规范路径。 在原生安装中,shared/… 和 policies/… 会解析到此软件包内(由安装程序供应);在 Claude Code 插件安装中,它们位于插件根目录,即 ${CLAUDE_PLUGIN_ROOT}/shared/…,距此文件向上两级目录。