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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ Whenever the `coverage` category runs on a **base** canvas, the result also carr
| `spacing` | 20 | Off-scale padding/gap values, plus spacing variety measured against the canvas's **declared** scale (`set_variables`/`generate_scale` spacing tokens) — only values *not* on that scale count toward sprawl, so a design built entirely from its own multi-step scale (framesmith's generated scale ships nine: `space-3xs`…`space-3xl`) isn't penalized for using it. No declared scale falls back to the old total-unique-values count, with a suggestion to declare one |
| `color` | 25 | WCAG AA contrast ratios for text against nearest background — checked in BOTH themes when a `dark` token layer exists (dark-run issues carry `theme: "dark"`, score takes the worse theme, and dark failures point at the dark layer instead of carrying a literal fix). WCAG-passing pairs that are perceptually weak by **APCA** get an info advisory (Lc vs ~75 body / ~60 large) — APCA is a candidate method, not a standard; it never blocks |
| `typography` | 20 | Type-scale ratios (1.1–2.0; adjacent sizes both declared as typography tokens are pinned and skip the check), font-family count, weight variation — plus score-neutral craft advisories: measure (prose past ~75 characters per line; warning past 90), tracking (display text ≥28px without negative letter-spacing, autofixable), and tabular numerals (numeric table columns without `tabularNums`, autofixable) |
| `structure` | 15 | Tree depth, naming coverage, design-token usage %, component reuse |
| `structure` | 15 | Tree depth, naming coverage, design-token usage %, component reuse, unknown icon names (an icon that resolves in neither set renders an empty slot: a directive-blocking warning, −10 structure score each; autofixable when one spelling resolves) |
| `consistency` | 20 | Frames missing `layout`, sibling overlap (detailed mode) — plus score-neutral advisories: **token detachment** (a literal `fill`/`stroke`/`color`/`cornerRadius` that exactly equals a token's value flags with a mechanical `$ref` autofix; ambiguous matches list candidates and never guess) and **ad-hoc motion** (3+ distinct duration/easing combos with no motion tokens declared) |
| `cliche` | 15 | 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 — the `$success`/`$warning`/`$danger` status vocabulary is exempt when referenced as tokens, since those are semantics rather than a second accent; the same hues as literals still count). Each issue carries a `tell` discriminator; all advisory (warning/info). Relaxable per `genre`. The tells know dashboard vocabulary: a violet reached only through a `$chart-*` or `$*-tint` token doesn't count as the default-purple tell (a literal violet, or one through any other token, still does), a gradient reached through a `$token` doesn't count toward gradient overuse, thin rounded sparkline bars aren't mistaken for traffic-light dots (fake-chrome now requires a near-square shape), and an uppercase label sitting beside a large tabular figure reads as a KPI-card metric, not an eyebrow. It also knows retail copy: a numeral bound to a unit noun by a tight hyphen (`"30-day returns"`, `"2-year warranty"`, `"24/7 support"`) is a compound modifier, not a numbered section eyebrow. |
| `coverage` | 10 | State coverage on **base** canvases with data-bearing content: a detected table demands designed `empty` + `loading` variants, a form (3+ input controls) demands `error` — each missing state is a directive-blocking warning whose suggestion names the `canvas_add_variant` + scaffold path. Variant canvases and non-data screens get no findings. Table detection needs three matching horizontal rows to read as data (two stacked rows are treated as a layout, e.g. a topbar over a greeting); an import-named `Table` frame keeps the looser two-row shape. On a **`loading`** variant itself, a warning fires per data surface (a detected table, or a frame with 2+ large tabular/numeric texts) that still shows live values while a sibling region is skeletoned — "Loading state is half-designed" — naming the region and suggesting the matching skeleton scaffold. The result's `coverage` field reports `{ dataBearing, states, missing }`. |
Expand Down Expand Up @@ -889,6 +889,7 @@ Runs `canvas_evaluate` in fast mode and returns just the subset of issues with a
- **Consistency** — frames with multiple children but no `layout` get `layout: "vertical"`; a literal `fill`/`stroke`/`color`/`cornerRadius` that exactly equals a single token's value gets re-pointed to `$token` (a value shared by multiple tokens is reported with candidates but never auto-fixed — no guessing).
- **Color** — recoverable WCAG contrast failures get `color: "#000000"` or `"#FFFFFF"`, 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` and 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(...)` delete; pure-black ink (`#000000` text/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` with a suggestion but carry **no** auto-fix op.
- **Structure** — an unknown icon name with exactly one spelling that resolves (hyphen ↔ underscore, or a missing `material:` prefix; if several resolve, the one matching the icon set the rest of the canvas uses) is rewritten via `U(id, { icon: "<name>" })`. Icons inside component definitions are flagged but carry no fix op — edit the component. A name with no resolving spelling is reported with a suggestion only.
- **Coverage** — missing state variants (empty/loading/error) have no mechanical fix either — designing a state is a judgment call. `canvas_evaluate` reports the gap with a suggestion naming the `canvas_add_variant` + scaffold path.

**Return shape**
Expand Down Expand Up @@ -1050,8 +1051,11 @@ I("parent", { type: "icon", icon: "heart", iconSize: 32, iconColor: "#ef4444" })
I("parent", { type: "icon", icon: "material:check", iconSize: 24, iconColor: "#b71421" })
I("parent", { type: "icon", icon: "material:settings", iconStyle: "rounded" })
I("parent", { type: "icon", icon: "material:star-fill" }) # "-fill" suffix = filled variant
I("parent", { type: "icon", icon: "material:chevron_right" }) # multi-word names keep Material's underscores
```

Spelling differs by set: Lucide hyphenates (`chevron-right`), Material keeps underscores and needs the prefix (`material:chevron_right`). An icon name that resolves in neither set renders an empty slot, so `canvas_evaluate` warns on it (structure category) and `canvas_autofix` rewrites it when exactly one spelling resolves.

`iconStyle` picks the Material variant (`"outlined"` default, `"rounded"`, `"sharp"`); it's ignored for Lucide.

## Input controls
Expand Down
9 changes: 5 additions & 4 deletions docs/GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ The `chart` node owns the value→coordinate math — give it data and domains,
- **Setting `fontFamily` on every text node.** The renderer defaults to a system sans-serif stack at the body level. Only set `fontFamily` when you want a *different* face — and prefer `system-ui, -apple-system, sans-serif`-style stacks; if you need quoted multi-word names (`'Segoe UI'`), they're supported, but keep them inside the double-quoted value.
- **Hardcoding pixel font sizes everywhere.** Large sizes get a `clamp()` treatment by the renderer to scale down on small viewports — only set `fontSize` to the *desktop* value and let the renderer handle the rest.
- **Unicode glyphs as icon stand-ins.** `✓ ● ▾ ○` read as unfinished. Use the `icon` node type — two sets render by name as inline SVG: 1,900+ [Lucide](https://lucide.dev) (`icon: "check"`) and 3,800+ [Material Symbols](https://fonts.google.com/icons) (`icon: "material:check"`, `iconStyle: "outlined" | "rounded" | "sharp"`, `-fill` suffix for filled variants). `I("parent", { type: "icon", icon: "material:check", iconSize: 16, iconColor: "$primary" })`. For Material-style design systems, prefer the Material set.
- **Spelling an icon in the other set's style.** Lucide hyphenates multi-word names (`chevron-right`); Material Symbols keeps underscores and needs the prefix (`material:chevron_right`, `material:chevron_right-fill`). A bare `chevron_right` is looked up in Lucide and renders nothing. A name that resolves in neither set renders an empty slot, so `canvas_evaluate` reports it as a `structure` warning naming the spellings that do resolve — and `canvas_autofix` rewrites it when exactly one does.
- **Baking uppercase into `content`.** Use `textTransform: "uppercase"` (with `letterSpacing` for tracking) so the underlying copy stays editable and case-styling lives in the design, not the data.
- **Faking form controls from frames + ellipses.** `toggle`, `checkbox`, `radio`, and `select` are real node types: `I("parent", { type: "toggle", checked: true })`. They style themselves from `$accent`/`$border`/`$bg-surface` tokens (neutral fallbacks when unthemed) and stay pixel-consistent; `fill`/`stroke`/`color` override.
- **Faking loading states from plain gray rectangles.** Use `{ type: "skeleton" }` — token-derived fill, correct default radius, and it pulses in the live viewer (always static in screenshots/exports so diffs stay deterministic). Stamp `skeleton-table` / `skeleton-card` / `skeleton-stat-card` instead of hand-placing bars — and skeleton EVERY data surface: the coverage check flags a loading variant that leaves live metric values beside skeletoned regions (a half-designed loading screen flashes real-looking data next to placeholders).
Expand Down Expand Up @@ -237,8 +238,8 @@ Exit codes are gate-friendly for both commands: `0` pass, `1` drift/mismatch, `2

## After designing

- **`canvas_evaluate`** scores the design on 8 categories (spacing, color, typography, structure, consistency, cliche, coverage, usability) and surfaces actionable issues with `nodeId` references — see "Design every state" below for what `coverage` demands. The `usability` category holds the mechanical UX floors beyond contrast: a control below the 24px hit-target floor is an **error** (WCAG 2.5.8 — a lone control inherits its padded row's box, so real settings rows pass), an unlabeled control is a warning, and vague action copy ("Click here") is an info. Controls also render with accent-derived `:focus-visible` rings in the live viewer — screenshots stay byte-identical because nothing is focused in a static capture. Use it in a generator-evaluator loop: `batch_design` → `canvas_evaluate` → fix the returned nodeIds.
- **`canvas_autofix`** runs `canvas_evaluate` internally and returns just the subset of issues that have a mechanically derived fix — off-scale spacing snaps to scale (`gap`, scalar padding, and array padding via a whole-array snap), missing layout becomes `vertical`, recoverable WCAG contrast failures get `#000` or `#FFF` based on background luminance. Pass `apply: true` to write every fix in the same call (the result reports applied/failed per op), or run the returned ops via `batch_design` yourself — either way, re-evaluate after. Closes the loop without judgment calls on your part.
- **`canvas_evaluate`** scores the design on 8 categories (spacing, color, typography, structure, consistency, cliche, coverage, usability) and surfaces actionable issues with `nodeId` references — see "Design every state" below for what `coverage` demands. The `usability` category holds the mechanical UX floors beyond contrast: a control below the 24px hit-target floor is an **error** (WCAG 2.5.8 — a lone control inherits its padded row's box, so real settings rows pass), an unlabeled control is a warning, and vague action copy ("Click here") is an info. The `structure` category also warns — directive-blocking — on unknown icon names (an empty slot in the render). Controls also render with accent-derived `:focus-visible` rings in the live viewer — screenshots stay byte-identical because nothing is focused in a static capture. Use it in a generator-evaluator loop: `batch_design` → `canvas_evaluate` → fix the returned nodeIds.
- **`canvas_autofix`** runs `canvas_evaluate` internally and returns just the subset of issues that have a mechanically derived fix — off-scale spacing snaps to scale (`gap`, scalar padding, and array padding via a whole-array snap), missing layout becomes `vertical`, recoverable WCAG contrast failures get `#000` or `#FFF` based on background luminance, and an unknown icon name with exactly one resolving spelling is rewritten to it. Pass `apply: true` to write every fix in the same call (the result reports applied/failed per op), or run the returned ops via `batch_design` yourself — either way, re-evaluate after. Closes the loop without judgment calls on your part.
- **`canvas_evaluate` with `mode: "llm"`** runs fast-mode heuristics plus a vision-model critique against a **fixed rubric** (Claude or GPT-4.1, picked from env). Returns the heuristic result with an extra `llmCritique` field: five axes — **hierarchy, execution, specificity, restraint, variety** — each scored 1–5 with a rationale, plus a derived overall, `summary`, `suggestions`, and `needsRevision` / `failingAxes` (any axis below the `floor`, default 3). The verdict is stamped on the canvas + build log so quality is auditable over time. Use this for the "is this visually well-designed?" question heuristics can't answer — composition, hierarchy, polish. Costs one API call per run; reach for it after the heuristic score plateaus.
- **`canvas_revise`** closes the loop: it judges, and for any failing axis asks the model for targeted `batch_design` ops, applies them, and re-judges — up to `maxIterations` passes (1–3). It **mutates the canvas**, reverts any pass that doesn't improve the overall, and stops on pass / cap / no-improvement. Opt-in and costly (≥2 API calls per pass); reach for it when you want the model to act on its own critique instead of you hand-translating it.
- **`screenshot_responsive`** renders the same scene at mobile / tablet / desktop. Inspect all three; if `responsive` hints are set correctly the mobile layout will look right with no extra work.
Expand Down Expand Up @@ -299,7 +300,7 @@ The bar is "designers say *wow*," not "competent." The cliché tells below are t
Polishing the design to the bar is **your** job, not the user's — they should never have to point out a missing icon, an off-scale gap, or a low score. So the loop is part of designing, not an afterthought:

1. `canvas_evaluate` the canvas.
2. **Resolve every warning and every cliché tell** — `canvas_autofix` for the mechanical subset (spacing/contrast/known-default accent), `batch_design` for the rest. (Cliché tells are info/warning but they're the slop signal — always fix them.) Pure advisories like "consider extracting components" are optional refinements that don't block.
2. **Resolve every warning and every cliché tell** — `canvas_autofix` for the mechanical subset (spacing/contrast/known-default accent/misspelled icon), `batch_design` for the rest. (Cliché tells are info/warning but they're the slop signal — always fix them.) Pure advisories like "consider extracting components" are optional refinements that don't block.
3. Re-run `canvas_evaluate`.
4. Repeat until there are **zero errors, zero warnings, and zero cliché tells**.

Expand Down Expand Up @@ -327,7 +328,7 @@ Polishing the design to the bar is **your** job, not the user's — they should
| **Accent consistency** | 3+ competing saturated accent hues (excludes neutrals + the page background) | Pick **one** accent hue — plus neutrals and at most one status color. Set it once as `$accent`. |

- **`cliche` is advisory** — tells are `warning`/`info`, never a hard error; they dent the score, they don't block.
- **`canvas_autofix` fixes the mechanical ones** — it swaps a *known-default* purple accent, deletes a fake-chrome strip, and softens pure black (`#000000`) ink to off-black. Gradient/glow, the hanging header, fabricated copy, eyebrow rhythm, slop copy, mixed radius systems, and competing accents carry a suggestion but no op (taste/judgment calls).
- **`canvas_autofix` fixes the mechanical ones** — it swaps a *known-default* purple accent, deletes a fake-chrome strip, and softens pure black (`#000000`) ink to off-black, and corrects a misspelled icon name when exactly one spelling resolves. Gradient/glow, the hanging header, fabricated copy, eyebrow rhythm, slop copy, mixed radius systems, and competing accents carry a suggestion but no op (taste/judgment calls).
- **Genre relaxes intentional tells** — stamp the canvas once with `canvas_set_genre` (no token churn; `null` clears; the stamp never moves the `versionHash`, so approvals stay valid), or pass `genre` per evaluate/autofix call, so a style that legitimately uses a tell isn't nagged. `genre: "material"` allows the purple accent **and** white elevated surfaces (both are intentional in Material Design); `genre: "dashboard"` (alias `"data"`) allows realistic figures on data-dense product screens, and `genre: "commerce"` (alias `"checkout"`) allows the money on a transactional screen — cart, checkout, order confirmation, billing history (both relax `honest-content`). Declare the genre the screen actually **is** — don't use it to dodge flags on a marketing page.
- **Genre follows what the screen is FOR, not what it contains.** "It shows figures → `dashboard`" is the intuitive heuristic and it's wrong: a *targets editor* stamped `dashboard` clears one figure tell while leaving the white-surface tells that `material` would have relaxed. Read screens presenting published figures → `dashboard`/`data`; a screen you complete a purchase on → `commerce`/`checkout`; editors and admin forms → `material`. A **pricing page is marketing**, not commerce: its numbers are claims about a product, not the arithmetic of a transaction, so it keeps the fabricated-content gate. The evaluate result's `genre` field makes this checkable: it reports the active genre, its source (`explicit` param vs `provenance` stamp), the tells it `relaxed`, and `notRelaxed` (`[{ tell, relaxedBy }]`) — tells still flagging that a *different* genre would relax. If your score is pinned by tells listed in `notRelaxed`, you likely declared the wrong genre. (`honest-content` is effectively the ceiling for any financial UI — declare `dashboard` on read screens, `commerce` on transactional ones, or the design's own figures cap the score.)

Expand Down
Loading
Loading