Skip to content

Draw wireframe screens, and give text a size - #36

Merged
parintorns merged 3 commits into
masterfrom
expose-text-size
Sep 20, 2026
Merged

parintorns merged 3 commits into
masterfrom
expose-text-size

Conversation

@parintorns

Copy link
Copy Markdown
Contributor

The board could draw diagrams and could not draw a screen. Asking an agent to
mock one 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 — 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 at m, because
size was never passed by any tool and never settable afterwards. 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, which is made of type hierarchy.
create_shape and update_shape now take it, validation comes from the
published schema, and update_shape's local size bag (which held w/h) is
dims so the label re-measure runs when either the box or the text size
changed.

Draw a whole UI screen from a tree of elements — create_screen takes
column / row / panel containers around input, select, button,
checkbox, radio, image, divider and text, and draws them into their 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. Any node may carry
a key, and every shape it produced comes back under that key. The layout is a
pure module beside graph.ts with heights supplied through a measure
callback — testable without a browser, and correct about wrapping, because in
the browser that callback is editor.textMeasure, the same measurement tldraw
uses on its own text shapes.

Let a box carry its own label — a button was a rectangle with a separate
text 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: 4 beside a
plain sibling gives a quarter and three quarters, and a panel sizes itself to
what it holds. 140 tests and tsc pass.

Known and accepted: check-box always shows a tick, so an unchecked box cannot
be drawn; a select value long enough to fill the field runs under its caret; and
read_frame still reports a wireframe's by-design overlaps as issues.

🤖 Generated with Claude Code

parintorns and others added 3 commits September 20, 2026 14:43
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>

@endgame-code-triage endgame-code-triage Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@parintorns
parintorns merged commit 05a6603 into master Sep 20, 2026
1 check passed
@parintorns
parintorns deleted the expose-text-size branch September 20, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant