@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 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
- /* The tab close (×) and the add-tab (+) button share one look: a centered,
243
- muted square glyph button that shows a box on hover like an active tab. Same
244
- size and color so they read as a matched pair (and the + centers vertically
245
- in the strip via the shared flex box + the bar's align-items: center). */
246
- .wt-tab-close,
247
- .wt-tab-new {
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",
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",
@@ -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
- focusInput();
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();