@cplieger/web-terminal-ui 3.3.1 → 3.4.1

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 CHANGED
@@ -92,16 +92,20 @@ individual features instead:
92
92
  - `presetSingle()` — single-pane desktop UI (context menu, clipboard,
93
93
  scroll-to-bottom, predictive echo, connection banner).
94
94
  - `presetTouch()` — `presetSingle()` plus the mobile key toolbar.
95
- - `presetTabbed()` — the generic tabbed UI: `presetTouch()` plus tabs and
96
- animations. Tabs are label-only (no activity dots); each tab's title follows
95
+ - `presetTabbed()` — the generic tabbed UI: `presetTouch()` plus tabs, the
96
+ activity monitor, and animations. Each tab's title is OSC-first — it follows
97
97
  the process window title (OSC 0/2) when the program sets one and keeps it
98
- updated, otherwise the last command submitted. Requires a server that speaks
99
- the session API (`/api/sessions` and `/ws?session=`), such as
100
- `web-terminal-server`.
101
- - `presetAgentTabbed()` — `presetTabbed()` plus the activity monitor, so each
102
- tab carries a live status dot (idle / working / done / needs-input) from the
103
- server's status SSE. Same title behavior as `presetTabbed`. For an agent shell
104
- such as `vibecli`.
98
+ updated, otherwise the last command submitted. The per-tab activity dot
99
+ reveals itself only when a session reports OSC 9;4 progress, so a plain shell
100
+ keeps clean, label-only tabs. Requires a server that speaks the session API
101
+ (`/api/sessions` and `/ws?session=`), such as `web-terminal-server`.
102
+ - `presetAgentTabbed()` — the same feature set as `presetTabbed()` (activity
103
+ monitor included), but with `preferInputTitle`: each tab's label follows the
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 `vibecli`
106
+ whose program emits a non-empty but useless OSC title. Its status dots
107
+ (idle / working / done / needs-input) come from the same activity monitor,
108
+ driven by the server's status SSE and its OSC-9 classifier.
105
109
 
106
110
  ### Options
107
111
 
package/css/00-tokens.css CHANGED
@@ -12,12 +12,23 @@
12
12
  overriding them via createTerminal's `theme` option (which sets them on the
13
13
  terminal root) or its own CSS. vibecli sets a purple palette. Everything
14
14
  above is the fixed design palette; everything here is the public theme API. */
15
- --accent: #a78bfa; /* accent action colour (mobile "+", toggled key button) */
16
- --tab-hover-bg: var(--text-active); /* hovered inactive tab / pressed controls */
17
- --tab-active-bg: color-mix(in oklch, var(--text) 22%, var(--surface)); /* active tab fill */
15
+ --accent: oklch(72% 0.15 250deg); /* light blue; themed per consumer */
16
+ --tab-bg: oklch(30% 0 0deg / 90%); /* inactive-tab chip (translucent) */
17
+ --tab-hover-bg: var(--text-active); /* pressed controls / mobile switcher hovers */
18
+ --tab-active-bg: color-mix(
19
+ in oklch,
20
+ var(--accent) 30%,
21
+ var(--surface)
22
+ ); /* active tab fill: accent-derived (a blue by default; themed per consumer), so the active tab reads blue with the lighter accent-edge border below */
18
23
  --tab-active-fg: var(--text); /* active tab text */
24
+ --tab-active-border: color-mix(in oklch, var(--accent) 90%, transparent); /* accent edge */
19
25
  --radius: 0.75rem;
20
26
  --radius-inner: 0.5rem;
27
+ /* Small square icon buttons (e.g. the 24px tab-close x): at that size the
28
+ 0.5rem inner radius is a third of the side and reads as far rounder than
29
+ the >=42px controls that also use --radius-inner. This third rung of the
30
+ 0.75/0.5/0.25rem ladder keeps a small hover box's corners in proportion. */
31
+ --radius-sm: 0.25rem;
21
32
  --touch-target: 2.75rem; /* 44px — Apple HIG minimum */
22
33
  --pill-outer: calc(
23
34
  var(--touch-target) + 2 * var(--sp-1) + 2px
package/css/02-app.css CHANGED
@@ -167,86 +167,6 @@
167
167
  display: block;
168
168
  }
169
169
 
170
- /* Connection-state banner: bottom-center on touch (lifted above the
171
- virtual keyboard via --kb-inset), top-center on desktop. Subtle
172
- backdrop blur. Hidden until status.ts sets `.visible`. Distinct from
173
- the `.pill` family because it wants to span the full width centrally
174
- rather than dock to a corner. */
175
- .conn-banner {
176
- position: fixed;
177
- bottom: var(--sp-3);
178
- left: 50%;
179
- transform: translateX(-50%);
180
- max-width: calc(100% - 2 * var(--sp-3));
181
- padding: var(--sp-1) var(--sp-3);
182
- background: var(--surface);
183
- backdrop-filter: var(--blur);
184
- border: 1px solid var(--border);
185
- border-radius: var(--radius);
186
- color: var(--text);
187
- font-family: var(--font-ui);
188
- font-size: 0.8125rem;
189
- font-weight: 500;
190
- text-align: center;
191
- z-index: 20;
192
- display: none;
193
- pointer-events: auto;
194
- opacity: 0;
195
- transition:
196
- opacity var(--dur-standard) var(--ease-standard),
197
- display var(--dur-standard) allow-discrete;
198
- }
199
-
200
- .conn-banner.visible {
201
- display: block;
202
- opacity: 1;
203
-
204
- @starting-style {
205
- opacity: 0;
206
- }
207
- }
208
-
209
- /* Color coding by notification type */
210
- .conn-banner[data-state="reconnecting"] {
211
- border-color: oklch(70% 0.15 60deg / 40%);
212
- }
213
-
214
- .conn-banner[data-state="offline"] {
215
- color: #f55;
216
- border-color: oklch(60% 0.2 25deg / 50%);
217
- }
218
-
219
- .conn-banner[data-state="restarted"] {
220
- color: #fa5;
221
- border-color: oklch(70% 0.15 60deg / 40%);
222
- }
223
-
224
- .conn-banner[data-state="toast"] {
225
- border-color: oklch(70% 0.1 160deg / 30%);
226
- }
227
-
228
- /* Virtual keyboard awareness: when the keyboard is open the
229
- visualViewport height shrinks. viewport.ts sets --kb-inset on
230
- termWrap; we use it here to lift the banner above the keyboard.
231
- Falls back to 0px when the variable isn't set (desktop). */
232
- @media (pointer: coarse) {
233
- .conn-banner {
234
- bottom: calc(var(--sp-3) + var(--kb-inset, 0px));
235
- }
236
- }
237
-
238
- /* Desktop (pointer: fine): move the banner to top-center so it no longer
239
- shares the bottom-center anchor with the scroll-to-bottom FAB
240
- (.kb-scroll-bottom). Sharing it let the banner (pointer-events:auto,
241
- z-index:20) render over the FAB and intercept its click. Touch keeps
242
- the bottom placement and the --kb-inset keyboard lift above. */
243
- @media (pointer: fine) {
244
- .conn-banner {
245
- top: var(--sp-3);
246
- bottom: auto;
247
- }
248
- }
249
-
250
170
  /* Clickable URLs. OSC 8 hyperlinks (base .term-link) underline on HOVER only:
251
171
  an application can attach one OSC 8 link to a whole padded/bordered region —
252
172
  e.g. a markdown link whose URL wraps inside a table cell, where the terminal
@@ -614,62 +534,3 @@
614
534
  transition: none;
615
535
  }
616
536
  }
617
-
618
- /* ===== Context menu ===== */
619
- .ctx-menu {
620
- position: fixed;
621
- display: none;
622
- flex-direction: column;
623
- background: var(--surface);
624
- backdrop-filter: var(--blur);
625
- border: 1px solid var(--border);
626
- border-radius: var(--radius);
627
- padding: var(--sp-1);
628
- z-index: 30;
629
- opacity: 0;
630
- scale: 0.95;
631
- transition:
632
- opacity var(--dur-exit) var(--ease-standard),
633
- scale var(--dur-exit) var(--ease-standard),
634
- display var(--dur-exit) allow-discrete;
635
- }
636
-
637
- .ctx-menu.visible {
638
- display: flex;
639
- opacity: 1;
640
- scale: 1;
641
-
642
- @starting-style {
643
- opacity: 0;
644
- scale: 0.95;
645
- }
646
- }
647
-
648
- .ctx-menu button {
649
- background: transparent;
650
- color: var(--text);
651
- border: none;
652
- border-radius: var(--radius-inner);
653
- font-family: var(--font-ui);
654
- font-size: 0.8125rem;
655
- font-weight: 500;
656
- padding: var(--sp-1) var(--sp-3);
657
- cursor: pointer;
658
- text-align: left;
659
- }
660
-
661
- .ctx-menu button:active {
662
- background: var(--text-active);
663
- }
664
-
665
- /* Touch (long-press) menu: bump the rows to the 44px --touch-target
666
- standard the rest of the touch UI uses (the key-toolbar grid is
667
- repeat(4, var(--touch-target))). Desktop right-click keeps the
668
- compact 24px menu. padding-block re-centres the label in the taller box. */
669
- @media (pointer: coarse) {
670
- .ctx-menu button {
671
- min-height: var(--touch-target);
672
- padding-block: var(--sp-2);
673
- touch-action: manipulation;
674
- }
675
- }
@@ -72,6 +72,7 @@
72
72
  font-weight: 500;
73
73
  text-align: center;
74
74
  z-index: 40;
75
+ pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
75
76
  display: none;
76
77
  opacity: 0;
77
78
  }
@@ -24,6 +24,7 @@
24
24
  font-weight: 500;
25
25
  text-align: center;
26
26
  z-index: 20;
27
+ pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
27
28
  display: none;
28
29
  }
29
30
 
@@ -59,8 +60,15 @@
59
60
  background: var(--surface);
60
61
  backdrop-filter: var(--blur);
61
62
  border: 1px solid var(--border);
62
- border-radius: var(--radius);
63
- color: color-mix(in oklch, var(--text) 70%, transparent);
63
+ /* Match the tab "+" buttons (and every other .wt-btn), which round to
64
+ --radius-inner. Both are 44px boxes, so matching the corner radius makes
65
+ the FAB read as the same-sized button as the new-tab "+" beside/above it
66
+ rather than a rounder, different-looking control. */
67
+ border-radius: var(--radius-inner);
68
+ /* Full --text icon, matching the switcher's keyboard/switch/"+" icons (which
69
+ use .wt-btn's default color). Was a muted 70% mix that read as grey next to
70
+ those white bar icons. */
71
+ color: var(--text);
64
72
  z-index: 10;
65
73
  display: none;
66
74
  }
@@ -70,20 +78,25 @@
70
78
  display: inline-flex;
71
79
  }
72
80
 
73
- /* Hover brightens the icon + border; press fills the background (the .wt-btn
74
- :active fill would otherwise be masked by this button's own surface fill). */
81
+ /* Hover brightens the border; press fills the background (the .wt-btn :active
82
+ fill would otherwise be masked by this button's own surface fill). The icon
83
+ is already full --text, so hover no longer needs to change it. */
75
84
  .wt-scroll-bottom:hover {
76
85
  border-color: color-mix(in oklch, var(--text) 35%, transparent);
77
- color: var(--text);
78
86
  }
79
87
 
80
88
  .wt-scroll-bottom:active {
81
89
  background: var(--text-active);
82
90
  }
83
91
 
84
- /* Mobile + tabbed: lift above the switcher bar and the keyboard (WS3). */
92
+ /* Mobile + tabbed: lift above the switcher bar and the keyboard (WS3), and pull
93
+ the right edge in to --sp-2 so the button sits directly above the switcher's
94
+ "+" (whose right edge is inset by the bar's --sp-2 padding). Both are 44px
95
+ border-box boxes, so matching the right inset lines them up in one vertical
96
+ column (they were 4px out: the FAB's base --sp-3 vs the "+"'s --sp-2). */
85
97
  @media (pointer: coarse) {
86
98
  .wt-tabbed .wt-scroll-bottom {
99
+ right: var(--sp-2);
87
100
  bottom: calc(
88
101
  var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
89
102
  );
@@ -158,6 +171,7 @@
158
171
  font-family: var(--font-ui);
159
172
  font-size: 0.75rem;
160
173
  z-index: 24;
174
+ pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
161
175
  display: none;
162
176
  }
163
177
 
package/css/05-tabs.css CHANGED
@@ -32,13 +32,16 @@
32
32
  display: inline-flex;
33
33
  align-items: center;
34
34
  gap: var(--sp-1);
35
- /* Browser-style tabs: 140px when there is room, shrinking toward a 90px floor
36
- as the strip fills; past the floor the bar scrolls horizontally
37
- (.wt-tab-bar has overflow-x: auto). box-sizing keeps the basis a true total. */
35
+ /* Browser-style tabs: up to 300px per tab when there is room, shrinking toward
36
+ a 100px floor as the strip fills; past the floor the bar scrolls horizontally
37
+ (.wt-tab-bar has overflow-x: auto). flex-grow is 0, so the 300px basis is a
38
+ hard cap — every tab is the same size and never stretches past it, whatever
39
+ its title; the label truncates instead. box-sizing keeps the basis a true
40
+ total. */
38
41
  box-sizing: border-box;
39
- flex: 0 1 140px;
40
- min-width: 90px;
41
- padding: var(--sp-2) var(--sp-2);
42
+ flex: 0 1 300px;
43
+ min-width: 100px;
44
+ padding: var(--sp-2);
42
45
  /* A transparent border is reserved on every tab so toggling the inactive
43
46
  outline (below) never shifts the strip's layout. */
44
47
  border: 1px solid transparent;
@@ -51,21 +54,28 @@
51
54
  user-select: none;
52
55
  }
53
56
 
54
- /* Inactive tabs read as outlined chips (so adjacent tabs stay visually
55
- separated) and take a subtle fill on hover. */
57
+ /* Inactive tabs read as outlined chips with a subtle dark fill (so adjacent tabs
58
+ stay separated from the bar and from each other) and lift on hover. */
56
59
  .wt-tab:not(.wt-tab-active) {
57
60
  border-color: var(--border);
61
+ background: var(--tab-bg);
58
62
  }
59
63
 
60
64
  .wt-tab:not(.wt-tab-active):hover {
61
- background: var(--tab-hover-bg);
65
+ /* A solid lift over the chip. --tab-hover-bg is a translucent wash meant for a
66
+ transparent-rest control; replacing the --tab-bg chip with it would read as
67
+ "more transparent", not "lighter", so bake the same 15% --text lift into the
68
+ chip colour instead. */
69
+ background: color-mix(in oklch, var(--tab-bg), var(--text) 15%);
62
70
  color: var(--tab-active-fg);
63
71
  }
64
72
 
65
- /* The active tab reads clearly above the inactive chips: a stronger fill.
66
- Themeable (vibecli tints these purple). */
73
+ /* The active tab reads clearly above the inactive chips: a stronger fill plus an
74
+ accent-coloured edge (inactive chips carry a faint white --border; the active
75
+ one gets the accent equivalent). Themeable via --accent (vibecli -> purple). */
67
76
  .wt-tab-active {
68
77
  background: var(--tab-active-bg);
78
+ border-color: var(--tab-active-border);
69
79
  color: var(--tab-active-fg);
70
80
  }
71
81
 
@@ -75,6 +85,7 @@
75
85
  flex: 1 1 auto;
76
86
  min-width: 0;
77
87
  overflow: hidden;
88
+ white-space: nowrap;
78
89
  text-overflow: ellipsis;
79
90
  }
80
91
 
@@ -91,12 +102,16 @@
91
102
  align-items: center;
92
103
  justify-content: center;
93
104
  flex: none;
94
- width: 1.375rem;
95
- height: 1.375rem;
105
+ width: 1.5rem;
106
+ height: 1.5rem;
96
107
  padding: 0;
97
108
  background: transparent;
98
109
  border: none;
99
- border-radius: var(--radius-inner);
110
+ /* --radius-sm (not --radius-inner): this is a small 24px box, where the
111
+ inner radius reads as a third of the side and looks far rounder than the
112
+ larger tab / "+" / menu controls. --radius-sm keeps the hover box in
113
+ proportion with them. */
114
+ border-radius: var(--radius-sm);
100
115
  color: color-mix(in oklch, var(--text) 60%, transparent);
101
116
  cursor: pointer;
102
117
  }
@@ -116,6 +131,16 @@
116
131
  visibility: visible;
117
132
  }
118
133
 
134
+ /* Coarse pointer (an iPad using the strip): no hover fires, so an inactive tab's close
135
+ would be unreachable without first activating the tab. Reveal every tab's close there,
136
+ matching the always-visible mobile switcher closes. Kept in lockstep with the strip-shown
137
+ gate (pointer: coarse, width >= 601px). */
138
+ @media (pointer: coarse) and (width >= 601px) {
139
+ .wt-tab:not(.wt-tab-active) .wt-tab-close {
140
+ visibility: visible;
141
+ }
142
+ }
143
+
119
144
  /* The add-tab (+) button: its hover box matches an active tab's fill and
120
145
  stretches to the strip height; the 2px strip gap (not a custom margin) spaces
121
146
  it from the last tab. Its glyph is an inline SVG. */
@@ -126,21 +151,25 @@
126
151
  flex: none;
127
152
  /* Square box that matches the tab height (a stretched flex item ignores
128
153
  aspect-ratio against its SVG's intrinsic width, so size it explicitly). The
129
- tab height is the close-button (1.375rem) + vertical padding (2×sp-2) +
154
+ tab height is the close-button (1.5rem) + vertical padding (2×sp-2) +
130
155
  border. */
131
- width: calc(1.375rem + 2 * var(--sp-2) + 2px);
132
- height: calc(1.375rem + 2 * var(--sp-2) + 2px);
156
+ width: calc(1.5rem + 2 * var(--sp-2) + 2px);
157
+ height: calc(1.5rem + 2 * var(--sp-2) + 2px);
133
158
  padding: 0;
134
159
  background: transparent;
135
160
  border: none;
136
161
  border-radius: var(--radius-inner);
137
- color: color-mix(in oklch, var(--text) 60%, transparent);
162
+ /* The "+" is the primary action, so carry it in the accent colour (a light
163
+ blue by default; consumer-themed) — matching the mobile switcher's "+"
164
+ (.wt-switcher-new) rather than the muted grey the close / tab controls use. */
165
+ color: var(--accent);
138
166
  cursor: pointer;
139
167
  }
140
168
 
141
169
  .wt-tab-new:hover {
170
+ /* Keep the accent glyph on hover (no colour override) so the "+" stays the
171
+ accent colour like the mobile "+"; hover only adds the fill. */
142
172
  background: var(--tab-active-bg);
143
- color: var(--tab-active-fg);
144
173
  }
145
174
 
146
175
  /* The +/x icons are inline SVG so they center exactly in their flex-centered
@@ -165,6 +194,54 @@
165
194
  height: 16px;
166
195
  }
167
196
 
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), where there is no
199
+ mobile switcher to carry one. Built + wired by the SAME factory as the mobile
200
+ switcher's keyboard button (see makeKbButton in tabs.ts). Uses the shared
201
+ .wt-btn primitive for its 44px box + centered icon. Hidden on a fine-pointer
202
+ desktop (a real keyboard is present) and until a keyboardToggle feature is
203
+ wired ([hidden]); shown only on a coarse pointer at strip widths (the media
204
+ block below, kept in lockstep with SWITCHER_MEDIA in src/features/tabs.ts).
205
+ Fixed at the strip's bottom-right — above the fixed bottom strip, with a
206
+ matching-surface fill — so it caps the strip's right edge and never scrolls
207
+ with the tabs. */
208
+ .wt-tab-kb {
209
+ display: none;
210
+ position: fixed;
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);
216
+ }
217
+
218
+ /* Grid open: filled so the toggled state reads at a glance (mirrors the
219
+ switcher keyboard button). */
220
+ .wt-tab-kb.wt-active {
221
+ background: var(--tab-hover-bg);
222
+ color: var(--tab-active-fg);
223
+ }
224
+
225
+ /* Sticky-Ctrl armed: invert (matches the switcher keyboard button + the armed
226
+ Ctrl key) so a pending Ctrl is visible with the grid closed. */
227
+ .wt-tab-kb.wt-armed {
228
+ background: var(--text);
229
+ color: var(--bg);
230
+ }
231
+
232
+ /* Show + reserve room only on a WIDE touchscreen (where the desktop strip is
233
+ used AND a keyboardToggle is wired, hence :not([hidden])): the strip gets
234
+ matching right padding so the tabs + "+" never scroll under the fixed button. */
235
+ @media (pointer: coarse) and (width >= 601px) {
236
+ .wt-tab-kb:not([hidden]) {
237
+ display: inline-flex;
238
+ }
239
+
240
+ .wt-tab-bar {
241
+ padding-right: calc(var(--touch-target) + 2 * var(--sp-2));
242
+ }
243
+ }
244
+
168
245
  /* The tab being dragged (reorder) dims while it moves. */
169
246
  .wt-tab-dragging {
170
247
  opacity: 0.5;
@@ -217,10 +294,12 @@
217
294
  overlap the last rows (a mirror of the old top offset). The tabs feature
218
295
  marks the surface (.wt-with-tabbar) and keeps the measured strip height in
219
296
  --wt-tabbar-h; the surface is position:fixed inset:0, so a bottom offset
220
- clears it. Desktop only: on a coarse pointer the strip is hidden and the
221
- mobile switcher's inset is applied through viewport.ts (--wt-reserve-bottom)
222
- instead. */
223
- @media (pointer: fine) {
297
+ clears it. Applies whenever the desktop strip is shown — a fine pointer OR a
298
+ wide viewport (an iPad) — the complement of the narrow-coarse switcher gate in
299
+ css/06-mobile.css (kept in lockstep with SWITCHER_MEDIA in tabs.ts). On the
300
+ narrow-coarse phone the strip is hidden and the mobile switcher's inset is
301
+ applied through viewport.ts (--wt-reserve-bottom) instead. */
302
+ @media (pointer: fine), (width >= 601px) {
224
303
  .term.wt-with-tabbar {
225
304
  bottom: var(--wt-tabbar-h, 44px);
226
305
  }
@@ -308,5 +387,9 @@
308
387
  @media (prefers-reduced-motion: reduce) {
309
388
  .wt-status-dot[data-status="working"] {
310
389
  animation: none;
390
+ /* The pulse is working's ONLY non-hue cue; with motion removed,
391
+ add a static shape cue (an inner hole -> a ring) so working differs
392
+ from done (a solid green disc) by shape, not hue alone (WCAG 1.4.1). */
393
+ box-shadow: inset 0 0 0 2px var(--bg);
311
394
  }
312
395
  }