Skip to content

feat(deck-kit): size an icon-only control's glyph, from one constant - #295

Merged
andraderaul merged 2 commits into
mainfrom
feat/icon-glyph-size
Aug 2, 2026
Merged

feat(deck-kit): size an icon-only control's glyph, from one constant#295
andraderaul merged 2 commits into
mainfrom
feat/icon-glyph-size

Conversation

@andraderaul

@andraderaul andraderaul commented Aug 2, 2026

Copy link
Copy Markdown
Owner

The 44x44 targets landed without the mark inside them changing, so a control could be fully operable
and still read as unpressable — an 11px glyph adrift in a 44px box. touch-target.ts sizes the box;
ICON_GLYPH_SIZE sizes the mark, and the two files point at each other.

export const ICON_GLYPH_SIZE = 'text-md leading-none'   // 18px, ~41% of the 44px target

The rule

Icon-only is the whole condition, and it excludes two things. A control with a visible label
already has the word carrying it, so ✕ clear and ◈ analyze keep the size they inherit — growing
punctuation beside a word only unbalances the line. And a glyph that is no control at all — a
Toast's variant mark, a threat level, the * on a modified Preset — is decoration beside its own
text, so it takes the size of the text it decorates.

Never over the canvas. There the backdrop is the user's artwork (ADR 0013) or the piece itself
(ADR 0021), which is why that chrome stays at its drawn size and buys its target as an overlay. A
bigger glyph grows the chrome, which is the same charge by another route.

A constant, not a text-* per callsite. The scale guard cannot cover text- — it is fontSize
colorstext-center at once (theme/audit.ts), so a mistyped font step renders nothing and
no guard objects. fontSize was also the one scale the root CLAUDE.md vocabulary section never
listed, leaving the only unguarded scale as the only undocumented one; it is now a row in that
table, marked unguarded, with the two traps that follow — md is 18px and sits between base and
lg, not where Tailwind puts it, and since the preset extends rather than replaces, text-3xl
resolves to Tailwind's 30px and silently leaves the deck scale.

Callsites

Kit: tooltip trigger , modal close , toast dismiss ×.
GLITCH: duplicate, remove, + add effect, and IconLabelButton's glyph ( randomize,
re-roll).
ASCII//Convert: the AI-config banner dismiss ×.

IconLabelButton takes ICON_GLYPH_SIZE sm:text-sm on its inner <span>, not on the Button: the
Button's font size is an arbitrary property ([font-size:var(--btn-secondary-size)]) that
tailwind-merge does not read as a conflict. Below sm the glyph stands alone at 18px; from sm the
label is beside it and the glyph drops back to the Button's own 13px.

Verified in the browser

Both apps driven at 390px and 900px, sizes read off getComputedStyle:

mobile 390px desktop 900px
tooltip (5 sliders) 18px, label row 18px
/ chain-editor 18px in 44×44 18px in 44×44
randomize / re-roll 18px (alone) 13px (with label)
× clear / / over canvas 11px, 32px tall 11px, 32px
export png / export txt 13px 13px

The tooltip was the one callsite with a layout cost, and it came in as predicted: the Slider label
row went 11px → 18px, exactly +7px, bounded by the pinned line box. The EDIT panel still closes at
bottom: 664 in a 664px viewport, so the params stay on screen. Canvas overlays are untouched in
both apps — the regression that mattered most not to cause.

Full suite green (2137 tests across 5 workspaces), typecheck and Biome clean.

Review follow-ups (second commit)

A two-axis review ran over the first commit. Standards found no hard breach; Spec found no scope
creep and nothing behaviourally wrong. Five judgement calls were applied:

  • touch-target.ts now points back at icon-glyph.ts — the cross-reference had only run one way,
    and the box side is the older and better-known of the two.
  • The leading-none rationale had been written in four places; it now lives in the constant's
    JSDoc alone, with local comments carrying only the local fact.
  • ICON_GLYPHICON_GLYPH_SIZE: it holds a size, not a glyph, and reads honestly beside
    TOUCH_TARGET_HEIGHT / TOUCH_TARGET_ICON.
  • The fontSize row added to the scale vocabulary table (above).
  • The JSDoc had promised to record every exclusion and recorded three of four; decorative glyphs
    are now named.

Known, not addressed here

  • in ASCII//Convert's header has no glyph in the display font and renders as tofu — pre-existing,
    and out of the rule's scope since it has a label beside it.
  • The + chip is 29px wide above sm (Chip has min-h-[44px] but no min-w, and the callsite's
    sm:w-auto shrinks it). Pre-existing; this change widened it from ~20px rather than causing it.

🤖 Generated with Claude Code

The 44x44 targets landed without the mark inside them changing, so a
control could be fully operable and still read as unpressable -- an 11px
glyph adrift in a 44px box. `touch-target.ts` sizes the box; `ICON_GLYPH`
sizes the mark, and the two files now point at each other.

18px, with the line box pinned. `leading-none` rides inside the constant
rather than being left to each caller: the tightest caller is a Slider's
label row, where three stack on a phone, and pinning is what keeps such a
row growing by the glyph's 7px instead of a multiple of them.

Icon-only is the whole condition. A control with a visible label already
has the word carrying it, so `✕ clear` and `◈ analyze` keep the size they
inherit -- growing punctuation beside a word only unbalances the line.
Decorative glyphs are not controls and stay put too.

Never over the canvas. There the backdrop is the user's artwork (ADR 0013)
or the piece itself (ADR 0021), which is why that chrome stays at its
drawn size and buys its target as an overlay; a bigger glyph grows the
chrome, which is the same charge by another route.

A constant rather than a `text-*` at each callsite because the scale guard
cannot cover `text-` -- it is `fontSize` union `colors` union `text-center`
at once (`theme/audit.ts`), so a mistyped font step renders nothing and no
guard objects. One constant means no program names a font step for a
glyph, and `icon-glyph.test.ts` pins the one step it does name to the
preset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ascii-art-converter Ready Ready Preview Aug 2, 2026 2:55pm
atlas-sprawl Ready Ready Preview Aug 2, 2026 2:55pm
cyberdeck-golem Ready Ready Preview Aug 2, 2026 2:55pm
glitch-studio Ready Ready Preview Aug 2, 2026 2:55pm

… the scale it names

Three follow-ups from review.

`ICON_GLYPH` held a size, not a glyph. `ICON_GLYPH_SIZE` reads honestly
beside `TOUCH_TARGET_HEIGHT` / `TOUCH_TARGET_ICON`, which is the pair it
is meant to be read with -- and six callsites is as cheap as this rename
will ever be.

The `fontSize` scale was the one the vocabulary section never listed,
which left the only unguarded scale as the only undocumented one. It is
now a row in the table, marked unguarded, with the two traps that follow
from it: `md` is 18px and sits between `base` and `lg`, not where Tailwind
puts it; and since the preset extends rather than replaces, a step it does
not name -- `text-3xl` -- resolves to Tailwind's 30px and silently leaves
the deck scale. The guard-gap argument now lives there alone; the
icon-glyph section points at it instead of restating it.

The constant's JSDoc promised to record every exclusion and recorded three
of four. A decorative glyph -- a Toast's variant mark, a threat level, the
`*` on a modified Preset -- is not a control, and takes the size of the
text it decorates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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