@cplieger/web-terminal-ui 7.2.2 → 7.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/README.md +26 -13
- package/css/00-tokens.css +32 -1
- package/css/02-terminal.css +3 -1
- package/css/10-primitives.css +1 -0
- package/css/23-toolbar.css +1 -0
- package/css/30-tabs.css +61 -13
- package/css/page.css +59 -1
- package/package.json +1 -1
- package/src/features/activity-monitor.ts +37 -10
- package/src/features/animations.ts +23 -10
- package/src/features/clipboard.ts +26 -4
- package/src/features/connection-banner.ts +18 -5
- package/src/features/context-menu.ts +68 -48
- package/src/features/mobile-toolbar.ts +24 -7
- package/src/features/predictive-echo.ts +25 -6
- package/src/features/scroll-to-bottom.ts +19 -5
- package/src/features/tabs/attention.ts +4 -4
- package/src/features/tabs/index.ts +55 -16
- package/src/features/tabs/model.ts +7 -6
- package/src/kernel/kernel.ts +57 -5
- package/src/kernel/scrollback-storage.ts +12 -0
- package/src/kernel/startup-copy.ts +18 -16
- package/src/kernel/style-contract.ts +35 -33
- package/src/presets/agent-tabbed.ts +10 -7
- package/src/presets/single.ts +9 -5
- package/src/presets/tabbed.ts +12 -9
- package/src/presets/touch.ts +8 -4
- package/src/presets.ts +23 -17
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:
|
|
@@ -133,16 +139,17 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
|
|
|
133
139
|
- `presetTabbed()`: the generic tabbed UI, `presetTouch()` plus tabs, the
|
|
134
140
|
activity monitor, and animations. Each tab's title is OSC-first: it follows
|
|
135
141
|
the process window title (OSC 0/2) when the program sets one and keeps it
|
|
136
|
-
updated, otherwise the
|
|
142
|
+
updated, otherwise a name the server infers from the foreground process or the
|
|
143
|
+
working directory. The per-tab activity dot
|
|
137
144
|
reveals itself only when a session reports OSC 9;4 progress, so a plain shell
|
|
138
145
|
keeps clean, label-only tabs. Requires a server that speaks the session API
|
|
139
|
-
(`/api/sessions
|
|
146
|
+
(`/api/sessions`, `/ws?session=`, and the status SSE `/api/sessions/events`),
|
|
147
|
+
such as `web-terminal-server`.
|
|
140
148
|
- `presetAgentTabbed()`: the same feature set as `presetTabbed()`, tuned for an
|
|
141
|
-
agent shell such as `web-terminal-kiro
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
session's first OSC 9;4 signal. Its status dots come from the same activity
|
|
149
|
+
agent shell such as `web-terminal-kiro`, and differing from `presetTabbed()`
|
|
150
|
+
only in presumed activity reporting. With `presumeReports`, the idle activity
|
|
151
|
+
dot shows from tab creation instead of waiting for the session's first
|
|
152
|
+
OSC 9;4 signal. Its status dots come from the same activity
|
|
146
153
|
monitor and cover eight states: the three animated OSC 9;4 progress states
|
|
147
154
|
(working, warning, error), the frozen ringed needs-input dot, two static discs
|
|
148
155
|
(a green finished turn, a red crashed process), and two hollow ones (idle and a
|
|
@@ -185,7 +192,7 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
|
|
|
185
192
|
| `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
193
|
| `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
194
|
| `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.
|
|
195
|
+
| `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
196
|
| `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
197
|
| `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
198
|
| `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 +492,7 @@ about your server rather than about this terminal.
|
|
|
485
492
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
486
493
|
| `src/**/*.ts` | The UI modules: the kernel (`kernel/`), opt-in features (`features/`), per-preset entries (`presets/`), IME, predictive echo, viewport. |
|
|
487
494
|
| `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
|
|
495
|
+
| `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
496
|
| `scaffold/index.html` | A reference full-page HTML host: `<head>` + one empty root element + importmap. |
|
|
490
497
|
|
|
491
498
|
## Related projects
|
|
@@ -518,3 +525,9 @@ This project was built with AI-assisted tooling using [Claude](https://claude.co
|
|
|
518
525
|
## License
|
|
519
526
|
|
|
520
527
|
MPL-2.0. See [LICENSE](LICENSE).
|
|
528
|
+
|
|
529
|
+
The `Web Terminal Glyphs` tiling overlay named in `css/page.css` is a separate
|
|
530
|
+
Apache-2.0 work, and its licence travels with the FONT FILE — which this package
|
|
531
|
+
does not ship, it only names the URL. So no new obligation lands on an npm or JSR
|
|
532
|
+
consumer; the obligation is the serving host's, which is why the full-page host
|
|
533
|
+
notes above ask for the font's `LICENSE` and `NOTICE` beside the `.woff2`.
|
package/css/00-tokens.css
CHANGED
|
@@ -141,13 +141,44 @@
|
|
|
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
|
-
|
|
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.
|
|
152
|
+
|
|
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;
|
|
145
165
|
/* Pre-measurement default for the chip labels' ink centring; the tabs feature
|
|
146
166
|
writes a measured px value per site (src/features/tabs/ink-centre.ts owns
|
|
147
167
|
why). Zero: flex centring already lands within 0.4px of the cap band, and is
|
|
148
168
|
the best answer when the measurement cannot run. Do not tune it by eye. */
|
|
149
169
|
--label-ink-shift: 0em;
|
|
150
170
|
--dur-micro: 0.1s;
|
|
171
|
+
/* The activity vocabulary's one beat period, read by all three animated tab
|
|
172
|
+
cues — the working dot's glow, its travelling ripple, and the secondary
|
|
173
|
+
activity mark's breath (30-tabs.css). A token rather than three literals
|
|
174
|
+
because the three are phase-locked BY sharing it: the ripple launches off the
|
|
175
|
+
disc as the glow peaks, and the mark beside them reports on the same clock, so
|
|
176
|
+
retuning one alone desynchronises cues a reader sees side by side. It was a
|
|
177
|
+
comment saying "change both or neither" until 2026-09, which is the same rule
|
|
178
|
+
with nothing enforcing it.
|
|
179
|
+
Shared with vibekit's `--dot-beat-dur` (01-tokens.css) for the reason the
|
|
180
|
+
--status-* inks are: one signal across the two apps, beat included. */
|
|
181
|
+
--dot-beat-dur: 1.2s;
|
|
151
182
|
--dur-standard: 0.2s;
|
|
152
183
|
--dur-enter: 0.25s;
|
|
153
184
|
--dur-exit: 0.15s;
|
package/css/02-terminal.css
CHANGED
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
touch-action: pan-x pan-y;
|
|
46
46
|
font-family: var(--font-mono);
|
|
47
47
|
/* Bold and italic are real faces (page.css); a synthesized stand-in must
|
|
48
|
-
never be drawn in their place.
|
|
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. */
|
|
49
51
|
font-synthesis: none;
|
|
50
52
|
font-size: 14px;
|
|
51
53
|
/* The cell height. The engine reads it (render.ts updateFontMetrics) and sizes
|
package/css/10-primitives.css
CHANGED
package/css/23-toolbar.css
CHANGED
package/css/30-tabs.css
CHANGED
|
@@ -815,7 +815,7 @@
|
|
|
815
815
|
transparent 92%
|
|
816
816
|
);
|
|
817
817
|
opacity: 0;
|
|
818
|
-
animation: wt-working-glow
|
|
818
|
+
animation: wt-working-glow var(--dot-beat-dur) ease-in-out infinite;
|
|
819
819
|
will-change: opacity;
|
|
820
820
|
}
|
|
821
821
|
|
|
@@ -850,7 +850,7 @@
|
|
|
850
850
|
var(--dot-color) 76%,
|
|
851
851
|
transparent 100%
|
|
852
852
|
);
|
|
853
|
-
animation: wt-working-ripple
|
|
853
|
+
animation: wt-working-ripple var(--dot-beat-dur) linear infinite;
|
|
854
854
|
}
|
|
855
855
|
|
|
856
856
|
/* Agent done / ready: solid disc (--status-done), no motion. The default green
|
|
@@ -983,14 +983,44 @@
|
|
|
983
983
|
this size reads as an off-centre pulse. */
|
|
984
984
|
:where(.wt-root) .wt-activity-mark[data-activity="working"] {
|
|
985
985
|
--wt-mark-ink: var(--status-working);
|
|
986
|
+
/* The band, named because the MASK below has to know it: the overlay's hole is
|
|
987
|
+
the mark's inradius less this. Paired with the declaration it describes so the
|
|
988
|
+
two cannot drift apart on width. */
|
|
989
|
+
--wt-mark-band: 2px;
|
|
986
990
|
|
|
987
|
-
border-width:
|
|
991
|
+
border-width: var(--wt-mark-band);
|
|
988
992
|
transform: translateZ(0);
|
|
989
993
|
}
|
|
990
994
|
|
|
991
995
|
/* The breath: a pre-painted radial animating OPACITY only, never colours, so the
|
|
992
996
|
beat costs compositing rather than a repaint per frame for as long as a
|
|
993
|
-
background task runs (the battery reason the dot's glow overlay records).
|
|
997
|
+
background task runs (the battery reason the dot's glow overlay records).
|
|
998
|
+
|
|
999
|
+
PLUS A MASK, which is the one thing a ring needs and a disc does not, and whose
|
|
1000
|
+
absence was a real defect rather than a missing nicety. Over a solid disc the
|
|
1001
|
+
overlay's bright core has almost no headroom above its own backdrop; over a
|
|
1002
|
+
ring's transparent HOLE it has all of it, so an unmasked core FILLED the hole at
|
|
1003
|
+
the peak and the mark rendered as a solid rounded square. That is the one thing
|
|
1004
|
+
this element may never be: the mark and the activity dot 8px away share their ink
|
|
1005
|
+
at zero hue distance, so the ring-against-disc silhouette is the entire WCAG
|
|
1006
|
+
1.4.1 channel between them, and a fill collapses it for the brightest third of
|
|
1007
|
+
every cycle. Masked, the beat lights the BAND and the hole's four corners, so
|
|
1008
|
+
what a reader sees is the hole morphing between a square and the circle inscribed
|
|
1009
|
+
in it — the ring survives the peak.
|
|
1010
|
+
|
|
1011
|
+
The divisor is the arithmetic rather than a literal, for the reason the footprint
|
|
1012
|
+
cancel above is: the size is a variable and the band is now one too, so a literal
|
|
1013
|
+
would be right until either moved. Ported from vibekit's `.tab-run-dot`
|
|
1014
|
+
(`12-tabs.css`, "THE GLOW BEAT"), which is the same mark solving the same problem
|
|
1015
|
+
and had the mask from the start.
|
|
1016
|
+
|
|
1017
|
+
NOTHING HERE EXTENDS PAST THE MARK. `box-sizing: border-box` (01-scope.css) puts
|
|
1018
|
+
the padding box — an absolutely positioned child's containing block — INSIDE the
|
|
1019
|
+
band, so on a 9px mark with a 2px band that box is 5x5 and `inset: -1px` resolves
|
|
1020
|
+
to 7x7, still inside the border box. Read it as "fill the mark", not as "overhang
|
|
1021
|
+
it": the same declaration on a mark with no band would reach outside, where a
|
|
1022
|
+
feathered edge's visible radius moves with the beat and renders differently at
|
|
1023
|
+
every pixel density. */
|
|
994
1024
|
:where(.wt-root) .wt-activity-mark[data-activity="working"]::before {
|
|
995
1025
|
content: "";
|
|
996
1026
|
position: absolute;
|
|
@@ -1002,8 +1032,13 @@
|
|
|
1002
1032
|
transparent 92%
|
|
1003
1033
|
);
|
|
1004
1034
|
opacity: 0;
|
|
1005
|
-
animation: wt-activity-breath
|
|
1035
|
+
animation: wt-activity-breath var(--dot-beat-dur) ease-in-out infinite;
|
|
1006
1036
|
will-change: opacity;
|
|
1037
|
+
mask: radial-gradient(
|
|
1038
|
+
closest-side,
|
|
1039
|
+
transparent 0 calc(var(--wt-mark-size) / 2 - var(--wt-mark-band)),
|
|
1040
|
+
#000 calc(var(--wt-mark-size) / 2 - var(--wt-mark-band))
|
|
1041
|
+
);
|
|
1007
1042
|
}
|
|
1008
1043
|
|
|
1009
1044
|
/* Waiting: the thin band plus a static halo. Stopped and resumable, nobody being
|
|
@@ -1054,8 +1089,10 @@
|
|
|
1054
1089
|
usable weight through the travel. The ramp-in is short enough to read as
|
|
1055
1090
|
instant while stopping the emission from popping in.
|
|
1056
1091
|
|
|
1057
|
-
The disc's own glow shares this
|
|
1058
|
-
|
|
1092
|
+
The disc's own glow shares this period so the two cues stay phase-locked, and
|
|
1093
|
+
since 2026-09 that is STRUCTURAL rather than a request: all three animated cues
|
|
1094
|
+
read `--dot-beat-dur` (00-tokens.css), so there is no longer a pair of literals
|
|
1095
|
+
to change both of. */
|
|
1059
1096
|
@keyframes wt-working-ripple {
|
|
1060
1097
|
0% {
|
|
1061
1098
|
transform: scale(1);
|
|
@@ -1076,9 +1113,16 @@
|
|
|
1076
1113
|
}
|
|
1077
1114
|
}
|
|
1078
1115
|
|
|
1079
|
-
/* The activity mark's breath,
|
|
1080
|
-
|
|
1081
|
-
|
|
1116
|
+
/* The activity mark's breath: rest, one swell, rest. Opacity only — a colour
|
|
1117
|
+
keyframe here would reintroduce the per-frame repaint this shape exists to avoid.
|
|
1118
|
+
|
|
1119
|
+
The dot's glow ENVELOPE, at a lower CEILING, and the ceiling is where the two
|
|
1120
|
+
deliberately part. 0.55 rather than 1 is vibekit's number for the same mark
|
|
1121
|
+
(`12-tabs.css`), and it is what keeps a strip of busy tabs reading as one pulse
|
|
1122
|
+
instead of a row of flashes — the dot can afford 1 because its overlay lifts a
|
|
1123
|
+
solid disc of its own hue and has almost no headroom above its own backdrop,
|
|
1124
|
+
while this one is masked onto a 2px band over the page and has all of it. Used by
|
|
1125
|
+
the mark alone, so the value moves nothing else. */
|
|
1082
1126
|
@keyframes wt-activity-breath {
|
|
1083
1127
|
0%,
|
|
1084
1128
|
70%,
|
|
@@ -1087,7 +1131,7 @@
|
|
|
1087
1131
|
}
|
|
1088
1132
|
|
|
1089
1133
|
35% {
|
|
1090
|
-
opacity:
|
|
1134
|
+
opacity: 0.55;
|
|
1091
1135
|
}
|
|
1092
1136
|
}
|
|
1093
1137
|
|
|
@@ -1131,9 +1175,13 @@
|
|
|
1131
1175
|
|
|
1132
1176
|
/* Working loses its breath, so it takes the HEAVIEST band instead: motion is
|
|
1133
1177
|
REPLACED by a channel rather than removed, and the three states then separate
|
|
1134
|
-
by band alone (3px bare / 1px halo / 2px halo).
|
|
1178
|
+
by band alone (3px bare / 1px halo / 2px halo).
|
|
1179
|
+
|
|
1180
|
+
Re-pointing --wt-mark-band rather than declaring `border-width` keeps the number
|
|
1181
|
+
in the one place the state's own rule reads it from, the mask's divisor included
|
|
1182
|
+
— which is moot here only because the overlay is gone one rule below. */
|
|
1135
1183
|
:where(.wt-root) .wt-activity-mark[data-activity="working"] {
|
|
1136
|
-
|
|
1184
|
+
--wt-mark-band: 3px;
|
|
1137
1185
|
}
|
|
1138
1186
|
|
|
1139
1187
|
:where(.wt-root) .wt-activity-mark[data-activity="working"]::before {
|
package/css/page.css
CHANGED
|
@@ -236,7 +236,33 @@ body {
|
|
|
236
236
|
monospace stack.
|
|
237
237
|
|
|
238
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.
|
|
239
|
+
background is the run padding in 02-terminal.css, never these metrics.
|
|
240
|
+
|
|
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.
|
|
252
|
+
|
|
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.
|
|
258
|
+
|
|
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. */
|
|
240
266
|
@font-face {
|
|
241
267
|
font-family: "Monaspace Neon NF";
|
|
242
268
|
src: url("/vendor/fonts/MonaspaceNeonNF-Regular.woff2") format("woff2");
|
|
@@ -268,3 +294,35 @@ body {
|
|
|
268
294
|
font-style: italic;
|
|
269
295
|
font-display: block;
|
|
270
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;
|
|
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.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": "MPL-2.0",
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
/**
|
|
2
|
+
* activityMonitor feature: subscribes to the server's status SSE
|
|
3
|
+
* (/api/sessions/events via the engine's connectStatusStream) and exposes each
|
|
4
|
+
* session's live status through its API. It is a
|
|
5
|
+
* pure data source with no chrome of its own; the tabs feature consumes it via
|
|
6
|
+
* ctx.use to render per-tab activity dots and drop exited/removed tabs. The
|
|
7
|
+
* server pushes an initial snapshot on every (re)open — but the snapshot
|
|
8
|
+
* carries no tombstones for sessions a REPLACEMENT server never knew (manager
|
|
9
|
+
* restart), so the stream-open signal is surfaced via onStreamOpen and the
|
|
10
|
+
* consumer (tabs) runs a one-shot GET /api/sessions reconcile there to drop
|
|
11
|
+
* zombie tabs.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
11
15
|
|
|
12
16
|
import { connectStatusStream } from "@cplieger/web-terminal-engine";
|
|
13
17
|
import type { SessionStatus } from "@cplieger/web-terminal-engine";
|
|
@@ -15,6 +19,14 @@ import type { TerminalFeature, Unsubscribe } from "../kernel/types.js";
|
|
|
15
19
|
|
|
16
20
|
const DEFAULT_EVENTS_PATH = "/api/sessions/events";
|
|
17
21
|
|
|
22
|
+
/** The value a peer feature reads through `ctx.use(activityMonitor(...))`: the
|
|
23
|
+
* live status of every session the server knows about.
|
|
24
|
+
*
|
|
25
|
+
* Read-only — nothing here starts, stops or mutates a session. `current` answers
|
|
26
|
+
* from the last event seen rather than from the network, so it is undefined for
|
|
27
|
+
* a session no event has covered yet (the initial snapshot arrives
|
|
28
|
+
* asynchronously after setup) and for one the stream has since reported
|
|
29
|
+
* `removed`. A caller that needs "eventually" rather than "now" subscribes. */
|
|
18
30
|
export interface ActivityMonitorApi {
|
|
19
31
|
/** Subscribe to every status update (idle/working/warning/failed/input/done/
|
|
20
32
|
* exited/crashed, plus removed), including the OSC 9;4 percentage and any OSC
|
|
@@ -32,6 +44,21 @@ export interface ActivityMonitorApi {
|
|
|
32
44
|
onStreamOpen?(cb: () => void): Unsubscribe;
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
/** Build the activityMonitor feature.
|
|
48
|
+
*
|
|
49
|
+
* Requires a server that serves the status SSE endpoint; on a server without one
|
|
50
|
+
* the stream never opens, every `current` stays undefined and no subscriber ever
|
|
51
|
+
* fires, which is why `tabs` treats the monitor as optional and polls
|
|
52
|
+
* `GET /api/sessions` instead when it is absent.
|
|
53
|
+
*
|
|
54
|
+
* `eventsPath` overrides the endpoint (default `/api/sessions/events`). Order it
|
|
55
|
+
* BEFORE any feature that reads its API through `ctx.use`, since a feature's
|
|
56
|
+
* setup can only see peers already set up. Teardown closes the stream and drops
|
|
57
|
+
* every subscriber and cached status.
|
|
58
|
+
*
|
|
59
|
+
* A throwing subscriber is caught and logged rather than propagated: it must not
|
|
60
|
+
* skip the remaining subscribers, and it must not reach the engine's SSE reader
|
|
61
|
+
* and kill the stream for everyone. */
|
|
35
62
|
export function activityMonitor(
|
|
36
63
|
opts: { eventsPath?: string } = {},
|
|
37
64
|
): TerminalFeature<ActivityMonitorApi> {
|
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
/**
|
|
2
|
+
* animations feature: opt-in motion for the chrome surfaces.
|
|
3
|
+
* Without it the chrome surfaces are instant (no transitions); with
|
|
4
|
+
* it, and when the user has not asked for reduced motion, it enables the shared
|
|
5
|
+
* animation vocabulary by marking the terminal root, which the feature
|
|
6
|
+
* stylesheets key their transitions off. A few pieces of motion animate
|
|
7
|
+
* unconditionally regardless of this feature (terminal-core cursor/SGR blink and
|
|
8
|
+
* the mobile key toolbar open/close in 02-app.css, plus the working-status-dot
|
|
9
|
+
* pulse in 05-tabs.css, its WCAG non-hue cue), though prefers-reduced-motion
|
|
10
|
+
* still defeats all of it. It tracks prefers-reduced-motion live so toggling the
|
|
11
|
+
* OS setting takes effect.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
11
15
|
|
|
12
16
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
13
17
|
|
|
18
|
+
/** Build the animations feature. Exposes no API — its whole effect is the
|
|
19
|
+
* `wt-animate` class on the terminal root, which the shipped stylesheets gate
|
|
20
|
+
* their transitions on, so a composition either includes it or gets instant
|
|
21
|
+
* chrome.
|
|
22
|
+
*
|
|
23
|
+
* Position in the feature list does not matter (nothing reads it through
|
|
24
|
+
* `ctx.use`). The class is applied during setup, re-evaluated whenever the
|
|
25
|
+
* `prefers-reduced-motion` media query changes, and removed on teardown along
|
|
26
|
+
* with the listener. */
|
|
14
27
|
export function animations(): TerminalFeature {
|
|
15
28
|
return {
|
|
16
29
|
name: "animations",
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* clipboard feature: copy/paste helpers, the desktop Ctrl+Shift+C/V shortcuts,
|
|
3
|
+
* and OSC 52 mirroring. Exposes a typed API so contextMenu
|
|
4
|
+
* can offer Copy/Paste through it (ctx.use). With this feature absent, inbound
|
|
5
|
+
* OSC 52 has no subscriber and is intentionally a no-op.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
5
9
|
|
|
6
10
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
7
11
|
|
|
12
|
+
/** The value a peer feature reads through `ctx.use(clipboard())` — contextMenu's
|
|
13
|
+
* Copy and Paste items are this API behind a button.
|
|
14
|
+
*
|
|
15
|
+
* Both members are fire-and-forget: the underlying Clipboard API calls are async
|
|
16
|
+
* and their outcome is reported to the user as a toast rather than returned, so a
|
|
17
|
+
* caller cannot await or branch on success. Both require a secure context
|
|
18
|
+
* (`navigator.clipboard` is absent on a plain-HTTP non-loopback host, a supported
|
|
19
|
+
* web-terminal-server deployment), where they toast "Clipboard unavailable" and
|
|
20
|
+
* do nothing. */
|
|
8
21
|
export interface ClipboardApi {
|
|
9
22
|
/** Write text to the system clipboard (surfaces a toast on success/failure). */
|
|
10
23
|
copy(text: string): void;
|
|
@@ -12,6 +25,15 @@ export interface ClipboardApi {
|
|
|
12
25
|
paste(): void;
|
|
13
26
|
}
|
|
14
27
|
|
|
28
|
+
/** Build the clipboard feature.
|
|
29
|
+
*
|
|
30
|
+
* It claims three things for the lifetime of the terminal: a kernel keydown
|
|
31
|
+
* intercept (Ctrl+Shift+C copies the browser selection, Ctrl+Shift+V pastes, and
|
|
32
|
+
* a plain Ctrl+V is consumed WITHOUT preventDefault so the browser's own paste
|
|
33
|
+
* event reaches the hidden textarea instead of being mapped to `\x16`), the
|
|
34
|
+
* `wire:clipboard` event that mirrors an application's OSC 52 copy to the system
|
|
35
|
+
* clipboard, and a document `copy` listener that toasts "Copied" for a selection
|
|
36
|
+
* inside the terminal surface only. Teardown releases all three. */
|
|
15
37
|
export function clipboard(): TerminalFeature<ClipboardApi> {
|
|
16
38
|
return {
|
|
17
39
|
name: "clipboard",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* connectionBanner feature: renders the kernel's connection state as a visible
|
|
3
|
+
* banner. The state machine, the loading
|
|
4
|
+
* lifecycle, and the give-up hook are kernel-owned; this feature is a pure
|
|
5
|
+
* renderer subscribing to connection:state. It uses the kernel's single
|
|
6
|
+
* announcer (ctx.announce) rather than spawning its own aria-live region.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
6
10
|
|
|
7
11
|
import type { ConnState, TerminalFeature } from "../kernel/types.js";
|
|
8
12
|
|
|
@@ -17,6 +21,15 @@ const TEXT: Partial<Record<ConnState, string>> = {
|
|
|
17
21
|
incompatible: "Terminal protocol mismatch; update the server or reload this page",
|
|
18
22
|
};
|
|
19
23
|
|
|
24
|
+
/** Build the connectionBanner feature. Exposes no API — it owns one element in
|
|
25
|
+
* the kernel's "banner" region and nothing else reads from it.
|
|
26
|
+
*
|
|
27
|
+
* Only the states with copy above are shown; `connected` and any other state
|
|
28
|
+
* hides the banner, so it is a transient surface rather than a permanent status
|
|
29
|
+
* line. `restarted`, `ended` and `incompatible` are the terminal ones a user
|
|
30
|
+
* cannot wait out. Each change is announced once through `ctx.announce`, so a
|
|
31
|
+
* flaky link re-entering the same state does not repeat itself to a screen
|
|
32
|
+
* reader. Teardown unsubscribes and removes the element. */
|
|
20
33
|
export function connectionBanner(): TerminalFeature {
|
|
21
34
|
return {
|
|
22
35
|
name: "connectionBanner",
|