Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,12 @@ Create a new canvas. If `projectId` is omitted, it lands in the built-in `Untitl
|-------|------|-------------|
| `name` | string? | Canvas name |
| `projectId` | string? | Target project. Defaults to the built-in Untitled project. See `project_list`. |
| `width` | number? | Artboard width in px, a positive integer (default 1440). A phone screen is `width: 390, height: 844`. |
| `height` | number? | Artboard height in px, a positive integer (default 900). Each dimension defaults on its own. |
| `width` | number? | Artboard width in px, a positive integer (default 1440, or the device width for `type: "mobile"`). |
| `height` | number? | Artboard height in px, a positive integer (default 900, or the device height for `type: "mobile"`). Each dimension defaults on its own. |
| `type` | `"web"` \| `"mobile"`? | What medium the canvas is on. `"mobile"` declares a phone app screen: the artboard defaults to the device size and the root gets the device's safe-area insets as `safeArea: { top, bottom }`, which the renderer adds to the root's top and bottom padding. Omitted, the canvas is undeclared and judged as web. |
| `device` | string? | Device profile for `type: "mobile"` (default `"iphone-390"`, the only profile today: 390×844, insets 47 top and 34 bottom). Passing it without `type: "mobile"`, or naming an unknown device, is an error. An explicit `width`/`height` overrides the profile's size for that side; the insets stay the profile's. |

The response also carries the artboard `width`/`height` and a `diversification` signal for the target project: the recently-built structures (newest first) and a hint to differ on at least one taxonomy axis, so successive canvases don't converge on the same layout. It's advisory — never blocking.
The response also carries the artboard `width`/`height`, `type`, `typeSource` (`"declared"` or `"default"`), `device` and `safeArea` for a mobile canvas, and a `diversification` signal for the target project: the recently-built structures (newest first) and a hint to differ on at least one taxonomy axis, so successive canvases don't converge on the same layout. It's advisory — never blocking.

### `canvas_list`

Expand Down Expand Up @@ -306,7 +308,7 @@ R("nodeId", { type: "text", content: "Replaced" })

**Node types:** `frame`, `text`, `rectangle`, `ellipse`, `image`, `icon`, `path`, `component`, `instance`, `toggle`, `checkbox`, `radio`, `select`, `chart`, `skeleton` (loading-placeholder block — token-derived neutral fill; pulses subtly in the live viewer, always static in screenshots/exports so diffs stay deterministic; `pulse: false` opts a block out)

**Properties:** `fill`, `gradient`, `stroke`, `strokeWidth`, `strokeStyle`, `borderTop`, `borderRight`, `borderBottom`, `borderLeft`, `cornerRadius`, `width`, `height`, `minWidth`, `minHeight`, `maxWidth`, `layout` (`"horizontal"` | `"vertical"` | `"grid"`), `gap`, `rowGap`, `gridColumns`, `gridColumn`, `gridRow`, `padding`, `alignItems`, `justifyContent`, `fontSize`, `fontFamily`, `fontWeight`, `color`, `content`, `textAlign`, `lineHeight`, `letterSpacing` (px), `textDecoration`, `textTransform`, `textOverflow` (`"ellipsis"` — designed single-line truncation; `canvas_stress` reports clips behind it as info), `tabularNums`, `fontVariationSettings`, `src`, `objectFit`, `opacity`, `shadow` (CSS string, or `"$elevation.<name>"` referencing an elevation token), `shadows`, `blur`, `backdropBlur`, `backdropFilter`, `overflow`, `wrap`, `position`, `x`, `y`, `icon`, `iconSize`, `iconColor`, `iconStyle`, `checked`, `disabled`, `value`, `d`, `viewBox`, `strokeLinecap`, `strokeLinejoin`, `strokeDasharray`, `animation`, `transition` (object, or `"$motion.<name>"` referencing a motion token), `kind`, `series`, `segments`, `innerRatio`, `centerValue`, `centerLabel`, `sparkKind`, `xDomain`, `yDomain`, `curve`, `gridlines`, `xLabels`, `yLabels`, `componentId`, `overrides`, `name` (a readable label — `find_nodes` and component overrides match on it), `pulse` (skeleton only), `responsive` (`"stack"` | `"wrap"`)
**Properties:** `fill`, `gradient`, `stroke`, `strokeWidth`, `strokeStyle`, `borderTop`, `borderRight`, `borderBottom`, `borderLeft`, `cornerRadius`, `width`, `height`, `minWidth`, `minHeight`, `maxWidth`, `layout` (`"horizontal"` | `"vertical"` | `"grid"`), `gap`, `rowGap`, `gridColumns`, `gridColumn`, `gridRow`, `padding`, `alignItems`, `justifyContent`, `fontSize`, `fontFamily`, `fontWeight`, `color`, `content`, `textAlign`, `lineHeight`, `letterSpacing` (px), `textDecoration`, `textTransform`, `textOverflow` (`"ellipsis"` — designed single-line truncation; `canvas_stress` reports clips behind it as info), `tabularNums`, `fontVariationSettings`, `src`, `objectFit`, `opacity`, `shadow` (CSS string, or `"$elevation.<name>"` referencing an elevation token), `shadows`, `blur`, `backdropBlur`, `backdropFilter`, `overflow`, `wrap`, `position`, `x`, `y`, `icon`, `iconSize`, `iconColor`, `iconStyle`, `checked`, `disabled`, `value`, `d`, `viewBox`, `strokeLinecap`, `strokeLinejoin`, `strokeDasharray`, `animation`, `transition` (object, or `"$motion.<name>"` referencing a motion token), `kind`, `series`, `segments`, `innerRatio`, `centerValue`, `centerLabel`, `sparkKind`, `xDomain`, `yDomain`, `curve`, `gridlines`, `xLabels`, `yLabels`, `componentId`, `overrides`, `safeArea` (`{ top, bottom }`, document root only — insets the renderer adds to the root's top and bottom padding; `canvas_create` with `type: "mobile"` writes the device's), `name` (a readable label — `find_nodes` and component overrides match on it), `pulse` (skeleton only), `responsive` (`"stack"` | `"wrap"`)

Chart `series` entries take their own keys: `data`, `stroke`, `strokeWidth`, `strokeDasharray`, `area`, `points`, `highlight`, `barGradient`.

Expand Down Expand Up @@ -466,7 +468,7 @@ The headline call for a new project's look: **one seed color + one personality
|-------|------|-------------|
| `seed` | string | The brand color everything derives from (`#RRGGBB`) |
| `personality` | string | **Required** — `technical`, `editorial`, `soft`, or `data-dense`. Dashboards → `data-dense`/`technical`, marketing/content → `editorial`, consumer → `soft` |
| `baseSize` / `ratio` | number? | Override the personality's type pivot / scale ratio |
| `baseSize` / `ratio` | number? | Override the type pivot / scale ratio. On a canvas declared `type: "mobile"` the pivot defaults to the device body size (17px; 16px for `data-dense`); `baseSize` still wins. The result reports the pivot as `baseSize` + `baseSizeSource` (`"explicit"`, `"personality"` or `"mobile"`). |
| `canvasId` / `projectId` / `workspaceId` | string? | Exactly one — the token layer written to |
| `preserveInherited` | boolean? | Canvas scope only. Default `true` — see "Inherited tokens" below. Pass `false` to write the generated language whole. |

Expand Down Expand Up @@ -797,6 +799,18 @@ Durably declare what a canvas *is* — writes the evaluation genre to `metadata.

Returns `{ canvasId, genre, canonical, previous, relaxes, presetRelaxes?, note?, versionHashUnchanged }` — `canonical` is the genre with its alias folded (`"dashboard"` → `"data"`, `"checkout"` → `"commerce"`, what evaluate reports); `relaxes` lists the cliché tells the stamp relaxes; `presetRelaxes` (`{ preset, relaxed }`) appears when the canvas's preset relaxes tells on its own (the material preset), which a genre clear doesn't remove; an unknown genre is stored but relaxes nothing, and the `note` says so immediately. Other provenance facts (structure, `importedFrom`) are preserved, and the stamp never moves the canvas's `versionHash` (the hash covers design content only) — recorded approvals stay valid.

### `canvas_set_type`

Declare what medium an existing canvas is on. Writes `metadata.surface` only: it never resizes the artboard and never writes the root's `safeArea`, because either would reflow what is already placed. For a new phone screen, `canvas_create({ type: "mobile" })` does all of it at once.

| Param | Type | Description |
|-------|------|-------------|
| `canvasId` | string | Canvas to declare |
| `type` | `"web"` \| `"mobile"` \| null | The medium; `null` clears the declaration (the canvas reads as undeclared web again) |
| `device` | string? | Device profile for `"mobile"` (default `"iphone-390"`). Only valid with `"mobile"`. |

Returns `{ canvasId, type, device, source, previous, artboard, hint?, versionHashUnchanged }`. `previous` is the declaration it replaced (or `null`); `artboard` is the root's unchanged size. Clearing the type leaves the root, including any `safeArea`, as it is. On a mobile result whose root doesn't reserve both insets or has a size other than the device's, `hint` names the ops that finish the job: `U("document", { safeArea: { top: 47, bottom: 34 } })` and `U("document", { width: 390, height: 844 })`. Those ops change the root, so they move the `versionHash`; the declaration alone is metadata and doesn't. It changes the evaluate result's `surface` report (and, as mobile checks register, which rules run), so re-evaluate after it.

### `canvas_evaluate`

Auto-score a design against quality heuristics. Returns an overall score (0–100), per-category scores, per-node actionable issues, and a **`directive`** — a present/keep-working verdict. Designed for generator-evaluator loops: build with `batch_design`, score with `canvas_evaluate`, fix the issues targeting the returned `nodeId`s, repeat until the directive says `READY`.
Expand All @@ -809,6 +823,8 @@ A `FRAGILE` stress run blocks too. Once `canvas_stress` has recorded a verdict f

Whenever the `cliche` category runs, the result also carries a **`genre`** field auditing the genre decision: `{ active, source, relaxed, notRelaxed, byPreset? }` — the genre in effect (canonical: a `"dashboard"` stamp reports `"data"`, a `"checkout"` one `"commerce"`), whether it came from the `genre` param or the canvas's provenance stamp, the tells relaxed, `notRelaxed` (`[{ tell, relaxedBy }]`) — tells still flagging that a *different* genre would relax — and `byPreset` (`{ preset, relaxed }`), present when the applied preset relaxed tells on its own (the material preset relaxes accent-hue and pure-black-white whatever the genre). A score pinned by tells listed in `notRelaxed` usually means the wrong genre was declared.

Every result also carries a **`surface`** field: `{ type, device, source, rulesOn, rulesOff, conflicts }` — the canvas type (`"web"` or `"mobile"`), its device profile (`null` on web), whether the type was `"declared"` (by `canvas_create` or `canvas_set_type`) or is the `"default"` web of an undeclared canvas, and the rule ids the composed type, genre and preset rulesets turned on (`rulesOn`) and off (`rulesOff`). A rule both enabled and disabled stays on: it is listed in `rulesOn` and in `conflicts`. No rule ids are registered yet, so both lists are empty today. `canvas_autofix` returns the same field.

Whenever the `coverage` category runs on a **base** canvas, the result also carries a **`coverage`** field: `{ dataBearing, states, missing }` — whether the screen carries data-bearing content, which state variants (`canvas_add_variant`) already exist, and which demanded states are missing. It's absent on variant canvases (a variant *is* a designed state, so it doesn't demand states of itself).

| Param | Type | Description |
Expand Down
2 changes: 1 addition & 1 deletion VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ _Full spec-driven breakdown in [`docs/specs/PHASE-29-SPEC.md`](docs/specs/PHASE-
The mobile attempt (2026-09-07) built one iOS screen end to end on v2.2.0 and got 100/100, READY TO PRESENT, without a single mobile check running: the touch-target rule only looks at form controls, and the screen had none. Mobile today is a screen size, not a surface. This phase gives a canvas a declared `type` (`web` or `mobile`), separates it from genre and preset so the three declarations stop colliding, and builds the three layers a surface needs: rules that judge a phone, vocabulary to build one, and starting points. Acceptance is literal: the attempt screen rebuilt from a mobile archetype, CLEAN with every tap target a primitive.

- [x] Slice A — untangle the declarations: genre gets its own field so `apply_preset` stops overwriting it, every stored canvas keeps its verdict through a legacy read, and `data` becomes the canonical purpose genre with `dashboard` as its alias
- [ ] Slice B — the type axis: `metadata.surface` with one device profile (`iphone-390`), `canvas_create({ type })` and `canvas_set_type`, a ruleset table keyed by type, genre and preset that composes by union and can turn rules on, a `surface` report on every evaluation, and a mobile type scale from `generate_design_system`
- [x] Slice B — the type axis: `metadata.surface` with one device profile (`iphone-390`), `canvas_create({ type })` and `canvas_set_type`, a ruleset table keyed by type, genre and preset that composes by union and can turn rules on, a `surface` report on every evaluation, and a mobile type scale from `generate_design_system`
- [ ] Slice C — the mobile ruleset: 44px targets as an error on every tappable primitive, safe areas, a type floor, undeclared phone-width canvases told to declare, hand-built tap targets and multi-column prose named as advisories, and stress that knows the type
- [ ] Slice D — vocabulary: `fixed` / `sticky` positioning, `safeArea` on the root, and the `status-bar`, `home-indicator`, `nav-bar`, `tab-bar`, `button`, `list-row` and `sheet` primitives
- [ ] Slice E — starting points: anatomy and realisation on structures, resolution by canvas type, six mobile realisations, and the pattern gate extended to them
Expand Down
15 changes: 13 additions & 2 deletions docs/GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ Canvases live inside a `Workspace > Project > Canvas` hierarchy. The built-in `P

- **`workspace_create({ name })`** — top-level container (e.g. "Client work", "Personal").
- **`project_create({ workspaceId, name })`** — group related canvases inside a workspace.
- **`canvas_create({ name, projectId, width?, height? })`** — drops the canvas in the given project (defaults to Untitled). The artboard is 1440×900 unless you pass `width`/`height` (positive integers, each defaulting on its own) — e.g. `width: 390, height: 844` for a phone screen.
- **`canvas_create({ name, projectId, width?, height?, type?, device? })`** — drops the canvas in the given project (defaults to Untitled). The artboard is 1440×900 unless you pass `width`/`height` (positive integers, each defaulting on its own). For a phone app screen pass `type: "mobile"` instead of sizing it by hand (see [Declaring the medium](#declaring-the-medium-web-or-mobile)).
- **`canvas_move({ canvasId, projectId })`** — reassign a canvas between projects.
- **`canvas_archive` / `canvas_unarchive`** — soft-delete: canvas stays on disk but hides from default listings. Reach for this when iterating; reach for `canvas_delete` (permanent) only when sure.

`workspace_delete` and `project_delete` refuse to remove non-empty containers. Clear the contents first or `canvas_move` them out.

## Two-line summary

1. **Author desktop-first at one design width.** Pick a width (1200 or 1440 is typical for a web page; a phone screen is its own artboard, `canvas_create({ width: 390, height: 844 })`), compose the design there.
1. **Author desktop-first at one design width.** Pick a width (1200 or 1440 is typical for a web page; a phone app screen is its own artboard, `canvas_create({ type: "mobile" })`), compose the design there.
2. **Adapt down with `responsive` hints + fluid widths.** The renderer derives the mobile/tablet layout from the same scene graph — you don't author it twice.

## Width strategies
Expand All @@ -36,6 +36,17 @@ Pick the right `width` per node — this is the single biggest lever for respons

**Artboard height is a viewport, not a limit.** The root's `height` sizes the default `screenshot` capture, but content taller than it is normal — it scrolls on a real page, and a `clip` finding from `canvas_stress` for exactly that case is `info`, not a problem to fix. To actually see past the artboard (a long page, a design that grew under stress content), pass `fullPage: true` to `screenshot` — or to `export`, which takes the same option — instead of editing the root height by hand.

## Declaring the medium (web or mobile)

A canvas declares what medium it is on, separately from its genre (what it is for) and its preset (what style it uses). The declaration selects a ruleset and the defaults the generators use. The declaration itself never changes a pixel: what renders comes only from the scene, so the artboard size and `safeArea` that `canvas_create` writes into the root are what change the render.

- **A phone app screen is `canvas_create({ type: "mobile" })`**, not a 390px-wide web canvas. It gets the device artboard (390×844 for the default `device: "iphone-390"`, the only profile today) and the root reserves the device's insets as `safeArea: { top: 47, bottom: 34 }`. An explicit `width` or `height` wins for that side; the insets and the device stay the profile's. Passing `device` without `type: "mobile"`, or an unknown device, is an error. `canvas_create` reports `type`, `typeSource` (`"declared"` or `"default"`), `device` and `safeArea`.
- **The renderer adds the insets to the root's own top and bottom padding**, at exact pixels even when the authored padding scales fluidly (padding of 32px or more), and the root's fill paints the bands, so in-flow content never sits under the status bar or the home indicator. A root with `padding: 24` starts its first child 71px down (24 + 47). `safeArea` is part of the root, so changing it moves the `versionHash`.
- **`type: "web"` is the default.** An undeclared canvas is judged as web and its evaluate result says `source: "default"`.
- **`canvas_set_type` re-declares an existing canvas** (`"web"`, `"mobile"`, or `null` to clear). It writes the declaration only, so it never moves the `versionHash`. It never resizes the artboard and never writes or removes `safeArea`, because either would reflow what you placed; clearing the type leaves the root as it is. When a mobile result has a `hint`, follow it: it names `U("document", { safeArea: { top: 47, bottom: 34 } })` when the root doesn't reserve both insets, and `U("document", { width: 390, height: 844 })` when the root is another size. Both ops change the render, so re-check the layout after them.
- **`generate_design_system` on a mobile canvas** pivots the type scale on the device body size: 17px for every personality except `data-dense`, which uses 16px. An explicit `baseSize` wins. The result reports the generated pivot as `baseSize` and `baseSizeSource` (`"mobile"` for both mobile pivots, `"explicit"`, or `"personality"` on web). A `$body` inherited from a workspace or project design system is preserved by default and listed in `designSystemConflicts`; pass `preserveInherited: false` to take the mobile body.
- **Every evaluate result carries a `surface` report**: `{ type, device, source, rulesOn, rulesOff, conflicts }`. Each axis value (type, genre, preset) contributes a ruleset, and the active rules are their union. `rulesOn` and `rulesOff` name the rule ids the rulesets turned on and off. A rule both enabled and disabled stays on: it is listed in `rulesOn` and in `conflicts`, so composing declarations can never switch a guardrail off. No rule ids are registered yet, so both lists are empty today; the mobile checks register theirs as they land.

## Responsive hints

Set `responsive` on **container nodes** (frames with `layout: "horizontal"` or `layout: "grid"` and children). The renderer emits the right media-query rules — flex-wrap for horizontal containers, a single-column collapse with spans reset for grid.
Expand Down
Loading
Loading