@cplieger/web-terminal-ui 3.0.4 → 3.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.
- package/README.md +6 -0
- package/css/00-tokens.css +13 -1
- package/css/02-app.css +24 -120
- package/css/03-primitives.css +82 -0
- package/css/04-feedback.css +166 -0
- package/css/05-tabs.css +310 -0
- package/css/06-mobile.css +335 -0
- package/css/07-animations.css +102 -0
- package/css/MANIFEST +7 -1
- package/package.json +2 -2
- package/src/features/clipboard.ts +10 -0
- package/src/features/connection-banner.ts +1 -1
- package/src/features/context-menu.ts +174 -42
- package/src/features/mobile-toolbar.ts +59 -1
- package/src/features/scroll-to-bottom.ts +22 -6
- package/src/features/tabs.ts +1272 -226
- package/src/kernel/kernel.ts +65 -4
- package/src/kernel/types.ts +8 -0
- package/src/presets.ts +36 -6
- package/src/viewport.ts +16 -1
- package/css/03-v3.css +0 -705
package/css/05-tabs.css
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/* ===== Tabs: desktop strip + activity vocabulary (tabs feature) =====
|
|
2
|
+
The desktop top-bar tab strip, its per-tab chrome (label, close, add, the
|
|
3
|
+
right-click menu), the surface offset that clears the fixed strip, and the
|
|
4
|
+
shared activity-dot status vocabulary. The mobile tab chrome (switcher bar,
|
|
5
|
+
overview sheet, external key grid) lives in 06-mobile.css; all motion lives in
|
|
6
|
+
07-animations.css. */
|
|
7
|
+
|
|
8
|
+
.wt-region-top-bar {
|
|
9
|
+
/* On desktop the tab strip docks to the top; the region is transparent, so
|
|
10
|
+
the bar element below owns its own fixed placement. */
|
|
11
|
+
display: contents;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.wt-tab-bar {
|
|
15
|
+
position: fixed;
|
|
16
|
+
bottom: var(--safe-bottom);
|
|
17
|
+
left: 0;
|
|
18
|
+
right: 0;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: var(--sp-1);
|
|
22
|
+
/* Breathing room around the tab area (a roomier bottom bar). */
|
|
23
|
+
padding: var(--sp-2);
|
|
24
|
+
background: var(--surface);
|
|
25
|
+
backdrop-filter: var(--blur);
|
|
26
|
+
border-top: 1px solid var(--border);
|
|
27
|
+
overflow-x: auto;
|
|
28
|
+
z-index: 15;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.wt-tab {
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
align-items: center;
|
|
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. */
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
flex: 0 1 140px;
|
|
40
|
+
min-width: 90px;
|
|
41
|
+
padding: var(--sp-2) var(--sp-2);
|
|
42
|
+
/* A transparent border is reserved on every tab so toggling the inactive
|
|
43
|
+
outline (below) never shifts the strip's layout. */
|
|
44
|
+
border: 1px solid transparent;
|
|
45
|
+
border-radius: var(--radius-inner);
|
|
46
|
+
color: color-mix(in oklch, var(--text) 70%, transparent);
|
|
47
|
+
font-family: var(--font-ui);
|
|
48
|
+
font-size: 0.8125rem;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
user-select: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Inactive tabs read as outlined chips (so adjacent tabs stay visually
|
|
55
|
+
separated) and take a subtle fill on hover. */
|
|
56
|
+
.wt-tab:not(.wt-tab-active) {
|
|
57
|
+
border-color: var(--border);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.wt-tab:not(.wt-tab-active):hover {
|
|
61
|
+
background: var(--tab-hover-bg);
|
|
62
|
+
color: var(--tab-active-fg);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* The active tab reads clearly above the inactive chips: a stronger fill.
|
|
66
|
+
Themeable (vibecli tints these purple). */
|
|
67
|
+
.wt-tab-active {
|
|
68
|
+
background: var(--tab-active-bg);
|
|
69
|
+
color: var(--tab-active-fg);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* The label flexes to fill the fixed-width tab and truncates with an ellipsis
|
|
73
|
+
(min-width: 0 lets a flex item shrink below its content width). */
|
|
74
|
+
.wt-tab-label {
|
|
75
|
+
flex: 1 1 auto;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
text-overflow: ellipsis;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* A little breathing room after the activity dot, before the label. */
|
|
82
|
+
.wt-tab-dot {
|
|
83
|
+
margin-right: 2px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* The tab close (x): a centered, muted square icon button with a box on hover.
|
|
87
|
+
Its glyph is an inline SVG (styled below), so flex-centering the button
|
|
88
|
+
centers it exactly. */
|
|
89
|
+
.wt-tab-close {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
flex: none;
|
|
94
|
+
width: 1.375rem;
|
|
95
|
+
height: 1.375rem;
|
|
96
|
+
padding: 0;
|
|
97
|
+
background: transparent;
|
|
98
|
+
border: none;
|
|
99
|
+
border-radius: var(--radius-inner);
|
|
100
|
+
color: color-mix(in oklch, var(--text) 60%, transparent);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.wt-tab-close:hover {
|
|
105
|
+
background: var(--text-active);
|
|
106
|
+
color: var(--text);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* On an inactive tab the close appears only on hover, kept in the layout via
|
|
110
|
+
visibility so the tab width never jumps; on the active tab it is always shown. */
|
|
111
|
+
.wt-tab:not(.wt-tab-active) .wt-tab-close {
|
|
112
|
+
visibility: hidden;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.wt-tab:not(.wt-tab-active):hover .wt-tab-close {
|
|
116
|
+
visibility: visible;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* The add-tab (+) button: its hover box matches an active tab's fill and
|
|
120
|
+
stretches to the strip height; the 2px strip gap (not a custom margin) spaces
|
|
121
|
+
it from the last tab. Its glyph is an inline SVG. */
|
|
122
|
+
.wt-tab-new {
|
|
123
|
+
display: inline-flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
justify-content: center;
|
|
126
|
+
flex: none;
|
|
127
|
+
/* Square box that matches the tab height (a stretched flex item ignores
|
|
128
|
+
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) +
|
|
130
|
+
border. */
|
|
131
|
+
width: calc(1.375rem + 2 * var(--sp-2) + 2px);
|
|
132
|
+
height: calc(1.375rem + 2 * var(--sp-2) + 2px);
|
|
133
|
+
padding: 0;
|
|
134
|
+
background: transparent;
|
|
135
|
+
border: none;
|
|
136
|
+
border-radius: var(--radius-inner);
|
|
137
|
+
color: color-mix(in oklch, var(--text) 60%, transparent);
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.wt-tab-new:hover {
|
|
142
|
+
background: var(--tab-active-bg);
|
|
143
|
+
color: var(--tab-active-fg);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* The +/x icons are inline SVG so they center exactly in their flex-centered
|
|
147
|
+
button and stay symmetric regardless of the UI font (fixes the off-center
|
|
148
|
+
glyph and the lopsided "+"). The + is a touch larger and bolder than the x. */
|
|
149
|
+
.wt-tab-close svg,
|
|
150
|
+
.wt-tab-new svg {
|
|
151
|
+
stroke: currentcolor;
|
|
152
|
+
fill: none;
|
|
153
|
+
stroke-width: 2.5;
|
|
154
|
+
stroke-linecap: round;
|
|
155
|
+
stroke-linejoin: round;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.wt-tab-close svg {
|
|
159
|
+
width: 13px;
|
|
160
|
+
height: 13px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.wt-tab-new svg {
|
|
164
|
+
width: 16px;
|
|
165
|
+
height: 16px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* The tab being dragged (reorder) dims while it moves. */
|
|
169
|
+
.wt-tab-dragging {
|
|
170
|
+
opacity: 0.5;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Tab context menu (tabs feature, desktop right-click): Close / Close others /
|
|
174
|
+
Close to the right / Close to the left / Close all. Replaces the old bar
|
|
175
|
+
"Close all" button. Mirrors .wt-ctx-menu (a fixed surface popover, clamped
|
|
176
|
+
into the viewport by the feature); disabled items are dimmed and inert. */
|
|
177
|
+
.wt-tab-menu {
|
|
178
|
+
position: fixed;
|
|
179
|
+
display: none;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
background: var(--surface);
|
|
182
|
+
backdrop-filter: var(--blur);
|
|
183
|
+
border: 1px solid var(--border);
|
|
184
|
+
border-radius: var(--radius);
|
|
185
|
+
padding: var(--sp-1);
|
|
186
|
+
z-index: 30;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.wt-tab-menu.visible {
|
|
190
|
+
display: flex;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.wt-tab-menu button {
|
|
194
|
+
background: transparent;
|
|
195
|
+
color: var(--text);
|
|
196
|
+
border: none;
|
|
197
|
+
border-radius: var(--radius-inner);
|
|
198
|
+
font-family: var(--font-ui);
|
|
199
|
+
font-size: 0.8125rem;
|
|
200
|
+
font-weight: 500;
|
|
201
|
+
padding: var(--sp-1) var(--sp-3);
|
|
202
|
+
cursor: pointer;
|
|
203
|
+
text-align: left;
|
|
204
|
+
white-space: nowrap;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.wt-tab-menu button:not(:disabled):hover {
|
|
208
|
+
background: var(--text-active);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.wt-tab-menu button:disabled {
|
|
212
|
+
color: color-mix(in oklch, var(--text) 35%, transparent);
|
|
213
|
+
cursor: default;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Pull the terminal surface up off the fixed BOTTOM tab strip so it does not
|
|
217
|
+
overlap the last rows (a mirror of the old top offset). The tabs feature
|
|
218
|
+
marks the surface (.wt-with-tabbar) and keeps the measured strip height in
|
|
219
|
+
--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) {
|
|
224
|
+
.term.wt-with-tabbar {
|
|
225
|
+
bottom: var(--wt-tabbar-h, 44px);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ===== Activity dots (activityMonitor via tabs) =====
|
|
230
|
+
Four-state, distinguishable without color (shape + motion, not hue alone),
|
|
231
|
+
per section 12 / WCAG 1.4.1. The one .wt-status-dot vocabulary is shared by
|
|
232
|
+
the desktop strip, the mobile switcher, and the overview sheet rows.
|
|
233
|
+
|
|
234
|
+
Opt-in: the dot is hidden unless its container carries .wt-status, which the
|
|
235
|
+
tabs feature adds only when a status source (activityMonitor) is wired. A
|
|
236
|
+
generic bash/sh terminal has no activity concept, so its tabs read as clean
|
|
237
|
+
labels; the agent consumer (vibecli) supplies the monitor and gets the dots.
|
|
238
|
+
The element stays in the DOM either way (JS sets data-status regardless), so
|
|
239
|
+
revealing it is a pure CSS switch. */
|
|
240
|
+
.wt-status-dot {
|
|
241
|
+
display: none;
|
|
242
|
+
width: 9px;
|
|
243
|
+
height: 9px;
|
|
244
|
+
border-radius: 50%;
|
|
245
|
+
flex: none;
|
|
246
|
+
/* Default / idle (a new session with no turn yet): a hollow grey dot, so
|
|
247
|
+
"nothing has happened" reads as an outline rather than a filled state.
|
|
248
|
+
The fill (solid vs hollow) is the non-hue cue that separates the idle state
|
|
249
|
+
from the filled done/working/input states (WCAG 1.4.1). */
|
|
250
|
+
background: transparent;
|
|
251
|
+
border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* A status source is wired (the tabs feature marked the chrome .wt-status): show
|
|
255
|
+
the dots. Without this class the whole activity vocabulary stays invisible. */
|
|
256
|
+
.wt-status .wt-status-dot {
|
|
257
|
+
display: block;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.wt-status-dot[data-status="idle"] {
|
|
261
|
+
background: transparent;
|
|
262
|
+
border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Agent working: solid pulsing white. (Was purple to match Kiro IDE's thinking
|
|
266
|
+
colour, but that collides with the purple active-tab fill; white reads clearly
|
|
267
|
+
on every tab state.) Motion is the primary non-hue cue. Server-side this is
|
|
268
|
+
driven by the OSC 9;4 progress signal, so it tracks agent activity, not the
|
|
269
|
+
user typing at the prompt. */
|
|
270
|
+
.wt-status-dot[data-status="working"] {
|
|
271
|
+
background: #fff;
|
|
272
|
+
border-color: #fff;
|
|
273
|
+
animation: wt-pulse 1.2s ease-in-out infinite;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* Agent done / ready: solid green, matching Kiro IDE's "Ready" (#22c55e), no
|
|
277
|
+
motion. The filled dot distinguishes it from the hollow idle without hue. */
|
|
278
|
+
.wt-status-dot[data-status="done"] {
|
|
279
|
+
background: #22c55e;
|
|
280
|
+
border-color: #22c55e;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* User action required (permission / approval): solid amber matching Kiro IDE's
|
|
284
|
+
"waiting-approval" (#fbbf24), plus a ring so it reads as "needs you" via shape
|
|
285
|
+
as well as hue. It persists until you act on or switch into the tab. */
|
|
286
|
+
.wt-status-dot[data-status="input"] {
|
|
287
|
+
background: #fbbf24;
|
|
288
|
+
border-color: #fbbf24;
|
|
289
|
+
box-shadow: 0 0 0 2px oklch(85% 0.16 85deg / 40%);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Exited: a dim hollow dot, static; distinct from idle by its lower opacity. */
|
|
293
|
+
.wt-status-dot[data-status="exited"] {
|
|
294
|
+
background: transparent;
|
|
295
|
+
border: 1.5px solid color-mix(in oklch, var(--text) 30%, transparent);
|
|
296
|
+
opacity: 0.5;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@keyframes wt-pulse {
|
|
300
|
+
50% {
|
|
301
|
+
opacity: 0.4;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Reduced-motion (or the animations feature off): drop the pulse, keep color. */
|
|
306
|
+
@media (prefers-reduced-motion: reduce) {
|
|
307
|
+
.wt-status-dot[data-status="working"] {
|
|
308
|
+
animation: none;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/* ===== Mobile tab chrome (tabs feature, section 12) =====
|
|
2
|
+
The bottom bar (switcher) is the mobile tab surface, the externally-driven key
|
|
3
|
+
grid docks above it, and the overview sheet lists every tab. The desktop strip
|
|
4
|
+
is hidden on a coarse pointer and these appear instead; all motion (bar number
|
|
5
|
+
fade, sheet slide) lives in 07-animations.css. */
|
|
6
|
+
|
|
7
|
+
/* The switcher is hidden on a fine pointer, where the top strip is used instead;
|
|
8
|
+
the top strip is correspondingly hidden on a coarse pointer so tabs live in
|
|
9
|
+
the thumb zone. */
|
|
10
|
+
.wt-switcher {
|
|
11
|
+
display: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@media (pointer: coarse) {
|
|
15
|
+
.wt-tab-bar,
|
|
16
|
+
.wt-tab-new {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.wt-switcher {
|
|
21
|
+
position: fixed;
|
|
22
|
+
left: 0;
|
|
23
|
+
right: 0;
|
|
24
|
+
/* Lift above the soft keyboard rather than hiding, so the switch surface
|
|
25
|
+
stays reachable in the terminal's primary working state (section 12). */
|
|
26
|
+
bottom: calc(var(--safe-bottom, 0px) + var(--kb-inset, 0px));
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column; /* bar on top; the tab list expands below it as the bar slides up */
|
|
29
|
+
background: var(--surface);
|
|
30
|
+
backdrop-filter: var(--blur);
|
|
31
|
+
border-top: 1px solid var(--border);
|
|
32
|
+
z-index: 25;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* The always-visible bar row: active tab + keyboard + "+". The padding gives the
|
|
37
|
+
active-tab chip breathing room above/below within the bar, matching the
|
|
38
|
+
desktop strip (both use --sp-2). */
|
|
39
|
+
.wt-switcher-bar {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: stretch;
|
|
42
|
+
gap: var(--sp-1);
|
|
43
|
+
padding: var(--sp-2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* The other tabs, revealed below the bar row on expand (the bar slides up).
|
|
47
|
+
Collapsed to zero height; expanded caps the height and scrolls. The max-height
|
|
48
|
+
transition lives in 07-animations.css. */
|
|
49
|
+
.wt-switcher-list {
|
|
50
|
+
list-style: none;
|
|
51
|
+
margin: 0;
|
|
52
|
+
/* Match the bar's horizontal padding (--sp-2) so a listed row lines up with
|
|
53
|
+
the bar: its dot/label left-aligns with the active tab's, and its right edge
|
|
54
|
+
aligns with the "+" button's right edge (both inset --sp-2 from the switcher
|
|
55
|
+
edges). No separator border between the bar and the list — the active tab's
|
|
56
|
+
fill already sets it apart. */
|
|
57
|
+
padding: 0 var(--sp-2);
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 2px;
|
|
61
|
+
max-height: 0;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
/* max-height caps the whole box (incl. the expanded padding) so an interactive
|
|
64
|
+
pull from collapsed starts at a true zero height rather than a padding-tall
|
|
65
|
+
sliver. */
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.wt-switcher-expanded .wt-switcher-list {
|
|
70
|
+
max-height: 50vh;
|
|
71
|
+
overflow-y: auto;
|
|
72
|
+
padding-top: var(--sp-1);
|
|
73
|
+
padding-bottom: var(--sp-1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* The active-tab row: a select/swipe surface plus a close (x) overlaid at the
|
|
77
|
+
right, mirroring the listed rows (two buttons can't nest, so the close is a
|
|
78
|
+
sibling over the surface's reserved right padding). The wrap is the flex item
|
|
79
|
+
that grows in the bar (and fills the space when the keyboard button collapses
|
|
80
|
+
on expand). */
|
|
81
|
+
.wt-switcher-current-wrap {
|
|
82
|
+
position: relative;
|
|
83
|
+
flex: 1 1 auto;
|
|
84
|
+
display: flex;
|
|
85
|
+
min-width: 0;
|
|
86
|
+
/* Clip the swiping content (dot + label + close all translate on a swipe) at
|
|
87
|
+
the tab's edges, so the close slides off/in cleanly rather than spilling
|
|
88
|
+
over the "+" button. */
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.wt-switcher-current {
|
|
93
|
+
position: relative;
|
|
94
|
+
flex: 1 1 auto;
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: var(--sp-2);
|
|
98
|
+
min-height: var(--touch-target);
|
|
99
|
+
/* Right padding reserves room for the close (x) overlaid inside the row. */
|
|
100
|
+
padding: 0 var(--touch-target) 0 var(--sp-2);
|
|
101
|
+
/* The bar row always represents the active tab, so it wears the active-tab
|
|
102
|
+
fill/foreground the desktop strip uses (themeable; purple in vibecli). */
|
|
103
|
+
background: var(--tab-active-bg);
|
|
104
|
+
color: var(--tab-active-fg);
|
|
105
|
+
border: none;
|
|
106
|
+
border-radius: var(--radius-inner);
|
|
107
|
+
font-family: var(--font-ui);
|
|
108
|
+
font-size: 0.875rem;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
overflow: hidden;
|
|
111
|
+
/* Own both horizontal (switch) and vertical (swipe-up = overview) gestures.
|
|
112
|
+
The bar is fixed with nothing to scroll, so claiming the gesture is safe and
|
|
113
|
+
makes swipe detection reliable under passive listeners. */
|
|
114
|
+
touch-action: none;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* The active row's close (x): overlaid at the right over the row's reserved
|
|
118
|
+
padding, mirroring the listed rows' close. Hover fill only on real hover
|
|
119
|
+
pointers so it never sticks white after a touch (a removed tab shifts the
|
|
120
|
+
list under the finger otherwise). */
|
|
121
|
+
.wt-switcher-current-close {
|
|
122
|
+
position: absolute;
|
|
123
|
+
/* Centre vertically via top/bottom (not a translateY transform), so transform
|
|
124
|
+
is free for the swipe translate — the close moves with the dot + label. And
|
|
125
|
+
no right offset, so it sits flush at the tab's right edge (no gap). */
|
|
126
|
+
top: 0;
|
|
127
|
+
bottom: 0;
|
|
128
|
+
right: 0;
|
|
129
|
+
color: color-mix(in oklch, var(--text) 55%, transparent);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@media (hover: hover) {
|
|
133
|
+
.wt-switcher-current-close:hover {
|
|
134
|
+
color: var(--text);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Press feedback: brighten the active fill slightly (a lighter press over an
|
|
139
|
+
already-filled row). */
|
|
140
|
+
.wt-switcher-current:active {
|
|
141
|
+
background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 12%);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* The row's content (dot + label + position). It fills the row and lays its
|
|
145
|
+
parts out; a horizontal swipe translates it (clipped by the row's
|
|
146
|
+
overflow: hidden) so the active tab area physically slides with the finger,
|
|
147
|
+
and the incoming tab's label slides in from the swipe direction on commit. */
|
|
148
|
+
.wt-switcher-current-inner {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
gap: var(--sp-2);
|
|
152
|
+
flex: 1 1 auto;
|
|
153
|
+
min-width: 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.wt-switcher-label {
|
|
157
|
+
flex: 1 1 auto;
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
text-overflow: ellipsis;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
text-align: left;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Keyboard button: hidden until a keyboardToggle is wired; filled while the key
|
|
165
|
+
grid is open so its toggled state reads at a glance. An explicit width (not
|
|
166
|
+
just the wt-btn min-width) makes it animatable: when the tab list expands the
|
|
167
|
+
button collapses to zero width and fades out (the key grid is closed too, in
|
|
168
|
+
JS), and the active row flex-grows to fill the freed space. */
|
|
169
|
+
.wt-switcher-kb {
|
|
170
|
+
width: var(--touch-target);
|
|
171
|
+
/* Override .wt-btn's min-width (the 44px tap floor). min-width is NOT
|
|
172
|
+
transitioned, so on collapse it would revert to 44px instantly and clamp the
|
|
173
|
+
animating width back to full — the button snapped to size on close while
|
|
174
|
+
animating smoothly on open. The 44px open size still holds via `width`. */
|
|
175
|
+
min-width: 0;
|
|
176
|
+
/* Drop the user-agent button padding: with border-box it would floor the
|
|
177
|
+
collapsed width at the padding (≈12px) instead of a clean 0. The icon
|
|
178
|
+
centres via wt-btn. */
|
|
179
|
+
padding: 0;
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.wt-switcher-kb[hidden] {
|
|
184
|
+
display: none;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.wt-switcher-kb.wt-active {
|
|
188
|
+
background: var(--tab-hover-bg);
|
|
189
|
+
color: var(--tab-active-fg);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Sticky-Ctrl armed: invert the keyboard button (the same solid fill the armed
|
|
193
|
+
Ctrl key uses) while a Ctrl press is pending, so the modifier stays visible
|
|
194
|
+
even with the key grid — and its own Ctrl button — closed. After .wt-active so
|
|
195
|
+
the armed fill wins when the grid is also open. */
|
|
196
|
+
.wt-switcher-kb.wt-armed {
|
|
197
|
+
background: var(--text);
|
|
198
|
+
color: var(--bg);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.wt-switcher-expanded .wt-switcher-kb {
|
|
202
|
+
width: 0;
|
|
203
|
+
/* Absorb the flex gap the collapsed button would otherwise leave behind. */
|
|
204
|
+
margin-inline-start: calc(-1 * var(--sp-1));
|
|
205
|
+
opacity: 0;
|
|
206
|
+
pointer-events: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Mobile "+": the primary action, carried in the accent colour so it reads as
|
|
210
|
+
permanent and prominent. Hover fill is gated to real hover pointers: on touch,
|
|
211
|
+
:hover sticks after a tap (no pointer-leave), so the purple would linger — the
|
|
212
|
+
press feedback there comes from :active, which clears on release. */
|
|
213
|
+
.wt-switcher-new {
|
|
214
|
+
flex: none;
|
|
215
|
+
color: var(--accent);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@media (hover: hover) {
|
|
219
|
+
.wt-switcher-new:hover {
|
|
220
|
+
background: var(--tab-hover-bg);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.wt-switcher-new:active {
|
|
225
|
+
background: var(--tab-hover-bg);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Aggregate needs-input attention now rides the active surface (the separate
|
|
229
|
+
overview button is gone): a background tab is blocked on you, and a tap /
|
|
230
|
+
swipe-up here opens the overview to resolve it. Shape + color (an inset ring
|
|
231
|
+
plus a corner glyph), never hue alone (WCAG 1.4.1). */
|
|
232
|
+
.wt-switcher-current[data-attention="input"] {
|
|
233
|
+
box-shadow: inset 0 0 0 2px oklch(70% 0.18 25deg / 45%);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.wt-switcher-current[data-attention="input"]::after {
|
|
237
|
+
content: "";
|
|
238
|
+
position: absolute;
|
|
239
|
+
top: 4px;
|
|
240
|
+
right: 4px;
|
|
241
|
+
width: 8px;
|
|
242
|
+
height: 8px;
|
|
243
|
+
border-radius: 50%;
|
|
244
|
+
background: oklch(70% 0.18 25deg);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ===== Externally-driven key grid (tabbed presets, WS3) =====
|
|
248
|
+
In the tabbed presets the mobile toolbar hides its own toggle (the tab bar's
|
|
249
|
+
keyboard button drives it) and the grid docks above the bar on the right, to
|
|
250
|
+
the left of where scroll-to-bottom sits. It collapses to fully hidden, since
|
|
251
|
+
there is no in-grid toggle to keep visible. */
|
|
252
|
+
@media (pointer: coarse) {
|
|
253
|
+
.wt-toolbar-external .kb-toggle {
|
|
254
|
+
display: none;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.key-toolbar.wt-toolbar-external {
|
|
258
|
+
top: auto;
|
|
259
|
+
right: calc(var(--sp-3) + var(--touch-target) + var(--sp-2));
|
|
260
|
+
bottom: calc(
|
|
261
|
+
var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
|
|
262
|
+
);
|
|
263
|
+
grid-template-columns: repeat(4, var(--touch-target));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.key-toolbar.wt-toolbar-external.collapsed {
|
|
267
|
+
display: none;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ===== Expanded tab-list rows (tabs feature) =====
|
|
272
|
+
One row per OTHER tab in the expanded list: a stretched select target (dot +
|
|
273
|
+
label) with the close (x) laid inside at the right, mirroring the desktop
|
|
274
|
+
tab's merged chrome. */
|
|
275
|
+
.wt-switcher-row {
|
|
276
|
+
position: relative;
|
|
277
|
+
display: flex;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.wt-switcher-row-select {
|
|
281
|
+
flex: 1 1 auto;
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: var(--sp-2);
|
|
285
|
+
min-height: var(--touch-target);
|
|
286
|
+
/* Right padding reserves room for the close (x) overlaid inside the box. */
|
|
287
|
+
padding: 0 var(--touch-target) 0 var(--sp-2);
|
|
288
|
+
background: transparent;
|
|
289
|
+
color: var(--text);
|
|
290
|
+
border: none;
|
|
291
|
+
border-radius: var(--radius-inner);
|
|
292
|
+
font-family: var(--font-ui);
|
|
293
|
+
font-size: 0.875rem;
|
|
294
|
+
text-align: left;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
overflow: hidden;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@media (hover: hover) {
|
|
300
|
+
.wt-switcher-row-select:hover {
|
|
301
|
+
background: var(--tab-hover-bg);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Press / touch-hold feedback on a listed (inactive) tab: the same subtle fill
|
|
306
|
+
the "+" button uses (--tab-hover-bg), NOT the stronger active-tab fill, so an
|
|
307
|
+
inactive row never looks like the active one. */
|
|
308
|
+
.wt-switcher-row-select:active {
|
|
309
|
+
background: var(--tab-hover-bg);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.wt-switcher-row-label {
|
|
313
|
+
flex: 1 1 auto;
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
text-overflow: ellipsis;
|
|
316
|
+
white-space: nowrap;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* The close sits flush at the row's right edge (no gap), vertically centered. */
|
|
320
|
+
.wt-switcher-row-close {
|
|
321
|
+
position: absolute;
|
|
322
|
+
top: 50%;
|
|
323
|
+
right: 0;
|
|
324
|
+
transform: translateY(-50%);
|
|
325
|
+
color: color-mix(in oklch, var(--text) 55%, transparent);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Hover only on real hover pointers: on touch, :hover sticks after a tap, so
|
|
329
|
+
when closing a tab shifts the list under the finger the newly-under close
|
|
330
|
+
would latch white. Touch feedback comes from the surrounding row's :active. */
|
|
331
|
+
@media (hover: hover) {
|
|
332
|
+
.wt-switcher-row-close:hover {
|
|
333
|
+
color: var(--text);
|
|
334
|
+
}
|
|
335
|
+
}
|