@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 +20 -14
- package/css/02-terminal.css +6 -0
- package/css/30-tabs.css +26 -13
- package/css/31-switcher.css +8 -6
- package/css/page.css +38 -1
- package/package.json +1 -1
- package/src/features/tabs/ink-centre.ts +37 -29
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
box
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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;
|
package/css/02-terminal.css
CHANGED
|
@@ -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 —
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
package/css/31-switcher.css
CHANGED
|
@@ -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
|
|
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
|
|
203
|
-
|
|
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
|
|
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
|
|
478
|
-
|
|
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.
|
|
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)
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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
|
|
27
|
-
//
|
|
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
|
|
68
|
-
*
|
|
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
|
|
71
|
-
return
|
|
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
|
|
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"
|
|
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
|
-
|
|
92
|
-
if (!(capInk > 0) || !(descInk > 0)) {
|
|
100
|
+
if (!(capInk > 0)) {
|
|
93
101
|
return null;
|
|
94
102
|
}
|
|
95
|
-
return { capInkEm: capInk / 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
|