本文へ移動
cccskills
無料GitHub で公開

openpencil-design

Use when designing UI with OpenPencil — creating layouts via op CLI, batch design DSL, sandboxed JS scripts, or MCP tools. Covers PenNode schema, semantic roles, typography, color, spacing, and common component patterns.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md35.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

OpenPencil Design

Generate production-quality vector designs by writing PenNode JSON trees. Use the op CLI or MCP tools to create, read, update, and delete nodes on the OpenPencil canvas.

When to Use

  • Creating or modifying UI designs in .op files
  • Using the op CLI to script design operations
  • Designing via MCP tools (batch_design, insert_node, design_skeleton)
  • Need reference for PenNode schema, roles, or layout rules

Quick Reference — 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

Global flags: --port <n>, --file <path>, --page <id>, --pretty. Inputs: inline string, @filepath, or - (stdin).

Building Designs — Three Approaches

Approach 1: op insert (Recommended)

The most reliable way to build designs. Use --parent to specify the parent node. Capture the returned nodeId to reference later. Always finish with design:refine to resolve icons and validate layout.

# 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"

Approach 2: Batch Design DSL

One operation per line. Bind results with name= for later reference. Nested children arrays are supported, but insert children separately when later operations need stable bindings for them.

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" })
OpSyntaxAction
Iname=I(parent, { node })Insert
UU(ref, { updates })Update
Cname=C(source, parent, { overrides })Copy
Rname=R(ref, { node })Replace
MM(ref, parent, index?)Move
DD(ref)Delete
Gname=G(parent, "search", "query")Generate image via search

Binding-friendly pattern — insert parent and children separately when they need later references:

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"}]})

Approach 3: Script mode (Recommended for repeated structures)

Use sandboxed JavaScript when loops or data-driven repetition are clearer than repeated DSL operations:

op design @ui.js                 # .js / .mjs implies script mode
op design '<js code>' --script   # inline or stdin; --script follows the payload

Via MCP, call batch_design with exactly one of nodes_json, operations, or script. Script mode provides two recording builders:

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) inserts a node and returns its remapped ID. K(kitComponentId, parent, overrides) instantiates a UI-kit component and also returns its remapped ID; use starter/<id>, shadcn/<id>, or <kit-id>/<component-id>. Use null for a root; otherwise pass an ID returned by an earlier recording call. C, U, D, M, R, G, and console.* exist only as no-op compatibility stubs in script mode; use the DSL for update, copy, replace, move, delete, or image-search operations.

Nested children arrays are safe in script mode. Limits are 256 KiB source, 4096 recorded operations, 8 MiB recorded output, 2 seconds of wall-clock evaluation, and 64 MiB of memory.

STRICT JSON Rules

When emitting PenNode JSON (via op insert, the batch DSL, nodes_json, or insert_node), you MUST produce strictly valid JSON. Script mode uses JavaScript object literals instead. Common mistakes that break JSON parsing:

  • Every property MUST have both a key and a value. NEVER emit ": 50 or : 50 without a key name. This often happens when you truncate/reformat — double-check.
  • Every key MUST be a double-quoted non-empty string.
  • fill is ALWAYS an array: "fill": [{"type": "solid", "color": "#hex"}]. Shorthand like "fill": "#hex" works but the array form is the canonical shape.
  • stroke is an object with a fill array: "stroke": {"thickness": 1, "fill": [{"type": "solid", "color": "#hex"}]}. NEVER "stroke": {"thickness": 1, "color": "#hex"} or "stroke": "#hex" (parser auto-converts these but the correct shape is preferred).
  • NO trailing commas before } or ].
  • NO comments inside JSON (// or /* */).
  • Use straight double quotes ", not smart/curly quotes.
  • content for text, NOT text: {"type": "text", "content": "Hello"}.
  • iconFontName for icons, NOT iconName or icon: {"type": "icon_font", "iconFontName": "lock"}.
  • Before finalizing the JSON, mentally verify: every key has a value, every value has a key, all brackets balance.

PenNode Schema

Common Properties

{
  "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
}

Container Properties (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": []
}

Text

{
  "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" }]
}

Rich text: "content": [{ "text": "Bold ", "fontWeight": "bold" }, { "text": "normal" }]

Icons — Two Options

Option A: icon_font (RECOMMENDED — renders directly, no post-processing needed)

{ "type": "icon_font", "name": "Lock Icon", "iconFontName": "lock",
  "width": 20, "height": 20,
  "fill": [{ "type": "solid", "color": "#6B7280" }] }

Field is iconFontName (NOT iconName, NOT icon). Values are lowercase kebab-case Lucide names: 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.

Works in ALL contexts: CLI, MCP tools, or direct .op files — no design:refine required.

Option B: path (requires post-processing)

{ "type": "path", "name": "HeartIcon", "width": 24, "height": 24,
  "fill": [{ "type": "solid", "color": "#111111" }] }

PascalCase + "Icon" suffix. Auto-resolved from Lucide set during post-processing.

Path icons need post-processing. After inserting path nodes, run op design:refine --root-id <id> or use op insert --post-process. Without this, path icons won't render visually. The standalone MCP server (used by ACP agents) does NOT have hook implementations registered, so path icons will NOT resolve there — prefer icon_font in MCP contexts.

Image

{ "type": "image", "src": "https://example.com/photo.jpg", "width": 400, "height": 300,
  "objectFit": "crop", "cornerRadius": 12 }

AI image placeholders (resolved by design:refine):

{ "type": "image", "width": 400, "height": 300,
  "imagePrompt": "A modern office workspace with natural light",
  "imageSearchQuery": "modern office workspace" }

Image adjustments (all -100 to 100): exposure, contrast, saturation, temperature, tint, highlights, shadows.

Polygon

{ "type": "polygon", "polygonCount": 6, "width": 80, "height": 80, "cornerRadius": 4,
  "fill": [{ "type": "solid", "color": "#6366F1" }] }

Icon Font

{ "type": "icon_font", "iconFontName": "lucide:home", "width": 24, "height": 24,
  "fill": [{ "type": "solid", "color": "#111111" }] }

Line

{ "type": "line", "x2": 200, "y2": 0,
  "stroke": { "thickness": 1, "fill": [{ "type": "solid", "color": "#E5E7EB" }] } }

Fill Types

{ "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" }

Image fill modes: fill, fit, crop, tile, stretch. Image fill also supports adjustment filters (exposure, contrast, saturation, etc.).

Ref Node (Component Instance)

{ "type": "ref", "ref": "reusable-frame-id",
  "descendants": { "child-id": { "content": "Override text" } } }

References a frame with reusable: true. Override specific descendant properties via descendants.

Design Variables

Reference with $ prefix: "color": "$primaryColor", "gap": "$spacing".

Semantic Roles

Roles declare intent — the engine applies smart defaults. Always prefer roles over manual styling.

CategoryRoles
Layoutsection, row, column, centered-content, divider, spacer
Navigationnavbar, nav-links, nav-link
Interactivebutton, icon-button, badge, tag, pill, input, form-input, search-bar
Cardscard, feature-card, stat-card, pricing-card, image-card
Contenthero, feature-grid, cta-section, footer, testimonial, stats-section
Typographyheading, subheading, body-text, caption, label
Mediaavatar, icon, phone-mockup, screenshot-frame
Tabletable, table-row, table-header, table-cell
Formform-group

Key defaults:

  • navbar → height: 56-72, horizontal, space_between, center-aligned
  • button → padding: [12, 24], cornerRadius: 8, centered
  • card → vertical, gap: 12, cornerRadius: 12, padding: 24
  • heading → lineHeight: 1.2, letterSpacing: -0.5
  • body-text → fill_container, textGrowth: fixed-width, lineHeight: 1.5

Layout Rules

  1. NEVER set x/y on children inside layout containers — engine positions them
  2. Siblings must use same width strategy — all fill_container or all fixed
  3. NEVER fill_container inside fit_content parent — circular dependency
  4. Cards in horizontal row: ALL width: "fill_container", height: "fill_container"

Sizing Decision

QuestionAnswer
Stretch to fill?"fill_container"
Shrink to content?"fit_content"
Exact size?number (px)

Design Type Sizing

TypeWidthHeight
Landing page12000 (auto)
Mobile screen375812
Dashboard12000 (auto)

Design Principles

Typography

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: use "Noto Sans SC/JP/KR", lineHeight >= 1.3, letterSpacing: 0 always.

Color

Primary text:   #111111       Secondary: #6B7280     Subtle: #9CA3AF
Background:     #FFFFFF       Surface:   #F9FAFB     Border: #E5E7EB

Max 2 saturated colors. WCAG AA: 4.5:1 body, 3:1 large. Dark bg: #0F172A, not #000000.

Spacing (8px grid)

Related:    8-16px     Components: 16-24px
Groups:     24-32px    Sections:   48-80px    Page padding: 80px

Shadows

// 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)" }

Copy Rules

Headlines: 2-6 words. Subtitles: max 15 words. Buttons: 1-3 words. No lorem ipsum. No emoji as icons.

Layered Workflow

For complex multi-section pages, use the three-step skeleton → content → refine flow:

StepMCP ToolCLI Equivalent
1. Create section structuredesign_skeletonop design:skeleton '<json>'
2. Populate each sectiondesign_content (with postProcess: true)op design:content <section-id> '<json>'
3. Validate + auto-fixdesign_refineop design:refine --root-id <id>

design:refine resolves icon names → SVG paths, fixes layout issues, and validates the tree. Always run as the final step.

Starting From a Template

Starting from a blank frame is rarely the fastest route. op templates lists the shipped catalogue — tutorials, comparisons, carousels and the 16:9 presentation decks — and op use-template <id> brings one in: it takes over an untouched starter page, and appends its boards to the right of anything else. The template carries its own palette, so the boards resolve against real variables rather than hard-coded colours.

op styles lists the style guides available to pin, including any DESIGN.md the user imported. Prefer the user's own guide when one exists — it is the material they chose.

StepMCP ToolCLI Equivalent
Browse templateslist_scene_templatesop templates [--scene S] [--tag T]
Start from oneuse_scene_templateop use-template <id>
Browse style assetslist_style_guidesop styles [<id>]

Delivering the Result

A design the user cannot open is not finished. Export it rather than describing what you would export.

GoalMCP ToolCLI Equivalent
One node / page / selectionexport_item, export_nodesop export --output PATH --format png|pdf
A presentation deckexport_deckop export-deck --output PATH --format pptx|html|pdf
Every frame on the pageexport_framesop export-frames --output-dir DIR
Check a deck before writing itget_deck_boards—

For decks, pptx is editable PowerPoint and the right default; html is one self-contained file; pdf is one page per slide. Hidden boards are skipped by design.

The destination argument is outputPath / outputDir, never filePath. filePath means "the .op document this call targets" everywhere in this API, so using it for a destination sends the call at a document that does not exist.

Codegen Pipeline

For incremental, framework-aware code generation from the design tree:

StepCLI CommandMCP ToolDescription
1. Planop codegen:plan '<json>'codegen_planDeclare chunks, dependencies, shared styles, and root layout
2. Submitop codegen:submit <planId> '<json>'codegen_submit_chunkSubmit generated code for individual nodes
3. Assembleop codegen:assemble <planId> --framework reactcodegen_assembleCombine all chunks into the final output
4. Cleanop codegen:clean <planId>codegen_cleanClear server-side codegen state

codegen:plan returns a planId; pass that value to every submit, assemble, and clean command in the same pipeline.

The plan JSON shape:

{
  "chunks": [{
    "id": "root",
    "name": "Root",
    "nodeIds": ["frame-1"],
    "role": "root",
    "suggestedComponentName": "Root",
    "dependencies": []
  }],
  "sharedStyles": [],
  "rootLayout": { "direction": "column", "gap": 0, "responsive": true }
}

The submit JSON shape:

{
  "chunkId": "root",
  "code": "export function Root() { return <main />; }",
  "contract": {
    "chunkId": "root",
    "componentName": "Root",
    "exportedProps": [],
    "slots": [],
    "cssClasses": [],
    "cssVariables": [],
    "imports": []
  }
}

Supported frameworks: react, html, vue, svelte, flutter, swiftui, compose, react-native.

Multi-Page Documents

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

Use --page <id> on any command to target a specific page. Most commands otherwise use the active page; codegen:plan is the exception and defaults to the first page.

Common Patterns

Patterns below show op insert --parent commands. Each pattern is copy-paste ready.

Navbar

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

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}'

Feature Card (in horizontal grid, ALL cards must use 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"}]}'

Form 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":"you@example.com","fontSize":15,"fill":[{"type":"solid","color":"#9CA3AF"}]}'

Footer

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"}]}'

Common Mistakes

MistakeFix
Setting x/y inside layout containerRemove x/y — engine auto-positions
Cards with different width strategiesAll siblings: same sizing (fill_container)
fill_container child in fit_content parentUse fixed width or switch parent to fill_container
Pure black text #000000Use #111111 or #0F172A
Heavy drop shadowsUse subtle rgba(0,0,0,0.05-0.12)
Emoji as iconsUse path nodes with icon names
Lorem ipsum placeholderWrite realistic, concise copy
Fixed height on textUse textGrowth: "fixed-width" instead
Space Grotesk for CJKUse "Noto Sans SC/JP/KR"
Negative letterSpacing on CJKAlways 0 for CJK text
Missing post-process after insertRun op design:refine --root-id <id> after building the tree
Icons inserted but not visiblePath nodes need design:refine or --post-process to resolve SVG
Cannot reference an inline child laterInsert it with a separate bound I() call
Missing postProcess: true in MCPAlways set for MCP tool calls

Full Example — op insert Workflow (Recommended)

Build a complete mobile login page using op insert --parent. This is the most reliable approach.

#!/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":"you@example.com","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 Example — Landing Page

DSL is suitable for operation-oriented edits. Separate parent and child operations provide bindings for later updates and copies.

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."})

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

このスキルの問題を報告する