mcpField
O
openpencil-design

openpencil-design

github.com/zseven-w设计与 UI6285 次安装11 周前更新
代码仓库

关于 openpencil-design

openpencil-design 是用于创建和编辑 OpenPencil 画布设计的 skill,可通过 PenNode JSON、`op` 命令行或 MCP 工具操作。它说明节点结构、布局规则及多种脚本化设计方式。

主要功能

  1. 01创建和编辑画布节点
  2. 02使用 `op` 命令行或 MCP 工具
  3. 03通过设计 DSL 批量操作
  4. 04用脚本模式生成重复设计
  5. 05查阅 PenNode 结构和布局规则

适用场景

  1. 01创建或更新 `.op` 文件中的 UI 设计
  2. 02脚本化生成重复设计结构
  3. 03批量修改画布节点

MCP 相关

介绍使用 `batch_design`、`insert_node` 和 `design_skeleton` 等 OpenPencil MCP 工具。`op` 命令行可启动或管理编辑器及其 MCP 服务。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zseven-w/openpencil-skill --skill openpencil-design

安装后在 Claude Code 中输入/openpencil-design确认已加载。

详细介绍

OpenPencil 设计

通过编写 PenNode JSON 树生成可用于生产环境的高质量矢量设计。使用 op CLI 或 MCP 工具,在 OpenPencil 画布上创建、读取、更新和删除节点。

适用场景

  • 在 .op 文件中创建或修改 UI 设计
  • 使用 op CLI 编写设计操作脚本
  • 通过 MCP 工具(batch_design、insert_node、design_skeleton)进行设计
  • 需要查阅 PenNode 架构、角色或布局规则

快速参考 — op CLI

# App and server control
op start [--file path.op]            # Launch the desktop editor and live MCP server
op start --web [--host H] [--file path.op]  # Serve the browser editor and MCP
op start --headless [--file path.op] # Run a windowless file-backed MCP server
op stop                              # Stop the running editor / MCP server
op status                            # Check whether MCP HTTP is reachable
op tools                             # List every MCP tool and input schema

# Skill management
op install [--target T]              # Install openpencil-skill for detected AI agents
op uninstall [--target T]            # Uninstall openpencil-skill
op skill:export <skill> [--out DIR]  # Export an embedded skill

# Document
op open [file.op]                    # Open file or connect to live canvas
op save <file.op>                    # Save current document
op get [--type T] [--name N] [--id ID] [--depth N] [--parent P]
op selection [--depth N]             # Get current canvas selection
op read-nodes [ids] [--depth N] [--vars]  # Read node subtree(s) with optional variable resolution
op layout [--parent P] [--depth N]   # Snapshot layout tree with computed positions
op find-space [--direction D] [--width N] [--height N]  # Find empty space on canvas

# Node operations
op insert '<json>' [--parent P] [--page PAGE] [--post-process]
op update <id> '<json>' [--page PAGE] [--post-process]
op delete <id> [--page PAGE]
op move <id> [--parent P] [--index N] [--page PAGE]
op copy <id> [--parent P] [--page PAGE]
op replace <id> '<json>' [--page PAGE] [--post-process]

# Batch design
op design '<dsl>'                    # Batch design DSL (inline, @file, or stdin) [--canvas-width N]
op design @ui.js                     # Sandboxed JS: I(parent, obj) + loops (.js/.mjs implies --script)
op design '<js>' --script            # Same in inline/stdin form; flag follows payload

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

# Design-as-code conversion
op design:upsert-vars --key K --file vars.json [--source P] [--hash H]
op design:upsert-component --key K --name N --file node.json [--source P] [--hash H]
op design:upsert-screen --key K --file node.json [--source P] [--hash H]
op design:status [--kind token|component|screen]
op design:lint [--node ID]

# Templates & style assets
op templates [--scene S] [--tag T]   # List shipped scene templates (incl. 16:9 decks)
op use-template <id>                 # Start from a template: takes over a blank
                                     #   starter page, otherwise appends its boards
op styles [<id>] [--tag T] [--platform P]   # List style guides: shipped corpus +
                                     #   your imported DESIGN.md files; a bare id
                                     #   prints that guide's markdown

# Export & delivery
op export [--item ID|--selection] --output PATH [--format png|jpeg|webp|pdf] [--scale N]
op export-deck --output PATH [--format pptx|html|pdf]   # Active page's boards as a deck
op export-frames --output-dir DIR [--format png|jpeg|webp]  # One image per top-level frame

# Import
op import:svg <file.svg> [--parent P]       # Import SVG as editable nodes
op import:html <file.html|url> [--parent P] [--viewport-height N] [--out out.op]
                                     # Import a page or live URL as nodes
op import:snapshot <snapshot.json> [--parent P] [--out out.op]
                                     # Import a web-capture snapshot
op import:figma <file.fig> [--out out.op]   # Convert Figma .fig to .op document

# Pages
op page list                         # List all pages
op page add [--name N]               # Add a new page
op page remove <id>                  # Remove a page
op page rename <id> '<name>'         # Rename a page
op page reorder <id> <index>         # Move page to position
op page duplicate <id>               # Clone page with new IDs

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

# Codegen pipeline
op codegen:plan '<json>'             # Submit plan (chunks, sharedStyles, rootLayout)
op codegen:submit <planId> '<json>'  # Submit a code chunk for the plan
op codegen:assemble <planId> [--framework F]  # Assemble final output
op codegen:clean <planId>            # Clear codegen state for the plan

全局标志:--port <n>、--file <path>、--page <id>、--pretty。输入:内联字符串、@filepath 或 -(标准输入)。

构建设计 — 三种方式

方式 1:op insert(推荐)

构建设计最可靠的方式。使用 --parent 指定父节点。保存返回的 nodeId 以供后续引用。始终以 design:refine 结束,以解析图标并验证布局。

# Create root frame, capture its ID
ROOT=$(op insert '{"type":"frame","name":"Page","width":375,"height":812,"layout":"vertical"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['nodeId'])")

# Insert children using --parent
op insert --parent "$ROOT" '{"type":"text","content":"Hello","fontSize":28,"fontWeight":700}'

# Post-process: resolve icons, validate layout
op design:refine --root-id "$ROOT"
方式 2:批量设计 DSL

每行一项操作。使用 name= 绑定结果,以便后续引用。支持嵌套的 children 数组;但如果后续操作需要稳定引用子节点,应单独插入这些子节点。

root=I(null, { "type": "frame", "width": 1200, "layout": "vertical" })
nav=I(root, { "type": "frame", "role": "navbar", "height": 72 })
U(nav, { "fill": [{"type": "solid", "color": "#FFFFFF"}] })
card2=C(card1, grid, { "name": "Card 2" })
M(sidebar, main, 0)
D(old_section)
R(old_btn, { "type": "rectangle", "role": "button" })
操作语法动作
Iname=I(parent, { node })插入
UU(ref, { updates })更新
Cname=C(source, parent, { overrides })复制
Rname=R(ref, { node })替换
MM(ref, parent, index?)移动
DD(ref)删除
Gname=G(parent, "search", "query")通过搜索生成图像

便于绑定的模式 — 如果后续需要引用父节点和子节点,应分别插入:

btn=I(form, {"type":"rectangle","role":"button","width":"fill_container","height":50,"cornerRadius":12,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})
I(btn, {"type":"text","content":"Submit","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})
方式 3:脚本模式(适用于重复结构)

当循环或基于数据的重复操作比重复编写 DSL 操作更清晰时,可使用沙箱化 JavaScript:

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

通过 MCP 调用 batch_design,并且只使用 nodes_json、operations 或 script 中的一个。脚本模式提供两个用于记录操作的构建器:

const section = I(null, {type: "frame", name: "Clients", layout: "vertical", width: "fill_container"});
const rows = ["Alice Chen", "Bob Ito", "Sam Rivera"];
for (const name of rows) {
  const row = I(section, {type: "frame", layout: "horizontal", width: "fill_container", padding: [12, 16]});
  I(row, {type: "text", content: name});
}

// Instantiate a component from an attached UI kit:
K("shadcn/btn-primary", section, {name: "Primary Action"});

I(parent, node) 会插入节点并返回重新映射后的 ID。K(kitComponentId, parent, overrides) 会实例化 UI 套件组件,并返回重新映射后的 ID;使用 starter/<id>、shadcn/<id> 或 <kit-id>/<component-id>。根节点使用 null;否则传入先前记录调用返回的 ID。C、U、D、M、R、G 和 console.* 在脚本模式下仅作为空操作兼容存根;要执行更新、复制、替换、移动、删除或图像搜索操作,请使用 DSL。

脚本模式支持嵌套的 children 数组。限制:源代码最大 256 KiB、记录的操作最多 4096 项、记录的输出最大 8 MiB、墙上时钟评估时间为 2 秒、内存为 64 MiB。

严格 JSON 规则

输出 PenNode JSON(通过 op insert、批处理 DSL、nodes_json 或 insert_node)时,必须生成严格有效的 JSON。脚本模式使用 JavaScript 对象字面量。以下是一些会导致 JSON 解析失败的常见错误:

  • 每个属性都必须同时有键和值。 绝不要输出 ": 50 或不带键名的 : 50。截断或重新格式化时常会发生这种情况——请仔细检查。
  • 每个键都必须是非空的双引号字符串。
  • fill 始终是数组:"fill": [{"type": "solid", "color": "#hex"}]。像 "fill": "#hex" 这样的简写也可用,但数组形式是规范格式。
  • stroke 是包含 fill 数组的对象:"stroke": {"thickness": 1, "fill": [{"type": "solid", "color": "#hex"}]}。绝不要使用 "stroke": {"thickness": 1, "color": "#hex"} 或 "stroke": "#hex"(解析器会自动转换这些格式,但建议使用正确格式)。
  • } 或 ] 前不能有尾随逗号。
  • JSON 内不能有注释(// 或 /* */)。
  • 使用直双引号 ",不要使用智能引号或弯引号。
  • 文本使用 content,不要使用 text:{"type": "text", "content": "Hello"}。
  • 图标使用 iconFontName,不要使用 iconName 或 icon:{"type": "icon_font", "iconFontName": "lock"}。
  • 最终确定 JSON 前,请在脑中检查:每个键都有值,每个值都有对应的键,所有括号都匹配。

PenNode 架构

通用属性
{
  "type": "frame|group|rectangle|ellipse|polygon|line|text|path|text_input|text_area|select|switch|checkbox|slider|radio_group|number_input|progress|tabs|image|icon_font|ref",
  "name": "Display Name",
  "role": "semantic-role",
  "x": 0, "y": 0,
  "rotation": 0, "opacity": 1, "visible": true
}
容器属性(frame、rectangle、group、ellipse)
{
  "width": 400,              // number | "fill_container" | "fit_content"
  "height": 300,
  "layout": "vertical",      // "none" | "vertical" | "horizontal"
  "gap": 16,
  "padding": [16, 24],       // number | [v, h] | [top, right, bottom, left]
  "justifyContent": "center", // "start" | "center" | "end" | "space_between" | "space_around"
  "alignItems": "center",    // "start" | "center" | "end"
  "clipContent": true,
  "cornerRadius": 12,        // number | [tl, tr, br, bl]
  "fill": [{ "type": "solid", "color": "#FFFFFF" }],
  "stroke": { "thickness": 1, "fill": [{ "type": "solid", "color": "#E5E7EB" }], "align": "inside", "dashPattern": [5, 3] },
  "effects": [{ "type": "shadow", "offsetX": 0, "offsetY": 4, "blur": 12, "spread": 0, "color": "rgba(0,0,0,0.08)" }],
  "children": []
}
文本
{
  "type": "text",
  "content": "Hello",          // string or StyledTextSegment[]
  "fontSize": 16, "fontFamily": "Inter", "fontWeight": 600,
  "textAlign": "center",       // "left" | "center" | "right"
  "textGrowth": "fixed-width", // "auto" | "fixed-width" | "fixed-width-height"
  "lineHeight": 1.5, "letterSpacing": 0,
  "fill": [{ "type": "solid", "color": "#111111" }]
}

富文本:"content": [{ "text": "Bold ", "fontWeight": "bold" }, { "text": "normal" }]

图标 — 两种选项
选项 A:icon_font(推荐——直接渲染,无需后处理)
{ "type": "icon_font", "name": "Lock Icon", "iconFontName": "lock",
  "width": 20, "height": 20,
  "fill": [{ "type": "solid", "color": "#6B7280" }] }

字段为 iconFontName(不是 iconName,也不是 icon)。 值为小写 kebab-case Lucide 名称:mail、lock、eye、eye-off、chrome、apple、message-circle、x、arrow-right、search、heart、star、check、plus、bell、home、user、settings、chevron-right、download、globe、layers、zap、shield、play。

适用于所有场景:CLI、MCP 工具或直接使用 .op 文件——无需运行 design:refine。

选项 B:path(需要后处理)
{ "type": "path", "name": "HeartIcon", "width": 24, "height": 24,
  "fill": [{ "type": "solid", "color": "#111111" }] }

采用 PascalCase,并添加 "Icon" 后缀。后处理期间会从 Lucide 集合中自动解析。

路径图标需要后处理。 插入路径节点后,运行 op design:refine --root-id <id>,或使用 op insert --post-process。否则,路径图标不会渲染出来。独立 MCP 服务(由 ACP Agent 使用)未注册钩子实现,因此在那里无法解析路径图标——在 MCP 场景中优先使用 icon_font。

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

AI 图像占位符(由 design:refine 解析):

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

图像调整(范围均为 -100 到 100):exposure、contrast、saturation、temperature、tint、highlights、shadows。

多边形
{ "type": "polygon", "polygonCount": 6, "width": 80, "height": 80, "cornerRadius": 4,
  "fill": [{ "type": "solid", "color": "#6366F1" }] }
图标字体
{ "type": "icon_font", "iconFontName": "lucide:home", "width": 24, "height": 24,
  "fill": [{ "type": "solid", "color": "#111111" }] }
线条
{ "type": "line", "x2": 200, "y2": 0,
  "stroke": { "thickness": 1, "fill": [{ "type": "solid", "color": "#E5E7EB" }] } }
填充类型
{ "type": "solid", "color": "#3B82F6" }
{ "type": "linear_gradient", "angle": 135,
  "stops": [{ "offset": 0, "color": "#6366F1" }, { "offset": 1, "color": "#8B5CF6" }] }
{ "type": "radial_gradient", "cx": 0.5, "cy": 0.5, "radius": 0.5,
  "stops": [{ "offset": 0, "color": "#FFF" }, { "offset": 1, "color": "#000" }] }
{ "type": "image", "url": "https://example.com/texture.jpg", "mode": "fill" }

图像填充模式:fill、fit、crop、tile、stretch。图像填充还支持调整滤镜(exposure、contrast、saturation 等)。

Ref 节点(组件实例)
{ "type": "ref", "ref": "reusable-frame-id",
  "descendants": { "child-id": { "content": "Override text" } } }

引用一个设置了 reusable: true 的 frame。可通过 descendants 覆盖特定后代节点的属性。

设计变量

使用 $ 前缀引用:"color": "$primaryColor"、"gap": "$spacing"。

语义角色

角色用于声明意图——引擎会应用智能默认值。始终优先使用角色,而不是手动设置样式。

类别角色
布局section、row、column、centered-content、divider、spacer
导航navbar、nav-links、nav-link
交互button、icon-button、badge、tag、pill、input、form-input、search-bar
卡片card、feature-card、stat-card、pricing-card、image-card
内容hero、feature-grid、cta-section、footer、testimonial、stats-section
排版heading、subheading、body-text、caption、label
媒体avatar、icon、phone-mockup、screenshot-frame
表格table、table-row、table-header、table-cell
表单form-group

关键默认值:

  • navbar → 高度:56-72,水平布局,space_between,居中对齐
  • button → 内边距:[12, 24],cornerRadius:8,居中
  • card → 垂直布局,间距:12,cornerRadius:12,内边距:24
  • heading → lineHeight:1.2,letterSpacing:-0.5
  • body-text → fill_container,textGrowth:fixed-width,lineHeight:1.5

布局规则

  1. 绝不要在布局容器内的子元素上设置 x/y——由引擎负责定位
  2. 同级元素必须使用相同的宽度策略——全部使用 fill_container,或全部使用固定宽度
  3. 绝不要在 fit_content 父元素内使用 fill_container——这会产生循环依赖
  4. 水平排列的卡片:全部使用 width: "fill_container"、height: "fill_container"
尺寸选择
问题答案
拉伸填满?"fill_container"
收缩以适应内容?"fit_content"
精确尺寸?数字(px)
设计类型尺寸
类型宽度高度
着陆页12000(自动)
移动端屏幕375812
仪表板12000(自动)

设计原则

排版
Display:    40-56px  700  letterSpacing: -1.5  lineHeight: 1.1   "Space Grotesk"
Heading:    28-36px  700  letterSpacing: -0.5  lineHeight: 1.2   "Space Grotesk"
Subheading: 20-24px  600  letterSpacing: -0.25 lineHeight: 1.3   "Space Grotesk"
Body:       15-18px  400  letterSpacing: 0     lineHeight: 1.5   "Inter"
Caption:    13-14px  400  letterSpacing: 0     lineHeight: 1.4   "Inter"

中日韩文字:使用 "Noto Sans SC/JP/KR",lineHeight >= 1.3,letterSpacing 始终为 0。

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

饱和色最多使用 2 种。WCAG AA:正文对比度为 4.5:1,大号文字为 3:1。深色背景使用 #0F172A,而非 #000000。

间距(8px 网格)
Related:    8-16px     Components: 16-24px
Groups:     24-32px    Sections:   48-80px    Page padding: 80px
阴影
// Subtle (cards)
{ "type": "shadow", "offsetY": 1, "blur": 3, "color": "rgba(0,0,0,0.05)" }
// Medium (dropdowns)
{ "type": "shadow", "offsetY": 4, "blur": 12, "color": "rgba(0,0,0,0.08)" }
// Elevated (modals)
{ "type": "shadow", "offsetY": 8, "blur": 24, "spread": -4, "color": "rgba(0,0,0,0.12)" }
文案规则

标题:2-6 个词。副标题:最多 15 个词。按钮:1-3 个词。不要使用 lorem ipsum。不要用表情符号作为图标。

分层工作流

对于包含多个复杂分区的页面,请采用三步流程:骨架 → 内容 → 优化:

步骤MCP 工具CLI 等效命令
1. 创建分区结构design_skeletonop design:skeleton '<json>'
2. 填充各个分区design_content(使用 postProcess: true)op design:content <section-id> '<json>'
3. 验证并自动修复design_refineop design:refine --root-id <id>

design:refine 会将图标名称解析为 SVG 路径、修复布局问题并验证树结构。务必将其作为最后一步运行。

从模板开始

从空白画框开始通常不是最快的方式。op templates 会列出随附的目录——其中包括教程、对比、轮播图和 16:9 演示文稿;使用 op use-template <id> 可以引入模板:它会接管尚未修改的起始页面,并将模板画板添加到其他内容的右侧。模板自带调色板,因此画板会使用实际变量,而不是硬编码颜色。

op styles 会列出可固定使用的样式指南,包括用户导入的任何 DESIGN.md。如果用户有自己的指南,请优先使用——这是他们选择的素材。

步骤MCP 工具CLI 等效命令
浏览模板list_scene_templatesop templates [--scene S] [--tag T]
从模板开始use_scene_templateop use-template <id>
浏览样式资源list_style_guidesop styles [<id>]

交付结果

用户无法打开的设计就不算完成。请导出设计,而不是描述你会如何导出。

目标MCP 工具CLI 等效命令
单个节点/页面/选区export_item、export_nodesop export --output PATH --format png|pdf
演示文稿export_deckop export-deck --output PATH --format pptx|html|pdf
页面上的所有画板export_framesop export-frames --output-dir DIR
写入前检查演示文稿get_deck_boards—

对于演示文稿,pptx 是可编辑的 PowerPoint 格式,也是首选默认格式;html 是单个自包含文件;pdf 每张幻灯片占一页。隐藏的画板会按设计跳过。

目标位置参数是 outputPath/outputDir,绝不是 filePath。 在整个 API 中,filePath 都表示“此调用所针对的 .op 文档”,因此将它用于指定目标位置时,调用会指向一个不存在的文档。

代码生成流水线

从设计树进行增量式、适配框架的代码生成:

步骤CLI 命令MCP 工具描述
1. 规划op codegen:plan '<json>'codegen_plan声明代码块、依赖项、共享样式和根布局
2. 提交op codegen:submit <planId> '<json>'codegen_submit_chunk提交各个节点的生成代码
3. 组装op codegen:assemble <planId> --framework reactcodegen_assemble将所有代码块组合成最终输出
4. 清理op codegen:clean <planId>codegen_clean清除服务端代码生成状态

codegen:plan 会返回一个 planId;在同一条流水线中,将该值传给每个提交、组装和清理命令。

规划 JSON 的结构:

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

提交 JSON 的结构:

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

支持的框架:react、html、vue、svelte、flutter、swiftui、compose、react-native。

多页面文档

op page list                          # List all pages with IDs
op page add --name "Settings"         # Add a new page
op page remove <page-id>              # Remove a page
op page rename <page-id> 'New Name'   # Rename a page
op page reorder <page-id> 2           # Move page to index 2
op page duplicate <page-id>           # Clone page with new IDs

使用任意命令上的 --page <id> 指定目标页面。除非另有说明,大多数命令会使用当前页面;codegen:plan 是例外,默认使用第一页。

常见模式

以下模式展示了 op insert --parent 命令。每个模式都可以直接复制粘贴使用。

导航栏
NAV=$(op insert --parent "$ROOT" '{"type":"frame","role":"navbar","width":"fill_container","height":72,"layout":"horizontal","padding":[0,80],"justifyContent":"space_between","alignItems":"center","fill":[{"type":"solid","color":"#FFFFFF"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#F3F4F6"}]}}' | ID)
op insert --parent "$NAV" '{"type":"text","content":"Brand","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"}'
LINKS=$(op insert --parent "$NAV" '{"type":"frame","role":"nav-links","layout":"horizontal","gap":32,"width":"fit_content","height":"fit_content"}' | ID)
op insert --parent "$LINKS" '{"type":"text","role":"nav-link","content":"Features","fontSize":15}'
op insert --parent "$LINKS" '{"type":"text","role":"nav-link","content":"Pricing","fontSize":15}'
CTA=$(op insert --parent "$NAV" '{"type":"rectangle","role":"button","padding":[10,24],"cornerRadius":8,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}' | ID)
op insert --parent "$CTA" '{"type":"text","content":"Get Started","fontSize":14,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}'
主视觉区
HERO=$(op insert --parent "$ROOT" '{"type":"frame","role":"hero","width":"fill_container","height":"fit_content","layout":"vertical","padding":[100,80],"gap":24,"alignItems":"center"}' | ID)
op insert --parent "$HERO" '{"type":"text","role":"heading","content":"Build something great","fontSize":56,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-1.5,"lineHeight":1.1,"textGrowth":"fixed-width","width":800}'
op insert --parent "$HERO" '{"type":"text","role":"subheading","content":"The modern platform for teams who ship fast.","fontSize":18,"textAlign":"center","lineHeight":1.6,"textGrowth":"fixed-width","width":560,"fill":[{"type":"solid","color":"#6B7280"}]}'
BTNS=$(op insert --parent "$HERO" '{"type":"frame","layout":"horizontal","gap":12,"width":"fit_content","height":"fit_content"}' | ID)
B1=$(op insert --parent "$BTNS" '{"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}' | ID)
op insert --parent "$B1" '{"type":"text","content":"Start Free","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}'
B2=$(op insert --parent "$BTNS" '{"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#F3F4F6"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}' | ID)
op insert --parent "$B2" '{"type":"text","content":"View Demo","fontSize":16,"fontWeight":600}'
功能卡片(在水平网格中,所有卡片都必须使用 fill_container)
CARD=$(op insert --parent "$GRID" '{"type":"rectangle","role":"feature-card","width":"fill_container","height":"fill_container","layout":"vertical","padding":28,"gap":16,"cornerRadius":16,"fill":[{"type":"solid","color":"#F9FAFB"}]}' | ID)
op insert --parent "$CARD" '{"type":"path","name":"ZapIcon","width":24,"height":24,"fill":[{"type":"solid","color":"#111111"}]}'
op insert --parent "$CARD" '{"type":"text","content":"Lightning Fast","fontSize":20,"fontWeight":600}'
op insert --parent "$CARD" '{"type":"text","role":"body-text","content":"Sub-second builds with smart caching.","fontSize":15,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]}'
表单输入框
GRP=$(op insert --parent "$FORM" '{"type":"frame","role":"form-group","layout":"vertical","gap":8,"width":"fill_container"}' | ID)
op insert --parent "$GRP" '{"type":"text","role":"label","content":"Email","fontSize":14,"fontWeight":500}'
INP=$(op insert --parent "$GRP" '{"type":"rectangle","role":"form-input","width":"fill_container","height":48,"cornerRadius":10,"layout":"horizontal","padding":[0,16],"gap":10,"alignItems":"center","fill":[{"type":"solid","color":"#F9FAFB"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#E5E7EB"}]}}' | ID)
op insert --parent "$INP" '{"type":"path","name":"MailIcon","width":18,"height":18,"fill":[{"type":"solid","color":"#9CA3AF"}]}'
op insert --parent "$INP" '{"type":"text","content":"[email protected]","fontSize":15,"fill":[{"type":"solid","color":"#9CA3AF"}]}'
页脚
FOOTER=$(op insert --parent "$ROOT" '{"type":"frame","role":"footer","width":"fill_container","height":"fit_content","layout":"horizontal","padding":[48,80],"gap":80,"fill":[{"type":"solid","color":"#F9FAFB"}]}' | ID)
COL1=$(op insert --parent "$FOOTER" '{"type":"frame","layout":"vertical","gap":16,"width":240}' | ID)
op insert --parent "$COL1" '{"type":"text","content":"Brand","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"}'
op insert --parent "$COL1" '{"type":"text","content":"Building the future of design.","fontSize":14,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]}'
COL2=$(op insert --parent "$FOOTER" '{"type":"frame","layout":"vertical","gap":12,"width":"fit_content"}' | ID)
op insert --parent "$COL2" '{"type":"text","content":"Product","fontSize":14,"fontWeight":600}'
op insert --parent "$COL2" '{"type":"text","content":"Features","fontSize":14,"fill":[{"type":"solid","color":"#6B7280"}]}'
op insert --parent "$COL2" '{"type":"text","content":"Pricing","fontSize":14,"fill":[{"type":"solid","color":"#6B7280"}]}'

常见错误

错误修正方法
在布局容器内设置 x/y移除 x/y——引擎会自动定位
卡片使用不同的宽度策略所有同级元素都使用相同的尺寸策略(fill_container)
fill_container 子元素位于 fit_content 父元素中使用固定宽度,或将父元素切换为 fill_container
使用纯黑文本 #000000使用 #111111 或 #0F172A
使用较重的投影使用细微的 rgba(0,0,0,0.05-0.12)
将 Emoji 用作图标使用带有图标名称的路径节点
使用 Lorem ipsum 占位文本编写真实、简洁的文案
为文本设置固定高度改用 textGrowth: "fixed-width"
CJK 文本使用 Space Grotesk使用 "Noto Sans SC/JP/KR"
CJK 文本使用负 letterSpacingCJK 文本的值始终设为 0
插入后未进行后处理构建完树后运行 op design:refine --root-id <id>
插入的图标不可见路径节点需要通过 design:refine 或 --post-process 解析 SVG
无法在之后引用内联子元素使用单独的绑定 I() 调用插入它
MCP 中缺少 postProcess: true对 MCP 工具调用始终设置此项

完整示例——op insert 工作流(推荐)

使用 op insert --parent 构建完整的移动端登录页面。这是最可靠的方法。

#!/bin/bash
set -e
ID() { python3 -c "import sys,json; print(json.load(sys.stdin)['nodeId'])"; }

# Root frame (mobile)
ROOT=$(op insert '{"type":"frame","name":"Login","width":375,"height":812,"layout":"vertical","fill":[{"type":"solid","color":"#FFFFFF"}]}' | ID)

# Header
TOP=$(op insert --parent "$ROOT" '{"type":"frame","width":"fill_container","height":"fit_content","layout":"vertical","padding":[80,32,40,32],"gap":14,"alignItems":"center"}' | ID)
op insert --parent "$TOP" '{"type":"path","name":"ShieldIcon","width":48,"height":48,"fill":[{"type":"solid","color":"#6366F1"}]}'
op insert --parent "$TOP" '{"type":"text","content":"Welcome Back","fontSize":28,"fontWeight":700,"fontFamily":"Space Grotesk","letterSpacing":-0.5,"textAlign":"center"}'

# Form
FORM=$(op insert --parent "$ROOT" '{"type":"frame","width":"fill_container","height":"fit_content","layout":"vertical","padding":[0,32],"gap":20}' | ID)

# Email input
GRP=$(op insert --parent "$FORM" '{"type":"frame","role":"form-group","layout":"vertical","gap":8,"width":"fill_container"}' | ID)
op insert --parent "$GRP" '{"type":"text","role":"label","content":"Email","fontSize":14,"fontWeight":500}'
INP=$(op insert --parent "$GRP" '{"type":"rectangle","role":"form-input","width":"fill_container","height":48,"cornerRadius":10,"layout":"horizontal","padding":[0,16],"gap":10,"alignItems":"center","fill":[{"type":"solid","color":"#F9FAFB"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#E5E7EB"}]}}' | ID)
op insert --parent "$INP" '{"type":"path","name":"MailIcon","width":18,"height":18,"fill":[{"type":"solid","color":"#9CA3AF"}]}'
op insert --parent "$INP" '{"type":"text","content":"[email protected]","fontSize":15,"fill":[{"type":"solid","color":"#9CA3AF"}]}'

# Login button
BTN=$(op insert --parent "$FORM" '{"type":"rectangle","role":"button","width":"fill_container","height":50,"cornerRadius":12,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}' | ID)
op insert --parent "$BTN" '{"type":"text","content":"Sign In","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}'

# IMPORTANT: resolve icons + validate layout
op design:refine --root-id "$ROOT"

DSL 示例——着陆页

DSL 适用于面向操作的编辑。将父级和子级操作分开,可以为后续更新和复制提供绑定。

root=I(null, {"type":"frame","name":"Landing","width":1200,"height":0,"layout":"vertical","fill":[{"type":"solid","color":"#FFFFFF"}]})

nav=I(root, {"type":"frame","role":"navbar","width":"fill_container","height":72,"layout":"horizontal","padding":[0,80],"justifyContent":"space_between","alignItems":"center"})
I(nav, {"type":"text","content":"Acme","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"})
links=I(nav, {"type":"frame","role":"nav-links","layout":"horizontal","gap":32,"width":"fit_content","height":"fit_content"})
I(links, {"type":"text","role":"nav-link","content":"Features","fontSize":15})
I(links, {"type":"text","role":"nav-link","content":"Pricing","fontSize":15})
cta=I(nav, {"type":"rectangle","role":"button","padding":[10,24],"cornerRadius":8,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})
I(cta, {"type":"text","content":"Get Started","fontSize":14,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})

hero=I(root, {"type":"frame","role":"hero","width":"fill_container","height":"fit_content","layout":"vertical","padding":[100,80],"gap":24,"alignItems":"center"})
I(hero, {"type":"text","role":"heading","content":"Ship faster with Acme","fontSize":56,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-1.5,"lineHeight":1.1,"textGrowth":"fixed-width","width":800})
I(hero, {"type":"text","role":"subheading","content":"Turn ideas into production apps in minutes.","fontSize":18,"textAlign":"center","lineHeight":1.6,"textGrowth":"fixed-width","width":560,"fill":[{"type":"solid","color":"#6B7280"}]})
btns=I(hero, {"type":"frame","layout":"horizontal","gap":12,"width":"fit_content","height":"fit_content"})
b1=I(btns, {"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})
I(b1, {"type":"text","content":"Start Free","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})
b2=I(btns, {"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#F3F4F6"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})
I(b2, {"type":"text","content":"View Demo","fontSize":16,"fontWeight":600})

feat=I(root, {"type":"frame","role":"section","width":"fill_container","height":"fit_content","layout":"vertical","padding":[80,80],"gap":48,"alignItems":"center"})
I(feat, {"type":"text","role":"heading","content":"Everything you need","fontSize":36,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-0.5})
grid=I(feat, {"type":"frame","role":"feature-grid","width":"fill_container","layout":"horizontal","gap":24})
c1=I(grid, {"type":"rectangle","role":"feature-card","width":"fill_container","height":"fill_container","layout":"vertical","padding":28,"gap":16,"cornerRadius":16,"fill":[{"type":"solid","color":"#F9FAFB"}]})
I(c1, {"type":"path","name":"ZapIcon","width":24,"height":24,"fill":[{"type":"solid","color":"#111111"}]})
I(c1, {"type":"text","content":"Lightning Fast","fontSize":20,"fontWeight":600})
I(c1, {"type":"text","role":"body-text","content":"Sub-second builds with smart caching.","fontSize":15,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]})
c2=C(c1, grid, {})
U(c2+"/0", {"name":"ShieldIcon"})
U(c2+"/1", {"content":"Enterprise Security"})
U(c2+"/2", {"content":"SOC 2 certified with end-to-end encryption."})
c3=C(c1, grid, {})
U(c3+"/0", {"name":"GitBranchIcon"})
U(c3+"/1", {"content":"Git-Native Workflow"})
U(c3+"/2", {"content":"Preview deploys on every push with instant rollback."})

评论

支持 Markdown