@cplieger/web-terminal-ui 3.0.3 → 3.0.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/css/03-v3.css +84 -8
- package/package.json +1 -1
- package/src/features/tabs.ts +54 -1
package/css/03-v3.css
CHANGED
|
@@ -219,6 +219,9 @@
|
|
|
219
219
|
align-items: center;
|
|
220
220
|
gap: var(--sp-1);
|
|
221
221
|
padding: var(--sp-1) var(--sp-2);
|
|
222
|
+
/* A transparent border is reserved on every tab so toggling the inactive
|
|
223
|
+
outline (below) never shifts the strip's layout. */
|
|
224
|
+
border: 1px solid transparent;
|
|
222
225
|
border-radius: var(--radius-inner);
|
|
223
226
|
color: color-mix(in oklch, var(--text) 70%, transparent);
|
|
224
227
|
font-family: var(--font-ui);
|
|
@@ -228,6 +231,17 @@
|
|
|
228
231
|
user-select: none;
|
|
229
232
|
}
|
|
230
233
|
|
|
234
|
+
/* Inactive tabs read as outlined chips (so adjacent tabs stay visually
|
|
235
|
+
separated) and take a subtle fill on hover. */
|
|
236
|
+
.wt-tab:not(.wt-tab-active) {
|
|
237
|
+
border-color: var(--border);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.wt-tab:not(.wt-tab-active):hover {
|
|
241
|
+
background: var(--text-active);
|
|
242
|
+
color: var(--text);
|
|
243
|
+
}
|
|
244
|
+
|
|
231
245
|
.wt-tab-active {
|
|
232
246
|
background: color-mix(in oklch, var(--text) 10%, var(--surface));
|
|
233
247
|
color: var(--text);
|
|
@@ -239,12 +253,14 @@
|
|
|
239
253
|
text-overflow: ellipsis;
|
|
240
254
|
}
|
|
241
255
|
|
|
242
|
-
/*
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
256
|
+
/* A little breathing room after the activity dot, before the label. */
|
|
257
|
+
.wt-tab-dot {
|
|
258
|
+
margin-right: 2px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* The tab close (×): a centered, muted square glyph button with a box on hover.
|
|
262
|
+
font: inherit so the glyph uses the UI font and centers cleanly in its box. */
|
|
263
|
+
.wt-tab-close {
|
|
248
264
|
display: inline-flex;
|
|
249
265
|
align-items: center;
|
|
250
266
|
justify-content: center;
|
|
@@ -255,13 +271,72 @@
|
|
|
255
271
|
border: none;
|
|
256
272
|
border-radius: var(--radius-inner);
|
|
257
273
|
color: color-mix(in oklch, var(--text) 60%, transparent);
|
|
274
|
+
font: inherit;
|
|
258
275
|
font-size: 1rem;
|
|
259
276
|
line-height: 1;
|
|
260
277
|
cursor: pointer;
|
|
261
278
|
}
|
|
262
279
|
|
|
263
|
-
.wt-tab-close:hover
|
|
280
|
+
.wt-tab-close:hover {
|
|
281
|
+
background: var(--text-active);
|
|
282
|
+
color: var(--text);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* On an inactive tab the close appears only on hover, kept in the layout via
|
|
286
|
+
visibility so the tab width never jumps; on the active tab it is always shown. */
|
|
287
|
+
.wt-tab:not(.wt-tab-active) .wt-tab-close {
|
|
288
|
+
visibility: hidden;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.wt-tab:not(.wt-tab-active):hover .wt-tab-close {
|
|
292
|
+
visibility: visible;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* The add-tab (+) button: its hover box matches an active tab's box (it
|
|
296
|
+
stretches to the strip height and uses the same active-tab fill), it centers
|
|
297
|
+
its glyph, and a left margin keeps its hover box clear of the last tab's. */
|
|
298
|
+
.wt-tab-new {
|
|
299
|
+
display: inline-flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
justify-content: center;
|
|
302
|
+
align-self: stretch;
|
|
303
|
+
min-width: 1.875rem;
|
|
304
|
+
margin-left: var(--sp-2);
|
|
305
|
+
padding: 0 var(--sp-1);
|
|
306
|
+
background: transparent;
|
|
307
|
+
border: none;
|
|
308
|
+
border-radius: var(--radius-inner);
|
|
309
|
+
color: color-mix(in oklch, var(--text) 60%, transparent);
|
|
310
|
+
font: inherit;
|
|
311
|
+
font-size: 1.1rem;
|
|
312
|
+
line-height: 1;
|
|
313
|
+
cursor: pointer;
|
|
314
|
+
}
|
|
315
|
+
|
|
264
316
|
.wt-tab-new:hover {
|
|
317
|
+
background: color-mix(in oklch, var(--text) 10%, var(--surface));
|
|
318
|
+
color: var(--text);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Close-all: a right-anchored text button (pushed to the far end of the strip). */
|
|
322
|
+
.wt-tab-closeall {
|
|
323
|
+
display: inline-flex;
|
|
324
|
+
align-items: center;
|
|
325
|
+
justify-content: center;
|
|
326
|
+
align-self: stretch;
|
|
327
|
+
margin-left: auto;
|
|
328
|
+
padding: 0 var(--sp-2);
|
|
329
|
+
background: transparent;
|
|
330
|
+
border: none;
|
|
331
|
+
border-radius: var(--radius-inner);
|
|
332
|
+
color: color-mix(in oklch, var(--text) 60%, transparent);
|
|
333
|
+
font-family: var(--font-ui);
|
|
334
|
+
font-size: 0.75rem;
|
|
335
|
+
white-space: nowrap;
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.wt-tab-closeall:hover {
|
|
265
340
|
background: var(--text-active);
|
|
266
341
|
color: var(--text);
|
|
267
342
|
}
|
|
@@ -363,7 +438,8 @@
|
|
|
363
438
|
|
|
364
439
|
.wt-animate .wt-tab,
|
|
365
440
|
.wt-animate .wt-tab-close,
|
|
366
|
-
.wt-animate .wt-tab-new
|
|
441
|
+
.wt-animate .wt-tab-new,
|
|
442
|
+
.wt-animate .wt-tab-closeall {
|
|
367
443
|
transition:
|
|
368
444
|
background-color var(--dur-micro) var(--ease-standard),
|
|
369
445
|
color var(--dur-micro) var(--ease-standard);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.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",
|
package/src/features/tabs.ts
CHANGED
|
@@ -91,6 +91,8 @@ const TAB_HTML = `
|
|
|
91
91
|
|
|
92
92
|
const NEW_HTML = `<button type="button" class="wt-tab-new" aria-label="New terminal">+</button>`;
|
|
93
93
|
|
|
94
|
+
const CLOSEALL_HTML = `<button type="button" class="wt-tab-closeall" aria-label="Close all terminals">Close all</button>`;
|
|
95
|
+
|
|
94
96
|
// Mobile bottom-switcher: the active tab (dot + label + position) as a tap/swipe
|
|
95
97
|
// surface, plus a >=44px overview control that carries the aggregate badge.
|
|
96
98
|
const SWITCHER_HTML = `
|
|
@@ -172,6 +174,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
172
174
|
// end of the tabs (addTabChrome inserts each tab before it).
|
|
173
175
|
const newBtn = fromHTML(NEW_HTML);
|
|
174
176
|
bar.appendChild(newBtn);
|
|
177
|
+
// A right-anchored "Close all" button (CSS margin-left: auto pushes it to
|
|
178
|
+
// the far end of the strip). Desktop only, since it lives inside the bar,
|
|
179
|
+
// which is hidden on a coarse pointer.
|
|
180
|
+
const closeAllBtn = fromHTML(CLOSEALL_HTML);
|
|
181
|
+
bar.appendChild(closeAllBtn);
|
|
175
182
|
|
|
176
183
|
// Push the terminal surface below the fixed strip on desktop so the bar
|
|
177
184
|
// does not overlap the first rows. The surface is position:fixed inset:0,
|
|
@@ -222,6 +229,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
222
229
|
let lastFocus: HTMLElement | null = null;
|
|
223
230
|
let hintShown = false;
|
|
224
231
|
let tabSeq = 0; // monotonic source for the "Tab N" fallback number
|
|
232
|
+
// On a touch device, focusing the hidden input opens the virtual keyboard.
|
|
233
|
+
// A switch triggered by tapping/swiping the tab UI must not do that (it is
|
|
234
|
+
// disruptive), so the input is focused on a switch only with a fine
|
|
235
|
+
// pointer; on touch the user taps the terminal itself to open the keyboard.
|
|
236
|
+
const coarse = window.matchMedia("(pointer: coarse)").matches;
|
|
225
237
|
|
|
226
238
|
// labelFor is a tab's base label before de-duplication. Preference order:
|
|
227
239
|
// the title derived from the user's first submitted line (best for a chat
|
|
@@ -475,6 +487,19 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
475
487
|
e.stopPropagation();
|
|
476
488
|
void close_(tab.id);
|
|
477
489
|
});
|
|
490
|
+
// Middle-click closes the tab (#8). Suppress the middle-click default on
|
|
491
|
+
// mousedown so the browser's autoscroll/paste affordance does not fire.
|
|
492
|
+
el.addEventListener("mousedown", (e) => {
|
|
493
|
+
if (e.button === 1) {
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
el.addEventListener("auxclick", (e) => {
|
|
498
|
+
if (e.button === 1) {
|
|
499
|
+
e.preventDefault();
|
|
500
|
+
void close_(tab.id);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
478
503
|
return tab;
|
|
479
504
|
}
|
|
480
505
|
|
|
@@ -513,7 +538,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
513
538
|
}
|
|
514
539
|
});
|
|
515
540
|
ctx.announce(`Switched to ${next.display}`);
|
|
516
|
-
|
|
541
|
+
if (!coarse) {
|
|
542
|
+
focusInput(); // desktop only; on touch this would pop the keyboard (#7)
|
|
543
|
+
}
|
|
517
544
|
}
|
|
518
545
|
|
|
519
546
|
// armCatchup shows the "catching up" cue only if the resume delta has not
|
|
@@ -606,6 +633,28 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
606
633
|
await dropTab(id, true);
|
|
607
634
|
}
|
|
608
635
|
|
|
636
|
+
// closeAll closes every tab (killing each process) and leaves one fresh
|
|
637
|
+
// terminal. Destructive - each tab is a running agent - so it confirms.
|
|
638
|
+
async function closeAll(): Promise<void> {
|
|
639
|
+
if (tabList.length === 0) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (!window.confirm(`Close all ${String(tabList.length)} terminals?`)) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const victims = [...tabList];
|
|
646
|
+
tabList.length = 0;
|
|
647
|
+
activeId = null;
|
|
648
|
+
for (const t of victims) {
|
|
649
|
+
t.aria.remove();
|
|
650
|
+
t.el.remove();
|
|
651
|
+
ctx.dropSession(t.id);
|
|
652
|
+
await apiClose(t.id);
|
|
653
|
+
}
|
|
654
|
+
syncChrome();
|
|
655
|
+
await create(); // there is always at least one terminal open
|
|
656
|
+
}
|
|
657
|
+
|
|
609
658
|
// One-time "swipe to switch" hint on first multi-tab state, mobile only.
|
|
610
659
|
function maybeSwipeHint(): void {
|
|
611
660
|
if (hintShown || tabList.length < 2) {
|
|
@@ -695,6 +744,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
695
744
|
newBtn.addEventListener("click", () => {
|
|
696
745
|
void create();
|
|
697
746
|
});
|
|
747
|
+
closeAllBtn.addEventListener("click", () => {
|
|
748
|
+
void closeAll();
|
|
749
|
+
});
|
|
698
750
|
overviewBtn.addEventListener("click", () => {
|
|
699
751
|
openOverview();
|
|
700
752
|
});
|
|
@@ -819,6 +871,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
819
871
|
tabList.length = 0;
|
|
820
872
|
bar.remove();
|
|
821
873
|
newBtn.remove();
|
|
874
|
+
closeAllBtn.remove();
|
|
822
875
|
switcher.remove();
|
|
823
876
|
sheet.remove();
|
|
824
877
|
scrim.remove();
|