@cplieger/web-terminal-ui 8.5.0 → 8.6.0-dev.10

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 CHANGED
@@ -14,7 +14,7 @@ web-terminal-ui is built for a shell or a coding agent served by web-terminal-en
14
14
 
15
15
  - One `createTerminal()` call builds the UI inside an element you provide. Four presets cover a single, a touch and two tabbed layouts.
16
16
  - On a touchscreen it adds a key bar with Tab, Esc, arrows, Enter and a sticky Ctrl, a long-press paste menu and iOS soft-keyboard handling.
17
- - Tabs show activity dots from the program's `OSC 9;4` progress reports and raise browser notifications. Two tabs can sit side by side. A full-page terminal gives each tab and each split its own page address, and names the browser tab after the active tab, followed by the page's own title: `fix build · Web Terminal`.
17
+ - Tabs show activity dots from the program's `OSC 7501` status, `OSC 9;4` progress and classified `OSC 9` notifications, and raise browser notifications. Two tabs can sit side by side. A full-page terminal gives each tab and each split its own page address, and names the browser tab after the active tab, followed by the page's own title: `fix build · Web Terminal`.
18
18
  - Native text selection, IME input and dictation work, and predictive echo hides latency.
19
19
 
20
20
  Consider [xterm.js](https://github.com/xtermjs/xterm.js) if you want a terminal component for any PTY backend, such as node-pty. It has an optional GPU-accelerated renderer and a set of addons, and VS Code uses it. To build a UI of your own on web-terminal-engine, depend on the engine alone.
package/css/00-tokens.css CHANGED
@@ -26,8 +26,8 @@
26
26
  /* ===== Themeable tokens (the consumer-configurable "template") =====
27
27
  These default to the neutral built-in look; a consumer recolors the UI by
28
28
  overriding them via createTerminal's `theme` option (which sets them on the
29
- terminal root) or its own CSS. web-terminal-kiro sets a purple palette. Everything
30
- above is the fixed design palette; everything here is the public theme API. */
29
+ terminal root) or its own CSS. Everything above is the fixed design palette;
30
+ everything here is the public theme API. */
31
31
  --accent: oklch(72% 0.15 250deg); /* light blue; themed per consumer */
32
32
  --tab-bg: oklch(30% 0 0deg / 90%); /* inactive-tab chip (translucent) */
33
33
  --tab-hover-bg: var(--text-active); /* pressed controls / mobile switcher hovers */
@@ -61,10 +61,8 @@
61
61
  accent-derived fill (--tab-active-bg) rather than on the bar. Measured
62
62
  (sRGB, WCAG 1.4.3 formula, each layer composited over the page's #000):
63
63
  8.0:1 against the bar, but only 2.6:1 against the active-tab fill -- under
64
- the 3:1 non-text minimum. This comment previously claimed 5.2:1 for the
65
- active-tab case, which was wrong by ~2x; a consumer's code review quoted the
66
- figure back as its baseline and built a finding on it, so treat the number
67
- here as load-bearing and re-measure rather than adjust it by eye.
64
+ the 3:1 non-text minimum. Treat the number as load-bearing and re-measure
65
+ rather than adjust it by eye.
68
66
 
69
67
  Why it is nevertheless not a 1.4.1 failure, and why the fill is not simply
70
68
  darkened: hue never carries state on its own here -- working is a travelling
@@ -82,11 +80,12 @@
82
80
  --status-working: #52a9fe; /* agent working (waves): the same blue as the default --accent */
83
81
  --status-done: #22c55e; /* turn finished: solid green */
84
82
  --status-input: #fb923c; /* action required: solid orange + ring */
85
- /* OSC 9;4 progress states 4 (warning) and 2 (error). Both render as the SAME
86
- object as working -- identical glow + travelling wave, one shared rule and
87
- one shared pair of keyframes (30-tabs.css) -- differing ONLY in hue, because
88
- the escape code models one progress indicator with several states rather
89
- than three unrelated dots. Neither wears a ring: the frozen hard ring is
83
+ /* The warning and failed statuses (STATUS_WARNING and STATUS_FAILED in
84
+ features/tabs/model.ts name their sources). Both render as the SAME object
85
+ as working -- identical glow + travelling wave, one shared rule and one
86
+ shared pair of keyframes (30-tabs.css) -- differing ONLY in hue, because
87
+ OSC 9;4 models one progress indicator with several states rather than three
88
+ unrelated dots. Neither wears a ring: the frozen hard ring is
90
89
  --status-input's exclusive meaning ("blocked, waiting on you"), and neither
91
90
  warning nor error asks the user to act.
92
91
  LIGHTNESS SEPARATION IS LOAD-BEARING, NOT DECORATIVE. Three states that
@@ -102,8 +101,8 @@
102
101
  indistinguishable to a colour-blind user, whatever their hues. warning is
103
102
  YELLOW, deliberately not amber -- amber would collide with --status-input's
104
103
  orange (L 75.8%), which sits between working and warning. */
105
- --status-warning: #facc15; /* OSC 9;4 state 4: yellow disc, working's wave */
106
- --status-failed: #dc2626; /* OSC 9;4 state 2: red disc, working's wave; crashed reuses it solid and static */
104
+ --status-warning: #facc15; /* warning: yellow disc, working's wave */
105
+ --status-failed: #dc2626; /* a reported error: red disc, working's wave; crashed reuses it solid and static */
107
106
  --radius: 0.75rem;
108
107
  --radius-inner: 0.5rem;
109
108
  /* Small square icon buttons (e.g. the 24px tab-close x): at that size the
@@ -173,11 +172,7 @@
173
172
  activity mark's breath (30-tabs.css). A token rather than three literals
174
173
  because the three are phase-locked BY sharing it: the ripple launches off the
175
174
  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 marotte's `--dot-beat-dur` (01-tokens.css) for the reason the
180
- --status-* inks are: one signal across the two apps, beat included. */
175
+ retuning one alone desynchronises cues a reader sees side by side. */
181
176
  --dot-beat-dur: 1.6s;
182
177
  --dur-standard: 0.2s;
183
178
  --dur-enter: 0.25s;
package/css/01-scope.css CHANGED
@@ -44,8 +44,8 @@
44
44
  color-scheme: dark;
45
45
  /* Design-system scrollbar COLOR on every scroller in the subtree (the
46
46
  terminal, the tab scroller, the expanded switcher list): border-grey
47
- thumb, no track — the marotte/subflux treatment. scrollbar-color
48
- INHERITS, so the boundary is the one place to set it. Where it is not
47
+ thumb, no track. scrollbar-color INHERITS, so the boundary is the one
48
+ place to set it. Where it is not
49
49
  supported the color-scheme above still renders the UA fallback dark.
50
50
  Overlay-scrollbar platforms are unaffected. */
51
51
  scrollbar-color: var(--border) transparent;
@@ -68,16 +68,11 @@
68
68
  "calt" 0;
69
69
  font-kerning: none;
70
70
 
71
- /* Note: do NOT use `contain: layout style` here. iOS Safari has
72
- paint-suppression bugs in WebKit where layout-contained elements
73
- receiving frequent replaceChildren() updates can drop paint
74
- invalidations for inline children whose styling-only changes
75
- don't trigger layout — most visibly the inverse-styled "cursor"
76
- span Ink draws inside our rows, which would visually stick at
77
- its old column on space/arrow key presses until a content-
78
- changing keystroke forced a repaint. Without `contain`, every
79
- row update participates in normal paint flow and the cursor
80
- cell tracks the column on every flush. */
71
+ /* No `contain: layout style` here: iOS Safari (WebKit) can drop paint
72
+ invalidations for inline children of a layout-contained element that
73
+ replaceChildren() updates, so a program's inverse-styled cursor span
74
+ would stick at its old column on space or arrow keys until a
75
+ content-changing keystroke forced a repaint. */
81
76
  }
82
77
 
83
78
  :where(.wt-root) .term-row:empty::after {
@@ -213,17 +208,13 @@
213
208
  display: block;
214
209
  }
215
210
 
216
- /* Clickable URLs. OSC 8 hyperlinks (base .term-link) underline on HOVER where a
217
- pointer can hover: an application can attach one OSC 8 link to a whole
218
- padded/bordered region — e.g. a markdown link whose URL wraps inside a table
219
- cell, where the terminal keeps the link open across the cell padding, the
220
- borders and the empty adjacent column — so a persistent underline risks
221
- bleeding across the whole cell and, on wrap, the full row. Hover-only matches
222
- iTerm2 / VTE / kitty. The app's own text colour (which the engine renders)
223
- keeps the link discoverable at rest.
224
- Auto-detected bare URLs (.term-autolink) are tightly scoped to the URL text and
225
- never bleed, so they keep a subtle persistent underline. (The engine also only
226
- anchors runs with real link text, so the underline hugs the text either way.) */
211
+ /* Clickable URLs. An OSC 8 link (.term-link) underlines on HOVER only, as in
212
+ iTerm2, VTE and kitty: an application can hold one link open across a table
213
+ cell's padding, borders and the empty column beside it, so a persistent
214
+ underline would bleed across the cell and, on wrap, the whole row. The app's
215
+ own text colour keeps it discoverable at rest. A detected bare URL
216
+ (.term-autolink) spans only the URL text, so it keeps a subtle persistent
217
+ underline. */
227
218
  :where(.wt-root) .term-link {
228
219
  color: inherit;
229
220
  text-decoration: none;
@@ -238,25 +229,13 @@
238
229
  text-decoration: underline solid;
239
230
  }
240
231
 
241
- /* Where nothing can hover, hover-only is no affordance at all: a touch user sees
242
- an app-declared link as plain text and has to guess. Measured on kiro-cli at 41
243
- columns: it emits OSC 8 for every markdown link (its hyperlink capability
244
- unlocks on the TERM_PROGRAM=iTerm.app identity the engine advertises), so a
245
- phone session is full of tappable, invisible links. Give them the same subtle
246
- dotted underline the auto-detected ones carry.
247
-
248
- The bleed risk above is what kept this off, and it is narrower than it reads:
249
- the engine refuses to anchor a run with no real link text, so the padding, the
250
- box-drawing borders and the indent cells a wrapped link leaks onto the next row
251
- stay unanchored and unmarked (verified against a captured OSC 8 link that wraps
252
- mid-URL: the three leaked indent cells carry the href on the wire and get no
253
- anchor). What remains is an app leaving one link open across genuine text in an
254
- adjacent column, which would be underlined too. On a device that cannot hover,
255
- an occasional over-underline beats a link with no affordance.
256
-
257
- `hover: none` is the pointing device's capability, not a viewport width, so a
258
- narrow window on a desktop keeps hover styling and a tablet with a trackpad is
259
- treated as hoverable. */
232
+ /* Where the primary pointing device cannot hover, a hover-only underline gives
233
+ a touch interaction no persistent affordance: a TUI that emits OSC 8 for
234
+ every link it renders (a program may enable hyperlinks for the iTerm2
235
+ identity the engine advertises) fills a phone session with tappable,
236
+ invisible links. The bleed risk above stays small because the engine anchors
237
+ no run without real link text. `hover: none` describes the primary pointing
238
+ device, not viewport width, so a narrow desktop window keeps hover styling. */
260
239
  @media (hover: none) {
261
240
  :where(.wt-root) .term-link {
262
241
  text-decoration: underline dotted;
@@ -78,7 +78,7 @@
78
78
  real <button>: tabs, +, keyboard, switcher rows, menu items, the FAB).
79
79
  :focus-visible only — a tap/click never shows the ring (the UA heuristic),
80
80
  so the touch-first surfaces stay clean while Tab-key navigation gets the
81
- family's accent ring (the marotte/subflux convention: 2px accent, 2px out).
81
+ accent ring (2px accent, 2px out).
82
82
  Follows each control's border-radius; themeable via --accent like the "+"
83
83
  glyphs. The terminal's own hidden textarea is not a button and keeps its
84
84
  outline: none (02-terminal.css). */
package/css/30-tabs.css CHANGED
@@ -107,8 +107,8 @@
107
107
  width: 300px;
108
108
  flex: 0 1 auto;
109
109
  min-width: 100px;
110
- /* Positioning context for the chip's own overlay: the OSC 9;4 determinate
111
- progress bar sits on its bottom edge (see .wt-progress-bar). Nothing else
110
+ /* Positioning context for the chip's own overlay: the determinate progress
111
+ bar sits on its bottom edge (see .wt-progress-bar). Nothing else
112
112
  inside a chip is absolutely positioned, and the strip's two root-level
113
113
  overlays (.wt-tab-ghost, .wt-tab-menu) are children of .wt-root, not of a
114
114
  chip, so this cannot capture them. */
@@ -188,7 +188,7 @@
188
188
 
189
189
  /* The active tab reads clearly above the inactive chips: a stronger fill plus an
190
190
  accent-coloured edge (inactive chips carry a faint white --border; the active
191
- one gets the accent equivalent). Themeable via --accent (web-terminal-kiro -> purple). */
191
+ one gets the accent equivalent). Themeable via --accent. */
192
192
  :where(.wt-root) .wt-tab-active {
193
193
  background: var(--tab-active-bg);
194
194
  border-color: var(--tab-active-border);
@@ -474,55 +474,6 @@
474
474
  color: var(--tab-active-fg);
475
475
  }
476
476
 
477
- /* The split glyph (both split buttons) is filled, never stroked: a stroke is
478
- centred on its outline, so its edges sit half a stroke off the shape's.
479
- 20px is a whole number of device px at 1, 1.25, 1.5 and 2 dppx, and at 1.25
480
- and 1.5 the shapes move to that DPR's device grid, written as device px over
481
- the ratio (25 and 30 across, both mirrored about the centre). */
482
- :where(.wt-root) .wt-btn > .wt-split-icon {
483
- width: 20px;
484
- height: 20px;
485
- fill: currentcolor;
486
- stroke: none;
487
- }
488
-
489
- @media (resolution: 1.25dppx) {
490
- :where(.wt-root) .wt-split-icon-left {
491
- x: calc(1px / 1.25);
492
- width: calc(8px / 1.25);
493
- }
494
-
495
- :where(.wt-root) .wt-split-icon-divider {
496
- x: calc(11px / 1.25);
497
- y: calc(2px / 1.25);
498
- width: calc(3px / 1.25);
499
- height: calc(21px / 1.25);
500
- rx: calc(1.5px / 1.25);
501
- }
502
-
503
- :where(.wt-root) .wt-split-icon-right {
504
- x: calc(16px / 1.25);
505
- width: calc(8px / 1.25);
506
- }
507
- }
508
-
509
- @media (resolution: 1.5dppx) {
510
- :where(.wt-root) .wt-split-icon-left {
511
- x: calc(2px / 1.5);
512
- width: calc(9px / 1.5);
513
- }
514
-
515
- :where(.wt-root) .wt-split-icon-divider {
516
- x: calc(14px / 1.5);
517
- width: calc(2px / 1.5);
518
- }
519
-
520
- :where(.wt-root) .wt-split-icon-right {
521
- x: calc(19px / 1.5);
522
- width: calc(9px / 1.5);
523
- }
524
- }
525
-
526
477
  /* ===== Reorder drop slot (tabs feature, desktop drag) =====
527
478
  The dragged chip stays in the strip's flow for the whole gesture and renders as
528
479
  the SLOT it will land in: an empty dashed outline in the accent over a faint
@@ -708,25 +659,14 @@
708
659
  bottom: calc(var(--wt-tabbar-h, 44px) + var(--safe-bottom, 0px));
709
660
  }
710
661
 
711
- /* ===== Activity dots (activityMonitor via tabs) =====
712
- Eight-state, distinguishable without color (shape + motion, not hue alone),
713
- per section 12 / WCAG 1.4.1: three ANIMATED progress states (working /
714
- warning / failed — one object, three hues, spread across lightness so they
715
- survive greyscale and deuteranopia), a frozen ringed disc (input), two static
716
- discs (green done, red crashed), and two hollow ones (idle, dim exited).
717
- The one .wt-status-dot vocabulary is shared by the desktop strip, the mobile
718
- switcher, and the overview sheet rows.
719
-
720
- Revealed PER TAB: the dot is hidden until its own session reports activity
721
- (OSC 9;4 progress, or a classified OSC 9 notification), at which point the
722
- tabs feature adds .wt-reports to that dot (paintStatusDot). A program that
723
- emits no OSC 9 signal (a plain bash/sh) has no activity concept, so its tabs
724
- stay clean, label-only; a progress-reporting agent (kiro-cli, Claude Code)
725
- lights up. An agent-shell consumer can PRESUME the report instead (tabs'
726
- presumeReports option, on in presetAgentTabbed): .wt-reports is then set
727
- from tab creation, so the idle dot shows immediately rather than popping in
728
- after the agent's first signal. The element stays in the DOM either way (JS
729
- sets data-status regardless), so revealing it is a pure CSS switch. */
662
+ /* ===== Activity dots: one .wt-status-dot vocabulary for the strip, the
663
+ switcher and the overview rows. Eight built-in states told apart without
664
+ colour (WCAG 1.4.1, by shape and motion): three animated progress hues
665
+ (working, warning, failed) spread across lightness, a ringed disc (input), two
666
+ static discs (done, crashed) and two hollow ones (idle, exited); any other
667
+ status keeps the default hollow dot below. A dot shows once its session
668
+ reports activity (.wt-reports, set by paintStatusDot) or from creation under
669
+ tabs' presumeReports option; data-status is set either way. */
730
670
  :where(.wt-root) .wt-status-dot {
731
671
  display: none;
732
672
  width: 9px;
@@ -748,12 +688,10 @@
748
688
  animation: none;
749
689
  }
750
690
 
751
- /* ===== Determinate progress bar (OSC 9;4 percentage) =====
752
- The second display mode the specs name for a progress report (iTerm2 calls it
753
- a progress bar; ConEmu also puts the value on the taskbar, which is the
754
- document-title prefix's job). One 2px line on the chip's bottom edge, shared
755
- by all three chip sites (desktop strip chip, mobile active row, expanded list
756
- row) because it ships in the one chipContent builder.
691
+ /* ===== Determinate progress bar (the session's percentage: OSC 7501 progress, else OSC 9;4) =====
692
+ One 2px line on the chip's bottom edge, shared by all three chip sites
693
+ (desktop strip chip, mobile active row, expanded list row) because it ships
694
+ in the one chipContent builder.
757
695
 
758
696
  The ACCENT colour, not a per-state tint: the bar answers "how far", the dot
759
697
  answers "in what state", and tinting the bar per state would say the same
@@ -806,8 +744,8 @@
806
744
  the element), which read as a blink and coupled the layers — an element's
807
745
  opacity always multiplies onto its pseudos, so true independence requires
808
746
  animating disc COLORS, not element opacity. Server-side this is driven by the
809
- OSC 9;4 progress signal, so it tracks agent activity, not the user typing at
810
- the prompt.
747
+ program's own status reports, so it tracks agent activity, not the user
748
+ typing at the prompt.
811
749
 
812
750
  WORKING, WARNING AND FAILED SHARE THIS RULE, AND BOTH KEYFRAMES, VERBATIM.
813
751
  OSC 9;4 models ONE progress indicator with several states (1/3 active,
@@ -925,8 +863,8 @@
925
863
  }
926
864
 
927
865
  /* Agent done / ready: solid disc (--status-done), no motion. The default green
928
- matches Kiro IDE's "Ready" (#22c55e). It sits at almost exactly working's
929
- default lightness now that working is the accent blue (both ~72% OKLab L), so
866
+ (#22c55e) sits at almost exactly working's default lightness now that
867
+ working is the accent blue (both ~72% OKLab L), so
930
868
  the pair separates by hue plus the wave's motion rather than by lightness —
931
869
  and under prefers-reduced-motion by shape, since working becomes a donut. */
932
870
  :where(.wt-root) .wt-status-dot[data-status="done"] {
@@ -934,17 +872,11 @@
934
872
  border-color: var(--status-done);
935
873
  }
936
874
 
937
- /* User action required (permission / approval): a solid dot inside a static,
938
- HARD-edged 2px ring, motionless (--status-input; the default orange is
939
- shifted away from the old amber so it separates from both blue working and
940
- green done). The stillness plus the hard edge is the message: the
941
- agent's turn is mid-flight but blocked on you, and the surround is a fixed
942
- outline rather than working's soft travelling wave. (Before the wave landed,
943
- working's ripple was a 2px ring too and this state was literally that ping
944
- frozen at its birth frame; the two now differ by edge quality as well as
945
- motion, which reads more distinctly at 9px.) The ring derives from the same
946
- token as the dot, so a theme override recolors both together. It persists
947
- until you act on or switch into the tab. */
875
+ /* User action required: a solid dot inside a static, hard-edged 2px ring
876
+ (--status-input). Stillness and the hard edge separate it from working's
877
+ soft travelling wave. The ring derives from the dot's token, so a theme
878
+ override recolors both. It persists until you act on or switch into the
879
+ tab. */
948
880
  :where(.wt-root) .wt-status-dot[data-status="input"] {
949
881
  background: var(--status-input);
950
882
  border-color: var(--status-input);
@@ -140,7 +140,7 @@
140
140
  /* Right padding reserves room for the close (x) overlaid inside the row. */
141
141
  padding: 0 var(--touch-target) 0 var(--sp-2);
142
142
  /* The bar row always represents the active tab, so it wears the active-tab
143
- fill/foreground the desktop strip uses (themeable; purple in web-terminal-kiro). */
143
+ fill/foreground the desktop strip uses (themeable). */
144
144
  background: var(--tab-active-bg);
145
145
  color: var(--tab-active-fg);
146
146
  border: 1px solid var(--tab-active-border);
@@ -360,7 +360,7 @@
360
360
  process crashed or reported an error. Those four only (CueStatus) — the rule
361
361
  the user set is "only states that want the user, like a notification", so the
362
362
  ongoing progress states (working / warning) never raise it. `failed` DOES,
363
- because OSC 9;4 state 2 is a result the program parked rather than a phase in
363
+ because a reported error is a result the program parked rather than a phase in
364
364
  progress; the override below is what keeps it looking like a notification here.
365
365
  Reuses the exact .wt-status-dot rules (30-tabs.css, --status-* tokens) so it
366
366
  matches the per-tab dots. Hidden until the tabs feature sets data-status;
@@ -410,7 +410,7 @@
410
410
 
411
411
  /* Mobile "+": the primary action, carried in the accent colour so it reads as
412
412
  permanent and prominent. Hover fill is gated on any-hover (see the note in
413
- 10-primitives.css): on a bare touchscreen any-hover is none, so the purple
413
+ 10-primitives.css): on a bare touchscreen any-hover is none, so the accent
414
414
  cannot linger after a tap and the press feedback comes from :active; a trackpad
415
415
  or Pencil reports hover and gets the wash, which a (hover: hover) gate denied it.
416
416
  */
package/css/MANIFEST CHANGED
@@ -1,7 +1,7 @@
1
1
  # web-terminal-ui CSS — the REFERENCE FULL-PAGE bundle.
2
- # What the full-page product serves (web-terminal-server, web-terminal-kiro,
3
- # scaffold/index.html): the page kit (page.css — html/body reset + @font-face;
4
- # full-page hosts ONLY) plus the complete tabbed component set. An EMBEDDING
2
+ # What a full-page host serves (as scaffold/index.html does): the page kit
3
+ # (page.css — html/body reset + @font-face; full-page hosts ONLY) plus the
4
+ # complete tabbed component set. An EMBEDDING
5
5
  # consumer must NOT serve this file's bundle — it concatenates the per-preset
6
6
  # manifest matching its composition instead (MANIFEST.single / MANIFEST.touch /
7
7
  # MANIFEST.tabbed), which contains no page-level rules.
@@ -1,6 +1,6 @@
1
1
  # Component bundle for presetTouch() compositions (single + the mobile key
2
2
  # toolbar). Library styles only — root-scoped, no page-level rules; the host
3
- # page owns its own reset/fonts (marotte's embedded panel serves exactly this).
3
+ # page owns its own reset/fonts (an embedded panel serves exactly this).
4
4
  # Files concatenate in declared order; the order IS the cascade.
5
5
  00-tokens.css
6
6
  01-scope.css
package/css/page.css CHANGED
@@ -1,9 +1,9 @@
1
1
  /* ===== Page kit (full-page hosts ONLY — never ship this to an embedder) =====
2
2
  The page-level half of the full-page terminal product: what the HOST PAGE
3
- owns when the terminal IS the page (web-terminal-server, web-terminal-kiro,
4
- scaffold/index.html). The component bundle (00-10) never touches the host
5
- document; everything here is deliberately page-scoped. An embedding consumer
6
- (marotte) serves a per-preset manifest instead and owns its own page.
3
+ owns when the terminal IS the page (as in scaffold/index.html). The component
4
+ bundle (00-10) never touches the host document; everything here is
5
+ deliberately page-scoped. An embedding consumer serves a per-preset manifest
6
+ instead and owns its own page.
7
7
  Concatenated FIRST by the reference MANIFEST. */
8
8
 
9
9
  *,
@@ -39,30 +39,13 @@ body {
39
39
  }
40
40
 
41
41
  /* ===== The pre-JS loading overlay =====
42
- ONE definition of the loading screen, owned here. Before this, the library
43
- only PRESCRIBED it in scaffold/index.html and every full-page consumer carried
44
- its own copy in an inline <style> — web-terminal-server's was byte-identical to
45
- the scaffold's, web-terminal-kiro's had drifted (renamed keyframe, extra
46
- states) — so the "one" loading screen existed three times and none of them
47
- could be improved without editing three files.
48
-
49
- Why it can live in a stylesheet at all, since the overlay's whole job is to be
50
- on screen before the app is: a full-page host loads this bundle through a plain
51
- render-blocking <link rel="stylesheet"> in <head>, so NOTHING paints until it
52
- arrives. An inline copy buys no earlier paint. What an inline copy does buy is
53
- the case where this stylesheet FAILS to load, which a consumer's own bootstrap
54
- watchdog reports — so a consumer keeps a deliberately minimal inline fallback
55
- for that failure surface only, not a second copy of everything below.
56
-
57
- Consumer overrides work the same way the component tokens do, except simpler:
58
- the overlay is the consumer's OWN element, so it sets the tokens on #loading in
59
- its own CSS and needs no JS. web-terminal-kiro already did exactly this with
60
- --accent before any of this was tokenised.
61
-
62
- Selected by CLASS, never by the consumer's `id`: the library owns .wt-loading*
63
- and must not reach into a host document's own identifiers (the same
64
- no-element-id-contract rule the kernel follows for the terminal subtree), and
65
- an unnamespaced `.bar` in a host page would collide with anything. */
42
+ The one definition, so a full-page consumer links it rather than carrying a
43
+ drifting inline copy. A full-page host loads this bundle render-blocking in
44
+ <head>, so an inline copy paints no earlier; a consumer keeps only a minimal
45
+ inline fallback for a failed stylesheet load. The overlay is the consumer's
46
+ own element, so it overrides by setting the tokens on it. Selected by CLASS,
47
+ never by id: the library owns .wt-loading* and does not reach into the host
48
+ document's identifiers, and an unnamespaced .bar would collide. */
66
49
  .wt-loading {
67
50
  /* The overlay's accent: the bar and the status text derive from it, so one
68
51
  override recolors the whole screen. Defaults to the component --accent
@@ -225,44 +208,12 @@ body {
225
208
  }
226
209
 
227
210
  /* ===== Fonts =====
228
- The bundled web font, served by the full-page host at /vendor/fonts (both
229
- web-terminal-server and web-terminal-kiro ship these files there; see their
230
- Dockerfiles). It backs BOTH --font-mono (the terminal display) and --font-ui
231
- (the chrome), the latter deliberately: chip-label ink centring is a per-font
232
- constant, so the chrome cannot afford a system-font stack that resolves to a
233
- different typeface per engine (see --label-ink-shift in 00-tokens.css). An
234
- embedder does not serve these files and never inherits these URLs, so it
235
- either ships its own @font-face or lets both tokens fall back to the platform
236
- monospace stack.
237
-
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.
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. */
211
+ The bundled faces, served by the full-page host at /vendor/fonts
212
+ (docs/host-page.md owns the files and their licences). They back BOTH
213
+ --font-mono and --font-ui: chip-label ink centring is a per-font constant
214
+ (--label-ink-shift, 00-tokens.css), so the chrome cannot take a system stack
215
+ that resolves differently per engine. No face sets ascent- or
216
+ descent-override: the cell background is the run padding in 02-terminal.css. */
266
217
  @font-face {
267
218
  font-family: "Monaspace Neon NF";
268
219
  src: url("/vendor/fonts/MonaspaceNeonNF-Regular.woff2") format("woff2");
@@ -295,6 +246,14 @@ body {
295
246
  font-display: block;
296
247
  }
297
248
 
249
+ /* "Web Terminal Glyphs" is ONE upright 400 file under FOUR descriptor sets, so
250
+ a bold run resolves a real face instead of an emboldened stand-in (Blink and
251
+ Gecko). Blink still slants an italic run; font-synthesis: none on .term
252
+ (02-terminal.css) keeps it upright, and both are needed. The bytes download
253
+ once. No unicode-range: the font's cmap is its range (tiling codepoints
254
+ only), so letters fall through to Monaspace by family fallback, and a list
255
+ here would need upkeep against each font release. 00-tokens.css owns why it
256
+ is first in --font-mono and absent from --font-ui. */
298
257
  @font-face {
299
258
  font-family: "Web Terminal Glyphs";
300
259
  src: url("/vendor/fonts/WebTerminalGlyphs.woff2") format("woff2");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "8.5.0",
3
+ "version": "8.6.0-dev.10",
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",
@@ -44,7 +44,7 @@
44
44
  "verify": "bash scripts/verify.sh"
45
45
  },
46
46
  "peerDependencies": {
47
- "@cplieger/web-terminal-engine": "^6.0.0 || ^6.2.0-dev"
47
+ "@cplieger/web-terminal-engine": "^6.4.0-dev.4"
48
48
  },
49
49
  "overrides": {
50
50
  "qs": "^6.15.2"
@@ -54,22 +54,22 @@
54
54
  "@stryker-mutator/core": "10.0.0",
55
55
  "@stryker-mutator/typescript-checker": "10.0.0",
56
56
  "@stryker-mutator/vitest-runner": "10.0.0",
57
- "@types/node": "26.6.4",
57
+ "@types/node": "26.6.5",
58
58
  "@typescript/native": "npm:typescript@7.0.2",
59
- "@vitest/browser": "5.0.3",
60
- "@vitest/browser-playwright": "5.0.3",
61
- "@vitest/coverage-v8": "5.0.3",
59
+ "@vitest/browser": "5.0.5",
60
+ "@vitest/browser-playwright": "5.0.5",
61
+ "@vitest/coverage-v8": "5.0.5",
62
62
  "eslint": "10.12.0",
63
63
  "fast-check": "4.10.2",
64
64
  "html-validate": "11.16.2",
65
- "knip": "6.40.0",
66
- "playwright": "1.64.0",
67
- "prettier": "3.9.9",
65
+ "knip": "6.41.0",
66
+ "playwright": "1.64.1",
67
+ "prettier": "3.9.10",
68
68
  "stylelint": "17.16.0",
69
69
  "stylelint-config-standard": "40.0.0",
70
70
  "typescript": "npm:@typescript/typescript6@6.0.2",
71
- "typescript-eslint": "8.71.1",
72
- "vitest": "5.0.3"
71
+ "typescript-eslint": "8.71.2",
72
+ "vitest": "5.0.5"
73
73
  },
74
74
  "publishConfig": {
75
75
  "access": "public"
@@ -16,7 +16,7 @@
16
16
  arrives with /style.css above, which is render-blocking, so nothing paints
17
17
  before it either way. A consumer that also wants a failure surface for the
18
18
  case where /style.css itself fails to load adds its own minimal inline block
19
- for that (web-terminal-kiro does); a bare host does not need one. -->
19
+ for that; a bare host does not need one. -->
20
20
  </head>
21
21
  <body>
22
22
  <!-- createTerminal() builds the entire terminal UI inside this single root