# Openpencil Design

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

- **Type:** Skill
- **Install:** `agentstack add skill-zseven-w-openpencil-skill-openpencil-design`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ZSeven-W](https://agentstack.voostack.com/s/zseven-w)
- **Installs:** 0
- **Category:** [Search](https://agentstack.voostack.com/c/search)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ZSeven-W](https://github.com/ZSeven-W)
- **Source:** https://github.com/ZSeven-W/openpencil-skill/tree/main/skills/openpencil-design

## Install

```sh
agentstack add skill-zseven-w-openpencil-skill-openpencil-design
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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

```bash
# App control
op start [--desktop|--web]           # Launch app
op stop                              # Stop running instance
op status                            # Check if running

# Document
op open [file.op]                    # Open file or connect to live canvas
op save                     # Save current document
op get [--depth N] [--pretty]        # Get document tree
op selection [--depth N]             # Get current canvas selection
op read-nodes [id...] [--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 '' [--parent P]     # Insert node (--index N, --post-process)
op update  ''              # Update node
op delete                        # Delete node
op move   [index]        # Move node
op copy                  # Deep-copy node
op replace  ''             # Replace node

# Batch design
op design ''                    # Batch design DSL (inline, @file, or stdin) [--canvas-width N]

# Layered workflow
op design:skeleton ''          # Create section structure
op design:content  ''      # Populate section content
op design:refine --root-id       # Validate + auto-fix (resolves icons) [--canvas-width N]

# Import
op import:svg  [--parent P]       # Import SVG as editable nodes
op import:figma  [--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                   # Remove a page
op page rename  ''         # Rename a page
op page reorder           # Move page to position
op page duplicate                # Clone page with new IDs

# Variables & Themes
op vars / op vars:set ''       # Variables (--replace to replace all)
op themes / op themes:set ''   # Themes (--replace to replace all)
op theme:save          # Save current theme as preset file
op theme:load          # Load a theme preset file
op theme:list             # List .optheme presets in directory

# Codegen pipeline
op codegen:plan ''             # Submit codegen plan (framework, rootIds, options)
op codegen:submit ''           # Submit a code chunk for a node
op codegen:assemble [--framework F]  # Assemble all submitted chunks into final output
op codegen:clean                     # Clear codegen state
```

Global flags: `--file `, `--page `, `--pretty`. Inputs: inline string, `@filepath`, or `-` (stdin).

## Building Designs — Two 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.

```bash
# 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. Best for simple, flat structures.

> **Limitation:** The DSL parser cannot handle deeply nested JSON (e.g., `children` arrays with nested objects, or multiple levels of array nesting). Keep each `I()` call to a **single level of nesting**. For complex nodes with children, use separate `I()` calls for parent and children, or use `op insert --parent`.

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

| Op | Syntax | Action |
|----|--------|--------|
| `I` | `name=I(parent, { node })` | Insert |
| `U` | `U(ref, { updates })` | Update |
| `C` | `name=C(source, parent, { overrides })` | Copy |
| `R` | `name=R(ref, { node })` | Replace |
| `M` | `M(ref, parent, index?)` | Move |
| `D` | `D(ref)` | Delete |
| `G` | `name=G(parent, "search", "query")` | Generate image via search |

**DSL safe pattern** — always insert parent and children separately:

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

## STRICT JSON Rules

When emitting PenNode JSON (via `op insert`, `op design`, `batch_design`, `insert_node`), you MUST produce strictly valid JSON. Common mistakes that break 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

```json
{
  "type": "frame|rectangle|text|ellipse|line|polygon|path|image|icon_font|group|ref",
  "name": "Display Name",
  "role": "semantic-role",
  "x": 0, "y": 0,
  "rotation": 0, "opacity": 1, "visible": true
}
```

### Container Properties (frame, rectangle, group, ellipse)

```json
{
  "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

```json
{
  "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)

```json
{ "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)

```json
{ "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 ` 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

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

AI image placeholders (resolved by `design:refine`):

```json
{ "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

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

### Icon Font

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

### Line

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

### Fill Types

```json
{ "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)

```json
{ "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.

| Category | Roles |
|----------|-------|
| **Layout** | `section`, `row`, `column`, `centered-content`, `divider`, `spacer` |
| **Navigation** | `navbar`, `nav-links`, `nav-link` |
| **Interactive** | `button`, `icon-button`, `badge`, `tag`, `pill`, `input`, `form-input`, `search-bar` |
| **Cards** | `card`, `feature-card`, `stat-card`, `pricing-card`, `image-card` |
| **Content** | `hero`, `feature-grid`, `cta-section`, `footer`, `testimonial`, `stats-section` |
| **Typography** | `heading`, `subheading`, `body-text`, `caption`, `label` |
| **Media** | `avatar`, `icon`, `phone-mockup`, `screenshot-frame` |
| **Table** | `table`, `table-row`, `table-header`, `table-cell` |
| **Form** | `form-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

| Question | Answer |
|----------|--------|
| Stretch to fill? | `"fill_container"` |
| Shrink to content? | `"fit_content"` |
| Exact size? | number (px) |

### Design Type Sizing

| Type | Width | Height |
|------|-------|--------|
| Landing page | 1200 | 0 (auto) |
| Mobile screen | 375 | 812 |
| Dashboard | 1200 | 0 (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

```json
// 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:

| Step | MCP Tool | CLI Equivalent |
|------|----------|----------------|
| 1. Create section structure | `design_skeleton` | `op design:skeleton ''` |
| 2. Populate each section | `design_content` (with `postProcess: true`) | `op design:content  ''` |
| 3. Validate + auto-fix | `design_refine` | `op design:refine --root-id ` |

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

## Codegen Pipeline

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

| Step | CLI Command | MCP Tool | Description |
|------|------------|----------|-------------|
| 1. Plan | `op codegen:plan ''` | `codegen_plan` | Declare framework, root node IDs, and options |
| 2. Submit | `op codegen:submit ''` | `codegen_submit_chunk` | Submit generated code for individual nodes |
| 3. Assemble | `op codegen:assemble --framework react` | `codegen_assemble` | Combine all chunks into the final output |
| 4. Clean | `op codegen:clean` | `codegen_clean` | Clear server-side codegen state |

The plan JSON shape:
```json
{ "framework": "react", "root

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ZSeven-W](https://github.com/ZSeven-W)
- **Source:** [ZSeven-W/openpencil-skill](https://github.com/ZSeven-W/openpencil-skill)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-zseven-w-openpencil-skill-openpencil-design
- Seller: https://agentstack.voostack.com/s/zseven-w
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
