@cplieger/web-terminal-ui 4.5.1 → 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
@@ -121,6 +121,17 @@
121
121
  overflow: hidden;
122
122
  white-space: nowrap;
123
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;
124
135
  }
125
136
 
126
137
  /* A little breathing room after the activity dot, before the label. */
@@ -401,15 +412,14 @@
401
412
  border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
402
413
  }
403
414
 
404
- /* Agent working: two INDEPENDENTLY animated layers on one element make the
405
- "alive" read (the status-badge "processing" idiom — Ant Design's processing
406
- badge, the classic CSS live-indicator ripple): the solid disc keeps full
407
- opacity and gently GLOWS (its colors brighten mid-beat), while a ::after
408
- ring — born exactly on the static 2px ring — RIPPLES outward and dissolves,
409
- re-emitted each cycle. The static ring itself never moves, so working keeps
410
- input's exact rest silhouette (a live turn ripples; a turn blocked on the
411
- user is that same silhouette frozen — see input below). Motion separates
412
- 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
413
423
  single-layer pulse faded disc + ring together (opacity on the element),
414
424
  which read as a blink and coupled the layers — an element's opacity always
415
425
  multiplies onto its pseudos, so true independence requires animating disc
@@ -420,16 +430,15 @@
420
430
  position: relative;
421
431
  background: var(--status-working);
422
432
  border-color: var(--status-working);
423
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
424
433
  animation: wt-working-glow 1.2s ease-in-out infinite;
425
434
  }
426
435
 
427
- /* The ripple layer: at scale(1) its 2px border overlays the static ring's
428
- 9->13px band exactly (inset -2px), then the wave detaches, expanding and
429
- fading. transform + opacity only, so it composites without repaints. It
430
- exists only in the working state — no stale layer to pin when a dot changes
431
- status. (The switcher notification dot never shows working — input/done
432
- 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
433
442
  position: relative.) */
434
443
  :where(.wt-root) .wt-status-dot[data-status="working"]::after {
435
444
  content: "";
@@ -449,18 +458,19 @@
449
458
  border-color: var(--status-done);
450
459
  }
451
460
 
452
- /* User action required (permission / approval): working's exact silhouette —
453
- solid dot + 2px ring — frozen and recolored (--status-input; the default
454
- orange is shifted away from the old amber so it separates from both
455
- default-lime working and green done). The stillness is the message: the
456
- agent's turn is mid-flight but blocked on you. The ring keeps it distinct
457
- from done's bare disc without leaning on hue; it derives from the same token
458
- at 40% alpha, so a theme override recolors dot + ring together. It persists
459
- 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. */
460
470
  :where(.wt-root) .wt-status-dot[data-status="input"] {
461
471
  background: var(--status-input);
462
472
  border-color: var(--status-input);
463
- 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);
464
474
  }
465
475
 
466
476
  /* Exited: a dim hollow dot, static; distinct from idle by its lower opacity. */
@@ -490,13 +500,14 @@
490
500
  }
491
501
  }
492
502
 
493
- /* The ripple wave: one emission per cycle, launching off the static ring and
494
- dissolving by ~2.1x. Opacity starts at the static ring's own 40% weight so
495
- 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. */
496
507
  @keyframes wt-working-ripple {
497
508
  0% {
498
509
  transform: scale(1);
499
- opacity: 0.4;
510
+ opacity: 0.3;
500
511
  }
501
512
 
502
513
  100% {
@@ -509,13 +520,14 @@
509
520
  @media (prefers-reduced-motion: reduce) {
510
521
  :where(.wt-root) .wt-status-dot[data-status="working"] {
511
522
  animation: none;
512
- /* Motion is what separates working from input's frozen silhouette; with
513
- it removed, punch an inner hole (dot -> donut) so working differs from
514
- input (solid ringed disc) and done (bare disc) by shape, not hue alone
515
- (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. */
516
528
  box-shadow:
517
529
  inset 0 0 0 2px var(--bg),
518
- 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);
519
531
  }
520
532
 
521
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.1",
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",