{"slug": "framesmith-1-7-a-quality-gate-that-tells-an-ai-agent-when-a-ui-is-done", "title": "Framesmith 1.7 – a quality gate that tells an AI agent when a UI is done", "summary": "Framesmith 1.7, an open-source MCP server, provides AI coding agents with a visual canvas for UI design, enabling sketching, review, and approval before code generation. The tool includes a quality inspector that scores designs and highlights issues, and integrates with AI assistants via standard MCP protocol.", "body_md": "An open-source MCP server that gives your AI coding agent a visual canvas. Sketch the UI, review it in a browser, agree on the design — before any framework code gets written.\n\n**Contents:** [Viewer](#viewer) · [Installation](#installation) · [Tools](#tools) · [Usage Example](#usage-example) · [Workflow](#workflow) · [Development](#development)\n\nAbove: the framesmith viewer. Workspaces and projects in the sidebar, canvases as live thumbnails on the right. AI agents create canvases via MCP tools; you browse them like Figma files.\n\n```\nMCP Client → stdio → framesmith server\n                        ↓\n              Scene Graph (in-memory JSON tree)\n                        ↓\n              HTML/CSS Renderer (inline styles)\n                        ↓\n              Puppeteer (headless Chromium → PNG)\n```\n\nRun `npx -p framesmith framesmith-viewer`\n\nto start the standalone browser viewer (default port 3001). Open any canvas to review it at multiple breakpoints, compare them side-by-side, inspect the underlying JSON, or archive / delete.\n\nAbove: a single canvas in the detail view. The toolbar across the top exposes the breakpoint preview modes, Compare for side-by-side rendering, Fit for max-width, JSON for the raw scene graph, and lifecycle actions.\n\n**Quality panel.** The canvas detail view shows a read-only **quality inspector** on the right: the heuristic `canvas_evaluate`\n\nscore (0–100), per-category bars, and the issue list — each cliché tell with its `category · tell`\n\nbadge, severity, and suggestion. Issues that `canvas_autofix`\n\ncan resolve carry an **auto-fixable** tag, and clicking any issue **highlights its node** in the live preview. Every gallery card also shows a color-coded score badge so weak canvases stand out at a glance. The score matches what your agent sees over MCP (same fast-mode evaluation, genre-relaxed by the canvas's preset) — it's computed for display only and never written back.\n\n**Design-system panel.** A second inspector tab shows the canvas's effective design tokens — color swatches, type scale, spacing, and radius — resolved through the full workspace ▸ project ▸ canvas inheritance chain. Each section notes its dominant source layer, and any token that *overrides* it is tagged (`canvas`\n\n/ `project`\n\n) so you can see at a glance what a given canvas customized versus inherited.\n\nThe viewer is purely read-only — every canvas is authored through MCP tool calls from your AI assistant. Files persist to `~/.framesmith/canvases/`\n\nso the viewer keeps showing them across sessions.\n\nNo clone or build needed — register framesmith with your MCP client via `npx`\n\n(requires Node 20+).\n\n```\nclaude mcp add framesmith -- npx -y framesmith\n```\n\nAdd to `~/.codex/config.toml`\n\n:\n\n```\n[mcp_servers.framesmith]\ncommand = \"npx\"\nargs = [\"-y\", \"framesmith\"]\n```\n\nAdd to `~/.cursor/mcp.json`\n\n(or per-project `.cursor/mcp.json`\n\n):\n\n```\n{\n  \"mcpServers\": {\n    \"framesmith\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"framesmith\"]\n    }\n  }\n}\n```\n\nAdd to `~/.codeium/windsurf/mcp_config.json`\n\n:\n\n```\n{\n  \"mcpServers\": {\n    \"framesmith\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"framesmith\"]\n    }\n  }\n}\n```\n\nAdd to `.vscode/mcp.json`\n\n(project-scoped) or your global MCP settings:\n\n```\n{\n  \"servers\": {\n    \"framesmith\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"framesmith\"]\n    }\n  }\n}\n```\n\nframesmith speaks standard stdio MCP. Point your client at `npx -y framesmith`\n\nusing whatever config shape your client expects.\n\nOptional:set`FRAMESMITH_VIEWER_URL=http://localhost:3001`\n\nin the MCP server env to pin it to a long-lived standalone viewer process — see[Running the viewer].\n\n```\ngit clone https://github.com/vicmaster/framesmith.git\ncd framesmith\nnpm install\nnpm run build\n# then point your client at: node /path/to/framesmith/dist/index.js\n```\n\nOne-call onboarding — **the recommended first call each session**, and safe to run repeatedly (idempotent). Binds the current repo if it isn't already (canvases become checked-in JSON under `.framesmith/`\n\n), ensures the convention projects exist, and returns the live state you need to start working.\n\n| Param | Type | Description |\n|---|---|---|\n`dir` |\nstring? | Directory to bind / detect. Defaults to the nearest git repo root above the server working directory. |\n`workspaceName` |\nstring? | Name for the workspace when binding fresh. Defaults to the repo folder name. |\n`projects` |\nstring[]? | Projects to ensure exist (default: `[\"Foundations\", \"UI\"]` ). Existing projects are never removed, so it's safe for adding feature/area projects like `Onboarding` . |\n\nReturns the bound workspace + project IDs (binding **re-keys** IDs to `repo-*`\n\n— use the ones `init`\n\nreturns), the on-disk layout, the workspace-layer token count, a workflow cheatsheet, the current gotchas, the `framesmith://guidelines`\n\nURI, and the viewer URL. It does **not** seed design tokens — set those at the workspace layer with `workspace_set_design_system`\n\n. The default `Foundations`\n\nproject is just a canvas that visualizes the workspace tokens (which is where the design system actually lives).\n\nCreate a new canvas. If `projectId`\n\nis omitted, it lands in the built-in `Untitled`\n\nproject of the `Personal`\n\nworkspace.\n\n| Param | Type | Description |\n|---|---|---|\n`name` |\nstring? | Canvas name |\n`projectId` |\nstring? | Target project. Defaults to the built-in Untitled project. See `project_list` . |\n\nThe response also carries a `diversification`\n\nsignal 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.\n\nList canvases. Excludes archived canvases by default.\n\n| Param | Type | Description |\n|---|---|---|\n`projectId` |\nstring? | Scope to one project |\n`includeArchived` |\nbool? | Include archived canvases (default false) |\n\nReturns `[{ id, name, createdAt, lastModified, projectId, archived }]`\n\n.\n\nCanvas lifecycle. `canvas_move`\n\nreassigns a canvas to a different project. `canvas_archive`\n\nsets a soft-delete flag (canvas stays on disk, hidden from default `canvas_list`\n\n); `canvas_unarchive`\n\nclears it. `canvas_delete`\n\nremoves the canvas and its file permanently — irreversible.\n\nGet the URL of the live viewer plus per-canvas URLs. Share these with the user so they can open the design in their browser. No params.\n\n```\n{\n  \"url\": \"http://localhost:3001\",\n  \"gallery\": \"http://localhost:3001\",\n  \"canvases\": [\n    { \"name\": \"Login\", \"viewer\": \"http://localhost:3001/canvas/abc123\" }\n  ]\n}\n```\n\n`canvas_create`\n\nalready returns the per-canvas viewer URL in its response; reach for `viewer_url`\n\nwhen you want the gallery URL or to enumerate every existing canvas's URL in one call.\n\nTop-level container CRUD. The built-in `Personal`\n\nworkspace cannot be deleted, and `workspace_delete`\n\nrefuses if the workspace still contains projects (move or delete them first).\n\nMid-level container CRUD inside a workspace. The built-in `Untitled`\n\nproject cannot be deleted. `project_delete`\n\nrefuses if the project still contains any canvases (archived ones still count — move or delete them first).\n\nBind a workspace to the current project directory so its canvases live **in the repo** as open JSON — a `.framesmith/`\n\ndirectory checked in alongside the code, instead of the global `~/.framesmith`\n\nstore. Run it once per repo.\n\n| Param | Type | Description |\n|---|---|---|\n`workspaceId` |\nstring? | Workspace whose projects + canvases migrate into the repo. Defaults to the built-in Personal workspace. |\n`dir` |\nstring? | Directory to bind. Defaults to the nearest git repo root above the server's working directory. |\n\nIt creates `.framesmith/workspace.json`\n\n(the binding plus the design system, so a fresh clone resolves tokens identically) and one subdirectory per project holding one slug-named file per canvas:\n\n```\n.framesmith/\n  workspace.json     # workspace + projects[] + design system\n  design-system/\n    design-tokens.json\n  ui/\n    bloom-landing.json\n    login-form.json\n```\n\nIt migrates the workspace's projects + canvases in and makes the repo the source of truth for the rest of the session. A canvas is either repo-bound or global, never both. Afterwards the server auto-detects `.framesmith/`\n\non startup (walking up from its working directory). **Commit .framesmith/** so designs travel with the code and diff cleanly in review.\n\nThe bind also records the repo in `~/.framesmith/registry.json`\n\n, so the standalone viewer shows bound repos alongside your global workspaces in one gallery (it rebuilds that read-only mirror on launch and whenever the registry changes).\n\nExecute operations on the scene graph. Operations are line-separated strings:\n\n```\n# Insert a frame into the document root\nheader=I(\"document\", { type: \"frame\", layout: \"horizontal\", fill: \"#1a1a2e\", padding: 24, gap: 16, width: 1440, height: 80 })\n\n# Insert text into the header\nI(header, { type: \"text\", content: \"My App\", fontSize: 24, fontWeight: 700, color: \"#ffffff\" })\n\n# Update a node\nU(\"nodeId\", { fill: \"#e94560\" })\n\n# Delete a node\nD(\"nodeId\")\n\n# Copy a node to a new parent\ncopy=C(\"sourceId\", \"parentId\", { fill: \"#0f3460\" })\n\n# Move a node\nM(\"nodeId\", \"newParentId\", 0)\n\n# Replace a node entirely\nR(\"nodeId\", { type: \"text\", content: \"Replaced\" })\n```\n\n**Returns** `{ ok, nodeIds, results }`\n\n. `nodeIds`\n\nmaps each bound variable to the node ID it created — e.g. `{ \"header\": \"n_a1b2\" }`\n\n— so you can target those nodes in later calls (bindings only live within a single call). `results`\n\nlists each op's outcome in order.\n\n**Node types:** `frame`\n\n, `text`\n\n, `rectangle`\n\n, `ellipse`\n\n, `image`\n\n, `icon`\n\n, `path`\n\n, `component`\n\n, `instance`\n\n, `toggle`\n\n, `checkbox`\n\n, `radio`\n\n, `select`\n\n**Properties:** `fill`\n\n, `gradient`\n\n, `stroke`\n\n, `strokeWidth`\n\n, `cornerRadius`\n\n, `width`\n\n, `height`\n\n, `layout`\n\n(`\"horizontal\"`\n\n| `\"vertical\"`\n\n), `gap`\n\n, `padding`\n\n, `alignItems`\n\n, `justifyContent`\n\n, `fontSize`\n\n, `fontFamily`\n\n, `fontWeight`\n\n, `color`\n\n, `content`\n\n, `textAlign`\n\n, `lineHeight`\n\n, `letterSpacing`\n\n(px), `textDecoration`\n\n, `textTransform`\n\n, `fontVariationSettings`\n\n, `src`\n\n, `objectFit`\n\n, `opacity`\n\n, `shadow`\n\n, `shadows`\n\n, `blur`\n\n, `backdropBlur`\n\n, `backdropFilter`\n\n, `overflow`\n\n, `wrap`\n\n, `position`\n\n, `x`\n\n, `y`\n\n, `icon`\n\n, `iconSize`\n\n, `iconColor`\n\n, `iconStyle`\n\n, `checked`\n\n, `disabled`\n\n, `value`\n\n, `d`\n\n, `viewBox`\n\n, `strokeLinecap`\n\n, `strokeLinejoin`\n\n, `animation`\n\n, `transition`\n\n, `componentId`\n\n, `overrides`\n\nUse `textTransform: \"uppercase\"`\n\nfor uppercase labels (don't bake casing into `content`\n\n), `letterSpacing`\n\nfor tracking, and `fontVariationSettings`\n\n(e.g. `'\"wght\" 650'`\n\n) for variable-font axes.\n\nRender canvas to PNG (returned as base64 image).\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas ID |\n`nodeId` |\nstring? | Specific node to capture |\n`width` |\nnumber? | Viewport width (default 1440) |\n`height` |\nnumber? | Viewport height (default 900) |\n`scale` |\nnumber? | Device scale (default 2) |\n\nRead node data from the scene graph.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas ID |\n`nodeIds` |\nstring[]? | Node IDs to read (default: root) |\n`maxDepth` |\nnumber? | Max traversal depth (default 5) |\n\nGet computed bounding boxes via browser rendering.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas ID |\n`nodeId` |\nstring? | Root node to start from |\n`maxDepth` |\nnumber? | Max depth (default 10) |\n\nRead and write design tokens (colors, spacing, radius, typography). Use `$tokenName`\n\nin node properties to reference variables.\n\n```\n{\n  \"colors\": { \"primary\": \"#e94560\", \"bg\": \"#1a1a2e\" },\n  \"spacing\": { \"sm\": 8, \"md\": 16, \"lg\": 24 },\n  \"radius\": { \"sm\": 4, \"md\": 8 }\n}\n```\n\nThen use in nodes: `{ fill: \"$primary\", padding: \"$md\", cornerRadius: \"$sm\" }`\n\nSet tokens at the workspace level — every project + canvas under the workspace inherits them. Resolution order at render is `canvas.variables`\n\n(override) → `project.designSystem`\n\n→ `workspace.designSystem`\n\n→ built-in defaults, with the rightmost layer winning. Per-category merge: setting only `colors`\n\ndoesn't reset `spacing`\n\n.\n\n```\nworkspace_set_design_system({\n  workspaceId: \"...\",\n  variables: {\n    colors: { primary: \"#f59e0b\", bg: \"#0a0a0a\" },\n    spacing: { sm: 8, md: 16, lg: 24 }\n  }\n})\n```\n\n`workspace_apply_preset({ workspaceId, preset })`\n\nis a shortcut that copies a named preset (`\"dark\"`\n\n, `\"light\"`\n\n, `\"material\"`\n\n, `\"minimal\"`\n\n) into the workspace.\n\nSame shape, but at the project layer between workspace and canvas. Use for sub-brand overrides (e.g., a `Marketing`\n\nproject that overrides one color while inheriting everything else from the workspace).\n\n**Fonts load by name automatically** — naming a `fontFamily`\n\nin a typography token (or on a node) resolves it from Google Fonts at token-write time, with a render-time backstop catching anything else. Binaries are cached under `~/.framesmith/fonts/`\n\n, so renders are offline and deterministic after the first resolve; `typography.body.fontFamily`\n\nbecomes the document default. An unresolvable family renders in the fallback stack **and** adds a `Font warnings`\n\nitem to the screenshot/export result.\n\n`set_fonts`\n\ncovers explicit registration. Three forms, combinable:\n\n```\n{\n  \"families\": [\"Inter\", \"JetBrains Mono\"],\n  \"fonts\": [\n    { \"family\": \"Inter\", \"url\": \"https://fonts.googleapis.com/css2?family=Inter:wght@400;700\" },\n    { \"family\": \"Brand Face\", \"url\": \"https://example.com/brand.woff2\", \"weight\": 400 }\n  ]\n}\n```\n\n`families`\n\n— resolve by name from Google Fonts and merge into the existing declarations.`fonts`\n\nwith a Google Fonts CSS URL (`fonts.googleapis.com/css2?...`\n\n) — faces are extracted from the stylesheet automatically.`fonts`\n\nwith a direct binary URL (`.woff2`\n\n/`.woff`\n\n/`.ttf`\n\n/`.otf`\n\nor a`data:`\n\nURI) — for non-Google sources.\n\n`fonts`\n\nreplaces declarations wholesale (pass `[]`\n\nto clear); `families`\n\nmerges. The renderer emits `@font-face`\n\nblocks plus `<link rel=\"preconnect\">`\n\nper remote origin, with `font-display: swap`\n\n.\n\nExport a canvas or specific nodes to files on disk.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas ID |\n`format` |\nstring | `\"png\"` , `\"jpeg\"` , `\"webp\"` , or `\"pdf\"` |\n`outputPath` |\nstring | Directory to save files |\n`nodeIds` |\nstring[]? | Specific nodes to export (default: full canvas) |\n`width` |\nnumber? | Viewport width (default 1440) |\n`height` |\nnumber? | Viewport height (default 900) |\n`scale` |\nnumber? | Device scale (default 2) |\n\nList available style guide presets. No params. Returns preset names and descriptions.\n\nApply a style guide preset to a canvas. Merges preset design tokens into the canvas variables, and copies in any reusable components (`button`\n\n, `card`\n\n, `badge`\n\n) the preset defines so they can be instanced. The preset is also recorded in the canvas provenance + per-project build log.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas ID |\n`preset` |\nstring | Preset name: `\"dark\"` , `\"light\"` , `\"material\"` , `\"minimal\"` |\n\nList available layout structures — named scaffolds you stamp onto a canvas and then populate. Returns each structure's name, `kind`\n\n, description, and (for pages) taxonomy axes. Distinct from presets: structures define **layout skeleton**, presets define **color/token theme** — they compose.\n\n| Param | Type | Description |\n|---|---|---|\n`projectId` |\nstring? | If given, also return a `diversification` signal for the project (recently-built structures + a hint to differ on ≥ 1 axis), so you pick a shape that contrasts with recent work. Omit it to get just the structure list. |\n\nTwo kinds:\n\n— whole-page scaffolds stamped once at the canvas root:`page`\n\n`marquee-hero`\n\n,`bento-grid`\n\n,`stat-led`\n\n,`editorial-longform`\n\n,`split-workbench`\n\n,`catalogue`\n\n,`dashboard`\n\n,`auth`\n\n,`pricing`\n\n,`settings`\n\n,`onboarding`\n\n. Each is tagged on four independent axes —`heroTreatment`\n\n,`density`\n\n,`rhythm`\n\n,`alignment`\n\n— so you can deliberately vary page shape instead of defaulting to the same layout. Every page scaffold is regression-tested to score ≥ 90 with zero cliché tells across themes (the pattern library's taste bar), so it's a non-slop starting point you adapt, not boilerplate.— reusable fragments stamped under`component`\n\n**any** node via`targetId`\n\n, repeatably:`data-table`\n\n(header + 3 rows with avatar/name/email, role chip, status toggle, actions),`form-field`\n\n,`toolbar`\n\n,`stat-card`\n\n,`toggle-row`\n\n. A high-fidelity table costs one stamp instead of ~80 hand-placed nodes.\n\nStamp a layout structure onto a canvas and return the placeholder node IDs to populate. Seeds neutral default colors so the scaffold renders even before a preset is applied. Populate the placeholders with `batch_design`\n\n`U`\n\nops, then `screenshot`\n\nto verify.\n\n**Page scaffolds** insert at the canvas root (refusing on a non-empty canvas unless`replace`\n\n), record provenance (`metadata.provenance`\n\n), and append to the**per-project build log** that feeds the`diversification`\n\nsignal.**Component scaffolds** insert under`targetId`\n\n(default root), repeatably — every stamp re-keys its node IDs (`form-field-1-…`\n\n,`form-field-2-…`\n\n) and returns an`idMap`\n\n(template ID → live ID) for follow-up ops. Component stamps don't touch provenance or the build log: they don't shape the page.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas ID |\n`structure` |\nstring | Structure name (use `list_structures` , e.g. `\"marquee-hero\"` , `\"data-table\"` ) |\n`replace` |\nboolean? | Page scaffolds: if the root already has children, clear them before stamping. Default `false` (refuses on a non-empty canvas) |\n`targetId` |\nstring? | Component scaffolds: node to stamp under (default `\"document\"` ) |\n\nImport an HTML snippet (+ optional CSS) as an editable canvas — the reverse of `export`\n\n. The markup renders headlessly and a computed-style DOM walk maps it to the scene graph:\n\n| Source | → Scene graph |\n|---|---|\n| flex/block container | `frame` + `layout` /`gap` /`padding` /`alignItems` /`justifyContent` /`wrap` |\n`display: grid` |\nrows of proportional columns from the computed track template (`grid-column` spans honored — numeric spans win, else the box width decides); irregular templates degrade to a stack with a warning. Recorded in `report.layout` |\ncentered content (`margin: auto` , `max-width` , flex-center) |\nthe parent centers (`alignItems` ) and the child keeps its real width — `max-width` becomes the fluid `width: \"100%\"` + `maxWidth` idiom. Recorded in `report.layout` |\n| floats / inline-block / unmodeled multi-column CSS | geometry clustering: children's computed boxes group into row bands (≥50% vertical overlap, consistent column counts) → rows of proportional columns. Conservative by design — anything that looks multi-column but doesn't cluster consistently imports as an honest stack with a `stack-fallback` entry + warning |\n`<table>` / `<tr>` / `<td>` /`<th>` |\na vertical frame of horizontal row frames with proportional percentage cell widths (from the computed boxes — colspan handled free); `thead` /`tbody` unwrap, `<caption>` becomes a text node, bottom borders become hairline divider frames. Recorded in `report.layout` |\n| text run | `text` (size, weight, color, family, line-height, letter-spacing, transform, align) |\n`<img>` (absolute/data URL) |\n`image` |\ninline `<svg>` |\n`icon` when the path data matches a bundled Lucide/Material glyph; else `path` |\ncheckbox / radio / `role=\"switch\"` / `<select>` |\nthe input-primitive node types, with live `checked` /selected state |\n| background / border / radius / shadow / opacity / overflow | `fill` / `stroke` +`strokeWidth` / `cornerRadius` / `shadows` / `opacity` / `overflow` |\n\n**Lossy by design.** Every import returns a `report`\n\n— counts, warnings (dropped background images, grid containers, truncations), unmatched icons/fonts — and that report is the contract; the goal is an *editable, honest* starting point, not a pixel-perfect clone. Single-child wrapper divs collapse, same-style text runs merge, invisible nodes drop (all tunable via `flatten`\n\n).\n\n| Param | Type | Description |\n|---|---|---|\n`html` |\nstring | The snippet to import |\n`css` |\nstring? | CSS to apply — e.g. the compiled Tailwind stylesheet. A bare Tailwind snippet has no runtime, so classes render unstyled without this |\n`projectId` |\nstring? | Project to create the canvas in (default project if omitted) |\n`name` |\nstring? | Canvas name (default `\"Imported HTML\"` ) |\n`selector` |\nstring? | Import only the first match within the snippet |\n`width` |\nnumber? | Container width layouts resolve against (default 1440) |\n`flatten` |\nobject? | `{ collapseWrappers, mergeTextRuns, dropInvisible, maxDepth }` |\n`tokenMatch` |\nobject? | `{ source: \"workspace\" | \"designMd\" | \"tailwind\" | \"none\", tolerance?, designMd? }` — snap concrete values back to `$token` refs (default: the target project's merged design system) |\n`tailwind` |\nobject? | `{ theme: { name: value } }` — the project's `@theme` map; widens which class names map to `$tokens` |\n\n**Token re-mapping** makes the import a token-driven design instead of a pile of hex:\n\n**Tailwind intent first**— class names carry intent a computed value can't:`bg-surface`\n\n→`fill: \"$surface\"`\n\n,`gap-4`\n\n→`16`\n\n,`rounded-xl`\n\n→`12`\n\n,`text-sm font-semibold uppercase`\n\n→ typography props. Custom utilities resolve via`tailwind.theme`\n\n; palette classes (`bg-red-500`\n\n) map to the**bundled v4 palette** as hex literals (generated from the official oklch values by Chrome itself — see`scripts/generate-tailwind-palette.ts`\n\n), so a bare snippet styles without compiled CSS; arbitrary values and unknowns fall through to computed styles. Geometry intent and palette literals only fill gaps the CSS didn't set; token-ref colors override computed literals.**Nearest-color snapping second**— remaining literal colors snap to the matched design system within`tolerance`\n\n(exact matches always; near-ties between two tokens are*reported and left literal*, never guessed). Spacing/radius/fontSize values that equal a scale token are reported under`report.scaleMatches`\n\n.- Fonts seen in computed styles feed the font-by-name resolver, so the imported canvas renders in the same faces.\n\nReturns `{ canvasId, rootId, report }`\n\n— `report.snapped`\n\n/ `literals`\n\n/ `scaleMatches`\n\n/ `warnings`\n\nare the contract.\n\nImport a **live page** as an editable, token-mapped canvas — point at a running app and the screen becomes the design-of-record without redrawing. Same engine and token re-mapping as `canvas_import_html`\n\n, plus live-page controls:\n\n| Param | Type | Description |\n|---|---|---|\n`url` |\nstring | The page to import (http/https only) |\n`viewport` |\nobject? | `{ width, height }` — the width layouts resolve against (default 1440×900) |\n`selector` |\nstring? | Import one component instead of the whole page (default `body` ) |\n`waitFor` |\nstring | number? | CSS selector to await, or a delay in ms — for client-rendered UI |\n`auth` |\nobject? | `{ headers?, cookies? }` for gated pages — used in a throwaway browser context, never persisted to the canvas, provenance, or report |\n`projectId` / `name` / `flatten` / `tokenMatch` / `tailwind` |\n— | Same as `canvas_import_html` |\n\nRelative image URLs resolve against the page; fonts seen in computed styles load through the font-by-name resolver so the canvas renders in the same faces. The source URL (never auth) is recorded in `metadata.provenance.importedFrom`\n\n.\n\nDrift detection — the design-of-record as a **living contract**. Re-imports a live page *ephemerally* (no canvas created, nothing mutated) and pixel-diffs it against an existing canvas at the same viewport:\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | The canvas that is the design-of-record |\n`url` |\nstring | The live page to compare (http/https) |\n`viewport` |\nobject? | Compare size (defaults to the canvas root size) |\n`selector` / `waitFor` / `auth` |\n— | Same as `canvas_import_url` (auth in a throwaway context, never persisted) |\n\nReturns the diff image (changed regions in red), `changePercent`\n\n, `changedPixels`\n\n/`totalPixels`\n\n, and the import report. Both sides render at scale 1, so the percentage is comparable run-to-run — an unchanged page diffs at ~0%.\n\n**CI pattern** (a pattern, not a shipped feature): after deploy, call `canvas_sync_from_url`\n\nfor each route ↔ canvas pair and fail the job when `changePercent`\n\nexceeds your threshold — design ↔ code divergence becomes a build failure instead of a surprise.\n\nImport a [DESIGN.md](https://github.com/VoltAgent/awesome-design-md) file as a design system preset. Parses the Google Stitch format and extracts colors, typography, spacing, and border radius. It also extracts reusable component skeletons (`button`\n\n, `card`\n\n, `badge`\n\n) from the \"Component Styling\" section — `apply_preset`\n\nthen makes them available as instanceable components on the canvas. After importing, use `apply_preset`\n\nto apply it to any canvas.\n\n| Param | Type | Description |\n|---|---|---|\n`content` |\nstring? | Raw DESIGN.md content (provide this OR `filePath` ) |\n`filePath` |\nstring? | Absolute path to a DESIGN.md file |\n`name` |\nstring? | Override the preset name |\n\nCompatible with the 55+ design systems in [awesome-design-md](https://github.com/VoltAgent/awesome-design-md) (Stripe, Notion, Figma, Vercel, Linear, etc.).\n\n**Accepted token formats.** Each category is read from a loosely-matched heading section (`Colors`\n\n/ `Color Palette`\n\n, `Spacing`\n\n, `Border Radius`\n\n/ `Radius`\n\n, `Typography`\n\n). Within a section, tokens may be written as a list item (`- name: value`\n\n), a 2-column table row (`| name | value |`\n\n), or a `name: value`\n\n/ `**name** (\\`\n\nvalue`)` line — where value is a color (`\n\n#hex`, `\n\nrgba(...)`) for colors, `\n\nNpx`for spacing/radius, and`\n\nNpx`(optionally`\n\n/ weight`, e.g. `\n\n16px / 600`) for typography. Named spacing tokens (`\n\nmd: 12px`) are honored verbatim; a scale is synthesized **only** when no named tokens are given and a `\n\nBase unit: Npx`is stated — otherwise nothing is fabricated. Radius accepts the scale names`\n\nsm`/`\n\nmd`/`\n\nlg`/`\n\nxl`/`\n\nfull`/`\n\npill`.\n\nRender a canvas at multiple viewport sizes. Defaults to mobile (390x844), tablet (768x1024), and desktop (1440x900).\n\nThe renderer emits `clamp()`\n\nfor paddings ≥ 32px and font sizes ≥ 24px, so headlines and large spacing shrink proportionally at narrower viewports (assuming a 1440px design width). Smaller values stay static.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas ID |\n`breakpoints` |\narray? | `[{label, width, height}]` — custom breakpoints |\n`scale` |\nnumber? | Device scale (default 2) |\n\nCompare two canvases visually. Returns a diff image with changed regions highlighted in red.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId1` |\nstring | First canvas ID |\n`canvasId2` |\nstring | Second canvas ID |\n`width` |\nnumber? | Viewport width (default 1440) |\n`height` |\nnumber? | Viewport height (default 900) |\n`scale` |\nnumber? | Device scale (default 1) |\n\nAuto-score a design against quality heuristics. Returns an overall score (0–100), per-category scores, and per-node actionable issues. Designed for generator-evaluator loops: build with `batch_design`\n\n, score with `canvas_evaluate`\n\n, fix the issues targeting the returned `nodeId`\n\ns, repeat.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas ID to evaluate |\n`mode` |\n`\"fast\"` | `\"detailed\"` | `\"llm\"` |\n`\"fast\"` = JSON-tree analysis only (<100ms). `\"detailed\"` adds Puppeteer-based pixel-level overlap checks. `\"llm\"` runs fast-mode heuristics plus a vision-model critique (provider picked from `FRAMESMITH_LLM_PROVIDER` or whichever of `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` is set — costs one paid API call per invocation). Default `\"fast\"` . |\n`categories` |\nstring[]? | Subset of `spacing` , `color` , `typography` , `structure` , `consistency` , `cliche` . Defaults to all. |\n`genre` |\nstring? | Style that relaxes specific `cliche` gates (e.g. `\"material\"` allows purple). Defaults to the canvas's provenance preset if stamped. |\n\n**Categories and what they check**\n\n| Category | Weight | Checks |\n|---|---|---|\n`spacing` |\n20 | Off-scale padding/gap values, too many unique spacing values |\n`color` |\n25 | WCAG AA contrast ratios for text against nearest background |\n`typography` |\n20 | Type-scale ratios (1.15–1.75), font-family count, weight variation |\n`structure` |\n15 | Tree depth, naming coverage, design-token usage %, component reuse |\n`consistency` |\n20 | Frames missing `layout` , inconsistent sibling padding, sibling overlap (detailed mode) |\n`cliche` |\n15 | Machine-made tells: default purple/indigo accent, gradient/glow overuse, fake browser/OS chrome (traffic-light dots), the hanging eyebrow-beside-heading header, fabricated metrics/testimonials/logos, eyebrow rhythm (an eyebrow above nearly every section), slop copy (stock AI phrasing — filler verbs, scroll cues, placeholder names, hype labels), radius consistency (too many distinct corner radii), pure black/white (`#000000` ink / `#ffffff` page vs off-black/off-white), accent consistency (multiple competing accent hues). Each issue carries a `tell` discriminator; all advisory (warning/info). Relaxable per `genre` . |\n\n**Return shape**\n\n```\n{\n  \"overallScore\": 87,\n  \"categories\": [{ \"name\": \"spacing\", \"score\": 90, \"issueCount\": 1, \"weight\": 20 }],\n  \"issues\": [\n    {\n      \"category\": \"color\",\n      \"severity\": \"error\",\n      \"nodeId\": \"abc123\",\n      \"message\": \"Text \\\"Sign In\\\" has contrast ratio 2.8:1 against #1a1a2e. WCAG AA requires 4.5:1.\",\n      \"suggestion\": \"Increase contrast by darkening/lightening the text or background.\"\n    }\n  ],\n  \"summary\": \"Overall quality: Good (87/100). Strongest: spacing (90/100). Weakest: color (75/100)...\",\n  \"stats\": { \"totalNodes\": 14, \"textNodes\": 5, \"frameNodes\": 8, \"maxDepth\": 4, \"tokenUsagePercent\": 61, \"componentReusePercent\": 0 },\n  \"mode\": \"fast\"\n}\n```\n\n**With mode: \"llm\"** (Phase 13), the vision model scores a\n\n**fixed rubric**— five axes, each 1–5 with a rationale — instead of one opaque number. The verdict is stamped on the canvas (\n\n`metadata.critique`\n\n) and the per-project build log so quality is auditable over time. Add `floor`\n\n(1–5, default 3, or `FRAMESMITH_CRITIQUE_FLOOR`\n\n) to set the per-axis threshold that trips `needsRevision`\n\n.\n\n```\n{\n  \"llmCritique\": {\n    \"provider\": \"anthropic\",\n    \"model\": \"claude-sonnet-4-6\",\n    \"rubric\": {\n      \"hierarchy\":   { \"score\": 4, \"rationale\": \"clear primary metric, secondary stats recede\" },\n      \"execution\":   { \"score\": 4, \"rationale\": \"tidy alignment and consistent spacing\" },\n      \"specificity\": { \"score\": 3, \"rationale\": \"reads a touch generic for a dashboard\" },\n      \"restraint\":   { \"score\": 5, \"rationale\": \"flat surfaces, no gratuitous effects\" },\n      \"variety\":     { \"score\": 2, \"rationale\": \"the default centered three-card row\" }\n    },\n    \"score\": 72,\n    \"summary\": \"Clean, restrained dashboard; layout is conventional.\",\n    \"suggestions\": [\"break the symmetric three-card row with an asymmetric feature tile\"],\n    \"needsRevision\": true,\n    \"failingAxes\": [{ \"axis\": \"variety\", \"score\": 2, \"rationale\": \"the default centered three-card row\" }]\n  }\n}\n```\n\nAxes: **hierarchy** (focal order), **execution** (craft — alignment/spacing/contrast), **specificity** (designed-for-purpose vs generic), **restraint** (no overdone effects — the LLM sibling of the `cliche`\n\ncategory), **variety** (avoids same-shape sameness). `score`\n\nis **derived**: `round(mean(axisScores) / 5 * 100)`\n\n. To close the loop automatically, see `canvas_revise`\n\n.\n\nProvider selection: `FRAMESMITH_LLM_PROVIDER`\n\nenv var (`anthropic`\n\n| `openai`\n\n), else falls back to whichever of `ANTHROPIC_API_KEY`\n\n/ `OPENAI_API_KEY`\n\nis set. Default models: `claude-sonnet-4-6`\n\n/ `gpt-4.1`\n\n(override via `FRAMESMITH_LLM_ANTHROPIC_MODEL`\n\n/ `FRAMESMITH_LLM_OPENAI_MODEL`\n\n). Adding a third provider is one entry in the `judges`\n\ntable in `src/llm-judge.ts`\n\n.\n\n**Example generator-evaluator loop**\n\n``` js\nbatch_design({ canvasId, operations: \"...\" })\nconst r = canvas_evaluate({ canvasId, mode: \"fast\" })\n// r.issues[].nodeId points to exactly what to fix\nbatch_design({ canvasId, operations: `U(\"${r.issues[0].nodeId}\", { color: \"#ffffff\" })` })\ncanvas_evaluate({ canvasId })  // re-score\n```\n\nIssues that have a mechanical fix come back with an extra `fix: { op, rationale }`\n\nfield — see `canvas_autofix`\n\nbelow.\n\nRuns `canvas_evaluate`\n\nin fast mode and returns just the subset of issues with a mechanically derived fix — no judgement calls. Each fix carries a ready-to-paste `batch_design`\n\nUpdate op string. Closes the generator-evaluator loop without a second AI hop.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas to autofix |\n`categories` |\nstring[]? | Restrict to fixes from these categories (default: all) |\n`genre` |\nstring? | Style that relaxes specific `cliche` gates (e.g. `\"material\"` allows purple). Defaults to the canvas's provenance preset if stamped. |\n\n**What gets auto-fixed**\n\n**Spacing**— off-scale`gap`\n\nor scalar`padding`\n\nsnaps to the nearest scale value. Array`padding`\n\nis skipped (ambiguous which index).**Consistency**— frames with multiple children but no`layout`\n\nget`layout: \"vertical\"`\n\n.**Color**— recoverable WCAG contrast failures get`color: \"#000000\"`\n\nor`\"#FFFFFF\"`\n\n, whichever wins against the resolved background. Failures so bad that neither black nor white meets the threshold are not auto-fixed (the background also needs to change).**Cliché**— a*known-default*purple/indigo accent (`#6366f1`\n\nand friends) written literally on a node swaps to a neutral accent; a dedicated fake-chrome strip (a row that is just traffic-light dots) gets a`D(...)`\n\ndelete; pure-black ink (`#000000`\n\ntext/icon/stroke) softens to off-black. Taste-dependent tells (gradient/glow overuse, the hanging header, fabricated copy, eyebrow rhythm, slop copy, mixed radius systems, competing accents) are reported by`canvas_evaluate`\n\nwith a suggestion but carry**no** auto-fix op.\n\n**Return shape**\n\n```\n{\n  \"totalIssues\": 18,\n  \"fixableCount\": 5,\n  \"fixes\": [\n    {\n      \"nodeId\": \"abc123\",\n      \"category\": \"color\",\n      \"op\": \"U(\\\"abc123\\\", { color: \\\"#000000\\\" })\",\n      \"rationale\": \"Switch text color to #000000 for WCAG AA contrast against #F8FAFC\",\n      \"message\": \"Text \\\"Sign In\\\" has contrast ratio 2.8:1 against #F8FAFC. WCAG AA requires 4.5:1.\"\n    }\n  ]\n}\n```\n\nApply the ops by joining them with newlines and passing to `batch_design`\n\n, then re-evaluate.\n\nCloses the critique loop (Phase 13). Judges the canvas against the rubric; if any axis is below the floor, asks an LLM for targeted `batch_design`\n\nops that raise the failing axes, applies them, re-renders, and re-judges — up to `maxIterations`\n\npasses. **Mutates the canvas.** Opt-in and bounded; it never runs implicitly.\n\n| Param | Type | Description |\n|---|---|---|\n`canvasId` |\nstring | Canvas to revise |\n`maxIterations` |\nnumber? | Revise passes, 1–3 (default 1) |\n`floor` |\nnumber? | Per-axis rubric floor 1–5 (default 3 / `FRAMESMITH_CRITIQUE_FLOOR` ) |\n`provider` |\n`\"anthropic\"` | `\"openai\"` ? |\nForce an LLM provider (default auto-detect) |\n\n**Loop & safety**\n\n- Each pass: render → judge → if\n`needsRevision`\n\n, revise the failing axes → apply (validated through`batch_design`\n\n) → re-render → re-judge. **Stops** when the canvas passes (`passed`\n\n), at the cap (`max-iterations`\n\n), when a pass doesn't improve the overall (`no-improvement`\n\n— the regressing edit is**reverted**), when the reviser returns nothing (`no-ops`\n\n), or when an op fails to apply (`apply-error`\n\n— the partial edit is reverted).- Every\n**accepted** pass re-stamps`metadata.critique`\n\n+ the build log. Costs ≥2 paid API calls per pass (one judge + one revise) and renders between passes (Chrome required).\n\n**Return shape**\n\n```\n{\n  \"iterations\": [\n    { \"pass\": 1, \"overallBefore\": 72, \"failingAxes\": [\"variety\"],\n      \"opsApplied\": \"U(\\\"cards\\\", { ... })\", \"overallAfter\": 84 }\n  ],\n  \"finalVerdict\": { \"rubric\": { \"...\": {} }, \"score\": 84, \"needsRevision\": false, \"failingAxes\": [] },\n  \"stoppedReason\": \"passed\"\n}\n```\n\n— markdown authoring guide: width strategies (fixed / percentage / fluid+cap / floor / fit-content), responsive hint semantics (`framesmith://guidelines`\n\n`stack`\n\n/`wrap`\n\n/`fixed`\n\n), common patterns (pricing tiers, two-column hero, tag list, toolbar), and anti-patterns. Source:.`docs/GUIDELINES.md`\n\n`npm run bench`\n\nruns `canvas_evaluate`\n\nover a fixed corpus of canvases (a high-quality dashboard hero, a minimal well-formed canvas, an intentional-contrast-failure canvas) and diffs the result against [ benchmark/baselines.json](/vicmaster/framesmith/blob/master/benchmark/baselines.json). Catches drift in scoring across renderer / evaluator changes — exit code is nonzero on any score, issue-count, or issue-message change. Re-baseline with\n\n`npx tsx benchmark/run.ts --update`\n\nafter intentional evaluator rewrites.Nodes support linear and radial gradients via the `gradient`\n\nproperty:\n\n```\n# Linear gradient (angle in degrees)\nI(\"parent\", { type: \"frame\", width: 400, height: 200, gradient: { type: \"linear\", angle: 135, stops: [{color: \"#667eea\", position: 0}, {color: \"#764ba2\", position: 100}] } })\n\n# Radial gradient\nI(\"parent\", { type: \"frame\", width: 200, height: 200, gradient: { type: \"radial\", stops: [{color: \"#fff\", position: 0}, {color: \"#000\", position: 100}] } })\n```\n\nWhen `gradient`\n\nis set, it takes precedence over `fill`\n\n. Both can coexist (`fill`\n\nas fallback).\n\nStructured shadows, blur filters, and backdrop blur:\n\n```\n# Structured shadow (supports multiple shadows)\nI(\"parent\", { type: \"frame\", fill: \"#fff\", shadows: [{x: 0, y: 4, blur: 12, spread: 0, color: \"rgba(0,0,0,0.15)\"}] })\n\n# Blur filter\nI(\"parent\", { type: \"frame\", fill: \"#3b82f6\", blur: 4 })\n\n# Backdrop blur (single-function shorthand for `blur`)\nI(\"parent\", { type: \"frame\", fill: \"rgba(255,255,255,0.5)\", backdropBlur: 8 })\n\n# Glassmorphism (composable backdrop-filter: blur + saturate + brightness + contrast)\nI(\"parent\", {\n  type: \"frame\",\n  fill: \"rgba(255, 255, 255, 0.4)\",\n  backdropFilter: { blur: 12, saturate: 180, brightness: 110 }\n})\n```\n\nThe structured `backdropFilter`\n\nform takes precedence over `backdropBlur`\n\nwhen both are set. The renderer also emits the `-webkit-backdrop-filter`\n\nprefix so glass effects render in Safari/iOS without extra work.\n\nThe legacy `shadow`\n\nstring property still works for simple cases.\n\nTwo bundled sets are available via the `icon`\n\nnode type, rendering as inline SVGs with configurable size and color:\n\n**Lucide** (1,900+, stroke style) — unprefixed names, [browse here](https://lucide.dev):\n\n```\nI(\"parent\", { type: \"icon\", icon: \"search\", iconSize: 24, iconColor: \"#888\" })\nI(\"parent\", { type: \"icon\", icon: \"heart\", iconSize: 32, iconColor: \"#ef4444\" })\n```\n\n**Material Symbols** (3,800+, fill style) — `material:`\n\nprefix, [browse here](https://fonts.google.com/icons):\n\n```\nI(\"parent\", { type: \"icon\", icon: \"material:check\", iconSize: 24, iconColor: \"#b71421\" })\nI(\"parent\", { type: \"icon\", icon: \"material:settings\", iconStyle: \"rounded\" })\nI(\"parent\", { type: \"icon\", icon: \"material:star-fill\" })   # \"-fill\" suffix = filled variant\n```\n\n`iconStyle`\n\npicks the Material variant (`\"outlined\"`\n\ndefault, `\"rounded\"`\n\n, `\"sharp\"`\n\n); it's ignored for Lucide.\n\n`toggle`\n\n, `checkbox`\n\n, `radio`\n\n, and `select`\n\nare first-class node types — static renders with a `checked`\n\n/ `value`\n\n/ `disabled`\n\nstate, so app UI doesn't have to be faked from frames and ellipses:\n\n```\nI(\"parent\", { type: \"toggle\", checked: true })\nI(\"parent\", { type: \"checkbox\", checked: true })\nI(\"parent\", { type: \"radio\" })\nI(\"parent\", { type: \"select\", value: \"Administrator\", width: 220 })\nI(\"parent\", { type: \"select\" })                      # renders a muted \"Select…\" placeholder\nI(\"parent\", { type: \"toggle\", checked: true, disabled: true })   # 50% opacity\n```\n\nColors default from the design system — `$accent`\n\n(falling back to `$primary`\n\n) for active states, `$border`\n\nfor outlines, `$bg-surface`\n\n/ `$text-primary`\n\nfor the select — with neutral fallbacks on unthemed canvases. Explicit `fill`\n\n/ `stroke`\n\n/ `color`\n\noverride. Defaults: toggle 44×24, checkbox/radio 18×18, select `fit-content`\n\n(give it a `width`\n\nfor form layouts).\n\nFor custom shapes and brand marks beyond the Lucide library, use the `path`\n\nnode type with a raw SVG `d`\n\nattribute:\n\n```\nI(\"parent\", { type: \"path\", width: 24, height: 24,\n  d: \"M 12 2 L 22 22 L 2 22 Z\", fill: \"#f59e0b\" })\n\n# With stroke + viewBox (defaults to `0 0 width height`)\nI(\"parent\", { type: \"path\", width: 48, height: 48, viewBox: \"0 0 24 24\",\n  d: \"M 12 2 L 22 22 L 2 22 Z\",\n  fill: \"none\", stroke: \"#000\", strokeWidth: 2,\n  strokeLinecap: \"round\", strokeLinejoin: \"round\" })\n```\n\n`fill`\n\n/`stroke`\n\n/`strokeWidth`\n\napply to the path itself (not the wrapper). `d`\n\nand `viewBox`\n\nare validated for safe characters — anything that could break out of the attribute is rejected.\n\nReference a built-in keyframe to make a node animate in on page load. The renderer auto-emits the `@keyframes`\n\nblock only when referenced.\n\n```\nI(\"hero\", { type: \"frame\", animation: { name: \"fadeIn\", duration: 400 } })\nI(\"title\", { type: \"text\", animation: { name: \"slideUp\", duration: 300, delay: 100 } })\n```\n\nBuilt-in keyframe names: `fadeIn`\n\n, `slideUp`\n\n, `slideDown`\n\n, `scaleIn`\n\n. All end at the natural resting state with `animation-fill-mode: both`\n\n, so the start state applies pre-animation and the end state sticks after.\n\n`animation`\n\n: `{ name, duration?: 300ms, delay?: 0ms, easing?: \"ease-out\", iteration?: 1 | \"infinite\" }`\n\n. Easing is whitelisted: `ease`\n\n, `ease-in`\n\n, `ease-out`\n\n, `ease-in-out`\n\n, `linear`\n\n(anything else falls back to `ease-out`\n\n).\n\n`transition`\n\n: `{ property?: \"all\", duration, easing?: \"ease\", delay?: 0ms }`\n\n. Transitions only fire on state change, so they're inert until interactive states exist in the renderer — included today so a future hover/focus PR has a place to land.\n\nDefine reusable components and create instances with overrides:\n\n```\n# Define a component (a frame subtree that gets registered)\ncard=I(\"document\", { type: \"component\", name: \"Card\", width: 300, fill: \"#1a1a1a\", cornerRadius: 12, layout: \"vertical\", padding: 16, gap: 8 })\nI(card, { type: \"text\", name: \"title\", content: \"Default Title\", fontSize: 20, color: \"#fff\" })\nI(card, { type: \"text\", name: \"subtitle\", content: \"Default subtitle\", fontSize: 14, color: \"#888\" })\n\n# Create instances with overrides (matched by child name)\nI(\"document\", { type: \"instance\", componentId: card, overrides: { title: { content: \"My Card\" }, subtitle: { content: \"Custom text\" } } })\n```\n\nHere's a complete session building a login card:\n\n**1. Create a canvas and set design tokens**\n\n```\ncanvas_create({ name: \"Login\" })\n→ {\n    \"canvasId\": \"abc123\",\n    \"rootId\": \"xyz789\",\n    \"name\": \"Login\",\n    \"projectId\": \"default-project\",\n    \"viewerUrl\": \"http://localhost:3001/canvas/abc123\",\n    \"galleryUrl\": \"http://localhost:3001\"\n  }\n\nset_variables({\n  canvasId: \"abc123\",\n  variables: {\n    colors: { bg: \"#0a0a0a\", surface: \"#1a1a2e\", accent: \"#e94560\", text: \"#ffffff\" },\n    spacing: { sm: 8, md: 16, lg: 24, xl: 32 },\n    radius: { md: 8, lg: 16 }\n  }\n})\n```\n\n**2. Build the layout with batch_design**\n\n```\nbatch_design({\n  canvasId: \"abc123\",\n  operations: `\n    page=I(\"document\", { type: \"frame\", width: 1440, height: 900, fill: \"$bg\", layout: \"vertical\", alignItems: \"center\", justifyContent: \"center\" })\n    card=I(page, { type: \"frame\", width: 400, fill: \"$surface\", cornerRadius: \"$lg\", padding: [32, 32, 32, 32], layout: \"vertical\", gap: 24 })\n    I(card, { type: \"text\", content: \"Sign In\", fontSize: 28, fontWeight: 700, color: \"$text\" })\n    I(card, { type: \"frame\", width: \"100%\", height: 44, fill: \"#ffffff10\", cornerRadius: \"$md\", padding: [0, 16, 0, 16], layout: \"horizontal\", alignItems: \"center\" })\n    I(card, { type: \"frame\", width: \"100%\", height: 44, fill: \"#ffffff10\", cornerRadius: \"$md\", padding: [0, 16, 0, 16], layout: \"horizontal\", alignItems: \"center\" })\n    btn=I(card, { type: \"frame\", width: \"100%\", height: 44, fill: \"$accent\", cornerRadius: \"$md\", layout: \"horizontal\", alignItems: \"center\", justifyContent: \"center\" })\n    I(btn, { type: \"text\", content: \"Continue\", fontSize: 16, fontWeight: 600, color: \"$text\" })\n  `\n})\n```\n\n**3. Take a screenshot to see the result**\n\n```\nscreenshot({ canvasId: \"abc123\" })\n→ returns base64 PNG image\n```\n\n**4. Iterate — update the button color and verify**\n\n```\nbatch_design({\n  canvasId: \"abc123\",\n  operations: `U(\"btn-id\", { fill: \"#3b82f6\" })`\n})\n\nscreenshot({ canvasId: \"abc123\" })\n```\n\nThe viewer runs in one of two modes — embedded (auto-starts inside the MCP server process) or standalone (long-lived in its own terminal). Standalone is recommended; the embedded mode stops the moment your MCP session ends, so any viewer URL you shared becomes unreachable.\n\n```\n# In a separate terminal tab — stays alive independently of any MCP session\nnpx -p framesmith framesmith-viewer\n\n# Or on a specific port\nnpx -p framesmith framesmith-viewer 3004\n```\n\nWorking from a clone instead of npm? Run\n\n`npm run viewer`\n\n(or`npm run viewer -- 3004`\n\n) from the repo root — same standalone viewer, run from source.\n\nThe standalone viewer:\n\n**Persists across sessions**— URLs keep working after Claude / Cursor / Windsurf finishes** Shared across projects**— multiple MCP sessions (from different projects) all use the same viewer** Auto-detects new canvases**— watches`~/.framesmith/canvases/`\n\nfor changes and picks them up immediately**Auto-detected by MCP**— when the MCP server starts, it probes for a running standalone viewer and uses it instead of starting its own\n\n**Gallery**(`/`\n\n) — browse all canvases as clickable cards with live thumbnails**Project**(`/project/:id`\n\n) — same gallery but scoped to one project**Archive**(`/archive`\n\n) — soft-deleted canvases with restore / permadelete actions**Canvas detail**(`/canvas/:id`\n\n) — full rendered design with responsive viewport buttons (Mobile / Tablet / Desktop), Compare mode, Fit toggle, and JSON inspector**Raw HTML**(`/canvas/:id/html`\n\n) — the rendered HTML for embedding or inspection**JSON API**(`/api/canvases`\n\n,`/api/canvas/:id/meta`\n\n) — programmatic access**Live auto-refresh**— the viewer polls for changes every 2 seconds, so the browser updates automatically as your agent runs`batch_design`\n\nAll canvases persist to `~/.framesmith/canvases/`\n\nas JSON files and survive process restarts. Set `FRAMESMITH_VIEWER_URL`\n\nin the MCP server env to point at a viewer running on a non-default port.\n\n- Start the standalone viewer in a terminal tab:\n`npx -p framesmith framesmith-viewer`\n\n`canvas_create`\n\n→ get canvas ID- Open the viewer URL in your browser for live preview\n`apply_preset`\n\nor`set_variables`\n\n→ set up design tokens`batch_design`\n\n→ build the UI with frames, text, icons, components, gradients- Watch the viewer auto-refresh as you design\n`screenshot_responsive`\n\n→ preview at mobile/tablet/desktop sizes`canvas_diff`\n\n→ compare before/after changes visually`export`\n\n→ save final designs to PNG/PDF files\n\n```\ngit clone https://github.com/vicmaster/framesmith.git\ncd framesmith\nnpm install\nnpm run build\n```\n\n| Command | What it does |\n|---|---|\n`npm run build` |\nCompile TypeScript to `dist/` . Required before the installed MCP server picks up changes — it loads `dist/index.js` . |\n`npm run dev` |\nRun the server directly via `tsx` for local iteration. Does not affect the registered MCP server. |\n`npm run viewer [port]` |\nStart the standalone viewer (default auto-picks from 3001). |\n`npx tsx test-*.ts` |\nRun ad-hoc test scripts at the repo root. |\n\n| Variable | Purpose |\n|---|---|\n`FRAMESMITH_VIEWER_URL` |\nPoint the MCP server at an external viewer (skips starting an embedded one). |\n`FRAMESMITH_VIEWER_PORT` |\nOverride the standalone viewer's port. |\n`FRAMESMITH_CHROME_PATH` |\nChrome binary for screenshots/exports (falls back to `PUPPETEER_EXECUTABLE_PATH` , then the Puppeteer-managed Chrome). Set it in the MCP server's env config — clients often launch servers with a minimal env. |\n\n- ESM only (\n`\"type\": \"module\"`\n\n). Imports in TypeScript source use`.js`\n\nextensions even when the source file is`.ts`\n\n. - Don't edit\n`dist/`\n\n— it's regenerated by`tsc`\n\n. - New MCP tool? Register it in\n`src/index.ts`\n\n, document it in the Tools section above, and update`VISION.md`\n\n's phase checklist.\n\nMIT — see [LICENSE](/vicmaster/framesmith/blob/master/LICENSE).\n\nCopyright (c) 2026 Victor Velazquez.", "url": "https://wpnews.pro/news/framesmith-1-7-a-quality-gate-that-tells-an-ai-agent-when-a-ui-is-done", "canonical_source": "https://github.com/vicmaster/framesmith", "published_at": "2026-06-26 19:35:36+00:00", "updated_at": "2026-06-26 20:05:53.286151+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["Framesmith", "MCP", "Puppeteer", "Node.js", "Claude", "Codex", "Cursor", "Windsurf"], "alternates": {"html": "https://wpnews.pro/news/framesmith-1-7-a-quality-gate-that-tells-an-ai-agent-when-a-ui-is-done", "markdown": "https://wpnews.pro/news/framesmith-1-7-a-quality-gate-that-tells-an-ai-agent-when-a-ui-is-done.md", "text": "https://wpnews.pro/news/framesmith-1-7-a-quality-gate-that-tells-an-ai-agent-when-a-ui-is-done.txt", "jsonld": "https://wpnews.pro/news/framesmith-1-7-a-quality-gate-that-tells-an-ai-agent-when-a-ui-is-done.jsonld"}}