@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
@@ -2,26 +2,29 @@
2
2
  The bottom bar (switcher) is the mobile tab surface, the externally-driven key
3
3
  grid docks above it, and the overview sheet lists every tab. The desktop strip
4
4
  is hidden only on a narrow coarse-pointer device (a phone) and these appear
5
- instead; all motion (bar number fade, sheet slide) lives in 07-animations.css. */
5
+ instead; all motion (bar number fade, sheet slide) lives in 40-animations.css. */
6
6
 
7
7
  /* The switcher is used only on a NARROW coarse-pointer device (a phone), where
8
8
  the top strip is hidden so tabs live in the thumb zone. A wide touchscreen (an
9
9
  iPad) and any fine-pointer device keep the multi-tab top strip — a single
10
- giant tab wastes a big screen. This media query MUST stay in lockstep with
11
- SWITCHER_MEDIA in src/features/tabs.ts and the complementary strip clearance
12
- in css/05-tabs.css. */
13
- .wt-switcher {
10
+ giant tab wastes a big screen. "Narrow" is the kernel's .wt-narrow root class
11
+ (ROOT width at the single kernel breakpoint — so an embedded terminal in a
12
+ narrow panel counts as narrow); this file pairs it with (pointer: coarse),
13
+ and css/30-tabs.css carries the complementary strip clearance. */
14
+ :where(.wt-root) .wt-switcher {
14
15
  display: none;
15
16
  }
16
17
 
17
- @media (pointer: coarse) and (width <= 600px) {
18
- .wt-tab-bar,
19
- .wt-tab-new {
18
+ @media (pointer: coarse) {
19
+ :where(.wt-root.wt-narrow) .wt-tab-bar,
20
+ :where(.wt-root.wt-narrow) .wt-tab-new {
20
21
  display: none;
21
22
  }
22
23
 
23
- .wt-switcher {
24
- position: fixed;
24
+ :where(.wt-root.wt-narrow) .wt-switcher {
25
+ /* Absolute against .wt-root, like all chrome (identical geometry in
26
+ wt-viewport mode; see 01-scope.css). */
27
+ position: absolute;
25
28
  left: 0;
26
29
  right: 0;
27
30
  /* Lift above the soft keyboard rather than hiding, so the switch surface
@@ -39,7 +42,7 @@
39
42
  /* The always-visible bar row: active tab + keyboard + "+". The padding gives the
40
43
  active-tab chip breathing room above/below within the bar, matching the
41
44
  desktop strip (both use --sp-2). */
42
- .wt-switcher-bar {
45
+ :where(.wt-root) .wt-switcher-bar {
43
46
  display: flex;
44
47
  align-items: stretch;
45
48
  gap: var(--sp-1);
@@ -48,8 +51,8 @@
48
51
 
49
52
  /* The other tabs, revealed below the bar row on expand (the bar slides up).
50
53
  Collapsed to zero height; expanded caps the height and scrolls. The max-height
51
- transition lives in 07-animations.css. */
52
- .wt-switcher-list {
54
+ transition lives in 40-animations.css. */
55
+ :where(.wt-root) .wt-switcher-list {
53
56
  list-style: none;
54
57
  margin: 0;
55
58
  /* Match the bar's horizontal padding (--sp-2) so a listed row lines up with
@@ -73,12 +76,16 @@
73
76
  box-sizing: border-box;
74
77
  }
75
78
 
76
- .wt-switcher-expanded .wt-switcher-list {
79
+ :where(.wt-root) .wt-switcher-expanded .wt-switcher-list {
77
80
  /* Cap against the DYNAMIC viewport so a shown mobile browser toolbar can't push the list
78
81
  (and the bar above it) off the top; the switcher's own bottom: kb-inset already lifts it
79
82
  clear of the soft keyboard. */
80
83
  max-height: var(--wt-list-h, 50dvh);
81
84
  overflow-y: auto;
85
+ /* Reaching the list's end must not hand the touch scroll to an embedding
86
+ page (wt-container mode) — the tray sits over the terminal, and a chained
87
+ scroll behind an open tray is disorienting. */
88
+ overscroll-behavior-y: contain;
82
89
  /* No top padding: the bar's --sp-2 bottom padding already sits between the
83
90
  active tab and the first row, so the seam equals the --sp-2 inter-row gap. */
84
91
  padding-top: 0;
@@ -90,7 +97,7 @@
90
97
  sibling over the surface's reserved right padding). The wrap is the flex item
91
98
  that grows in the bar (and fills the space when the keyboard button collapses
92
99
  on expand). */
93
- .wt-switcher-current-wrap {
100
+ :where(.wt-root) .wt-switcher-current-wrap {
94
101
  position: relative;
95
102
  flex: 1 1 auto;
96
103
  display: flex;
@@ -101,7 +108,7 @@
101
108
  overflow: hidden;
102
109
  }
103
110
 
104
- .wt-switcher-current {
111
+ :where(.wt-root) .wt-switcher-current {
105
112
  position: relative;
106
113
  flex: 1 1 auto;
107
114
  display: flex;
@@ -130,7 +137,7 @@
130
137
  padding, mirroring the listed rows' close. Hover fill only on real hover
131
138
  pointers so it never sticks white after a touch (a removed tab shifts the
132
139
  list under the finger otherwise). */
133
- .wt-switcher-current-close {
140
+ :where(.wt-root) .wt-switcher-current-close {
134
141
  position: absolute;
135
142
  /* Centre vertically via top/bottom (not a translateY transform), so transform
136
143
  is free for the swipe translate — the close moves with the dot + label. And
@@ -142,14 +149,14 @@
142
149
  }
143
150
 
144
151
  @media (hover: hover) {
145
- .wt-switcher-current-close:hover {
152
+ :where(.wt-root) .wt-switcher-current-close:hover {
146
153
  color: var(--text);
147
154
  }
148
155
  }
149
156
 
150
157
  /* Press feedback: brighten the active fill slightly (a lighter press over an
151
158
  already-filled row). */
152
- .wt-switcher-current:active {
159
+ :where(.wt-root) .wt-switcher-current:active {
153
160
  background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 12%);
154
161
  }
155
162
 
@@ -157,7 +164,7 @@
157
164
  parts out; a horizontal swipe translates it (clipped by the row's
158
165
  overflow: hidden) so the active tab area physically slides with the finger,
159
166
  and the incoming tab's label slides in from the swipe direction on commit. */
160
- .wt-switcher-current-inner {
167
+ :where(.wt-root) .wt-switcher-current-inner {
161
168
  display: flex;
162
169
  align-items: center;
163
170
  gap: var(--sp-2);
@@ -165,7 +172,7 @@
165
172
  min-width: 0;
166
173
  }
167
174
 
168
- .wt-switcher-label {
175
+ :where(.wt-root) .wt-switcher-label {
169
176
  flex: 1 1 auto;
170
177
  overflow: hidden;
171
178
  text-overflow: ellipsis;
@@ -178,7 +185,7 @@
178
185
  just the wt-btn min-width) makes it animatable: when the tab list expands the
179
186
  button collapses to zero width and fades out (the key grid is closed too, in
180
187
  JS), and the active row flex-grows to fill the freed space. */
181
- .wt-switcher-kb {
188
+ :where(.wt-root) .wt-switcher-kb {
182
189
  /* Rigid 44px box like the "+" and switch buttons (flex: none), so a long
183
190
  active-tab title that overflows the bar is absorbed entirely by the
184
191
  ellipsizing active-tab field, not shared with this button. It is otherwise
@@ -199,11 +206,11 @@
199
206
  overflow: hidden;
200
207
  }
201
208
 
202
- .wt-switcher-kb[hidden] {
209
+ :where(.wt-root) .wt-switcher-kb[hidden] {
203
210
  display: none;
204
211
  }
205
212
 
206
- .wt-switcher-kb.wt-active {
213
+ :where(.wt-root) .wt-switcher-kb.wt-active {
207
214
  background: var(--tab-hover-bg);
208
215
  color: var(--tab-active-fg);
209
216
  }
@@ -212,12 +219,12 @@
212
219
  Ctrl key uses) while a Ctrl press is pending, so the modifier stays visible
213
220
  even with the key grid — and its own Ctrl button — closed. After .wt-active so
214
221
  the armed fill wins when the grid is also open. */
215
- .wt-switcher-kb.wt-armed {
222
+ :where(.wt-root) .wt-switcher-kb.wt-armed {
216
223
  background: var(--text);
217
224
  color: var(--bg);
218
225
  }
219
226
 
220
- .wt-switcher-expanded .wt-switcher-kb {
227
+ :where(.wt-root) .wt-switcher-expanded .wt-switcher-kb {
221
228
  width: 0;
222
229
  /* Absorb the flex gap the collapsed button would otherwise leave behind. */
223
230
  margin-inline-start: calc(-1 * var(--sp-1));
@@ -230,9 +237,9 @@
230
237
  absolutely placed in the top-right corner. It only earns its place with ≥2
231
238
  tabs (a single tab has nothing to switch to), so it is COLLAPSED to zero width
232
239
  by default and revealed by .wt-switcher-multi (the tabs feature sets it on tab
233
- count); the appear/collapse is animated in 07-animations.css and the active
240
+ count); the appear/collapse is animated in 40-animations.css and the active
234
241
  chip shrinks/grows to fill the freed space in lockstep (flex layout). */
235
- .wt-switcher-switch {
242
+ :where(.wt-root) .wt-switcher-switch {
236
243
  position: relative;
237
244
  flex: none;
238
245
  width: 0;
@@ -247,7 +254,7 @@
247
254
  }
248
255
 
249
256
  /* Revealed once a second tab exists: full 44px box, interactive, no gap eat. */
250
- .wt-switcher-multi .wt-switcher-switch {
257
+ :where(.wt-root) .wt-switcher-multi .wt-switcher-switch {
251
258
  width: var(--touch-target);
252
259
  opacity: 1;
253
260
  pointer-events: auto;
@@ -256,10 +263,10 @@
256
263
 
257
264
  /* When the tab list expands the switch button collapses too (its job is done:
258
265
  the list is open), mirroring the keyboard button's collapse in the same
259
- animation (07-animations.css animates width + margin + opacity). Placed after
266
+ animation (40-animations.css animates width + margin + opacity). Placed after
260
267
  the .wt-switcher-multi reveal so it wins when both apply (expanded, two or
261
268
  more tabs). */
262
- .wt-switcher-expanded .wt-switcher-switch {
269
+ :where(.wt-root) .wt-switcher-expanded .wt-switcher-switch {
263
270
  width: 0;
264
271
  opacity: 0;
265
272
  margin-inline-start: calc(-1 * var(--sp-1));
@@ -267,17 +274,17 @@
267
274
  }
268
275
 
269
276
  @media (hover: hover) {
270
- .wt-switcher-switch:hover {
277
+ :where(.wt-root) .wt-switcher-switch:hover {
271
278
  background: var(--tab-hover-bg);
272
279
  }
273
280
  }
274
281
 
275
282
  /* Latest-wins background-tab notification dot on the switch button — the moved
276
283
  attention cue. Amber = a background terminal needs input, green = a background
277
- turn finished. Reuses the exact .wt-status-dot colours (05-tabs.css) so it
284
+ turn finished. Reuses the exact .wt-status-dot colours (30-tabs.css) so it
278
285
  matches the per-tab dots. Hidden until the tabs feature sets data-status;
279
286
  cleared when the list opens (expandSwitcher). */
280
- .wt-switcher-switch-dot {
287
+ :where(.wt-root) .wt-switcher-switch-dot {
281
288
  position: absolute;
282
289
  top: 6px;
283
290
  right: 6px;
@@ -285,10 +292,10 @@
285
292
 
286
293
  /* The dot carries the .wt-status-dot class, so its size, shape, and colour come
287
294
  straight from the shared .wt-status-dot[data-status="input"|"done"] rules
288
- (05-tabs.css) — the SAME dot colours every tab already uses, defined once.
295
+ (30-tabs.css) — the SAME dot colours every tab already uses, defined once.
289
296
  .wt-status-dot is display:none by default; force it visible only while a
290
297
  notification is set (data-status present). */
291
- .wt-switcher-switch-dot[data-status] {
298
+ :where(.wt-root) .wt-switcher-switch-dot[data-status] {
292
299
  display: block;
293
300
  }
294
301
 
@@ -296,72 +303,70 @@
296
303
  permanent and prominent. Hover fill is gated to real hover pointers: on touch,
297
304
  :hover sticks after a tap (no pointer-leave), so the purple would linger — the
298
305
  press feedback there comes from :active, which clears on release. */
299
- .wt-switcher-new {
306
+ :where(.wt-root) .wt-switcher-new {
300
307
  flex: none;
301
308
  color: var(--accent);
302
309
  }
303
310
 
304
311
  @media (hover: hover) {
305
- .wt-switcher-new:hover {
312
+ :where(.wt-root) .wt-switcher-new:hover {
306
313
  background: var(--tab-hover-bg);
307
314
  }
308
315
  }
309
316
 
310
- .wt-switcher-new:active {
317
+ :where(.wt-root) .wt-switcher-new:active {
311
318
  background: var(--tab-hover-bg);
312
319
  }
313
320
 
314
321
  /* ===== Externally-driven key grid (tabbed presets, WS3) =====
315
- In the tabbed presets the mobile toolbar hides its own toggle (the tab bar's
316
- keyboard button drives it) and the grid docks above the bar on the right, to
317
- the left of where scroll-to-bottom sits. It collapses to fully hidden, since
318
- there is no in-grid toggle to keep visible. */
319
- @media (pointer: coarse) and (width <= 600px) {
320
- .wt-toolbar-external .kb-toggle {
322
+ In the tabbed presets the mobile toolbar hides its own toggle (a tab-chrome
323
+ keyboard button drives it — the switcher's on a narrow phone, the strip's
324
+ .wt-tab-kb on a wide touchscreen) and the grid docks bottom-right above the
325
+ tab chrome, its RIGHT edge aligned with the keyboard button that opened it,
326
+ so the popup visually hangs off its toggle in every mode (it may overlap the
327
+ scroll-to-bottom button — deliberate: the grid is transient and on top). It
328
+ collapses to fully hidden, since there is no in-grid toggle to keep visible.
329
+ The BASE rules here are the wide-touchscreen (strip) placement — above
330
+ --wt-tabbar-h, right-aligned to the strip's .wt-tab-kb (the bar's last flex
331
+ item, inset --sp-2 by the bar padding); the .wt-narrow overrides below
332
+ re-anchor above the switcher bar (--wt-switcher-h) and shift left past the
333
+ bar controls sitting right of the switcher's kb button: the "+" (always)
334
+ and, once a second tab reveals it, the switch button (.wt-switcher-multi
335
+ from the tabs feature, read here via :has() since the grid is a sibling
336
+ region of the switcher). Everything else (toggle hidden, grid columns,
337
+ collapsed fully hidden) is identical in all modes. */
338
+ @media (pointer: coarse) {
339
+ :where(.wt-root) .wt-toolbar-external .kb-toggle {
321
340
  display: none;
322
341
  }
323
342
 
324
- .key-toolbar.wt-toolbar-external {
343
+ :where(.wt-root) .key-toolbar.wt-toolbar-external {
325
344
  top: auto;
326
- right: calc(var(--sp-3) + var(--touch-target) + var(--sp-2));
345
+ right: var(--sp-2);
327
346
  bottom: calc(
328
- var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
347
+ var(--wt-tabbar-h, 44px) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
329
348
  );
330
349
  grid-template-columns: repeat(4, var(--touch-target));
331
350
  }
332
351
 
333
- .key-toolbar.wt-toolbar-external.collapsed {
334
- display: none;
335
- }
336
- }
337
-
338
- /* Desktop tab strip on a WIDE touchscreen (an iPad): the mobile switcher is
339
- gone, so the desktop strip carries its OWN keyboard button (.wt-tab-kb, built
340
- by the same factory as the switcher's — see css/05-tabs.css + tabs.ts) to open
341
- the key grid. So, exactly like switcher mode, the toolbar's own toggle is
342
- hidden and the collapsed grid fully hidden — the tab-strip keyboard button is
343
- the sole control (this replaces the old CSS-only workaround that un-hid the
344
- toolbar's own toggle here). The grid still docks bottom-right ABOVE the strip:
345
- above the tabs (never overlapping them, even when many are open and the strip
346
- scrolls) and to the left of the scroll-to-bottom FAB (same right offset as
347
- switcher mode). Complements the strip-shown breakpoint in css/05-tabs.css;
348
- keep in lockstep with SWITCHER_MEDIA in src/features/tabs.ts. */
349
- @media (pointer: coarse) and (width >= 601px) {
350
- .wt-toolbar-external .kb-toggle {
352
+ :where(.wt-root) .key-toolbar.wt-toolbar-external.collapsed {
351
353
  display: none;
352
354
  }
353
355
 
354
- .key-toolbar.wt-toolbar-external {
355
- top: auto;
356
- right: calc(var(--sp-3) + var(--touch-target) + var(--sp-2));
356
+ /* Narrow phone (mobile switcher mode): dock above the switcher bar,
357
+ right-aligned to the switcher's kb button — one 44px control (+ gap)
358
+ in from the edge past the "+"... */
359
+ :where(.wt-root.wt-narrow) .key-toolbar.wt-toolbar-external {
360
+ right: calc(var(--sp-2) + var(--touch-target) + var(--sp-1));
357
361
  bottom: calc(
358
- var(--wt-tabbar-h, 44px) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
362
+ var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
359
363
  );
360
- grid-template-columns: repeat(4, var(--touch-target));
361
364
  }
362
365
 
363
- .key-toolbar.wt-toolbar-external.collapsed {
364
- display: none;
366
+ /* ...and a second control in once the switch button is revealed (>= 2 tabs).
367
+ Ordered after the single-tab rule so it wins when both match. */
368
+ :where(.wt-root.wt-narrow:has(.wt-switcher-multi)) .key-toolbar.wt-toolbar-external {
369
+ right: calc(var(--sp-2) + 2 * (var(--touch-target) + var(--sp-1)));
365
370
  }
366
371
  }
367
372
 
@@ -369,12 +374,12 @@
369
374
  One row per OTHER tab in the expanded list: a stretched select target (dot +
370
375
  label) with the close (x) laid inside at the right, mirroring the desktop
371
376
  tab's merged chrome. */
372
- .wt-switcher-row {
377
+ :where(.wt-root) .wt-switcher-row {
373
378
  position: relative;
374
379
  display: flex;
375
380
  }
376
381
 
377
- .wt-switcher-row-select {
382
+ :where(.wt-root) .wt-switcher-row-select {
378
383
  flex: 1 1 auto;
379
384
  display: flex;
380
385
  align-items: center;
@@ -394,7 +399,7 @@
394
399
  }
395
400
 
396
401
  @media (hover: hover) {
397
- .wt-switcher-row-select:hover {
402
+ :where(.wt-root) .wt-switcher-row-select:hover {
398
403
  background: color-mix(in oklch, var(--tab-bg), var(--text) 15%);
399
404
  }
400
405
  }
@@ -403,11 +408,11 @@
403
408
  chip (the same 15% --text lift the desktop tabs use), not the translucent
404
409
  --tab-hover-bg wash that would erase the chip fill. The active tab is set apart
405
410
  by its accent border, so a lit inactive row is never mistaken for it. */
406
- .wt-switcher-row-select:active {
411
+ :where(.wt-root) .wt-switcher-row-select:active {
407
412
  background: color-mix(in oklch, var(--tab-bg), var(--text) 15%);
408
413
  }
409
414
 
410
- .wt-switcher-row-label {
415
+ :where(.wt-root) .wt-switcher-row-label {
411
416
  flex: 1 1 auto;
412
417
  overflow: hidden;
413
418
  text-overflow: ellipsis;
@@ -415,7 +420,7 @@
415
420
  }
416
421
 
417
422
  /* The close sits flush at the row's right edge (no gap), vertically centered. */
418
- .wt-switcher-row-close {
423
+ :where(.wt-root) .wt-switcher-row-close {
419
424
  position: absolute;
420
425
  top: 50%;
421
426
  right: 0;
@@ -427,7 +432,7 @@
427
432
  when closing a tab shifts the list under the finger the newly-under close
428
433
  would latch white. Touch feedback comes from the surrounding row's :active. */
429
434
  @media (hover: hover) {
430
- .wt-switcher-row-close:hover {
435
+ :where(.wt-root) .wt-switcher-row-close:hover {
431
436
  color: var(--text);
432
437
  }
433
438
  }
@@ -4,10 +4,10 @@
4
4
  base rules in 03-06 carry the resting state, and these rules add the
5
5
  transitions and keyframes on top. A few pieces of motion are intentionally NOT
6
6
  gated on .wt-animate and animate unconditionally: terminal-core motion (cursor
7
- and SGR blink) and the mobile key toolbar open/close in 02-app.css, plus the
8
- working-status-dot pulse in 05-tabs.css (motion is its only non-hue cue, per
9
- WCAG 1.4.1). prefers-reduced-motion still defeats all of it via the reset in
10
- 01-reset.css. Keeping the chrome-surface motion in one file is the
7
+ and SGR blink) and the mobile key toolbar open/close in 23-toolbar.css, plus the
8
+ working-status-dot pulse in 30-tabs.css (motion is its only non-hue cue, per
9
+ WCAG 1.4.1). prefers-reduced-motion still defeats all of it via the scoped
10
+ kill-switch in 01-scope.css. Keeping the chrome-surface motion in one file is the
11
11
  design-system seam for "does this chrome animate, and how". */
12
12
 
13
13
  /* Fade the toast OUT on hide (display allow-discrete holds display:block for
@@ -21,16 +21,16 @@
21
21
  shared opacity transition would be inert while the `display allow-discrete`
22
22
  half held them fully visible for --dur-standard before popping out on hide.
23
23
  They show and hide instantly. */
24
- .wt-animate .wt-toast {
24
+ :where(.wt-root.wt-animate) .wt-toast {
25
25
  transition:
26
26
  opacity var(--dur-standard) var(--ease-standard),
27
27
  display var(--dur-standard) allow-discrete;
28
28
  }
29
29
 
30
30
  /* Tab chrome micro-transitions (hover/active color shifts). */
31
- .wt-animate .wt-tab,
32
- .wt-animate .wt-tab-close,
33
- .wt-animate .wt-tab-new {
31
+ :where(.wt-root.wt-animate) .wt-tab,
32
+ :where(.wt-root.wt-animate) .wt-tab-close,
33
+ :where(.wt-root.wt-animate) .wt-tab-new {
34
34
  transition:
35
35
  background-color var(--dur-micro) var(--ease-standard),
36
36
  color var(--dur-micro) var(--ease-standard);
@@ -41,7 +41,7 @@
41
41
  immediately with no exit animation, so the strip reflows in one frame (a
42
42
  delayed exit made the "+" teleport and put a last-tab replacement in the wrong
43
43
  slot). scale() is a transform, so the enter never shifts the "+". */
44
- .wt-animate .wt-tab-enter {
44
+ :where(.wt-root.wt-animate) .wt-tab-enter {
45
45
  animation: wt-tab-in var(--dur-enter) var(--ease-enter);
46
46
  }
47
47
 
@@ -58,7 +58,7 @@
58
58
  — animating only max-height let the padding snap away instantly at the start
59
59
  of a collapse (a choppy, two-step close). The tabs feature clears the rows
60
60
  after the collapse. */
61
- .wt-animate .wt-switcher-list {
61
+ :where(.wt-root.wt-animate) .wt-switcher-list {
62
62
  transition:
63
63
  max-height var(--dur-enter) var(--ease-standard),
64
64
  padding var(--dur-enter) var(--ease-standard);
@@ -67,7 +67,7 @@
67
67
  /* When the tab list expands, the keyboard button collapses to zero width and
68
68
  fades (the key grid is closed in JS); the active row flex-grows into the freed
69
69
  space in lockstep as the button's width animates. */
70
- .wt-animate .wt-switcher-kb {
70
+ :where(.wt-root.wt-animate) .wt-switcher-kb {
71
71
  transition:
72
72
  width var(--dur-enter) var(--ease-standard),
73
73
  margin var(--dur-enter) var(--ease-standard),
@@ -78,7 +78,7 @@
78
78
  at one tab (.wt-switcher-multi, tabs feature). Animate its width + margin +
79
79
  opacity so it slides in from zero width — the active-tab chip shrinks to make
80
80
  room in lockstep (flex) — rather than popping. Mirrors the keyboard button. */
81
- .wt-animate .wt-switcher-switch {
81
+ :where(.wt-root.wt-animate) .wt-switcher-switch {
82
82
  transition:
83
83
  width var(--dur-enter) var(--ease-standard),
84
84
  margin var(--dur-enter) var(--ease-standard),
@@ -89,15 +89,15 @@
89
89
  reads as a change rather than a flicker. A swipe additionally slides the
90
90
  content in from the swipe's direction (next from the right, prev from the
91
91
  left, WS5). */
92
- .wt-animate .term.wt-switching .term-output {
92
+ :where(.wt-root.wt-animate) .term.wt-switching .term-output {
93
93
  animation: wt-switch-in var(--dur-standard) var(--ease-standard);
94
94
  }
95
95
 
96
- .wt-animate .term.wt-switching-next .term-output {
96
+ :where(.wt-root.wt-animate) .term.wt-switching-next .term-output {
97
97
  animation: wt-switch-next var(--dur-standard) var(--ease-standard);
98
98
  }
99
99
 
100
- .wt-animate .term.wt-switching-prev .term-output {
100
+ :where(.wt-root.wt-animate) .term.wt-switching-prev .term-output {
101
101
  animation: wt-switch-prev var(--dur-standard) var(--ease-standard);
102
102
  }
103
103
 
package/css/MANIFEST CHANGED
@@ -1,12 +1,20 @@
1
- # web-terminal-ui CSS bundle manifest
2
- # Files concatenated in declared order to produce the served style.css.
3
- # The order IS the cascade: tokens + reset + terminal core, then the v3 design
4
- # system (shared primitives, then per-feature chrome), then all motion last.
1
+ # web-terminal-ui CSS — the REFERENCE FULL-PAGE bundle.
2
+ # What the full-page product serves (web-terminal-server, web-terminal-kiro,
3
+ # scaffold/index.html): the page kit (page.css — html/body reset + @font-face;
4
+ # full-page hosts ONLY) plus the complete tabbed component set. An EMBEDDING
5
+ # consumer must NOT serve this file's bundle — it concatenates the per-preset
6
+ # manifest matching its composition instead (MANIFEST.single / MANIFEST.touch /
7
+ # MANIFEST.tabbed), which contains no page-level rules.
8
+ # Files concatenate in declared order; the order IS the cascade.
9
+ page.css
5
10
  00-tokens.css
6
- 01-reset.css
7
- 02-app.css
8
- 03-primitives.css
9
- 04-feedback.css
10
- 05-tabs.css
11
- 06-mobile.css
12
- 07-animations.css
11
+ 01-scope.css
12
+ 02-terminal.css
13
+ 10-primitives.css
14
+ 20-connection-banner.css
15
+ 21-scroll-to-bottom.css
16
+ 22-context-menu.css
17
+ 23-toolbar.css
18
+ 30-tabs.css
19
+ 31-switcher.css
20
+ 40-animations.css
@@ -0,0 +1,12 @@
1
+ # Component bundle for presetSingle() compositions (no toolbar, no tabs, no
2
+ # animations feature). Library styles only — root-scoped, no page-level rules;
3
+ # the host page owns its own reset/fonts (full-page hosts use the reference
4
+ # MANIFEST instead, whose page.css carries those).
5
+ # Files concatenate in declared order; the order IS the cascade.
6
+ 00-tokens.css
7
+ 01-scope.css
8
+ 02-terminal.css
9
+ 10-primitives.css
10
+ 20-connection-banner.css
11
+ 21-scroll-to-bottom.css
12
+ 22-context-menu.css
@@ -0,0 +1,16 @@
1
+ # Component bundle for presetTabbed() / presetAgentTabbed() compositions: the
2
+ # touch set plus tabs (strip + mobile switcher) and the animations feature's
3
+ # motion. Library styles only — root-scoped, no page-level rules. A full-page
4
+ # host serves the reference MANIFEST instead (this set + page.css).
5
+ # Files concatenate in declared order; the order IS the cascade.
6
+ 00-tokens.css
7
+ 01-scope.css
8
+ 02-terminal.css
9
+ 10-primitives.css
10
+ 20-connection-banner.css
11
+ 21-scroll-to-bottom.css
12
+ 22-context-menu.css
13
+ 23-toolbar.css
14
+ 30-tabs.css
15
+ 31-switcher.css
16
+ 40-animations.css
@@ -0,0 +1,12 @@
1
+ # Component bundle for presetTouch() compositions (single + the mobile key
2
+ # toolbar). Library styles only — root-scoped, no page-level rules; the host
3
+ # page owns its own reset/fonts (vibekit's embedded panel serves exactly this).
4
+ # Files concatenate in declared order; the order IS the cascade.
5
+ 00-tokens.css
6
+ 01-scope.css
7
+ 02-terminal.css
8
+ 10-primitives.css
9
+ 20-connection-banner.css
10
+ 21-scroll-to-bottom.css
11
+ 22-context-menu.css
12
+ 23-toolbar.css
@@ -1,4 +1,11 @@
1
- /* ===== Reset ===== */
1
+ /* ===== Page kit (full-page hosts ONLY — never ship this to an embedder) =====
2
+ The page-level half of the full-page terminal product: what the HOST PAGE
3
+ owns when the terminal IS the page (web-terminal-server, web-terminal-kiro,
4
+ scaffold/index.html). The component bundle (00-10) never touches the host
5
+ document; everything here is deliberately page-scoped. An embedding consumer
6
+ (vibekit) serves a per-preset manifest instead and owns its own page.
7
+ Concatenated FIRST by the reference MANIFEST. */
8
+
2
9
  *,
3
10
  *::before,
4
11
  *::after {
@@ -11,7 +18,10 @@ body {
11
18
  padding: 0;
12
19
  height: 100%;
13
20
  overflow: hidden;
14
- background: var(--bg);
21
+ /* The pre-JS backdrop (the loading state paints over it). Matches the
22
+ component's --bg default (css/00-tokens.css, oklch(0% 0 0deg)); a themed
23
+ full-page host overrides both together. */
24
+ background: #000;
15
25
  color-scheme: dark;
16
26
  -webkit-font-smoothing: antialiased;
17
27
  /* Disable the browser's landscape text autosizing ("text inflation"): without
@@ -29,23 +39,12 @@ body {
29
39
  touch-action: pan-x pan-y;
30
40
  }
31
41
 
32
- /* stylelint-disable declaration-no-important, time-min-milliseconds --
33
- Canonical prefers-reduced-motion override. !important and near-zero
34
- durations are required to forcibly defeat any animation/transition rule
35
- regardless of specificity, which is the whole point of this a11y reset. */
36
- @media (prefers-reduced-motion: reduce) {
37
- *,
38
- *::before,
39
- *::after {
40
- animation-duration: 0.01ms !important;
41
- animation-iteration-count: 1 !important;
42
- transition-duration: 0.01ms !important;
43
- scroll-behavior: auto !important;
44
- }
45
- }
46
- /* stylelint-enable declaration-no-important, time-min-milliseconds */
47
-
48
- /* ===== Fonts ===== */
42
+ /* ===== Fonts =====
43
+ The terminal web font, served by the full-page host at /vendor/fonts (both
44
+ web-terminal-server and web-terminal-kiro ship these files there; see their
45
+ Dockerfiles). An embedder does not serve this file, so it never inherits
46
+ these URLs — it either ships its own @font-face or lets --font-mono fall back
47
+ to the platform monospace stack. */
49
48
  @font-face {
50
49
  font-family: "MonaspiceNe NFM";
51
50
  src: url("/vendor/fonts/MonaspiceNeNerdFontMono-Regular.otf") format("opentype");