Merge pull request #687 from open-pencil/native-authoring
refactor: share design authoring guidance across agents
This commit is contained in:
commit
25cc522f12
|
|
@ -113,7 +113,7 @@ Use Conventional Commits (`feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `bu
|
|||
- CLI commands own CLI UX independently; `eval` exposes operations through `FigmaAPI`.
|
||||
- MCP-only filesystem/server tools live in `packages/mcp/src/tool/registration.ts`; listener/session lifecycle lives under `server/`, stdio under `stdio/`, and transport discovery under `transport/`. File access must resolve symlinks inside the effective MCP root; CLI defaults are cwd on macOS/Linux and home on Windows.
|
||||
- Keep MCP transport tests under `tests/engine/mcp/{server,stdio,transport}` and shared fixtures under `tests/helpers/mcp`; isolate tests from user runtime discovery.
|
||||
- Core codegen prompts live under `packages/core/src/tools/prompts/`; app chat/ACP prompts under `src/app/ai/**`.
|
||||
- Shared scene-authoring guidance and tested examples live under `packages/core/src/design-jsx/reference/`; `reference.ts` combines them with renderer metadata. Core codegen prompts under `packages/core/src/tools/prompts/` and the app chat/ACP prompt compose that public reference rather than copying it. Run `bun run generate:authoring-reference` after changes; committed skill/docs copies are checked by `check:authoring-reference` (also part of `check:docs`). Do not edit generated reference files directly.
|
||||
- The installable agent skill is maintained in `skills/open-pencil/`. Changes to agent-facing APIs, CLI/MCP behavior, or design authoring must update affected skill examples, prompts, and public documentation in the same change. Keep examples valid in their actual execution environment; do not advertise library exports as scripting globals unless exposed there. Prefer runtime discovery and canonical references over duplicated API/tool inventories.
|
||||
|
||||
## ACP and collaboration
|
||||
|
|
@ -143,6 +143,8 @@ Private tooling belongs under `tools/<domain>/{src,tests}`, with kebab-case doma
|
|||
|
||||
- Use `@/` for app cross-directory imports. Never escape an alias root with `../` (for example `#tests/../vite`); fix module ownership instead. Package aliases are `#vue/*`, `#cli/*`, `#dom-css/*`, `#mcp/*`, and `#core/*`; prefer clear relative imports nearby.
|
||||
- No `any`, non-null assertions, or `Math.random()`; use precise types, guards, and `crypto.getRandomValues()`.
|
||||
- Use the existing `dedent` package for multiline prompt composition and embedded examples instead of escaped newline strings. Keep substantial prompt prose in the owning Markdown source; compose it rather than duplicating it. Apply this convention across app, packages, and tools.
|
||||
- Tooling must resolve the workspace with `resolveWorkspaceRoot` from `@open-pencil/package-artifacts`, not parent-directory traversal. Use domain aliases for cross-directory tooling imports, including tests; keep nearby sibling imports relative.
|
||||
- Reuse named types and primitives from `@open-pencil/scene-graph`; do not respell `Color`, `Vector`, `SceneNode`, `Effect`, `Fill`, or `Stroke` shapes.
|
||||
- Window API augmentations belong in the owning compilation boundary: app declarations in `src/global.d.ts`, package DOM gaps in the owning package's `global.d.ts`, and native-test declarations in `tests/helpers/tauri/native-global.d.ts`. Never put `declare global` in specs or implementation modules. Include canonical declarations through tsconfig instead of duplicating them.
|
||||
- Keep app API contracts named and owned by their implementation domain; declaration files import those types. Derive vendor API types from top-level type imports rather than hand-copying signatures. Optional runtime globals remain optional and require a runtime guard.
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ Local clients discover the private Unix socket automatically and fall back to lo
|
|||
|
||||
**File access:** Set `OPENPENCIL_MCP_ROOT` to scope file operations (`open_file`, `new_document`, export `path` param) to a directory. Defaults to the current working directory.
|
||||
|
||||
### AI agent skill
|
||||
### [AI agent skill](skills/open-pencil/SKILL.md)
|
||||
|
||||
Teach your AI coding agent to use OpenPencil — inspect designs, export assets, analyze tokens, modify .fig files:
|
||||
|
||||
|
|
|
|||
11
bun.lock
11
bun.lock
|
|
@ -182,6 +182,7 @@
|
|||
"acorn": "^8.18.0",
|
||||
"canvaskit-wasm": "^0.41.1",
|
||||
"culori": "^4.0.2",
|
||||
"dedent": "^1.7.2",
|
||||
"destr": "^2.0.5",
|
||||
"diff": "^8.0.4",
|
||||
"es-toolkit": "^1.51.0",
|
||||
|
|
@ -374,6 +375,14 @@
|
|||
"canvaskit-wasm",
|
||||
],
|
||||
},
|
||||
"tools/authoring-reference": {
|
||||
"name": "@open-pencil/authoring-reference-tools",
|
||||
"dependencies": {
|
||||
"@open-pencil/core": "workspace:*",
|
||||
"@open-pencil/package-artifacts": "workspace:*",
|
||||
"dedent": "^1.7.2",
|
||||
},
|
||||
},
|
||||
"tools/docs": {
|
||||
"name": "@open-pencil/docs-tools",
|
||||
},
|
||||
|
|
@ -1163,6 +1172,8 @@
|
|||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@open-pencil/authoring-reference-tools": ["@open-pencil/authoring-reference-tools@workspace:tools/authoring-reference"],
|
||||
|
||||
"@open-pencil/cli": ["@open-pencil/cli@workspace:packages/cli"],
|
||||
|
||||
"@open-pencil/core": ["@open-pencil/core@workspace:packages/core"],
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"packages/docs",
|
||||
"tools/docs",
|
||||
"tools/package-artifacts",
|
||||
"tools/authoring-reference",
|
||||
"tools/package-quality",
|
||||
"tools/release-packages",
|
||||
"tools/visual-oracles"
|
||||
|
|
@ -41,7 +42,9 @@
|
|||
"check": "bun run build:packages && bun run lint && tsgo --noEmit && bun run check:vue && bun run check:native-test && bun run check:i18n && bun run check:docs && bun run check:changelog && bun run check:packages && bun run check:deps && bun run check:audit && bun run check:secrets && bun run check:monorepo && bun run check:arch && bun run test:type-shapes && bun run test:tools && bun run test:dupes",
|
||||
"check:changelog": "bun tools/release-packages/src/check-changelog.ts",
|
||||
"check:deps": "knip --include unlisted,unresolved,binaries",
|
||||
"check:docs": "bun --filter @open-pencil/docs check",
|
||||
"check:docs": "bun --filter @open-pencil/docs check && bun run check:authoring-reference",
|
||||
"check:authoring-reference": "bun tools/authoring-reference/src/check.ts",
|
||||
"generate:authoring-reference": "bun tools/authoring-reference/src/generate.ts",
|
||||
"check:audit": "bun audit --audit-level=critical",
|
||||
"check:secrets": "bun tools/secret-scan/src/index.ts",
|
||||
"check:monorepo": "sherif --ignore-rule root-package-dependencies",
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@
|
|||
"acorn": "^8.18.0",
|
||||
"canvaskit-wasm": "^0.41.1",
|
||||
"culori": "^4.0.2",
|
||||
"dedent": "^1.7.2",
|
||||
"destr": "^2.0.5",
|
||||
"diff": "^8.0.4",
|
||||
"es-toolkit": "^1.51.0",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,5 @@ export {
|
|||
} from './schema'
|
||||
export { transformDesignJSXExpression } from './transform'
|
||||
|
||||
import jsxReference from '#core/tools/prompts/jsx-reference.md?raw'
|
||||
|
||||
export { sceneNodeToJSX, selectionToJSX, type JSXFormat } from '#core/io/formats/jsx'
|
||||
export const JSX_REFERENCE: string = jsxReference
|
||||
export { JSX_REFERENCE, AUTHORING_EXAMPLES, type AuthoringExample } from './reference'
|
||||
|
|
|
|||
30
packages/core/src/design-jsx/reference.ts
Normal file
30
packages/core/src/design-jsx/reference.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import dedent from 'dedent'
|
||||
|
||||
import authoring from './reference/authoring.md?raw'
|
||||
import { AUTHORING_EXAMPLES } from './reference/examples'
|
||||
import {
|
||||
DESIGN_JSX_ELEMENTS,
|
||||
DESIGN_JSX_HELPERS,
|
||||
DESIGN_JSX_SUPPORTED_PROPERTY_NAMES
|
||||
} from './schema'
|
||||
|
||||
export { AUTHORING_EXAMPLES, type AuthoringExample } from './reference/examples'
|
||||
|
||||
/** Canonical scene-authoring guidance shared by runtime prompts and generated skill references. */
|
||||
export const JSX_REFERENCE = [
|
||||
authoring.trim(),
|
||||
...AUTHORING_EXAMPLES.map(
|
||||
({ title, jsx }) => dedent`
|
||||
## ${title}
|
||||
|
||||
\`\`\`jsx
|
||||
${jsx}
|
||||
\`\`\`
|
||||
`
|
||||
),
|
||||
'## Supported syntax inventory',
|
||||
'Generated from the renderer metadata. This inventory lists accepted names, not arbitrary browser CSS support.',
|
||||
`**Elements:** ${DESIGN_JSX_ELEMENTS.map(({ name }) => `\`${name}\``).join(', ')}.`,
|
||||
`**Helpers:** ${DESIGN_JSX_HELPERS.map(({ name }) => `\`${name}\``).join(', ')}.`,
|
||||
`**Properties:** ${DESIGN_JSX_SUPPORTED_PROPERTY_NAMES.map((name) => `\`${name}\``).join(', ')}.`
|
||||
].join('\n\n')
|
||||
41
packages/core/src/design-jsx/reference/authoring.md
Normal file
41
packages/core/src/design-jsx/reference/authoring.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# OpenPencil design authoring
|
||||
|
||||
This reference describes scene creation, not React DOM output. Use the `render` tool for JSX strings, or import `Frame`, `Text`, `renderTree`, and other authoring exports from `@open-pencil/core/design-jsx` in library code. Library exports are not automatically globals in agent `eval`; use only the bindings exposed by that execution environment.
|
||||
|
||||
## Composition and layout
|
||||
|
||||
- `flex="row"` / `flex="col"` enables auto-layout. Use it for content; reserve explicit `x`/`y` or `position="absolute"` for intentional overlays and artwork. Without layout, children share the origin unless positioned.
|
||||
- `w` / `h` accept pixels, `"hug"` (content-sized), or `"fill"` (available space in a supported layout parent). Use Hug for notes, cards, and long pages instead of guessing heights. Fixed viewport sizes and artwork geometry are intentional exceptions.
|
||||
- `gap` controls spacing. `p`, `px`, `py`, and `pt`/`pr`/`pb`/`pl` control padding; longhands override shorthands. There is no margin shorthand.
|
||||
- `justify="start"|"end"|"center"|"between"` controls the primary axis; `items="start"|"end"|"center"|"stretch"` controls the cross axis. Distribution needs available space: `between` cannot create extra room in a Hug container.
|
||||
- `grow` distributes available space. Avoid circular Hug/Fill dependencies and redundant fixed widths on growing children. Keep Fill sizing through intermediate containers that should stretch.
|
||||
- For wrapping text in a column, prefer `w="fill"`; fixed-width text can use `textAutoResize="height"`. `maxLines` / `truncate` are intentional truncation, not fixes for accidental overflow.
|
||||
- `wrap` and `rowGap` enable wrapped flex rows. `grid`, `columns`, and `rows` enable grid (for example `columns="1fr 200px 1fr"`). Grid children use `colStart`, `rowStart`, `colSpan`, and `rowSpan`. The current grid `gap` shorthand takes precedence over `columnGap` and `rowGap`.
|
||||
- `flow="auto"|"ltr"|"rtl"` controls container flow; text `dir` controls writing direction. Preserve these separately.
|
||||
- Use measured node bounds and the existing `arrange` tool for independent artboards. Prefer layout constraints to calculating child coordinates; ordinary JavaScript arithmetic is appropriate when real geometry calculations are needed.
|
||||
|
||||
## Paint, text, and artwork
|
||||
|
||||
- `bg` / `fill`, `stroke`, and text `color` accept colors and supported variable references. Set colors explicitly for predictable contrast. `fills` accepts structured paints; gradient helpers include `linearGradient`, `radialGradient`, `angularGradient`, and `diamondGradient`.
|
||||
- `rounded` and `roundedTL`/`roundedTR`/`roundedBL`/`roundedBR` control corners. `strokeWidth`, `opacity`, `rotate`, and `blendMode` control appearance. `overflow="hidden"` clips content; do not hide accidental text overflow to make a broken layout appear correct.
|
||||
- `effects` accepts structured effects such as `dropShadow`, `innerShadow`, and `layerBlur`. `shadow="offsetX offsetY blur #color"` and `blur` are convenient shorthands.
|
||||
- Text content belongs inside `Text`. Use `size`, `font`, `weight`, `lineHeight`, `letterSpacing`, `textAlign`, `textDecoration`, and `textCase`. Verify fonts actually load before judging dimensions; do not assume every font is available.
|
||||
- `Icon` uses an Iconify name, size, and color. Prefer icons to emoji when reliable vector output is needed. Image fills belong on appropriate leaf shapes, not containers whose children must remain visible.
|
||||
- Design JSX props are the portable authoring interface. Some CSS-style aliases are supported, but this is not a browser CSS engine; do not assume arbitrary HTML, classes, or styles work.
|
||||
|
||||
## Variables and components
|
||||
|
||||
- Create document variables before referencing them with `designVar('id-or-name')`. `defineVars` groups references; it does not create variable collections.
|
||||
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`. Grid `columnGap`/`rowGap` and wrapped flex `rowGap` also support FLOAT references; grid `gap` overrides both axis-specific gaps. Use numbers or FLOAT references for these scalar props, not CSS unit strings.
|
||||
- References preserve real graph bindings, not just copied values. Set the intended collection mode on the parent before creating scalar-bound content: initial scalar layout resolves that inherited mode. This does not guarantee automatic scalar layout recomputation after a later mode switch. Verify resulting geometry as well as paint when changing modes. Missing or incorrectly typed scalar variables are errors.
|
||||
- `bind` maps supported scene-field paths to variable IDs or references when no shorthand exists. Use semantic tokens consistently rather than declaring unused collections.
|
||||
- A reusable JavaScript function shares source code, not component identity. Use `Component`, `ComponentSet`, and `Instance` for editable main components and linked instances.
|
||||
- `Instance` resolves an existing component through `of`, `component`, or `componentId`. Component-set children named `variant=Primary`, for example, define variants that can be selected when instantiating the set.
|
||||
- Distinguish a main component's default size from its instance's placement constraints. Verify the actual instance bounds in narrower parents; a requested Fill dimension alone is not proof that inherited sizing changed. Do not compensate for a sizing mismatch with guessed heights, clipping, or manually positioned siblings.
|
||||
- Reuse existing local or library components before recreating them. Expose meaningful text, visibility, and swap properties through the existing component-property APIs; do not assume every native property API is already exposed as a JSX prop.
|
||||
|
||||
## Verification
|
||||
|
||||
Inspect structure and actual rendered output. Node counts and `describe` diagnostics do not establish visual fidelity. Check wrapping with longer content, narrower containers, component edits, and relevant modes. Resolve overflow and contrast problems at their source. Reuse IDs returned by creation tools rather than repeatedly searching for the same nodes.
|
||||
|
||||
The examples below are executed by the authoring-reference tests. Create the named variables before running a variable-bound example.
|
||||
23
packages/core/src/design-jsx/reference/examples.ts
Normal file
23
packages/core/src/design-jsx/reference/examples.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import dedent from 'dedent'
|
||||
|
||||
export interface AuthoringExample {
|
||||
title: string
|
||||
jsx: string
|
||||
}
|
||||
|
||||
/** Shared verbatim by runtime prompts and the installable skill; exercised in engine tests. */
|
||||
export const AUTHORING_EXAMPLES: readonly AuthoringExample[] = [
|
||||
{
|
||||
title: 'Content-sized review note',
|
||||
jsx: dedent`<Frame name="Review note" w={280} h="hug" flex="col" gap={8} p={16} bg="#FFFFFF">
|
||||
<Text name="Author" size={12} weight="medium" color="#252A31">June Lee</Text>
|
||||
<Text name="Message" w="fill" size={12} color="#6B7079">Give the date a little more room at the bottom.</Text>
|
||||
</Frame>`
|
||||
},
|
||||
{
|
||||
title: 'Variable-bound spacing and typography',
|
||||
jsx: dedent`<Frame name="Bound note" w={280} h="hug" flex="col" gap={designVar('Space/small')} p={designVar('Space/medium')} bg="#FFFFFF">
|
||||
<Text name="Message" w="fill" size={designVar('Type/body')} lineHeight={designVar('Type/body-leading')} letterSpacing={designVar('Type/body-tracking')} color="#252A31">A note that grows with its content.</Text>
|
||||
</Frame>`
|
||||
}
|
||||
]
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import codegenPrompt from './tools/prompts/codegen.md?raw'
|
||||
import jsxReference from './tools/prompts/jsx-reference.md?raw'
|
||||
export { CODEGEN_PROMPT } from './tools/prompts'
|
||||
export { JSX_REFERENCE } from './design-jsx/reference'
|
||||
|
||||
export { randomHex, randomInt, randomIndex } from './random'
|
||||
|
||||
|
|
@ -431,8 +431,6 @@ export {
|
|||
export * from './io'
|
||||
export * from './lint'
|
||||
|
||||
export const CODEGEN_PROMPT: string = codegenPrompt
|
||||
export const JSX_REFERENCE: string = jsxReference
|
||||
export {
|
||||
setPexelsAPIKey,
|
||||
setUnsplashAccessKey,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import codegenPrompt from './prompts/codegen.md?raw'
|
||||
export { CODEGEN_PROMPT } from './prompts'
|
||||
|
||||
export { getComponentCatalog, registerComponentCatalog } from './component-catalog'
|
||||
export type {
|
||||
|
|
@ -7,7 +7,6 @@ export type {
|
|||
ComponentCatalogLibraryAsset
|
||||
} from './component-catalog'
|
||||
export { ALL_TOOLS, CORE_TOOLS, EXTENDED_TOOLS } from './registry'
|
||||
export const CODEGEN_PROMPT: string = codegenPrompt
|
||||
export { exportImage } from './vector'
|
||||
export {
|
||||
defineTool,
|
||||
|
|
|
|||
|
|
@ -244,3 +244,7 @@ assets/
|
|||
- `textAutoResize: NONE` → fixed width and height
|
||||
- `textDecoration` → `text-decoration`
|
||||
- `textCase` → `text-transform`
|
||||
|
||||
## Scene authoring reference
|
||||
|
||||
The appended reference applies when inspecting or editing OpenPencil scenes, not to the target frontend framework's API.
|
||||
|
|
|
|||
12
packages/core/src/tools/prompts/index.ts
Normal file
12
packages/core/src/tools/prompts/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import dedent from 'dedent'
|
||||
|
||||
import { JSX_REFERENCE } from '#core/design-jsx/reference'
|
||||
|
||||
import codegen from './codegen.md?raw'
|
||||
|
||||
/** Keep frontend-code generation instructions distinct from scene-authoring syntax. */
|
||||
export const CODEGEN_PROMPT = dedent`
|
||||
${codegen.trim()}
|
||||
|
||||
${JSX_REFERENCE}
|
||||
`
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
# OpenPencil JSX Reference
|
||||
|
||||
## Elements
|
||||
|
||||
| Tag | Description |
|
||||
| --------- | -------------------------------------- |
|
||||
| Frame | Container / auto-layout frame |
|
||||
| Rectangle | Rectangle shape |
|
||||
| Ellipse | Circle / ellipse shape |
|
||||
| Text | Text node (children = text content) |
|
||||
| Line | Line shape |
|
||||
| Star | Star shape |
|
||||
| Polygon | Polygon shape (default 3 sides) |
|
||||
| Vector | Vector path |
|
||||
| Group | Group container |
|
||||
| Section | Section (like Frame, for organization) |
|
||||
| Component | Component definition |
|
||||
| Icon | Iconify icon (requires name prop) |
|
||||
|
||||
Aliases: View = Frame, Rect = Rectangle
|
||||
|
||||
## Layout Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| -------------- | ----------------------------------------- | ------------------------------ |
|
||||
| flex | "row" \| "col" | Enable auto-layout direction |
|
||||
| gap | number | Spacing between children |
|
||||
| wrap | boolean | Enable flex wrap |
|
||||
| rowGap | number | Cross-axis gap when wrap is on |
|
||||
| justify | "start" \| "end" \| "center" \| "between" | Main axis alignment |
|
||||
| items | "start" \| "end" \| "center" \| "stretch" | Cross axis alignment |
|
||||
| p | number | Padding (all sides) |
|
||||
| px | number | Horizontal padding |
|
||||
| py | number | Vertical padding |
|
||||
| pt, pr, pb, pl | number | Individual side padding |
|
||||
| grow | number | Flex grow factor |
|
||||
|
||||
## Sizing Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| -------- | ------------------------- | --------------------------------------- |
|
||||
| w | number \| "fill" \| "hug" | Width |
|
||||
| h | number \| "fill" \| "hug" | Height |
|
||||
| x | number | X position |
|
||||
| y | number | Y position |
|
||||
| position | "absolute" | Absolute positioning inside auto-layout |
|
||||
| top | number | Top offset (sets absolute positioning) |
|
||||
| left | number | Left offset (sets absolute positioning) |
|
||||
| minW | number | Minimum width |
|
||||
| maxW | number | Maximum width |
|
||||
|
||||
## Appearance Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --------------- | -------- | -------------------------------------- |
|
||||
| bg | string | Background color (hex, e.g. "#FF0000") |
|
||||
| stroke | string | Stroke color |
|
||||
| strokeWidth | number | Stroke weight (default 1) |
|
||||
| rounded | number | Corner radius (all corners) |
|
||||
| roundedTL | number | Top-left corner radius |
|
||||
| roundedTR | number | Top-right corner radius |
|
||||
| roundedBL | number | Bottom-left corner radius |
|
||||
| roundedBR | number | Bottom-right corner radius |
|
||||
| cornerSmoothing | number | iOS-style corner smoothing |
|
||||
| opacity | number | 0–1 opacity |
|
||||
| rotate | number | Rotation in degrees |
|
||||
| blendMode | string | Blend mode (e.g. "multiply") |
|
||||
| overflow | "hidden" | Clip content |
|
||||
|
||||
## Text Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| -------------- | -------------------------------------------- | -------------------------------------- |
|
||||
| size | number | Font size (default 14) |
|
||||
| font | string | Font family |
|
||||
| weight | number \| "bold" \| "medium" | Font weight |
|
||||
| color | string | Text color (hex) |
|
||||
| textAlign | "left" \| "center" \| "right" \| "justified" | Text alignment |
|
||||
| lineHeight | number | Line height in px |
|
||||
| letterSpacing | number | Letter spacing in px |
|
||||
| textDecoration | "underline" \| "strikethrough" | Text decoration |
|
||||
| textCase | "upper" \| "lower" \| "title" | Text transform |
|
||||
| maxLines | number | Max visible lines (enables truncation) |
|
||||
| truncate | boolean | Enable text truncation |
|
||||
|
||||
## Shape Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ----------- | ------ | ----------------------------------------------- |
|
||||
| points | number | Point count (Star default 5, Polygon default 3) |
|
||||
| innerRadius | number | Star inner radius ratio |
|
||||
|
||||
## Effect Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ------ | ------ | ----------------------------------------- |
|
||||
| shadow | string | Drop shadow: "offsetX offsetY blur color" |
|
||||
| blur | number | Layer blur radius |
|
||||
|
||||
## Grid Layout
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --------- | ---------------- | ----------------------------------------------- |
|
||||
| grid | boolean | Enable CSS Grid layout |
|
||||
| columns | string \| number | Grid template columns (e.g. "1fr 1fr 1fr" or 3) |
|
||||
| rows | string \| number | Grid template rows |
|
||||
| columnGap | number | Column gap |
|
||||
| rowGap | number | Row gap |
|
||||
| colStart | number | Grid column start (child) |
|
||||
| rowStart | number | Grid row start (child) |
|
||||
| colSpan | number | Grid column span (child) |
|
||||
| rowSpan | number | Grid row span (child) |
|
||||
|
||||
## Icon Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ----- | ------ | --------------------------------------- |
|
||||
| name | string | Iconify icon name (e.g. "lucide:heart") |
|
||||
| size | number | Icon size (default 24) |
|
||||
| color | string | Icon color (hex) |
|
||||
| label | string | Display name for the icon node |
|
||||
|
||||
## Examples
|
||||
|
||||
```jsx
|
||||
{
|
||||
/* Card with title and description */
|
||||
}
|
||||
;<Frame name="Card" w={320} flex="col" gap={16} p={24} bg="#FFFFFF" rounded={16}>
|
||||
<Text size={18} weight="bold" color="#111">
|
||||
Card Title
|
||||
</Text>
|
||||
<Text size={14} color="#6B7280">
|
||||
Description text here
|
||||
</Text>
|
||||
</Frame>
|
||||
|
||||
{
|
||||
/* Badge in corner of a card */
|
||||
}
|
||||
;<Frame name="Card" w={320} h={200} flex="col" p={24} bg="#FFFFFF" rounded={16}>
|
||||
<Frame position="absolute" top={8} left={280} w={24} h={24} bg="#EF4444" rounded={12}>
|
||||
<Text size={10} weight="bold" color="#FFF">
|
||||
3
|
||||
</Text>
|
||||
</Frame>
|
||||
<Text size={18} weight="bold" color="#111">
|
||||
Card Title
|
||||
</Text>
|
||||
</Frame>
|
||||
|
||||
{
|
||||
/* Horizontal button row */
|
||||
}
|
||||
;<Frame flex="row" gap={8} items="center">
|
||||
<Rectangle w={40} h={40} bg="#3B82F6" rounded={8} />
|
||||
<Text size={14} weight="medium" color="#000">
|
||||
Click me
|
||||
</Text>
|
||||
</Frame>
|
||||
|
||||
{
|
||||
/* Grid layout */
|
||||
}
|
||||
;<Frame grid columns="1fr 1fr 1fr" gap={16} p={16} w={400}>
|
||||
<Rectangle w={100} h={100} bg="#EF4444" rounded={8} />
|
||||
<Rectangle w={100} h={100} bg="#22C55E" rounded={8} />
|
||||
<Rectangle w={100} h={100} bg="#3B82F6" rounded={8} />
|
||||
</Frame>
|
||||
|
||||
{
|
||||
/* Star shape */
|
||||
}
|
||||
;<Star w={48} h={48} points={5} innerRadius={0.38} bg="#EAB308" />
|
||||
```
|
||||
|
|
@ -64,6 +64,8 @@ export const programmableSidebar = (
|
|||
{ text: labels.analyzing, link: `${prefix}/programmable/cli/analyzing` },
|
||||
{ text: labels.scripting, link: `${prefix}/programmable/cli/scripting` },
|
||||
{ text: labels.jsxRenderer, link: `${prefix}/programmable/jsx-renderer` },
|
||||
{ text: 'Native JavaScript APIs', link: '/programmable/native-api' },
|
||||
{ text: 'Design authoring reference', link: '/reference/design-authoring' },
|
||||
{ text: labels.mcpServer, link: '/programmable/mcp-server' },
|
||||
{ text: labels.aiChat, link: `${prefix}/programmable/ai-chat` },
|
||||
...(!prefix
|
||||
|
|
|
|||
|
|
@ -1,116 +1,35 @@
|
|||
---
|
||||
title: JSX Renderer
|
||||
description: Create designs with JSX — the syntax LLMs already know from millions of React components.
|
||||
description: Create editable designs using the same declarative authoring library as OpenPencil agents.
|
||||
---
|
||||
|
||||
# JSX Renderer
|
||||
|
||||
OpenPencil uses JSX as its design creation language. LLMs have seen millions of React components — describing a layout as `<Frame><Text>` is natural, no special training needed. Every token matters when an AI agent performs dozens of operations, and JSX is the most compact declarative representation.
|
||||
|
||||
JSX is also diffable. When an AI modifies a design, the change is a JSX diff — readable, reviewable, version-controllable.
|
||||
OpenPencil's design JSX creates scene nodes, not browser DOM. You can use readable component trees, JavaScript expressions, and reusable functions to construct editable designs.
|
||||
|
||||
## Creating Designs
|
||||
|
||||
The `render` tool (available in AI chat, MCP, and CLI eval) accepts JSX:
|
||||
The `render` tool accepts JSX strings in AI chat and MCP. In application or headless library code, import `Frame`, `Text`, `renderTree`, and other authoring exports from `@open-pencil/core/design-jsx`. The scripting environment determines which APIs are exposed to `eval`; package exports are not automatically globals there.
|
||||
|
||||
```jsx
|
||||
<Frame name="Card" w={320} h="hug" flex="col" gap={16} p={24} bg="#FFF" rounded={16}>
|
||||
<Text size={18} weight="bold">Card Title</Text>
|
||||
<Text size={14} color="#666">Description text</Text>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
In the MCP server and AI chat, the `render` tool accepts JSX strings directly. In the CLI, use the `export` command to go the other direction — [exporting designs as JSX](./cli/exporting).
|
||||
The [shared design-authoring reference](../reference/design-authoring) contains executable examples, layout guidance, and the supported syntax inventory. It is generated from the same source used by chat, ACP, codegen prompts, and the installable agent skill.
|
||||
|
||||
## Elements
|
||||
|
||||
All node types are available as JSX elements:
|
||||
|
||||
| Element | Creates | Aliases |
|
||||
|---------|---------|---------|
|
||||
| `<Frame>` | Frame (container, supports auto-layout) | `<View>` |
|
||||
| `<Rectangle>` | Rectangle | `<Rect>` |
|
||||
| `<Ellipse>` | Ellipse / circle | |
|
||||
| `<Text>` | Text node (children become text content) | |
|
||||
| `<Line>` | Line | |
|
||||
| `<Star>` | Star | |
|
||||
| `<Polygon>` | Polygon | |
|
||||
| `<Vector>` | Vector path | |
|
||||
| `<Group>` | Group | |
|
||||
| `<Section>` | Section | |
|
||||
Use frames and text for composition, shape nodes for artwork, and real components and instances for reusable editor content. A JavaScript function alone does not create component identity. See the [element inventory](../reference/design-authoring#supported-syntax-inventory).
|
||||
|
||||
## Style Props
|
||||
|
||||
Compact shorthand props inspired by Tailwind's naming.
|
||||
|
||||
### Layout
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `flex` | `"row"` or `"col"` — enables auto-layout |
|
||||
| `gap` | Space between children |
|
||||
| `wrap` | Wrap children to next line |
|
||||
| `rowGap` | Counter-axis spacing when wrapping |
|
||||
| `justify` | `"start"`, `"end"`, `"center"`, `"between"` |
|
||||
| `items` | `"start"`, `"end"`, `"center"`, `"stretch"` |
|
||||
| `p`, `px`, `py`, `pt`, `pr`, `pb`, `pl` | Padding |
|
||||
|
||||
### Size & Position
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `w`, `h` | Width/height — number, `"fill"`, or `"hug"` |
|
||||
| `minW`, `maxW`, `minH`, `maxH` | Size constraints |
|
||||
| `x`, `y` | Position |
|
||||
|
||||
### Appearance
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `bg` | Background fill (hex color) |
|
||||
| `fill` | Alias for `bg` |
|
||||
| `stroke` | Stroke color |
|
||||
| `strokeWidth` | Stroke width (default: 1) |
|
||||
| `rounded` | Corner radius (or `roundedTL`, `roundedTR`, `roundedBL`, `roundedBR`) |
|
||||
| `cornerSmoothing` | iOS-style smooth corners (0–1) |
|
||||
| `opacity` | 0–1 |
|
||||
| `shadow` | Drop shadow (e.g. `"0 4 8 #00000040"`) |
|
||||
| `blur` | Layer blur radius |
|
||||
| `rotate` | Rotation in degrees |
|
||||
| `blendMode` | Blend mode |
|
||||
| `overflow` | `"hidden"` or `"visible"` |
|
||||
|
||||
### Typography
|
||||
|
||||
| Prop | Description |
|
||||
|------|-------------|
|
||||
| `size` / `fontSize` | Font size |
|
||||
| `font` / `fontFamily` | Font family |
|
||||
| `weight` / `fontWeight` | `"bold"`, `"medium"`, `"normal"`, or number |
|
||||
| `color` | Text color |
|
||||
| `textAlign` | `"left"`, `"center"`, `"right"`, `"justified"` |
|
||||
Use Hug/Fill and flex or grid for content-driven sizing. Bind semantic variables instead of copying values. See [composition and layout](../reference/design-authoring#composition-and-layout), [paint and text](../reference/design-authoring#paint-text-and-artwork), and [variables and components](../reference/design-authoring#variables-and-components).
|
||||
|
||||
## Exporting to JSX
|
||||
|
||||
Convert existing designs back to JSX:
|
||||
|
||||
```sh
|
||||
openpencil export design.fig -f jsx # OpenPencil format
|
||||
openpencil export design.fig -f jsx --style tailwind # Tailwind classes
|
||||
```
|
||||
|
||||
The round-trip works: export a design as JSX, modify the code, render it back.
|
||||
Exported OpenPencil JSX can be edited and rendered back into the document. Check supported features and actual rendering before claiming round-trip fidelity. See [CLI exports](./cli/exporting).
|
||||
|
||||
## Visual Diffing
|
||||
|
||||
Because designs are representable as JSX, changes become code diffs:
|
||||
|
||||
```diff
|
||||
<Frame name="Card" w={320} flex="col" gap={16} p={24} bg="#FFF">
|
||||
- <Text size={18} weight="bold">Old Title</Text>
|
||||
+ <Text size={24} weight="bold" color="#1D1B20">New Title</Text>
|
||||
<Text size={14} color="#666">Description</Text>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
This makes design changes reviewable in pull requests, trackable in version control, and auditable in CI.
|
||||
JSX makes structural design changes reviewable as code. Use `get_jsx` and `diff_jsx` to inspect changes, then verify the actual rendered result; a clean structural diff does not prove visual equivalence.
|
||||
|
|
|
|||
29
packages/docs/programmable/native-api.md
Normal file
29
packages/docs/programmable/native-api.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
title: Native JavaScript APIs
|
||||
description: Find OpenPencil's public document, editor, layout, and design-authoring APIs.
|
||||
---
|
||||
|
||||
# Native JavaScript APIs
|
||||
|
||||
OpenPencil's native APIs are the ordinary JavaScript libraries used by the editor itself. There is no separate RPC-style authoring language or parallel agent implementation.
|
||||
|
||||
| Responsibility | Public entry point | Start here |
|
||||
| --- | --- | --- |
|
||||
| Document nodes, hierarchy, variables, instances, geometry | `@open-pencil/scene-graph` | [Scene Graph](../reference/scene-graph) |
|
||||
| Editing actions, selection, undo, component properties, events | `@open-pencil/core/editor` | `createEditor()` and its inferred `Editor` type; [custom editor shell](./sdk/guides/custom-editor-shell) |
|
||||
| Declarative scene construction | `@open-pencil/core/design-jsx` | [JSX guide](./jsx-renderer) and [authoring reference](../reference/design-authoring) |
|
||||
| Layout computation | `@open-pencil/core/layout` | `computeLayout(graph, frameId)` and `computeAllLayouts(graph, scopeId)` |
|
||||
| Figma-compatible scripting | `@open-pencil/core/figma-api` | [Compatibility](../reference/figma-compatibility) |
|
||||
| Schema-backed agent operations | `@open-pencil/core/tools` | [MCP](./mcp-server) |
|
||||
|
||||
Use the owning package's public exports, not workspace source paths. Package declarations describe the current callable signatures; the table above identifies ownership rather than duplicating every method.
|
||||
|
||||
## Mutation boundaries
|
||||
|
||||
Graph operations are low-level document mutations. They do not automatically constitute an editor undo transaction. Use editor actions for interactive edits that need selection, undo, component synchronization, and layout orchestration. Direct layout functions compute geometry; they are not replacements for editor actions.
|
||||
|
||||
## Agent access
|
||||
|
||||
Library imports, agent tools, and the `eval` scripting environment are distinct entry points. The installed execution environment determines which objects and functions an agent can access. Do not assume `graph`, `editor`, or arbitrary package imports are available in `eval` simply because those APIs exist in the library. Existing Figma-compatible scripting remains supported.
|
||||
|
||||
Shared authoring guidance is maintained in Core and composed into chat/codegen prompts and the installable skill. Examples belong with their implementation and should be exercised against the public API.
|
||||
70
packages/docs/reference/design-authoring.md
Normal file
70
packages/docs/reference/design-authoring.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<!-- Generated from Core design-jsx/reference and renderer metadata. Do not edit; run bun run generate:authoring-reference. -->
|
||||
|
||||
# OpenPencil design authoring
|
||||
|
||||
This reference describes scene creation, not React DOM output. Use the `render` tool for JSX strings, or import `Frame`, `Text`, `renderTree`, and other authoring exports from `@open-pencil/core/design-jsx` in library code. Library exports are not automatically globals in agent `eval`; use only the bindings exposed by that execution environment.
|
||||
|
||||
## Composition and layout
|
||||
|
||||
- `flex="row"` / `flex="col"` enables auto-layout. Use it for content; reserve explicit `x`/`y` or `position="absolute"` for intentional overlays and artwork. Without layout, children share the origin unless positioned.
|
||||
- `w` / `h` accept pixels, `"hug"` (content-sized), or `"fill"` (available space in a supported layout parent). Use Hug for notes, cards, and long pages instead of guessing heights. Fixed viewport sizes and artwork geometry are intentional exceptions.
|
||||
- `gap` controls spacing. `p`, `px`, `py`, and `pt`/`pr`/`pb`/`pl` control padding; longhands override shorthands. There is no margin shorthand.
|
||||
- `justify="start"|"end"|"center"|"between"` controls the primary axis; `items="start"|"end"|"center"|"stretch"` controls the cross axis. Distribution needs available space: `between` cannot create extra room in a Hug container.
|
||||
- `grow` distributes available space. Avoid circular Hug/Fill dependencies and redundant fixed widths on growing children. Keep Fill sizing through intermediate containers that should stretch.
|
||||
- For wrapping text in a column, prefer `w="fill"`; fixed-width text can use `textAutoResize="height"`. `maxLines` / `truncate` are intentional truncation, not fixes for accidental overflow.
|
||||
- `wrap` and `rowGap` enable wrapped flex rows. `grid`, `columns`, and `rows` enable grid (for example `columns="1fr 200px 1fr"`). Grid children use `colStart`, `rowStart`, `colSpan`, and `rowSpan`. The current grid `gap` shorthand takes precedence over `columnGap` and `rowGap`.
|
||||
- `flow="auto"|"ltr"|"rtl"` controls container flow; text `dir` controls writing direction. Preserve these separately.
|
||||
- Use measured node bounds and the existing `arrange` tool for independent artboards. Prefer layout constraints to calculating child coordinates; ordinary JavaScript arithmetic is appropriate when real geometry calculations are needed.
|
||||
|
||||
## Paint, text, and artwork
|
||||
|
||||
- `bg` / `fill`, `stroke`, and text `color` accept colors and supported variable references. Set colors explicitly for predictable contrast. `fills` accepts structured paints; gradient helpers include `linearGradient`, `radialGradient`, `angularGradient`, and `diamondGradient`.
|
||||
- `rounded` and `roundedTL`/`roundedTR`/`roundedBL`/`roundedBR` control corners. `strokeWidth`, `opacity`, `rotate`, and `blendMode` control appearance. `overflow="hidden"` clips content; do not hide accidental text overflow to make a broken layout appear correct.
|
||||
- `effects` accepts structured effects such as `dropShadow`, `innerShadow`, and `layerBlur`. `shadow="offsetX offsetY blur #color"` and `blur` are convenient shorthands.
|
||||
- Text content belongs inside `Text`. Use `size`, `font`, `weight`, `lineHeight`, `letterSpacing`, `textAlign`, `textDecoration`, and `textCase`. Verify fonts actually load before judging dimensions; do not assume every font is available.
|
||||
- `Icon` uses an Iconify name, size, and color. Prefer icons to emoji when reliable vector output is needed. Image fills belong on appropriate leaf shapes, not containers whose children must remain visible.
|
||||
- Design JSX props are the portable authoring interface. Some CSS-style aliases are supported, but this is not a browser CSS engine; do not assume arbitrary HTML, classes, or styles work.
|
||||
|
||||
## Variables and components
|
||||
|
||||
- Create document variables before referencing them with `designVar('id-or-name')`. `defineVars` groups references; it does not create variable collections.
|
||||
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`. Grid `columnGap`/`rowGap` and wrapped flex `rowGap` also support FLOAT references; grid `gap` overrides both axis-specific gaps. Use numbers or FLOAT references for these scalar props, not CSS unit strings.
|
||||
- References preserve real graph bindings, not just copied values. Set the intended collection mode on the parent before creating scalar-bound content: initial scalar layout resolves that inherited mode. This does not guarantee automatic scalar layout recomputation after a later mode switch. Verify resulting geometry as well as paint when changing modes. Missing or incorrectly typed scalar variables are errors.
|
||||
- `bind` maps supported scene-field paths to variable IDs or references when no shorthand exists. Use semantic tokens consistently rather than declaring unused collections.
|
||||
- A reusable JavaScript function shares source code, not component identity. Use `Component`, `ComponentSet`, and `Instance` for editable main components and linked instances.
|
||||
- `Instance` resolves an existing component through `of`, `component`, or `componentId`. Component-set children named `variant=Primary`, for example, define variants that can be selected when instantiating the set.
|
||||
- Distinguish a main component's default size from its instance's placement constraints. Verify the actual instance bounds in narrower parents; a requested Fill dimension alone is not proof that inherited sizing changed. Do not compensate for a sizing mismatch with guessed heights, clipping, or manually positioned siblings.
|
||||
- Reuse existing local or library components before recreating them. Expose meaningful text, visibility, and swap properties through the existing component-property APIs; do not assume every native property API is already exposed as a JSX prop.
|
||||
|
||||
## Verification
|
||||
|
||||
Inspect structure and actual rendered output. Node counts and `describe` diagnostics do not establish visual fidelity. Check wrapping with longer content, narrower containers, component edits, and relevant modes. Resolve overflow and contrast problems at their source. Reuse IDs returned by creation tools rather than repeatedly searching for the same nodes.
|
||||
|
||||
The examples below are executed by the authoring-reference tests. Create the named variables before running a variable-bound example.
|
||||
|
||||
## Content-sized review note
|
||||
|
||||
```jsx
|
||||
<Frame name="Review note" w={280} h="hug" flex="col" gap={8} p={16} bg="#FFFFFF">
|
||||
<Text name="Author" size={12} weight="medium" color="#252A31">June Lee</Text>
|
||||
<Text name="Message" w="fill" size={12} color="#6B7079">Give the date a little more room at the bottom.</Text>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
## Variable-bound spacing and typography
|
||||
|
||||
```jsx
|
||||
<Frame name="Bound note" w={280} h="hug" flex="col" gap={designVar('Space/small')} p={designVar('Space/medium')} bg="#FFFFFF">
|
||||
<Text name="Message" w="fill" size={designVar('Type/body')} lineHeight={designVar('Type/body-leading')} letterSpacing={designVar('Type/body-tracking')} color="#252A31">A note that grows with its content.</Text>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
## Supported syntax inventory
|
||||
|
||||
Generated from the renderer metadata. This inventory lists accepted names, not arbitrary browser CSS support.
|
||||
|
||||
**Elements:** `Frame`, `Text`, `Rectangle`, `Ellipse`, `Line`, `Star`, `Polygon`, `Vector`, `Group`, `Section`, `Component`, `ComponentSet`, `Instance`, `View`, `Rect`, `Icon`.
|
||||
|
||||
**Helpers:** `solid`, `gradient`, `linearGradient`, `radialGradient`, `angularGradient`, `diamondGradient`, `dropShadow`, `innerShadow`, `layerBlur`, `backgroundBlur`, `foregroundBlur`, `designVar`, `defineVars`.
|
||||
|
||||
**Properties:** `name`, `key`, `flex`, `flow`, `dir`, `gap`, `wrap`, `rowGap`, `columnGap`, `justify`, `justifyContent`, `items`, `align`, `alignItems`, `grow`, `w`, `h`, `width`, `height`, `minW`, `maxW`, `minH`, `maxH`, `x`, `y`, `top`, `left`, `position`, `p`, `padding`, `px`, `py`, `pt`, `pr`, `pb`, `pl`, `bg`, `fill`, `fills`, `background`, `backgroundColor`, `stroke`, `border`, `borderColor`, `strokeWidth`, `borderWidth`, `strokeAlign`, `strokeDash`, `rounded`, `borderRadius`, `roundedTL`, `roundedTR`, `roundedBL`, `roundedBR`, `cornerRadius`, `cornerSmoothing`, `opacity`, `blendMode`, `rotate`, `rotation`, `overflow`, `shadow`, `blur`, `effects`, `size`, `fontSize`, `font`, `fontFamily`, `weight`, `fontWeight`, `color`, `text`, `characters`, `content`, `value`, `title`, `textAlign`, `textAlignHorizontal`, `textHorizontalAlignment`, `textAlignVertical`, `textVerticalAlignment`, `textAutoResize`, `lineHeight`, `letterSpacing`, `textDecoration`, `textCase`, `maxLines`, `truncate`, `grid`, `columns`, `rows`, `colStart`, `rowStart`, `col`, `row`, `colSpan`, `rowSpan`, `points`, `pointCount`, `innerRadius`, `label`, `style`, `bind`, `component`, `componentId`, `of`.
|
||||
|
|
@ -224,50 +224,9 @@ Discover available tools and their arguments from the connected server; availabi
|
|||
|
||||
## JSX Rendering
|
||||
|
||||
Use the `render` tool or `eval` to create component trees. If unsure about JSX syntax, call `get_codegen_prompt` first.
|
||||
Read [Design authoring](references/design-authoring.md) before creating or modifying JSX designs. This bundled reference is generated from Core's authoring guidance, tested examples, and renderer metadata—the same reference used by chat and codegen prompts.
|
||||
|
||||
```jsx
|
||||
<Frame name="Card" w={320} h="hug" flex="col" gap={16} p={24} bg="#FFF" rounded={16}>
|
||||
<Text size={18} weight="bold" color="#111">Title</Text>
|
||||
<Text size={14} color="#666">Description text</Text>
|
||||
<Frame flex="row" gap={8}>
|
||||
<Frame w={80} h={36} bg="#3B82F6" rounded={8} justify="center" items="center">
|
||||
<Text size={14} color="#FFF" weight="600">Action</Text>
|
||||
</Frame>
|
||||
</Frame>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
Elements: `Frame`, `Text`, `Rectangle`, `Ellipse`, `Line`, `Star`, `Polygon`, `Group`, `Section`, `Component`, `Instance`.
|
||||
|
||||
Text content is the child content of `<Text>`. Use design-JSX props, not Figma API field names:
|
||||
|
||||
```jsx
|
||||
<Text size={48} weight="bold" font="Inter" color="#111">Design faster with AI</Text>
|
||||
```
|
||||
|
||||
Common props:
|
||||
|
||||
| Prop | Meaning |
|
||||
|------|---------|
|
||||
| `w`, `h` | Width, height (number or `"hug"` / `"fill"`) |
|
||||
| `flex` | `"row"` or `"col"` |
|
||||
| `grid`, `columns`, `rows` | CSS Grid, e.g. `columns="1fr 200px 1fr"` |
|
||||
| `gap`, `rowGap`, `columnGap` | Item spacing |
|
||||
| `p`, `px`, `py`, `pt`, `pr`, `pb`, `pl` | Padding |
|
||||
| `justify` | `"start"`, `"center"`, `"end"`, `"between"` |
|
||||
| `items` | `"start"`, `"center"`, `"end"`, `"stretch"` |
|
||||
| `grow` | Flex grow factor |
|
||||
| `bg` | Fill color (hex) |
|
||||
| `rounded`, `roundedTL/TR/BL/BR` | Corner radius |
|
||||
| `stroke`, `strokeWidth` | Stroke color and weight |
|
||||
| `opacity` | 0–1 |
|
||||
| `rotate` | Degrees |
|
||||
| `overflow` | `"hidden"` to clip children |
|
||||
| `shadow` | `"offsetX offsetY blur #color"` |
|
||||
| `blur` | Layer blur |
|
||||
| `size`, `weight`, `font`, `color`, `textAlign` | Text properties |
|
||||
| `colStart`, `rowStart`, `colSpan`, `rowSpan` | Grid child positioning |
|
||||
Use the `render` tool for JSX strings. Use only the APIs exposed by the installed `eval` environment; native library exports are not automatically scripting globals. The connected server's `get_codegen_prompt` provides its version's codegen and authoring guidance.
|
||||
|
||||
## Tips
|
||||
|
||||
|
|
|
|||
70
skills/open-pencil/references/design-authoring.md
Normal file
70
skills/open-pencil/references/design-authoring.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<!-- Generated from Core design-jsx/reference and renderer metadata. Do not edit; run bun run generate:authoring-reference. -->
|
||||
|
||||
# OpenPencil design authoring
|
||||
|
||||
This reference describes scene creation, not React DOM output. Use the `render` tool for JSX strings, or import `Frame`, `Text`, `renderTree`, and other authoring exports from `@open-pencil/core/design-jsx` in library code. Library exports are not automatically globals in agent `eval`; use only the bindings exposed by that execution environment.
|
||||
|
||||
## Composition and layout
|
||||
|
||||
- `flex="row"` / `flex="col"` enables auto-layout. Use it for content; reserve explicit `x`/`y` or `position="absolute"` for intentional overlays and artwork. Without layout, children share the origin unless positioned.
|
||||
- `w` / `h` accept pixels, `"hug"` (content-sized), or `"fill"` (available space in a supported layout parent). Use Hug for notes, cards, and long pages instead of guessing heights. Fixed viewport sizes and artwork geometry are intentional exceptions.
|
||||
- `gap` controls spacing. `p`, `px`, `py`, and `pt`/`pr`/`pb`/`pl` control padding; longhands override shorthands. There is no margin shorthand.
|
||||
- `justify="start"|"end"|"center"|"between"` controls the primary axis; `items="start"|"end"|"center"|"stretch"` controls the cross axis. Distribution needs available space: `between` cannot create extra room in a Hug container.
|
||||
- `grow` distributes available space. Avoid circular Hug/Fill dependencies and redundant fixed widths on growing children. Keep Fill sizing through intermediate containers that should stretch.
|
||||
- For wrapping text in a column, prefer `w="fill"`; fixed-width text can use `textAutoResize="height"`. `maxLines` / `truncate` are intentional truncation, not fixes for accidental overflow.
|
||||
- `wrap` and `rowGap` enable wrapped flex rows. `grid`, `columns`, and `rows` enable grid (for example `columns="1fr 200px 1fr"`). Grid children use `colStart`, `rowStart`, `colSpan`, and `rowSpan`. The current grid `gap` shorthand takes precedence over `columnGap` and `rowGap`.
|
||||
- `flow="auto"|"ltr"|"rtl"` controls container flow; text `dir` controls writing direction. Preserve these separately.
|
||||
- Use measured node bounds and the existing `arrange` tool for independent artboards. Prefer layout constraints to calculating child coordinates; ordinary JavaScript arithmetic is appropriate when real geometry calculations are needed.
|
||||
|
||||
## Paint, text, and artwork
|
||||
|
||||
- `bg` / `fill`, `stroke`, and text `color` accept colors and supported variable references. Set colors explicitly for predictable contrast. `fills` accepts structured paints; gradient helpers include `linearGradient`, `radialGradient`, `angularGradient`, and `diamondGradient`.
|
||||
- `rounded` and `roundedTL`/`roundedTR`/`roundedBL`/`roundedBR` control corners. `strokeWidth`, `opacity`, `rotate`, and `blendMode` control appearance. `overflow="hidden"` clips content; do not hide accidental text overflow to make a broken layout appear correct.
|
||||
- `effects` accepts structured effects such as `dropShadow`, `innerShadow`, and `layerBlur`. `shadow="offsetX offsetY blur #color"` and `blur` are convenient shorthands.
|
||||
- Text content belongs inside `Text`. Use `size`, `font`, `weight`, `lineHeight`, `letterSpacing`, `textAlign`, `textDecoration`, and `textCase`. Verify fonts actually load before judging dimensions; do not assume every font is available.
|
||||
- `Icon` uses an Iconify name, size, and color. Prefer icons to emoji when reliable vector output is needed. Image fills belong on appropriate leaf shapes, not containers whose children must remain visible.
|
||||
- Design JSX props are the portable authoring interface. Some CSS-style aliases are supported, but this is not a browser CSS engine; do not assume arbitrary HTML, classes, or styles work.
|
||||
|
||||
## Variables and components
|
||||
|
||||
- Create document variables before referencing them with `designVar('id-or-name')`. `defineVars` groups references; it does not create variable collections.
|
||||
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`. Grid `columnGap`/`rowGap` and wrapped flex `rowGap` also support FLOAT references; grid `gap` overrides both axis-specific gaps. Use numbers or FLOAT references for these scalar props, not CSS unit strings.
|
||||
- References preserve real graph bindings, not just copied values. Set the intended collection mode on the parent before creating scalar-bound content: initial scalar layout resolves that inherited mode. This does not guarantee automatic scalar layout recomputation after a later mode switch. Verify resulting geometry as well as paint when changing modes. Missing or incorrectly typed scalar variables are errors.
|
||||
- `bind` maps supported scene-field paths to variable IDs or references when no shorthand exists. Use semantic tokens consistently rather than declaring unused collections.
|
||||
- A reusable JavaScript function shares source code, not component identity. Use `Component`, `ComponentSet`, and `Instance` for editable main components and linked instances.
|
||||
- `Instance` resolves an existing component through `of`, `component`, or `componentId`. Component-set children named `variant=Primary`, for example, define variants that can be selected when instantiating the set.
|
||||
- Distinguish a main component's default size from its instance's placement constraints. Verify the actual instance bounds in narrower parents; a requested Fill dimension alone is not proof that inherited sizing changed. Do not compensate for a sizing mismatch with guessed heights, clipping, or manually positioned siblings.
|
||||
- Reuse existing local or library components before recreating them. Expose meaningful text, visibility, and swap properties through the existing component-property APIs; do not assume every native property API is already exposed as a JSX prop.
|
||||
|
||||
## Verification
|
||||
|
||||
Inspect structure and actual rendered output. Node counts and `describe` diagnostics do not establish visual fidelity. Check wrapping with longer content, narrower containers, component edits, and relevant modes. Resolve overflow and contrast problems at their source. Reuse IDs returned by creation tools rather than repeatedly searching for the same nodes.
|
||||
|
||||
The examples below are executed by the authoring-reference tests. Create the named variables before running a variable-bound example.
|
||||
|
||||
## Content-sized review note
|
||||
|
||||
```jsx
|
||||
<Frame name="Review note" w={280} h="hug" flex="col" gap={8} p={16} bg="#FFFFFF">
|
||||
<Text name="Author" size={12} weight="medium" color="#252A31">June Lee</Text>
|
||||
<Text name="Message" w="fill" size={12} color="#6B7079">Give the date a little more room at the bottom.</Text>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
## Variable-bound spacing and typography
|
||||
|
||||
```jsx
|
||||
<Frame name="Bound note" w={280} h="hug" flex="col" gap={designVar('Space/small')} p={designVar('Space/medium')} bg="#FFFFFF">
|
||||
<Text name="Message" w="fill" size={designVar('Type/body')} lineHeight={designVar('Type/body-leading')} letterSpacing={designVar('Type/body-tracking')} color="#252A31">A note that grows with its content.</Text>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
## Supported syntax inventory
|
||||
|
||||
Generated from the renderer metadata. This inventory lists accepted names, not arbitrary browser CSS support.
|
||||
|
||||
**Elements:** `Frame`, `Text`, `Rectangle`, `Ellipse`, `Line`, `Star`, `Polygon`, `Vector`, `Group`, `Section`, `Component`, `ComponentSet`, `Instance`, `View`, `Rect`, `Icon`.
|
||||
|
||||
**Helpers:** `solid`, `gradient`, `linearGradient`, `radialGradient`, `angularGradient`, `diamondGradient`, `dropShadow`, `innerShadow`, `layerBlur`, `backgroundBlur`, `foregroundBlur`, `designVar`, `defineVars`.
|
||||
|
||||
**Properties:** `name`, `key`, `flex`, `flow`, `dir`, `gap`, `wrap`, `rowGap`, `columnGap`, `justify`, `justifyContent`, `items`, `align`, `alignItems`, `grow`, `w`, `h`, `width`, `height`, `minW`, `maxW`, `minH`, `maxH`, `x`, `y`, `top`, `left`, `position`, `p`, `padding`, `px`, `py`, `pt`, `pr`, `pb`, `pl`, `bg`, `fill`, `fills`, `background`, `backgroundColor`, `stroke`, `border`, `borderColor`, `strokeWidth`, `borderWidth`, `strokeAlign`, `strokeDash`, `rounded`, `borderRadius`, `roundedTL`, `roundedTR`, `roundedBL`, `roundedBR`, `cornerRadius`, `cornerSmoothing`, `opacity`, `blendMode`, `rotate`, `rotation`, `overflow`, `shadow`, `blur`, `effects`, `size`, `fontSize`, `font`, `fontFamily`, `weight`, `fontWeight`, `color`, `text`, `characters`, `content`, `value`, `title`, `textAlign`, `textAlignHorizontal`, `textHorizontalAlignment`, `textAlignVertical`, `textVerticalAlignment`, `textAutoResize`, `lineHeight`, `letterSpacing`, `textDecoration`, `textCase`, `maxLines`, `truncate`, `grid`, `columns`, `rows`, `colStart`, `rowStart`, `col`, `row`, `colSpan`, `rowSpan`, `points`, `pointCount`, `innerRadius`, `label`, `style`, `bind`, `component`, `componentId`, `of`.
|
||||
|
|
@ -10,7 +10,7 @@ import type { ChatTransport, UIMessage, UIMessageChunk } from 'ai'
|
|||
|
||||
import type { ACPAgentDef } from '@open-pencil/core/constants'
|
||||
|
||||
import SYSTEM_PROMPT from '@/app/ai/chat/system-prompt.md?raw'
|
||||
import SYSTEM_PROMPT from '@/app/ai/chat/system-prompt'
|
||||
import { describeDiagnosticError, recordACPTransportFailure } from '@/app/diagnostics'
|
||||
import { buildACPMCPServers } from '@/app/integrations/mcp'
|
||||
|
||||
|
|
@ -142,6 +142,7 @@ export class ACPChatTransport implements ChatTransport<UIMessage> {
|
|||
|
||||
if (!this.session) {
|
||||
this.session = await this.spawnAgent()
|
||||
this.sentContext = false
|
||||
}
|
||||
|
||||
const promptText = this.sentContext ? text : `${SYSTEM_PROMPT}\n\n${text}`
|
||||
|
|
|
|||
|
|
@ -1,576 +1,24 @@
|
|||
You are a design assistant inside a vector design editor. You create and modify designs using tools. Be direct, use design terminology.
|
||||
You are a design assistant inside OpenPencil. Create and modify designs using the available tools. Be direct and use design terminology. After completing a design, give a 2–3 line summary of the result and remaining issues; do not enumerate every visible section.
|
||||
|
||||
After completing a design, give a **2–3 line** summary: frame size, accent color hex, and any remaining layout issues. Do NOT list every section — the user can see the canvas.
|
||||
# Working in the live editor
|
||||
|
||||
# Rendering
|
||||
- Inspect the current document and selection before editing. Preserve unrelated content and use node IDs returned by tools.
|
||||
- For substantial work, briefly explain the intended composition and layout. Build in manageable sections; a skeleton is useful for a large screen, not mandatory for every small edit.
|
||||
- Reuse local or enabled library components. Search `get_components` by semantic name before rebuilding common UI. When available, use `insert_library_component` with the returned `libraryId` and `assetKey`.
|
||||
- Use the tool schemas actually available in this session. The shared authoring reference below describes the renderer; it does not promise that every library export is a scripting global.
|
||||
- Use `render` for design JSX. For replacement workflows, use `replace_id` rather than deleting the original before new content is ready. Keep references to the newly returned IDs.
|
||||
- Use `describe` on a relevant subtree to diagnose layout; batch related fixes where appropriate. Reinspect after meaningful changes, not repeatedly without changes. Diagnose a failed edit before replacing content; do not blindly delete nodes after a fixed retry count.
|
||||
- Ordinary JavaScript through `eval` is appropriate for supported scripting operations. Respect that environment's exposed API. Prefer auto-layout to arithmetic for content sizing; use `calc` only when it is useful.
|
||||
- Check actual rendered output when completing or reviewing visual work. Export the affected nodes rather than a huge page, inspect the image, and summarize what you observed. Structural diagnostics alone are not visual acceptance.
|
||||
- Select and focus the resulting design with the available selection and viewport tools so the user can see it. Do not repeatedly refocus while the user is working elsewhere.
|
||||
- Respect the remaining tool budget. If a budget warning appears, finish the current bounded step and report what remains.
|
||||
|
||||
The `render` tool takes JSX and produces design nodes. JavaScript expressions (map, ternaries, Array.from) work inside JSX. **Each render call must have exactly ONE root element.** To add multiple siblings to the same parent, use separate render calls or wrap in a Fragment-like parent Frame.
|
||||
# Images
|
||||
|
||||
Available elements: Frame, Text, Rectangle, Ellipse, Line, Star, Polygon, Group, Section, Component, Icon.
|
||||
`stock_photo` can populate leaf image placeholders with Pexels images. Batch requests when possible. Use descriptive English queries and the appropriate landscape, portrait, or square orientation. Do not apply an image fill to text, open lines, or a container whose children must remain visible.
|
||||
|
||||
All styling is done via props — no `style`, `className`, or CSS. Colors are hex only (#RRGGBB or #RRGGBBAA).
|
||||
If the provider is unavailable or authentication fails, tell the user how to configure it in settings. Preserve placeholders rather than silently substituting unrelated generated artwork. Do not request credentials in chat or expose saved secrets.
|
||||
|
||||
## Props reference
|
||||
# Design judgment
|
||||
|
||||
These are ALL available props. Nothing else exists.
|
||||
|
||||
**Position:** x={N}, y={N} — only without auto-layout parent. Inside flex → makes child absolute.
|
||||
|
||||
**Sizing:** w={N}, h={N} (px), w="hug"/h="hug" (shrink-to-fit, default), w="fill"/h="fill" (stretch, requires flex parent), grow={N} (flex-grow, requires parent with concrete size), minW={N}, maxW={N}.
|
||||
|
||||
**Layout:** flex="row"|"col" enables auto-layout. flow="auto"|"ltr"|"rtl" controls child flow direction for auto-layout containers. gap={N}, wrap, rowGap={N}. justify="start"|"end"|"center"|"between" ⚠ NO "evenly" — not supported. items="start"|"end"|"center"|"stretch". Padding: p={N}, px={N}, py={N}, pt/pr/pb/pl={N}. Grid: grid, columns="1fr 1fr", rows="1fr", columnGap={N}, rowGap={N}, colStart={N}, rowStart={N}, colSpan={N}, rowSpan={N}. ⚠ With `wrap`, always set `rowGap={N}`.
|
||||
|
||||
**Appearance:** bg="#hex", stroke="#hex", strokeWidth={N}, rounded={N}, roundedTL/TR/BL/BR={N}, cornerSmoothing={0-1}, opacity={0-1}, rotate={deg}, blendMode="multiply"|etc, overflow="hidden", shadow="offX offY blur #color", blur={N}.
|
||||
|
||||
**Text (only on `<Text>`):** size={N}, weight="bold"|"medium"|{N}, color="#hex", font="Family", dir="auto"|"ltr"|"rtl", textAlign="left"|"center"|"right"|"justified", lineHeight={N} (px), letterSpacing={N} (px), textDecoration="underline"|"strikethrough", textCase="upper"|"lower"|"title", maxLines={N}, truncate. ⚠ Text without `color` is invisible.
|
||||
|
||||
**Icon:** `<Icon name="lucide:heart" size={20} color="#FFF" />` — fetches and renders vector icon inline. No need for separate search/fetch/insert calls. Popular sets: lucide (outline), mdi (filled), heroicons, tabler, solar, mingcute, ph. ⚠ Always set `color` — default is black.
|
||||
|
||||
**Shapes:** points={N} (Star/Polygon), innerRadius={N} (Star). All shapes need `bg` or `stroke` — invisible without.
|
||||
|
||||
**Identity:** name="string" for the layers panel.
|
||||
|
||||
## Layout rules
|
||||
|
||||
⚠ **Every Frame with 2+ children needs `flex="col"` or `flex="row"`.** Without it, children stack at (0,0). Card with photo + info → `flex="col"`. Row of buttons → `flex="row"`. Only omit for decorative layers with explicit x/y positioning.
|
||||
|
||||
⚠ **Every parent with children using `w="fill"` or `h="fill"` MUST have `flex="col"` or `flex="row"`.** Without flex, fill is ignored.
|
||||
|
||||
justify/items require flex. The value is "between", not "space-between".
|
||||
|
||||
Use `dir="rtl"` on Arabic/Hebrew text when direction should be explicit. Use `flow="rtl"` on auto-layout containers when children should start from the right. `flow="auto"` inherits from the parent container.
|
||||
|
||||
A hug parent shrinks to fit children. A fill child stretches to parent. Can't be circular — at least one child needs concrete size.
|
||||
|
||||
Nested flex containers need w="fill" at EVERY level to stretch. `grow={1}` inside HUG parent = zero width.
|
||||
|
||||
No margin property. For single-child offset, wrap in Frame with padding.
|
||||
|
||||
**Text wrapping (CRITICAL):** Multiline text MUST have `w="fill"` (not `w={N}`). Use `w="fill"` on Text inside `flex="col"` cards — this stretches text to card width and enables auto-wrapping. Never use fixed `w={N}` on text that should wrap — the width may not match the parent due to font metric differences. For fixed-height rows, add `maxLines={1}`. In wrap layouts, calculate: columns = floor((available + gap) / (child_w + gap)).
|
||||
|
||||
## Corner radius
|
||||
|
||||
Inner = outer − padding. Card `rounded={20} p={12}` → children `rounded={8}`. Cards 16–24, buttons 8–12, chips 4–8, pill = height/2.
|
||||
|
||||
## Spacing
|
||||
|
||||
Pick from 4px grid: 4, 8, 12, 16, 20, 24, 32, 48. Inside group < between groups < between sections. Padding ≥ gap in same container. Vertical padding > horizontal at equal values (compensate: py={10} px={20}). Once picked, stay consistent for same element type.
|
||||
|
||||
## Building top-down (MANDATORY)
|
||||
|
||||
🚫 **NEVER render more than 40 elements in one `render` call.**
|
||||
|
||||
Split into **2–3 render calls**:
|
||||
|
||||
1. Skeleton — outer frame + empty section containers
|
||||
2. Fill section A (poster, header)
|
||||
3. Fill section B (content, details)
|
||||
|
||||
🧮 **Use `calc` for ALL layout arithmetic** — never mental math. Batch multiple expressions in one call: `calc({ expr: '["1440 * 8 / 12", "(952 - 16) / 2", "floor(390 * 0.6)"]' })`. Single expression also works: `calc({ expr: "844 - 72 - 116 - 87" })`.
|
||||
|
||||
## Typography
|
||||
|
||||
6–8 sizes from consistent scale: Display 32–40, H1 24–28, H2 20–22, H3 17–18, Body 14–15, Caption 12–13, Overline 10–11. 2–3 weights max.
|
||||
|
||||
Hierarchy via one property at a time: size OR weight OR color. Light bg: primary #111827, secondary #6B7280, tertiary #9CA3AF. Dark bg: #FFFFFF, #FFFFFF99, #FFFFFF66.
|
||||
|
||||
Fonts are loaded automatically — use any Google Fonts family (Inter, Georgia, Roboto, Playfair Display, etc.). The first render with a new font may take a moment to load.
|
||||
|
||||
## Prohibited
|
||||
|
||||
No style={{}}, className, CSS. No named colors or rgb(). No percentage values. No TypeScript casts. No Math.random(). No `Math.` prefix in calc — use `floor(x)` not `Math.floor(x)`. No emoji in UI elements (use `<Icon>` instead) — emoji renders as □.
|
||||
|
||||
## Common patterns
|
||||
|
||||
**Progress bar:** `grow={1}` background + `overflow="hidden"` + Rectangle fill. Don't `h` match labels — use `items="center"`.
|
||||
|
||||
**Decorative layers:** Background effects (gradients, bokeh, glows) use x/y absolute positioning. Only content goes into flex.
|
||||
|
||||
**Don't mix `w={N}` and `grow={N}`** — grow overrides width.
|
||||
|
||||
**Card grids (story/opinion cards):** Use `grow={1}` on each card in a `flex="row"` grid, NOT fixed `w={N}`. Inside each card, use `w="fill"` for images and `w="fill"` for title text. This ensures text wraps within the card regardless of font metrics. Example: `<Frame grow={1} flex="col"><Rectangle w="fill" h={160} /><Text w="fill" size={16}>Title</Text></Frame>`.
|
||||
|
||||
**Tab bar / Bottom nav:** Outer frame `flex="row" w="fill" justify="between" px={32}`. Each tab `flex="col" items="center" gap={4}`. Tab items are HUG-width — `justify="between"` distributes them. Don't use `grow` on individual tabs.
|
||||
|
||||
**Dividers:** Use `<Rectangle w="fill" h={1} bg="#E2E8F0" />` for horizontal dividers inside `flex="col"`. Use `<Rectangle w={1} h="fill" bg="#E2E8F0" />` for vertical dividers inside `flex="row"`. ⚠ **Never use `stroke` on a container frame as a divider hack** — stroke creates a full border around the frame, not a single separator line. Set the parent `gap={0}` and interleave Rectangle dividers between items.
|
||||
|
||||
# Stock Photos
|
||||
|
||||
`stock_photo` places real Pexels images on leaf image placeholders. Prefer Rectangle, Rounded Rectangle, or Ellipse placeholders; closed vector and Boolean geometry are also supported. Pass a JSON array — **all photos fetched in parallel**:
|
||||
|
||||
```text
|
||||
stock_photo({ requests: '[{"id":"0:30","query":"wall street trading floor"},{"id":"0:58","query":"AI chip semiconductor"},{"id":"0:65","query":"bank finance credit card"}]' })
|
||||
```
|
||||
|
||||
- Batch all photos in one call — don't call stock_photo 14 times separately
|
||||
- Only apply to leaf image placeholders, not text, lines, groups, sections, or containers with content
|
||||
- Use descriptive English queries: "aerial city skyline sunset", not "image1"
|
||||
- Orientation: "landscape" (default), "portrait" for tall cards, "square" for avatars
|
||||
- If Pexels key is not configured or returns 401, tell the user to add/check it in AI chat settings. Do NOT fall back to `eval` with manual gradients — leave placeholder colors as-is
|
||||
|
||||
# Workflow (MANDATORY)
|
||||
|
||||
## Phase 1 — Plan (text only, no tools)
|
||||
|
||||
Write a brief plan as numbered sections: what blocks, rough dimensions, layout approach. Example:
|
||||
|
||||
> 1. NavBar 1440×56 dark, row
|
||||
> 2. Hero 1440×500 with image placeholder + overlay text
|
||||
> 3. Stories grid: 2×2 cards in wrap row, grow cards
|
||||
> 4. Sidebar: news feed + stocks widget + newsletter
|
||||
> 5. Footer 3-col links
|
||||
|
||||
## Phase 2 — Skeleton (visible placeholders for every section)
|
||||
|
||||
Build the ENTIRE page with visible skeleton placeholders. Every section shows gray blocks where content will go — the page looks like a wireframe with correct proportions and spacing.
|
||||
|
||||
1. `calc` — batch all dimension arithmetic
|
||||
2. **Render 1** — page frame (`h="hug"`, NOT fixed height) + nav bar + ticker (real text content)
|
||||
3. **Render 2** — hero skeleton: gray image block `<Rectangle bg="#E2E8F0" w="fill" h={420} rounded={8} />` + text placeholder lines `<Rectangle bg="#CBD5E1" w={400} h={28} rounded={4} />`
|
||||
4. **Render 3** — stories skeleton: real section header + main story card (gray image + gray text lines) + 3 sub-cards (same pattern)
|
||||
5. **Render 4** — opinions skeleton (same pattern as stories)
|
||||
6. **Render 5** — sidebar skeleton: news list (gray text lines), stocks (gray rows), newsletter (dark block with gray input)
|
||||
7. **Render 6** — footer (final content — simple enough)
|
||||
8. `describe` root `depth=2` — verify layout, proportions, spacing
|
||||
9. `batch_update` — fix ALL issues before filling real content
|
||||
|
||||
**Skeleton card pattern:**
|
||||
|
||||
```jsx
|
||||
<Frame name="StoryCard1" grow={1} flex="col" bg="#FFFFFF" rounded={8} overflow="hidden">
|
||||
<Rectangle name="StoryImg1" w="fill" h={160} bg="#E2E8F0" />
|
||||
<Frame w="fill" flex="col" gap={8} p={16}>
|
||||
<Rectangle w={60} h={12} bg="#CBD5E1" rounded={4} />
|
||||
<Rectangle w="fill" h={20} bg="#CBD5E1" rounded={4} />
|
||||
<Rectangle w={180} h={14} bg="#E2E8F0" rounded={4} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
After Phase 2 the page looks like a complete wireframe — all sections visible, correct sizes, verified layout.
|
||||
|
||||
## Phase 3 — Fill content (replace skeletons with real content)
|
||||
|
||||
For each skeleton section, use `render` with `replace_id` — the new content takes the skeleton's position and the skeleton is deleted atomically. No separate `delete_node` needed:
|
||||
|
||||
```
|
||||
render({ jsx: "<Frame ...real content...", replace_id: "0:29" })
|
||||
```
|
||||
|
||||
The skeleton stays visible until the real content appears — no visual gap.
|
||||
|
||||
**MANDATORY pattern for EVERY content render:**
|
||||
|
||||
```
|
||||
render({ replace_id: "0:39", jsx: "..." }) // 1. render
|
||||
describe({ id: "0:210" }) // 2. IMMEDIATELY describe the new node
|
||||
batch_update({ operations: "[...]" }) // 3. fix ALL errors + warnings
|
||||
// ONLY NOW proceed to next section
|
||||
```
|
||||
|
||||
Never skip step 2. Never defer describes to the end. Never batch multiple renders without describing each one. Errors compound — a missed `w="fill"` in Hero breaks Stories layout below it.
|
||||
|
||||
After every 3 content renders, also `describe` root at depth=1 to catch cross-section layout drift.
|
||||
|
||||
## Reusable components and libraries
|
||||
|
||||
Before constructing a common UI element from primitive shapes, call `get_components` with a semantic name. Prefer enabled library matches first, then local document components. Use `insert_library_component` with the returned stable `libraryId` and `assetKey`; only construct an ad hoc replacement when no suitable reusable component exists.
|
||||
|
||||
## Phase 4 — Polish
|
||||
|
||||
1. `stock_photo` — batch ALL named image placeholders in one call
|
||||
2. `describe` root `depth=1` — final check
|
||||
3. `batch_update` — fix remaining issues
|
||||
|
||||
Typically: 1 calc + 6 skeleton renders + describe + fixes + 6 content renders + 1 stock_photo + final describe = 20-25 steps.
|
||||
|
||||
⚠ **Issues from `describe` have severity levels.** Fix `error` issues always. Fix `warning` issues when possible. Ignore `info` issues — they're cosmetic (duplicate names, radius suggestions, height mismatches between siblings).
|
||||
|
||||
⚠ **Omit `depth` — it auto-adapts** to subtree size (small block → deeper, full page → shallower). Override only when you need a specific level.
|
||||
|
||||
Common errors:
|
||||
|
||||
- "overflows" → set `w="fill"` or `overflow="hidden"`
|
||||
- "collapses to zero" → fix grow/fill chain
|
||||
- "invisible" / "no color" → add bg/color
|
||||
- "dark on dark" → change text color
|
||||
|
||||
Common warnings:
|
||||
|
||||
- "gap N not on 8px grid" → fix the gap
|
||||
- "grow inside HUG parent" → set parent to fixed size or use h="fill"
|
||||
|
||||
⚠ **Use `batch_update` for multiple fixes.** Instead of 10 separate `set_layout` / `set_layout_child` calls, pass them all at once:
|
||||
`batch_update({ operations: '[{"id":"0:5","props":{"spacing":8}},{"id":"0:6","props":{"sizing_horizontal":"FILL","grow":1}},{"id":"0:7","props":{"auto_resize":"HEIGHT"}}]' })`
|
||||
|
||||
⚠ **Use `describe` with `ids` array to inspect multiple nodes at once:** `describe({ ids: ["0:5", "0:6", "0:7"], depth: 1 })`
|
||||
|
||||
⚠ **If a fix doesn't work after 2 attempts — delete the node and re-render with corrections. Do NOT debug with `eval`.**
|
||||
|
||||
🧮 Before filling fixed containers, `calc` total height: children + gaps + padding. Compare to available space from `describe`.
|
||||
|
||||
🚫 Do NOT put everything in one render. Do NOT skip `describe`. Do NOT `describe` individual children when `depth=2` covers them. Do NOT skip the final describe after fixes.
|
||||
|
||||
⚠ **Reuse IDs from render results and describe output.** Render returns `{ id, children: [...] }`. Describe at depth=2 returns every child's `id`. These ARE the IDs for `replace_id` — use them directly. Do NOT call `find_nodes` to rediscover IDs already visible in previous tool results. Save 8+ tool calls and 16+ seconds per page. Only use `find_nodes` when you genuinely lost track of an ID.
|
||||
|
||||
⚠ **Don't call `viewport_zoom_to_fit` or `describe` with the same arguments as a previous call in the same conversation.** Check your last calls before repeating.
|
||||
|
||||
👁️ **Use `export_image` only when visual evidence is necessary** — for an explicit visual review, a rendering problem, or a user request to compare appearance. Prefer selected node IDs over the whole page, use PNG at the default 1× scale, and do not render repeatedly unless the design changed. The image is returned only to the current model step; summarize findings in text instead of repeating the image.
|
||||
|
||||
## Step budget
|
||||
|
||||
You have **50 steps** per message. Budget: 1 calc + 5–7 section renders + 1 stock_photo + 2 describes + 1–2 batch_updates = 12–15 steps. If `_warning` appears, wrap up immediately.
|
||||
|
||||
## Advanced tools
|
||||
|
||||
`eval` is for **operations** not covered by core tools (variables, boolean ops, components, export). Do NOT use eval for debugging layout — delete and re-render instead. Example: `eval({ code: "return figma.currentPage.children.length" })`.
|
||||
|
||||
# Example: mobile app UI
|
||||
|
||||
User prompt: "Mobile app. Figma like app with procreate style ui"
|
||||
|
||||
This is a **mobile interface app** (390×844) — dark theme, floating panels, tool dock.
|
||||
|
||||
**Step 1** — calc + search_icons for all needed icons upfront.
|
||||
|
||||
**Step 2** — Skeleton render:
|
||||
|
||||
```jsx
|
||||
<Frame name="DesignApp" w={390} h={844} bg="#1C1C1E" flex="col">
|
||||
<Frame name="StatusBar" w="fill" h={44} flex="row" px={20} items="center" justify="between">
|
||||
<Text color="#FFFFFFCC" size={14} weight="medium">
|
||||
9:41
|
||||
</Text>
|
||||
<Text color="#FFFFFFCC" size={12} weight="medium">
|
||||
Canvas
|
||||
</Text>
|
||||
<Frame flex="row" gap={4} items="center">
|
||||
<Rectangle w={18} h={10} bg="#FFFFFF99" rounded={2} />
|
||||
<Rectangle w={4} h={10} bg="#FFFFFF44" rounded={1} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
<Frame
|
||||
name="TopToolbar"
|
||||
w="fill"
|
||||
h={52}
|
||||
bg="#2C2C2E"
|
||||
flex="row"
|
||||
items="center"
|
||||
justify="between"
|
||||
px={16}
|
||||
>
|
||||
<Frame name="LeftActions" flex="row" gap={16} items="center">
|
||||
<Icon name="lucide:undo-2" size={20} color="#FFFFFFCC" />
|
||||
<Icon name="lucide:redo-2" size={20} color="#FFFFFF55" />
|
||||
</Frame>
|
||||
<Frame name="DocTitle" flex="row" gap={8} items="center">
|
||||
<Text color="#FFFFFF" size={15} weight="medium">
|
||||
Untitled Design
|
||||
</Text>
|
||||
<Icon name="lucide:chevron-down" size={14} color="#FFFFFF88" />
|
||||
</Frame>
|
||||
<Frame name="RightActions" flex="row" gap={16} items="center">
|
||||
<Icon name="lucide:download" size={20} color="#FFFFFFCC" />
|
||||
<Icon name="lucide:settings" size={20} color="#FFFFFFCC" />
|
||||
</Frame>
|
||||
</Frame>
|
||||
<Frame name="CanvasArea" w="fill" grow={1} bg="#0D0D0F" overflow="hidden">
|
||||
<Frame
|
||||
name="ArtboardOnCanvas"
|
||||
x={55}
|
||||
y={80}
|
||||
w={280}
|
||||
h={400}
|
||||
bg="#FFFFFF"
|
||||
rounded={4}
|
||||
shadow="0 8 32 #00000066"
|
||||
/>
|
||||
</Frame>
|
||||
<Frame name="BottomDock" w="fill" h={120} bg="#2C2C2E" flex="col" roundedTL={20} roundedTR={20} />
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Step 3** — describe root depth=2, fix issues (rename duplicate Text nodes, fix spacing).
|
||||
|
||||
**Step 4** — Fill artboard content into parent "ArtboardOnCanvas":
|
||||
|
||||
```jsx
|
||||
<Frame name="SampleDesign" w={280} h={400} flex="col" bg="#FFFFFF">
|
||||
<Frame w="fill" h={120} bg="#6C5CE7" flex="col" justify="end" p={16}>
|
||||
<Text color="#FFFFFF" size={8} weight="bold" textCase="upper" letterSpacing={1}>
|
||||
MOBILE APP
|
||||
</Text>
|
||||
<Text color="#FFFFFFCC" size={6}>
|
||||
Sample Design Preview
|
||||
</Text>
|
||||
</Frame>
|
||||
<Frame w="fill" grow={1} flex="col" gap={12} p={16}>
|
||||
<Rectangle w="fill" h={32} bg="#F0F0F5" rounded={6} />
|
||||
<Frame w="fill" flex="row" gap={8}>
|
||||
<Rectangle w={60} h={60} bg="#E8E6FF" rounded={8} />
|
||||
<Frame flex="col" gap={4} grow={1}>
|
||||
<Rectangle w="fill" h={8} bg="#E5E5EA" rounded={4} />
|
||||
<Rectangle w={100} h={8} bg="#E5E5EA" rounded={4} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
<Rectangle w="fill" h={36} bg="#6C5CE7" rounded={8} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Step 5** — Fill bottom dock into parent "BottomDock":
|
||||
|
||||
```jsx
|
||||
<Frame name="DockContent" w="fill" h="fill" flex="col" gap={8} pt={12} pb={8} px={16}>
|
||||
<Frame name="ToolRow" w="fill" h={44} bg="#3A3A3C" rounded={22} flex="row" items="center" px={4} justify="between">
|
||||
<Frame name="Tool_Select" w={36} h={36} bg="#6C5CE7" rounded={18} flex="row" items="center" justify="center">
|
||||
<Icon name="lucide:mouse-pointer-2" size={18} color="#FFFFFF" />
|
||||
</Frame>
|
||||
<Frame name="Tool_Move" w={36} h={36} rounded={18} flex="row" items="center" justify="center">
|
||||
<Icon name="lucide:move" size={18} color="#FFFFFF88" />
|
||||
</Frame>
|
||||
<!-- ...6 more tool buttons with unique names... -->
|
||||
</Frame>
|
||||
<Frame name="BrushColorRow" w="fill" h={40} flex="row" items="center" gap={12}>
|
||||
<Frame name="BrushSizeSlider" grow={1} h={40} flex="row" items="center" gap={12}>
|
||||
<Ellipse w={8} h={8} bg="#FFFFFF66" />
|
||||
<Frame name="SliderTrack" grow={1} h={4} bg="#3A3A3C" rounded={2} overflow="hidden">
|
||||
<Rectangle name="SliderFill" w={120} h={4} bg="#6C5CE7" rounded={2} />
|
||||
</Frame>
|
||||
<Ellipse w={20} h={20} bg="#FFFFFF66" />
|
||||
</Frame>
|
||||
<Frame name="ColorSwatch" w={40} h={40} rounded={20} bg="#3A3A3C" flex="row" items="center" justify="center" stroke="#FFFFFF22" strokeWidth={2}>
|
||||
<Ellipse w={28} h={28} bg="#6C5CE7" />
|
||||
</Frame>
|
||||
</Frame>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Step 6** — Add floating overlays into "CanvasArea" (selection handles, zoom, properties):
|
||||
|
||||
```jsx
|
||||
<Frame
|
||||
name="FloatingZoom"
|
||||
x={12}
|
||||
y={540}
|
||||
w={44}
|
||||
h={120}
|
||||
bg="#2C2C2ECC"
|
||||
rounded={22}
|
||||
flex="col"
|
||||
items="center"
|
||||
justify="center"
|
||||
gap={16}
|
||||
py={12}
|
||||
>
|
||||
<Icon name="lucide:plus" size={16} color="#FFFFFFCC" />
|
||||
<Text color="#FFFFFF88" size={10} weight="medium">
|
||||
75%
|
||||
</Text>
|
||||
<Icon name="lucide:minus" size={16} color="#FFFFFFCC" />
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Step 7** — describe depth=2, fix remaining issues, add shadows, final describe.
|
||||
|
||||
Key patterns in this example:
|
||||
|
||||
- **Every multi-child Frame has `flex`** — no exceptions
|
||||
- **Named all nodes** — Tool_Select, Tool_Move, BrushSizeSlider, etc.
|
||||
- **Floating panels use x/y** — inside non-flex CanvasArea parent
|
||||
- **Procreate aesthetic**: `#2C2C2ECC` semi-transparent panels, `rounded={22}` pill shapes, `shadow` for depth
|
||||
- **Icons with explicit color** — `color="#FFFFFFCC"` or `color="#FFFFFF88"` for hierarchy
|
||||
- **3 renders** (skeleton → content A → content B) + **3 describes** + fix pass
|
||||
|
||||
# Example: desktop business news site
|
||||
|
||||
User prompt: "business media desktop site with real images, 12-col grid, 8 cols main, 4 cols sidebar, breaking news, hero, stories, opinions, sidebar news + stocks + newsletter, footer"
|
||||
|
||||
This is a **desktop media site** (1440px wide, scrollable) — light theme, 12-col grid, card-based layout.
|
||||
|
||||
**Step 1** — calc all grid dimensions in one batch:
|
||||
|
||||
```
|
||||
calc({ expr: '["1440 - 48 - 48 - 24", "floor((1320) * 8 / 12)", "floor((1320) * 4 / 12)"]' })
|
||||
```
|
||||
|
||||
→ Content area 1320px, Main 880px, Sidebar 440px.
|
||||
|
||||
**Step 2** — Skeleton render (entire page with gray placeholders):
|
||||
|
||||
```jsx
|
||||
<Frame name="BusinessMediaSite" w={1440} h="hug" bg="#F5F5F0" flex="col">
|
||||
{/* NavBar — real content */}
|
||||
<Frame
|
||||
name="NavBar"
|
||||
w="fill"
|
||||
h={56}
|
||||
bg="#0F1923"
|
||||
flex="row"
|
||||
items="center"
|
||||
justify="between"
|
||||
px={48}
|
||||
>
|
||||
<Frame name="NavLeft" flex="row" gap={32} items="center">
|
||||
<Text name="Logo" color="#FFFFFF" size={22} weight="bold" font="Playfair Display">
|
||||
THE MARKETS
|
||||
</Text>
|
||||
<Frame name="NavLinks" flex="row" gap={24} items="center">
|
||||
<Text color="#FFFFFFCC" size={14} weight="medium">
|
||||
Markets
|
||||
</Text>
|
||||
<Text color="#FFFFFFCC" size={14} weight="medium">
|
||||
Economy
|
||||
</Text>
|
||||
<Text color="#FFFFFFCC" size={14} weight="medium">
|
||||
Technology
|
||||
</Text>
|
||||
</Frame>
|
||||
</Frame>
|
||||
<Frame name="NavRight" flex="row" gap={16} items="center">
|
||||
<Icon name="lucide:search" size={18} color="#FFFFFFCC" />
|
||||
<Frame name="SubscribeBtn" h={32} px={16} bg="#D4382C" rounded={4} flex="row" items="center">
|
||||
<Text color="#FFFFFF" size={13} weight="bold">
|
||||
Subscribe
|
||||
</Text>
|
||||
</Frame>
|
||||
</Frame>
|
||||
</Frame>
|
||||
|
||||
{/* Breaking News — real content */}
|
||||
<Frame
|
||||
name="BreakingNewsTicker"
|
||||
w="fill"
|
||||
h={40}
|
||||
bg="#D4382C"
|
||||
flex="row"
|
||||
items="center"
|
||||
px={48}
|
||||
gap={16}
|
||||
>
|
||||
<Frame bg="#FFFFFF" px={12} py={4} rounded={2} flex="row" items="center">
|
||||
<Text color="#D4382C" size={11} weight="bold" textCase="upper">
|
||||
BREAKING
|
||||
</Text>
|
||||
</Frame>
|
||||
<Text color="#FFFFFF" size={13} weight="medium">
|
||||
Fed signals rate cut — S&P 500 hits record
|
||||
</Text>
|
||||
</Frame>
|
||||
|
||||
{/* Content area with skeleton placeholders */}
|
||||
<Frame name="ContentArea" w="fill" flex="row" px={48} py={32} gap={24}>
|
||||
<Frame name="MainColumn" w={880} flex="col" gap={32}>
|
||||
{/* Hero skeleton */}
|
||||
<Frame name="HeroArticle" w="fill" flex="col" bg="#FFFFFF" rounded={8} overflow="hidden">
|
||||
<Rectangle name="HeroImg" w="fill" h={420} bg="#E2E8F0" />
|
||||
<Frame w="fill" flex="col" gap={12} p={24}>
|
||||
<Rectangle w={100} h={14} bg="#D4382C" rounded={4} />
|
||||
<Rectangle w="fill" h={32} bg="#CBD5E1" rounded={4} />
|
||||
<Rectangle w={600} h={32} bg="#CBD5E1" rounded={4} />
|
||||
<Rectangle w={200} h={14} bg="#E2E8F0" rounded={4} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
{/* Stories skeleton */}
|
||||
<Frame name="StoriesSection" w="fill" flex="col" gap={20}>
|
||||
<Rectangle w={120} h={24} bg="#CBD5E1" rounded={4} />
|
||||
<Frame w="fill" flex="row" gap={20}>
|
||||
<Frame name="StoryMain" w={440} flex="col" bg="#FFFFFF" rounded={8} overflow="hidden">
|
||||
<Rectangle name="StoryMainImg" w="fill" h={240} bg="#E2E8F0" />
|
||||
<Frame w="fill" flex="col" gap={8} p={16}>
|
||||
<Rectangle w={80} h={12} bg="#CBD5E1" rounded={4} />
|
||||
<Rectangle w="fill" h={20} bg="#CBD5E1" rounded={4} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
<Frame w={420} flex="col" gap={16}>
|
||||
{Array.from({ length: 3 }, (_, i) => (
|
||||
<Frame
|
||||
name={`StoryCard${i + 1}`}
|
||||
key={i}
|
||||
w="fill"
|
||||
flex="row"
|
||||
bg="#FFFFFF"
|
||||
rounded={8}
|
||||
overflow="hidden"
|
||||
h={120}
|
||||
>
|
||||
<Rectangle name={`StoryCardImg${i + 1}`} w={160} h="fill" bg="#E2E8F0" />
|
||||
<Frame w="fill" flex="col" gap={6} p={12}>
|
||||
<Rectangle w={60} h={10} bg="#CBD5E1" rounded={4} />
|
||||
<Rectangle w="fill" h={16} bg="#CBD5E1" rounded={4} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
))}
|
||||
</Frame>
|
||||
</Frame>
|
||||
</Frame>
|
||||
{/* Opinions skeleton — same pattern */}
|
||||
<Frame name="OpinionsSection" w="fill" flex="col" gap={20}>
|
||||
{/* ... same structure as StoriesSection ... */}
|
||||
</Frame>
|
||||
</Frame>
|
||||
{/* Sidebar skeletons */}
|
||||
<Frame name="Sidebar" w={440} flex="col" gap={24}>
|
||||
<Frame name="LatestNewsBlock" w="fill" flex="col" bg="#FFFFFF" rounded={8} overflow="hidden">
|
||||
<Frame w="fill" h={48} bg="#0F1923" flex="row" items="center" px={16}>
|
||||
<Rectangle w={120} h={18} bg="#FFFFFF44" rounded={4} />
|
||||
</Frame>
|
||||
{Array.from({ length: 6 }, (_, i) => (
|
||||
<Frame key={i} w="fill" flex="row" gap={12} p={16}>
|
||||
<Rectangle w={80} h={60} bg="#E2E8F0" rounded={4} />
|
||||
<Frame w="fill" flex="col" gap={6}>
|
||||
<Rectangle w="fill" h={14} bg="#CBD5E1" rounded={4} />
|
||||
<Rectangle w={80} h={10} bg="#E2E8F0" rounded={4} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
))}
|
||||
</Frame>
|
||||
<Frame name="StocksWidget" w="fill" h={360} bg="#FFFFFF" rounded={8} />
|
||||
<Frame name="NewsletterBlock" w="fill" bg="#0F1923" rounded={8} p={24} gap={16}>
|
||||
<Rectangle w={200} h={22} bg="#FFFFFF22" rounded={4} />
|
||||
<Rectangle w="fill" h={44} bg="#D4382C" rounded={8} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
</Frame>
|
||||
{/* Footer — real content */}
|
||||
<Frame name="Footer" w="fill" flex="col" bg="#0F1923" px={48} pt={48} pb={24} gap={32}>
|
||||
{/* ... footer columns ... */}
|
||||
</Frame>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Step 3** — `describe` root depth=2, fix layout with `batch_update`.
|
||||
|
||||
**Steps 4–9** — Replace each skeleton with real content using `replace_id`:
|
||||
|
||||
```
|
||||
render({ jsx: "<Frame name=\"HeroArticle\" ...real content...", replace_id: "0:25" })
|
||||
render({ jsx: "<Frame name=\"StoriesSection\" ...real content...", replace_id: "0:33" })
|
||||
render({ jsx: "<Frame name=\"OpinionsSection\" ...real content...", replace_id: "0:65" })
|
||||
render({ jsx: "<Frame name=\"LatestNewsBlock\" ...real content...", replace_id: "0:98" })
|
||||
render({ jsx: "<Frame name=\"StocksWidget\" ...real content...", replace_id: "0:138" })
|
||||
render({ jsx: "<Frame name=\"NewsletterBlock\" ...real content...", replace_id: "0:162" })
|
||||
```
|
||||
|
||||
**Step 10** — `describe` depth=2, `batch_update` fixes.
|
||||
|
||||
**Step 11** — `stock_photo` batch all image placeholders in one call:
|
||||
|
||||
```
|
||||
stock_photo({ requests: '[{"id":"0:203","query":"federal reserve building"},{"id":"0:221","query":"apple silicon valley technology"},...]' })
|
||||
```
|
||||
|
||||
**Step 12** — Final `describe` depth=1, viewport_zoom_to_fit.
|
||||
|
||||
Key patterns in this example:
|
||||
|
||||
- **h="hug" on page frame** — never fixed height, content determines page length
|
||||
- **Skeleton first** — gray `#E2E8F0` / `#CBD5E1` placeholders show layout before content
|
||||
- **replace_id** — skeleton stays visible until content replaces it atomically
|
||||
- **Named all image placeholders** — `HeroImg`, `StoryMainImg`, `StoryCardImg1` etc. for stock_photo
|
||||
- **12-col grid** — MainColumn w={880} + Sidebar w={440} + gap 24 + padding 48×2 = 1440
|
||||
- **Card pattern**: white bg + rounded + overflow hidden + shadow. Image rectangle + text frame with padding.
|
||||
- **Section header pattern**: row with title + "See all →" link, red accent bar `<Rectangle w={4} h={24} bg="#D4382C" />`
|
||||
- **One batch stock_photo** — 17 images in parallel, not 17 sequential calls
|
||||
- **Footer real content from skeleton** — simple enough to render once
|
||||
- **Total: 1 calc + 1 skeleton + 6 replace renders + 1 stock_photo + 2 describes + fixes = ~15 steps**
|
||||
Follow the user's visual direction and existing design system. Use purposeful typography, spacing, imagery, and color rather than defaulting to gradient dashboards or a canned template. Prefer a coherent, editable composition over displaying every effect at once. Report unresolved font, layout, or fidelity limitations honestly.
|
||||
|
|
|
|||
14
src/app/ai/chat/system-prompt.ts
Normal file
14
src/app/ai/chat/system-prompt.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import dedent from 'dedent'
|
||||
|
||||
import { JSX_REFERENCE } from '@open-pencil/core/design-jsx'
|
||||
|
||||
import behavior from './system-prompt.md?raw'
|
||||
|
||||
/** Chat and ACP share scene-authoring knowledge without copying the renderer reference. */
|
||||
const SYSTEM_PROMPT = dedent`
|
||||
${behavior.trim()}
|
||||
|
||||
${JSX_REFERENCE}
|
||||
`
|
||||
|
||||
export default SYSTEM_PROMPT
|
||||
|
|
@ -10,7 +10,7 @@ import type { ACPAgentID, AIProviderID } from '@open-pencil/core/constants'
|
|||
import { classifyAIChatError, type AIChatFailure } from '@/app/ai/chat/failure'
|
||||
import { resolveLanguageModelID } from '@/app/ai/chat/model'
|
||||
import { buildReasoningProviderOptions, type AIProviderOptions } from '@/app/ai/chat/reasoning'
|
||||
import SYSTEM_PROMPT from '@/app/ai/chat/system-prompt.md?raw'
|
||||
import SYSTEM_PROMPT from '@/app/ai/chat/system-prompt'
|
||||
import { createAIModelRuntime, resolveModelConnectionAPIKey } from '@/app/ai/models'
|
||||
import { MAX_AGENT_STEPS, createAITools, recordStep, resetRunSteps } from '@/app/ai/tools'
|
||||
import {
|
||||
|
|
|
|||
97
tests/engine/render/jsx/reference.test.ts
Normal file
97
tests/engine/render/jsx/reference.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
JSX_REFERENCE as BARREL_REFERENCE,
|
||||
CODEGEN_PROMPT as BARREL_CODEGEN
|
||||
} from '@open-pencil/core'
|
||||
import { AUTHORING_EXAMPLES, JSX_REFERENCE, renderJSX } from '@open-pencil/core/design-jsx'
|
||||
import { CODEGEN_PROMPT } from '@open-pencil/core/tools'
|
||||
|
||||
import SYSTEM_PROMPT from '@/app/ai/chat/system-prompt'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { makeSceneGraph } from '#tests/helpers/scene'
|
||||
|
||||
function exampleGraph() {
|
||||
const graph = makeSceneGraph()
|
||||
graph.addCollection({
|
||||
id: 'tokens',
|
||||
name: 'Tokens',
|
||||
modes: [
|
||||
{ modeId: 'default', name: 'Default' },
|
||||
{ modeId: 'compact', name: 'Compact' }
|
||||
],
|
||||
defaultModeId: 'default',
|
||||
variableIds: []
|
||||
})
|
||||
for (const [name, value, compact] of [
|
||||
['Space/small', 8, 4],
|
||||
['Space/medium', 16, 12],
|
||||
['Type/body', 12, 11],
|
||||
['Type/body-leading', 20, 16],
|
||||
['Type/body-tracking', 0.2, 0]
|
||||
] as const) {
|
||||
graph.addVariable({
|
||||
id: name,
|
||||
name,
|
||||
type: 'FLOAT',
|
||||
collectionId: 'tokens',
|
||||
valuesByMode: { default: value, compact },
|
||||
description: '',
|
||||
hiddenFromPublishing: false
|
||||
})
|
||||
}
|
||||
return graph
|
||||
}
|
||||
|
||||
for (const example of AUTHORING_EXAMPLES) {
|
||||
test(`shared authoring example: ${example.title}`, async () => {
|
||||
const graph = exampleGraph()
|
||||
const [result] = await renderJSX(graph, example.jsx)
|
||||
const frame = getNodeOrThrow(graph, result.id)
|
||||
expect(frame.width).toBe(280)
|
||||
expect(frame.height).toBeGreaterThan(32)
|
||||
expect(frame.layoutMode).toBe('VERTICAL')
|
||||
expect(graph.getChildren(frame.id).every((child) => child.type === 'TEXT')).toBe(true)
|
||||
expect(JSX_REFERENCE).toContain(example.jsx)
|
||||
})
|
||||
}
|
||||
|
||||
test('shared bound example retains bindings and resolves the initial inherited mode', async () => {
|
||||
const graph = exampleGraph()
|
||||
const parent = graph.createNode('FRAME', graph.getPages()[0].id, {
|
||||
variableModes: { tokens: 'compact' }
|
||||
})
|
||||
for (const example of AUTHORING_EXAMPLES) {
|
||||
await renderJSX(graph, example.jsx, { parentId: parent.id })
|
||||
}
|
||||
const bound = graph.getChildren(parent.id).find((node) => node.boundVariables.itemSpacing)
|
||||
if (!bound) throw new Error('Expected a variable-bound authoring example')
|
||||
expect(bound.itemSpacing).toBe(4)
|
||||
expect(bound.paddingTop).toBe(12)
|
||||
expect(bound.boundVariables).toMatchObject({
|
||||
itemSpacing: 'Space/small',
|
||||
paddingTop: 'Space/medium',
|
||||
paddingRight: 'Space/medium',
|
||||
paddingBottom: 'Space/medium',
|
||||
paddingLeft: 'Space/medium'
|
||||
})
|
||||
const [text] = graph.getChildren(bound.id)
|
||||
expect(text.fontSize).toBe(11)
|
||||
expect(text.lineHeight).toBe(16)
|
||||
expect(text.letterSpacing).toBe(0)
|
||||
expect(text.boundVariables).toMatchObject({
|
||||
fontSize: 'Type/body',
|
||||
lineHeight: 'Type/body-leading',
|
||||
letterSpacing: 'Type/body-tracking'
|
||||
})
|
||||
})
|
||||
|
||||
test('public exports and runtime prompts use the same authoring reference', () => {
|
||||
expect(BARREL_REFERENCE).toBe(JSX_REFERENCE)
|
||||
expect(BARREL_CODEGEN).toBe(CODEGEN_PROMPT)
|
||||
expect(CODEGEN_PROMPT).toContain(JSX_REFERENCE)
|
||||
expect(SYSTEM_PROMPT).toContain(JSX_REFERENCE)
|
||||
expect(CODEGEN_PROMPT.split(JSX_REFERENCE)).toHaveLength(2)
|
||||
expect(SYSTEM_PROMPT.split(JSX_REFERENCE)).toHaveLength(2)
|
||||
})
|
||||
16
tools/authoring-reference/package.json
Normal file
16
tools/authoring-reference/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@open-pencil/authoring-reference-tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#authoring-reference/*": "./src/*.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "bun test tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@open-pencil/core": "workspace:*",
|
||||
"@open-pencil/package-artifacts": "workspace:*",
|
||||
"dedent": "^1.7.2"
|
||||
}
|
||||
}
|
||||
32
tools/authoring-reference/src/artifacts.ts
Normal file
32
tools/authoring-reference/src/artifacts.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
import { JSX_REFERENCE } from '@open-pencil/core/design-jsx'
|
||||
|
||||
const HEADER =
|
||||
'<!-- Generated from Core design-jsx/reference and renderer metadata. Do not edit; run bun run generate:authoring-reference. -->\n\n'
|
||||
|
||||
export const AUTHORING_REFERENCE_ARTIFACTS = {
|
||||
'skills/open-pencil/references/design-authoring.md': HEADER + JSX_REFERENCE,
|
||||
'packages/docs/reference/design-authoring.md': HEADER + JSX_REFERENCE
|
||||
}
|
||||
|
||||
export async function writeReferences(root: string): Promise<void> {
|
||||
for (const [path, content] of Object.entries(AUTHORING_REFERENCE_ARTIFACTS)) {
|
||||
const target = join(root, path)
|
||||
await mkdir(dirname(target), { recursive: true })
|
||||
await writeFile(target, content)
|
||||
}
|
||||
}
|
||||
|
||||
export async function staleReferences(root: string): Promise<string[]> {
|
||||
const stale: string[] = []
|
||||
for (const [path, expected] of Object.entries(AUTHORING_REFERENCE_ARTIFACTS)) {
|
||||
const actual = await readFile(join(root, path), 'utf8').catch((error: unknown) => {
|
||||
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return null
|
||||
throw error
|
||||
})
|
||||
if (actual !== expected) stale.push(path)
|
||||
}
|
||||
return stale
|
||||
}
|
||||
14
tools/authoring-reference/src/check.ts
Normal file
14
tools/authoring-reference/src/check.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { staleReferences } from '#authoring-reference/artifacts'
|
||||
import dedent from 'dedent'
|
||||
|
||||
import { resolveWorkspaceRoot } from '@open-pencil/package-artifacts'
|
||||
|
||||
const stale = await staleReferences(await resolveWorkspaceRoot(process.cwd()))
|
||||
if (stale.length > 0) {
|
||||
console.error(dedent`
|
||||
Stale authoring references:
|
||||
${stale.join('\n')}
|
||||
Run bun run generate:authoring-reference.
|
||||
`)
|
||||
process.exitCode = 1
|
||||
}
|
||||
5
tools/authoring-reference/src/generate.ts
Normal file
5
tools/authoring-reference/src/generate.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { writeReferences } from '#authoring-reference/artifacts'
|
||||
|
||||
import { resolveWorkspaceRoot } from '@open-pencil/package-artifacts'
|
||||
|
||||
await writeReferences(await resolveWorkspaceRoot(process.cwd()))
|
||||
29
tools/authoring-reference/tests/artifacts.test.ts
Normal file
29
tools/authoring-reference/tests/artifacts.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { afterEach, expect, test } from 'bun:test'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import {
|
||||
AUTHORING_REFERENCE_ARTIFACTS,
|
||||
staleReferences,
|
||||
writeReferences
|
||||
} from '#authoring-reference/artifacts'
|
||||
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
test('detects missing and edited references without rewriting during checks', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'open-pencil-authoring-'))
|
||||
roots.push(root)
|
||||
const paths = Object.keys(AUTHORING_REFERENCE_ARTIFACTS)
|
||||
expect(await staleReferences(root)).toEqual(paths)
|
||||
await writeReferences(root)
|
||||
expect(await staleReferences(root)).toEqual([])
|
||||
const path = paths[0]
|
||||
expect(path).toBeDefined()
|
||||
await writeFile(join(root, path), 'outdated')
|
||||
expect(await staleReferences(root)).toEqual([path])
|
||||
expect(await staleReferences(root)).toEqual([path])
|
||||
})
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"#authoring-reference/*": ["./tools/authoring-reference/src/*"],
|
||||
"#ci/*": ["./tools/ci/src/*"],
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
|
|
|
|||
Loading…
Reference in a new issue