@zseven-w/openpencil 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2156,7 +2156,7 @@ This context will guide the subtask decomposition and style guide selection.`
2156
2156
  "budget": 500,
2157
2157
  "category": "base"
2158
2158
  },
2159
- "content": 'OVERFLOW PREVENTION (CRITICAL):\n\n- Text in vertical layout: width="fill_container" + textGrowth="fixed-width". In horizontal: width="fit_content".\n- NEVER set fixed pixel width on text inside layout frames (e.g. width:378 in 195px card - overflows!).\n- Fixed-width children must be <= parent content area (parent width - padding).\n- Badges: short labels only (CJK <=8 chars / Latin <=16 chars).'
2159
+ "content": 'OVERFLOW PREVENTION (CRITICAL):\n\n- Text in vertical layout: width="fill_container" + textGrowth="fixed-width". In horizontal: width="fit_content".\n- NEVER set fixed pixel width on text inside layout frames (e.g. width:378 in 195px card - overflows!).\n- Fixed-width children must be <= parent content area (parent width - padding).\n- Badges: short labels only (CJK <=8 chars / Latin <=16 chars).\n\n## HORIZONTAL SCROLL ROWS (cards / chips / categories)\n\nWhen the spec says "horizontal scrolling cards", "swipeable row", "chip row", or similar, generate EXACTLY this structure \u2014 do NOT just emit 6 cards inside a horizontal layout, the children will spill outside the page frame.\n\nStructure:\n\n- A wrapper frame with `width="fill_container"`, `height="fit_content"`, `layout="vertical"`, `clipContent=true`.\n- Inside it, a row frame with `width="fit_content"`, `height="fit_content"`, `layout="horizontal"`, `gap=12`, `padding=[0,20]`.\n- The row frame holds the actual cards.\n\nEvery card in the row MUST:\n\n- Have a FIXED numeric `width` (typically 120-160 for mobile, 200-260 for desktop). Never `fill_container`, never `fit_content` - fixed pixels.\n- Share identical width with its siblings for visual rhythm.\n\nExample - 6 workout cards inside a 375px-wide mobile page:\n\n```json\n{\n "id": "cards-scroll",\n "type": "frame",\n "name": "Workouts Scroll",\n "width": "fill_container",\n "height": "fit_content",\n "layout": "vertical",\n "clipContent": true,\n "children": [\n {\n "id": "cards-row",\n "type": "frame",\n "name": "Workouts Row",\n "width": "fit_content",\n "height": "fit_content",\n "layout": "horizontal",\n "gap": 12,\n "padding": [0, 20],\n "children": [\n {\n "id": "card-hiit",\n "type": "frame",\n "width": 140,\n "height": 160,\n "cornerRadius": 20,\n "layout": "vertical",\n "gap": 8,\n "padding": 16,\n "fill": [{ "type": "solid", "color": "#1a1a1a" }],\n "children": []\n },\n {\n "id": "card-strength",\n "type": "frame",\n "width": 140,\n "height": 160,\n "cornerRadius": 20,\n "layout": "vertical",\n "gap": 8,\n "padding": 16,\n "fill": [{ "type": "solid", "color": "#1a1a1a" }],\n "children": []\n }\n ]\n }\n ]\n}\n```\n\nAnti-patterns (do NOT emit any of these):\n\n- Putting 5+ cards directly inside a `layout="horizontal"` page-root frame (they overflow the phone width).\n- Using `fill_container` on cards in a horizontal row (they squish down to invisibility).\n- Using `width="fit_content"` on cards - text-driven widths are unpredictable and break rhythm.\n- Skipping the `clipContent=true` wrapper and relying on Skia to clip (it doesn\'t \u2014 only `clipContent:true` enables clipping).'
2160
2160
  },
2161
2161
  {
2162
2162
  "meta": {
@@ -2339,14 +2339,19 @@ This context will guide the subtask decomposition and style guide selection.`
2339
2339
  "name": "incremental-add",
2340
2340
  "description": "Rules for adding new elements to existing designs",
2341
2341
  "phase": [
2342
- "maintenance"
2342
+ "maintenance",
2343
+ "generation"
2343
2344
  ],
2344
2345
  "trigger": {
2345
2346
  "keywords": [
2346
2347
  "add",
2347
2348
  "insert",
2348
2349
  "new section",
2349
- "append"
2350
+ "append",
2351
+ "continue",
2352
+ "\u7EE7\u7EED",
2353
+ "\u518D\u52A0",
2354
+ "\u8FFD\u52A0"
2350
2355
  ]
2351
2356
  },
2352
2357
  "priority": 20,
@@ -4723,7 +4728,7 @@ async function handleSetVariables(params) {
4723
4728
  if (params.replace) {
4724
4729
  doc.variables = params.variables;
4725
4730
  } else {
4726
- doc.variables = { ...doc.variables ?? {}, ...params.variables };
4731
+ doc.variables = { ...doc.variables, ...params.variables };
4727
4732
  }
4728
4733
  await saveDocument(filePath, doc);
4729
4734
  return { variables: doc.variables };
@@ -4734,7 +4739,7 @@ async function handleSetThemes(params) {
4734
4739
  if (params.replace) {
4735
4740
  doc.themes = params.themes;
4736
4741
  } else {
4737
- doc.themes = { ...doc.themes ?? {}, ...params.themes };
4742
+ doc.themes = { ...doc.themes, ...params.themes };
4738
4743
  }
4739
4744
  await saveDocument(filePath, doc);
4740
4745
  return { themes: doc.themes };
@@ -5042,11 +5047,18 @@ var init_font_manager = __esm({
5042
5047
  systemFontFamilies = /* @__PURE__ */ new Set();
5043
5048
  /** In-flight font fetch promises to avoid duplicate requests */
5044
5049
  pendingFetches = /* @__PURE__ */ new Map();
5050
+ /** Cached set of native (OS-installed) font families from Local Font Access API (lowercase) */
5051
+ nativeFontSet = null;
5052
+ /** Full native font entries with blob accessors, keyed by lowercase family name */
5053
+ nativeFontMap = /* @__PURE__ */ new Map();
5054
+ /** Current permission state for native font access (Local Font Access API) */
5055
+ nativeFontPermission = "prompt";
5045
5056
  constructor(ck, options) {
5046
5057
  this.provider = ck.TypefaceFontProvider.Make();
5047
5058
  this.fontBasePath = options?.fontBasePath ?? "/fonts/";
5048
5059
  if (!this.fontBasePath.endsWith("/")) this.fontBasePath += "/";
5049
5060
  this.googleFontsCssUrl = options?.googleFontsCssUrl ?? "https://fonts.googleapis.com/css2";
5061
+ this._checkPermissionState();
5050
5062
  }
5051
5063
  getProvider() {
5052
5064
  return this.provider;
@@ -5118,7 +5130,8 @@ var init_font_manager = __esm({
5118
5130
  }
5119
5131
  }
5120
5132
  /**
5121
- * Ensure a font family is loaded. Tries bundled fonts first, then Google Fonts.
5133
+ * Ensure a font family is loaded. Tries bundled fonts first, then native
5134
+ * fonts (Local Font Access API + canvas heuristic), then Google Fonts CDN.
5122
5135
  */
5123
5136
  async ensureFont(family, weights = [400, 500, 600, 700]) {
5124
5137
  const key = family.toLowerCase();
@@ -5133,6 +5146,7 @@ var init_font_manager = __esm({
5133
5146
  this.pendingFetches.delete(key);
5134
5147
  if (!result) {
5135
5148
  if (isSystemFont(family)) {
5149
+ console.warn(`[FontManager] "${family}" is now a system font fallback after failed load.`);
5136
5150
  this.systemFontFamilies.add(key);
5137
5151
  } else {
5138
5152
  this.failedFamilies.add(key);
@@ -5152,6 +5166,91 @@ var init_font_manager = __esm({
5152
5166
  });
5153
5167
  return loaded;
5154
5168
  }
5169
+ /**
5170
+ * Request native font access from the user via the Local Font Access API.
5171
+ * Must be called from a user gesture context (click handler) for the
5172
+ * browser to show the permission prompt.
5173
+ *
5174
+ * Returns true if access was granted and fonts were enumerated.
5175
+ */
5176
+ async requestNativeFontAccess() {
5177
+ if (typeof window === "undefined") {
5178
+ this.nativeFontPermission = "unavailable";
5179
+ return false;
5180
+ }
5181
+ if (!("queryLocalFonts" in window)) {
5182
+ console.warn("[FontManager] Local Font Access API not available in this browser.");
5183
+ this.nativeFontPermission = "unavailable";
5184
+ return false;
5185
+ }
5186
+ try {
5187
+ const fonts = await window.queryLocalFonts();
5188
+ const families = /* @__PURE__ */ new Set();
5189
+ this.nativeFontMap.clear();
5190
+ for (const f of fonts) {
5191
+ const key = f.family.toLowerCase();
5192
+ families.add(key);
5193
+ const entry = {
5194
+ family: f.family,
5195
+ fullName: f.fullName,
5196
+ postscriptName: f.postscriptName,
5197
+ style: f.style,
5198
+ blob: f.blob.bind(f)
5199
+ };
5200
+ const existing = this.nativeFontMap.get(key) ?? [];
5201
+ existing.push(entry);
5202
+ this.nativeFontMap.set(key, existing);
5203
+ }
5204
+ this.nativeFontSet = families;
5205
+ this.nativeFontPermission = "granted";
5206
+ console.log(`[FontManager] Native font access granted \u2014 ${families.size} families found.`);
5207
+ return true;
5208
+ } catch (e) {
5209
+ const errMsg = e instanceof Error ? e.message : String(e);
5210
+ if (e instanceof DOMException && e.name === "NotAllowedError") {
5211
+ try {
5212
+ const status = await navigator.permissions.query({
5213
+ name: "local-fonts"
5214
+ });
5215
+ if (status.state === "prompt") {
5216
+ console.warn(
5217
+ "[FontManager] Native font access not yet prompted \u2014 will retry on next user gesture."
5218
+ );
5219
+ this.nativeFontPermission = "prompt";
5220
+ this.nativeFontSet = null;
5221
+ return false;
5222
+ }
5223
+ } catch {
5224
+ }
5225
+ console.warn("[FontManager] Native font access denied by user.");
5226
+ this.nativeFontPermission = "denied";
5227
+ } else {
5228
+ console.warn("[FontManager] Native font access failed:", errMsg);
5229
+ this.nativeFontPermission = "denied";
5230
+ }
5231
+ this.nativeFontSet = /* @__PURE__ */ new Set();
5232
+ return false;
5233
+ }
5234
+ }
5235
+ /**
5236
+ * Check the current permission state without triggering a prompt.
5237
+ */
5238
+ async _checkPermissionState() {
5239
+ if (typeof navigator === "undefined" || !navigator.permissions) {
5240
+ return;
5241
+ }
5242
+ try {
5243
+ const result = await navigator.permissions.query({
5244
+ name: "local-fonts"
5245
+ });
5246
+ this.nativeFontPermission = result.state;
5247
+ if (result.state === "granted") {
5248
+ this.requestNativeFontAccess().catch(() => {
5249
+ });
5250
+ }
5251
+ } catch {
5252
+ }
5253
+ }
5155
5254
  async _loadFont(family, weights) {
5156
5255
  const bundled = BUNDLED_FONTS[family.toLowerCase()];
5157
5256
  if (bundled) {
@@ -5159,10 +5258,56 @@ var init_font_manager = __esm({
5159
5258
  const ok = await this._fetchLocalFonts(family, urls, bundled);
5160
5259
  if (ok) return true;
5161
5260
  }
5162
- if (isSystemFont(family)) {
5261
+ if (isKnownNonGoogleFont(family)) {
5163
5262
  return false;
5164
5263
  }
5165
- return this._fetchGoogleFont(family, weights);
5264
+ const nativeLoaded = await this._loadNativeFontData(family);
5265
+ if (nativeLoaded) return true;
5266
+ const nativeFonts = await this.getNativeFontSet();
5267
+ if (nativeFonts.has(family.toLowerCase())) {
5268
+ const blobLoaded = await this._loadNativeFontData(family);
5269
+ if (blobLoaded) return true;
5270
+ this.systemFontFamilies.add(family.toLowerCase());
5271
+ return false;
5272
+ }
5273
+ if (isFontLocallyAvailable(family)) {
5274
+ this.systemFontFamilies.add(family.toLowerCase());
5275
+ return false;
5276
+ }
5277
+ const isFontFromGoogle = await this._fetchGoogleFont(family, weights);
5278
+ if (isFontFromGoogle) return true;
5279
+ return false;
5280
+ }
5281
+ /**
5282
+ * Attempt to load a native font from the Local Font Access API blob data
5283
+ * into CanvasKit's TypefaceFontProvider for vector rendering.
5284
+ */
5285
+ async _loadNativeFontData(family) {
5286
+ const key = family.toLowerCase();
5287
+ const entries = this.nativeFontMap.get(key);
5288
+ if (!entries || entries.length === 0) return false;
5289
+ let registered = 0;
5290
+ for (const entry of entries) {
5291
+ try {
5292
+ const blob = await entry.blob();
5293
+ const buffer = await blob.arrayBuffer();
5294
+ if (buffer.byteLength > 0 && this.registerFont(buffer, family)) {
5295
+ registered++;
5296
+ }
5297
+ } catch (e) {
5298
+ console.warn(
5299
+ `[FontManager] Failed to load native font blob for "${entry.fullName}":`,
5300
+ e instanceof Error ? e.message : String(e)
5301
+ );
5302
+ }
5303
+ }
5304
+ if (registered > 0) {
5305
+ console.log(
5306
+ `[FontManager] Loaded "${family}" from native fonts (${registered}/${entries.length} variants).`
5307
+ );
5308
+ return true;
5309
+ }
5310
+ return false;
5166
5311
  }
5167
5312
  async _fetchLocalFonts(family, urls, relPaths) {
5168
5313
  try {
@@ -5217,8 +5362,8 @@ var init_font_manager = __esm({
5217
5362
  const fontBuffers = await Promise.all(
5218
5363
  urls.map(async (url) => {
5219
5364
  try {
5220
- const resp = await fetchWithTimeout(url, 8e3);
5221
- return resp.ok ? resp.arrayBuffer() : null;
5365
+ const resp = fetchWithTimeout(url, 8e3);
5366
+ return (await resp).ok ? (await resp).arrayBuffer() : null;
5222
5367
  } catch {
5223
5368
  return null;
5224
5369
  }
@@ -5234,12 +5379,27 @@ var init_font_manager = __esm({
5234
5379
  }
5235
5380
  return false;
5236
5381
  }
5382
+ /**
5383
+ * Build a set of all native (OS-installed) font families using the
5384
+ * Local Font Access API (Chrome 103+, Edge 103+).
5385
+ * Falls back to empty set if API is unavailable or permission denied.
5386
+ * Results are cached after the first successful call.
5387
+ */
5388
+ async getNativeFontSet() {
5389
+ if (this.nativeFontSet) return this.nativeFontSet;
5390
+ if (this.nativeFontPermission !== "denied" && this.nativeFontPermission !== "unavailable") {
5391
+ await this.requestNativeFontAccess();
5392
+ }
5393
+ return this.nativeFontSet ?? /* @__PURE__ */ new Set();
5394
+ }
5237
5395
  dispose() {
5238
5396
  this.provider.delete();
5239
5397
  this.loadedFamilies.clear();
5240
5398
  this.failedFamilies.clear();
5241
5399
  this.systemFontFamilies.clear();
5242
5400
  this.pendingFetches.clear();
5401
+ this.nativeFontSet = null;
5402
+ this.nativeFontMap.clear();
5243
5403
  }
5244
5404
  };
5245
5405
  localFontCache = /* @__PURE__ */ new Map();
@@ -6776,8 +6936,8 @@ async function handleLoadThemePreset(params) {
6776
6936
  if (data.type !== "openpencil-theme-preset") {
6777
6937
  throw new Error("Invalid theme preset file");
6778
6938
  }
6779
- doc.themes = { ...doc.themes ?? {}, ...data.themes };
6780
- doc.variables = { ...doc.variables ?? {}, ...data.variables };
6939
+ doc.themes = { ...doc.themes, ...data.themes };
6940
+ doc.variables = { ...doc.variables, ...data.variables };
6781
6941
  await saveDocument(filePath, doc);
6782
6942
  return {
6783
6943
  themes: doc.themes,
@@ -11923,6 +12083,8 @@ function mapSingleFill(paint) {
11923
12083
  type: "image",
11924
12084
  url,
11925
12085
  mode: mapScaleMode(paint.imageScaleMode),
12086
+ originalSize: normalizeOriginalSize(paint.originalImageWidth, paint.originalImageHeight),
12087
+ transform: normalizeImageTransform(paint.transform),
11926
12088
  opacity: paint.opacity
11927
12089
  };
11928
12090
  }
@@ -11934,6 +12096,26 @@ function gradientAngleFromTransform(m) {
11934
12096
  const mathAngle = Math.atan2(m.m10, m.m00) * (180 / Math.PI);
11935
12097
  return Math.round(90 - mathAngle);
11936
12098
  }
12099
+ function normalizeOriginalSize(width, height) {
12100
+ if (typeof width !== "number" || typeof height !== "number" || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
12101
+ return void 0;
12102
+ }
12103
+ return { width, height };
12104
+ }
12105
+ function normalizeImageTransform(transform) {
12106
+ if (!transform) return void 0;
12107
+ if (Math.abs(transform.m00 - 1) <= IMAGE_TRANSFORM_EPSILON && Math.abs(transform.m01) <= IMAGE_TRANSFORM_EPSILON && Math.abs(transform.m02) <= IMAGE_TRANSFORM_EPSILON && Math.abs(transform.m10) <= IMAGE_TRANSFORM_EPSILON && Math.abs(transform.m11 - 1) <= IMAGE_TRANSFORM_EPSILON && Math.abs(transform.m12) <= IMAGE_TRANSFORM_EPSILON) {
12108
+ return void 0;
12109
+ }
12110
+ return {
12111
+ m00: transform.m00,
12112
+ m01: transform.m01,
12113
+ m02: transform.m02,
12114
+ m10: transform.m10,
12115
+ m11: transform.m11,
12116
+ m12: transform.m12
12117
+ };
12118
+ }
11937
12119
  function mapScaleMode(mode) {
11938
12120
  switch (mode) {
11939
12121
  case "FIT":
@@ -11944,11 +12126,13 @@ function mapScaleMode(mode) {
11944
12126
  return "fill";
11945
12127
  }
11946
12128
  }
12129
+ var IMAGE_TRANSFORM_EPSILON;
11947
12130
  var init_figma_fill_mapper = __esm({
11948
12131
  "packages/pen-figma/src/figma-fill-mapper.ts"() {
11949
12132
  "use strict";
11950
12133
  init_define_import_meta_env();
11951
12134
  init_figma_color_utils();
12135
+ IMAGE_TRANSFORM_EPSILON = 1e-6;
11952
12136
  }
11953
12137
  });
11954
12138
 
@@ -13388,7 +13572,7 @@ var init_skill_bundle = __esm({
13388
13572
  skill_bundle_default = {
13389
13573
  version: "0.7.0",
13390
13574
  files: {
13391
- "skills/openpencil-design/SKILL.md": '---\nname: openpencil-design\ndescription: Use when designing UI with OpenPencil \u2014 creating layouts via op CLI, batch design DSL, 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 \u2014 `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]\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 \u2014 Two 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** \u2014 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## 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### Path (Icons)\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. Common: `SearchIcon`, `MenuIcon`, `HomeIcon`, `UserIcon`, `SettingsIcon`, `MailIcon`, `HeartIcon`, `StarIcon`, `CheckIcon`, `XIcon`, `ChevronRightIcon`, `ArrowRightIcon`, `ZapIcon`, `ShieldIcon`, `CodeIcon`, `LockIcon`, `SparklesIcon`, `PlayIcon`, `BellIcon`, `EyeIcon`, `DownloadIcon`, `PlusIcon`, `GlobeIcon`, `LayersIcon`.\n\n> **Icon rendering requires post-processing.** After inserting path nodes, you MUST run `op design:refine --root-id <id>` or use `op insert --post-process` to resolve icon names into actual SVG paths. Without this step, icons will exist in the tree but not render visually. Lucide icons use stroke rendering \u2014 the engine will clear `fill` and set `stroke` automatically during post-processing.\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 \u2014 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` \u2192 height: 56-72, horizontal, space_between, center-aligned\n- `button` \u2192 padding: [12, 24], cornerRadius: 8, centered\n- `card` \u2192 vertical, gap: 12, cornerRadius: 12, padding: 24\n- `heading` \u2192 lineHeight: 1.2, letterSpacing: -0.5\n- `body-text` \u2192 fill_container, textGrowth: fixed-width, lineHeight: 1.5\n\n## Layout Rules\n\n1. **NEVER set x/y on children inside layout containers** \u2014 engine positions them\n2. **Siblings must use same width strategy** \u2014 all `fill_container` or all fixed\n3. **NEVER `fill_container` inside `fit_content` parent** \u2014 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 \u2192 content \u2192 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 \u2192 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 \u2014 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 \u2014 insert parent and children separately |\n| Missing `postProcess: true` in MCP | Always set for MCP tool calls |\n\n## Full Example \u2014 `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 \u2014 Landing Page\n\nDSL is suitable for simpler structures. **Avoid inline `children`** \u2014 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',
13575
+ "skills/openpencil-design/SKILL.md": '---\nname: openpencil-design\ndescription: Use when designing UI with OpenPencil \u2014 creating layouts via op CLI, batch design DSL, 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 \u2014 `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]\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 \u2014 Two 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** \u2014 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## 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 \u2014 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 \u2014 Two Options\n\n#### Option A: `icon_font` (RECOMMENDED \u2014 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 \u2014 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 \u2014 **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 \u2014 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` \u2192 height: 56-72, horizontal, space_between, center-aligned\n- `button` \u2192 padding: [12, 24], cornerRadius: 8, centered\n- `card` \u2192 vertical, gap: 12, cornerRadius: 12, padding: 24\n- `heading` \u2192 lineHeight: 1.2, letterSpacing: -0.5\n- `body-text` \u2192 fill_container, textGrowth: fixed-width, lineHeight: 1.5\n\n## Layout Rules\n\n1. **NEVER set x/y on children inside layout containers** \u2014 engine positions them\n2. **Siblings must use same width strategy** \u2014 all `fill_container` or all fixed\n3. **NEVER `fill_container` inside `fit_content` parent** \u2014 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 \u2192 content \u2192 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 \u2192 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 \u2014 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 \u2014 insert parent and children separately |\n| Missing `postProcess: true` in MCP | Always set for MCP tool calls |\n\n## Full Example \u2014 `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 \u2014 Landing Page\n\nDSL is suitable for simpler structures. **Avoid inline `children`** \u2014 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',
13392
13576
  ".claude-plugin/plugin.json": '{\n "name": "openpencil-skill",\n "description": "Design skill for OpenPencil \u2014 op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices",\n "version": "0.7.0",\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',
13393
13577
  ".claude-plugin/marketplace.json": '{\n "name": "openpencil-skill",\n "description": "Design skill for OpenPencil \u2014 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 \u2014 op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices",\n "version": "0.7.0",\n "source": "./",\n "author": {\n "name": "ZSeven-W",\n "email": "xkayshen@gmail.com"\n }\n }\n ]\n}\n',
13394
13578
  ".cursor-plugin/plugin.json": '{\n "name": "openpencil-skill",\n "displayName": "OpenPencil Design",\n "description": "Design skill for OpenPencil \u2014 op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices",\n "version": "0.7.0",\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',
@@ -13751,7 +13935,7 @@ init_define_import_meta_env();
13751
13935
  // apps/cli/package.json
13752
13936
  var package_default = {
13753
13937
  name: "@zseven-w/openpencil",
13754
- version: "0.7.1",
13938
+ version: "0.7.3",
13755
13939
  description: "CLI for OpenPencil \u2014 control the design tool from your terminal",
13756
13940
  homepage: "https://github.com/ZSeven-W/openpencil/tree/main/apps/cli",
13757
13941
  bugs: {
@@ -13778,8 +13962,8 @@ var package_default = {
13778
13962
  compile: "cd ../.. && bun run cli:compile"
13779
13963
  },
13780
13964
  dependencies: {
13781
- "@zseven-w/pen-figma": "0.7.1",
13782
- "@zseven-w/pen-mcp": "0.7.1"
13965
+ "@zseven-w/pen-figma": "0.7.3",
13966
+ "@zseven-w/pen-mcp": "0.7.3"
13783
13967
  }
13784
13968
  };
13785
13969