---
name: artifact-canvas
description: Create sandboxed canvas artifacts as an ordered list of frames (HTML/CSS/JS); the platform arranges, steps, and paginates them — a deck is one `slides` frame per slide, a document (letter/contract/proposal) is ONE fixed-page-box `pages` frame the platform flows across pages with letterhead furniture, so never hand-roll a deck/stepper/page-grid.
loading: on-demand
---

# Artifact Canvas

Use this skill when the user asks for a visual or interactive output that should appear in the Studiograph canvas: slide decks, letterhead/print documents, social graphics, diagrams, prototypes, whiteboards, calculators, one-off tools, or any other artifact you can express as HTML/CSS/JS.

An artifact is an **ordered list of frames** (the Figma/Paper artboard sense). Each frame owns its own content and geometry; the **platform** owns the arrangement (a tidy row-major grid), the present/export sequence, the stepper, scaling, and all canvas chrome. You author **content per frame** — you never write deck/stepper/grid machinery yourself.

## Workflow

1. Gather the source material from the user's prompt and any relevant entries.
2. Build an `ArtifactBundle` (shape below) with `frames` ordered the way they should present.
3. Call `create_artifact` with the bundle. Its result reports the resolved frame structure (`manifest`) and any `warnings` — check the frame count and per-frame layouts against what the user asked for (a deck is N `slides` frames, not one `app` frame; a document is ONE `pages` frame the platform flows across pages, not an `app`/`artboard` frame with a hand-built page grid), and resolve any warning (e.g. `packed-slides`, `packed-pages`), before you respond. This structural check is free; do it every time.
4. Call `inspect_artifact` when you need a visual (pixel) check after meaningful work — the create/patch manifest already covers frame count and per-frame layout/geometry, but it counts characters and CANNOT measure rendered height, so it cannot see CROPPED content. For a large or content-driven artifact, inspect it: the render reports `framesOverflowing` (frames whose content is taller than their declared height and is clipped from view). A `pages` document frame is NOT in this list — it paginates instead of cropping. For an `app`/`artboard` frame that overflows, set its `geometry.height` to `"auto"` (or split the content) and re-inspect until `framesOverflowing` is empty.
5. Keep your chat response short; the canvas carries the artifact itself.
6. If the chat is scoped to an open artifact entry, `create_artifact` updates that canvas. If the user references a different EXISTING artifact entry, pass `target.entityId` and `target.repo` when known. To create a SEPARATE new artifact while scoped to one, pass `createNew: true` (optionally with `target.repo` to pick the destination folder) — the scoped canvas is left untouched. Otherwise it creates a new `artifact` graph entry.

## Storage — always use the artifact tools

An `artifact` entry stores the ArtifactBundle as **JSON** in its body — never markdown or prose. Always create and edit artifacts through the artifact tools, never `create_entity` / `update_entity` / `patch_entity`:

- Create or fill: `create_artifact` with the full bundle.
- Edit an existing one: `read_artifact` (returns the current bundle + a `hash`), modify the full bundle, then `create_artifact` passing that `hash` as `baseHash` so a concurrent change is detected instead of silently overwritten.

`create_artifact` assembles and validates the bundle for you; the generic entry tools reject a hand-written artifact body. If you only have raw markdown/prose you haven't laid out yet, create a `document` entry instead — not an `artifact`.

Make a revision as ONE `create_artifact` that rewrites the whole bundle — don't fan out several edits to the same artifact in parallel. Each write replaces the entire bundle, so concurrent writes race; a sibling write that lands first makes yours fail the `baseHash` precondition (re-read and retry rather than retrying blindly).

## Large artifacts — build a skeleton, then grow it incrementally

A whole bundle is emitted as ONE streamed `create_artifact` argument. A very large bundle (long decks, multi-section documents) can blow past the model's per-response output-token limit **mid-content**: the response is cut off, but a partial bundle can still close validly and SAVE — so you get a silently truncated artifact (missing frames, content ending mid-sentence) that reports success. This is NOT the 512KB storage cap; it's the model's output limit, and it fires long before 512KB.

Do not try to one-shot a large artifact. Author it in steps:

1. **Skeleton first.** `create_artifact` with every frame present but each holding only its heading/placeholder (a few lines). This establishes the structure cheaply, well under the output limit.
2. **Fill incrementally with `patch_artifact`** — one frame (or one section) per call. Use `set_frame_content` to populate a skeleton frame, or `add_frame` to append a new frame. Each patch is its own response with its own output budget, so the total content can far exceed what a single `create_artifact` could emit. (Pass the `base_hash` from the previous read/write; re-read if it drifts.)
3. **Heed the truncation warning.** If a `create_artifact` / `patch_artifact` result says output was TRUNCATED (it hit the model limit), do NOT assume the write finished — `inspect_artifact` to see what landed, then append the rest with `patch_artifact`.

Rule of thumb: more than ~8–10 content-heavy frames, or any single frame with a lot of prose? Go skeleton-first + patch. It is the reliable path, not a fallback.

## Bundle shape

A valid bundle is a JSON object:

```jsonc
{
  "schemaVersion": 2,
  "artifactId": "client-overview",   // stable id, starts alphanumeric
  "title": "Client Overview",
  "frames": [ /* ordered, at least one */ ],
  "tokens": { "brand": "#e8482b" },   // optional CSS design variables → :root custom props
  "shared": {                          // optional code shared across ALL frames
    "files": { "deck.css": "…", "deck.js": "…" },
    "head": "<link rel=\"preconnect\" …>"
  },
  "libraries": ["motion"],            // optional curated runtimes: three | d3 | motion
  "display": { "background": "neutral" }   // optional: neutral | transparent
}
```

### Frames

Each frame:

```jsonc
{
  "id": "cover",                 // stable, unique within the artifact
  "title": "Cover",              // optional human label shown above the frame
  "layout": "slides",            // slides | pages | artboard | app | whiteboard
  "geometry": { "width": 1280, "height": 720 },
  "content": "<section>…</section>",  // the frame's HTML FRAGMENT (no <html>/<body>)
  "background": "transparent"    // optional per-frame override
}
```

**Letterhead, running headers & footers — use general CSS Paged Media.** A `pages` document is plain HTML/CSS — design it as richly as you like (full-bleed colored bands, logos, zebra-striped tables, multi-column footers). To make content **repeat on every page** (a letterhead, running header, footer, page numbers), use **standard CSS Paged Media** in a `shared` CSS file — the open, fully-flexible mechanism (Paged.js renders it). This is the PRIMARY way; aim for a real designed letterhead, not a plain text line.

- Mark an element as *running* and pull it into a page-margin box. Put the element once at the top of the frame `content`; it renders in the margin of EVERY page:
  ```css
  .letterhead { position: running(lh); }
  @page {
    margin: 132px 64px 90px;                /* room for the running header/footer */
    @top-center    { content: element(lh); }
    @bottom-center { content: element(ft); }
    @bottom-right  { content: "Page " counter(page) " of " counter(pages); }
  }
  ```
  You get the full surface: any of the 16 margin boxes (`@top-left`/`@bottom-right`/…), `:first`/`:left`/`:right` page selectors, named pages, counters.
- **Full-bleed branded band** (a colored letterhead that runs edge-to-edge): set the side page margins to `0`, give the running element a background + fixed height, and pad the body itself:
  ```css
  @page { margin: 104px 0 64px; @top-center { content: element(band); } }
  .band { position: running(band); height:104px; background:#16315c; color:#fff;
          display:flex; justify-content:space-between; align-items:center; padding:0 64px; }
  .doc  { padding: 0 64px; }                /* the body supplies its own side gutters */
  ```
- Anything you author this way **overrides the platform defaults** (the default margins + the auto page number) — you are in full control.

The platform also offers two **optional shortcuts** for the simple case; any author CSS Paged Media above overrides them, and for a full-bleed band or any non-trivial layout you should use CSS Paged Media, not these:

- **`furniture`** (a `pages` frame field) — a plain inset running header/footer: `{ header?, footer? }`, each with `first` (page 1) / `middle` (the rest + fallback) / `last` (final page) HTML. The platform tucks the header into the top margin and the footer into the bottom and never lets them overlap the body. Inset to the content column (not full-bleed). Use only when you want a plain running line and don't want to write `@page` CSS. `furniture` positions itself from `pageMargins` (or the defaults) — so if you set your own `@page { margin }` in CSS, don't also use `furniture`; put your header/footer in `@page` margin boxes instead.
- **`pageMargins`** (a `pages` frame field, px per side; absent sides default to top 96 / sides 72 / bottom 84) — a shortcut for `@page { margin: … }`.

- **`content` is a fragment**, not a full document. The platform assembles ONE sandboxed document: it injects `tokens` as `:root` custom properties, the `shared.head`, every `shared.files` stylesheet/script, then each frame's `content`. So a class defined in a `shared` CSS file is available to every frame — you do not write `<link>`/`<style>`/`<head>` yourself (put shared CSS/JS in `shared.files`, not inline `<style>` repeated per frame).
- **Order is the meaning**: it is the present/export/pagination sequence. Reordering is a list move.
- Use stable `data-sg-name` / `data-sg-role` attributes on important elements (e.g. `data-sg-name="Pricing table"`) so the user can click to select them and ask for scoped edits in natural language.

### Layouts

- **`slides`** — a fixed-size deck frame (e.g. `1280×720` or `1920×1080`), stepped one at a time in present. One frame = one slide.
- **`pages`** — a print/document frame. Give it a **fixed page-box width AND height** (US-Letter `816×1056`; Legal `816×1344`; Tabloid `1056×1632`; A4 `794×1123`; A3 `1123×1587`; A5 `559×794` — all @96dpi portrait; swap width/height for landscape) and put the **whole document body** in ONE `pages` frame's `content`. The platform **flows the text across as many pages as needed**, numbers them ("Page N of M"), and exports a real multi-page PDF. A fixed `pages` box does NOT crop — overflow flows onto the next page. Use `break-before: page` to force a new page (e.g. a new section/exhibit) and `break-inside: avoid` to keep a block together; for repeating letterhead/headers/footers style with **CSS Paged Media** (see "Letterhead, running headers & footers" above). This is the model for letters, contracts, proposals, agreements, and multi-page reports. (A `pages` frame with `height: "auto"` is the exception: it is NOT paginated — it renders as one tall continuous page. Use it only for a deliberately single, scroll-not-paginate document.)
- **`artboard`** — a fixed-size design surface: banner, social graphic, diagram, poster.
- **`app`** — a full-bleed responsive prototype / app screen. Use `geometry: { "width": "fill", "height": "fill" }`; the frame spans the viewport and its content reflows. An `app`/`fill` artifact is single-frame.
- **`whiteboard`** — one large pannable freeform surface (e.g. `2400×1600`). Opens at 100% centered and pans; no stepper. The platform draws a dot-grid surface; you place content (notes, diagrams, images) absolutely.

### Geometry

`width` is a number (px) or `"fill"`. `height` is a number, `"auto"` (content-driven), or `"fill"`. `"fill"` = span the available viewport (single-frame `app`).

**On a `pages` frame, a fixed page-box height does NOT crop — it paginates.** A multi-page document (letter, contract, proposal, report) is a `pages` frame with a fixed page-box width AND height (e.g. `816×1056`); the platform flows the overflow onto more pages. This is almost always what you want for a written document, so it can print on letterhead with page numbers. `height: "auto"` on a `pages` frame turns pagination OFF (one tall continuous page) — use it only when you deliberately want a single non-paginated scroll.

**On a non-`pages` frame, a fixed numeric height CROPS content taller than it** — the frame is a hard box with `overflow:hidden`, so anything past the declared height is clipped from view. For a content-driven `app`/`artboard` surface whose length you can't predict, declare `height: "auto"` so it grows to fit; do NOT guess a tall fixed number (e.g. `4000`) "to be safe". Use a fixed numeric height for genuinely fixed surfaces (a `slides` deck, a poster/banner `artboard`) where the canvas size IS the design.

## Sizing — author at natural 1:1

Frames render at their **declared pixel size**, and the host opens them at **100% (1:1)** by default. Size content the way you would for a real 1:1 screen — do NOT inflate fonts/elements for a large surface:

- **slides**: the geometry (e.g. 1920×1080) IS the design size; size type to read on a projected slide.
- **artboard / pages**: the declared geometry is the 1:1 design surface; don't inflate beyond what reads at that size.
- **whiteboard**: body text ~14–18px, section titles ~28–32px, post-its ~200px — not 60px+ titles. If a single element is wider than ~⅓ of the canvas or its body text is over ~20px, it is probably over-scaled. Cluster primary content near the centre (visible on open), secondary material spread out to pan to.

## Whiteboard theming — use the ink tokens

The whiteboard is the **one** surface the platform themes: its paper + dot-grid follow the user's light/dark setting. `slides`/`pages`/`artboard`/`app` are author-designed end to end and are **not** themed. So whiteboard content drawn directly on the bare surface must adapt, or "default ink" authored for light paper goes invisible on the dark surface. Opt into the platform tokens for ink on the bare surface:

- `var(--sg-ink)` — primary foreground: connector lines, outlines, line-art, monochrome icons, body text, the title.
- `var(--sg-ink-muted)` — secondary: eyebrows, labels, hairlines, captions.
- `var(--sg-surface)` — the paper color, for a panel/shape that should match the board.

For SVG line-art, set `color: var(--sg-ink)` on the `<svg>` and use `currentColor` for `stroke`/`fill` (presentation attributes can't take `var()`). **Do NOT theme intentional content**: anything with its OWN background — a colored post-it, a photo, a filled callout — keeps its own colors and pins its own (usually dark) text; don't let it inherit `--sg-ink` or it turns light-on-light in dark mode.

## JavaScript & libraries

JS runs only inside a sandboxed iframe. Write self-contained JS in `shared.files` (e.g. `deck.js`); it runs after any requested libraries are ready. Do not rely on network access, external packages, cookies, localStorage, parent-window access, downloads, forms, or navigation. For 3D/data-viz/animation, request a curated local runtime instead of a CDN:

- `"three"` exposes `window.THREE`; `"d3"` exposes `window.d3`; `"motion"` exposes `window.Motion`.

Do not request framework/component kits (shadcn, Bits UI, React, Tailwind runtime). Build components as semantic HTML + self-contained CSS.

## Constraints

- Keep the whole bundle under 512KB.
- Do not reference external URLs for scripts, styles, fonts, images, APIs, or tracking.
- Custom fonts may be referenced only from uploaded Studiograph asset URLs (`/api/assets/…`) or `data:` URLs (declare `@font-face` in a `shared` CSS file or `shared.head`).
- Use embedded SVG, CSS shapes, data URIs, or uploaded asset URLs for imagery.

## Example — a slide deck

```json
{
  "schemaVersion": 2,
  "artifactId": "client-overview",
  "title": "Client Overview",
  "tokens": { "brand": "#e8482b", "ink": "#16110f" },
  "shared": {
    "files": {
      "deck.css": "*{box-sizing:border-box;margin:0}.slide{width:100%;height:100%;padding:80px;font-family:Inter,system-ui;color:var(--ink);display:flex;flex-direction:column;justify-content:center;gap:24px}.tag{color:var(--brand);font-weight:600;text-transform:uppercase;letter-spacing:.08em;font-size:24px}h1{font-size:84px;line-height:1.02}h2{font-size:54px}p{font-size:30px;color:#5a514d;max-width:62ch}"
    }
  },
  "frames": [
    { "id": "cover", "title": "Cover", "layout": "slides", "geometry": { "width": 1280, "height": 720 },
      "content": "<div class=\"slide\"><div class=\"tag\">Overview</div><h1 data-sg-name=\"Cover title\">Client Overview</h1><p>Q3 engagement summary.</p></div>" },
    { "id": "approach", "title": "Approach", "layout": "slides", "geometry": { "width": 1280, "height": 720 },
      "content": "<div class=\"slide\"><div class=\"tag\">Approach</div><h2>How we work</h2></div>" }
  ]
}
```

Each slide is one `slides` frame; the shared `deck.css` styles every frame; the platform steps and exports them — there is no hand-rolled navigation or grid.

## Example — a flowing letterhead document (full-bleed band via CSS Paged Media)

A letter, contract, or proposal is ONE `pages` frame at a fixed page size; the `content` is the whole body and the platform flows it across pages. The letterhead is a **running element** placed in a `@page` margin box, so it repeats on every page; side margins are `0` so the band runs edge-to-edge, and the body supplies its own gutters. (Page numbers come from the `@bottom-right` counter; `.foot` is a running footer.)

```json
{
  "schemaVersion": 2,
  "artifactId": "offer-letter",
  "title": "Offer of Employment",
  "shared": {
    "files": {
      "doc.css": "*{box-sizing:border-box}body{margin:0;font:14px/1.6 -apple-system,Segoe UI,Roboto,sans-serif;color:#1f2937}.doc{padding:28px 64px}h2{color:#16315c;font-size:13px;letter-spacing:.06em;text-transform:uppercase;border-bottom:1px solid #e5e7eb;padding-bottom:6px;margin:22px 0 12px}p{margin:0 0 12px}table{width:100%;border-collapse:collapse;font-size:13px}td{padding:9px 12px;border-bottom:1px solid #eef2f7}tr:nth-child(odd) td{background:#f7f9fc}.band{position:running(band);height:104px;background:#16315c;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 64px}.brand{display:flex;align-items:center;gap:14px}.logo{width:40px;height:40px;border-radius:8px;background:#e8492b;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:22px}.brand b{font-size:22px}.brand small{display:block;color:#9fb3d1;font-size:10px;letter-spacing:.12em}.addr{text-align:right;font-size:11px;color:#cdd9ea}.foot{position:running(foot);display:flex;justify-content:space-between;font-size:10px;color:#9ca3af;padding:0 64px}.foot .c{color:#e8492b;font-weight:600}.exhibit{break-before:page}@page{size:816px 1056px;margin:104px 0 70px;@top-center{content:element(band)}@bottom-center{content:element(foot)}@bottom-right{content:\"Page \" counter(page) \" of \" counter(pages)}}"
    }
  },
  "frames": [
    {
      "id": "letter",
      "layout": "pages",
      "geometry": { "width": 816, "height": 1056 },
      "content": "<div class=\"band\"><div class=\"brand\"><div class=\"logo\">A</div><div><b>Acme Inc.</b><small>PEOPLE &amp; TALENT</small></div></div><div class=\"addr\">1 Acme Plaza, Suite 800<br>San Francisco, CA 94105<br>hr@acmeinc.com</div></div><div class=\"foot\"><span class=\"c\">Confidential</span><span>Offer Letter — Jordan Lee</span><span>June 17, 2026</span></div><div class=\"doc\"><p style=\"color:#6b7280\">June 17, 2026</p><p><b>Jordan Lee</b><br>742 Evergreen Terrace<br>Portland, OR 97201</p><p>Dear Jordan,</p><p>On behalf of the entire team at Acme Inc., it is our great pleasure to extend this offer…</p><h2>Position Details</h2><table><tr><td>Job Title</td><td>Senior Product Manager</td></tr><tr><td>Start Date</td><td>July 14, 2026</td></tr></table><section class=\"exhibit\"><h2>Conditions of Employment</h2><p>…</p></section></div>"
    }
  ]
}
```

ONE `pages` frame, fixed `816×1056`; the platform paginates the body into as many pages as the content needs and the running band + footer repeat on every page. For a different size set `geometry` to a preset (A4 `794×1123`, Tabloid `1056×1632`, …). Never make one frame per page, and never simulate pages with a CSS grid. (For just a plain inset header/footer with no `@page` CSS, the `furniture` shortcut is the lighter option.)
