openpencil-design について
openpencil-designは、PenNode JSON、`op`コマンドライン、またはMCPツールでOpenPencilのキャンバスデザインを作成・編集するスキルです。ノード構造、レイアウト規則、スクリプトによる操作方法を解説します。
主な機能
- 01キャンバスノードの作成と編集
- 02`op`コマンドラインまたはMCPツールの利用
- 03デザインDSLによる一括操作
- 04スクリプトモードで反復デザインを生成
- 05PenNodeの構造とレイアウト規則の参照
活用シーン
- 01`.op`ファイル内のUIデザイン作成・更新
- 02反復するデザイン構造のスクリプト化
- 03キャンバスノードの一括変更
MCP 対応
`batch_design`、`insert_node`、`design_skeleton`などのOpenPencil MCPツールの利用方法を説明します。`op`コマンドラインからエディターとMCPサーバーを起動・管理できます。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zseven-w/ openpencil-skill --skill openpencil-designインストール後、Claude Code で/openpencil-designを入力して読み込まれたか確認します。
詳細
OpenPencil デザイン
PenNode の JSON ツリーを記述して、プロダクション品質のベクターデザインを生成します。op CLI または MCP ツールを使って、OpenPencil キャンバス上のノードを作成、読み取り、更新、削除できます。
使用する場面
.opファイルで UI デザインを作成または変更するときopCLI でデザイン操作をスクリプト化するとき- MCP ツール(
batch_design、insert_node、design_skeleton)でデザインするとき - PenNode のスキーマ、ロール、レイアウトルールのリファレンスが必要なとき
クイックリファレンス — op CLI
# App and server control
op start [--file path.op] # Launch the desktop editor and live MCP server
op start --web [--host H] [--file path.op] # Serve the browser editor and MCP
op start --headless [--file path.op] # Run a windowless file-backed MCP server
op stop # Stop the running editor / MCP server
op status # Check whether MCP HTTP is reachable
op tools # List every MCP tool and input schema
# Skill management
op install [--target T] # Install openpencil-skill for detected AI agents
op uninstall [--target T] # Uninstall openpencil-skill
op skill:export <skill> [--out DIR] # Export an embedded skill
# Document
op open [file.op] # Open file or connect to live canvas
op save <file.op> # Save current document
op get [--type T] [--name N] [--id ID] [--depth N] [--parent P]
op selection [--depth N] # Get current canvas selection
op read-nodes [ids] [--depth N] [--vars] # Read node subtree(s) with optional variable resolution
op layout [--parent P] [--depth N] # Snapshot layout tree with computed positions
op find-space [--direction D] [--width N] [--height N] # Find empty space on canvas
# Node operations
op insert '<json>' [--parent P] [--page PAGE] [--post-process]
op update <id> '<json>' [--page PAGE] [--post-process]
op delete <id> [--page PAGE]
op move <id> [--parent P] [--index N] [--page PAGE]
op copy <id> [--parent P] [--page PAGE]
op replace <id> '<json>' [--page PAGE] [--post-process]
# Batch design
op design '<dsl>' # Batch design DSL (inline, @file, or stdin) [--canvas-width N]
op design @ui.js # Sandboxed JS: I(parent, obj) + loops (.js/.mjs implies --script)
op design '<js>' --script # Same in inline/stdin form; flag follows payload
# Layered workflow
op design:skeleton '<json>' # Create section structure
op design:content <id> '<json>' # Populate section content
op design:refine --root-id <id> # Validate + auto-fix (resolves icons) [--canvas-width N]
# Design-as-code conversion
op design:upsert-vars --key K --file vars.json [--source P] [--hash H]
op design:upsert-component --key K --name N --file node.json [--source P] [--hash H]
op design:upsert-screen --key K --file node.json [--source P] [--hash H]
op design:status [--kind token|component|screen]
op design:lint [--node ID]
# Templates & style assets
op templates [--scene S] [--tag T] # List shipped scene templates (incl. 16:9 decks)
op use-template <id> # Start from a template: takes over a blank
# starter page, otherwise appends its boards
op styles [<id>] [--tag T] [--platform P] # List style guides: shipped corpus +
# your imported DESIGN.md files; a bare id
# prints that guide's markdown
# Export & delivery
op export [--item ID|--selection] --output PATH [--format png|jpeg|webp|pdf] [--scale N]
op export-deck --output PATH [--format pptx|html|pdf] # Active page's boards as a deck
op export-frames --output-dir DIR [--format png|jpeg|webp] # One image per top-level frame
# Import
op import:svg <file.svg> [--parent P] # Import SVG as editable nodes
op import:html <file.html|url> [--parent P] [--viewport-height N] [--out out.op]
# Import a page or live URL as nodes
op import:snapshot <snapshot.json> [--parent P] [--out out.op]
# Import a web-capture snapshot
op import:figma <file.fig> [--out out.op] # Convert Figma .fig to .op document
# Pages
op page list # List all pages
op page add [--name N] # Add a new page
op page remove <id> # Remove a page
op page rename <id> '<name>' # Rename a page
op page reorder <id> <index> # Move page to position
op page duplicate <id> # Clone page with new IDs
# Variables & Themes
op vars / op vars:set '<json>' # Variables (--replace to replace all)
op themes / op themes:set '<json>' # Themes (--replace to replace all)
op theme:save <file.optheme> # Save current theme as preset file
op theme:load <file.optheme> # Load a theme preset file
op theme:list <directory> # List .optheme presets in directory
# Codegen pipeline
op codegen:plan '<json>' # Submit plan (chunks, sharedStyles, rootLayout)
op codegen:submit <planId> '<json>' # Submit a code chunk for the plan
op codegen:assemble <planId> [--framework F] # Assemble final output
op codegen:clean <planId> # Clear codegen state for the plan
グローバルフラグ:--port <n>、--file <path>、--page <id>、--pretty。入力形式:インライン文字列、@filepath、または -(stdin)。
デザインの構築 — 3 つの方法
方法 1:op insert(推奨)
デザインを構築する最も信頼性の高い方法です。--parent を使って親ノードを指定します。返された nodeId を取得し、後で参照できるようにします。アイコンの解決とレイアウトの検証のため、必ず最後に design:refine を実行してください。
# Create root frame, capture its ID
ROOT=$(op insert '{"type":"frame","name":"Page","width":375,"height":812,"layout":"vertical"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['nodeId'])")
# Insert children using --parent
op insert --parent "$ROOT" '{"type":"text","content":"Hello","fontSize":28,"fontWeight":700}'
# Post-process: resolve icons, validate layout
op design:refine --root-id "$ROOT"
方法 2:一括デザイン DSL
1 行につき 1 つの操作を記述します。後で参照する場合は、name= で結果を割り当てます。ネストされた children 配列を使用できますが、後続の操作で子要素を安定して参照する必要がある場合は、子要素を個別に挿入してください。
root=I(null, { "type": "frame", "width": 1200, "layout": "vertical" })
nav=I(root, { "type": "frame", "role": "navbar", "height": 72 })
U(nav, { "fill": [{"type": "solid", "color": "#FFFFFF"}] })
card2=C(card1, grid, { "name": "Card 2" })
M(sidebar, main, 0)
D(old_section)
R(old_btn, { "type": "rectangle", "role": "button" })
| 操作 | 構文 | 処理 |
|---|---|---|
I | name=I(parent, { node }) | 挿入 |
U | U(ref, { updates }) | 更新 |
C | name=C(source, parent, { overrides }) | コピー |
R | name=R(ref, { node }) | 置換 |
M | M(ref, parent, index?) | 移動 |
D | D(ref) | 削除 |
G | name=G(parent, "search", "query") | 検索で画像を生成 |
バインディングしやすいパターン — 後で参照する必要がある場合は、親要素と子要素を個別に挿入します。
btn=I(form, {"type":"rectangle","role":"button","width":"fill_container","height":50,"cornerRadius":12,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})
I(btn, {"type":"text","content":"Submit","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})
方法 3:スクリプトモード(繰り返し構造におすすめ)
ループやデータに基づく繰り返しを、DSL 操作の繰り返しよりも明確に記述できる場合は、サンドボックス化された JavaScript を使用します。
op design @ui.js # .js / .mjs implies script mode
op design '<js code>' --script # inline or stdin; --script follows the payload
MCP 経由の場合は、batch_design を呼び出し、nodes_json、operations、script のいずれか 1 つだけを指定します。スクリプトモードでは、2 つの記録用ビルダーを使用できます。
const section = I(null, {type: "frame", name: "Clients", layout: "vertical", width: "fill_container"});
const rows = ["Alice Chen", "Bob Ito", "Sam Rivera"];
for (const name of rows) {
const row = I(section, {type: "frame", layout: "horizontal", width: "fill_container", padding: [12, 16]});
I(row, {type: "text", content: name});
}
// Instantiate a component from an attached UI kit:
K("shadcn/btn-primary", section, {name: "Primary Action"});
I(parent, node) はノードを挿入し、ID を再割り当てして返します。K(kitComponentId, parent, overrides) は UI キットのコンポーネントをインスタンス化し、同様に ID を再割り当てして返します。starter/<id>、shadcn/<id>、または <kit-id>/<component-id> を使用してください。ルートには null を指定します。それ以外の場合は、以前の記録呼び出しで返された ID を渡します。スクリプトモードで C、U、D、M、R、G、console.* が利用できるのは、何も実行しない互換性用スタブとしてのみです。更新、コピー、置換、移動、削除、画像検索の操作には DSL を使用してください。
スクリプトモードでは、ネストされた children 配列を安全に使用できます。上限は、ソースが 256 KiB、記録される操作が 4096 件、記録される出力が 8 MiB、実行時間が 2 秒、メモリが 64 MiB です。
厳密な JSON ルール
(op insert、バッチ DSL、nodes_json、または insert_node を使って)PenNode JSON を出力する場合は、厳密に有効な JSON を生成する必要があります。スクリプトモードでは、代わりに JavaScript のオブジェクトリテラルを使用します。JSON の解析に失敗する、よくある間違いは次のとおりです。
- すべてのプロパティには、キーと値の両方が必要です。キー名なしで
": 50や: 50を出力してはいけません。切り詰めや再フォーマットの際によく発生するため、必ず再確認してください。 - すべてのキーは、空ではない二重引用符付きの文字列である必要があります。
fillは必ず配列にします:"fill": [{"type": "solid", "color": "#hex"}]。"fill": "#hex"のような省略形も動作しますが、配列形式が標準の形式です。strokeはfill配列を持つオブジェクトです:"stroke": {"thickness": 1, "fill": [{"type": "solid", "color": "#hex"}]}。"stroke": {"thickness": 1, "color": "#hex"}や"stroke": "#hex"は使用してはいけません(パーサーが自動変換しますが、正しい形式を使用することを推奨します)。}または]の前に 末尾のカンマを付けてはいけません。- JSON 内に コメントを含めてはいけません(
//または/* */)。 - スマートクォート/カールクォートではなく、通常の二重引用符
"を使用します。 - テキストには
textではなくcontentを使用します:{"type": "text", "content": "Hello"}。 - アイコンには
iconNameやiconではなくiconFontNameを使用します:{"type": "icon_font", "iconFontName": "lock"}。 - JSON を確定する前に、すべてのキーに値があり、すべての値にキーがあり、すべての括弧の対応が取れていることを頭の中で確認してください。
PenNode スキーマ
共通プロパティ
{
"type": "frame|group|rectangle|ellipse|polygon|line|text|path|text_input|text_area|select|switch|checkbox|slider|radio_group|number_input|progress|tabs|image|icon_font|ref",
"name": "Display Name",
"role": "semantic-role",
"x": 0, "y": 0,
"rotation": 0, "opacity": 1, "visible": true
}
コンテナのプロパティ(frame、rectangle、group、ellipse)
{
"width": 400, // number | "fill_container" | "fit_content"
"height": 300,
"layout": "vertical", // "none" | "vertical" | "horizontal"
"gap": 16,
"padding": [16, 24], // number | [v, h] | [top, right, bottom, left]
"justifyContent": "center", // "start" | "center" | "end" | "space_between" | "space_around"
"alignItems": "center", // "start" | "center" | "end"
"clipContent": true,
"cornerRadius": 12, // number | [tl, tr, br, bl]
"fill": [{ "type": "solid", "color": "#FFFFFF" }],
"stroke": { "thickness": 1, "fill": [{ "type": "solid", "color": "#E5E7EB" }], "align": "inside", "dashPattern": [5, 3] },
"effects": [{ "type": "shadow", "offsetX": 0, "offsetY": 4, "blur": 12, "spread": 0, "color": "rgba(0,0,0,0.08)" }],
"children": []
}
テキスト
{
"type": "text",
"content": "Hello", // string or StyledTextSegment[]
"fontSize": 16, "fontFamily": "Inter", "fontWeight": 600,
"textAlign": "center", // "left" | "center" | "right"
"textGrowth": "fixed-width", // "auto" | "fixed-width" | "fixed-width-height"
"lineHeight": 1.5, "letterSpacing": 0,
"fill": [{ "type": "solid", "color": "#111111" }]
}
リッチテキスト:"content": [{ "text": "Bold ", "fontWeight": "bold" }, { "text": "normal" }]
アイコン — 2 つの方法
方法 A:icon_font(推奨 — 直接レンダリングされ、後処理は不要です)
{ "type": "icon_font", "name": "Lock Icon", "iconFontName": "lock",
"width": 20, "height": 20,
"fill": [{ "type": "solid", "color": "#6B7280" }] }
フィールド名は iconFontName です(iconName でも icon でもありません)。 値には、小文字のケバブケース形式の Lucide 名を指定します:mail、lock、eye、eye-off、chrome、apple、message-circle、x、arrow-right、search、heart、star、check、plus、bell、home、user、settings、chevron-right、download、globe、layers、zap、shield、play。
CLI、MCP ツール、.op ファイルの直接使用など、すべてのコンテキストで動作します。design:refine は不要です。
方法 B:path(後処理が必要です)
{ "type": "path", "name": "HeartIcon", "width": 24, "height": 24,
"fill": [{ "type": "solid", "color": "#111111" }] }
PascalCase に「Icon」接尾辞を付けます。後処理時に Lucide セットから自動的に解決されます。
パスアイコンには後処理が必要です。 パスノードを挿入した後、
op design:refine --root-id <id>を実行するか、op insert --post-processを使用してください。これを行わないと、パスアイコンは視覚的にレンダリングされません。単独の MCP サーバー(ACP エージェントが使用)はフック実装が登録されていないため、パスアイコンはそこで解決されません。MCP のコンテキストではicon_fontを優先してください。
画像
{ "type": "image", "src": "https://example.com/photo.jpg", "width": 400, "height": 300,
"objectFit": "crop", "cornerRadius": 12 }
AI 画像プレースホルダー(design:refine によって解決されます):
{ "type": "image", "width": 400, "height": 300,
"imagePrompt": "A modern office workspace with natural light",
"imageSearchQuery": "modern office workspace" }
画像の調整(すべて -100 から 100):exposure、contrast、saturation、temperature、tint、highlights、shadows。
ポリゴン
{ "type": "polygon", "polygonCount": 6, "width": 80, "height": 80, "cornerRadius": 4,
"fill": [{ "type": "solid", "color": "#6366F1" }] }
アイコンフォント
{ "type": "icon_font", "iconFontName": "lucide:home", "width": 24, "height": 24,
"fill": [{ "type": "solid", "color": "#111111" }] }
線
{ "type": "line", "x2": 200, "y2": 0,
"stroke": { "thickness": 1, "fill": [{ "type": "solid", "color": "#E5E7EB" }] } }
塗りつぶしの種類
{ "type": "solid", "color": "#3B82F6" }
{ "type": "linear_gradient", "angle": 135,
"stops": [{ "offset": 0, "color": "#6366F1" }, { "offset": 1, "color": "#8B5CF6" }] }
{ "type": "radial_gradient", "cx": 0.5, "cy": 0.5, "radius": 0.5,
"stops": [{ "offset": 0, "color": "#FFF" }, { "offset": 1, "color": "#000" }] }
{ "type": "image", "url": "https://example.com/texture.jpg", "mode": "fill" }
画像の塗りつぶしモード:fill、fit、crop、tile、stretch。画像の塗りつぶしでは、調整フィルター(exposure、contrast、saturation など)もサポートされています。
Ref ノード(コンポーネントインスタンス)
{ "type": "ref", "ref": "reusable-frame-id",
"descendants": { "child-id": { "content": "Override text" } } }
reusable: true を指定した frame を参照します。descendants を使って、特定の子孫のプロパティを上書きできます。
デザイン変数
$ 接頭辞を付けて参照します:"color": "$primaryColor"、"gap": "$spacing"。
セマンティックロール
ロールは意図を宣言します。エンジンが適切なデフォルト値を適用します。手動でスタイルを設定するより、常にロールを優先してください。
| カテゴリ | ロール |
|---|---|
| レイアウト | section、row、column、centered-content、divider、spacer |
| ナビゲーション | navbar、nav-links、nav-link |
| インタラクティブ | button、icon-button、badge、tag、pill、input、form-input、search-bar |
| カード | card、feature-card、stat-card、pricing-card、image-card |
| コンテンツ | hero、feature-grid、cta-section、footer、testimonial、stats-section |
| タイポグラフィ | heading、subheading、body-text、caption、label |
| メディア | avatar、icon、phone-mockup、screenshot-frame |
| テーブル | table、table-row、table-header、table-cell |
| フォーム | form-group |
主なデフォルト値:
navbar→ 高さ:56~72、横方向、space_between、中央揃えbutton→ パディング:[12, 24]、cornerRadius:8、中央揃えcard→ 縦方向、gap:12、cornerRadius:12、padding:24heading→ lineHeight:1.2、letterSpacing:-0.5body-text→ fill_container、textGrowth:fixed-width、lineHeight:1.5
レイアウトルール
- レイアウトコンテナ内の子要素には、絶対に x/y を設定しないでください — 配置はエンジンが行います
- 兄弟要素には同じ幅の戦略を使用してください — すべて
fill_containerか、すべて固定幅にします fit_contentの親要素内で 絶対にfill_containerを使わないでください — 循環依存が発生します- 横並びの行に配置するカードは、すべて
width: "fill_container"、height: "fill_container"にします
サイズの決定
| 質問 | 回答 |
|---|---|
| いっぱいに広げますか? | "fill_container" |
| コンテンツに合わせて縮小しますか? | "fit_content" |
| 正確なサイズを指定しますか? | 数値(px) |
デザインタイプごとのサイズ
| タイプ | 幅 | 高さ |
|---|---|---|
| ランディングページ | 1200 | 0(自動) |
| モバイル画面 | 375 | 812 |
| ダッシュボード | 1200 | 0(自動) |
デザイン原則
タイポグラフィ
Display: 40-56px 700 letterSpacing: -1.5 lineHeight: 1.1 "Space Grotesk"
Heading: 28-36px 700 letterSpacing: -0.5 lineHeight: 1.2 "Space Grotesk"
Subheading: 20-24px 600 letterSpacing: -0.25 lineHeight: 1.3 "Space Grotesk"
Body: 15-18px 400 letterSpacing: 0 lineHeight: 1.5 "Inter"
Caption: 13-14px 400 letterSpacing: 0 lineHeight: 1.4 "Inter"
CJK:"Noto Sans SC/JP/KR" を使用し、lineHeight は 1.3 以上、letterSpacing は常に 0 にします。
色
Primary text: #111111 Secondary: #6B7280 Subtle: #9CA3AF
Background: #FFFFFF Surface: #F9FAFB Border: #E5E7EB
彩度の高い色は最大 2 色にします。WCAG AA:本文は 4.5:1、大きな文字は 3:1。暗い背景には #000000 ではなく #0F172A を使用します。
余白(8px グリッド)
Related: 8-16px Components: 16-24px
Groups: 24-32px Sections: 48-80px Page padding: 80px
影
// Subtle (cards)
{ "type": "shadow", "offsetY": 1, "blur": 3, "color": "rgba(0,0,0,0.05)" }
// Medium (dropdowns)
{ "type": "shadow", "offsetY": 4, "blur": 12, "color": "rgba(0,0,0,0.08)" }
// Elevated (modals)
{ "type": "shadow", "offsetY": 8, "blur": 24, "spread": -4, "color": "rgba(0,0,0,0.12)" }
コピーのルール
見出し:2~6 語。サブタイトル:最大 15 語。ボタン:1~3 語。lorem ipsum は使用しないでください。アイコンとして絵文字を使用しないでください。
段階的なワークフロー
複数のセクションからなる複雑なページでは、スケルトン → コンテンツ → 調整の 3 ステップの流れを使用します。
| ステップ | MCP ツール | CLI 相当の操作 |
|---|---|---|
| 1. セクション構造を作成する | design_skeleton | op design:skeleton '<json>' |
| 2. 各セクションに内容を追加する | design_content(postProcess: true を指定) | op design:content <section-id> '<json>' |
| 3. 検証して自動修正する | design_refine | op design:refine --root-id <id> |
design:refine はアイコン名を SVG パスに解決し、レイアウトの問題を修正して、ツリーを検証します。必ず最後のステップとして実行してください。
テンプレートから始める
空のフレームから始めるのが最速とは限りません。op templates で、チュートリアル、比較、カルーセル、16:9 のプレゼンテーションデッキなど、提供されているカタログを一覧表示できます。op use-template <id> でテンプレートを取り込めます。手つかずのスターターページを置き換え、既存の要素がある場合はその右側にボードを追加します。テンプレートには独自のパレットが含まれているため、ハードコードされた色ではなく、実際の変数に基づいてボードが表示されます。
op styles では、固定して使えるスタイルガイドを一覧表示できます。ユーザーがインポートした DESIGN.md も含まれます。ユーザー独自のガイドがある場合は、それを優先してください。ユーザー自身が選んだ資料だからです。
| ステップ | MCP ツール | CLI 相当の操作 |
|---|---|---|
| テンプレートを閲覧する | list_scene_templates | op templates [--scene S] [--tag T] |
| テンプレートから始める | use_scene_template | op use-template <id> |
| スタイル素材を閲覧する | list_style_guides | op styles [<id>] |
結果の出力
ユーザーが開けないデザインは完成していません。出力する予定を説明するのではなく、実際にエクスポートしてください。
| 目的 | MCP ツール | CLI の同等コマンド |
|---|---|---|
| 1 つのノード/ページ/選択範囲 | export_item, export_nodes | op export --output PATH --format png|pdf |
| プレゼンテーションデッキ | export_deck | op export-deck --output PATH --format pptx|html|pdf |
| ページ上のすべてのフレーム | export_frames | op export-frames --output-dir DIR |
| デッキを書き出す前の確認 | get_deck_boards | — |
デッキでは、編集可能な PowerPoint 形式の pptx が適切なデフォルトです。html は自己完結型の 1 ファイルで、pdf はスライド 1 枚につき 1 ページです。非表示のボードは仕様上スキップされます。
出力先の引数は outputPath/outputDir であり、filePath ではありません。
この API では、filePath は呼び出しの対象となる .op ドキュメントを常に意味します。そのため、出力先に filePath を使うと、存在しないドキュメントに対して呼び出しが実行されます。
コード生成パイプライン
デザインツリーから、差分に対応したフレームワーク対応のコードを生成するには、次の手順を実行します。
| 手順 | CLI コマンド | MCP ツール | 説明 |
|---|---|---|---|
| 1. 計画 | op codegen:plan '<json>' | codegen_plan | チャンク、依存関係、共有スタイル、ルートレイアウトを宣言します |
| 2. 送信 | op codegen:submit <planId> '<json>' | codegen_submit_chunk | 個々のノードの生成コードを送信します |
| 3. 組み立て | op codegen:assemble <planId> --framework react | codegen_assemble | すべてのチャンクを最終出力にまとめます |
| 4. クリーンアップ | op codegen:clean <planId> | codegen_clean | サーバー側のコード生成状態を消去します |
codegen:plan は planId を返します。同じパイプライン内のすべての送信、組み立て、クリーンアップコマンドに、その値を渡してください。
計画 JSON の形式:
{
"chunks": [{
"id": "root",
"name": "Root",
"nodeIds": ["frame-1"],
"role": "root",
"suggestedComponentName": "Root",
"dependencies": []
}],
"sharedStyles": [],
"rootLayout": { "direction": "column", "gap": 0, "responsive": true }
}
送信 JSON の形式:
{
"chunkId": "root",
"code": "export function Root() { return <main />; }",
"contract": {
"chunkId": "root",
"componentName": "Root",
"exportedProps": [],
"slots": [],
"cssClasses": [],
"cssVariables": [],
"imports": []
}
}
対応フレームワーク:react、html、vue、svelte、flutter、swiftui、compose、react-native。
複数ページのドキュメント
op page list # List all pages with IDs
op page add --name "Settings" # Add a new page
op page remove <page-id> # Remove a page
op page rename <page-id> 'New Name' # Rename a page
op page reorder <page-id> 2 # Move page to index 2
op page duplicate <page-id> # Clone page with new IDs
任意のコマンドで --page <id> を使うと、特定のページを対象にできます。それ以外のほとんどのコマンドはアクティブなページを使用します。例外は codegen:plan で、デフォルトでは最初のページを使用します。
よく使われるパターン
以下のパターンは、op insert --parent コマンドの例です。どれもコピーしてすぐに使用できます。
ナビゲーションバー
NAV=$(op insert --parent "$ROOT" '{"type":"frame","role":"navbar","width":"fill_container","height":72,"layout":"horizontal","padding":[0,80],"justifyContent":"space_between","alignItems":"center","fill":[{"type":"solid","color":"#FFFFFF"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#F3F4F6"}]}}' | ID)
op insert --parent "$NAV" '{"type":"text","content":"Brand","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"}'
LINKS=$(op insert --parent "$NAV" '{"type":"frame","role":"nav-links","layout":"horizontal","gap":32,"width":"fit_content","height":"fit_content"}' | ID)
op insert --parent "$LINKS" '{"type":"text","role":"nav-link","content":"Features","fontSize":15}'
op insert --parent "$LINKS" '{"type":"text","role":"nav-link","content":"Pricing","fontSize":15}'
CTA=$(op insert --parent "$NAV" '{"type":"rectangle","role":"button","padding":[10,24],"cornerRadius":8,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}' | ID)
op insert --parent "$CTA" '{"type":"text","content":"Get Started","fontSize":14,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}'
ヒーローセクション
HERO=$(op insert --parent "$ROOT" '{"type":"frame","role":"hero","width":"fill_container","height":"fit_content","layout":"vertical","padding":[100,80],"gap":24,"alignItems":"center"}' | ID)
op insert --parent "$HERO" '{"type":"text","role":"heading","content":"Build something great","fontSize":56,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-1.5,"lineHeight":1.1,"textGrowth":"fixed-width","width":800}'
op insert --parent "$HERO" '{"type":"text","role":"subheading","content":"The modern platform for teams who ship fast.","fontSize":18,"textAlign":"center","lineHeight":1.6,"textGrowth":"fixed-width","width":560,"fill":[{"type":"solid","color":"#6B7280"}]}'
BTNS=$(op insert --parent "$HERO" '{"type":"frame","layout":"horizontal","gap":12,"width":"fit_content","height":"fit_content"}' | ID)
B1=$(op insert --parent "$BTNS" '{"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}' | ID)
op insert --parent "$B1" '{"type":"text","content":"Start Free","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}'
B2=$(op insert --parent "$BTNS" '{"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#F3F4F6"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}' | ID)
op insert --parent "$B2" '{"type":"text","content":"View Demo","fontSize":16,"fontWeight":600}'
フィーチャーカード(横並びのグリッドでは、すべてのカードで fill_container を使用する必要があります)
CARD=$(op insert --parent "$GRID" '{"type":"rectangle","role":"feature-card","width":"fill_container","height":"fill_container","layout":"vertical","padding":28,"gap":16,"cornerRadius":16,"fill":[{"type":"solid","color":"#F9FAFB"}]}' | ID)
op insert --parent "$CARD" '{"type":"path","name":"ZapIcon","width":24,"height":24,"fill":[{"type":"solid","color":"#111111"}]}'
op insert --parent "$CARD" '{"type":"text","content":"Lightning Fast","fontSize":20,"fontWeight":600}'
op insert --parent "$CARD" '{"type":"text","role":"body-text","content":"Sub-second builds with smart caching.","fontSize":15,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]}'
フォーム入力欄
GRP=$(op insert --parent "$FORM" '{"type":"frame","role":"form-group","layout":"vertical","gap":8,"width":"fill_container"}' | ID)
op insert --parent "$GRP" '{"type":"text","role":"label","content":"Email","fontSize":14,"fontWeight":500}'
INP=$(op insert --parent "$GRP" '{"type":"rectangle","role":"form-input","width":"fill_container","height":48,"cornerRadius":10,"layout":"horizontal","padding":[0,16],"gap":10,"alignItems":"center","fill":[{"type":"solid","color":"#F9FAFB"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#E5E7EB"}]}}' | ID)
op insert --parent "$INP" '{"type":"path","name":"MailIcon","width":18,"height":18,"fill":[{"type":"solid","color":"#9CA3AF"}]}'
op insert --parent "$INP" '{"type":"text","content":"[email protected]","fontSize":15,"fill":[{"type":"solid","color":"#9CA3AF"}]}'
フッター
FOOTER=$(op insert --parent "$ROOT" '{"type":"frame","role":"footer","width":"fill_container","height":"fit_content","layout":"horizontal","padding":[48,80],"gap":80,"fill":[{"type":"solid","color":"#F9FAFB"}]}' | ID)
COL1=$(op insert --parent "$FOOTER" '{"type":"frame","layout":"vertical","gap":16,"width":240}' | ID)
op insert --parent "$COL1" '{"type":"text","content":"Brand","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"}'
op insert --parent "$COL1" '{"type":"text","content":"Building the future of design.","fontSize":14,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]}'
COL2=$(op insert --parent "$FOOTER" '{"type":"frame","layout":"vertical","gap":12,"width":"fit_content"}' | ID)
op insert --parent "$COL2" '{"type":"text","content":"Product","fontSize":14,"fontWeight":600}'
op insert --parent "$COL2" '{"type":"text","content":"Features","fontSize":14,"fill":[{"type":"solid","color":"#6B7280"}]}'
op insert --parent "$COL2" '{"type":"text","content":"Pricing","fontSize":14,"fill":[{"type":"solid","color":"#6B7280"}]}'
よくある間違い
| 間違い | 修正方法 |
|---|---|
| レイアウトコンテナー内で x/y を設定する | x/y を削除します。エンジンが自動で配置します |
| カードごとに幅の指定方法が異なる | すべての兄弟要素で同じサイズ指定(fill_container)を使用します |
fit_content の親に fill_container の子を配置する | 固定幅を指定するか、親を fill_container に変更します |
純粋な黒のテキスト #000000 | #111111 または #0F172A を使用します |
| 濃いドロップシャドウ | 控えめな rgba(0,0,0,0.05-0.12) を使用します |
| アイコンとして絵文字を使用する | アイコン名を指定したパスノードを使用します |
| Lorem ipsum のプレースホルダー | 現実的で簡潔な文言を記述します |
| テキストに固定の高さを設定する | 代わりに textGrowth: "fixed-width" を使用します |
| CJK に Space Grotesk を使用する | "Noto Sans SC/JP/KR" を使用します |
| CJK の letterSpacing を負の値にする | CJK のテキストでは常に 0 にします |
| 挿入後のポスト処理がない | ツリーの構築後に op design:refine --root-id <id> を実行します |
| アイコンを挿入したのに表示されない | パスノードで SVG を解決するには、design:refine または --post-process が必要です |
| インラインの子要素を後から参照できない | 個別のバインドされた I() 呼び出しで挿入します |
MCP で postProcess: true が指定されていない | MCP ツールの呼び出しでは必ず設定します |
完全な例 — op insert ワークフロー(推奨)
op insert --parent を使用して、モバイルのログインページを完成させます。これが最も確実な方法です。
#!/bin/bash
set -e
ID() { python3 -c "import sys,json; print(json.load(sys.stdin)['nodeId'])"; }
# Root frame (mobile)
ROOT=$(op insert '{"type":"frame","name":"Login","width":375,"height":812,"layout":"vertical","fill":[{"type":"solid","color":"#FFFFFF"}]}' | ID)
# Header
TOP=$(op insert --parent "$ROOT" '{"type":"frame","width":"fill_container","height":"fit_content","layout":"vertical","padding":[80,32,40,32],"gap":14,"alignItems":"center"}' | ID)
op insert --parent "$TOP" '{"type":"path","name":"ShieldIcon","width":48,"height":48,"fill":[{"type":"solid","color":"#6366F1"}]}'
op insert --parent "$TOP" '{"type":"text","content":"Welcome Back","fontSize":28,"fontWeight":700,"fontFamily":"Space Grotesk","letterSpacing":-0.5,"textAlign":"center"}'
# Form
FORM=$(op insert --parent "$ROOT" '{"type":"frame","width":"fill_container","height":"fit_content","layout":"vertical","padding":[0,32],"gap":20}' | ID)
# Email input
GRP=$(op insert --parent "$FORM" '{"type":"frame","role":"form-group","layout":"vertical","gap":8,"width":"fill_container"}' | ID)
op insert --parent "$GRP" '{"type":"text","role":"label","content":"Email","fontSize":14,"fontWeight":500}'
INP=$(op insert --parent "$GRP" '{"type":"rectangle","role":"form-input","width":"fill_container","height":48,"cornerRadius":10,"layout":"horizontal","padding":[0,16],"gap":10,"alignItems":"center","fill":[{"type":"solid","color":"#F9FAFB"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#E5E7EB"}]}}' | ID)
op insert --parent "$INP" '{"type":"path","name":"MailIcon","width":18,"height":18,"fill":[{"type":"solid","color":"#9CA3AF"}]}'
op insert --parent "$INP" '{"type":"text","content":"[email protected]","fontSize":15,"fill":[{"type":"solid","color":"#9CA3AF"}]}'
# Login button
BTN=$(op insert --parent "$FORM" '{"type":"rectangle","role":"button","width":"fill_container","height":50,"cornerRadius":12,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}' | ID)
op insert --parent "$BTN" '{"type":"text","content":"Sign In","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}'
# IMPORTANT: resolve icons + validate layout
op design:refine --root-id "$ROOT"
DSL の例 — ランディングページ
DSL は、操作を中心とした編集に適しています。親と子の操作を分けることで、後から更新やコピーを行うためのバインディングを利用できます。
root=I(null, {"type":"frame","name":"Landing","width":1200,"height":0,"layout":"vertical","fill":[{"type":"solid","color":"#FFFFFF"}]})
nav=I(root, {"type":"frame","role":"navbar","width":"fill_container","height":72,"layout":"horizontal","padding":[0,80],"justifyContent":"space_between","alignItems":"center"})
I(nav, {"type":"text","content":"Acme","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"})
links=I(nav, {"type":"frame","role":"nav-links","layout":"horizontal","gap":32,"width":"fit_content","height":"fit_content"})
I(links, {"type":"text","role":"nav-link","content":"Features","fontSize":15})
I(links, {"type":"text","role":"nav-link","content":"Pricing","fontSize":15})
cta=I(nav, {"type":"rectangle","role":"button","padding":[10,24],"cornerRadius":8,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})
I(cta, {"type":"text","content":"Get Started","fontSize":14,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})
hero=I(root, {"type":"frame","role":"hero","width":"fill_container","height":"fit_content","layout":"vertical","padding":[100,80],"gap":24,"alignItems":"center"})
I(hero, {"type":"text","role":"heading","content":"Ship faster with Acme","fontSize":56,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-1.5,"lineHeight":1.1,"textGrowth":"fixed-width","width":800})
I(hero, {"type":"text","role":"subheading","content":"Turn ideas into production apps in minutes.","fontSize":18,"textAlign":"center","lineHeight":1.6,"textGrowth":"fixed-width","width":560,"fill":[{"type":"solid","color":"#6B7280"}]})
btns=I(hero, {"type":"frame","layout":"horizontal","gap":12,"width":"fit_content","height":"fit_content"})
b1=I(btns, {"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})
I(b1, {"type":"text","content":"Start Free","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})
b2=I(btns, {"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#F3F4F6"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})
I(b2, {"type":"text","content":"View Demo","fontSize":16,"fontWeight":600})
feat=I(root, {"type":"frame","role":"section","width":"fill_container","height":"fit_content","layout":"vertical","padding":[80,80],"gap":48,"alignItems":"center"})
I(feat, {"type":"text","role":"heading","content":"Everything you need","fontSize":36,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-0.5})
grid=I(feat, {"type":"frame","role":"feature-grid","width":"fill_container","layout":"horizontal","gap":24})
c1=I(grid, {"type":"rectangle","role":"feature-card","width":"fill_container","height":"fill_container","layout":"vertical","padding":28,"gap":16,"cornerRadius":16,"fill":[{"type":"solid","color":"#F9FAFB"}]})
I(c1, {"type":"path","name":"ZapIcon","width":24,"height":24,"fill":[{"type":"solid","color":"#111111"}]})
I(c1, {"type":"text","content":"Lightning Fast","fontSize":20,"fontWeight":600})
I(c1, {"type":"text","role":"body-text","content":"Sub-second builds with smart caching.","fontSize":15,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]})
c2=C(c1, grid, {})
U(c2+"/0", {"name":"ShieldIcon"})
U(c2+"/1", {"content":"Enterprise Security"})
U(c2+"/2", {"content":"SOC 2 certified with end-to-end encryption."})
c3=C(c1, grid, {})
U(c3+"/0", {"name":"GitBranchIcon"})
U(c3+"/1", {"content":"Git-Native Workflow"})
U(c3+"/2", {"content":"Preview deploys on every push with instant rollback."})