| name | canvas |
| description | Create, read, and manipulate shapes on the canvas. The canvas is the primary surface for showing live UI previews via iframe embeds (using the mockup-sandbox skill), as well as static shapes, text, notes, images, and videos. Use this skill for any canvas operation — reading board state, placing shapes, or managing iframe lifecycle. You must read this skill (0-500 lines) before any canvas operation |
Canvas Skill
IMPORTANT: Place Building Iframes Before Building
When the user's request will produce new visual content on the canvas (e.g. "create a landing page", "show me 3 card variants", "mockup a dashboard"), your very first action must be to place building placeholders — before writing any code or setting up servers:
- Call
getCanvasState() via code_execution to read the board and find empty space.
- Call
applyCanvasActions() to create iframe shapes with state: "building" and componentName for every element you plan to produce. No URL is needed — the UI shows a building indicator.
- Only then proceed with the rest of the work (mockup-sandbox setup, writing code, starting servers, etc.).
- As each element becomes ready, update its iframe to
state: "live" with the real URL.
Do not set up the mockup sandbox first. Do not write component code first. Place the building iframes first, then do everything else. The user sees the placeholders appear on the canvas immediately, giving them instant feedback that work is underway and showing the planned layout.
This does not apply to read-only requests (e.g. "what's on the canvas?"), modifications to existing shapes (e.g. "move the card to the right"), or non-iframe work (e.g. "add a text label").
Overview
The workspace canvas is an infinite board where you can create, position, and manipulate visual elements. It supports shapes, iframes (primarily used for design exploration), and artifacts (live-running apps such as websites or mobile apps).
When users want to view frames at full size, they must click the preview button above the frame. Users can also toggle in and out of the canvas using the canvas button below the workspace-level preview window. When telling the user where to view canvas content, say "open the Preview tab and toggle on the canvas" — there is no "Canvas tab".
Artifact frames have special constraints - they cannot be deleted or freely resized (to maintain the snap back in ratio).
You have the following tools:
Beyond iframes, the canvas also supports static shapes (rectangles, ellipses, text, notes), images, and videos for diagrams, annotations, and layouts.
Callbacks
You have three callbacks available via code_execution:
getCanvasState -- Read what shapes are on the board, their positions, types, and properties.
applyCanvasActions -- Create, update, delete, move, resize, reorder, align, or distribute shapes.
focusCanvasShapes -- Pan and zoom the viewport to show specific shapes.
All callbacks are async and must be awaited. Call them directly in code_execution -- they are pre-registered.
Parameter casing. All canvas callbacks accept camelCase keys (e.g. shapeIds, animateMs, focusArea, shapeId, componentName). Passing snake_case keys causes a pydantic validation error like shapeIds Field required. The schemas below reflect the camelCase keys you must actually pass.
Canvas + Mockup Sandbox
For any request that involves showing rendered UI on the canvas, you need both this skill and the mockup-sandbox skill working together:
- mockup-sandbox sets up the isolated vite dev server, creates the component files, and gives you preview URLs.
- canvas (this skill) places those URLs as iframe shapes on the board, manages layout, and handles the iframe lifecycle.
Coordinate System
- Origin
(0, 0) is at the top-left of the canvas.
- Positive
x goes right, positive y goes down.
- All positions and sizes are in canvas units.
Parameter Schema
applyCanvasActions
Modify the canvas board by applying an ordered list of actions in a single atomic batch. Always call getCanvasState first to see existing shapes and find empty space.
{
"$defs": {
"ScribeAlignActionInput": {
"properties": {
"type": { "const": "align", "description": "Align multiple shapes.", "type": "string" },
"shapeIds": { "description": "Target shape ids.", "items": { "type": "string" }, "type": "array" },
"alignment": { "description": "Alignment mode.", "enum": ["left", "center-horizontal", "right", "top", "center-vertical", "bottom"], "type": "string" }
},
"required": ["type", "shapeIds", "alignment"]
},
"ScribeCreateActionInput": {
"properties": {
"type": { "const": "create", "description": "Create a shape.", "type": "string" },
"shape": { "$ref": "#/$defs/ScribeShapeInput", "description": "Shape payload." },
"shapeId": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "Optional deterministic id." }
},
"required": ["type", "shape"]
},
"ScribeDeleteActionInput": {
"properties": {
"type": { "const": "delete", "description": "Delete a shape.", "type": "string" },
"shapeId": { "description": "Target shape id.", "type": "string" }
},
"required": ["type", "shapeId"]
},
"ScribeDistributeActionInput": {
"properties": {
"type": { "const": "distribute", "description": "Distribute multiple shapes.", "type": "string" },
"shapeIds": { "description": "Target shape ids.", "items": { "type": "string" }, "type": "array" },
"direction": { "description": "Distribution axis.", "enum": ["horizontal", "vertical"], "type": "string" }
},
"required": ["type", "shapeIds", "direction"]
},
"ScribeMoveActionInput": {
"properties": {
"type": { "const": "move", "description": "Move a shape.", "type": "string" },
"shapeId": { "description": "Target shape id.", "type": "string" },
"x": { "description": "Destination x.", "type": "number" },
"y": { "description": "Destination y.", "type": "number" }
},
"required": ["type", "shapeId", "x", "y"]
},
"ScribeReorderActionInput": {
"properties": {
"type": { "const": "reorder", "description": "Reorder a shape.", "type": "string" },
"shapeId": { "description": "Target shape id.", "type": "string" },
"direction": { "description": "Stacking direction.", "enum": ["front", "back"], "type": "string" }
},
"required": ["type", "shapeId", "direction"]
},
"ScribeResizeActionInput": {
"properties": {
"type": { "const": "resize", "description": "Resize a shape.", "type": "string" },
"shapeId": { "description": "Target shape id.", "type": "string" },
"w": { "description": "Destination width.", "type": "number" },
"h": { "description": "Destination height.", "type": "number" }
},
"required": ["type", "shapeId", "w", "h"]
},
"ScribeShapeInput": {
"properties": {
"type": { "description": "Shape type: 'geo' (rectangle/ellipse), 'text' (label), 'note' (sticky note), 'iframe' (embedded web content), 'image' (embedded image), or 'video' (embedded video).", "enum": ["geo", "text", "note", "iframe", "image", "video"], "type": "string" },
"x": { "description": "X position on canvas (0 is left).", "type": "number" },
"y": { "description": "Y position on canvas (0 is top).", "type": "number" },
"w": { "description": "Shape width in canvas units.", "type": "number" },
"h": { "description": "Shape height in canvas units.", "type": "number" },
"geo": { "anyOf": [{ "enum": ["cloud", "rectangle", "ellipse", "triangle", "diamond", "pentagon", "hexagon", "octagon", "star", "rhombus", "rhombus-2", "oval", "trapezoid", "arrow-right", "arrow-left", "arrow-up", "arrow-down", "x-box", "check-box", "heart"], "type": "string" }, { "type": "null" }], "default": null, "description": "Geometry sub-type for 'geo' shapes." },
"color": { "anyOf": [{ "enum": ["black", "grey", "light-violet", "violet", "blue", "light-blue", "yellow", "orange", "green", "light-green", "light-red", "red", "white"], "type": "string" }, { "type": "null" }], "default": null, "description": "Shape color." },
"labelColor": { "anyOf": [{ "enum": ["black", "grey", "light-violet", "violet", "blue", "light-blue", "yellow", "orange", "green", "light-green", "light-red", "red", "white"], "type": "string" }, { "type": "null" }], "default": null, "description": "Text label color. Overrides the shape color for text content." },
"fill": { "anyOf": [{ "enum": ["none", "semi", "solid", "pattern", "fill", "lined-fill"], "type": "string" }, { "type": "null" }], "default": null, "description": "Fill style." },
"text": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "Text content displayed inside the shape." },
"url": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "For iframe shapes: the https URL to embed. Optional when creating with state 'building'; required when setting state to 'live'." },
"src": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "For image/video shapes: the source URL of the media." },
"altText": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "For image/video shapes: alt text describing the media." },
"componentPath": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "For iframe shapes: file path shown in the shape title bar." },
"componentName": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "For iframe shapes: component name shown in the shape title bar." },
"componentProps": { "anyOf": [{ "additionalProperties": true, "type": "object" }, { "type": "null" }], "default": null, "description": "For iframe shapes: extra props to pass." },
"state": { "anyOf": [{ "enum": ["building", "modifying", "live"], "type": "string" }, { "type": "null" }], "default": null, "description": "For iframe shapes: lifecycle state. Set 'building' on create, 'modifying' before edits, 'live' when the component is ready." }
},
"required": ["type", "x", "y", "w", "h"]
},
"ScribeUpdateActionInput": {
"properties": {
"type": { "const": "update", "description": "Update a shape.", "type": "string" },
"shapeId": { "description": "Target shape id.", "type": "string" },
"updates": { "$ref": "#/$defs/ScribeUpdateFieldsInput", "description": "Partial shape update." }
},
"required": ["type", "shapeId", "updates"]
},
"ScribeUpdateFieldsInput": {
"properties": {
"shapeType": { "description": "The type of shape being updated. Always required -- controls how the update is serialized.", "enum": ["geo", "text", "note", "iframe", "image", "video"], "type": "string" },
"x": { "anyOf": [{ "type": "number" }, { "type": "null" }], "default": null, "description": "New x position." },
"y": { "anyOf": [{ "type": "number" }, { "type": "null" }], "default": null, "description": "New y position." },
"w": { "anyOf": [{ "type": "number" }, { "type": "null" }], "default": null, "description": "New width." },
"h": { "anyOf": [{ "type": "number" }, { "type": "null" }], "default": null, "description": "New height." },
"geo": { "anyOf": [{ "enum": ["cloud", "rectangle", "ellipse", "triangle", "diamond", "pentagon", "hexagon", "octagon", "star", "rhombus", "rhombus-2", "oval", "trapezoid", "arrow-right", "arrow-left", "arrow-up", "arrow-down", "x-box", "check-box", "heart"], "type": "string" }, { "type": "null" }], "default": null, "description": "New geometry sub-type." },
"color": { "anyOf": [{ "enum": ["black", "grey", "light-violet", "violet", "blue", "light-blue", "yellow", "orange", "green", "light-green", "light-red", "red", "white"], "type": "string" }, { "type": "null" }], "default": null, "description": "New shape color." },
"labelColor": { "anyOf": [{ "enum": ["black", "grey", "light-violet", "violet", "blue", "light-blue", "yellow", "orange", "green", "light-green", "light-red", "red", "white"], "type": "string" }, { "type": "null" }], "default": null, "description": "New text label color." },
"fill": { "anyOf": [{ "enum": ["none", "semi", "solid", "pattern", "fill", "lined-fill"], "type": "string" }, { "type": "null" }], "default": null, "description": "New fill style." },
"text": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "New text content." },
"url": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "New URL for iframe shapes (must be https)." },
"src": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "New source URL for image/video shapes." },
"altText": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "New alt text for image/video shapes." },
"componentPath": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "New component path for iframe shapes." },
"componentName": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "New component name for iframe shapes." },
"componentProps": { "anyOf": [{ "additionalProperties": true, "type": "object" }, { "type": "null" }], "default": null, "description": "Props to merge into iframe shape props." },
"state": { "anyOf": [{ "enum": ["building", "modifying", "live"], "type": "string" }, { "type": "null" }], "default": null, "description": "New lifecycle state for iframe shapes. Set 'modifying' before edits, 'live' when ready." }
},
"required": ["shapeType"]
}
},
"properties": {
"actions": {
"description": "Ordered list of canvas actions to apply.",
"items": {
"anyOf": [
{ "$ref": "#/$defs/ScribeCreateActionInput" },
{ "$ref": "#/$defs/ScribeUpdateActionInput" },
{ "$ref": "#/$defs/ScribeDeleteActionInput" },
{ "$ref": "#/$defs/ScribeMoveActionInput" },
{ "$ref": "#/$defs/ScribeResizeActionInput" },
{ "$ref": "#/$defs/ScribeReorderActionInput" },
{ "$ref": "#/$defs/ScribeAlignActionInput" },
{ "$ref": "#/$defs/ScribeDistributeActionInput" }
]
},
"type": "array"
}
},
"required": ["actions"]
}
getCanvasState
Read the current state of the canvas board. Returns shapes at three detail levels based on distance from the viewport or focus area.
{
"$defs": {
"FocusAreaInput": {
"properties": {
"x": { "description": "Left edge x coordinate.", "type": "number" },
"y": { "description": "Top edge y coordinate.", "type": "number" },
"w": { "description": "Width of the region.", "type": "number" },
"h": { "description": "Height of the region.", "type": "number" }
},
"required": ["x", "y", "w", "h"]
}
},
"properties": {
"focusArea": {
"anyOf": [{ "$ref": "#/$defs/FocusAreaInput" }, { "type": "null" }],
"default": null,
"description": "Optional region to zoom into. Shapes inside get full detail. If omitted, uses the current user viewport."
}
}
}
Response fields:
- focusedShapes -- Full detail for shapes inside the viewport/focus area. Geo/text/note shapes include color, fill, text. Iframe shapes include
url, componentName, componentPath, state. Image shapes include src, altText, filepath. Video shapes include src, altText.
- blurryShapes -- Position and basic info for shapes farther away. Iframe shapes include
componentName and state. Image shapes include src and filepath. Video shapes include src.
- peripheralClusters -- Aggregated counts for distant shape groups.
- summary -- Quick text description of everything on the canvas.
- viewport -- The current visible region
{ x, y, w, h }.
focusCanvasShapes
Pan and zoom the user's canvas viewport to center on specific shapes. Only call after the user asks to see your work — except for the empty-canvas mockup exception (see "Focusing the Viewport" below).
{
"properties": {
"shapeIds": { "description": "List of shape IDs to focus on.", "items": { "type": "string" }, "type": "array" },
"animateMs": { "anyOf": [{ "type": "number" }, { "type": "null" }], "default": null, "description": "Optional animation duration in milliseconds for the viewport transition. Use 500 for smooth transitions." },
"padding": { "anyOf": [{ "type": "number" }, { "type": "null" }], "default": null, "description": "Optional padding around the focused shapes in canvas units." }
},
"required": ["shapeIds"]
}
Reading the Board: getCanvasState
Returns shapes at three detail levels:
- focusedShapes -- Full detail for shapes near the viewport or focus area. Geo/text/note shapes include color, fill, text. Iframe shapes include
url, componentName, componentPath, and state. Image shapes include src, altText, and filepath (local file path in .canvas/assets/, if applicable). Video shapes include src and altText.
- blurryShapes -- Position and basic info for shapes farther away. Iframe shapes include
componentName and state. Image shapes include src and filepath. Video shapes include src.
- peripheralClusters -- Aggregated counts for distant shape groups.
- summary -- Quick text description of everything on the canvas.
- viewport -- The current visible region
{x, y, w, h}.
Pass an optional focusArea ({x, y, w, h}) to zoom into a specific region.
const state = await getCanvasState();
console.log(state.summary);
console.log(JSON.stringify(state.focusedShapes, null, 2));
const region = await getCanvasState({ focusArea: { x: 0, y: 0, w: 2000, h: 1500 } });
Example response:
{
"focusedShapes": [
{ "shapeId": "preview-1", "shapeType": "iframe", "x": 400, "y": 100, "w": 1280, "h": 720,
"url": "https://example.replit.dev/preview/hello-world/Card",
"componentName": "Card", "state": "live" }
],
"blurryShapes": [...],
"peripheralClusters": [],
"viewport": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
"summary": "1 shape on canvas."
}
Modifying the Board: applyCanvasActions
Send an ordered list of actions. Each action has a type field. Results are returned per-action with generated shapeId values.
const result = await applyCanvasActions({ actions: [
] });
console.log(JSON.stringify(result, null, 2));
Create Iframe
Embed live web content. Use the state field to manage the iframe lifecycle:
"building" -- Set on create when the component or server isn't ready yet. URL is optional in this state -- the UI shows a building indicator.
"modifying" -- Set before editing an existing component's backing file.
"live" -- Set when the component is ready to display. URL is required in this state.
Create the iframe immediately with state: "building", then update it to "live" once the URL is available:
await applyCanvasActions({ actions: [
{
type: "create",
shapeId: "app-preview",
shape: {
type: "iframe",
x: 0, y: 0, w: 1280, h: 720,
state: "building",
componentName: "App Preview"
}
}
] });
await applyCanvasActions({ actions: [
{
type: "update",
shapeId: "app-preview",
updates: {
shapeType: "iframe",
state: "live",
url: "https://<resolved-domain>.replit.dev"
}
}
] });
Note: create and update actions have different payload structures.
- Create defines a new shape from scratch, so it takes a full
shape object: shape: { type: "iframe", ... }
- Update patches an existing shape, so it takes a partial
updates object: updates: { shapeType: "iframe", ... } -- the field is shapeType (not type) because type is already the action discriminator
- Do not copy the payload key from a create into an update or vice versa -- the wrong key passes validation but the action will fail when applied.
To get the URL for a Replit dev server, run echo $REPLIT_DOMAINS in the shell to get the domain, then construct the full URL. For the main app on port 5000, no port suffix is needed. For other ports, append :<port>.
url -- Full https:// URL to embed. Required when state is "live".
state -- Lifecycle state. Always set this on create and when transitioning.
componentPath -- File path shown in the title bar (metadata label only).
componentName -- Display name shown in the title bar (metadata label only).
componentProps -- Extra props dict merged into shape props.
To embed individual React components (not just the full app), use the mockup-sandbox skill. It sets up a vite preview server where each component gets its own route at /preview/{folder}/{ComponentName}. Embed these URLs as iframe shapes. For example, a pricing card component at mockup/client/src/components/mockups/pricing/Card.tsx would be embedded with URL https://<domain>:8000/preview/pricing/Card.
Create Image
Embed an image on the canvas.
From an external URL:
await applyCanvasActions({ actions: [
{
type: "create",
shapeId: "hero-image",
shape: {
type: "image",
x: 0, y: 0, w: 800, h: 600,
src: "https://example.com/hero.png",
altText: "Hero banner image"
}
}
] });
From a local file (copy to .canvas/assets/, resolve domain, use port 5904):
await applyCanvasActions({ actions: [
{
type: "create",
shapeId: "hero-image",
shape: {
type: "image",
x: 0, y: 0, w: 800, h: 600,
src: "https://<resolved-domain>:5904/hero.png",
altText: "Hero banner image"
}
}
] });
Align and Distribute Shapes
Prefer align (2+ shapes) and distribute (3+ shapes) over hand-computing x/y. Faster to emit, and pixel-perfect where hand math is often off by a few units. Fewer shapes than the minimum returns INSUFFICIENT_SHAPES.
When to use:
- Row or column of 3+ related items you are placing together (cards, iframes, thumbnails) --
align one axis, distribute the other.
- Pair of shapes you are placing together and want to share an edge (e.g. before/after side-by-side) --
align only; do not distribute 2 shapes.
- Cleaning up a group of shapes the user already placed and explicitly asked to be lined up -- all of them will move.
When NOT to use: to place a new shape next to existing user content without moving that content, do not pass the existing shape into align -- align moves every shape in shapeIds. Read the anchor's position with getCanvasState, then compute the new shape's coordinates from the anchor: share the aligned axis (e.g. y: anchor.y for tops) and offset the other by anchor.x + anchor.w + gap (or anchor.y + anchor.h + gap) -- keep the anchor.x/anchor.y term so the new shape lands beside the anchor, not at the origin.
Rule of thumb (3+ shapes): if you're chaining x3 = x2 + w + gutter, x4 = x3 + w + gutter, stop -- place shapes at approximate positions and line them up with align/distribute in the same batch. For a 2-shape pair, the gap still has to come from the x/y you pass on create -- distribute rejects 2 shapes and align only matches the shared edge. align modes: left snaps all shapes to the leftmost x; right to the max right edge; top to the minimum y; bottom to the max bottom edge; center-horizontal/center-vertical snap to the mean center.
Recipe: row of three mockup iframes. Place at roughly different y values, then align tops and distribute horizontally in one batch:
await applyCanvasActions({ actions: [
{ type: "create", shapeId: "mobile", shape: { type: "iframe", x: 0, y: 20, w: 390, h: 844, state: "building", componentName: "Mobile" } },
{ type: "create", shapeId: "tablet", shape: { type: "iframe", x: 500, y: 0, w: 768, h: 1024, state: "building", componentName: "Tablet" } },
{ type: "create", shapeId: "desktop", shape: { type: "iframe", x: 1400, y: 40, w: 1280, h: 720, state: "building", componentName: "Desktop" } },
{ type: "align", shapeIds: ["mobile", "tablet", "desktop"], alignment: "top" },
{ type: "distribute", shapeIds: ["mobile", "tablet", "desktop"], direction: "horizontal" }
] });
Recipe: column of cards -- align left, distribute vertically:
await applyCanvasActions({ actions: [
{ type: "align", shapeIds: ["card-1", "card-2", "card-3", "card-4"], alignment: "left" },
{ type: "distribute", shapeIds: ["card-1", "card-2", "card-3", "card-4"], direction: "vertical" }
] });
Recipe: annotate an existing shape without moving it. Search both focusedShapes and blurryShapes; on a large board the anchor can be omitted from both (overflow lands in peripheralClusters, which has no per-shape data), so re-query with focusArea. Do NOT pass the anchor into align, that would move it:
const find = (s) => s.shapeId === "pricing-card";
const locate = (st) => st.focusedShapes.find(find) ?? st.blurryShapes.find(find);
let card = locate(await getCanvasState());
if (!card) card = locate(await getCanvasState({ focusArea: { x: 1800, y: 100, w: 600, h: 400 } }));
if (!card) throw new Error("pricing-card not on canvas");
await applyCanvasActions({ actions: [
{ type: "create", shapeId: "pricing-label", shape: { type: "text", x: card.x + card.w + 40, y: card.y, w: 240, h: 40, text: "Pricing card v2" } }
] });
Do not follow align/distribute with a manual move on any of the same shapes in the same batch -- it undoes the alignment. To rigidly translate a laid-out row or column (preserving gutters), read current positions via getCanvasState and issue one move per shape with the same delta applied. Re-running align/distribute recomputes the layout from scratch, not a translation.
Focusing the Viewport: focusCanvasShapes
Pan and zoom the user's canvas viewport to center on specific shapes. Only call after the user asks to see your work -- don't auto-focus after creating or updating shapes. Finish your work and ask the user if they'd like to see it. Moving the viewport while the user is working is disorienting.
Exception: the mockup-sandbox skill overrides this when getCanvasState returns zero shapes — focus on the just-placed placeholders so the user sees them appear. Once any shape exists, the default rule above applies.
Iframe Rules & Gotchas
- Use
state for lifecycle -- Set "building" on create (URL optional), "modifying" before edits, "live" when ready (URL required).
- URL must be
https -- http and about:blank are rejected.
- Resolve the domain first -- run
echo $REPLIT_DOMAINS in the shell, then build the URL from the result. Never pass a literal template string as the URL.
- Port rules: no port suffix = port 5000 (main app). For other servers, append
:<port>.
- External sites may block embedding -- sites with
X-Frame-Options: DENY or restrictive CSP headers will show a blank iframe. Replit dev URLs work fine.
- Never embed the main app URL for component previews -- the main app URL shows the entire app with navigation, layout, and routing -- not an isolated component. Use the mockup-sandbox skill to set up a preview server, then embed
/preview/{folder}/{Component} URLs. This gives you isolated components that can be iterated on independently. The mockup sandbox runs on port 8000 (append :8000 to the domain).
Typical Workflow
- Call
getCanvasState() to see what's on the board.
- Use the
summary and focusedShapes to understand positions and IDs.
- Call
applyCanvasActions with a batch of changes.
- CRITICAL — Present the result. After your final canvas action, you MUST call
presentArtifact({ artifactId, shapeIds: [...] }) with the IDs of all shapes you created or modified. This is how the user finds your work — without it, they cannot navigate to the shapes. Do NOT skip this step. Do NOT ask the user if they want to focus — just present.
Error Codes
SHAPE_NOT_FOUND -- Shape ID doesn't exist.
UNSUPPORTED_SHAPE_TYPE -- Invalid shape type.
INVALID_PROPS -- Bad property values (e.g., non-https iframe URL).
VALIDATION_FAILED -- Shape with that ID already exists.
INSUFFICIENT_SHAPES -- Not enough shapes for align/distribute.
Best Practices
- Read before writing -- Always call
getCanvasState before layout-sensitive changes.
- Set shapeId on create -- So you can reference, update, or delete the shape later.
- Always call
presentArtifact after canvas work. After creating or modifying shapes, pass all affected shape IDs to presentArtifact. Never skip this. Never ask the user if they want to see the shapes. Do NOT call focusCanvasShapes as a separate step (except for the narrow first-build mockup exception above).
- Batch actions -- Group related changes in one
applyCanvasActions call.
- Use https URLs -- Iframe shapes reject http URLs.
- Label iframes -- Set
componentPath and componentName so users can identify embedded content.
- Use
focusArea -- For large boards, pass a region to getCanvasState to get detail where you need it.
- Prefer
align/distribute over manual coordinates when placing shapes together -- For rows or columns of 3+ shapes you are laying out together, add distribute so you don't hand-compute gutters. align repositions every shape in shapeIds (no anchor), so only pass shapes you actually want moved. To place a new shape next to existing user content, read the anchor's position with getCanvasState (check both focusedShapes and blurryShapes) and create beside it -- share the aligned axis (e.g. y: anchor.y), offset the other by the anchor's position plus its size plus a gap (e.g. x: anchor.x + anchor.w + gap). Do not pass the anchor into align. See "Align and Distribute Shapes" above.
Iframe Sizing
Size the iframe to fit the content -- don't put small components in huge iframes (they look lost in whitespace) and don't put full pages in tiny ones.
Content-aware defaults:
- Cards, forms, small components: Use a snug frame that fits the content (e.g. 400 x 500 for a card). Center the component in the page with
flex items-center justify-center min-h-screen.
- Full-page mockups (desktop): 1280 x 900 -- shows hero + start of next section, user scrolls within the iframe.
- Full-page mockups (full view): 1280 x 2400 -- shows entire page without scrolling (screenshot-style review).
- Multi-page app (desktop): 1280 x 800 -- standard app viewport.
- Multi-page app (mobile): 390 x 844 -- iPhone viewport.
Responsive comparison presets -- when showing the same component at multiple screen widths, place the iframes at approximate positions then align their tops and distribute horizontally (see "Align and Distribute Shapes"):
- Mobile: 390 x 844
- Tablet: 768 x 1024
- Desktop: 1280 x 720