Draw wireframe screens, and give text a size - #36
Conversation
Every piece of text on the board rendered at `m`: `size` was never passed by any tool and never settable afterwards, so a heading could not outrank body text. That is tolerable in a diagram, where every node label is a peer, and fatal for a screen mock, which is made of type hierarchy. `create_shape` and `update_shape` now take `size` (`s`/`m`/`l`/`xl`). Validation comes from the published schema, so the sweep tests cover the new enum without a hand-written case. Two things fell out of it. tldraw keys stroke width off the same style (2 / 3.5 / 5 / 10 px), so a bigger label arrives with a heavier border — a standalone `text` shape is the way to scale type without that, and the skill now says so. And `update_shape`'s local `size` bag held w/h, which the new prop collides with: it is `dims` now, and the label re-measure runs when either the box or the text size changed, so growY cannot go stale and crop a label. Verified on the live canvas: text heights measured 24.3 / 32.4 / 48.6 / 59.4, which is tldraw's 18 / 24 / 36 / 44 px at its 1.35 line height, a box built at `s` and pushed to `xl` afterwards reported no issues, and the same call round-tripped through MCP. 122 tests and tsc pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Asking an agent to mock a screen out of `create_shape` meant it invented the numbers every time: field height, the gap between a label and its box, how far in a placeholder sits. Two runs of the same brief came out differently, and neither was wrong in a way you could point at. `create_screen` takes a tree — `column`, `row` and `panel` containers around `input`, `select`, `button`, `checkbox`, `radio`, `image`, `divider` and text — and draws it into its own frame. No coordinates and no sizes are passed: the spacing scale, field height, type scale and widths live in the code, so the same tree always draws the same screen. A `row` splits twelfths, and a child may claim `span`; every other width follows from the container. Any node may carry a `key`, and the ids it produced come back under it — every shape of that element, so the ids delete or nudge the whole thing rather than half of it. The layout is a pure module beside `graph.ts`, with text heights supplied through a `measure` callback. That is what keeps it testable without a browser, and it is also what makes wrapping correct: heights come from `editor.textMeasure`, the same measurement tldraw uses on its own text shapes, so a label that runs to two lines pushes what follows it down. A fake measure in the tests proves the same thing without a font. Three decisions worth naming. No geo shape carries a label — a box is a box and every string is a text shape — because tldraw keys stroke width off the same `size` style as label text, so a box that carried its own label would have had its outline weight decided by its font size. Wireframes are drawn in `black` and `grey` only, deliberately overriding the per-agent attribution colour, since a screen drawn in an agent's blue reads as a design decision that was never made. And `findIssues` is not run here: a wireframe overlaps by construction, so the diagram checks would report the layout working as a defect. Verified on the live canvas at both presets: Thai text wraps and measures correctly, a two-line checkbox label grows downward instead of reaching back over the element above it, `span: 4` beside a plain sibling gives a quarter and three quarters, and a panel sizes itself to what it holds. 139 tests and tsc pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A button was a rectangle with a separate text shape centred over it, and an input was a box with its placeholder floated inside. Both looked right and both were wrong for a board two people edit: drag the button in the browser and the word stays behind. `button`, `input` and `select` now put the string in the box's own label, where tldraw moves and resizes it with the shape. `align: 'start'` keeps a placeholder left, which the hardcoded centring in `create_shape` had made look impossible, and `labelColor` greys it without touching the outline colour. The cost is that a box label is drawn at `LABEL_FONT_SIZES[size]` — the same style that sets stroke width — so every box stays at `size: 's'` and its text is 18px rather than the 24px of body copy. That reads as a deliberate UI size, and it is the price of the label belonging to the box. tldraw grows a box whose label does not fit rather than clipping it, so the height is worked out here the same way tldraw works it out — measured at 18px against the width less 16px of padding per side — and the layout below clears the grown box instead of being overlapped by it. A test covers that. Known and accepted: a select value long enough to fill the field runs under its caret, because a box label's width is tldraw's to decide, not ours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed at c295d6d. I read the full files: browser/src/screen.ts, browser/src/screen.test.ts, browser/src/tools.ts, src/server.ts, src/server.test.ts, .claude-plugin/plugin.json, skills/endgame-canvas-diagrams/SKILL.md, README.md.
I traced the create_screen tool from TOOL_DEFS through schema validation, the dispatcher handler, runTool, and layoutScreen. I verified the size parameter flows through create_shape and update_shape to shape props and triggers label re-measure. I confirmed screen.ts is a pure module (no tldraw imports) consistent with the graph.ts convention. I verified the cross-file sweep: every new TOOL_DEFS entry has a handler, a browser-side branch, and a dispatch test; the existing "every enum" and "every tool has a handler" sweep tests cover the new entries; the plugin version was bumped.
Confidence: 90/100 — the layout module is well-structured, defensively coded, and thoroughly tested. The known deferrals (unchecked box, select caret overflow, wireframe overlaps as read_frame issues) are explicitly called out in the PR body.
No review comments to provide on this diff.
The board could draw diagrams and could not draw a screen. Asking an agent to
mock one out of
create_shapemeant it invented the numbers every time — fieldheight, the gap between a label and its box, how far in a placeholder sits — so
two runs of the same brief came out differently and neither was wrong in a way
you could point at.
Three commits, in the order they had to happen.
Expose text size— every string on the board rendered atm, becausesizewas never passed by any tool and never settable afterwards. A headingcould not outrank body text. That is tolerable in a diagram, where every node
label is a peer, and fatal for a screen, which is made of type hierarchy.
create_shapeandupdate_shapenow take it, validation comes from thepublished schema, and
update_shape's localsizebag (which held w/h) isdimsso the label re-measure runs when either the box or the text sizechanged.
Draw a whole UI screen from a tree of elements—create_screentakescolumn/row/panelcontainers aroundinput,select,button,checkbox,radio,image,dividerand text, and draws them into their ownframe. No coordinates and no sizes are passed: the spacing scale, field height,
type scale and widths live in the code, so the same tree always draws the same
screen. A
rowsplits twelfths and a child may claimspan. Any node may carrya
key, and every shape it produced comes back under that key. The layout is apure module beside
graph.tswith heights supplied through ameasurecallback — testable without a browser, and correct about wrapping, because in
the browser that callback is
editor.textMeasure, the same measurement tldrawuses on its own text shapes.
Let a box carry its own label— a button was a rectangle with a separatetext shape centred over it. It looked right and was wrong for a board two people
edit: drag the button and the word stays behind. Boxes now hold their own
labels, left-aligned and greyed for a placeholder, and the layout works out the
height tldraw will grow them to rather than being overlapped by it.
A wireframe here says what is on a screen and where. It carries no colour,
corner radius, font or weight and is drawn in black and grey only — deliberately
overriding the per-agent attribution colour, since a screen drawn in an agent's
blue reads as a design decision nobody made. Deciding what a screen should look
like is a different tool's job.
Verified on the live canvas at both presets, by eye as well as by assertion:
text heights measure 24.3 / 32.4 / 48.6 / 59.4 (tldraw's 18/24/36/44 at its 1.35
line height), Thai wraps and measures correctly, a two-line checkbox label grows
downward instead of reaching back over the element above it,
span: 4beside aplain sibling gives a quarter and three quarters, and a panel sizes itself to
what it holds. 140 tests and
tscpass.Known and accepted:
check-boxalways shows a tick, so an unchecked box cannotbe drawn; a select value long enough to fill the field runs under its caret; and
read_framestill reports a wireframe's by-design overlaps as issues.🤖 Generated with Claude Code