@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.
- package/README.md +114 -18
- package/css/30-tabs.css +21 -0
- package/css/31-switcher.css +15 -0
- package/css/32-split.css +176 -0
- package/css/MANIFEST +1 -0
- package/css/MANIFEST.tabbed +1 -0
- package/package.json +13 -12
- package/src/composition.ts +165 -237
- package/src/features/activity-monitor.ts +20 -55
- package/src/features/animations.ts +12 -30
- package/src/features/clipboard.ts +36 -64
- package/src/features/connection-banner.ts +10 -19
- package/src/features/context-menu.ts +70 -145
- package/src/features/dom.ts +44 -54
- package/src/features/menu-position.ts +24 -28
- package/src/features/mobile-toolbar.ts +32 -62
- package/src/features/predictive-echo.ts +15 -49
- package/src/features/scroll-to-bottom.ts +12 -24
- package/src/features/tabs/index.ts +1398 -1806
- package/src/features/tabs/ink-centre.ts +45 -54
- package/src/features/tabs/model.ts +301 -383
- package/src/features/tabs/strip.ts +45 -97
- package/src/features/tabs/switcher.ts +70 -48
- package/src/index.ts +13 -13
- package/src/kernel/a11y.ts +106 -27
- package/src/kernel/attention.ts +157 -0
- package/src/kernel/conn-state.ts +28 -22
- package/src/kernel/fatal.ts +76 -0
- package/src/kernel/feature-host.ts +136 -0
- package/src/kernel/gesture.ts +9 -13
- package/src/kernel/kernel.ts +57 -2139
- package/src/kernel/layout-policy.ts +44 -0
- package/src/kernel/loading-status.ts +30 -88
- package/src/kernel/navigation.ts +11 -17
- package/src/kernel/notify.ts +158 -0
- package/src/kernel/pane.ts +1415 -0
- package/src/kernel/realm.ts +11 -0
- package/src/kernel/regions.ts +8 -11
- package/src/kernel/scrollback.ts +88 -168
- package/src/kernel/selection.ts +25 -0
- package/src/kernel/shell.ts +1434 -0
- package/src/kernel/split-handle.ts +280 -0
- package/src/kernel/status-share.ts +100 -0
- package/src/kernel/types.ts +457 -441
- package/src/predict.ts +144 -170
- package/src/viewport.ts +124 -197
- package/src/features/tabs/attention.ts +0 -250
- 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
|
-
[](https://www.npmjs.com/package/@cplieger/web-terminal-ui)
|
|
4
|
-
[](https://jsr.io/@cplieger/web-terminal-ui)
|
|
5
|
-
[](https://github.com/cplieger/web-terminal-ui/actions/workflows/coverage.yml)
|
|
6
|
-
[](https://github.com/cplieger/web-terminal-ui/issues?q=label%3Astryker-tracker)
|
|
7
|
-
[](https://www.bestpractices.dev/projects/13431)
|
|
8
|
-
[](https://scorecard.dev/viewer/?uri=github.com/cplieger/web-terminal-ui)
|
|
3
|
+
[](https://www.npmjs.com/package/@cplieger/web-terminal-ui) [](https://jsr.io/@cplieger/web-terminal-ui) [](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,
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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.
|
|
431
|
-
|
|
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
|
|
437
|
-
|
|
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
|
package/css/31-switcher.css
CHANGED
|
@@ -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
|
package/css/32-split.css
ADDED
|
@@ -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
package/css/MANIFEST.tabbed
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "
|
|
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": "^
|
|
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.
|
|
53
|
+
"@types/node": "26.6.1",
|
|
54
54
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
55
|
-
"@vitest/browser": "5.0.
|
|
56
|
-
"@vitest/browser-playwright": "5.0.
|
|
57
|
-
"@vitest/coverage-v8": "5.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.
|
|
60
|
-
"html-validate": "11.
|
|
61
|
-
"knip": "6.
|
|
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.
|
|
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.
|
|
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
|
}
|