# Canvas Intake Recipe — rendering Claude-Design artboards to PNG

Deterministic recipe for capturing the screens of a Claude Design feature canvas
that exist **only** as artboards inside the canvas HTML (not exported as standalone
PNGs). Replaces the ad-hoc "spin up a server, debug CORS, script Playwright" dance
with a repeatable procedure.

## When this runs

Invoked by [discovery-phase.md](discovery-phase.md) § "Step 1.6.4b → b2 → A5",
once per resolved `canvas_ref`, only for slots lacking a confident pre-render PNG.
**Never run standalone** — it assumes A0–A4 already chose the canvas and its slot
set. Outputs feed `mockups.canvas_render` in the state file.

## Why a local HTTP server is mandatory

The canvas HTML loads its screens via Babel:
`<script type="text/babel" src="design-canvas.jsx">` (+ the feature's `*.jsx`
deps). Opening the file as `file://` makes the browser **block** those `src`
fetches under the CORS/`file:` origin policy → Babel never executes → the artboard
DOM is never created → `[data-dc-slot]` / `[data-dc-section]` do not exist. This is
exactly the `slots=0` failure seen when rendering from `file://`. Serving `root`
over `http://` lets the jsx load and Babel render.

> Slot attributes are **runtime-only**: the static HTML has `<DCSection id="s31">`
> JSX; the `data-dc-section` / `data-dc-slot` attributes only appear after Babel
> runs on the served page. Intake-time slot enumeration greps `id="s<N>"` (static);
> rendering selects `[data-dc-section]` / `[data-dc-slot]` (runtime).

## Selectors

| Selector              | Granularity                  | Source JSX        |
|-----------------------|------------------------------|-------------------|
| `[data-dc-section="s3x"]` | one **screen** (3.x)      | `<DCSection id>`  |
| `[data-dc-slot="…"]`  | one **artboard / variant** within a screen | `<DCArtboard id>` |

Capture at screen granularity (`[data-dc-section]`) by default; drop to
`[data-dc-slot]` only when a screen needs per-variant PNGs.

## Preflight (degrade, never auto-install)

```bash
npx playwright --version    # must succeed
npx http-server --version   # must succeed
```

If either is missing: do **NOT** auto-install (this runs in consumer repos —
respect their toolchain). Emit a one-line hint
(`npm i -D playwright http-server && npx playwright install chromium`), set
`mockups.canvas_render.render_status: skipped`, and return control to A5, which
degrades to pre-render PNGs + `source: live-html`.

## Serve

Pick a free port (loop 8080→8090, advancing on `EADDRINUSE`):

```bash
npx http-server "$root" -p <PORT> -c-1 --silent   # run in background; -c-1 disables caching
```

Record `mockups.canvas_render.served_port: <PORT>`.

## Render loop (Playwright)

```js
// /tmp/canvas-shoot.mjs — node, run with the project's playwright
import { chromium } from 'playwright';
const PORT = process.env.PORT, CANVAS = process.env.CANVAS, OUT = process.env.OUT;
const SLOTS = JSON.parse(process.env.SLOTS); // [{ slot_id, screen_id }]
const browser = await chromium.launch();
const page = await browser.newPage({ deviceScaleFactor: 2 }); // ≥2x export
await page.goto(`http://127.0.0.1:${PORT}/${encodeURIComponent(CANVAS)}`);
await page.waitForSelector('[data-dc-section]', { timeout: 15000 }); // confirms Babel ran
await page.waitForLoadState('networkidle');
for (const { slot_id, screen_id } of SLOTS) {
  const el = await page.$(`[data-dc-section="${slot_id}"]`) || await page.$(`[data-dc-slot="${slot_id}"]`);
  if (!el) { console.log(`MISS ${slot_id}`); continue; }
  await el.screenshot({ path: `${OUT}/${screen_id.replace('.', '-')}.png` }); // <slug>__3-1.png upstream
  console.log(`OK ${slot_id} -> ${screen_id}`);
}
await browser.close();
```

Name outputs `<slug>__<screen-id>.png` (e.g. `conferma-ordine__3-1.png`). Record
each in `mockups.canvas_render.rendered_slots[]`.

## Teardown

Kill the background `http-server` (by PID). Set `render_status`:
- `ok` — every requested slot captured.
- `partial` — some slots captured, some `MISS` (record which).
- `failed` — `waitForSelector('[data-dc-section]')` timed out (Babel never ran:
  offline CDN, broken jsx). Degrade as in preflight.

## Dedup vs designer pre-renders

If a slot has BOTH a designer pre-render (A4) and a freshly rendered PNG, **prefer
the designer's pre-render** — keep our render only if it differs materially in
dimensions/content, and then name it `<screen-id>__rendered.png`. Never ship two
PNGs for the same screen state.

## Failure → degradation summary

| Failure                          | Detection                          | Degradation                                      |
|----------------------------------|------------------------------------|--------------------------------------------------|
| Playwright / http-server missing | preflight version check fails      | `skipped` → pre-render + `live-html`, install hint |
| All ports busy                   | EADDRINUSE through 8090            | try an ephemeral port; else `skipped`            |
| Babel never runs (CORS/offline)  | `[data-dc-section]` wait times out | `failed` → pre-render + `live-html`              |
| Some artboards missing           | `MISS` in loop                     | `partial` — capture the rest, log the misses     |

Global rule: **never abort the PRD**. Every failure here returns to A5 with a
degraded `render_status`; intake proceeds with whatever PNGs exist plus live-HTML
references.
