@cplieger/web-terminal-ui 5.3.0 → 5.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/css/00-tokens.css CHANGED
@@ -144,27 +144,33 @@
144
144
  --font-mono: "Monaspace Neon NF", monospace;
145
145
  /* How far a chip label's INK sits below its line box's centre, so flex
146
146
  centring can be corrected by exactly that much (see .wt-tab-label in
147
- 30-tabs.css).
147
+ 30-tabs.css). The band is the CAP band — cap top to baseline, the visible
148
+ mass of a label — not cap-to-descender, which reserves empty descender room
149
+ below the letters and reads high (measured: 1.32px above the chip's centre
150
+ at 13px, while the dot and close glyph beside it sat at 0.00px).
148
151
 
149
152
  This value is only the PRE-MEASUREMENT DEFAULT — what the first paint uses
150
153
  before the tabs feature replaces it, per site, with a pixel value measured
151
154
  in the live engine (src/features/tabs/ink-centre.ts). It is not the answer,
152
155
  because no single value can be: engines round the font's ascent and descent
153
156
  to whole CSS pixels before building the line box, so the ideal shift is a
154
- sawtooth in font size rather than a ratio. Against the bundled font in
155
- WebKit it is 0.1190em at 13px and 0.0915em at 14px, since 0.25em x 13 =
156
- 3.25 rounds to 3 (a 16px box) while 0.25em x 14 = 3.5 rounds to 4 (an 18px
157
- box). Engines then disagree with each other (Firefox makes that 14px box
158
- 17px) and platforms disagree within one engine (iOS reads metrics through
159
- CoreText, Linux WebKit through FreeType). Four fixes were hardcoded
160
- constants and each was correct only where it was tuned.
157
+ sawtooth in font size rather than a ratio, and on the cap band it does not
158
+ even keep one sign. Measured against the bundled font it runs from about
159
+ -0.021em (Blink 151 at 13px, a 17px box) to +0.028em (Gecko at 14px, where
160
+ the box is 17px against Blink's 18px), and platforms disagree inside one
161
+ engine too (iOS reads metrics through CoreText, Linux WebKit through
162
+ FreeType) — which is why four hardcoded constants each held only where they
163
+ were tuned.
161
164
 
162
- 0.1147em is kept as the default because it is the midpoint of that
163
- sawtooth: worst case 0.34px before the measurement lands, versus 1.5px for
164
- no correction at all. Do not tune it by eye — a wrong default is invisible
165
- the moment the measured value arrives, so eye-tuning it optimises nothing
166
- and pins the wrong number in place. */
167
- --label-ink-shift: 0.1147em;
165
+ ZERO is the default because the whole range is within ~0.03em of it: flex
166
+ centring of the font box is already within 0.4px of cap-band centring, so
167
+ the first paint starts uncorrected and the measurement lands within a frame.
168
+ That is also the best behaviour if the measurement never runs at all (no
169
+ canvas, a display:none probe): 0.4px out rather than the 1.3px a
170
+ descender-inclusive constant was. Do not tune it by eye — a wrong default is
171
+ invisible the moment the measured value arrives, so eye-tuning it optimises
172
+ nothing and pins the wrong number in place. */
173
+ --label-ink-shift: 0em;
168
174
  --dur-micro: 0.1s;
169
175
  --dur-standard: 0.2s;
170
176
  --dur-enter: 0.25s;
@@ -45,6 +45,12 @@
45
45
  touch-action: pan-x pan-y;
46
46
  font-family: var(--font-mono);
47
47
  font-size: 14px;
48
+ /* The cell height. It is PAIRED with the bundled font's overridden vertical
49
+ metrics (page.css): a cell's background is painted by an inline span, whose
50
+ box comes from the font's ascent + descent, so those must stay >= this value
51
+ or every row boundary shows an unpainted stripe. Changing either number
52
+ alone reopens that gap. The engine reads this as its cell height
53
+ (render.ts updateFontMetrics) and sizes the PTY from it. */
48
54
  line-height: 17px;
49
55
  padding: 4px;
50
56
  background: var(--bg);
package/css/30-tabs.css CHANGED
@@ -215,9 +215,16 @@
215
215
  the baseline than below it (the bundled font: 0.995em over, 0.250em under),
216
216
  while the ink of a mixed-case title runs ~0.734em over (cap height) to ~0.203em
217
217
  under (descender). Flex centring centres the BOX, which leaves the visible text
218
- --label-ink-shift low — about 1.5px at this size — reading as baseline-aligned
219
- with the activity dot's bottom edge instead of level with its centre. Two
220
- declarations, one job, and a third that must NOT be added:
218
+ --label-ink-shift low — reading as baseline-aligned with the activity dot's
219
+ bottom edge instead of level with its centre. Two declarations, one job, and a
220
+ third that must NOT be added:
221
+
222
+ The band being centred is cap top to BASELINE. Descender ink is deliberately
223
+ outside it: including it reserves empty room below the letters, and the label
224
+ then reads HIGH — measured on this chip at 13px, the title's cap band sat
225
+ 1.32px above the chip's centre while the dot and the close glyph beside it sat
226
+ at 0.00px. Same edges as `text-box-edge: cap alphabetic`. Descenders overhang
227
+ the band, which is what the padding below is for.
221
228
 
222
229
  - padding-block is the ink allowance, not spacing. `overflow: hidden` (which
223
230
  the ellipsis requires) clips at the padding box, and the line box clears the
@@ -227,6 +234,9 @@
227
234
  - margin-block hangs TWICE the shift below the box. Flex centring centres the
228
235
  MARGIN box, so a bottom-heavy margin lifts the border box by half of it --
229
236
  exactly one shift -- and unlike padding it does not enlarge the clipping box.
237
+ The shift can be NEGATIVE (a top-heavy margin, pushing the box down) at sizes
238
+ where the engine's rounding already overshoots; that is a normal outcome of
239
+ the measurement, not a sign the arithmetic is inverted.
230
240
  - line-height stays at `normal`, deliberately. Setting it looks like the
231
241
  responsible thing to do and it is the trap: `normal` is the ONE value whose
232
242
  half-leading is zero by definition, which is what makes the baseline sit at
@@ -240,16 +250,19 @@
240
250
  --label-ink-shift is MEASURED, not declared. The tabs feature writes a pixel
241
251
  value per site from the line box the running engine produced for this font at
242
252
  this size (src/features/tabs/ink-centre.ts); the em value in 00-tokens.css is
243
- only what the first paint uses. Four earlier fixes were constants, and the
244
- reason none held is that engines round the font's ascent and descent to whole
245
- CSS pixels before laying out the line box, so the required shift is a sawtooth
246
- in font size, not a ratio: in WebKit 0.1190em at 13px against 0.0915em at 14px,
247
- because 0.25em x 13 = 3.25 rounds to 3 (a 16px box) while 0.25em x 14 = 3.5
248
- rounds to 4 (an 18px box). The three engines also report slightly different ink
249
- extents for one font file (cap 0.7344 / 0.7350 / 0.7300em in WebKit, Blink and
250
- Gecko), Firefox makes that 14px box 17px rather than 18px, and iOS resolves
251
- metrics through CoreText where Linux WebKit uses FreeType — which is why the
252
- 13px strip measured centred in every local check and still read high on an
253
+ only what the first paint uses, and it is 0 because the cap-band correction is
254
+ within 0.4px of nothing on every engine measured. Four earlier fixes were
255
+ constants, and the reason none held is that engines round the font's ascent and
256
+ descent to whole CSS pixels before laying out the line box, so the required
257
+ shift is a sawtooth in font size, not a ratio: on the cap band it runs from
258
+ about -0.021em (Blink 151 at 13px, a 17px box) to +0.0279em (Gecko at 14px,
259
+ where the box is 17px against Blink's 18px), changing sign along the way,
260
+ because 0.25em x 13 = 3.25 and 0.25em x 14 = 3.5 round to different whole
261
+ pixels. The three engines
262
+ also report slightly different ink extents for one font file (cap 0.7344 /
263
+ 0.7350 / 0.7300em in WebKit, Blink and Gecko), and iOS resolves metrics through
264
+ CoreText where Linux WebKit uses FreeType — which is why the 13px strip
265
+ measured centred in every local check and still read high on an
253
266
  iPad. Nothing here is tunable by eye; if the centring looks wrong, run
254
267
  scripts/verify-chip-geometry.mjs and fix what it measures.
255
268
 
@@ -196,11 +196,12 @@
196
196
 
197
197
  /* Ink-centring geometry, identical to .wt-tab-label: a symmetric ink allowance
198
198
  the ellipsis clipping needs, plus a bottom margin of twice --label-ink-shift
199
- that lifts the ink to the row centre. These labels are 14px against the strip's
199
+ that lifts the CAP band (cap top to baseline, never cap-to-descender) to the row
200
+ centre. These labels are 14px against the strip's
200
201
  13px, which is exactly why --label-ink-shift is MEASURED per site at runtime
201
202
  (src/features/tabs/ink-centre.ts) rather than declared once: the shift an engine
202
- needs is a sawtooth in font size, not a ratio, so the single em constant that
203
- served both sizes was correct at 13px and 0.33px high here. See 30-tabs.css for
203
+ needs is a sawtooth in font size, not a ratio — it does not even keep one sign
204
+ across the two sizes. See 30-tabs.css for
204
205
  the full derivation, for why line-height must stay `normal`, and for why a
205
206
  measured shift beats a text-box trim behind an @supports gate. */
206
207
  :where(.wt-root) .wt-switcher-label {
@@ -471,11 +472,12 @@
471
472
 
472
473
  /* Ink-centring geometry, identical to .wt-tab-label: a symmetric ink allowance
473
474
  the ellipsis clipping needs, plus a bottom margin of twice --label-ink-shift
474
- that lifts the ink to the row centre. These labels are 14px against the strip's
475
+ that lifts the CAP band (cap top to baseline, never cap-to-descender) to the row
476
+ centre. These labels are 14px against the strip's
475
477
  13px, which is exactly why --label-ink-shift is MEASURED per site at runtime
476
478
  (src/features/tabs/ink-centre.ts) rather than declared once: the shift an engine
477
- needs is a sawtooth in font size, not a ratio, so the single em constant that
478
- served both sizes was correct at 13px and 0.33px high here. See 30-tabs.css for
479
+ needs is a sawtooth in font size, not a ratio — it does not even keep one sign
480
+ across the two sizes. See 30-tabs.css for
479
481
  the full derivation, for why line-height must stay `normal`, and for why a
480
482
  measured shift beats a text-box trim behind an @supports gate. */
481
483
  :where(.wt-root) .wt-switcher-row-label {
package/css/page.css CHANGED
@@ -215,13 +215,44 @@ body {
215
215
  different typeface per engine (see --label-ink-shift in 00-tokens.css). An
216
216
  embedder does not serve these files and never inherits these URLs, so it
217
217
  either ships its own @font-face or lets both tokens fall back to the platform
218
- monospace stack. */
218
+ monospace stack.
219
+
220
+ EVERY face overrides the font's vertical metrics to 0.995em over the baseline
221
+ and 0.250em under, and that is load-bearing for the terminal, not taste.
222
+
223
+ A cell's background is painted by an inline span, and an inline box paints its
224
+ FONT's content area, never the line box: at `font-size: 14px` with
225
+ `line-height: 17px` (02-terminal.css) a font whose ascent + descent is under
226
+ 17px leaves an unpainted stripe between consecutive rows, so an application
227
+ drawing a solid column of background (kiro-cli's green and purple block
228
+ gutters) renders as dashes. GitHub's own Monaspace webfont declares
229
+ ascent 1890/2000 and descent 400/2000 with USE_TYPO_METRICS set, which is
230
+ 0.945em + 0.200em = 16.03px at 14px, and Chromium rounds that to a 16px box
231
+ inside a 17px row: a 1px gap on every row boundary, measured in the sidecar.
232
+
233
+ 0.995em + 0.250em is what the nerd-fonts-patched MonaspiceNe NFM OTFs carried
234
+ until 5.3.0 swapped them for these WOFF2 files (the swap was gated on
235
+ horizontal advances only, which is how the regression shipped). It rounds to
236
+ an 18px box, so a row's background overhangs its box by 1px and consecutive
237
+ rows meet with no seam — and the baseline does not move (13px from the row's
238
+ top edge, before and after), so no glyph shifts by restoring it. It is also
239
+ the metric pair the rest of this package is already tuned and documented
240
+ against: --label-ink-shift's sawtooth and ink-centre.ts both derive from
241
+ 0.995em / 0.250em by name.
242
+
243
+ Do not "modernise" these away, and do not close the gap by lowering
244
+ `line-height` instead: that shrinks the cell, so it changes how many rows fit
245
+ a viewport and what the engine reports to the PTY. If a Monaspace bump ever
246
+ changes the outlines enough that a 0.995em ascent clips them, re-measure both
247
+ halves together — the terminal needs ascent + descent >= the cell height. */
219
248
  @font-face {
220
249
  font-family: "Monaspace Neon NF";
221
250
  src: url("/vendor/fonts/MonaspaceNeonNF-Regular.woff2") format("woff2");
222
251
  font-weight: 400;
223
252
  font-style: normal;
224
253
  font-display: block;
254
+ ascent-override: 99.5%;
255
+ descent-override: 25%;
225
256
  }
226
257
 
227
258
  @font-face {
@@ -230,6 +261,8 @@ body {
230
261
  font-weight: 700;
231
262
  font-style: normal;
232
263
  font-display: block;
264
+ ascent-override: 99.5%;
265
+ descent-override: 25%;
233
266
  }
234
267
 
235
268
  @font-face {
@@ -238,6 +271,8 @@ body {
238
271
  font-weight: 400;
239
272
  font-style: italic;
240
273
  font-display: block;
274
+ ascent-override: 99.5%;
275
+ descent-override: 25%;
241
276
  }
242
277
 
243
278
  @font-face {
@@ -246,4 +281,6 @@ body {
246
281
  font-weight: 700;
247
282
  font-style: italic;
248
283
  font-display: block;
284
+ ascent-override: 99.5%;
285
+ descent-override: 25%;
249
286
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "5.3.0",
3
+ "version": "5.3.1",
4
4
  "type": "module",
5
5
  "description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
6
6
  "license": "GPL-3.0-or-later",
@@ -4,27 +4,36 @@
4
4
  //
5
5
  // Flex centring centres a label's LINE BOX. A font's metrics box reaches much
6
6
  // further above the baseline than below (the bundled Monaspace Neon NF: 0.995em
7
- // over, 0.250em under) while a mixed-case title's ink runs cap height to
8
- // descender (0.734em over, 0.203em under), so the visible text sits low inside
9
- // the box it is centred by. The correction is the gap between those two centres.
7
+ // over, 0.250em under), so the visible text sits low inside the box it is
8
+ // centred by, and the correction is the gap between the box's centre and the
9
+ // text's own.
10
10
  //
11
- // That gap is NOT a font-relative constant, which is what four previous fixes
12
- // assumed. Engines round the font's ascent and descent to whole CSS pixels
13
- // before building the line box, so the ideal shift jumps whenever a size
14
- // crosses a rounding boundary: measured in WebKit against this font it is
15
- // 0.1190em at 13px but 0.0915em at 14px, because 0.25em x 13 = 3.25 rounds to 3
16
- // (16px box) while 0.25em x 14 = 3.5 rounds to 4 (18px box). Sweeping 11-20px
17
- // gives a sawtooth between 0.092em and 0.119em, so one em value is right only at
18
- // the sizes whose rounding it was tuned against — the shipped 0.1147em was
19
- // correct on the 13px desktop strip and 0.33px high on the 14px mobile chip.
20
- // Engines disagree with each other too (Firefox makes that same 14px box 17px,
21
- // not 18px), and so do platforms within one engine: iOS resolves metrics through
11
+ // WHICH BAND is the text's own is the load-bearing decision, and the first
12
+ // version of this got it wrong. It centred cap-top-to-DESCENDER-bottom, which
13
+ // reserves descender room below the letters and reads high: measured on the real
14
+ // chip in Blink, the title's cap band sat 1.320px above the chip's centre while
15
+ // the status dot and the close glyph beside it sat at exactly 0.000px, and a
16
+ // title is judged against those two neighbours. So the band is cap-top to
17
+ // BASELINE — the visible mass of a UI label, and the same pair of edges CSS
18
+ // spells `text-box-edge: cap alphabetic`. Descenders overhang it deliberately;
19
+ // the label's `padding-block` (30-tabs.css) is the allowance that keeps them
20
+ // clear of `overflow: hidden`, so nothing is clipped by the choice.
21
+ //
22
+ // The correction is NOT a font-relative constant, which is what four earlier
23
+ // fixes assumed. Engines round the font's ascent and descent to whole CSS pixels
24
+ // before building the line box, so the ideal shift jumps whenever a size crosses
25
+ // a rounding boundary: 0.25em x 13 = 3.25 rounds to 3 (a 16px box) while
26
+ // 0.25em x 14 = 3.5 rounds to 4 (an 18px box), and the shift moves with it.
27
+ // Engines disagree with each other too (Gecko makes that same 14px box 17px, not
28
+ // 18px), and so do platforms within one engine: iOS resolves metrics through
22
29
  // CoreText, Linux WebKit through FreeType, which is why the strip read correct
23
- // in every local check and high on an iPad.
30
+ // in every local check and high on an iPad. Measured across 11-20px the cap-band
31
+ // shift sawtooths between about -0.010em and +0.028em — small, but still wider
32
+ // than the 0.33px error that was reported as a defect, so it stays measured.
24
33
  //
25
34
  // So nothing here predicts. Each measurement asks the engine for the line box
26
- // and baseline it actually produced, asks the font for its actual ink extents,
27
- // and writes the difference to --label-ink-shift as pixels. The CSS keeps an em
35
+ // and baseline it actually produced, asks the font for its actual cap ink, and
36
+ // writes the difference to --label-ink-shift as pixels. The CSS keeps an em
28
37
  // default for the pre-measurement paint (see 00-tokens.css); this only ever
29
38
  // narrows the error.
30
39
  import { fromHTML } from "../dom.js";
@@ -42,7 +51,6 @@ const PROBE_CLASS = "wt-ink-probe";
42
51
  /** Cap ink, x-height ink and descender ink in one string, so the DOM probe's
43
52
  * line box is the one a real mixed-case title produces. */
44
53
  const PROBE_TEXT = "Hxp";
45
-
46
54
  /** Class chains that resolve each site's label font. Deliberately the shortest
47
55
  * chain that carries the font-size (`.wt-tab` and `.wt-switcher-current`) and
48
56
  * not the real ancestry: `.wt-switcher` is `display: none` off a coarse pointer
@@ -59,16 +67,17 @@ export interface InkMetrics {
59
67
  readonly baselinePx: number;
60
68
  /** Cap ink above the baseline, em. */
61
69
  readonly capInkEm: number;
62
- /** Descender ink below the baseline, em. */
63
- readonly descInkEm: number;
64
70
  readonly fontSizePx: number;
65
71
  }
66
72
 
67
- /** How far a label's visible ink sits BELOW its line box's centre. Positive is
68
- * the normal case (ink low), and the CSS lifts the box by exactly this much. */
73
+ /** How far a label's visible ink sits BELOW its line box's centre, where
74
+ * "visible ink" is the cap band (cap top to baseline). Positive is the normal
75
+ * case (ink low), and the CSS lifts the box by exactly this much.
76
+ *
77
+ * Descender ink is deliberately NOT part of the band; see the header. */
69
78
  export function inkShiftPx(m: InkMetrics): number {
70
- const inkCentre = m.baselinePx + ((m.descInkEm - m.capInkEm) * m.fontSizePx) / 2;
71
- return inkCentre - m.fontBoxPx / 2;
79
+ const capCentre = m.baselinePx - (m.capInkEm * m.fontSizePx) / 2;
80
+ return capCentre - m.fontBoxPx / 2;
72
81
  }
73
82
 
74
83
  let ctx2d: CanvasRenderingContext2D | null | undefined;
@@ -78,21 +87,20 @@ function canvas2d(): CanvasRenderingContext2D | null {
78
87
  return ctx2d;
79
88
  }
80
89
 
81
- /** The font's own ink extents, in em. Canvas rather than the DOM because only
90
+ /** The font's own cap ink, in em. Canvas rather than the DOM because only
82
91
  * TextMetrics reports where the glyphs' ink actually starts and stops; a DOM
83
92
  * rect only ever reports the box around it. */
84
- function inkExtents(font: string): Pick<InkMetrics, "capInkEm" | "descInkEm"> | null {
93
+ function inkExtents(font: string): Pick<InkMetrics, "capInkEm"> | null {
85
94
  const ctx = canvas2d();
86
95
  if (!ctx) {
87
96
  return null;
88
97
  }
89
98
  ctx.font = font;
90
99
  const capInk = ctx.measureText("H").actualBoundingBoxAscent;
91
- const descInk = ctx.measureText("p").actualBoundingBoxDescent;
92
- if (!(capInk > 0) || !(descInk > 0)) {
100
+ if (!(capInk > 0)) {
93
101
  return null;
94
102
  }
95
- return { capInkEm: capInk / REF_PX, descInkEm: descInk / REF_PX };
103
+ return { capInkEm: capInk / REF_PX };
96
104
  }
97
105
 
98
106
  /** The engine's line box and baseline for a laid-out element's own font. The