@cplieger/web-terminal-ui 7.2.1 → 7.3.0
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/README.md +18 -6
- package/css/00-tokens.css +24 -28
- package/css/02-terminal.css +19 -8
- package/css/30-tabs.css +11 -68
- package/css/page.css +57 -34
- package/package.json +2 -2
- package/src/features/tabs/ink-centre.ts +8 -38
- package/src/kernel/kernel.ts +57 -5
package/README.md
CHANGED
|
@@ -63,10 +63,16 @@ page that has one empty container element, then call
|
|
|
63
63
|
**Full-page host** (the terminal IS the page, as in `web-terminal-server` and
|
|
64
64
|
`web-terminal-kiro`): concatenate `css/MANIFEST` into the `style.css` your page
|
|
65
65
|
links. That reference bundle is `css/page.css` (the page kit: `html/body`
|
|
66
|
-
reset + the
|
|
67
|
-
`/vendor/fonts/`) plus the complete component set. The
|
|
68
|
-
the terminal display and the
|
|
69
|
-
|
|
66
|
+
reset + the `@font-face` rules for two families, expecting the font files at
|
|
67
|
+
`/vendor/fonts/`) plus the complete component set. The set is the four
|
|
68
|
+
`MonaspaceNeonNF-*.woff2` faces, which back both the terminal display and the
|
|
69
|
+
chrome text, plus `WebTerminalGlyphs.woff2` — a tiling overlay that supplies the
|
|
70
|
+
box-drawing, block, braille and powerline cells the terminal draws — served
|
|
71
|
+
alongside that font's `LICENSE` and `NOTICE`. Serving the Monaspace faces is not
|
|
72
|
+
optional for a full-page host. A host that has not yet added the overlay file
|
|
73
|
+
degrades cleanly rather than breaking: the face errors out, and the terminal
|
|
74
|
+
renders exactly as it did before, with the tiling ranges coming from Monaspace
|
|
75
|
+
and the system font.
|
|
70
76
|
|
|
71
77
|
**Embedder** (the terminal lives inside your app's layout, as a panel or pane):
|
|
72
78
|
concatenate the per-preset manifest matching your composition instead:
|
|
@@ -185,7 +191,7 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
|
|
|
185
191
|
| `features` | _(none; bare kernel)_ | A FUNCTION returning the feature list. Omitted builds only the terminal (no chrome). Pass a preset by name (`features: presetTabbed`) or a factory of your own; it is called inside the startup-failure boundary, so a preset that throws is handled rather than escaping. |
|
|
186
192
|
| `layout` | `"viewport"` | How the terminal claims space. `"viewport"`: the root becomes a fixed full-viewport box (the full-page product). `"container"`: the root fills your container element, which becomes the styling and positioning boundary (the embedded case). |
|
|
187
193
|
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
188
|
-
| `fontReady` | `'14px "Monaspace Neon NF"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback.
|
|
194
|
+
| `fontReady` | `'14px "Monaspace Neon NF"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback. Only the family that CARRIES those metrics is named: the tiling overlay copies them and holds no letter for the width probe to measure, and naming it as well would open the gate on the overlay alone in WebKit (Safari, every iOS browser), while Monaspace was still loading. The wait is bounded at 3s whatever the fonts do, the `font-display: block` period the bundled faces declare, so a request that stalls leaves the terminal sized on fallback metrics rather than never sized at all, and a font that lands after the bound costs one corrective resize. |
|
|
189
195
|
| `scrollbackLines` | _(engine default)_ | Retained scrollback lines per terminal, and per tab under the tabs feature (every per-tab cache is built through the same budget). This is the page's dominant memory dial: it bounds the styled-run arrays each session retains AND the DOM rows the renderer keeps (one row element per retained line), so a memory-constrained host (iOS Safari, where the content process is reclaimed under system pressure) passes a smaller budget at the price of a shorter scroll-back. A history budget floored at the live screen: the engine never evicts the current window, so a value at or below the terminal height keeps the full screen with no scrollback, but choose a value comfortably above the largest expected terminal height (a few multiples), since near or below it the batched eviction degrades to per-line churn. Non-integer or non-positive values are ignored. Left unset, the engine decides, and it decides twice: 5000 against a server that cannot serve history back, dropping to a 1500-line resident tail plus an on-demand cache once a server declares demand-paged scrollback. Prefer leaving it unset: the depth then lives on the server and the phone holds a working set. An explicit value opts OUT of that flip: it is an explicit memory decision and holds in both states. |
|
|
190
196
|
| `persistScrollback` | _(off)_ | Persist each session's scrollback across a page discard, through storage YOU supply. A page that is discarded and reloaded otherwise resumes holding nothing, so it asks the server for everything and refills its whole buffer over the wire (the normal case on iOS, where Safari evicts backgrounded tabs under memory pressure and returning to one re-runs the page). With a snapshot restored, the resume asks only for what was printed while the tab was gone. It helps the FRESH-LOAD case only: a warm reconnect and an in-page tab switch already replay nothing. Off by default because `localStorage` is a shared, origin-wide, quota-limited resource and this library is embedded in applications that keep their own state there: an application decides durability for its own users, a library does not decide it for an embedder. Applies to a single terminal and to every tab alike. See below. |
|
|
191
197
|
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders. |
|
|
@@ -485,7 +491,7 @@ about your server rather than about this terminal.
|
|
|
485
491
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
486
492
|
| `src/**/*.ts` | The UI modules: the kernel (`kernel/`), opt-in features (`features/`), per-preset entries (`presets/`), IME, predictive echo, viewport. |
|
|
487
493
|
| `css/*.css` + manifests | Root-scoped component styles. `MANIFEST` = the reference full-page bundle (`page.css` + the tabbed set); `MANIFEST.single/touch/tabbed` = component-only per-preset bundles for embedders. Concatenate in manifest order. |
|
|
488
|
-
| `css/page.css` | The page kit (full-page hosts only): `html/body` reset + the
|
|
494
|
+
| `css/page.css` | The page kit (full-page hosts only): `html/body` reset + the `@font-face` rules for the terminal's two web fonts, `Monaspace Neon NF` and the `Web Terminal Glyphs` tiling overlay. |
|
|
489
495
|
| `scaffold/index.html` | A reference full-page HTML host: `<head>` + one empty root element + importmap. |
|
|
490
496
|
|
|
491
497
|
## Related projects
|
|
@@ -518,3 +524,9 @@ This project was built with AI-assisted tooling using [Claude](https://claude.co
|
|
|
518
524
|
## License
|
|
519
525
|
|
|
520
526
|
MPL-2.0. See [LICENSE](LICENSE).
|
|
527
|
+
|
|
528
|
+
The `Web Terminal Glyphs` tiling overlay named in `css/page.css` is a separate
|
|
529
|
+
Apache-2.0 work, and its licence travels with the FONT FILE — which this package
|
|
530
|
+
does not ship, it only names the URL. So no new obligation lands on an npm or JSR
|
|
531
|
+
consumer; the obligation is the serving host's, which is why the full-page host
|
|
532
|
+
notes above ask for the font's `LICENSE` and `NOTICE` beside the `.woff2`.
|
package/css/00-tokens.css
CHANGED
|
@@ -141,35 +141,31 @@
|
|
|
141
141
|
strip (MANIFEST.touch excludes 30-tabs.css), so nothing metric-dependent
|
|
142
142
|
rides on that fallback. */
|
|
143
143
|
--font-ui: "Monaspace Neon NF", ui-monospace, monospace;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
144
|
+
/* The terminal display is an OVERLAY over a companion: "Web Terminal Glyphs"
|
|
145
|
+
is a tiling-only face (box drawing, blocks, braille, powerline — 1,098
|
|
146
|
+
codepoints, no letters, no digits, not even a space), so the browser takes
|
|
147
|
+
those cells from it and everything else from Monaspace by ordinary family
|
|
148
|
+
fallback. It leads deliberately, so its cell-exact glyphs win where the
|
|
149
|
+
companion also has a codepoint; carrying no text glyphs, it cannot move the
|
|
150
|
+
companion's metrics or its look. page.css declares the four faces and owns
|
|
151
|
+
the rest of the reasoning.
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
the
|
|
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. */
|
|
153
|
+
--font-ui above does NOT get the overlay, and that is a scope choice rather
|
|
154
|
+
than an omission: the chrome renders labels and never a tiling codepoint, so
|
|
155
|
+
the overlay would be a family the browser consults on every glyph and never
|
|
156
|
+
draws from. It would not cost a label geometry to add it — the overlay's
|
|
157
|
+
vertical metrics are copied from the companion (the upstream font's
|
|
158
|
+
dist/cell.json, 1890/-400/200 with USE_TYPO_METRICS set on both), so the
|
|
159
|
+
line box is the same either way. The reason to keep the chrome's stack at one
|
|
160
|
+
family is least mechanism, and --label-ink-shift below is why that matters:
|
|
161
|
+
it is written from the line box the engine actually built
|
|
162
|
+
(src/features/tabs/ink-centre.ts), so anything that DOES move the chrome's
|
|
163
|
+
metrics lands straight in the chip labels. */
|
|
164
|
+
--font-mono: "Web Terminal Glyphs", "Monaspace Neon NF", monospace;
|
|
165
|
+
/* Pre-measurement default for the chip labels' ink centring; the tabs feature
|
|
166
|
+
writes a measured px value per site (src/features/tabs/ink-centre.ts owns
|
|
167
|
+
why). Zero: flex centring already lands within 0.4px of the cap band, and is
|
|
168
|
+
the best answer when the measurement cannot run. Do not tune it by eye. */
|
|
173
169
|
--label-ink-shift: 0em;
|
|
174
170
|
--dur-micro: 0.1s;
|
|
175
171
|
--dur-standard: 0.2s;
|
package/css/02-terminal.css
CHANGED
|
@@ -44,13 +44,15 @@
|
|
|
44
44
|
(WebKit bug 240917), which the switcher-bar gesture relies on. */
|
|
45
45
|
touch-action: pan-x pan-y;
|
|
46
46
|
font-family: var(--font-mono);
|
|
47
|
+
/* Bold and italic are real faces (page.css); a synthesized stand-in must
|
|
48
|
+
never be drawn in their place. Not redundant with those declarations: Blink
|
|
49
|
+
obliques an italic run over the tiling overlay despite its exact italic face,
|
|
50
|
+
so this line is the only thing keeping box drawing upright there. */
|
|
51
|
+
font-synthesis: none;
|
|
47
52
|
font-size: 14px;
|
|
48
|
-
/* The cell height.
|
|
49
|
-
|
|
50
|
-
|
|
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. */
|
|
53
|
+
/* The cell height. The engine reads it (render.ts updateFontMetrics) and sizes
|
|
54
|
+
the PTY from it. The row background owes it nothing: see the run padding
|
|
55
|
+
below. */
|
|
54
56
|
line-height: 17px;
|
|
55
57
|
padding: 4px;
|
|
56
58
|
background: var(--bg);
|
|
@@ -82,6 +84,14 @@
|
|
|
82
84
|
content: "\00a0";
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
/* The cell background is THIS padding, not the font: an inline run paints its
|
|
88
|
+
font's content area (16px of the 17px row), and shipping Safari has no
|
|
89
|
+
ascent-override to widen it (webkit.org/b/219735, fixed in trunk 2026-08). */
|
|
90
|
+
:where(.wt-root) .term-row span,
|
|
91
|
+
:where(.wt-root) .term-row a {
|
|
92
|
+
padding-block: 1px;
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
/* "earlier output trimmed" marker (vterm renderer inserts .term-trim-marker
|
|
86
96
|
as the first row when history older than the buffer holds was evicted —
|
|
87
97
|
client-side cap or a server-reported resume gap). A quiet, centered,
|
|
@@ -91,6 +101,7 @@
|
|
|
91
101
|
font-size: 0.6875rem;
|
|
92
102
|
text-align: center;
|
|
93
103
|
padding: 2px 0;
|
|
104
|
+
-webkit-user-select: none;
|
|
94
105
|
user-select: none;
|
|
95
106
|
font-family: var(--font-ui);
|
|
96
107
|
}
|
|
@@ -141,12 +152,12 @@
|
|
|
141
152
|
explicitly and must never let an ancestor override them. `user-select`
|
|
142
153
|
is spec'd as non-inherited but WebKit/Chromium treat it as inherited,
|
|
143
154
|
so a stray `user-select: none` on html/body/.term would silently kill
|
|
144
|
-
selection here; keep it `text
|
|
145
|
-
form, so no `-webkit-` pair is needed). `-webkit-touch-callout: default`
|
|
155
|
+
selection here; keep it `text`. `-webkit-touch-callout: default`
|
|
146
156
|
keeps the iOS callout (a page-level `none` suppresses it) and has no
|
|
147
157
|
standard equivalent. No JS may `preventDefault` a touch/pointer/
|
|
148
158
|
contextmenu over this element or the native selection dies (see the
|
|
149
159
|
kernel focus handling + contextMenu feature). */
|
|
160
|
+
-webkit-user-select: text;
|
|
150
161
|
user-select: text;
|
|
151
162
|
-webkit-touch-callout: default;
|
|
152
163
|
|
package/css/30-tabs.css
CHANGED
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
font-size: 0.8125rem;
|
|
153
153
|
cursor: pointer;
|
|
154
154
|
white-space: nowrap;
|
|
155
|
+
-webkit-user-select: none;
|
|
155
156
|
user-select: none;
|
|
156
157
|
/* Kills the 300ms double-tap-to-zoom delay, which is what makes a double-TAP
|
|
157
158
|
usable as the touch rename entry: on iPadOS a long-press on this chip starts
|
|
@@ -224,81 +225,22 @@
|
|
|
224
225
|
background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 22%);
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
/*
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
the baseline than below it (the bundled font: 0.995em over, 0.250em under),
|
|
233
|
-
while the ink of a mixed-case title runs ~0.734em over (cap height) to ~0.203em
|
|
234
|
-
under (descender). Flex centring centres the BOX, which leaves the visible text
|
|
235
|
-
--label-ink-shift low — reading as baseline-aligned with the activity dot's
|
|
236
|
-
bottom edge instead of level with its centre. Two declarations, one job, and a
|
|
237
|
-
third that must NOT be added:
|
|
238
|
-
|
|
239
|
-
The band being centred is cap top to BASELINE. Descender ink is deliberately
|
|
240
|
-
outside it: including it reserves empty room below the letters, and the label
|
|
241
|
-
then reads HIGH — measured on this chip at 13px, the title's cap band sat
|
|
242
|
-
1.32px above the chip's centre while the dot and the close glyph beside it sat
|
|
243
|
-
at 0.00px. Same edges as `text-box-edge: cap alphabetic`. Descenders overhang
|
|
244
|
-
the band, which is what the padding below is for.
|
|
245
|
-
|
|
246
|
-
- padding-block is the ink allowance, not spacing. `overflow: hidden` (which
|
|
247
|
-
the ellipsis requires) clips at the padding box, and the line box clears the
|
|
248
|
-
tallest ink by only 0.6px, so an unpadded label shaves the top off every
|
|
249
|
-
acute accent and caron. 0.125em per side buys 2.2px of clearance at 13px, and
|
|
250
|
-
being symmetric it does not move the centred text.
|
|
251
|
-
- margin-block hangs TWICE the shift below the box. Flex centring centres the
|
|
252
|
-
MARGIN box, so a bottom-heavy margin lifts the border box by half of it --
|
|
253
|
-
exactly one shift -- and unlike padding it does not enlarge the clipping box.
|
|
254
|
-
The shift can be NEGATIVE (a top-heavy margin, pushing the box down) at sizes
|
|
255
|
-
where the engine's rounding already overshoots; that is a normal outcome of
|
|
256
|
-
the measurement, not a sign the arithmetic is inverted.
|
|
257
|
-
- line-height stays at `normal`, deliberately. Setting it looks like the
|
|
258
|
-
responsible thing to do and it is the trap: `normal` is the ONE value whose
|
|
259
|
-
half-leading is zero by definition, which is what makes the baseline sit at
|
|
260
|
-
exactly the engine's ascent below the box top. Any other value leaves
|
|
261
|
-
(line-height - font box) / 2 to be halved and rounded, and Chromium floors
|
|
262
|
-
it -- measured with an explicit 1.25 the 14px switcher labels came out a
|
|
263
|
-
FULL pixel high (line box 17.5 under an 18px font box, so half-leading
|
|
264
|
-
-0.25 floored to -1) while the 13px strip label was fine, which is exactly
|
|
265
|
-
the kind of works-where-I-looked bug this rule exists to end.
|
|
266
|
-
|
|
267
|
-
--label-ink-shift is MEASURED, not declared. The tabs feature writes a pixel
|
|
268
|
-
value per site from the line box the running engine produced for this font at
|
|
269
|
-
this size (src/features/tabs/ink-centre.ts); the em value in 00-tokens.css is
|
|
270
|
-
only what the first paint uses, and it is 0 because the cap-band correction is
|
|
271
|
-
within 0.4px of nothing on every engine measured. Four earlier fixes were
|
|
272
|
-
constants, and the reason none held is that engines round the font's ascent and
|
|
273
|
-
descent to whole CSS pixels before laying out the line box, so the required
|
|
274
|
-
shift is a sawtooth in font size, not a ratio: on the cap band it runs from
|
|
275
|
-
about -0.021em (Blink 151 at 13px, a 17px box) to +0.0279em (Gecko at 14px,
|
|
276
|
-
where the box is 17px against Blink's 18px), changing sign along the way,
|
|
277
|
-
because 0.25em x 13 = 3.25 and 0.25em x 14 = 3.5 round to different whole
|
|
278
|
-
pixels. The three engines
|
|
279
|
-
also report slightly different ink extents for one font file (cap 0.7344 /
|
|
280
|
-
0.7350 / 0.7300em in WebKit, Blink and Gecko), and iOS resolves metrics through
|
|
281
|
-
CoreText where Linux WebKit uses FreeType — which is why the 13px strip
|
|
282
|
-
measured centred in every local check and still read high on an
|
|
283
|
-
iPad. Nothing here is tunable by eye; if the centring looks wrong, run
|
|
284
|
-
scripts/verify-chip-geometry.mjs and fix what it measures.
|
|
285
|
-
|
|
286
|
-
What this deliberately does NOT use is `text-box: trim-both`, which three of
|
|
287
|
-
the four previous attempts were built on. It ships in Blink and WebKit only
|
|
288
|
-
(Firefox 151 still reports false for it), so it can only be applied behind an
|
|
289
|
-
@supports gate, and a gate means two engines render two different geometries of
|
|
290
|
-
which only the one the author is looking at ever gets tuned. Trimming to
|
|
291
|
-
`cap alphabetic` and giving the descender back as asymmetric padding IS
|
|
292
|
-
size-independent and holds to 0.05px in both engines that implement it — but
|
|
293
|
-
ungated it swings to 0.39px in Firefox, and a measured shift needs no gate and
|
|
294
|
-
no second code path to beat it. */
|
|
228
|
+
/* Centres the label's INK (cap top to baseline), not its line box; the shift is
|
|
229
|
+
measured per site by features/tabs/ink-centre.ts, which owns why. Not
|
|
230
|
+
`text-box: trim-both` (Firefox lacks it; a gate is two geometries, one
|
|
231
|
+
untuned), and `line-height` stays `normal`: Chromium floors any other
|
|
232
|
+
half-leading (1.25 put 14px labels a pixel high). */
|
|
295
233
|
:where(.wt-root) .wt-tab-label {
|
|
296
234
|
flex: 1 1 auto;
|
|
297
235
|
min-width: 0;
|
|
298
236
|
overflow: hidden;
|
|
299
237
|
white-space: nowrap;
|
|
300
238
|
text-overflow: ellipsis;
|
|
239
|
+
/* The ink allowance `overflow: hidden` needs (the line box clears an accent by
|
|
240
|
+
~0.6px); symmetric, so it moves nothing. */
|
|
301
241
|
padding-block: 0.125em;
|
|
242
|
+
/* Flex centres the MARGIN box, so twice the shift below lifts the border box
|
|
243
|
+
by one shift without growing the clip. A negative shift is a normal result. */
|
|
302
244
|
margin-block: 0 calc(2 * var(--label-ink-shift));
|
|
303
245
|
}
|
|
304
246
|
|
|
@@ -553,6 +495,7 @@
|
|
|
553
495
|
even though the property is non-inherited per spec (the same deviation the
|
|
554
496
|
touch model documents for .term-output). Without this, caret placement and
|
|
555
497
|
text selection break inside the field in Safari — i.e. on the iPad. */
|
|
498
|
+
-webkit-user-select: text;
|
|
556
499
|
user-select: text;
|
|
557
500
|
/* The chip is cursor: pointer; a text field must not inherit that. */
|
|
558
501
|
cursor: text;
|
package/css/page.css
CHANGED
|
@@ -28,7 +28,6 @@ body {
|
|
|
28
28
|
this, rotating a phone to landscape zooms the text. iOS Safari implements
|
|
29
29
|
ONLY the -webkit- prefixed form (the unprefixed property is ignored there),
|
|
30
30
|
so both are required — unprefixed alone left iPhones inflating on rotate. */
|
|
31
|
-
/* stylelint-disable-next-line property-no-vendor-prefix -- iOS Safari only implements the prefixed form */
|
|
32
31
|
-webkit-text-size-adjust: 100%;
|
|
33
32
|
text-size-adjust: 100%;
|
|
34
33
|
overscroll-behavior: none;
|
|
@@ -236,42 +235,40 @@ body {
|
|
|
236
235
|
either ships its own @font-face or lets both tokens fall back to the platform
|
|
237
236
|
monospace stack.
|
|
238
237
|
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
No face carries ascent-override or descent-override: the terminal's cell
|
|
239
|
+
background is the run padding in 02-terminal.css, never these metrics.
|
|
241
240
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
241
|
+
The tiling overlay, "Web Terminal Glyphs", is the second family below, and it
|
|
242
|
+
is ONE file declared under FOUR descriptor sets. The released face is a single
|
|
243
|
+
upright 400 weight, so a lone rule would leave the engine free to synthesise a
|
|
244
|
+
stand-in for a bold or italic run. What the four sets (400/700 x normal/italic)
|
|
245
|
+
against the same URL foreclose is the EMBOLDENED stand-in, in Blink and in
|
|
246
|
+
Gecko both: a bold run resolves a real face where the same asset under one rule
|
|
247
|
+
is thickened. They do NOT foreclose the OBLIQUE one in Blink, which honours the
|
|
248
|
+
italic descriptor for face SELECTION and slants the run anyway; the only thing
|
|
249
|
+
keeping an italic box-drawing corner upright there is font-synthesis: none on
|
|
250
|
+
.term (02-terminal.css). Both mechanisms are load-bearing and neither is the
|
|
251
|
+
whole guarantee. All four rules point at one file, so the bytes download once.
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
top edge, before and after), so no glyph shifts by restoring it. It is also
|
|
258
|
-
the metric pair the rest of this package is already tuned and documented
|
|
259
|
-
against: --label-ink-shift's sawtooth and ink-centre.ts both derive from
|
|
260
|
-
0.995em / 0.250em by name.
|
|
253
|
+
No overlay face declares unicode-range, and that absence is deliberate: the
|
|
254
|
+
font's own cmap IS its range (1,098 tiling codepoints, no letters, no digits,
|
|
255
|
+
no space), so a letter falls through to Monaspace by ordinary family fallback,
|
|
256
|
+
and a range here would only narrow that fallback to a list somebody has to
|
|
257
|
+
maintain against the next release of the font.
|
|
261
258
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
The overlay is listed FIRST in --font-mono and never in --font-ui
|
|
260
|
+
(00-tokens.css owns why). A full-page host must serve
|
|
261
|
+
WebTerminalGlyphs.woff2 beside the Monaspace faces, along with that font's
|
|
262
|
+
LICENSE and NOTICE: it is a separate Apache-2.0 work, and §4 binds whoever
|
|
263
|
+
ships the bytes. This package ships none, it only names the URL — so a host
|
|
264
|
+
that has not caught up errors the face out and renders exactly as it did
|
|
265
|
+
before, with the tiling ranges coming from Monaspace and the system font. */
|
|
267
266
|
@font-face {
|
|
268
267
|
font-family: "Monaspace Neon NF";
|
|
269
268
|
src: url("/vendor/fonts/MonaspaceNeonNF-Regular.woff2") format("woff2");
|
|
270
269
|
font-weight: 400;
|
|
271
270
|
font-style: normal;
|
|
272
271
|
font-display: block;
|
|
273
|
-
ascent-override: 99.5%;
|
|
274
|
-
descent-override: 25%;
|
|
275
272
|
}
|
|
276
273
|
|
|
277
274
|
@font-face {
|
|
@@ -280,8 +277,6 @@ body {
|
|
|
280
277
|
font-weight: 700;
|
|
281
278
|
font-style: normal;
|
|
282
279
|
font-display: block;
|
|
283
|
-
ascent-override: 99.5%;
|
|
284
|
-
descent-override: 25%;
|
|
285
280
|
}
|
|
286
281
|
|
|
287
282
|
@font-face {
|
|
@@ -290,8 +285,6 @@ body {
|
|
|
290
285
|
font-weight: 400;
|
|
291
286
|
font-style: italic;
|
|
292
287
|
font-display: block;
|
|
293
|
-
ascent-override: 99.5%;
|
|
294
|
-
descent-override: 25%;
|
|
295
288
|
}
|
|
296
289
|
|
|
297
290
|
@font-face {
|
|
@@ -300,6 +293,36 @@ body {
|
|
|
300
293
|
font-weight: 700;
|
|
301
294
|
font-style: italic;
|
|
302
295
|
font-display: block;
|
|
303
|
-
|
|
304
|
-
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@font-face {
|
|
299
|
+
font-family: "Web Terminal Glyphs";
|
|
300
|
+
src: url("/vendor/fonts/WebTerminalGlyphs.woff2") format("woff2");
|
|
301
|
+
font-weight: 400;
|
|
302
|
+
font-style: normal;
|
|
303
|
+
font-display: block;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@font-face {
|
|
307
|
+
font-family: "Web Terminal Glyphs";
|
|
308
|
+
src: url("/vendor/fonts/WebTerminalGlyphs.woff2") format("woff2");
|
|
309
|
+
font-weight: 700;
|
|
310
|
+
font-style: normal;
|
|
311
|
+
font-display: block;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@font-face {
|
|
315
|
+
font-family: "Web Terminal Glyphs";
|
|
316
|
+
src: url("/vendor/fonts/WebTerminalGlyphs.woff2") format("woff2");
|
|
317
|
+
font-weight: 400;
|
|
318
|
+
font-style: italic;
|
|
319
|
+
font-display: block;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@font-face {
|
|
323
|
+
font-family: "Web Terminal Glyphs";
|
|
324
|
+
src: url("/vendor/fonts/WebTerminalGlyphs.woff2") format("woff2");
|
|
325
|
+
font-weight: 700;
|
|
326
|
+
font-style: italic;
|
|
327
|
+
font-display: block;
|
|
305
328
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
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": "MPL-2.0",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@vitest/browser-playwright": "5.0.0",
|
|
57
57
|
"@vitest/coverage-v8": "5.0.0",
|
|
58
58
|
"eslint": "10.10.0",
|
|
59
|
-
"fast-check": "4.
|
|
59
|
+
"fast-check": "4.10.0",
|
|
60
60
|
"html-validate": "11.15.0",
|
|
61
61
|
"knip": "6.35.1",
|
|
62
62
|
"playwright": "1.63.0",
|
|
@@ -1,41 +1,11 @@
|
|
|
1
|
-
// Optical centring for the three chip labels
|
|
2
|
-
//
|
|
3
|
-
// font's
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// text's own.
|
|
10
|
-
//
|
|
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
|
|
29
|
-
// CoreText, Linux WebKit through FreeType, which is why the strip read correct
|
|
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.
|
|
33
|
-
//
|
|
34
|
-
// So nothing here predicts. Each measurement asks the engine for the line box
|
|
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
|
|
37
|
-
// default for the pre-measurement paint (see 00-tokens.css); this only ever
|
|
38
|
-
// narrows the error.
|
|
1
|
+
// Optical centring for the three chip labels, measured in the engine rather than
|
|
2
|
+
// predicted from the font's tables. Flex centring centres the LINE BOX, and a
|
|
3
|
+
// font's box reaches further above the baseline than below it, so the text sits
|
|
4
|
+
// low; the correction is the gap from the box's centre to the CAP band's (cap
|
|
5
|
+
// top to baseline, the `text-box-edge: cap alphabetic` edges; a band including
|
|
6
|
+
// descender ink reads high beside the dot and close glyph). Measured, not a
|
|
7
|
+
// constant, because engines round ascent and descent to whole CSS pixels before
|
|
8
|
+
// building the line box, and round differently per engine and platform.
|
|
39
9
|
import { fromHTML } from "../dom.js";
|
|
40
10
|
|
|
41
11
|
/** Ink extents are measured at this size and scaled back to em: WebKit and Blink
|
package/src/kernel/kernel.ts
CHANGED
|
@@ -54,7 +54,23 @@ import type {
|
|
|
54
54
|
const { mapKeyboardEvent, bracketTextForPaste, prepareTextForTerminal } = keyboard;
|
|
55
55
|
|
|
56
56
|
const DEFAULT_WS_PATH = "/ws";
|
|
57
|
+
// The family that carries the CELL METRICS, alone: deliberately not --font-mono's
|
|
58
|
+
// list, which leads with the tiling overlay. WebKit resolves load() over a list as
|
|
59
|
+
// soon as the FIRST family whose descriptor covers the sample has loaded
|
|
60
|
+
// (CSSFontFaceSet::matchingFacesExcludingPreinstalledFonts, `if (found) break;`),
|
|
61
|
+
// so naming the overlay would settle the gate on its 11 KB while the companion is
|
|
62
|
+
// still loading — and the overlay copies the companion's metrics and carries no
|
|
63
|
+
// "M", the glyph render.ts measures, so waiting on it defends no number anyway.
|
|
57
64
|
const DEFAULT_FONT_READY = '14px "Monaspace Neon NF"';
|
|
65
|
+
// The deadline on the WHOLE gate below, because neither document.fonts.load nor
|
|
66
|
+
// document.fonts.ready carries one: a response held open leaves both pending for
|
|
67
|
+
// as long as that request lives, and a gate that never settles reports no size at
|
|
68
|
+
// all. The budget is the block period the faces themselves buy with font-display:
|
|
69
|
+
// block (css/page.css declares it on every one; 3s is what CSS Fonts 4 recommends
|
|
70
|
+
// for `block`) — how long an engine withholds glyphs before it paints the
|
|
71
|
+
// fallback. Once it has, the cell on screen IS the fallback cell, so a longer wait
|
|
72
|
+
// defends metrics nobody is looking at.
|
|
73
|
+
const FONT_READY_TIMEOUT_MS = 3000;
|
|
58
74
|
|
|
59
75
|
const TOAST_MS = 3000;
|
|
60
76
|
// A touch that focuses the input (opens the soft keyboard) must be a genuine
|
|
@@ -1383,13 +1399,49 @@ function buildTerminal(
|
|
|
1383
1399
|
});
|
|
1384
1400
|
};
|
|
1385
1401
|
try {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1402
|
+
// false once the fonts have landed, true when the deadline opened the gate
|
|
1403
|
+
// without them. The race covers the whole wait, not just the rejection arm: a
|
|
1404
|
+
// request that stalls with nothing rejecting leaves load() ITSELF pending, so a
|
|
1405
|
+
// deadline armed on the rejection would bound one of the ways a font can hang.
|
|
1406
|
+
const landed: Promise<boolean> = document.fonts.load(fontReady).then(
|
|
1407
|
+
() => false,
|
|
1408
|
+
(err: unknown) => {
|
|
1390
1409
|
console.warn(`web-terminal-ui: web font ${fontReady} failed to load`, err);
|
|
1391
|
-
|
|
1410
|
+
// One load() over a comma-separated stack rejects as a UNIT, so settling
|
|
1411
|
+
// here would let an unavailable OPTIONAL family abandon the gate for the
|
|
1412
|
+
// REQUIRED one and announce a size measured on fallback metrics. `ready`
|
|
1413
|
+
// settles the initial load INCLUDING a failed one (the same property
|
|
1414
|
+
// features/tabs/ink-centre.ts relies on), so the families that ARE served
|
|
1415
|
+
// still land their bytes before the first resize is measured.
|
|
1416
|
+
return document.fonts.ready.then(() => false);
|
|
1417
|
+
},
|
|
1418
|
+
);
|
|
1419
|
+
const deadline = new Promise<boolean>((resolve) => {
|
|
1420
|
+
window.setTimeout(() => {
|
|
1421
|
+
resolve(true);
|
|
1422
|
+
}, FONT_READY_TIMEOUT_MS);
|
|
1423
|
+
});
|
|
1424
|
+
// Whichever settles first opens the gate exactly once; the loser is ignored.
|
|
1425
|
+
void Promise.race([landed, deadline]).then((timedOut) => {
|
|
1426
|
+
onFontSettled();
|
|
1427
|
+
if (!timedOut) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
// The gate opened on fallback metrics, and the swap period is infinite: bytes
|
|
1431
|
+
// that land after this change the cell width with fontsLoaded already true,
|
|
1432
|
+
// and nothing re-measures until the next viewport transition. So one
|
|
1433
|
+
// corrective announce, through the path the viewport settle uses.
|
|
1434
|
+
// sendResize deduplicates, so a font that never lands costs nothing.
|
|
1435
|
+
void document.fonts.ready.then(() => {
|
|
1436
|
+
// The bytes can arrive arbitrarily late, so this is the one arm of the gate
|
|
1437
|
+
// that outlives its terminal; destroy() wins, as it does for a feature's
|
|
1438
|
+
// un-cancelled async.
|
|
1439
|
+
if (isDestroyed() || measurableSize() === null) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
connection.sendResize();
|
|
1392
1443
|
});
|
|
1444
|
+
}, onFontSettled);
|
|
1393
1445
|
} catch (err) {
|
|
1394
1446
|
console.warn(`web-terminal-ui: invalid fontReady ${fontReady}`, err);
|
|
1395
1447
|
onFontSettled();
|