@cplieger/web-terminal-ui 7.3.2 → 8.0.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.
Files changed (48) hide show
  1. package/README.md +114 -18
  2. package/css/30-tabs.css +21 -0
  3. package/css/31-switcher.css +15 -0
  4. package/css/32-split.css +176 -0
  5. package/css/MANIFEST +1 -0
  6. package/css/MANIFEST.tabbed +1 -0
  7. package/package.json +13 -12
  8. package/src/composition.ts +165 -237
  9. package/src/features/activity-monitor.ts +20 -55
  10. package/src/features/animations.ts +12 -30
  11. package/src/features/clipboard.ts +36 -64
  12. package/src/features/connection-banner.ts +10 -19
  13. package/src/features/context-menu.ts +70 -145
  14. package/src/features/dom.ts +44 -54
  15. package/src/features/menu-position.ts +24 -28
  16. package/src/features/mobile-toolbar.ts +32 -62
  17. package/src/features/predictive-echo.ts +15 -49
  18. package/src/features/scroll-to-bottom.ts +12 -24
  19. package/src/features/tabs/index.ts +1398 -1806
  20. package/src/features/tabs/ink-centre.ts +45 -54
  21. package/src/features/tabs/model.ts +301 -383
  22. package/src/features/tabs/strip.ts +45 -97
  23. package/src/features/tabs/switcher.ts +70 -48
  24. package/src/index.ts +13 -13
  25. package/src/kernel/a11y.ts +106 -27
  26. package/src/kernel/attention.ts +157 -0
  27. package/src/kernel/conn-state.ts +28 -22
  28. package/src/kernel/fatal.ts +76 -0
  29. package/src/kernel/feature-host.ts +136 -0
  30. package/src/kernel/gesture.ts +9 -13
  31. package/src/kernel/kernel.ts +57 -2139
  32. package/src/kernel/layout-policy.ts +44 -0
  33. package/src/kernel/loading-status.ts +30 -88
  34. package/src/kernel/navigation.ts +11 -17
  35. package/src/kernel/notify.ts +158 -0
  36. package/src/kernel/pane.ts +1415 -0
  37. package/src/kernel/realm.ts +11 -0
  38. package/src/kernel/regions.ts +8 -11
  39. package/src/kernel/scrollback.ts +88 -168
  40. package/src/kernel/selection.ts +25 -0
  41. package/src/kernel/shell.ts +1434 -0
  42. package/src/kernel/split-handle.ts +280 -0
  43. package/src/kernel/status-share.ts +100 -0
  44. package/src/kernel/types.ts +457 -441
  45. package/src/predict.ts +144 -170
  46. package/src/viewport.ts +124 -197
  47. package/src/features/tabs/attention.ts +0 -250
  48. package/src/features/tabs/notify.ts +0 -265
package/README.md CHANGED
@@ -1,11 +1,6 @@
1
1
  # @cplieger/web-terminal-ui
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@cplieger/web-terminal-ui)](https://www.npmjs.com/package/@cplieger/web-terminal-ui)
4
- [![JSR](https://jsr.io/badges/@cplieger/web-terminal-ui)](https://jsr.io/@cplieger/web-terminal-ui)
5
- [![Test coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/web-terminal-ui/badges/coverage.json)](https://github.com/cplieger/web-terminal-ui/actions/workflows/coverage.yml)
6
- [![Mutation (TS)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/web-terminal-ui/badges/mutation-ts.json)](https://github.com/cplieger/web-terminal-ui/issues?q=label%3Astryker-tracker)
7
- [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13431/badge)](https://www.bestpractices.dev/projects/13431)
8
- [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/cplieger/web-terminal-ui/badge)](https://scorecard.dev/viewer/?uri=github.com/cplieger/web-terminal-ui)
3
+ [![npm](https://img.shields.io/npm/v/@cplieger/web-terminal-ui)](https://www.npmjs.com/package/@cplieger/web-terminal-ui) [![JSR](https://jsr.io/badges/@cplieger/web-terminal-ui)](https://jsr.io/@cplieger/web-terminal-ui) [![Mutation (TS)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/web-terminal-ui/badges/mutation-ts.json)](https://github.com/cplieger/web-terminal-ui/issues?q=label%3Astryker-tracker)
9
4
 
10
5
  The reference touch-first browser UI for
11
6
  [`@cplieger/web-terminal-engine`](https://github.com/cplieger/web-terminal-engine).
@@ -20,7 +15,7 @@ terminal:
20
15
  - a **display-only** terminal output (native text selection survives redraws)
21
16
  and a hidden `<textarea>` that owns the keyboard and IME (the kernel)
22
17
  - **tabs**: multiple independent terminals with a desktop strip, a mobile
23
- bottom switcher, and a modal overview sheet
18
+ bottom switcher, a modal overview sheet, and an opt-in two-pane split view
24
19
  - an **activity monitor** that drives per-tab status dots (working / idle /
25
20
  needs-input / exited) from the server's status stream
26
21
  - a **mobile key toolbar** (Tab / Esc / arrows / Enter / sticky-Ctrl) and a
@@ -118,10 +113,13 @@ an **element** only when you already hold one you created yourself. The
118
113
  difference matters; see "Startup failures" below. There is no element-id
119
114
  contract for the host page to reproduce, and every style and CSS custom
120
115
  property is scoped to the `wt-root` class it stamps on your element (removed
121
- again by `destroy()`). Call it exactly once; the engine's
122
- render/connection/scroll modules are single-instance per page (tabs multiplex
123
- sessions over the one kernel). `scaffold/index.html` is a complete reference page
124
- to copy and adapt.
116
+ again by `destroy()`). Call it at most once per document while the previous
117
+ terminal is alive: the document title, the loading overlay, the status stream and
118
+ the notification permission are properties of the document, so one terminal owns
119
+ them, and a second call reports a `kernel-init` failure and throws without
120
+ touching the page (see "Startup failures"); `destroy()` the first terminal before
121
+ building another, or use the `split` option for a second pane.
122
+ `scaffold/index.html` is a complete reference page to copy and adapt.
125
123
 
126
124
  Four presets are provided; each is a plain feature-array factory, so you can
127
125
  spread and edit it. Import the barrel (`@cplieger/web-terminal-ui/presets`) for
@@ -199,6 +197,7 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
199
197
  | `onFatalError` | _(built-in recovery)_ | Called with a `TerminalStartupFailure` after a fatal startup failure, in either phase (`feature-setup` or `kernel-init`); behavior below. |
200
198
  | `onSessionEnded` | _(none)_ | Called when the active session's process has ended and nothing is retrying: the fact the banner renders as "Session ended". Wire it only if you can act on it: the recovery move is `handle.reattach()`, and whether that helps depends on what your endpoint does on the next connect (see "When a session ends"). Observation only; everything the kernel does about the end happens first and happens regardless, and a handler that throws is logged rather than allowed to take the banner down with it. |
201
199
  | `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`, `--status-warning`, `--status-failed`. The library ships neutral defaults. If you retheme the animated trio (`--status-working` / `--status-warning` / `--status-failed`), keep their LIGHTNESS spread: they differ only in hue, so equal-lightness replacements collapse into one another in greyscale and under deuteranopia. |
200
+ | `split` | _(off)_ | Two panes side by side, opened and closed from the tab chrome (see "Split view"). Requires the tabs feature (`tabs()` or a tabbed preset) in `features`; a terminal without it, or a value other than `true` or `false`, fails at `kernel-init`. Without the option the DOM is exactly what it was before the option existed. |
202
201
 
203
202
  `createTerminal()` returns a handle: `focus()` re-focuses the terminal input
204
203
  (and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
@@ -207,7 +206,9 @@ for a "type this command" affordance); `reset()` drops the local scrollback and
207
206
  screen without injecting keystrokes (send a redraw keystroke yourself if you
208
207
  want one, for example Ctrl+L); `reattach()` attaches again to whatever the server
209
208
  serves now, for a session that has ended (see "When a session ends"); and
210
- `destroy()` tears every feature down and releases the kernel.
209
+ `destroy()` tears every feature down and releases the kernel. With `split: true`
210
+ the handle also carries `split`, the controller of the "Split view" section;
211
+ `focus()`, `send()`, `reset()` and `reattach()` act on the selected pane.
211
212
 
212
213
  ### Mouse and focus
213
214
 
@@ -424,17 +425,32 @@ the library entirely and leaves the page spinning under your overlay with nothin
424
425
  but a console error. Those were the two failures every consumer used to
425
426
  hand-build its own dialog for.
426
427
 
427
- One case has no panel by design: an embedded terminal (`layout: "container"`)
428
- whose mount target does not exist. It is one panel inside a host application that
428
+ Two cases have no panel by design, both `kernel-init` with `surface: undefined`
429
+ to say there is nowhere to render. An embedded terminal (`layout: "container"`)
430
+ whose mount target does not exist: it is one panel inside a host application that
429
431
  is otherwise working, so claiming the viewport to report its own failure would
430
- break a healthy page. The failure is still delivered to `onFatalError` and still
431
- rethrown, with `surface: undefined` to say there is nowhere to render.
432
+ break a healthy page. And a second `createTerminal` while the document already
433
+ holds a live terminal, in either layout: the root it names may be the first
434
+ terminal's, so nothing is written to the page and no overlay is faded. Both are
435
+ still delivered to `onFatalError` and still rethrown.
436
+
437
+ Under `split: true` a failure in the SECOND pane (its kernel or one of its pane
438
+ features, at the first open) is not the terminal's failure: the first pane keeps
439
+ running, the panel renders into the failed pane's own root, non-modally, with its
440
+ one Reload button, and `onFatalError` receives it with that pane root as
441
+ `surface`. The shared split button in the tab row closes the split and discards
442
+ the failed pane, and the next open builds a fresh one. A pane feature that
443
+ rejects only after the split has already closed finds no place for a panel: the
444
+ healthy pane fills the view again, takes over the tab the failed pane was
445
+ showing, and the failed pane is discarded at once, so `onFatalError` receives
446
+ that failure with `surface: undefined` and nothing is rendered.
432
447
 
433
448
  `onFatalError` receives the failure after cleanup. Discriminate on `phase`:
434
449
  `feature-setup` names the offending `feature`, `kernel-init` does not, because
435
450
  feature composition never began. `surface` names the element the built-in panel
436
- would fill, and is the element to render into if you claim it. Return `true` only
437
- when you have rendered replacement recovery UI there.
451
+ would fill, and is the element to render into if you claim it; it is `undefined`
452
+ in the cases above where nothing is rendered, so check it first. Return `true`
453
+ only when you have rendered replacement recovery UI there.
438
454
 
439
455
  If your page also carries an inline bootstrap watchdog (a script that reports
440
456
  "the JS bundle never loaded at all", a rung below `import`), it cannot import
@@ -443,6 +459,86 @@ the package root and at `@cplieger/web-terminal-ui/startup-copy`, which imports
443
459
  nothing and touches no DOM so a build script can read it) and substitute the
444
460
  strings into your HTML at build time, rather than restating them by hand.
445
461
 
462
+ ### Split view
463
+
464
+ `split: true` lets the person show two tabs side by side. It requires the tabs
465
+ feature, because everything that drives it lives there: the shared tab row, the
466
+ split button, the snap items and the layout record. Your root becomes the shell
467
+ root holding one root per pane and the shared chrome; each pane is a complete
468
+ terminal with its own engine, so an open split holds up to two WebSocket
469
+ connections, one per shown pane. Opening, closing and resizing the split
470
+ is a display re-arrangement only: no tab or session is ever created or closed by
471
+ it, and a tab that leaves a pane stays in the row as an ordinary tab.
472
+
473
+ Three ways in, all in the tab chrome, none a key chord:
474
+
475
+ - **The split button** at the right edge of the tab row (and in the mobile
476
+ switcher bar on a touchscreen) toggles the split. Opening puts the shown tab in
477
+ the left pane with the right pane empty; the next tab clicked, or the next "+",
478
+ fills the empty pane. Closing keeps the SELECTED pane's tab and hides the
479
+ other pane. It is an ordinary button: Tab reaches it, Enter or Space toggles
480
+ it, `aria-expanded` is its state.
481
+ - **"Snap to left" and "Snap to right"** in a tab's context menu, on a
482
+ right-click, a touch long-press, or the keyboard (the context-menu key or
483
+ Shift+F10 on the focused tab, where the browser raises the menu event; Safari
484
+ binds no key to it, so a keyboard-only person there reaches the menu through
485
+ an assistive technology's own command). A snapped tab shows on the named side,
486
+ whatever was there becomes an ordinary tab, and the tab's old side is left
487
+ empty.
488
+ - **Dragging a tab** out of the row onto the left or right half of the terminal
489
+ area, which highlights the half under the pointer while the drag lasts.
490
+
491
+ A tab click follows one rule: an empty pane fills first, otherwise the selected
492
+ pane's tab is replaced. The selected pane is the one that receives typing, the
493
+ last one clicked, touched or typed in. The grip handle between the panes shows
494
+ it: a 6 px pill in the 10 px gutter whose accent edge faces the selected pane,
495
+ and the selected pane keeps the filled blinking cursor while the other pane's is
496
+ hollow and steady. The tab row does not change for selection; both shown tabs
497
+ render identically active. A screen reader hears "Left terminal selected" or
498
+ "Right terminal selected" as a polite announcement, and each pane's tabpanel is
499
+ described by its side and whether it is selected.
500
+
501
+ While the split is open each shown pane's input is a stop in the Tab order, so
502
+ Tab runs left pane, divider, right pane, then the tab row; an empty pane adds no
503
+ stop. With the split closed the one pane is entered by typing or a click and Tab
504
+ from outside the terminal lands on the tab row.
505
+
506
+ The handle is a focusable `role="separator"` with `aria-valuenow`, `aria-valuemin`
507
+ and `aria-valuemax` as percentages of the left share. Dragging it (mouse or
508
+ touch, a 24 px hit area) resizes both panes as the pointer moves; the shells are
509
+ told their new size at most every 100 ms and once on release. A pane squeezed
510
+ under 360 px dims, and releasing there closes that pane; releasing above 360 px
511
+ after such a dip snaps the handle back to where the drag started. From the
512
+ keyboard ArrowLeft and ArrowRight nudge the divider by 16 px and Home and End take
513
+ it to the bounds, and every key stops AT the 360 px minimum rather than closing a
514
+ pane; the record is written when the key is released. The default is 50/50.
515
+
516
+ Under 730 px of pane-row width (two minimum panes and the gutter) the split
517
+ buttons hide, the snap items are disabled, a drop does nothing, and an OPEN split
518
+ collapses to the selected pane, restoring itself when the row is wide again. At
519
+ 730 px or wider a window resize keeps both panes at or over 360 px by clamping the
520
+ displayed share; the share the person chose is remembered and comes back when
521
+ the row is wide enough for it.
522
+
523
+ The arrangement is remembered on the server, beside the tab order, as
524
+ `GET`/`PUT /api/sessions/layout`: the session in each pane, the handle share, the
525
+ selected pane and whether the split is open. It is read once at load, written on
526
+ every change, and dies with the session list, so a reload or another device
527
+ restores the same panes; the active tab is the selected pane's session and is no
528
+ longer kept in `localStorage`. Against a server without the route the split still
529
+ works and nothing is remembered; one console warning says so.
530
+
531
+ Two rules for a `features` function under `split: true`, both checked at
532
+ `kernel-init`. It is invoked once per pane and must return FRESH feature objects
533
+ every time, because a feature object's `api` and its `ctx.use` identity belong to
534
+ one pane; a saved array (`const f = presetTabbed(); features: () => f`) is
535
+ refused, naming the reused feature. A feature that must exist once per terminal
536
+ rather than once per pane (the built-in tabs, mobile toolbar, activity monitor
537
+ and animations) declares `scope: "shell"`: it is set up once, from the first
538
+ invocation, with a context whose `surface`, `render`, `scroll`, `modes`,
539
+ `session`, `send` and `paste` resolve to the selected pane at call time and whose
540
+ `ctx.shell.pane(side)` reaches one pane explicitly.
541
+
446
542
  ### When a session ends
447
543
 
448
544
  A session whose process exits is over: the engine closes with its definitive
package/css/30-tabs.css CHANGED
@@ -433,6 +433,27 @@
433
433
  }
434
434
  }
435
435
 
436
+ :where(.wt-root) .wt-tab-split {
437
+ flex: none;
438
+ margin-left: auto;
439
+ }
440
+
441
+ /* .wt-btn's display would otherwise outrank the attribute. */
442
+ :where(.wt-root) .wt-tab-split[hidden] {
443
+ display: none;
444
+ }
445
+
446
+ /* Two auto margins would share the free space between the two buttons. */
447
+ :where(.wt-root) .wt-tab-split:not([hidden]) ~ .wt-tab-kb {
448
+ margin-left: 0;
449
+ }
450
+
451
+ @media (any-hover: hover) {
452
+ :where(.wt-root) .wt-tab-split:hover {
453
+ background: var(--tab-hover-bg);
454
+ }
455
+ }
456
+
436
457
  /* ===== Reorder drop slot (tabs feature, desktop drag) =====
437
458
  The dragged chip stays in the strip's flow for the whole gesture and renders as
438
459
  the SLOT it will land in: an empty dashed outline in the accent over a faint
@@ -287,6 +287,21 @@
287
287
  pointer-events: none;
288
288
  }
289
289
 
290
+ :where(.wt-root) .wt-switcher-split {
291
+ flex: none;
292
+ width: var(--touch-target);
293
+ }
294
+
295
+ @media (any-hover: hover) {
296
+ :where(.wt-root) .wt-switcher-split:hover {
297
+ background: var(--tab-hover-bg);
298
+ }
299
+ }
300
+
301
+ :where(.wt-root) .wt-switcher-split[hidden] {
302
+ display: none;
303
+ }
304
+
290
305
  /* Tab-switcher button: toggles the list open/closed, sitting between the
291
306
  keyboard and "+" buttons. position: relative anchors its notification dot,
292
307
  absolutely placed in the top-right corner. It only earns its place with ≥2
@@ -0,0 +1,176 @@
1
+ :where(.wt-root.wt-split) {
2
+ --wt-split-gutter: 10px;
3
+ --wt-split-ratio: 0.5;
4
+
5
+ display: grid;
6
+ grid-template-columns: minmax(0, 1fr);
7
+ }
8
+
9
+ :where(.wt-root.wt-split.wt-split-open) {
10
+ grid-template-columns:
11
+ calc((100% - var(--wt-split-gutter)) * var(--wt-split-ratio)) var(--wt-split-gutter)
12
+ minmax(0, 1fr);
13
+ }
14
+
15
+ /* Two panes given the same column (the collapse) would otherwise be auto-placed
16
+ into a second row. */
17
+ :where(.wt-root.wt-split) > .wt-split-pane {
18
+ grid-row: 1;
19
+ min-inline-size: 0;
20
+ min-block-size: 0;
21
+ }
22
+
23
+ :where(.wt-root.wt-split) > .wt-split-pane.wt-side-left {
24
+ grid-column: 1;
25
+ }
26
+
27
+ :where(.wt-root.wt-split) > .wt-split-pane.wt-side-right {
28
+ grid-column: 3;
29
+ }
30
+
31
+ :where(.wt-root.wt-split) > .wt-split-pane.wt-pane-hidden {
32
+ display: none;
33
+ }
34
+
35
+ :where(.wt-root.wt-split) > .wt-split-pane.wt-pane-closing {
36
+ opacity: 0.4;
37
+ }
38
+
39
+ /* 02-terminal.css draws the cursor from the textarea's focus; while the split is
40
+ open the selection class outranks it in both directions. */
41
+ :where(.wt-root.wt-split.wt-split-open) .wt-pane-selected .term:not(.focus) .term-cursor {
42
+ background: var(--text);
43
+ color: var(--bg);
44
+ box-shadow: none;
45
+ }
46
+
47
+ :where(.wt-root.wt-split.wt-split-open) .wt-pane-selected .term:not(.focus) .term-cursor-underline {
48
+ border-bottom: 2px solid var(--text);
49
+ box-shadow: none;
50
+ }
51
+
52
+ :where(.wt-root.wt-split.wt-split-open) .wt-pane-selected .term:not(.focus) .term-cursor-bar {
53
+ border-left: 2px solid var(--text);
54
+ box-shadow: none;
55
+ }
56
+
57
+ /* Same specificity as the filled rule above and later in the cascade, so the
58
+ blink phase still shows. */
59
+ :where(.wt-root.wt-split.wt-split-open .wt-pane-selected)
60
+ .term:not(.focus).cursor-blink-off
61
+ .term-cursor {
62
+ background: transparent;
63
+ color: var(--text);
64
+ }
65
+
66
+ :where(.wt-root.wt-split.wt-split-open) .wt-split-pane:not(.wt-pane-selected) .term .term-cursor,
67
+ :where(.wt-root.wt-split.wt-split-open)
68
+ .wt-split-pane:not(.wt-pane-selected)
69
+ .term
70
+ .term-cursor-underline,
71
+ :where(.wt-root.wt-split.wt-split-open)
72
+ .wt-split-pane:not(.wt-pane-selected)
73
+ .term
74
+ .term-cursor-bar {
75
+ background: transparent;
76
+ color: var(--text);
77
+ border: none;
78
+ box-shadow: inset 0 0 0 1px var(--text);
79
+ animation: none;
80
+ }
81
+
82
+ :where(.wt-root.wt-split) > .wt-split-handle {
83
+ display: none;
84
+ }
85
+
86
+ :where(.wt-root.wt-split.wt-split-open) > .wt-split-handle {
87
+ display: block;
88
+ grid-column: 2;
89
+ grid-row: 1;
90
+ position: relative;
91
+ justify-self: center;
92
+ inline-size: 24px;
93
+ margin-inline: -7px;
94
+ cursor: col-resize;
95
+ touch-action: none;
96
+ z-index: 4;
97
+ }
98
+
99
+ :where(.wt-root.wt-split) .wt-split-handle::before {
100
+ content: "";
101
+ position: absolute;
102
+ inset-inline-start: 9px;
103
+ inline-size: 6px;
104
+ inset-block: 12px calc(var(--wt-tabbar-h, 0px) + 12px);
105
+ border-radius: 3px;
106
+ background: var(--tab-bg);
107
+ border-inline: 1px solid var(--border);
108
+ }
109
+
110
+ :where(.wt-root.wt-split) .wt-split-handle[data-faces="left"]::before {
111
+ border-inline-start: 2px solid var(--accent);
112
+ }
113
+
114
+ :where(.wt-root.wt-split) .wt-split-handle[data-faces="right"]::before {
115
+ border-inline-end: 2px solid var(--accent);
116
+ }
117
+
118
+ :where(.wt-root.wt-split) .wt-split-handle.wt-handle-held::before {
119
+ background: color-mix(in oklch, var(--tab-bg), var(--text) 25%);
120
+ }
121
+
122
+ /* 10-primitives.css scopes the focus ring to button:focus-visible; the handle is
123
+ a div, and the ring goes on the pill so it follows the visible control. */
124
+ :where(.wt-root.wt-split) .wt-split-handle:focus-visible {
125
+ outline: none;
126
+ }
127
+
128
+ :where(.wt-root.wt-split) .wt-split-handle:focus-visible::before {
129
+ outline: 2px solid var(--accent);
130
+ outline-offset: 2px;
131
+ }
132
+
133
+ /* Collapsed, the unselected pane is hidden in place rather than display: none:
134
+ what it measures is the width it will have if the split closes, so no
135
+ zero-size frame reaches its PTY. */
136
+ :where(.wt-root.wt-split.wt-split-open.wt-split-collapsed) {
137
+ grid-template-columns: minmax(0, 1fr);
138
+ }
139
+
140
+ /* Both side classes named, so this outranks the per-side column rules above. */
141
+ :where(.wt-root.wt-split.wt-split-open.wt-split-collapsed) > .wt-split-pane.wt-side-left,
142
+ :where(.wt-root.wt-split.wt-split-open.wt-split-collapsed) > .wt-split-pane.wt-side-right {
143
+ grid-column: 1;
144
+ }
145
+
146
+ :where(.wt-root.wt-split.wt-split-open.wt-split-collapsed) > .wt-split-handle {
147
+ display: none;
148
+ }
149
+
150
+ :where(.wt-root.wt-split.wt-split-open.wt-split-collapsed) > .wt-split-pane:not(.wt-pane-selected) {
151
+ visibility: hidden;
152
+ pointer-events: none;
153
+ }
154
+
155
+ :where(.wt-root.wt-split.wt-drop-left)::before,
156
+ :where(.wt-root.wt-split.wt-drop-right)::after {
157
+ content: "";
158
+ position: absolute;
159
+ inset-block: 0 var(--wt-tabbar-h, 0);
160
+ inline-size: 50%;
161
+ background: color-mix(in oklch, var(--accent) 12%, transparent);
162
+ pointer-events: none;
163
+ z-index: 5;
164
+ }
165
+
166
+ :where(.wt-root.wt-split.wt-drop-left)::before {
167
+ inset-inline-start: 0;
168
+ }
169
+
170
+ :where(.wt-root.wt-split.wt-drop-right)::after {
171
+ inset-inline-start: 50%;
172
+ }
173
+
174
+ :where(.wt-root.wt-split.wt-animate.wt-split-closing) {
175
+ transition: grid-template-columns var(--dur-standard) var(--ease-standard);
176
+ }
package/css/MANIFEST CHANGED
@@ -17,4 +17,5 @@ page.css
17
17
  23-toolbar.css
18
18
  30-tabs.css
19
19
  31-switcher.css
20
+ 32-split.css
20
21
  40-animations.css
@@ -13,4 +13,5 @@
13
13
  23-toolbar.css
14
14
  30-tabs.css
15
15
  31-switcher.css
16
+ 32-split.css
16
17
  40-animations.css
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "7.3.2",
3
+ "version": "8.0.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": "MPL-2.0",
@@ -40,7 +40,7 @@
40
40
  "verify": "bash scripts/verify.sh"
41
41
  },
42
42
  "peerDependencies": {
43
- "@cplieger/web-terminal-engine": "^5.2.0"
43
+ "@cplieger/web-terminal-engine": "^6.0.0"
44
44
  },
45
45
  "overrides": {
46
46
  "qs": "^6.15.2"
@@ -50,22 +50,22 @@
50
50
  "@stryker-mutator/core": "10.0.0",
51
51
  "@stryker-mutator/typescript-checker": "10.0.0",
52
52
  "@stryker-mutator/vitest-runner": "10.0.0",
53
- "@types/node": "26.5.1",
53
+ "@types/node": "26.6.1",
54
54
  "@typescript/native": "npm:typescript@7.0.2",
55
- "@vitest/browser": "5.0.0",
56
- "@vitest/browser-playwright": "5.0.0",
57
- "@vitest/coverage-v8": "5.0.0",
55
+ "@vitest/browser": "5.0.1",
56
+ "@vitest/browser-playwright": "5.0.1",
57
+ "@vitest/coverage-v8": "5.0.1",
58
58
  "eslint": "10.10.0",
59
- "fast-check": "4.10.0",
60
- "html-validate": "11.15.0",
61
- "knip": "6.35.1",
59
+ "fast-check": "4.10.1",
60
+ "html-validate": "11.16.0",
61
+ "knip": "6.36.0",
62
62
  "playwright": "1.63.0",
63
- "prettier": "3.9.6",
63
+ "prettier": "3.9.7",
64
64
  "stylelint": "17.15.0",
65
65
  "stylelint-config-standard": "40.0.0",
66
66
  "typescript": "npm:@typescript/typescript6@6.0.2",
67
67
  "typescript-eslint": "8.70.0",
68
- "vitest": "5.0.0"
68
+ "vitest": "5.0.1"
69
69
  },
70
70
  "publishConfig": {
71
71
  "access": "public"
@@ -97,6 +97,7 @@
97
97
  "LICENSE",
98
98
  "README.md",
99
99
  "!src/**/*.test.ts",
100
- "!src/**/*-setup.ts"
100
+ "!src/**/*-setup.ts",
101
+ "!src/**/test-helpers/**"
101
102
  ]
102
103
  }