fix(release): invert the width model so it cannot under-estimate

Chasing individual wide glyphs does not converge. Each review pass found
another one the list had missed — `W`, then CJK and emoji, then the `ae`
ligature, which passes the Latin range check and took the lowercase 0.6
factor while rendering about 1.05 em. Every miss crops the card silently,
because an under-estimate skips both the wrap and the textLength clamp.

The model is now inverted: narrow characters are enumerated and everything
else is assumed wide (1.15 em). Accented Latin, ligatures, Cyrillic, Greek,
CJK, kana, hangul and emoji all take the wide default, so the estimate can
only ever run high — and running high costs an early line break nobody
sees. There is no list left to miss an entry from.

The real guard is a new test that renders each sample through sharp and
asserts the estimate never falls below the measured ink width. It covers
Latin, ligatures, CJK, hangul, Cyrillic, symbols and accents, and would
have caught every one of the preceding rounds at once. Measured locally:
34 `W` renders 1669px against a 2033px estimate, 34 `ae` 1601px, 28 `CJK`
1459px — every case over-estimated, including both widths reported in
review from a different font environment.

190 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-27 10:38:14 +02:00
1 parent ae7077c283
commit a32ab20b0b
3 files changed
+91 -42

No files matched your search

+16 -6
View File
@@ -52,12 +52,22 @@ keep it headline-sized — roughly what the hero card fits on one line.
The cap is an authoring guideline, not a rendering guarantee: character count
is not width. Card text wraps by *estimated rendered width*
(`estimateTextWidth`, calibrated against a measured bold rendering and erring
high), because 34 `W` at font-size 52 measures ~1948px where 1072px are
available — a character-capped line still ran off the canvas. Text that cannot
fit even after wrapping is ellipsized, and every emitted line carries an SVG
`textLength` clamp when the estimate says it would still overflow, so a
mis-measured glyph compresses rather than crops.
(`estimateTextWidth`), because 34 `W` at font-size 52 measures ~1948px where
1072px are available — a character-capped line still ran off the canvas.
That estimate is deliberately **inverted**: narrow characters are enumerated
and everything else is assumed wide. Enumerating the wide ones instead cannot
converge — successive review passes each found another under-estimated glyph
(`W`, then CJK and emoji, then the `ae` ligature) — and a glyph the list misses
crops the card while every unit test still passes. With the wide default the
estimate can only run high, and running high costs an early line break nobody
sees. `highlight-cards.test.mjs` renders each sample through sharp and asserts
the estimate never falls below the measured ink width, which is the guard
against that whole class of bug.
Text that cannot fit even after wrapping is ellipsized, and each emitted line
carries an SVG `textLength` clamp when the estimate still says it would
overflow.
Highlights drive three behaviors: