@cplieger/web-terminal-ui 3.5.0 → 4.1.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 (41) hide show
  1. package/README.md +60 -27
  2. package/css/00-tokens.css +18 -2
  3. package/css/01-scope.css +76 -0
  4. package/css/{02-app.css → 02-terminal.css} +113 -239
  5. package/css/10-primitives.css +98 -0
  6. package/css/20-connection-banner.css +42 -0
  7. package/css/21-scroll-to-bottom.css +65 -0
  8. package/css/22-context-menu.css +45 -0
  9. package/css/23-toolbar.css +181 -0
  10. package/css/{05-tabs.css → 30-tabs.css} +127 -66
  11. package/css/{06-mobile.css → 31-switcher.css} +85 -80
  12. package/css/{07-animations.css → 40-animations.css} +15 -15
  13. package/css/MANIFEST +19 -11
  14. package/css/MANIFEST.single +12 -0
  15. package/css/MANIFEST.tabbed +16 -0
  16. package/css/MANIFEST.touch +12 -0
  17. package/css/{01-reset.css → page.css} +18 -19
  18. package/package.json +19 -6
  19. package/scaffold/index.html +5 -0
  20. package/src/features/activity-monitor.ts +32 -2
  21. package/src/features/connection-banner.ts +1 -0
  22. package/src/features/context-menu.ts +6 -33
  23. package/src/features/menu-position.ts +50 -0
  24. package/src/features/mobile-toolbar.ts +5 -4
  25. package/src/features/{tabs.ts → tabs/index.ts} +310 -721
  26. package/src/features/tabs/model.ts +342 -0
  27. package/src/features/tabs/strip.ts +101 -0
  28. package/src/features/tabs/switcher.ts +129 -0
  29. package/src/index.ts +1 -0
  30. package/src/kernel/a11y.ts +30 -5
  31. package/src/kernel/conn-state.ts +16 -6
  32. package/src/kernel/kernel.ts +170 -59
  33. package/src/kernel/types.ts +68 -15
  34. package/src/presets/agent-tabbed.ts +15 -0
  35. package/src/presets/single.ts +27 -0
  36. package/src/presets/tabbed.ts +42 -0
  37. package/src/presets/touch.ts +13 -0
  38. package/src/presets.ts +12 -76
  39. package/src/viewport.ts +28 -15
  40. package/css/03-primitives.css +0 -83
  41. package/css/04-feedback.css +0 -180
@@ -1,8 +1,27 @@
1
1
  /* ===== Terminal ===== */
2
- .term {
3
- position: fixed;
2
+ :where(.wt-root) .term {
3
+ /* Absolute against .wt-root (the positioning boundary in BOTH layout modes):
4
+ in wt-viewport the root is a fixed full-viewport box, so this is
5
+ geometrically identical to the pre-v4 `position: fixed`; in wt-container it
6
+ fills the embedder's panel. viewport.ts still pins top/bottom inline to
7
+ track the visual viewport around the soft keyboard. */
8
+ position: absolute;
4
9
  inset: 0;
5
10
  overflow: hidden auto;
11
+ /* Scroll stays in the terminal: reaching the top of scrollback (or the
12
+ bottom) must not chain the gesture into an embedding page (wt-container
13
+ mode), and a touch pull inside the terminal must not trigger the host's
14
+ pull-to-refresh. Full-page hosts already get this via page.css's
15
+ html/body overscroll-behavior: none; this covers the embedded case. */
16
+ overscroll-behavior: contain;
17
+ /* Reserve the scrollbar gutter from the start on classic-scrollbar
18
+ platforms. Without it the scrollbar pops in when scrollback first
19
+ exceeds a screen, narrowing the content box -> ResizeObserver -> column
20
+ recount -> PTY resize -> the running program reflows: a one-time layout
21
+ jump every fresh session. Real terminals reserve this space permanently;
22
+ with the boundary's thin dark scrollbars (01-scope.css) the idle gutter
23
+ is a subtle strip. No-op on overlay-scrollbar platforms. */
24
+ scrollbar-gutter: stable;
6
25
  /* The scroll controller (vterm scroll.ts) is the single owner of
7
26
  scrollTop: it pins to the bottom only while the user is following,
8
27
  and does nothing while they are scrolled up reading. New output
@@ -27,7 +46,7 @@
27
46
  color: var(--text);
28
47
  }
29
48
 
30
- .term-row {
49
+ :where(.wt-root) .term-row {
31
50
  white-space: pre;
32
51
  min-height: 17px;
33
52
  font-variant-ligatures: none;
@@ -48,7 +67,7 @@
48
67
  cell tracks the column on every flush. */
49
68
  }
50
69
 
51
- .term-row:empty::after {
70
+ :where(.wt-root) .term-row:empty::after {
52
71
  content: "\00a0";
53
72
  }
54
73
 
@@ -56,7 +75,7 @@
56
75
  as the first row when history older than the buffer holds was evicted —
57
76
  client-side cap or a server-reported resume gap). A quiet, centered,
58
77
  non-selectable divider so the user knows scrollback doesn't go further up. */
59
- .term-trim-marker {
78
+ :where(.wt-root) .term-trim-marker {
60
79
  color: color-mix(in oklch, var(--text) 60%, transparent);
61
80
  font-size: 0.6875rem;
62
81
  text-align: center;
@@ -65,7 +84,7 @@
65
84
  font-family: var(--font-ui);
66
85
  }
67
86
 
68
- .term-input {
87
+ :where(.wt-root) .term-input {
69
88
  /* The hidden keyboard target. position:absolute (NOT fixed) so it lives in
70
89
  the terminal's scroll/content coordinate space alongside .composition-view
71
90
  and .pred-cursor: composition.positionCompositionView() moves it to the
@@ -94,7 +113,7 @@
94
113
  font-size: 16px;
95
114
  }
96
115
 
97
- .term-output {
116
+ :where(.wt-root) .term-output {
98
117
  position: relative;
99
118
  outline: none;
100
119
 
@@ -128,7 +147,7 @@
128
147
  so the candidate popup tracks what the user is typing.
129
148
  Visibility is gated by the `.active` class so empty state doesn't
130
149
  render a phantom box on top of the cursor. */
131
- .composition-view {
150
+ :where(.wt-root) .composition-view {
132
151
  position: absolute;
133
152
  pointer-events: none;
134
153
  background: var(--surface);
@@ -143,7 +162,7 @@
143
162
  display: none;
144
163
  }
145
164
 
146
- .composition-view.active {
165
+ :where(.wt-root) .composition-view.active {
147
166
  display: block;
148
167
  }
149
168
 
@@ -153,7 +172,7 @@
153
172
  cursor (predict.ts deactivates on bail and the renderer hides on
154
173
  coincidence). Dashed outline + reduced opacity reads as "tentative"
155
174
  — distinct from the solid block of the real cursor. */
156
- .pred-cursor {
175
+ :where(.wt-root) .pred-cursor {
157
176
  position: absolute;
158
177
  pointer-events: none;
159
178
  border: 1px dashed var(--text);
@@ -163,7 +182,7 @@
163
182
  box-sizing: border-box;
164
183
  }
165
184
 
166
- .pred-cursor.visible {
185
+ :where(.wt-root) .pred-cursor.visible {
167
186
  display: block;
168
187
  }
169
188
 
@@ -177,17 +196,17 @@
177
196
  Auto-detected bare URLs (.term-autolink) are tightly scoped to the URL text and
178
197
  never bleed, so they keep a subtle persistent underline. (The engine also only
179
198
  anchors runs with real link text, so the underline hugs the text either way.) */
180
- .term-link {
199
+ :where(.wt-root) .term-link {
181
200
  color: inherit;
182
201
  text-decoration: none;
183
202
  cursor: pointer;
184
203
  }
185
204
 
186
- .term-autolink {
205
+ :where(.wt-root) .term-autolink {
187
206
  text-decoration: underline dotted;
188
207
  }
189
208
 
190
- .term-link:hover {
209
+ :where(.wt-root) .term-link:hover {
191
210
  text-decoration: underline solid;
192
211
  }
193
212
 
@@ -197,9 +216,9 @@
197
216
  the viewport. JS-driven instead of CSS animation because the cursor
198
217
  span is recreated every row update — a CSS animation would restart on
199
218
  each recreation and never reach the "off" frame during streaming. */
200
- .term-cursor,
201
- .term-cursor-underline,
202
- .term-cursor-bar {
219
+ :where(.wt-root) .term-cursor,
220
+ :where(.wt-root) .term-cursor-underline,
221
+ :where(.wt-root) .term-cursor-bar {
203
222
  box-sizing: border-box;
204
223
  display: inline-block;
205
224
  height: 17px;
@@ -207,35 +226,70 @@
207
226
  vertical-align: top;
208
227
  }
209
228
 
210
- .term-cursor {
229
+ :where(.wt-root) .term-cursor {
211
230
  background: var(--text);
212
231
  color: var(--bg);
213
232
  }
214
233
 
215
234
  /* Underline cursor (DECSCUSR 3/4) */
216
- .term-cursor-underline {
235
+ :where(.wt-root) .term-cursor-underline {
217
236
  background: transparent;
218
237
  color: var(--text);
219
238
  border-bottom: 2px solid var(--text);
220
239
  }
221
240
 
222
241
  /* Bar cursor (DECSCUSR 5/6) */
223
- .term-cursor-bar {
242
+ :where(.wt-root) .term-cursor-bar {
224
243
  background: transparent;
225
244
  color: var(--text);
226
245
  border-left: 2px solid var(--text);
227
246
  }
228
247
 
229
- .cursor-blink-off .term-cursor {
248
+ :where(.wt-root) .cursor-blink-off .term-cursor {
230
249
  background: transparent;
231
250
  color: var(--text);
232
251
  }
233
252
 
234
- .term-blink {
235
- animation: term-blink-anim 1s step-end infinite;
253
+ /* Caret overlay (engine >= v3): the renderer paints the REAL cursor as one
254
+ absolutely-positioned element in the scroll container (a sibling of
255
+ .pred-cursor, below it in the stack), not as a restyled span inside the
256
+ cursor row — rows stay pure content, so a native text selection survives
257
+ cursor motion and typing. The engine sets left/top/width/height/line-height
258
+ inline each flush; the .term-cursor / -underline / -bar variant classes
259
+ above keep supplying the colors and borders, and the JS-driven
260
+ .cursor-blink-off toggle (now on the scroll container) keeps driving the
261
+ block blink. */
262
+ :where(.wt-root) .term-cursor-overlay {
263
+ position: absolute;
264
+ pointer-events: none;
265
+ white-space: pre;
266
+ overflow: hidden;
267
+ z-index: 3;
268
+ box-sizing: border-box;
269
+ }
270
+
271
+ :where(.wt-root) .term-cursor-overlay:not(.visible) {
272
+ display: none;
273
+ }
274
+
275
+ /* DECSCNM reverse video (private mode 5): the engine toggles
276
+ .term-reverse-video on the scroll container (render.updateReverseVideo);
277
+ this rule swaps the DEFAULT color pair for the terminal subtree via the
278
+ captured copies from 00-tokens.css. Everything painted with var(--text)/
279
+ var(--bg) — default-colored cells, the screen background, the block cursor's
280
+ inverted cell — follows automatically; explicitly colored cells keep their
281
+ colors, matching xterm's DECSCNM semantics (the default pair swaps, not a
282
+ per-pixel inversion). */
283
+ :where(.wt-root) .term.term-reverse-video {
284
+ --text: var(--bg-default);
285
+ --bg: var(--text-default);
236
286
  }
237
287
 
238
- @keyframes term-blink-anim {
288
+ :where(.wt-root) .term-blink {
289
+ animation: wt-blink-anim 1s step-end infinite;
290
+ }
291
+
292
+ @keyframes wt-blink-anim {
239
293
  50% {
240
294
  opacity: 0;
241
295
  }
@@ -243,9 +297,9 @@
243
297
 
244
298
  /* When the terminal isn't focused, render the cursor as an outline
245
299
  rather than a filled block (xterm.js convention). */
246
- .term:not(.focus) .term-cursor,
247
- .term:not(.focus) .term-cursor-underline,
248
- .term:not(.focus) .term-cursor-bar {
300
+ :where(.wt-root) .term:not(.focus) .term-cursor,
301
+ :where(.wt-root) .term:not(.focus) .term-cursor-underline,
302
+ :where(.wt-root) .term:not(.focus) .term-cursor-bar {
249
303
  background: transparent;
250
304
  color: var(--text);
251
305
  border: none;
@@ -257,280 +311,100 @@
257
311
  }
258
312
 
259
313
  /* ===== Terminal color classes (basic 16 — themeable) ===== */
260
- .term-fg-0 {
314
+ :where(.wt-root) .term-fg-0 {
261
315
  color: #000;
262
316
  }
263
- .term-fg-1 {
317
+ :where(.wt-root) .term-fg-1 {
264
318
  color: #a00;
265
319
  }
266
- .term-fg-2 {
320
+ :where(.wt-root) .term-fg-2 {
267
321
  color: #0a0;
268
322
  }
269
- .term-fg-3 {
323
+ :where(.wt-root) .term-fg-3 {
270
324
  color: #a50;
271
325
  }
272
- .term-fg-4 {
326
+ :where(.wt-root) .term-fg-4 {
273
327
  color: #00a;
274
328
  }
275
- .term-fg-5 {
329
+ :where(.wt-root) .term-fg-5 {
276
330
  color: #a0a;
277
331
  }
278
- .term-fg-6 {
332
+ :where(.wt-root) .term-fg-6 {
279
333
  color: #0aa;
280
334
  }
281
- .term-fg-7 {
335
+ :where(.wt-root) .term-fg-7 {
282
336
  color: #aaa;
283
337
  }
284
- .term-fg-8 {
338
+ :where(.wt-root) .term-fg-8 {
285
339
  color: #555;
286
340
  }
287
- .term-fg-9 {
341
+ :where(.wt-root) .term-fg-9 {
288
342
  color: #f55;
289
343
  }
290
- .term-fg-10 {
344
+ :where(.wt-root) .term-fg-10 {
291
345
  color: #5f5;
292
346
  }
293
- .term-fg-11 {
347
+ :where(.wt-root) .term-fg-11 {
294
348
  color: #ff5;
295
349
  }
296
- .term-fg-12 {
350
+ :where(.wt-root) .term-fg-12 {
297
351
  color: #55f;
298
352
  }
299
- .term-fg-13 {
353
+ :where(.wt-root) .term-fg-13 {
300
354
  color: #f5f;
301
355
  }
302
- .term-fg-14 {
356
+ :where(.wt-root) .term-fg-14 {
303
357
  color: #5ff;
304
358
  }
305
- .term-fg-15 {
359
+ :where(.wt-root) .term-fg-15 {
306
360
  color: #fff;
307
361
  }
308
362
 
309
- .term-bg-0 {
363
+ :where(.wt-root) .term-bg-0 {
310
364
  background: #000;
311
365
  }
312
- .term-bg-1 {
366
+ :where(.wt-root) .term-bg-1 {
313
367
  background: #a00;
314
368
  }
315
- .term-bg-2 {
369
+ :where(.wt-root) .term-bg-2 {
316
370
  background: #0a0;
317
371
  }
318
- .term-bg-3 {
372
+ :where(.wt-root) .term-bg-3 {
319
373
  background: #a50;
320
374
  }
321
- .term-bg-4 {
375
+ :where(.wt-root) .term-bg-4 {
322
376
  background: #00a;
323
377
  }
324
- .term-bg-5 {
378
+ :where(.wt-root) .term-bg-5 {
325
379
  background: #a0a;
326
380
  }
327
- .term-bg-6 {
381
+ :where(.wt-root) .term-bg-6 {
328
382
  background: #0aa;
329
383
  }
330
- .term-bg-7 {
384
+ :where(.wt-root) .term-bg-7 {
331
385
  background: #aaa;
332
386
  }
333
- .term-bg-8 {
387
+ :where(.wt-root) .term-bg-8 {
334
388
  background: #555;
335
389
  }
336
- .term-bg-9 {
390
+ :where(.wt-root) .term-bg-9 {
337
391
  background: #f55;
338
392
  }
339
- .term-bg-10 {
393
+ :where(.wt-root) .term-bg-10 {
340
394
  background: #5f5;
341
395
  }
342
- .term-bg-11 {
396
+ :where(.wt-root) .term-bg-11 {
343
397
  background: #ff5;
344
398
  }
345
- .term-bg-12 {
399
+ :where(.wt-root) .term-bg-12 {
346
400
  background: #55f;
347
401
  }
348
- .term-bg-13 {
402
+ :where(.wt-root) .term-bg-13 {
349
403
  background: #f5f;
350
404
  }
351
- .term-bg-14 {
405
+ :where(.wt-root) .term-bg-14 {
352
406
  background: #5ff;
353
407
  }
354
- .term-bg-15 {
408
+ :where(.wt-root) .term-bg-15 {
355
409
  background: #fff;
356
410
  }
357
-
358
- /* ===== Key toolbar (touch devices only) =====
359
- Uses interpolate-size: allow-keywords (on :root) to animate
360
- width/height between a fixed collapsed size and auto (intrinsic).
361
- Toggle is first in DOM so it's at top-left of the grid — visible
362
- when the container clips to 1-cell size. Container is right-anchored
363
- so it grows leftward on open. */
364
- .key-toolbar {
365
- display: contents;
366
- }
367
-
368
- /* Touch-only toolbar children: the on-screen Esc/Tab/arrows/hamburger
369
- are useless on desktop where the user has a real keyboard. Hidden by
370
- default; the @media (pointer: coarse) block below restores them. */
371
- .kb-key,
372
- .kb-toggle {
373
- display: none;
374
- }
375
-
376
- @media (pointer: coarse) {
377
- .key-toolbar {
378
- position: fixed;
379
- top: calc(var(--safe-top) + var(--sp-2) + var(--vv-top, 0px));
380
- right: var(--sp-2);
381
- z-index: 10;
382
- display: grid;
383
- grid-template-columns: repeat(4, var(--touch-target)) var(--touch-target);
384
- grid-template-rows: var(--touch-target) var(--touch-target);
385
- place-content: start end;
386
- gap: var(--sp-1);
387
- padding: var(--sp-1);
388
- background: var(--surface);
389
- backdrop-filter: var(--blur);
390
- border: 1px solid var(--border);
391
- border-radius: var(--radius);
392
- overflow: hidden;
393
- width: auto;
394
- height: auto;
395
- transition:
396
- width var(--dur-enter) var(--ease-standard),
397
- height var(--dur-enter) var(--ease-standard);
398
- }
399
-
400
- /* Collapsed: fixed size = 1 button + padding + border. Content is
401
- right-aligned (justify-content:end) so the toggle (col 5) stays at
402
- the right edge, visible within the clipped area. --pill-outer
403
- includes the 2px border (box-sizing:border-box); omitting it left
404
- the content box 2px short, pinning the button top-right with
405
- unequal bottom/left gaps. */
406
- .key-toolbar.collapsed {
407
- width: var(--pill-outer);
408
- height: var(--pill-outer);
409
- }
410
-
411
- /* Grid placement: toggle at col 5 row 1 (top-right). */
412
- .kb-toggle {
413
- grid-area: 1 / 5;
414
- position: relative;
415
- }
416
- .kb-r1c1 {
417
- grid-area: 1 / 1;
418
- }
419
- .kb-r1c2 {
420
- grid-area: 1 / 2;
421
- }
422
- .kb-r1c3 {
423
- grid-area: 1 / 3;
424
- }
425
- .kb-r1c4 {
426
- grid-area: 1 / 4;
427
- }
428
- .kb-r2c1 {
429
- grid-area: 2 / 1;
430
- }
431
- .kb-r2c2 {
432
- grid-area: 2 / 2;
433
- }
434
- .kb-r2c3 {
435
- grid-area: 2 / 3;
436
- }
437
- .kb-r2c4 {
438
- grid-area: 2 / 4;
439
- }
440
-
441
- /* Keys fade in/out during open/close. The container's width/height
442
- transition handles the background slide; opacity handles the
443
- individual button appearance. Stagger the fade slightly after
444
- the container starts growing (delay on open) and before it
445
- finishes shrinking (no delay on close). */
446
- .kb-key {
447
- opacity: 1;
448
- transition: opacity var(--dur-standard) var(--ease-enter) 0.05s;
449
- }
450
-
451
- .key-toolbar.collapsed .kb-key {
452
- opacity: 0;
453
- transition: opacity var(--dur-exit) var(--ease-exit);
454
- }
455
-
456
- /* Shared button */
457
- .key-toolbar button {
458
- width: var(--touch-target);
459
- height: var(--touch-target);
460
- background: transparent;
461
- color: var(--text);
462
- border: none;
463
- border-radius: var(--radius-inner);
464
- font-size: 0.9375rem;
465
- font-family: var(--font-ui);
466
- font-weight: 500;
467
- cursor: pointer;
468
- touch-action: manipulation;
469
- user-select: none;
470
- display: flex;
471
- align-items: center;
472
- justify-content: center;
473
- }
474
-
475
- .key-toolbar button:active {
476
- background: var(--text-active);
477
- }
478
-
479
- /* Sticky Ctrl, armed: inverted fill so the toggle state reads clearly
480
- in the monochrome palette (stays filled while pressed). */
481
- .kb-key.armed,
482
- .kb-key.armed:active {
483
- background: var(--text);
484
- color: var(--bg);
485
- }
486
-
487
- .key-toolbar button svg {
488
- width: var(--icon);
489
- height: var(--icon);
490
- stroke: currentcolor;
491
- fill: none;
492
- stroke-width: 2;
493
- stroke-linecap: round;
494
- stroke-linejoin: round;
495
- }
496
-
497
- /* Toggle icon crossfade. Both icons are stacked absolute so they
498
- can independently fade + rotate during the open<->close
499
- transition. inset:0 + margin:auto centers them within the
500
- button (works because the SVGs carry explicit width/height); a
501
- bare `position: absolute` would pin them at the top-left, giving
502
- the toggle visibly unequal left/right padding when collapsed. */
503
- .kb-toggle .icon-hamburger,
504
- .kb-toggle .icon-close {
505
- position: absolute;
506
- inset: 0;
507
- margin: auto;
508
- transition:
509
- opacity var(--dur-standard) linear,
510
- transform var(--dur-standard) linear;
511
- }
512
-
513
- .kb-toggle .icon-close {
514
- opacity: 1;
515
- transform: rotate(0deg);
516
- }
517
- .kb-toggle .icon-hamburger {
518
- opacity: 0;
519
- transform: rotate(-90deg);
520
- }
521
- .key-toolbar.collapsed .kb-toggle .icon-close {
522
- opacity: 0;
523
- transform: rotate(90deg);
524
- }
525
- .key-toolbar.collapsed .kb-toggle .icon-hamburger {
526
- opacity: 1;
527
- transform: rotate(0deg);
528
- }
529
-
530
- /* No-transition on first render */
531
- .key-toolbar.no-transition,
532
- .key-toolbar.no-transition .kb-toggle .icon-hamburger,
533
- .key-toolbar.no-transition .kb-toggle .icon-close {
534
- transition: none;
535
- }
536
- }
@@ -0,0 +1,98 @@
1
+ /* ===== web-terminal-ui design system: primitives =====
2
+ The kernel builds named layout regions (wt-region-*) as siblings of the
3
+ terminal surface (.term, styled in 02-terminal.css); features mount chrome
4
+ into region slots. The regions are transparent groupings (display: contents)
5
+ so each feature's element floats over the root-filling terminal, and an
6
+ absent feature contributes nothing.
7
+
8
+ The stylesheet is layered by concern and concatenated in manifest order:
9
+ tokens (00) -> scope + layout modes (01) -> terminal core (02) -> these
10
+ shared primitives (10) -> per-feature chrome (20-23, 30-31, one file per
11
+ feature so a per-preset manifest ships only what its composition mounts) ->
12
+ animations (40, where every .wt-animate-gated transition/keyframe lives, so
13
+ motion is opt-in from one place). Cascade order is the manifest file order.
14
+
15
+ Primitives are the building blocks every feature reuses: the transparent
16
+ layout regions, the 44px touch button, and the toast surface. */
17
+
18
+ :where(.wt-root) .wt-region,
19
+ :where(.wt-root) .wt-slot {
20
+ display: contents;
21
+ }
22
+
23
+ /* ===== Shared button primitive (44px touch target) ===== */
24
+ :where(.wt-root) .wt-btn {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ min-width: var(--touch-target);
29
+ min-height: var(--touch-target);
30
+ background: transparent;
31
+ color: var(--text);
32
+ border: none;
33
+ border-radius: var(--radius-inner);
34
+ font: inherit;
35
+ cursor: pointer;
36
+ touch-action: manipulation;
37
+ user-select: none;
38
+ }
39
+
40
+ :where(.wt-root) .wt-btn:active {
41
+ background: var(--text-active);
42
+ }
43
+
44
+ /* Keyboard-focus ring for every chrome control (all interactive chrome is a
45
+ real <button>: tabs, +, keyboard, switcher rows, menu items, the FAB).
46
+ :focus-visible only — a tap/click never shows the ring (the UA heuristic),
47
+ so the touch-first surfaces stay clean while Tab-key navigation gets the
48
+ family's accent ring (the vibekit/subflux convention: 2px accent, 2px out).
49
+ Follows each control's border-radius; themeable via --accent like the "+"
50
+ glyphs. The terminal's own hidden textarea is not a button and keeps its
51
+ outline: none (02-terminal.css). */
52
+ :where(.wt-root) button:focus-visible {
53
+ outline: 2px solid var(--accent);
54
+ outline-offset: 2px;
55
+ }
56
+
57
+ :where(.wt-root) .wt-btn svg {
58
+ width: var(--icon);
59
+ height: var(--icon);
60
+ stroke: currentcolor;
61
+ fill: none;
62
+ stroke-width: 2;
63
+ stroke-linecap: round;
64
+ stroke-linejoin: round;
65
+ }
66
+
67
+ /* ===== Toast (kernel shared primitive) =====
68
+ Top-centre on every form factor (both desktop and the tabbed mobile UI dock a
69
+ bar at the bottom, so a bottom toast would collide with it; top-centre is
70
+ clear and the same everywhere). The vv-top/safe-top offset keeps it below the
71
+ notch and follows iOS's visual-viewport shift. Absolute against .wt-root,
72
+ like all chrome (identical geometry in wt-viewport mode; see 01-scope.css). */
73
+ :where(.wt-root) .wt-toast {
74
+ position: absolute;
75
+ top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
76
+ left: 50%;
77
+ transform: translateX(-50%);
78
+ max-width: calc(100% - 2 * var(--sp-3));
79
+ padding: var(--sp-1) var(--sp-3);
80
+ background: var(--surface);
81
+ backdrop-filter: var(--blur);
82
+ border: 1px solid oklch(70% 0.1 160deg / 30%);
83
+ border-radius: var(--radius);
84
+ color: var(--text);
85
+ font-family: var(--font-ui);
86
+ font-size: 0.8125rem;
87
+ font-weight: 500;
88
+ text-align: center;
89
+ z-index: 40;
90
+ pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
91
+ display: none;
92
+ opacity: 0;
93
+ }
94
+
95
+ :where(.wt-root) .wt-toast.visible {
96
+ display: block;
97
+ opacity: 1;
98
+ }
@@ -0,0 +1,42 @@
1
+ /* ===== Connection banner (connectionBanner feature) =====
2
+ Top-centre on every form factor (same rationale as the toast: a bottom bar is
3
+ always docked now). Absolute against .wt-root, like all chrome (identical
4
+ geometry in wt-viewport mode; see 01-scope.css). */
5
+ :where(.wt-root) .wt-conn-banner {
6
+ position: absolute;
7
+ top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
8
+ left: 50%;
9
+ transform: translateX(-50%);
10
+ max-width: calc(100% - 2 * var(--sp-3));
11
+ padding: var(--sp-1) var(--sp-3);
12
+ background: var(--surface);
13
+ backdrop-filter: var(--blur);
14
+ border: 1px solid var(--border);
15
+ border-radius: var(--radius);
16
+ color: var(--text);
17
+ font-family: var(--font-ui);
18
+ font-size: 0.8125rem;
19
+ font-weight: 500;
20
+ text-align: center;
21
+ z-index: 20;
22
+ pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
23
+ display: none;
24
+ }
25
+
26
+ :where(.wt-root) .wt-conn-banner.visible {
27
+ display: block;
28
+ }
29
+
30
+ :where(.wt-root) .wt-conn-banner[data-state="reconnecting"] {
31
+ border-color: oklch(70% 0.15 60deg / 40%);
32
+ }
33
+
34
+ :where(.wt-root) .wt-conn-banner[data-state="offline"] {
35
+ color: #f55;
36
+ border-color: oklch(60% 0.2 25deg / 50%);
37
+ }
38
+
39
+ :where(.wt-root) .wt-conn-banner[data-state="restarted"] {
40
+ color: #fa5;
41
+ border-color: oklch(70% 0.15 60deg / 40%);
42
+ }