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)。
まず、フレックスレイアウトを使った Board としてベースコンポーネントを作成します(すべての値は
penpot-foundations のトークンを使用)。次に、各軸に沿ってバリアントを生成し、バリアントコンテナにまとめます
(2.17 以降では penpotUtils.createVariantContainer、低レベルの代替手段として
penpot.createVariantFromComponents を使用)。最後に、完全性を検証します。
2. 最も重要な唯一のルール
すべてのインタラクティブコンポーネントに、必要な状態をすべて用意し、すべての値をトークンにします。 塗りや間隔の
ハードコードは禁止です。システムで明示的に指定されていない限り、Hover/Pressed/Focus/Disabled を
欠かしてはいけません。一度に1つのバリアントを作成し、まとめる前にチェックポイントを設けます。
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 以降):コンテナ、軸、値を1回の呼び出しで作成 — 重複ファイルのゲートを通す必要あり |
penpot.createVariantFromComponents(mainInstances) | ヘルパーがない場合の低レベルの代替手段 |
instance.switchVariant(pos, value) | バリアントを表示/切り替え(pos = variants.properties 内のインデックス) |
export_shape | マトリクスの視覚的なチェックポイント |
4. Plugin API の要点
注意点の番号は shared/plugin-api-gotchas.md を参照してください。
- ベースは
addFlexLayout()を使った Board(createFrameではなくcreateBoard)として作成します。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 インスタンスの内部を変更する前に detach します — ただし、バリアントインスタンスに対しては絶対に行わないでください(#12 を参照)。
- #12 バリアントの変更はファイルを破損させます — このスキルにおける重大な障害モードです。
setVariantProperty(pos, value)はバリアントプロパティを更新しますが、バリアントルート内部の:variant-nameは更新しません。そのため、ファイルはバックエンドの参照整合性検証に失敗します。それ以降、コンポーネントに触れるすべての変更が HTTP 400 で拒否されますが、プラグインには表示されません — 呼び出しは約30秒間ハングし、セッションは終了し、未フラッシュの変更はロールバックされます。同じ問題は、addVariant()+ rename、バリアントに対するcomp.instance()+detach()、detach したバリアントインスタンスに対するcreateComponent、バリアントボードに対するshape.remove()でも発生します。復旧は手動で行う必要があります。バリアントの読み取りと、特定のバリアントのインスタンス化は安全です。安全な方法: 各状態を独立した Board として作成し、createComponent([board])を1つずつ実行して、Component / Stateという名前を付けます。第3フェーズ(§6)に、重複ファイルの作成/保存の検証/代替手順が記載されています。 - 不明なシグネチャは、まず
penpot_api_info('VariantContainer')/penpot_api_info('Variants')で確認します。
5. トークンを意識した簡潔な契約
- コンテキスト — 有効なデザインシステム/トークンセット、コンポーネントの目的。
- 目標 — 単一のコンポーネントと、そのバリアント軸。
- 入力 — 基本レイアウト、トークンセット(
penpot-foundations)、必須の軸。 - 制約 — すべての値をトークン化すること、必須の状態をすべて含めること、命名規則は
Property=Value。 - 受け入れ基準 — マトリクスが完成していること、ハードコードされた値がゼロであること、すべての状態が判読可能でAAコントラストを満たすこと、名前が規約に従っていること。
シニアコンポーネントエンジニアとして行動してください。
6. 必須ワークフロー
視覚的なセルフレビュー(必須): 視覚的な成果物を提示するすべての✋チェックポイントの前に、
shared/visual-self-review.mdに記載されたエクスポート → 確認 → 修正のループを実行してください。作成したばかりのユニットを エクスポートし、チェックリストに照らして画像を自分で確認し、目に見える不具合を修正して(最大2 回の反復)、残った不具合があればその内容を明記して、同じエクスポートを提示してください。
フェーズ 0 — 調査。 high_level_overview を実行し、トークン(tokenOverview())と既存コンポーネントを確認します。軸を決定します(references/01-variant-axes.md)。✋チェックポイント:軸のマトリクスを確認します。
フェーズ 1 — 基本形。 フレックスレイアウトとトークンを使って基本の Board を構築し(scripts/buildComponentBase.js)、次に createComponent を実行します。✋チェックポイント:基本形を確認します(export_shape)。
フェーズ 2 — コンポーネントとしてバリアントを作成。 マトリクスの各セルについて、基本形を複製し、状態ごとにトークンを再適用して、
createComponent を実行します。createVariantFromComponents では、バリアントごとにコンポーネントが必要です。scripts/createVariants.js を使います(各セルのメインインスタンス ID を収集します)。✋チェックポイント:各セルを確認します。
フェーズ 3 — グループ化、軸の命名、整理。 ⚠️ 高リスクフェーズ — まず shared/plugin-api-gotchas.md
#12 を読んでください。 バリアントコンポーネントの変更(setVariantProperty、軸名の変更、バリアントの切り離し/削除)は、ライブセッションでファイルを破損させ、その後のすべての保存をハングさせることが確認されています(バックエンドが、表面化しない HTTP 400 を返してすべての保存を拒否します)。このフェーズの前に、✋チェックポイントとしてユーザーにファイルを複製するよう依頼してください(または、スクラッチファイルでのリスクを受け入れることを確認してください)。その後、バリアントの変更を1つ適用し、ファイルが引き続き保存できること(後続の読み取り専用呼び出しが成功し、変更が保持されること)を確認してから続行します。呼び出しが約30秒ハングした場合は、直ちに停止し、以下のフォールバックに切り替えてください。
バリアントのフロー:scripts/createVariantGroup.js は条件付きで実行されます。storage.cf.fileDuplicatedConfirmed === true でない場合、{ halted: true } を返します(ユーザーがファイルを複製し、保存が機能することを確認した後に限り、先行する呼び出しで設定してください)。その後、まず
penpotUtils.createVariantContainer(records.map(r => ({ shape: main, properties: r.properties })))
(パスは "helper"、バージョン2.17以上:軸と値を1回の呼び出しで設定)を試し、利用できない場合は
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)。フラッシュ待機を挟みながら1つずつ作成し、ラベル付きのグリッドに配置してください。Penpot は / より前のプレフィックスでグループ化します。ユーザーは後から UI で実際のバリアントに変換できます。
フェーズ 4 — 検証。 scripts/validateComponent.js を実行し、必須の状態がすべてそろっていること、すべての値がトークン化されていること、命名が正しいことを検証します。必要に応じて 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)。 | 変更を1つ行ったら、保存が維持されることを確認してから続行します。約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(セルごとに 1 つのコンポーネント)、createVariantGroup.js(条件付き。helper ≥ 2.17 またはレガシーパス)、switchVariantDemo.js、validateComponent.js。
ドクトリンのパス。 ネイティブインストールでは、shared/… と policies/… はこのバンドル内のパスとして解決されます(インストーラーによってベンダー化されます)。Claude Code プラグインのインストールでは、プラグインのルート、つまりこのファイルから 2 つ上のディレクトリにある ${CLAUDE_PLUGIN_ROOT}/shared/… に配置されます。