@cplieger/web-terminal-ui 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -143,7 +143,7 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
143
143
  | `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
144
144
  | `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. |
145
145
  | `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. |
146
- | `onFatalError` | _(built-in recovery)_ | Called with `{ phase, feature, cause }` after a fatal feature setup failure; behavior below. |
146
+ | `onFatalError` | _(built-in recovery)_ | Called with a `TerminalStartupFailure` after a fatal startup failure, in either phase (`feature-setup` or `kernel-init`); behavior below. |
147
147
  | `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 library ships neutral defaults. |
148
148
 
149
149
  `createTerminal()` returns a handle: `focus()` re-focuses the terminal input
@@ -154,11 +154,19 @@ screen without injecting keystrokes (send a redraw keystroke yourself if you
154
154
  want one, for example Ctrl+L); and `destroy()` tears every feature down and
155
155
  releases the kernel.
156
156
 
157
- If a feature's setup throws or rejects, the kernel stops the connection, tears
158
- down every completed feature and core listener, clears the broken subtree, and
159
- shows a reload surface. The surface is modal when the terminal owns the
160
- viewport and non-modal when it fills an embedded container. `onFatalError`
161
- receives the failure after cleanup; return `true` only when the host has rendered
157
+ Startup can fail in two phases, and both end at the same recovery surface. If a
158
+ feature's setup throws or rejects (`phase: "feature-setup"`), the kernel stops the
159
+ connection, tears down every completed feature and core listener, clears the
160
+ broken subtree, and shows a reload surface. If `createTerminal` itself throws
161
+ (`phase: "kernel-init"` — an invalid feature list, a DOM invariant), it shows the
162
+ same surface, lowers your loading overlay so the surface is visible, and then
163
+ rethrows, so a caller with its own error handling still sees the error. Either
164
+ way the surface is modal when the terminal owns the viewport and non-modal when
165
+ it fills an embedded container.
166
+
167
+ `onFatalError` receives the failure after cleanup. Discriminate on `phase`:
168
+ `feature-setup` names the offending `feature`, `kernel-init` does not, because
169
+ feature composition never began. Return `true` only when the host has rendered
162
170
  replacement recovery UI into the terminal root.
163
171
 
164
172
  ## What ships
@@ -24,10 +24,15 @@
24
24
  scrollbar-gutter: stable;
25
25
  /* The scroll controller (vterm scroll.ts) is the single owner of
26
26
  scrollTop: it pins to the bottom only while the user is following,
27
- and does nothing while they are scrolled up reading. New output
28
- always appends at the tail (the absolute-index model never inserts
29
- above the viewport), so native scroll anchoring is safe to leave on
30
- and keeps the read position steady on the rare insert-above. */
27
+ and does nothing while they are scrolled up reading.
28
+ Content ABOVE the reading position DOES change: rows are evicted
29
+ from the top of history at the retention cap, and the trim marker
30
+ is inserted there. Native scroll anchoring compensates for that on
31
+ Chrome and Firefox, so it stays enabled here — but WebKit has never
32
+ implemented it, which is why the renderer also corrects the drift
33
+ itself (render.ts captureReadAnchor / restoreReadAnchor). That
34
+ correction measures residual drift, so it is a no-op wherever this
35
+ declaration already did the job. */
31
36
  overflow-anchor: auto;
32
37
  /* .term is the scroll container, so the touch-action intersection for a
33
38
  touch on the output stops here (the html/body pan-x pan-y does not reach
package/css/30-tabs.css CHANGED
@@ -86,6 +86,12 @@
86
86
  cursor: pointer;
87
87
  white-space: nowrap;
88
88
  user-select: none;
89
+ /* Kills the 300ms double-tap-to-zoom delay, which is what makes a double-TAP
90
+ usable as the touch rename entry: on iPadOS a long-press on this chip starts
91
+ a reorder drag (it is draggable), so the context menu is unreachable by touch
92
+ and the double-tap is the only path in. Also removes the tap delay for the
93
+ ordinary switch tap. */
94
+ touch-action: manipulation;
89
95
  }
90
96
 
91
97
  /* Inactive tabs read as outlined chips with a subtle dark fill (so adjacent tabs
@@ -294,6 +300,20 @@
294
300
  margin-left: auto;
295
301
  }
296
302
 
303
+ /* Hover wash, matching every other bar control (.wt-tab-new, the switcher's
304
+ buttons) — without it this was the one control in the chrome that gave no
305
+ pointer feedback at all. Gated to real hover pointers because the button only
306
+ SHOWS on a coarse pointer (the iPad strip): a plain touchscreen has no
307
+ pointer-leave, so :hover would stick after a tap and the wash would linger;
308
+ the feedback there is :active, from .wt-btn. Before .wt-active / .wt-armed so
309
+ those toggled fills win on a hovered button (equal specificity, source order
310
+ decides). */
311
+ @media (hover: hover) {
312
+ :where(.wt-root) .wt-tab-kb:hover {
313
+ background: var(--tab-hover-bg);
314
+ }
315
+ }
316
+
297
317
  /* Grid open: filled so the toggled state reads at a glance (mirrors the
298
318
  switcher keyboard button). */
299
319
  :where(.wt-root) .wt-tab-kb.wt-active {
@@ -322,6 +342,66 @@
322
342
  opacity: 0.5;
323
343
  }
324
344
 
345
+ /* ===== Inline rename (tabs feature) =====
346
+ The edit surface is the LABEL's own box, so the width the user types into is
347
+ the width the label will have. The field takes the label's flex slot exactly;
348
+ the label is [hidden] and the close button is hidden for the duration (a 44px
349
+ destructive target next to a live text field is a mis-click waiting to happen,
350
+ and the freed width goes to the field). */
351
+ :where(.wt-root) .wt-tab-rename {
352
+ flex: 1 1 auto;
353
+ min-width: 0;
354
+ background: transparent;
355
+ border: none;
356
+ padding: 0;
357
+ color: var(--tab-active-fg);
358
+ font: inherit;
359
+ /* .wt-tab sets user-select: none, and WebKit CASCADES that into descendants
360
+ even though the property is non-inherited per spec (the same deviation the
361
+ touch model documents for .term-output). Without this, caret placement and
362
+ text selection break inside the field in Safari — i.e. on the iPad. */
363
+ user-select: text;
364
+ /* The chip is cursor: pointer; a text field must not inherit that. */
365
+ cursor: text;
366
+ }
367
+
368
+ /* The family focus ring is scoped to button:focus-visible in 10-primitives.css,
369
+ so the field needs its own. It is focused programmatically the instant it
370
+ appears, so :focus (not :focus-visible) is correct here — the ring IS the
371
+ affordance that says "type here". */
372
+ :where(.wt-root) .wt-tab-rename:focus {
373
+ outline: 2px solid var(--accent);
374
+ outline-offset: 1px;
375
+ }
376
+
377
+ /* While editing, hide the close button: a 44px destructive target adjacent to a
378
+ live text field is a mis-click waiting to happen, and the freed width goes to
379
+ the field.
380
+
381
+ The chip is NOT widened. An earlier draft gave the edited chip its full 300px
382
+ so a crowded strip (tabs shrink toward a 100px floor) would not offer a 100px
383
+ edit box — but nothing in this design system transitions `width` on a tab, so
384
+ that was an instant ~200px shove of every neighbouring tab plus a scroll jump,
385
+ at the exact moment the user is trying to read what they are typing. A cramped
386
+ field on a crowded strip is the lesser problem. */
387
+ :where(.wt-root) .wt-tab-editing .wt-tab-close {
388
+ display: none;
389
+ }
390
+
391
+ /* The drag preview clone (setDragGhost): a full-opacity copy of the chip parked
392
+ under .wt-root — OUTSIDE the bar's backdrop-filter, which is what stops WebKit
393
+ from rendering the automatic drag image (webkit.org/b/22787) — and laid exactly
394
+ over the real chip via inline left/top/width/height. It exists for less than a
395
+ frame (the browser snapshots it, then a rAF removes it), so it must not be
396
+ hidden (an invisible element makes no snapshot) and must not intercept the
397
+ pointer. z-index clears the bar so the copy, not the dimmed original, is what
398
+ would show if it ever painted. */
399
+ :where(.wt-root) .wt-tab-ghost {
400
+ position: absolute;
401
+ z-index: 16;
402
+ pointer-events: none;
403
+ }
404
+
325
405
  /* Tab context menu (tabs feature, desktop right-click): Close / Close others /
326
406
  Close to the right / Close to the left / Close all. Replaces the old bar
327
407
  "Close all" button. Mirrors .wt-ctx-menu (a fixed surface popover, clamped
@@ -375,6 +455,16 @@
375
455
  cursor: default;
376
456
  }
377
457
 
458
+ /* Group separator: the naming pair (Rename / Use automatic name) sits above the
459
+ move and close groups, so a hairline keeps the non-destructive actions visually
460
+ apart from the destructive ones. The menu is otherwise a flat flex column of
461
+ buttons with no separator vocabulary. */
462
+ :where(.wt-root) .wt-tab-menu-sep {
463
+ height: 1px;
464
+ margin: var(--sp-1) 0;
465
+ background: var(--border);
466
+ }
467
+
378
468
  /* Pull the terminal surface up off the docked BOTTOM tab strip so it does not
379
469
  overlap the last rows (a mirror of the old top offset). The tabs feature
380
470
  marks the surface (.wt-with-tabbar) and keeps the measured strip height in
@@ -208,6 +208,15 @@
208
208
  overflow: hidden;
209
209
  }
210
210
 
211
+ /* Hover wash like the switch and "+" buttons beside it; gated to real hover
212
+ pointers for the same reason they are (a sticky :hover after a tap). Before
213
+ .wt-active / .wt-armed so those fills win on a hovered button. */
214
+ @media (hover: hover) {
215
+ :where(.wt-root) .wt-switcher-kb:hover {
216
+ background: var(--tab-hover-bg);
217
+ }
218
+ }
219
+
211
220
  :where(.wt-root) .wt-switcher-kb[hidden] {
212
221
  display: none;
213
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "type": "module",
5
5
  "description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
6
6
  "license": "GPL-3.0-or-later",
@@ -38,7 +38,10 @@
38
38
  "verify": "bash scripts/verify.sh"
39
39
  },
40
40
  "peerDependencies": {
41
- "@cplieger/web-terminal-engine": "^3.0.0"
41
+ "@cplieger/web-terminal-engine": "^3.2.0"
42
+ },
43
+ "overrides": {
44
+ "qs": "^6.15.2"
42
45
  },
43
46
  "devDependencies": {
44
47
  "@eslint/js": "10.0.1",
@@ -50,9 +53,9 @@
50
53
  "@vitest/coverage-v8": "4.1.10",
51
54
  "eslint": "10.7.0",
52
55
  "fast-check": "4.9.0",
53
- "happy-dom": "20.11.0",
56
+ "happy-dom": "20.11.1",
54
57
  "html-validate": "11.5.6",
55
- "knip": "6.27.0",
58
+ "knip": "6.29.0",
56
59
  "prettier": "3.9.6",
57
60
  "stylelint": "17.14.1",
58
61
  "stylelint-config-standard": "40.0.0",