@cplieger/web-terminal-ui 3.4.2 → 3.4.4
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 +2 -2
- package/css/00-tokens.css +1 -1
- package/css/01-reset.css +6 -0
- package/css/05-tabs.css +17 -24
- package/css/06-mobile.css +13 -1
- package/package.json +13 -12
- package/scaffold/index.html +6 -4
- package/src/features/tabs.ts +103 -52
- package/src/presets.ts +2 -2
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ individual features instead:
|
|
|
102
102
|
- `presetAgentTabbed()` — the same feature set as `presetTabbed()` (activity
|
|
103
103
|
monitor included), but with `preferInputTitle`: each tab's label follows the
|
|
104
104
|
latest submitted line (persisted server-side and recovered on reload) and the
|
|
105
|
-
program's OSC 0/2 title is ignored — for an agent shell such as `
|
|
105
|
+
program's OSC 0/2 title is ignored — for an agent shell such as `web-terminal-kiro`
|
|
106
106
|
whose program emits a non-empty but useless OSC title. Its status dots
|
|
107
107
|
(idle / working / done / needs-input) come from the same activity monitor,
|
|
108
108
|
driven by the server's status SSE and its OSC-9 classifier.
|
|
@@ -143,7 +143,7 @@ The web-terminal family:
|
|
|
143
143
|
Consumers that ship this UI:
|
|
144
144
|
|
|
145
145
|
- [`vibekit`](https://github.com/cplieger/vibekit)
|
|
146
|
-
- [`
|
|
146
|
+
- [`web-terminal-kiro`](https://github.com/cplieger/web-terminal-kiro)
|
|
147
147
|
|
|
148
148
|
## Disclaimer
|
|
149
149
|
|
package/css/00-tokens.css
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/* ===== Themeable tokens (the consumer-configurable "template") =====
|
|
11
11
|
These default to the neutral built-in look; a consumer recolors the UI by
|
|
12
12
|
overriding them via createTerminal's `theme` option (which sets them on the
|
|
13
|
-
terminal root) or its own CSS.
|
|
13
|
+
terminal root) or its own CSS. web-terminal-kiro sets a purple palette. Everything
|
|
14
14
|
above is the fixed design palette; everything here is the public theme API. */
|
|
15
15
|
--accent: oklch(72% 0.15 250deg); /* light blue; themed per consumer */
|
|
16
16
|
--tab-bg: oklch(30% 0 0deg / 90%); /* inactive-tab chip (translucent) */
|
package/css/01-reset.css
CHANGED
|
@@ -14,6 +14,12 @@ body {
|
|
|
14
14
|
background: var(--bg);
|
|
15
15
|
color-scheme: dark;
|
|
16
16
|
-webkit-font-smoothing: antialiased;
|
|
17
|
+
/* Disable the browser's landscape text autosizing ("text inflation"): without
|
|
18
|
+
this, rotating a phone to landscape zooms the text. iOS Safari implements
|
|
19
|
+
ONLY the -webkit- prefixed form (the unprefixed property is ignored there),
|
|
20
|
+
so both are required — unprefixed alone left iPhones inflating on rotate. */
|
|
21
|
+
/* stylelint-disable-next-line property-no-vendor-prefix -- iOS Safari only implements the prefixed form */
|
|
22
|
+
-webkit-text-size-adjust: 100%;
|
|
17
23
|
text-size-adjust: 100%;
|
|
18
24
|
overscroll-behavior: none;
|
|
19
25
|
/* Disable pinch-to-zoom and double-tap-to-zoom. The viewport meta tag
|
package/css/05-tabs.css
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
/* The active tab reads clearly above the inactive chips: a stronger fill plus an
|
|
74
74
|
accent-coloured edge (inactive chips carry a faint white --border; the active
|
|
75
|
-
one gets the accent equivalent). Themeable via --accent (
|
|
75
|
+
one gets the accent equivalent). Themeable via --accent (web-terminal-kiro -> purple). */
|
|
76
76
|
.wt-tab-active {
|
|
77
77
|
background: var(--tab-active-bg);
|
|
78
78
|
border-color: var(--tab-active-border);
|
|
@@ -195,24 +195,21 @@
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/* Desktop-strip keyboard button (tabs feature): opens the on-screen key grid on
|
|
198
|
-
a WIDE touchscreen (an iPad using the multi-tab strip
|
|
199
|
-
mobile switcher to carry one. Built + wired by
|
|
200
|
-
switcher's keyboard button (see makeKbButton
|
|
201
|
-
.wt-btn primitive for its 44px box + centered
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
198
|
+
a WIDE touchscreen (an iPad using the multi-tab strip, or a phone rotated to
|
|
199
|
+
landscape), where there is no mobile switcher to carry one. Built + wired by
|
|
200
|
+
the SAME factory as the mobile switcher's keyboard button (see makeKbButton
|
|
201
|
+
in tabs.ts). Uses the shared .wt-btn primitive for its 44px box + centered
|
|
202
|
+
icon, borderless like the switcher's — the old fixed-overlay styling gave it
|
|
203
|
+
a border and pinned it right of the "+", which read as a different control
|
|
204
|
+
the moment a phone rotated to landscape. Now an ordinary flex item inside the
|
|
205
|
+
strip, before the "+" (kb-then-new, mirroring the mobile switcher's order).
|
|
206
|
+
Hidden on a fine-pointer desktop (a real keyboard is present) and until a
|
|
207
|
+
keyboardToggle feature is wired ([hidden]); shown only on a coarse pointer at
|
|
208
|
+
strip widths (the media block below, kept in lockstep with SWITCHER_MEDIA in
|
|
209
|
+
src/features/tabs.ts). */
|
|
208
210
|
.wt-tab-kb {
|
|
209
211
|
display: none;
|
|
210
|
-
|
|
211
|
-
right: var(--sp-2);
|
|
212
|
-
bottom: calc(var(--safe-bottom) + var(--sp-2));
|
|
213
|
-
z-index: 16;
|
|
214
|
-
background: var(--surface);
|
|
215
|
-
border: 1px solid var(--border);
|
|
212
|
+
flex: none;
|
|
216
213
|
}
|
|
217
214
|
|
|
218
215
|
/* Grid open: filled so the toggled state reads at a glance (mirrors the
|
|
@@ -229,17 +226,13 @@
|
|
|
229
226
|
color: var(--bg);
|
|
230
227
|
}
|
|
231
228
|
|
|
232
|
-
/* Show
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
/* Show only on a WIDE touchscreen (where the desktop strip is used AND a
|
|
230
|
+
keyboardToggle is wired, hence :not([hidden])). In-flow now, so the strip
|
|
231
|
+
needs no reserved right padding for it. */
|
|
235
232
|
@media (pointer: coarse) and (width >= 601px) {
|
|
236
233
|
.wt-tab-kb:not([hidden]) {
|
|
237
234
|
display: inline-flex;
|
|
238
235
|
}
|
|
239
|
-
|
|
240
|
-
.wt-tab-bar {
|
|
241
|
-
padding-right: calc(var(--touch-target) + 2 * var(--sp-2));
|
|
242
|
-
}
|
|
243
236
|
}
|
|
244
237
|
|
|
245
238
|
/* The tab being dragged (reorder) dims while it moves. */
|
package/css/06-mobile.css
CHANGED
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
/* Right padding reserves room for the close (x) overlaid inside the row. */
|
|
112
112
|
padding: 0 var(--touch-target) 0 var(--sp-2);
|
|
113
113
|
/* The bar row always represents the active tab, so it wears the active-tab
|
|
114
|
-
fill/foreground the desktop strip uses (themeable; purple in
|
|
114
|
+
fill/foreground the desktop strip uses (themeable; purple in web-terminal-kiro). */
|
|
115
115
|
background: var(--tab-active-bg);
|
|
116
116
|
color: var(--tab-active-fg);
|
|
117
117
|
border: 1px solid var(--tab-active-border);
|
|
@@ -254,6 +254,18 @@
|
|
|
254
254
|
margin-inline-start: 0;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/* When the tab list expands the switch button collapses too (its job is done:
|
|
258
|
+
the list is open), mirroring the keyboard button's collapse in the same
|
|
259
|
+
animation (07-animations.css animates width + margin + opacity). Placed after
|
|
260
|
+
the .wt-switcher-multi reveal so it wins when both apply (expanded, two or
|
|
261
|
+
more tabs). */
|
|
262
|
+
.wt-switcher-expanded .wt-switcher-switch {
|
|
263
|
+
width: 0;
|
|
264
|
+
opacity: 0;
|
|
265
|
+
margin-inline-start: calc(-1 * var(--sp-1));
|
|
266
|
+
pointer-events: none;
|
|
267
|
+
}
|
|
268
|
+
|
|
257
269
|
@media (hover: hover) {
|
|
258
270
|
.wt-switcher-switch:hover {
|
|
259
271
|
background: var(--tab-hover-bg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.4",
|
|
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",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
],
|
|
14
14
|
"types": "./src/index.ts",
|
|
15
15
|
"scripts": {
|
|
16
|
-
"typecheck": "
|
|
17
|
-
"typecheck:tests": "
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
17
|
+
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
18
18
|
"test": "vitest --run",
|
|
19
19
|
"test:watch": "vitest",
|
|
20
20
|
"test:coverage": "vitest --run --coverage",
|
|
@@ -29,19 +29,20 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@eslint/js": "10.0.1",
|
|
32
|
-
"@types/node": "25.9.
|
|
33
|
-
"@
|
|
34
|
-
"
|
|
35
|
-
"
|
|
32
|
+
"@types/node": "25.9.5",
|
|
33
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
34
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
35
|
+
"eslint": "10.7.0",
|
|
36
|
+
"fast-check": "4.9.0",
|
|
36
37
|
"happy-dom": "20.10.6",
|
|
37
38
|
"html-validate": "11.5.5",
|
|
38
|
-
"knip": "6.
|
|
39
|
-
"prettier": "3.9.
|
|
39
|
+
"knip": "6.26.0",
|
|
40
|
+
"prettier": "3.9.5",
|
|
40
41
|
"stylelint": "17.14.0",
|
|
41
42
|
"stylelint-config-standard": "40.0.0",
|
|
42
|
-
"typescript": "6.0.
|
|
43
|
-
"typescript-eslint": "8.
|
|
44
|
-
"vitest": "4.1.
|
|
43
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
44
|
+
"typescript-eslint": "8.63.0",
|
|
45
|
+
"vitest": "4.1.10"
|
|
45
46
|
},
|
|
46
47
|
"publishConfig": {
|
|
47
48
|
"access": "public"
|
package/scaffold/index.html
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
<link rel="stylesheet" href="/style.css">
|
|
10
10
|
<!-- [html-validate-disable-next no-style-tag -- critical CSS for instant loading indicator] -->
|
|
11
11
|
<style>
|
|
12
|
-
#loading{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
|
|
12
|
+
#loading{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000;color:var(--accent,#6ea8ff);transition:opacity .3s ease}
|
|
13
13
|
#loading.fade{opacity:0}
|
|
14
|
-
#loading .
|
|
15
|
-
|
|
14
|
+
#loading .bar{position:relative;inline-size:min(220px,62vw);block-size:5px;overflow:hidden;border-radius:999px;background:color-mix(in oklch,currentColor 16%,transparent)}
|
|
15
|
+
#loading .bar::before{content:"";position:absolute;inset-block:0;inline-size:42%;border-radius:inherit;background:linear-gradient(90deg,transparent,currentColor 28%,color-mix(in oklch,currentColor,#fff 45%) 50%,currentColor 72%,transparent);filter:drop-shadow(0 0 4px color-mix(in oklch,currentColor 55%,transparent));animation:wt-loading-slide 1.5s cubic-bezier(.65,0,.35,1) infinite}
|
|
16
|
+
@keyframes wt-loading-slide{0%{transform:translateX(-110%)}100%{transform:translateX(250%)}}
|
|
17
|
+
@media (prefers-reduced-motion:reduce){#loading .bar::before{animation-duration:3s}}
|
|
16
18
|
</style>
|
|
17
19
|
</head>
|
|
18
20
|
<body>
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
element. The loading overlay is a sibling (not a child of the root) so it is
|
|
21
23
|
never cleared; it is faded out once the first frame renders. -->
|
|
22
24
|
<div id="terminal"></div>
|
|
23
|
-
<div id="loading"><div class="
|
|
25
|
+
<div id="loading" role="status" aria-label="Loading"><div class="bar" aria-hidden="true"></div></div>
|
|
24
26
|
<!-- Deployment-specific: the importmap resolves the bare package specifiers to
|
|
25
27
|
the compiled JS your build emits. Adjust the paths to your build layout. -->
|
|
26
28
|
<script type="importmap">
|
package/src/features/tabs.ts
CHANGED
|
@@ -146,7 +146,7 @@ export interface TabsOptions {
|
|
|
146
146
|
/** Prefer the input-derived title over the process OSC 0/2 title. Default
|
|
147
147
|
* false (OSC-first: a program that sets its own window title wins, else the
|
|
148
148
|
* last submitted line). Set true for an agent shell whose program emits a
|
|
149
|
-
* non-empty but useless OSC title (kiro-cli under
|
|
149
|
+
* non-empty but useless OSC title (kiro-cli under web-terminal-kiro): the label then
|
|
150
150
|
* follows the latest submitted line (live) or the persisted client title (on
|
|
151
151
|
* reload), and the unreliable OSC title is ignored entirely. presetAgentTabbed
|
|
152
152
|
* enables this. */
|
|
@@ -160,10 +160,12 @@ export interface TabsOptions {
|
|
|
160
160
|
const CLOSE_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6 18 18M18 6 6 18"/></svg>`;
|
|
161
161
|
const NEW_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>`;
|
|
162
162
|
const KB_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="3.5"/><path d="M15 15 9.5 9.5M9.5 13V9.5H13"/></svg>`;
|
|
163
|
-
// Two-
|
|
164
|
-
// button
|
|
165
|
-
//
|
|
166
|
-
|
|
163
|
+
// Two-overlapping-windows glyph for the mobile switcher's dedicated open/close
|
|
164
|
+
// button: the browser-style "tab switcher" icon, more recognisable than the
|
|
165
|
+
// prior swap-arrows (which read like a keyboard Tab key). A latest-wins
|
|
166
|
+
// background-tab notification dot rides on it (see switchButtonHTML). Same
|
|
167
|
+
// viewBox + stroke=currentcolor treatment as the others.
|
|
168
|
+
const SWITCH_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="8" width="13" height="13" rx="2"/><path d="M8 8V6a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-3"/></svg>`;
|
|
167
169
|
|
|
168
170
|
// chipContent is the ONE builder for a tab chip's content — a status dot, a
|
|
169
171
|
// label, and a close (x) — shared by all three chip sites: the desktop strip
|
|
@@ -357,7 +359,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
357
359
|
}
|
|
358
360
|
// Persist the input-derived tab title server-side so it survives a page reload
|
|
359
361
|
// and shows on other devices. The engine stores it as the session's fallback
|
|
360
|
-
// title and uses it only when the program emits no OSC title (the
|
|
362
|
+
// title and uses it only when the program emits no OSC title (the web-terminal-kiro case:
|
|
361
363
|
// kiro-cli emits no usable OSC title, so the latest user message is the label).
|
|
362
364
|
// Best-effort and fire-and-forget: a failure never disrupts the terminal — the
|
|
363
365
|
// locally-derived title still displays, it just is not persisted this time.
|
|
@@ -456,20 +458,19 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
456
458
|
bar.className = "wt-tab-bar";
|
|
457
459
|
bar.setAttribute("role", "tablist");
|
|
458
460
|
slot.appendChild(bar);
|
|
459
|
-
//
|
|
460
|
-
// the
|
|
461
|
-
//
|
|
462
|
-
//
|
|
461
|
+
// Strip-end controls, both flex items INSIDE the strip so they flow after
|
|
462
|
+
// the tabs (appending them to the slot flowed them out of the strip):
|
|
463
|
+
// the keyboard button first, then the "+" — mirroring the mobile
|
|
464
|
+
// switcher's kb-before-new order, so rotating a phone to landscape (which
|
|
465
|
+
// swaps in this strip) keeps the keyboard button LEFT of the "+" rather
|
|
466
|
+
// than jumping to its right. Built + wired by the same shared factories
|
|
467
|
+
// as the mobile switcher's; the kb button is CSS-gated to a wide
|
|
468
|
+
// touchscreen and un-hidden below only when a keyboardToggle is wired.
|
|
469
|
+
// addTabChrome inserts each tab before deskKb, keeping [tabs… kb +].
|
|
470
|
+
const deskKb = makeKbButton("wt-tab-kb wt-btn");
|
|
471
|
+
bar.appendChild(deskKb);
|
|
463
472
|
const newBtn = makeNewButton("wt-tab-new");
|
|
464
473
|
bar.appendChild(newBtn);
|
|
465
|
-
// Desktop-strip keyboard button, built + wired by the SAME factory as the
|
|
466
|
-
// mobile switcher's. A SIBLING of the strip (appended to the region slot,
|
|
467
|
-
// not into .wt-tab-bar) so it never affects the strip's child order; CSS
|
|
468
|
-
// shows it only on a wide touchscreen (where the strip, not the switcher,
|
|
469
|
-
// is used) and hides it on a fine-pointer desktop. Un-hidden below only
|
|
470
|
-
// when a keyboardToggle feature is wired.
|
|
471
|
-
const deskKb = makeKbButton("wt-tab-kb wt-btn");
|
|
472
|
-
slot.appendChild(deskKb);
|
|
473
474
|
|
|
474
475
|
// Pull the terminal surface up off the fixed BOTTOM strip on desktop so
|
|
475
476
|
// the bar does not overlap the last rows. The surface is position:fixed
|
|
@@ -520,8 +521,12 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
520
521
|
swBar.append(swKb, swSwitch, swNew);
|
|
521
522
|
// Latest-wins notification state for the switch button's dot: overwritten
|
|
522
523
|
// by each qualifying background-tab event (applyStatus) and cleared when
|
|
523
|
-
// the list opens (expandSwitcher)
|
|
524
|
+
// the list opens (expandSwitcher) or the raising tab is visited or closed
|
|
525
|
+
// (acknowledgeSwitchNotify). "" = no pending cue; the dot is hidden.
|
|
524
526
|
let switchNotify: "" | "input" | "done" = "";
|
|
527
|
+
// The session that raised the pending cue, so arriving on that tab (a
|
|
528
|
+
// swipe or any switch) acknowledges it without opening the list.
|
|
529
|
+
let switchNotifyId: string | null = null;
|
|
525
530
|
function paintSwitchDot(): void {
|
|
526
531
|
// Reuse the per-tab status-dot colours (single source, css/05-tabs.css
|
|
527
532
|
// .wt-status-dot[data-status="input"|"done"]) instead of re-declaring
|
|
@@ -533,6 +538,21 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
533
538
|
swSwitchDot.dataset["status"] = switchNotify;
|
|
534
539
|
}
|
|
535
540
|
}
|
|
541
|
+
function clearSwitchNotify(): void {
|
|
542
|
+
switchNotify = "";
|
|
543
|
+
switchNotifyId = null;
|
|
544
|
+
paintSwitchDot();
|
|
545
|
+
}
|
|
546
|
+
// acknowledgeSwitchNotify clears the pending cue when its subject session
|
|
547
|
+
// is reached (switchTo — including a swipe arriving on it) or ceases to
|
|
548
|
+
// exist (a close/reap): the notification is resolved or moot then, and
|
|
549
|
+
// only opening the list used to clear it (the reported "swiping to the
|
|
550
|
+
// concerned tab leaves the dot lit").
|
|
551
|
+
function acknowledgeSwitchNotify(id: string): void {
|
|
552
|
+
if (switchNotifyId === id) {
|
|
553
|
+
clearSwitchNotify();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
536
556
|
ctx.region("bottom-switcher", "switcher").appendChild(switcher);
|
|
537
557
|
// The keyboard buttons open the key grid; show them only when a toolbar is
|
|
538
558
|
// wired to drive. Read the toolbar's API lazily at tap time (ctx.use), so
|
|
@@ -1099,33 +1119,43 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1099
1119
|
from = first - el.getBoundingClientRect().top; // survivor: old spot -> new
|
|
1100
1120
|
} else {
|
|
1101
1121
|
from = dir === "next" ? pitch : -pitch; // newcomer: in from the trailing edge
|
|
1102
|
-
el.style.opacity = "0";
|
|
1122
|
+
el.style.opacity = "0"; // fades in as it rotates to its slot (below)
|
|
1103
1123
|
}
|
|
1104
1124
|
el.style.transition = "none";
|
|
1105
1125
|
el.style.transform = `translateY(${String(Math.round(from))}px)`;
|
|
1106
1126
|
}
|
|
1107
1127
|
const exit = dir === "next" ? -pitch : pitch; // leaving row off the leading edge
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1128
|
+
// The leaving row starts at its captured spot, fully opaque.
|
|
1129
|
+
ghost.style.transition = "none";
|
|
1130
|
+
ghost.style.transform = "translateY(0)";
|
|
1131
|
+
ghost.style.opacity = "1";
|
|
1132
|
+
// Commit the from-state (transforms + opacities) with a forced reflow
|
|
1133
|
+
// BEFORE the to-state, so BOTH the transform and the opacity transitions
|
|
1134
|
+
// fire from it. The prior code used a bare rAF (letting the browser
|
|
1135
|
+
// collapse from->to into one recalc) and reverted the entering row's
|
|
1136
|
+
// opacity to "" (no explicit end value), so the fade never animated
|
|
1137
|
+
// (the reported "no fade in / fade out"). The modern display/visibility
|
|
1138
|
+
// transition (transition-behavior: allow-discrete + @starting-style,
|
|
1139
|
+
// Baseline 2024) does NOT apply here: these rows are reused and moved by
|
|
1140
|
+
// a JS transform FLIP, not toggled via display:none, so the reliable
|
|
1141
|
+
// path is a real reflow plus an explicit opacity transition.
|
|
1142
|
+
swList.getBoundingClientRect(); // read forces the reflow (commit the from-state)
|
|
1143
|
+
// Couple opacity to the SAME easing + duration as the transform so a
|
|
1144
|
+
// row's fade tracks its DISTANCE from its target slot (each reel row
|
|
1145
|
+
// travels one pitch): a row is transparent a pitch away (at the clipped
|
|
1146
|
+
// edge) and only fully opaque once it settles. Entering rows fade IN as
|
|
1147
|
+
// they rotate in, the leaving row fades OUT as it exits: no hard cutoff
|
|
1148
|
+
// at the list edges and no permanent edge mask.
|
|
1149
|
+
const trans =
|
|
1150
|
+
"transform 0.25s cubic-bezier(0.2, 0, 0, 1), opacity 0.25s cubic-bezier(0.2, 0, 0, 1)";
|
|
1151
|
+
for (const el of rowEls.values()) {
|
|
1152
|
+
el.style.transition = trans;
|
|
1153
|
+
el.style.transform = "translateY(0)";
|
|
1154
|
+
el.style.opacity = "1";
|
|
1155
|
+
}
|
|
1156
|
+
ghost.style.transition = trans;
|
|
1157
|
+
ghost.style.transform = `translateY(${String(Math.round(exit))}px)`;
|
|
1158
|
+
ghost.style.opacity = "0";
|
|
1129
1159
|
reelTimer = setTimeout(endReelNow, REEL_MS);
|
|
1130
1160
|
};
|
|
1131
1161
|
}
|
|
@@ -1211,8 +1241,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1211
1241
|
}
|
|
1212
1242
|
// Opening the list acknowledges any pending background-tab notification:
|
|
1213
1243
|
// the user is now looking at the tabs, so clear the switch button's dot.
|
|
1214
|
-
|
|
1215
|
-
paintSwitchDot();
|
|
1244
|
+
clearSwitchNotify();
|
|
1216
1245
|
renderSwitcherList();
|
|
1217
1246
|
setExpandedState(true);
|
|
1218
1247
|
ctx.announce("Terminal list expanded");
|
|
@@ -1245,8 +1274,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1245
1274
|
}
|
|
1246
1275
|
paintStatusDot(dot, info.status, info.reportsActivity ?? false);
|
|
1247
1276
|
const aria = tablist.registerTab(el);
|
|
1248
|
-
// Insert before the
|
|
1249
|
-
|
|
1277
|
+
// Insert before the strip-end controls (keyboard button, then "+") so
|
|
1278
|
+
// they stay the last items in the strip.
|
|
1279
|
+
bar.insertBefore(el, deskKb);
|
|
1250
1280
|
// Runtime-added tabs animate in; initial tabs do not (see `started`).
|
|
1251
1281
|
// The timer (not animationend) also clears the class on the hidden mobile
|
|
1252
1282
|
// strip, where the animation never fires.
|
|
@@ -1378,6 +1408,10 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1378
1408
|
// rebuild viewport-first, so the last-known screen paints with no
|
|
1379
1409
|
// round-trip. Then let the kernel reconnect the WS to it (resume delta).
|
|
1380
1410
|
activeId = next.id;
|
|
1411
|
+
// Arriving on the tab that raised the switch-button cue resolves it
|
|
1412
|
+
// (a swipe through the tabs must dismiss the dot, not only opening the
|
|
1413
|
+
// list).
|
|
1414
|
+
acknowledgeSwitchNotify(next.id);
|
|
1381
1415
|
try {
|
|
1382
1416
|
localStorage.setItem(ACTIVE_TAB_KEY, next.id);
|
|
1383
1417
|
} catch {
|
|
@@ -1510,9 +1544,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1510
1544
|
|
|
1511
1545
|
// dragTargetBefore returns the first tab whose horizontal midpoint is past
|
|
1512
1546
|
// x (the element the dragged tab should sit before), or null to drop at the
|
|
1513
|
-
// end (before the
|
|
1514
|
-
// strip's DOM order after a drag, so position indicators, the
|
|
1515
|
-
// close-to-the-right/left all follow the visible order.
|
|
1547
|
+
// end (before the strip-end controls). syncOrderFromDom rebuilds tabList to
|
|
1548
|
+
// match the strip's DOM order after a drag, so position indicators, the
|
|
1549
|
+
// switcher, and close-to-the-right/left all follow the visible order.
|
|
1516
1550
|
function dragTargetBefore(x: number): HTMLElement | null {
|
|
1517
1551
|
for (const el of bar.querySelectorAll<HTMLElement>(".wt-tab:not(.wt-tab-dragging)")) {
|
|
1518
1552
|
const rect = el.getBoundingClientRect();
|
|
@@ -1635,6 +1669,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1635
1669
|
// Tombstone this id briefly so a stale status snapshot/poll that predates
|
|
1636
1670
|
// the server reaping it cannot re-adopt the just-closed tab.
|
|
1637
1671
|
tombstone(id);
|
|
1672
|
+
// A pending switch-button cue whose subject just closed is moot: clear
|
|
1673
|
+
// it rather than leaving a dot no tab visit can ever resolve.
|
|
1674
|
+
acknowledgeSwitchNotify(id);
|
|
1638
1675
|
tab.aria.remove();
|
|
1639
1676
|
// Remove immediately (no exit animation): a lingering element made the
|
|
1640
1677
|
// "+" teleport after a delay, and made a last-tab replacement appear in
|
|
@@ -1699,6 +1736,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1699
1736
|
// server reaping these sessions cannot re-adopt (flash back) a just-
|
|
1700
1737
|
// closed tab -- mirrors the single-close path in dropTab (h-f2).
|
|
1701
1738
|
tombstone(t.id);
|
|
1739
|
+
acknowledgeSwitchNotify(t.id); // a cue for a closed tab is moot
|
|
1702
1740
|
t.aria.remove();
|
|
1703
1741
|
t.el.remove();
|
|
1704
1742
|
ctx.dropSession(t.id);
|
|
@@ -1868,6 +1906,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1868
1906
|
// dedicated button.
|
|
1869
1907
|
if (id !== activeId && (status === "input" || status === "done")) {
|
|
1870
1908
|
switchNotify = status;
|
|
1909
|
+
switchNotifyId = id;
|
|
1871
1910
|
paintSwitchDot();
|
|
1872
1911
|
}
|
|
1873
1912
|
// Record the raw server title; the displayed label (fallback + de-dup)
|
|
@@ -1902,15 +1941,20 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1902
1941
|
return;
|
|
1903
1942
|
}
|
|
1904
1943
|
// Adopt a session created in another browser so all clients converge.
|
|
1944
|
+
// Carry the persisted clientTitle through: the SSE snapshot usually
|
|
1945
|
+
// beats the initial GET /api/sessions (whose adoptSession then dedups
|
|
1946
|
+
// and never re-applies its fields), so dropping it here left every
|
|
1947
|
+
// SSE-adopted tab without its persisted title — in preferInputTitle
|
|
1948
|
+
// mode the label then fell back to "New tab" on reload.
|
|
1905
1949
|
adoptSession({
|
|
1906
1950
|
id: s.id,
|
|
1907
1951
|
title: s.title,
|
|
1908
|
-
clientTitle:
|
|
1952
|
+
clientTitle: s.clientTitle,
|
|
1909
1953
|
createdAt: s.createdAt,
|
|
1910
1954
|
status: s.status,
|
|
1911
1955
|
reportsActivity: s.reportsActivity ?? false,
|
|
1912
1956
|
});
|
|
1913
|
-
applyStatus(s.id, s.status, s.title,
|
|
1957
|
+
applyStatus(s.id, s.status, s.title, s.clientTitle, s.reportsActivity ?? false);
|
|
1914
1958
|
ensureActive();
|
|
1915
1959
|
syncChrome();
|
|
1916
1960
|
});
|
|
@@ -1982,7 +2026,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1982
2026
|
if (e.dataTransfer) {
|
|
1983
2027
|
e.dataTransfer.dropEffect = "move";
|
|
1984
2028
|
}
|
|
1985
|
-
bar.insertBefore(draggingEl, dragTargetBefore(e.clientX) ??
|
|
2029
|
+
bar.insertBefore(draggingEl, dragTargetBefore(e.clientX) ?? deskKb);
|
|
1986
2030
|
});
|
|
1987
2031
|
// Active-row close (x): closes the current tab (mirrors a listed row's x).
|
|
1988
2032
|
// stopPropagation so it is not read as a tap/swipe on the row surface.
|
|
@@ -2042,9 +2086,16 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2042
2086
|
return;
|
|
2043
2087
|
}
|
|
2044
2088
|
const target = e.target as HTMLElement | null;
|
|
2089
|
+
// The tab strip counts as chrome like the switcher: without it, the
|
|
2090
|
+
// desktop-strip keyboard button's own pointerup landed here, closed the
|
|
2091
|
+
// grid, and the button's click then re-opened it — so tapping the button
|
|
2092
|
+
// to CLOSE the grid never worked (a wide-touchscreen / landscape-phone
|
|
2093
|
+
// bug; the switcher's kb button was already exempt via .wt-switcher).
|
|
2045
2094
|
const inChrome =
|
|
2046
2095
|
target !== null &&
|
|
2047
|
-
(target.closest(".wt-switcher") !== null ||
|
|
2096
|
+
(target.closest(".wt-switcher") !== null ||
|
|
2097
|
+
target.closest(".wt-tab-bar") !== null ||
|
|
2098
|
+
target.closest(".key-toolbar") !== null);
|
|
2048
2099
|
if (inChrome) {
|
|
2049
2100
|
return;
|
|
2050
2101
|
}
|
package/src/presets.ts
CHANGED
|
@@ -66,7 +66,7 @@ function buildTabbed(preferInputTitle: boolean): TerminalFeature<unknown>[] {
|
|
|
66
66
|
/** Tabbed UI: the touch features, tabs, the mobile keyboard bar, the activity
|
|
67
67
|
* monitor, and animations. Requires a server that speaks the session API
|
|
68
68
|
* (`/api/sessions`, `/ws?session=`, and the status SSE `/api/sessions/events`),
|
|
69
|
-
* such as `web-terminal-server` or `
|
|
69
|
+
* such as `web-terminal-server` or `web-terminal-kiro`. Each tab's title follows the
|
|
70
70
|
* process OSC 0/2 window title when the program sets one, else the last
|
|
71
71
|
* submitted line; each tab's activity dot stays hidden until its session
|
|
72
72
|
* reports activity via OSC 9;4 progress (kiro-cli, Claude Code, …), so a plain
|
|
@@ -75,7 +75,7 @@ export function presetTabbed(): TerminalFeature<unknown>[] {
|
|
|
75
75
|
return buildTabbed(false);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Tabbed UI for an agent shell (
|
|
78
|
+
/** Tabbed UI for an agent shell (web-terminal-kiro). Same features as presetTabbed, but
|
|
79
79
|
* with `preferInputTitle`: the agent's program (kiro-cli) emits a non-empty but
|
|
80
80
|
* useless OSC 0/2 title, so each tab's label follows the latest submitted line
|
|
81
81
|
* (persisted server-side and recovered on reload) and the OSC title is ignored.
|