openpencil/crates/op-cli/assets/skill-bundle.json

13 lines
38 KiB
JSON

{
"version": "__OPENPENCIL_VERSION__",
"files": {
"skills/openpencil-design/SKILL.md": "---\nname: openpencil-design\ndescription: 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.\n---\n\n# OpenPencil Design\n\nGenerate 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.\n\n## When to Use\n\n- Creating or modifying UI designs in `.op` files\n- Using the `op` CLI to script design operations\n- Designing via MCP tools (`batch_design`, `insert_node`, `design_skeleton`)\n- Need reference for PenNode schema, roles, or layout rules\n\n## Quick Reference — `op` CLI\n\n```bash\n# App control\nop start [--desktop|--web] # Launch app\nop stop # Stop running instance\nop status # Check if running\n\n# Document\nop open [file.op] # Open file or connect to live canvas\nop save <file.op> # Save current document\nop get [--depth N] [--pretty] # Get document tree\nop selection [--depth N] # Get current canvas selection\nop read-nodes [id...] [--depth N] [--vars] # Read node subtree(s) with optional variable resolution\nop layout [--parent P] [--depth N] # Snapshot layout tree with computed positions\nop find-space [--direction D] [--width N] [--height N] # Find empty space on canvas\n\n# Node operations\nop insert '<json>' [--parent P] # Insert node (--index N, --post-process)\nop update <id> '<json>' # Update node\nop delete <id> # Delete node\nop move <id> <parent> [index] # Move node\nop copy <id> <parent> # Deep-copy node\nop replace <id> '<json>' # Replace node\n\n# Batch design\nop design '<dsl>' # Batch design DSL (inline, @file, or stdin) [--canvas-width N]\nop design @ui.js # Sandboxed JS script: I(parent, obj) + loops (.js/.mjs implies --script)\nop design '<js>' --script # Same, inline/stdin (flag must FOLLOW the payload)\n\n# Layered workflow\nop design:skeleton '<json>' # Create section structure\nop design:content <id> '<json>' # Populate section content\nop design:refine --root-id <id> # Validate + auto-fix (resolves icons) [--canvas-width N]\n\n# Import\nop import:svg <file.svg> [--parent P] # Import SVG as editable nodes\nop import:figma <file.fig> [--out out.op] # Convert Figma .fig to .op document\n\n# Pages\nop page list # List all pages\nop page add [--name N] # Add a new page\nop page remove <id> # Remove a page\nop page rename <id> '<name>' # Rename a page\nop page reorder <id> <index> # Move page to position\nop page duplicate <id> # Clone page with new IDs\n\n# Variables & Themes\nop vars / op vars:set '<json>' # Variables (--replace to replace all)\nop themes / op themes:set '<json>' # Themes (--replace to replace all)\nop theme:save <file.optheme> # Save current theme as preset file\nop theme:load <file.optheme> # Load a theme preset file\nop theme:list <directory> # List .optheme presets in directory\n\n# Codegen pipeline\nop codegen:plan '<json>' # Submit codegen plan (framework, rootIds, options)\nop codegen:submit '<json>' # Submit a code chunk for a node\nop codegen:assemble [--framework F] # Assemble all submitted chunks into final output\nop codegen:clean # Clear codegen state\n```\n\nGlobal flags: `--file <path>`, `--page <id>`, `--pretty`. Inputs: inline string, `@filepath`, or `-` (stdin).\n\n## Building Designs — Three Approaches\n\n### Approach 1: `op insert` (Recommended)\n\nThe 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.\n\n```bash\n# Create root frame, capture its ID\nROOT=$(op insert '{\"type\":\"frame\",\"name\":\"Page\",\"width\":375,\"height\":812,\"layout\":\"vertical\"}' \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['nodeId'])\")\n\n# Insert children using --parent\nop insert --parent \"$ROOT\" '{\"type\":\"text\",\"content\":\"Hello\",\"fontSize\":28,\"fontWeight\":700}'\n\n# Post-process: resolve icons, validate layout\nop design:refine --root-id \"$ROOT\"\n```\n\n### Approach 2: Batch Design DSL\n\nOne operation per line. Bind results with `name=` for later reference. Best for simple, flat structures.\n\n> **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`.\n\n```\nroot=I(null, { \"type\": \"frame\", \"width\": 1200, \"layout\": \"vertical\" })\nnav=I(root, { \"type\": \"frame\", \"role\": \"navbar\", \"height\": 72 })\nU(nav, { \"fill\": [{\"type\": \"solid\", \"color\": \"#FFFFFF\"}] })\ncard2=C(card1, grid, { \"name\": \"Card 2\" })\nM(sidebar, main, 0)\nD(old_section)\nR(old_btn, { \"type\": \"rectangle\", \"role\": \"button\" })\n```\n\n| Op | Syntax | Action |\n|----|--------|--------|\n| `I` | `name=I(parent, { node })` | Insert |\n| `U` | `U(ref, { updates })` | Update |\n| `C` | `name=C(source, parent, { overrides })` | Copy |\n| `R` | `name=R(ref, { node })` | Replace |\n| `M` | `M(ref, parent, index?)` | Move |\n| `D` | `D(ref)` | Delete |\n| `G` | `name=G(parent, \"search\", \"query\")` | Generate image via search |\n\n**DSL safe pattern** — always insert parent and children separately:\n\n```\nbtn=I(form, {\"type\":\"rectangle\",\"role\":\"button\",\"width\":\"fill_container\",\"height\":50,\"cornerRadius\":12,\"fill\":[{\"type\":\"solid\",\"color\":\"#111111\"}],\"layout\":\"horizontal\",\"justifyContent\":\"center\",\"alignItems\":\"center\"})\nI(btn, {\"type\":\"text\",\"content\":\"Submit\",\"fontSize\":16,\"fontWeight\":600,\"fill\":[{\"type\":\"solid\",\"color\":\"#FFFFFF\"}]})\n```\n\n### Approach 3: Script mode (loops & data-driven — RECOMMENDED for repeated structures)\n\n```\nop design @ui.js # .js / .mjs file implies script mode\nop design '<js code>' --script # inline or stdin; --script must FOLLOW the payload\n```\n\nVia MCP: call `batch_design` with the `script` argument (exactly ONE of `nodes_json` | `operations` | `script` per call). This is the SAME protocol OpenPencil's internal design agents use; follow the same contract:\n\nWrite a JavaScript program (no prose, no markdown fences) that builds the design by calling the global function I(parent, node):\n\n```\nconst id = I(parent, { ...node... }); // inserts node, RETURNS its id (a string)\n```\n\n`parent` is null for a root frame, otherwise an id returned by an EARLIER I(...) call. A node is a child of X only if you call I(X, {...}).\n\nI(...) is the ONLY function available — there is no console, and no other builder. Do not call console.log or any helper; just call I(...). Script mode is insert-only; use the DSL ops (Approach 2) for update/move/delete.\n\nUSE REAL JAVASCRIPT — const/let, arrays of data, and for...of / .forEach loops — to generate repeated structure (table rows, nav items, cards, list items) by looping over a data array. PREFER a loop over copy-pasting near-identical I(...) calls.\n\nEach node object starts with type (\"frame\"/\"text\"/\"rectangle\"/\"ellipse\"/\"path\"/\"icon_font\") and uses camelCase props (cornerRadius, fontSize, fontWeight, justifyContent, alignItems, clipContent). Do NOT set x/y on children inside layout frames.\n\nExample:\n\n```js\nconst sec = I(null, {type:\"frame\", name:\"Clients\", layout:\"vertical\", width:\"fill_container\", gap:0});\nconst tbl = I(sec, {type:\"frame\", layout:\"vertical\", width:\"fill_container\"});\nconst rows = [{name:\"Alice Chen\", status:\"Active\"}, {name:\"Bob Ito\", status:\"VIP\"}];\nfor (const r of rows) {\n const row = I(tbl, {type:\"frame\", layout:\"horizontal\", width:\"fill_container\", padding:[12,16]});\n const c1 = I(row, {type:\"frame\", width:\"fill_container\"}); I(c1, {type:\"text\", content:r.name});\n const c2 = I(row, {type:\"frame\", width:\"fill_container\"}); I(c2, {type:\"text\", content:r.status});\n}\n```\n\nGenerate EVERY row/card/item with realistic values. Output ONLY the JavaScript program.\n\nCLI/MCP notes:\n\n- **Nested `children` arrays are SAFE here** — the engine serializes each object to perfect single-line JSON, so the DSL's single-level-of-nesting limitation does not apply. Prefer separate `I()` calls anyway when you need the parent binding.\n- **Node ids are remapped on insert** — do not reference an authored `\"id\"` after the call; only the returned binding is meaningful, and only inside this script.\n- **Limits:** 256 KiB source, 4096 inserts, 8 MiB recorded output, 2 s CPU, 64 MiB memory. A script truncated mid-statement is best-effort repaired (the complete prefix still runs).\n\n## STRICT JSON Rules\n\nWhen emitting PenNode JSON (via `op insert`, `op design`, `batch_design`, `insert_node`), you MUST produce strictly valid JSON. Common mistakes that break parsing:\n\n- **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.\n- **Every key MUST be a double-quoted non-empty string.**\n- **`fill` is ALWAYS an array**: `\"fill\": [{\"type\": \"solid\", \"color\": \"#hex\"}]`. Shorthand like `\"fill\": \"#hex\"` works but the array form is the canonical shape.\n- **`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).\n- **NO trailing commas** before `}` or `]`.\n- **NO comments** inside JSON (`//` or `/* */`).\n- Use **straight double quotes** `\"`, not smart/curly quotes.\n- **`content` for text, NOT `text`**: `{\"type\": \"text\", \"content\": \"Hello\"}`.\n- **`iconFontName` for icons, NOT `iconName` or `icon`**: `{\"type\": \"icon_font\", \"iconFontName\": \"lock\"}`.\n- Before finalizing the JSON, mentally verify: every key has a value, every value has a key, all brackets balance.\n\n## PenNode Schema\n\n### Common Properties\n\n```json\n{\n \"type\": \"frame|rectangle|text|ellipse|line|polygon|path|image|icon_font|group|ref\",\n \"name\": \"Display Name\",\n \"role\": \"semantic-role\",\n \"x\": 0, \"y\": 0,\n \"rotation\": 0, \"opacity\": 1, \"visible\": true\n}\n```\n\n### Container Properties (frame, rectangle, group, ellipse)\n\n```json\n{\n \"width\": 400, // number | \"fill_container\" | \"fit_content\"\n \"height\": 300,\n \"layout\": \"vertical\", // \"none\" | \"vertical\" | \"horizontal\"\n \"gap\": 16,\n \"padding\": [16, 24], // number | [v, h] | [top, right, bottom, left]\n \"justifyContent\": \"center\", // \"start\" | \"center\" | \"end\" | \"space_between\" | \"space_around\"\n \"alignItems\": \"center\", // \"start\" | \"center\" | \"end\"\n \"clipContent\": true,\n \"cornerRadius\": 12, // number | [tl, tr, br, bl]\n \"fill\": [{ \"type\": \"solid\", \"color\": \"#FFFFFF\" }],\n \"stroke\": { \"thickness\": 1, \"fill\": [{ \"type\": \"solid\", \"color\": \"#E5E7EB\" }], \"align\": \"inside\", \"dashPattern\": [5, 3] },\n \"effects\": [{ \"type\": \"shadow\", \"offsetX\": 0, \"offsetY\": 4, \"blur\": 12, \"spread\": 0, \"color\": \"rgba(0,0,0,0.08)\" }],\n \"children\": []\n}\n```\n\n### Text\n\n```json\n{\n \"type\": \"text\",\n \"content\": \"Hello\", // string or StyledTextSegment[]\n \"fontSize\": 16, \"fontFamily\": \"Inter\", \"fontWeight\": 600,\n \"textAlign\": \"center\", // \"left\" | \"center\" | \"right\"\n \"textGrowth\": \"fixed-width\", // \"auto\" | \"fixed-width\" | \"fixed-width-height\"\n \"lineHeight\": 1.5, \"letterSpacing\": 0,\n \"fill\": [{ \"type\": \"solid\", \"color\": \"#111111\" }]\n}\n```\n\nRich text: `\"content\": [{ \"text\": \"Bold \", \"fontWeight\": \"bold\" }, { \"text\": \"normal\" }]`\n\n### Icons — Two Options\n\n#### Option A: `icon_font` (RECOMMENDED — renders directly, no post-processing needed)\n\n```json\n{ \"type\": \"icon_font\", \"name\": \"Lock Icon\", \"iconFontName\": \"lock\",\n \"width\": 20, \"height\": 20,\n \"fill\": [{ \"type\": \"solid\", \"color\": \"#6B7280\" }] }\n```\n\n**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`.\n\nWorks in ALL contexts: CLI, MCP tools, or direct `.op` files — no `design:refine` required.\n\n#### Option B: `path` (requires post-processing)\n\n```json\n{ \"type\": \"path\", \"name\": \"HeartIcon\", \"width\": 24, \"height\": 24,\n \"fill\": [{ \"type\": \"solid\", \"color\": \"#111111\" }] }\n```\n\nPascalCase + \"Icon\" suffix. Auto-resolved from Lucide set during post-processing.\n\n> **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.**\n\n### Image\n\n```json\n{ \"type\": \"image\", \"src\": \"https://example.com/photo.jpg\", \"width\": 400, \"height\": 300,\n \"objectFit\": \"crop\", \"cornerRadius\": 12 }\n```\n\nAI image placeholders (resolved by `design:refine`):\n\n```json\n{ \"type\": \"image\", \"width\": 400, \"height\": 300,\n \"imagePrompt\": \"A modern office workspace with natural light\",\n \"imageSearchQuery\": \"modern office workspace\" }\n```\n\nImage adjustments (all -100 to 100): `exposure`, `contrast`, `saturation`, `temperature`, `tint`, `highlights`, `shadows`.\n\n### Polygon\n\n```json\n{ \"type\": \"polygon\", \"polygonCount\": 6, \"width\": 80, \"height\": 80, \"cornerRadius\": 4,\n \"fill\": [{ \"type\": \"solid\", \"color\": \"#6366F1\" }] }\n```\n\n### Icon Font\n\n```json\n{ \"type\": \"icon_font\", \"iconFontName\": \"lucide:home\", \"width\": 24, \"height\": 24,\n \"fill\": [{ \"type\": \"solid\", \"color\": \"#111111\" }] }\n```\n\n### Line\n\n```json\n{ \"type\": \"line\", \"x2\": 200, \"y2\": 0,\n \"stroke\": { \"thickness\": 1, \"fill\": [{ \"type\": \"solid\", \"color\": \"#E5E7EB\" }] } }\n```\n\n### Fill Types\n\n```json\n{ \"type\": \"solid\", \"color\": \"#3B82F6\" }\n{ \"type\": \"linear_gradient\", \"angle\": 135,\n \"stops\": [{ \"offset\": 0, \"color\": \"#6366F1\" }, { \"offset\": 1, \"color\": \"#8B5CF6\" }] }\n{ \"type\": \"radial_gradient\", \"cx\": 0.5, \"cy\": 0.5, \"radius\": 0.5,\n \"stops\": [{ \"offset\": 0, \"color\": \"#FFF\" }, { \"offset\": 1, \"color\": \"#000\" }] }\n{ \"type\": \"image\", \"url\": \"https://example.com/texture.jpg\", \"mode\": \"fill\" }\n```\n\nImage fill modes: `fill`, `fit`, `crop`, `tile`, `stretch`. Image fill also supports adjustment filters (`exposure`, `contrast`, `saturation`, etc.).\n\n### Ref Node (Component Instance)\n\n```json\n{ \"type\": \"ref\", \"ref\": \"reusable-frame-id\",\n \"descendants\": { \"child-id\": { \"content\": \"Override text\" } } }\n```\n\nReferences a `frame` with `reusable: true`. Override specific descendant properties via `descendants`.\n\n### Design Variables\n\nReference with `$` prefix: `\"color\": \"$primaryColor\"`, `\"gap\": \"$spacing\"`.\n\n## Semantic Roles\n\nRoles declare intent — the engine applies smart defaults. Always prefer roles over manual styling.\n\n| Category | Roles |\n|----------|-------|\n| **Layout** | `section`, `row`, `column`, `centered-content`, `divider`, `spacer` |\n| **Navigation** | `navbar`, `nav-links`, `nav-link` |\n| **Interactive** | `button`, `icon-button`, `badge`, `tag`, `pill`, `input`, `form-input`, `search-bar` |\n| **Cards** | `card`, `feature-card`, `stat-card`, `pricing-card`, `image-card` |\n| **Content** | `hero`, `feature-grid`, `cta-section`, `footer`, `testimonial`, `stats-section` |\n| **Typography** | `heading`, `subheading`, `body-text`, `caption`, `label` |\n| **Media** | `avatar`, `icon`, `phone-mockup`, `screenshot-frame` |\n| **Table** | `table`, `table-row`, `table-header`, `table-cell` |\n| **Form** | `form-group` |\n\nKey defaults:\n- `navbar` → height: 56-72, horizontal, space_between, center-aligned\n- `button` → padding: [12, 24], cornerRadius: 8, centered\n- `card` → vertical, gap: 12, cornerRadius: 12, padding: 24\n- `heading` → lineHeight: 1.2, letterSpacing: -0.5\n- `body-text` → fill_container, textGrowth: fixed-width, lineHeight: 1.5\n\n## Layout Rules\n\n1. **NEVER set x/y on children inside layout containers** — engine positions them\n2. **Siblings must use same width strategy** — all `fill_container` or all fixed\n3. **NEVER `fill_container` inside `fit_content` parent** — circular dependency\n4. Cards in horizontal row: ALL `width: \"fill_container\"`, `height: \"fill_container\"`\n\n### Sizing Decision\n\n| Question | Answer |\n|----------|--------|\n| Stretch to fill? | `\"fill_container\"` |\n| Shrink to content? | `\"fit_content\"` |\n| Exact size? | number (px) |\n\n### Design Type Sizing\n\n| Type | Width | Height |\n|------|-------|--------|\n| Landing page | 1200 | 0 (auto) |\n| Mobile screen | 375 | 812 |\n| Dashboard | 1200 | 0 (auto) |\n\n## Design Principles\n\n### Typography\n\n```\nDisplay: 40-56px 700 letterSpacing: -1.5 lineHeight: 1.1 \"Space Grotesk\"\nHeading: 28-36px 700 letterSpacing: -0.5 lineHeight: 1.2 \"Space Grotesk\"\nSubheading: 20-24px 600 letterSpacing: -0.25 lineHeight: 1.3 \"Space Grotesk\"\nBody: 15-18px 400 letterSpacing: 0 lineHeight: 1.5 \"Inter\"\nCaption: 13-14px 400 letterSpacing: 0 lineHeight: 1.4 \"Inter\"\n```\n\nCJK: use `\"Noto Sans SC/JP/KR\"`, lineHeight >= 1.3, letterSpacing: 0 always.\n\n### Color\n\n```\nPrimary text: #111111 Secondary: #6B7280 Subtle: #9CA3AF\nBackground: #FFFFFF Surface: #F9FAFB Border: #E5E7EB\n```\n\nMax 2 saturated colors. WCAG AA: 4.5:1 body, 3:1 large. Dark bg: `#0F172A`, not `#000000`.\n\n### Spacing (8px grid)\n\n```\nRelated: 8-16px Components: 16-24px\nGroups: 24-32px Sections: 48-80px Page padding: 80px\n```\n\n### Shadows\n\n```json\n// Subtle (cards)\n{ \"type\": \"shadow\", \"offsetY\": 1, \"blur\": 3, \"color\": \"rgba(0,0,0,0.05)\" }\n// Medium (dropdowns)\n{ \"type\": \"shadow\", \"offsetY\": 4, \"blur\": 12, \"color\": \"rgba(0,0,0,0.08)\" }\n// Elevated (modals)\n{ \"type\": \"shadow\", \"offsetY\": 8, \"blur\": 24, \"spread\": -4, \"color\": \"rgba(0,0,0,0.12)\" }\n```\n\n### Copy Rules\n\nHeadlines: 2-6 words. Subtitles: max 15 words. Buttons: 1-3 words. No lorem ipsum. No emoji as icons.\n\n## Layered Workflow\n\nFor complex multi-section pages, use the three-step skeleton → content → refine flow:\n\n| Step | MCP Tool | CLI Equivalent |\n|------|----------|----------------|\n| 1. Create section structure | `design_skeleton` | `op design:skeleton '<json>'` |\n| 2. Populate each section | `design_content` (with `postProcess: true`) | `op design:content <section-id> '<json>'` |\n| 3. Validate + auto-fix | `design_refine` | `op design:refine --root-id <id>` |\n\n`design:refine` resolves icon names → SVG paths, fixes layout issues, and validates the tree. **Always run as the final step.**\n\n## Codegen Pipeline\n\nFor incremental, framework-aware code generation from the design tree:\n\n| Step | CLI Command | MCP Tool | Description |\n|------|------------|----------|-------------|\n| 1. Plan | `op codegen:plan '<json>'` | `codegen_plan` | Declare framework, root node IDs, and options |\n| 2. Submit | `op codegen:submit '<json>'` | `codegen_submit_chunk` | Submit generated code for individual nodes |\n| 3. Assemble | `op codegen:assemble --framework react` | `codegen_assemble` | Combine all chunks into the final output |\n| 4. Clean | `op codegen:clean` | `codegen_clean` | Clear server-side codegen state |\n\nThe plan JSON shape:\n```json\n{ \"framework\": \"react\", \"rootIds\": [\"frame-1\"], \"options\": { \"tailwind\": true } }\n```\n\nThe submit JSON shape:\n```json\n{ \"nodeId\": \"card-1\", \"code\": \"<Card className=\\\"...\\\">...</Card>\", \"imports\": [\"Card\"] }\n```\n\nSupported frameworks: `react`, `html`, `vue`, `svelte`, `flutter`, `swiftui`, `compose`, `rn` (React Native), `css`.\n\n## Multi-Page Documents\n\n```bash\nop page list # List all pages with IDs\nop page add --name \"Settings\" # Add a new page\nop page remove <page-id> # Remove a page\nop page rename <page-id> 'New Name' # Rename a page\nop page reorder <page-id> 2 # Move page to index 2\nop page duplicate <page-id> # Clone page with new IDs\n```\n\nUse `--page <id>` on any command to target a specific page. Without it, commands operate on the first page.\n\n## Common Patterns\n\nPatterns below show `op insert --parent` commands. Each pattern is copy-paste ready.\n\n### Navbar\n\n```bash\nNAV=$(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)\nop insert --parent \"$NAV\" '{\"type\":\"text\",\"content\":\"Brand\",\"fontSize\":20,\"fontWeight\":700,\"fontFamily\":\"Space Grotesk\"}'\nLINKS=$(op insert --parent \"$NAV\" '{\"type\":\"frame\",\"role\":\"nav-links\",\"layout\":\"horizontal\",\"gap\":32,\"width\":\"fit_content\",\"height\":\"fit_content\"}' | ID)\nop insert --parent \"$LINKS\" '{\"type\":\"text\",\"role\":\"nav-link\",\"content\":\"Features\",\"fontSize\":15}'\nop insert --parent \"$LINKS\" '{\"type\":\"text\",\"role\":\"nav-link\",\"content\":\"Pricing\",\"fontSize\":15}'\nCTA=$(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)\nop insert --parent \"$CTA\" '{\"type\":\"text\",\"content\":\"Get Started\",\"fontSize\":14,\"fontWeight\":600,\"fill\":[{\"type\":\"solid\",\"color\":\"#FFFFFF\"}]}'\n```\n\n### Hero\n\n```bash\nHERO=$(op insert --parent \"$ROOT\" '{\"type\":\"frame\",\"role\":\"hero\",\"width\":\"fill_container\",\"height\":\"fit_content\",\"layout\":\"vertical\",\"padding\":[100,80],\"gap\":24,\"alignItems\":\"center\"}' | ID)\nop 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}'\nop 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\"}]}'\nBTNS=$(op insert --parent \"$HERO\" '{\"type\":\"frame\",\"layout\":\"horizontal\",\"gap\":12,\"width\":\"fit_content\",\"height\":\"fit_content\"}' | ID)\nB1=$(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)\nop insert --parent \"$B1\" '{\"type\":\"text\",\"content\":\"Start Free\",\"fontSize\":16,\"fontWeight\":600,\"fill\":[{\"type\":\"solid\",\"color\":\"#FFFFFF\"}]}'\nB2=$(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)\nop insert --parent \"$B2\" '{\"type\":\"text\",\"content\":\"View Demo\",\"fontSize\":16,\"fontWeight\":600}'\n```\n\n### Feature Card (in horizontal grid, ALL cards must use fill_container)\n\n```bash\nCARD=$(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)\nop insert --parent \"$CARD\" '{\"type\":\"path\",\"name\":\"ZapIcon\",\"width\":24,\"height\":24,\"fill\":[{\"type\":\"solid\",\"color\":\"#111111\"}]}'\nop insert --parent \"$CARD\" '{\"type\":\"text\",\"content\":\"Lightning Fast\",\"fontSize\":20,\"fontWeight\":600}'\nop 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\"}]}'\n```\n\n### Form Input\n\n```bash\nGRP=$(op insert --parent \"$FORM\" '{\"type\":\"frame\",\"role\":\"form-group\",\"layout\":\"vertical\",\"gap\":8,\"width\":\"fill_container\"}' | ID)\nop insert --parent \"$GRP\" '{\"type\":\"text\",\"role\":\"label\",\"content\":\"Email\",\"fontSize\":14,\"fontWeight\":500}'\nINP=$(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)\nop insert --parent \"$INP\" '{\"type\":\"path\",\"name\":\"MailIcon\",\"width\":18,\"height\":18,\"fill\":[{\"type\":\"solid\",\"color\":\"#9CA3AF\"}]}'\nop insert --parent \"$INP\" '{\"type\":\"text\",\"content\":\"you@example.com\",\"fontSize\":15,\"fill\":[{\"type\":\"solid\",\"color\":\"#9CA3AF\"}]}'\n```\n\n### Footer\n\n```bash\nFOOTER=$(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)\nCOL1=$(op insert --parent \"$FOOTER\" '{\"type\":\"frame\",\"layout\":\"vertical\",\"gap\":16,\"width\":240}' | ID)\nop insert --parent \"$COL1\" '{\"type\":\"text\",\"content\":\"Brand\",\"fontSize\":20,\"fontWeight\":700,\"fontFamily\":\"Space Grotesk\"}'\nop insert --parent \"$COL1\" '{\"type\":\"text\",\"content\":\"Building the future of design.\",\"fontSize\":14,\"lineHeight\":1.6,\"fill\":[{\"type\":\"solid\",\"color\":\"#6B7280\"}]}'\nCOL2=$(op insert --parent \"$FOOTER\" '{\"type\":\"frame\",\"layout\":\"vertical\",\"gap\":12,\"width\":\"fit_content\"}' | ID)\nop insert --parent \"$COL2\" '{\"type\":\"text\",\"content\":\"Product\",\"fontSize\":14,\"fontWeight\":600}'\nop insert --parent \"$COL2\" '{\"type\":\"text\",\"content\":\"Features\",\"fontSize\":14,\"fill\":[{\"type\":\"solid\",\"color\":\"#6B7280\"}]}'\nop insert --parent \"$COL2\" '{\"type\":\"text\",\"content\":\"Pricing\",\"fontSize\":14,\"fill\":[{\"type\":\"solid\",\"color\":\"#6B7280\"}]}'\n```\n\n## Common Mistakes\n\n| Mistake | Fix |\n|---------|-----|\n| Setting x/y inside layout container | Remove x/y — engine auto-positions |\n| Cards with different width strategies | All siblings: same sizing (`fill_container`) |\n| `fill_container` child in `fit_content` parent | Use fixed width or switch parent to `fill_container` |\n| Pure black text `#000000` | Use `#111111` or `#0F172A` |\n| Heavy drop shadows | Use subtle `rgba(0,0,0,0.05-0.12)` |\n| Emoji as icons | Use path nodes with icon names |\n| Lorem ipsum placeholder | Write realistic, concise copy |\n| Fixed height on text | Use `textGrowth: \"fixed-width\"` instead |\n| Space Grotesk for CJK | Use `\"Noto Sans SC/JP/KR\"` |\n| Negative letterSpacing on CJK | Always 0 for CJK text |\n| Missing post-process after insert | Run `op design:refine --root-id <id>` after building the tree |\n| Icons inserted but not visible | Path nodes need `design:refine` or `--post-process` to resolve SVG |\n| Using DSL `I()` with inline `children` | DSL parser fails on nested JSON — insert parent and children separately |\n| Missing `postProcess: true` in MCP | Always set for MCP tool calls |\n\n## Full Example — `op insert` Workflow (Recommended)\n\nBuild a complete mobile login page using `op insert --parent`. This is the most reliable approach.\n\n```bash\n#!/bin/bash\nset -e\nID() { python3 -c \"import sys,json; print(json.load(sys.stdin)['nodeId'])\"; }\n\n# Root frame (mobile)\nROOT=$(op insert '{\"type\":\"frame\",\"name\":\"Login\",\"width\":375,\"height\":812,\"layout\":\"vertical\",\"fill\":[{\"type\":\"solid\",\"color\":\"#FFFFFF\"}]}' | ID)\n\n# Header\nTOP=$(op insert --parent \"$ROOT\" '{\"type\":\"frame\",\"width\":\"fill_container\",\"height\":\"fit_content\",\"layout\":\"vertical\",\"padding\":[80,32,40,32],\"gap\":14,\"alignItems\":\"center\"}' | ID)\nop insert --parent \"$TOP\" '{\"type\":\"path\",\"name\":\"ShieldIcon\",\"width\":48,\"height\":48,\"fill\":[{\"type\":\"solid\",\"color\":\"#6366F1\"}]}'\nop insert --parent \"$TOP\" '{\"type\":\"text\",\"content\":\"Welcome Back\",\"fontSize\":28,\"fontWeight\":700,\"fontFamily\":\"Space Grotesk\",\"letterSpacing\":-0.5,\"textAlign\":\"center\"}'\n\n# Form\nFORM=$(op insert --parent \"$ROOT\" '{\"type\":\"frame\",\"width\":\"fill_container\",\"height\":\"fit_content\",\"layout\":\"vertical\",\"padding\":[0,32],\"gap\":20}' | ID)\n\n# Email input\nGRP=$(op insert --parent \"$FORM\" '{\"type\":\"frame\",\"role\":\"form-group\",\"layout\":\"vertical\",\"gap\":8,\"width\":\"fill_container\"}' | ID)\nop insert --parent \"$GRP\" '{\"type\":\"text\",\"role\":\"label\",\"content\":\"Email\",\"fontSize\":14,\"fontWeight\":500}'\nINP=$(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)\nop insert --parent \"$INP\" '{\"type\":\"path\",\"name\":\"MailIcon\",\"width\":18,\"height\":18,\"fill\":[{\"type\":\"solid\",\"color\":\"#9CA3AF\"}]}'\nop insert --parent \"$INP\" '{\"type\":\"text\",\"content\":\"you@example.com\",\"fontSize\":15,\"fill\":[{\"type\":\"solid\",\"color\":\"#9CA3AF\"}]}'\n\n# Login button\nBTN=$(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)\nop insert --parent \"$BTN\" '{\"type\":\"text\",\"content\":\"Sign In\",\"fontSize\":16,\"fontWeight\":600,\"fill\":[{\"type\":\"solid\",\"color\":\"#FFFFFF\"}]}'\n\n# IMPORTANT: resolve icons + validate layout\nop design:refine --root-id \"$ROOT\"\n```\n\n## DSL Example — Landing Page\n\nDSL is suitable for simpler structures. **Avoid inline `children`** — insert parent and children as separate operations.\n\n```\nroot=I(null, {\"type\":\"frame\",\"name\":\"Landing\",\"width\":1200,\"height\":0,\"layout\":\"vertical\",\"fill\":[{\"type\":\"solid\",\"color\":\"#FFFFFF\"}]})\n\nnav=I(root, {\"type\":\"frame\",\"role\":\"navbar\",\"width\":\"fill_container\",\"height\":72,\"layout\":\"horizontal\",\"padding\":[0,80],\"justifyContent\":\"space_between\",\"alignItems\":\"center\"})\nI(nav, {\"type\":\"text\",\"content\":\"Acme\",\"fontSize\":20,\"fontWeight\":700,\"fontFamily\":\"Space Grotesk\"})\nlinks=I(nav, {\"type\":\"frame\",\"role\":\"nav-links\",\"layout\":\"horizontal\",\"gap\":32,\"width\":\"fit_content\",\"height\":\"fit_content\"})\nI(links, {\"type\":\"text\",\"role\":\"nav-link\",\"content\":\"Features\",\"fontSize\":15})\nI(links, {\"type\":\"text\",\"role\":\"nav-link\",\"content\":\"Pricing\",\"fontSize\":15})\ncta=I(nav, {\"type\":\"rectangle\",\"role\":\"button\",\"padding\":[10,24],\"cornerRadius\":8,\"fill\":[{\"type\":\"solid\",\"color\":\"#111111\"}],\"layout\":\"horizontal\",\"justifyContent\":\"center\",\"alignItems\":\"center\"})\nI(cta, {\"type\":\"text\",\"content\":\"Get Started\",\"fontSize\":14,\"fontWeight\":600,\"fill\":[{\"type\":\"solid\",\"color\":\"#FFFFFF\"}]})\n\nhero=I(root, {\"type\":\"frame\",\"role\":\"hero\",\"width\":\"fill_container\",\"height\":\"fit_content\",\"layout\":\"vertical\",\"padding\":[100,80],\"gap\":24,\"alignItems\":\"center\"})\nI(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})\nI(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\"}]})\nbtns=I(hero, {\"type\":\"frame\",\"layout\":\"horizontal\",\"gap\":12,\"width\":\"fit_content\",\"height\":\"fit_content\"})\nb1=I(btns, {\"type\":\"rectangle\",\"role\":\"button\",\"padding\":[14,32],\"cornerRadius\":10,\"fill\":[{\"type\":\"solid\",\"color\":\"#111111\"}],\"layout\":\"horizontal\",\"justifyContent\":\"center\",\"alignItems\":\"center\"})\nI(b1, {\"type\":\"text\",\"content\":\"Start Free\",\"fontSize\":16,\"fontWeight\":600,\"fill\":[{\"type\":\"solid\",\"color\":\"#FFFFFF\"}]})\nb2=I(btns, {\"type\":\"rectangle\",\"role\":\"button\",\"padding\":[14,32],\"cornerRadius\":10,\"fill\":[{\"type\":\"solid\",\"color\":\"#F3F4F6\"}],\"layout\":\"horizontal\",\"justifyContent\":\"center\",\"alignItems\":\"center\"})\nI(b2, {\"type\":\"text\",\"content\":\"View Demo\",\"fontSize\":16,\"fontWeight\":600})\n\nfeat=I(root, {\"type\":\"frame\",\"role\":\"section\",\"width\":\"fill_container\",\"height\":\"fit_content\",\"layout\":\"vertical\",\"padding\":[80,80],\"gap\":48,\"alignItems\":\"center\"})\nI(feat, {\"type\":\"text\",\"role\":\"heading\",\"content\":\"Everything you need\",\"fontSize\":36,\"fontWeight\":700,\"fontFamily\":\"Space Grotesk\",\"textAlign\":\"center\",\"letterSpacing\":-0.5})\ngrid=I(feat, {\"type\":\"frame\",\"role\":\"feature-grid\",\"width\":\"fill_container\",\"layout\":\"horizontal\",\"gap\":24})\nc1=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\"}]})\nI(c1, {\"type\":\"path\",\"name\":\"ZapIcon\",\"width\":24,\"height\":24,\"fill\":[{\"type\":\"solid\",\"color\":\"#111111\"}]})\nI(c1, {\"type\":\"text\",\"content\":\"Lightning Fast\",\"fontSize\":20,\"fontWeight\":600})\nI(c1, {\"type\":\"text\",\"role\":\"body-text\",\"content\":\"Sub-second builds with smart caching.\",\"fontSize\":15,\"lineHeight\":1.6,\"fill\":[{\"type\":\"solid\",\"color\":\"#6B7280\"}]})\nc2=C(c1, grid, {})\nU(c2+\"/0\", {\"name\":\"ShieldIcon\"})\nU(c2+\"/1\", {\"content\":\"Enterprise Security\"})\nU(c2+\"/2\", {\"content\":\"SOC 2 certified with end-to-end encryption.\"})\nc3=C(c1, grid, {})\nU(c3+\"/0\", {\"name\":\"GitBranchIcon\"})\nU(c3+\"/1\", {\"content\":\"Git-Native Workflow\"})\nU(c3+\"/2\", {\"content\":\"Preview deploys on every push with instant rollback.\"})\n```\n",
".claude-plugin/plugin.json": "{\n \"name\": \"openpencil-skill\",\n \"description\": \"Design skill for OpenPencil — op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices\",\n \"version\": \"__OPENPENCIL_VERSION__\",\n \"author\": {\n \"name\": \"ZSeven-W\",\n \"email\": \"xkayshen@gmail.com\"\n },\n \"homepage\": \"https://github.com/zseven-w/openpencil-skill\",\n \"repository\": \"https://github.com/zseven-w/openpencil-skill\",\n \"license\": \"MIT\",\n \"keywords\": [\n \"design\",\n \"ui\",\n \"vector\",\n \"openpencil\",\n \"cli\",\n \"mcp\",\n \"dsl\"\n ]\n}\n",
".claude-plugin/marketplace.json": "{\n \"name\": \"openpencil-skill\",\n \"description\": \"Design skill for OpenPencil — op CLI, batch DSL, MCP tools, and UI best practices\",\n \"owner\": {\n \"name\": \"ZSeven-W\",\n \"email\": \"xkayshen@gmail.com\"\n },\n \"plugins\": [\n {\n \"name\": \"openpencil-skill\",\n \"description\": \"Design skill for OpenPencil — op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices\",\n \"version\": \"__OPENPENCIL_VERSION__\",\n \"source\": \"./\",\n \"author\": {\n \"name\": \"ZSeven-W\",\n \"email\": \"xkayshen@gmail.com\"\n }\n }\n ]\n}\n",
".cursor-plugin/plugin.json": "{\n \"name\": \"openpencil-skill\",\n \"displayName\": \"OpenPencil Design\",\n \"description\": \"Design skill for OpenPencil — op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices\",\n \"version\": \"__OPENPENCIL_VERSION__\",\n \"author\": {\n \"name\": \"ZSeven-W\",\n \"email\": \"xkayshen@gmail.com\"\n },\n \"homepage\": \"https://github.com/zseven-w/openpencil-skill\",\n \"repository\": \"https://github.com/zseven-w/openpencil-skill\",\n \"license\": \"MIT\",\n \"keywords\": [\n \"design\",\n \"ui\",\n \"vector\",\n \"openpencil\",\n \"cli\",\n \"mcp\",\n \"dsl\"\n ],\n \"skills\": \"./skills/\"\n}\n",
"package.json": "{\n \"name\": \"openpencil-skill\",\n \"version\": \"__OPENPENCIL_VERSION__\",\n \"type\": \"module\"\n}\n",
"GEMINI.md": "@./skills/openpencil-design/SKILL.md\n",
"gemini-extension.json": "{\n \"name\": \"openpencil-skill\",\n \"description\": \"Design skill for OpenPencil — op CLI, batch DSL, MCP tools, and UI best practices\",\n \"version\": \"__OPENPENCIL_VERSION__\",\n \"contextFileName\": \"GEMINI.md\"\n}\n"
}
}