@cplieger/web-terminal-ui 4.5.0 → 4.5.2

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
@@ -140,15 +140,15 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
140
140
 
141
141
  ### Options
142
142
 
143
- | Option | Default | Purpose |
144
- | -------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
- | `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array. |
146
- | `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). |
147
- | `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
148
- | `fontReady` | `'14px "MonaspiceNe NFM"'` | 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. |
149
- | `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. |
150
- | `onFatalError` | _(built-in recovery)_ | Called after a fatal feature setup failure has stopped the connection, released the terminal runtime, and cleared the root. It receives `{ phase, feature, cause }`. Return `true` only after rendering replacement recovery UI into the root; otherwise the kernel shows its Reload page surface. |
151
- | `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input` (the working and input rings derive from their own tokens). The library ships neutral defaults. |
143
+ | Option | Default | Purpose |
144
+ | -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
145
+ | `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array. |
146
+ | `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). |
147
+ | `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
148
+ | `fontReady` | `'14px "MonaspiceNe NFM"'` | 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. |
149
+ | `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. |
150
+ | `onFatalError` | _(built-in recovery)_ | Called after a fatal feature setup failure has stopped the connection, released the terminal runtime, and cleared the root. It receives `{ phase, feature, cause }`. Return `true` only after rendering replacement recovery UI into the root; otherwise the kernel shows its Reload page surface. |
151
+ | `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input` (the working ripple and the input ring derive from their own tokens). The library ships neutral defaults. |
152
152
 
153
153
  `createTerminal()` returns a handle: `focus()` re-focuses the terminal input
154
154
  (and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
@@ -190,6 +190,11 @@ Consumers that ship this UI:
190
190
  - [`vibekit`](https://github.com/cplieger/vibekit)
191
191
  - [`web-terminal-kiro`](https://github.com/cplieger/web-terminal-kiro)
192
192
 
193
+ ## Contributing
194
+
195
+ Issues and PRs are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the
196
+ conventions and how to run the checks locally.
197
+
193
198
  ## Disclaimer
194
199
 
195
200
  This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.
@@ -198,4 +203,4 @@ This project was built with AI-assisted tooling using [Claude](https://claude.co
198
203
 
199
204
  ## License
200
205
 
201
- GPL-3.0-or-later. See `LICENSE`.
206
+ GPL-3.0-or-later. See [LICENSE](LICENSE).
package/css/30-tabs.css CHANGED
@@ -59,14 +59,21 @@
59
59
  display: inline-flex;
60
60
  align-items: center;
61
61
  gap: var(--sp-1);
62
- /* Browser-style tabs: up to 300px per tab when there is room, shrinking toward
63
- a 100px floor as the strip fills; past the floor the list scrolls horizontally
64
- (.wt-tab-scroll has overflow-x: auto). flex-grow is 0, so the 300px basis is a
65
- hard cap — every tab is the same size and never stretches past it, whatever
66
- its title; the label truncates instead. box-sizing keeps the basis a true
62
+ /* Browser-style tabs: every tab STARTS at the full 300px, shrinking toward a
63
+ 100px floor only once the strip is full; past the floor the list scrolls
64
+ horizontally (.wt-tab-scroll has overflow-x: auto). A definite width (not
65
+ flex-basis: 300px) is load-bearing: the scroller shrink-wraps its content,
66
+ and in intrinsic sizing a non-growable item's flex BASIS caps at its
67
+ content width (min(label, 300px)) — short-labeled tabs then rendered at
68
+ content width and every added tab dragged all widths toward the mean. A
69
+ definite width contributes the full 300px regardless of the label, while
70
+ flex-shrink still compresses it once the bar runs out of room. flex-grow
71
+ is 0, so 300px is also a hard cap — every tab is the same size whatever
72
+ its title; the label truncates instead. box-sizing keeps the width a true
67
73
  total. */
68
74
  box-sizing: border-box;
69
- flex: 0 1 300px;
75
+ width: 300px;
76
+ flex: 0 1 auto;
70
77
  min-width: 100px;
71
78
  padding: var(--sp-2);
72
79
  /* A transparent border is reserved on every tab so toggling the inactive
@@ -114,6 +121,17 @@
114
121
  overflow: hidden;
115
122
  white-space: nowrap;
116
123
  text-overflow: ellipsis;
124
+ /* Center the INK, not the line box: UI fonts carry more ascent than descent
125
+ (Noto Sans at 13px: 14px over / 4px under the baseline), so the flex-
126
+ centered line box leaves the visible text ~1.5px low — reading as
127
+ baseline-aligned with the activity dot's bottom instead of its center.
128
+ Trimming the box to the cap-height/alphabetic band makes flexbox center
129
+ the glyphs themselves, on every platform font. Descender ink still paints
130
+ (verified: ink overflow survives the overflow: hidden ellipsis clip).
131
+ Non-supporting browsers simply keep the untrimmed box. Shared by all
132
+ three chip labels (this one + the two switcher labels in
133
+ 31-switcher.css). */
134
+ text-box: trim-both cap alphabetic;
117
135
  }
118
136
 
119
137
  /* A little breathing room after the activity dot, before the label. */
@@ -394,15 +412,14 @@
394
412
  border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
395
413
  }
396
414
 
397
- /* Agent working: two INDEPENDENTLY animated layers on one element make the
398
- "alive" read (the status-badge "processing" idiom — Ant Design's processing
399
- badge, the classic CSS live-indicator ripple): the solid disc keeps full
400
- opacity and gently GLOWS (its colors brighten mid-beat), while a ::after
401
- ring — born exactly on the static 2px ring — RIPPLES outward and dissolves,
402
- re-emitted each cycle. The static ring itself never moves, so working keeps
403
- input's exact rest silhouette (a live turn ripples; a turn blocked on the
404
- user is that same silhouette frozen — see input below). Motion separates
405
- working from input; the ring separates both from done's bare disc. The old
415
+ /* Agent working: a solid disc with two animated cues (the status-badge
416
+ "processing" idiom — Ant Design's processing badge, the classic CSS
417
+ live-indicator ripple): the disc keeps full opacity and gently GLOWS (its
418
+ colors brighten mid-beat), while a ::after ring RIPPLES outward off the
419
+ disc's edge and dissolves, re-emitted each cycle. No static ring at rest —
420
+ the disc + live ripple ARE the working read; input freezes that exact ping
421
+ at its birth frame (see input below), so motion separates working from
422
+ input, and the ring separates both from done's bare disc. The old
406
423
  single-layer pulse faded disc + ring together (opacity on the element),
407
424
  which read as a blink and coupled the layers — an element's opacity always
408
425
  multiplies onto its pseudos, so true independence requires animating disc
@@ -413,16 +430,15 @@
413
430
  position: relative;
414
431
  background: var(--status-working);
415
432
  border-color: var(--status-working);
416
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
417
433
  animation: wt-working-glow 1.2s ease-in-out infinite;
418
434
  }
419
435
 
420
- /* The ripple layer: at scale(1) its 2px border overlays the static ring's
421
- 9->13px band exactly (inset -2px), then the wave detaches, expanding and
422
- fading. transform + opacity only, so it composites without repaints. It
423
- exists only in the working state — no stale layer to pin when a dot changes
424
- status. (The switcher notification dot never shows working — input/done
425
- only — so its position: absolute placement never meets this rule's
436
+ /* The ripple layer: at scale(1) its 2px border hugs the disc's edge (inset
437
+ -2px, the 9->13px band), then the wave detaches, expanding and fading.
438
+ transform + opacity only, so it composites without repaints. It exists only
439
+ in the working state — no stale layer to pin when a dot changes status.
440
+ (The switcher notification dot never shows working — input/done only — so
441
+ its position: absolute placement never meets this rule's
426
442
  position: relative.) */
427
443
  :where(.wt-root) .wt-status-dot[data-status="working"]::after {
428
444
  content: "";
@@ -442,18 +458,19 @@
442
458
  border-color: var(--status-done);
443
459
  }
444
460
 
445
- /* User action required (permission / approval): working's exact silhouette —
446
- solid dot + 2px ring — frozen and recolored (--status-input; the default
447
- orange is shifted away from the old amber so it separates from both
448
- default-lime working and green done). The stillness is the message: the
449
- agent's turn is mid-flight but blocked on you. The ring keeps it distinct
450
- from done's bare disc without leaning on hue; it derives from the same token
451
- at 40% alpha, so a theme override recolors dot + ring together. It persists
452
- until you act on or switch into the tab. */
461
+ /* User action required (permission / approval): the working ping frozen at
462
+ its birth frame — solid dot + the same 2px ring at the ripple's 30% birth
463
+ opacity, motionless (--status-input; the default orange is shifted away
464
+ from the old amber so it separates from both default-lime working and green
465
+ done). The stillness is the message: the agent's turn is mid-flight but
466
+ blocked on you. The ring keeps it distinct from done's bare disc without
467
+ leaning on hue; it derives from the same token, so a theme override
468
+ recolors dot + ring together. It persists until you act on or switch into
469
+ the tab. */
453
470
  :where(.wt-root) .wt-status-dot[data-status="input"] {
454
471
  background: var(--status-input);
455
472
  border-color: var(--status-input);
456
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-input) 40%, transparent);
473
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-input) 30%, transparent);
457
474
  }
458
475
 
459
476
  /* Exited: a dim hollow dot, static; distinct from idle by its lower opacity. */
@@ -483,13 +500,14 @@
483
500
  }
484
501
  }
485
502
 
486
- /* The ripple wave: one emission per cycle, launching off the static ring and
487
- dissolving by ~2.1x. Opacity starts at the static ring's own 40% weight so
488
- the wave reads as the ring letting go of a copy, not a new brighter object. */
503
+ /* The ripple wave: one emission per cycle, launching off the disc's edge and
504
+ dissolving by ~2.1x. The 0.3 birth opacity matches the input ring's frozen
505
+ weight, so a live ping and a stuck one read as the same object — subtle
506
+ against the bar now that no static ring doubles it at birth. */
489
507
  @keyframes wt-working-ripple {
490
508
  0% {
491
509
  transform: scale(1);
492
- opacity: 0.4;
510
+ opacity: 0.3;
493
511
  }
494
512
 
495
513
  100% {
@@ -502,13 +520,14 @@
502
520
  @media (prefers-reduced-motion: reduce) {
503
521
  :where(.wt-root) .wt-status-dot[data-status="working"] {
504
522
  animation: none;
505
- /* Motion is what separates working from input's frozen silhouette; with
506
- it removed, punch an inner hole (dot -> donut) so working differs from
507
- input (solid ringed disc) and done (bare disc) by shape, not hue alone
508
- (WCAG 1.4.1). The outer ring stays. */
523
+ /* Motion is what separates working from input's frozen ping; with it
524
+ removed, punch an inner hole (dot -> donut) and add a static ring so
525
+ working differs from input (solid ringed disc) and done (bare disc) by
526
+ shape, not hue alone (WCAG 1.4.1). The ring carries the same 30%
527
+ weight as input's. */
509
528
  box-shadow:
510
529
  inset 0 0 0 2px var(--bg),
511
- 0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
530
+ 0 0 0 2px color-mix(in srgb, var(--status-working) 30%, transparent);
512
531
  }
513
532
 
514
533
  /* The ripple layer disappears entirely (not just paused mid-wave). */
@@ -178,6 +178,8 @@
178
178
  text-overflow: ellipsis;
179
179
  white-space: nowrap;
180
180
  text-align: left;
181
+ /* Ink-centering trim, same as .wt-tab-label (see 30-tabs.css). */
182
+ text-box: trim-both cap alphabetic;
181
183
  }
182
184
 
183
185
  /* Keyboard button: hidden until a keyboardToggle is wired; filled while the key
@@ -418,6 +420,8 @@
418
420
  overflow: hidden;
419
421
  text-overflow: ellipsis;
420
422
  white-space: nowrap;
423
+ /* Ink-centering trim, same as .wt-tab-label (see 30-tabs.css). */
424
+ text-box: trim-both cap alphabetic;
421
425
  }
422
426
 
423
427
  /* The close sits flush at the row's right edge (no gap), vertically centered. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "type": "module",
5
5
  "description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
6
6
  "license": "GPL-3.0-or-later",
@@ -53,7 +53,7 @@
53
53
  "happy-dom": "20.11.0",
54
54
  "html-validate": "11.5.6",
55
55
  "knip": "6.27.0",
56
- "prettier": "3.9.5",
56
+ "prettier": "3.9.6",
57
57
  "stylelint": "17.14.1",
58
58
  "stylelint-config-standard": "40.0.0",
59
59
  "typescript": "npm:@typescript/typescript6@6.0.2",