@cplieger/web-terminal-ui 5.0.0 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/10-primitives.css +28 -1
- package/css/30-tabs.css +86 -23
- package/css/31-switcher.css +41 -25
- package/package.json +1 -1
- package/src/features/tabs/index.ts +19 -4
package/css/10-primitives.css
CHANGED
|
@@ -42,6 +42,33 @@
|
|
|
42
42
|
background: var(--text-active);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/* ===== Hover gating: any-hover, never hover =====
|
|
46
|
+
Every gated hover rule in this package uses @media (any-hover: hover). Do not
|
|
47
|
+
write @media (hover: hover) here.
|
|
48
|
+
|
|
49
|
+
What the gate is for: a bare touchscreen fires no pointer-leave, so a tapped
|
|
50
|
+
control keeps its :hover wash until the next tap somewhere else. What went
|
|
51
|
+
wrong: `hover` and `pointer` describe only the PRIMARY input, and iPadOS
|
|
52
|
+
reports hover: none / pointer: coarse whether or not a Magic Keyboard trackpad
|
|
53
|
+
is attached. So (hover: hover) is false on every touch-primary device,
|
|
54
|
+
INCLUDING the ones driving a real pointer.
|
|
55
|
+
|
|
56
|
+
For the coarse-only controls that made the gate not a tradeoff but a deletion,
|
|
57
|
+
and it shipped that way: .wt-tab-kb renders only under @media (pointer: coarse)
|
|
58
|
+
(30-tabs.css) and the whole .wt-switcher subtree only under (pointer: coarse)
|
|
59
|
+
plus .wt-narrow (31-switcher.css). Pairing either with (hover: hover) makes the
|
|
60
|
+
two conditions complementary, so the hover rule was unreachable on every device
|
|
61
|
+
that exists. Measured against the deployed bundle at iPad width: the keyboard
|
|
62
|
+
button is a visible 44x44 while a forced :hover leaves its background at
|
|
63
|
+
rgba(0, 0, 0, 0), where the ungated .wt-tab-new beside it resolves to
|
|
64
|
+
--tab-active-bg.
|
|
65
|
+
|
|
66
|
+
`any-hover` asks the question the gate meant to ask — can ANY attached device
|
|
67
|
+
hover — so a bare iPhone/iPad still answers none (no wash, nothing to stick)
|
|
68
|
+
while a trackpad or Pencil answers hover and brings a real pointer-leave with
|
|
69
|
+
it. Same defect class as Tailwind's hoverOnlyWhenSupported
|
|
70
|
+
(tailwindlabs/tailwindcss#11109). */
|
|
71
|
+
|
|
45
72
|
/* Keyboard-focus ring for every chrome control (all interactive chrome is a
|
|
46
73
|
real <button>: tabs, +, keyboard, switcher rows, menu items, the FAB).
|
|
47
74
|
:focus-visible only — a tap/click never shows the ring (the UA heuristic),
|
|
@@ -147,7 +174,7 @@
|
|
|
147
174
|
font-weight: 600;
|
|
148
175
|
}
|
|
149
176
|
|
|
150
|
-
@media (hover: hover) {
|
|
177
|
+
@media (any-hover: hover) {
|
|
151
178
|
:where(.wt-root) .wt-fatal-reload:hover {
|
|
152
179
|
background: color-mix(in oklch, var(--accent), var(--text) 12%);
|
|
153
180
|
}
|
package/css/30-tabs.css
CHANGED
|
@@ -75,6 +75,22 @@
|
|
|
75
75
|
width: 300px;
|
|
76
76
|
flex: 0 1 auto;
|
|
77
77
|
min-width: 100px;
|
|
78
|
+
/* ONE height for every control in the strip: the chip, the "+" and the
|
|
79
|
+
keyboard button all resolve to --touch-target, so a chip and a button can
|
|
80
|
+
never disagree about how tall the row is. Before this the chip's height was
|
|
81
|
+
whatever its tallest child happened to be, and the "+" hard-coded a COPY of
|
|
82
|
+
that arithmetic (close button + padding + border) — two sources of truth
|
|
83
|
+
for one row, which drifted the moment a child's box grew: a browser without
|
|
84
|
+
text-box support kept the label's untrimmed line box (31px, past the 24px
|
|
85
|
+
close button), so the chip rendered 49px tall next to a 42px "+" and a 44px
|
|
86
|
+
keyboard button. --touch-target is also the tap-target floor the rest of
|
|
87
|
+
the chrome already respects, including this chip's mobile counterpart
|
|
88
|
+
(.wt-switcher-current in 31-switcher.css, same declaration).
|
|
89
|
+
|
|
90
|
+
min-height, not height: the floor governs today (the tallest child is the
|
|
91
|
+
1.5rem close button, which fits with room to spare), and taller future
|
|
92
|
+
content grows the row rather than overflowing a fixed box. */
|
|
93
|
+
min-height: var(--touch-target);
|
|
78
94
|
padding: var(--sp-2);
|
|
79
95
|
/* A transparent border is reserved on every tab so toggling the inactive
|
|
80
96
|
outline (below) never shifts the strip's layout. */
|
|
@@ -112,9 +128,8 @@
|
|
|
112
128
|
|
|
113
129
|
/* Press (mousedown / touch-hold): one rung past the hover lift — same recipe,
|
|
114
130
|
deeper mix — so hover -> press reads as a single dimension. After the hover
|
|
115
|
-
rule so it wins while both apply.
|
|
116
|
-
|
|
117
|
-
it is. */
|
|
131
|
+
rule so it wins while both apply. The active chip has its own pair (above),
|
|
132
|
+
mixed against the active fill instead of the inactive one. */
|
|
118
133
|
:where(.wt-root) .wt-tab:not(.wt-tab-active):active {
|
|
119
134
|
background: color-mix(in oklch, var(--tab-bg), var(--text) 25%);
|
|
120
135
|
color: var(--tab-active-fg);
|
|
@@ -129,6 +144,35 @@
|
|
|
129
144
|
color: var(--tab-active-fg);
|
|
130
145
|
}
|
|
131
146
|
|
|
147
|
+
/* The active tab answers a press too. Clicking it selects nothing, which is why
|
|
148
|
+
it was originally left inert — but selection is not the only thing a chip does:
|
|
149
|
+
it is a drag handle for reordering, a right-click target for the tab menu, a
|
|
150
|
+
double-tap target for rename, and it carries a close button. A control with
|
|
151
|
+
four affordances and no pointer feedback reads as decoration, and it was the
|
|
152
|
+
only chip in the strip that gave none (the same gap that got the keyboard
|
|
153
|
+
button its hover wash).
|
|
154
|
+
|
|
155
|
+
The lift is mixed with --tab-active-fg rather than --text: this is a lighter
|
|
156
|
+
press over an already-filled control (the .wt-switcher-current / .wt-tab-new
|
|
157
|
+
recipe), not a wash over a transparent one, so it follows the filled-surface
|
|
158
|
+
vocabulary and stays themeable through one token. It is a rung subtler than the
|
|
159
|
+
inactive chip's 15%/25% because it starts from an already-lifted fill.
|
|
160
|
+
|
|
161
|
+
Hover is gated (see the any-hover note in 10-primitives.css). Tapping an
|
|
162
|
+
INACTIVE chip makes it active, so a touchscreen's sticky :hover leaves with the
|
|
163
|
+
class; tapping the ACTIVE chip changes nothing, so an ungated hover would stay
|
|
164
|
+
lit until the next tap somewhere else. Touch feedback there is :active, which is
|
|
165
|
+
correct. */
|
|
166
|
+
@media (any-hover: hover) {
|
|
167
|
+
:where(.wt-root) .wt-tab-active:hover {
|
|
168
|
+
background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 12%);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
:where(.wt-root) .wt-tab-active:active {
|
|
173
|
+
background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 22%);
|
|
174
|
+
}
|
|
175
|
+
|
|
132
176
|
/* The label flexes to fill the fixed-width tab and truncates with an ellipsis
|
|
133
177
|
(min-width: 0 lets a flex item shrink below its content width). */
|
|
134
178
|
:where(.wt-root) .wt-tab-label {
|
|
@@ -137,7 +181,9 @@
|
|
|
137
181
|
overflow: hidden;
|
|
138
182
|
white-space: nowrap;
|
|
139
183
|
text-overflow: ellipsis;
|
|
140
|
-
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Center the INK, not the line box: UI fonts carry far more ascent than
|
|
141
187
|
descent (the -apple-system/Noto Sans stack at 13px: 14px over / 4px under
|
|
142
188
|
the baseline), so the flex-centered line box leaves the visible text ~1.5px
|
|
143
189
|
low — reading as baseline-aligned with the activity dot's bottom instead of
|
|
@@ -159,15 +205,26 @@
|
|
|
159
205
|
measured: 3.3px of p/y/g/j tail and 2.3px of an acute accent, i.e. "pygmy
|
|
160
206
|
jaguar Ápple" rendered as "pyqmy iaquar Apple"). 0.5em restores the ink
|
|
161
207
|
overhang the trim removes from the box; being symmetric it does not move
|
|
162
|
-
the centered band, and being em-relative it tracks the font size.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
box
|
|
208
|
+
the centered band, and being em-relative it tracks the font size.
|
|
209
|
+
|
|
210
|
+
The pair is @supports-GATED because the padding is the trim's allowance and
|
|
211
|
+
nothing else: an untrimmed line box already contains that overhang, so
|
|
212
|
+
adding the padding there is not "harmless slack" (this comment used to
|
|
213
|
+
claim it was) — it grew the label's box to 31px, past the 24px close button,
|
|
214
|
+
and a browser without text-box support rendered 49px tab chips beside a
|
|
215
|
+
42px "+" and a 44px keyboard button. The chip's own --touch-target floor
|
|
216
|
+
pins the row height either way; the gate is what keeps the label from
|
|
217
|
+
pushing PAST it. Gating the shorthand (not the longhands) is deliberate: an
|
|
218
|
+
engine that ships text-box-trim without the shorthand does not apply the
|
|
219
|
+
declaration we actually write, so it must take the untrimmed branch too.
|
|
166
220
|
|
|
167
221
|
Shared by all three chip labels (this one + the two switcher labels in
|
|
168
222
|
31-switcher.css). */
|
|
169
|
-
|
|
170
|
-
|
|
223
|
+
@supports (text-box: trim-both ex alphabetic) {
|
|
224
|
+
:where(.wt-root) .wt-tab-label {
|
|
225
|
+
text-box: trim-both ex alphabetic;
|
|
226
|
+
padding-block: 0.5em;
|
|
227
|
+
}
|
|
171
228
|
}
|
|
172
229
|
|
|
173
230
|
/* A little breathing room after the activity dot, before the label. */
|
|
@@ -241,12 +298,16 @@
|
|
|
241
298
|
align-items: center;
|
|
242
299
|
justify-content: center;
|
|
243
300
|
flex: none;
|
|
244
|
-
/*
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
301
|
+
/* A SQUARE box of exactly the strip's control height, so it lines up with the
|
|
302
|
+
tab chips beside it and with the keyboard button after it (the .wt-btn
|
|
303
|
+
primitive's 44px box, same token). Sized explicitly on both axes rather
|
|
304
|
+
than by aspect-ratio: a stretched flex item ignores aspect-ratio against
|
|
305
|
+
its SVG's intrinsic width. It used to restate the chip's height arithmetic
|
|
306
|
+
(close button + padding + border) as a calc(), which is how it ended up
|
|
307
|
+
7px short of a chip whose content grew — see the min-height note on
|
|
308
|
+
.wt-tab. */
|
|
309
|
+
width: var(--touch-target);
|
|
310
|
+
height: var(--touch-target);
|
|
250
311
|
padding: 0;
|
|
251
312
|
background: transparent;
|
|
252
313
|
border: none;
|
|
@@ -322,13 +383,15 @@
|
|
|
322
383
|
|
|
323
384
|
/* Hover wash, matching every other bar control (.wt-tab-new, the switcher's
|
|
324
385
|
buttons) — without it this was the one control in the chrome that gave no
|
|
325
|
-
pointer feedback at all. Gated
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
386
|
+
pointer feedback at all. Gated on any-hover, NOT hover: this button renders only
|
|
387
|
+
under (pointer: coarse) below, and a touch-primary device reports hover: none
|
|
388
|
+
even with a trackpad attached, so a (hover: hover) gate here made the wash
|
|
389
|
+
unreachable on every device rather than restricting it to hovering ones (the
|
|
390
|
+
note in 10-primitives.css has the measurement). On a bare touchscreen any-hover
|
|
391
|
+
is none, so the feedback there is still :active, from .wt-btn. Before
|
|
392
|
+
.wt-active / .wt-armed so those toggled fills win on a hovered button (equal
|
|
393
|
+
specificity, source order decides). */
|
|
394
|
+
@media (any-hover: hover) {
|
|
332
395
|
:where(.wt-root) .wt-tab-kb:hover {
|
|
333
396
|
background: var(--tab-hover-bg);
|
|
334
397
|
}
|
package/css/31-switcher.css
CHANGED
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
color: color-mix(in oklch, var(--text) 55%, transparent);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
@media (hover: hover) {
|
|
151
|
+
@media (any-hover: hover) {
|
|
152
152
|
:where(.wt-root) .wt-switcher-current-close:hover {
|
|
153
153
|
color: var(--text);
|
|
154
154
|
}
|
|
@@ -178,11 +178,16 @@
|
|
|
178
178
|
text-overflow: ellipsis;
|
|
179
179
|
white-space: nowrap;
|
|
180
180
|
text-align: left;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Ink-centering trim + its mandatory descender allowance, same as .wt-tab-label
|
|
184
|
+
(see 30-tabs.css for why `ex`, why the padding is not optional, and why the
|
|
185
|
+
pair is @supports-gated rather than left to fall back). */
|
|
186
|
+
@supports (text-box: trim-both ex alphabetic) {
|
|
187
|
+
:where(.wt-root) .wt-switcher-label {
|
|
188
|
+
text-box: trim-both ex alphabetic;
|
|
189
|
+
padding-block: 0.5em;
|
|
190
|
+
}
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
/* Keyboard button: hidden until a keyboardToggle is wired; filled while the key
|
|
@@ -211,10 +216,11 @@
|
|
|
211
216
|
overflow: hidden;
|
|
212
217
|
}
|
|
213
218
|
|
|
214
|
-
/* Hover wash like the switch and "+" buttons beside it; gated
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
/* Hover wash like the switch and "+" buttons beside it; gated on any-hover for
|
|
220
|
+
the same reason they are (see the note in 10-primitives.css — this whole
|
|
221
|
+
subtree renders only on a coarse pointer, where hover: hover is always false).
|
|
222
|
+
Before .wt-active / .wt-armed so those fills win on a hovered button. */
|
|
223
|
+
@media (any-hover: hover) {
|
|
218
224
|
:where(.wt-root) .wt-switcher-kb:hover {
|
|
219
225
|
background: var(--tab-hover-bg);
|
|
220
226
|
}
|
|
@@ -287,7 +293,7 @@
|
|
|
287
293
|
pointer-events: none;
|
|
288
294
|
}
|
|
289
295
|
|
|
290
|
-
@media (hover: hover) {
|
|
296
|
+
@media (any-hover: hover) {
|
|
291
297
|
:where(.wt-root) .wt-switcher-switch:hover {
|
|
292
298
|
background: var(--tab-hover-bg);
|
|
293
299
|
}
|
|
@@ -315,15 +321,17 @@
|
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
/* Mobile "+": the primary action, carried in the accent colour so it reads as
|
|
318
|
-
permanent and prominent. Hover fill is gated
|
|
319
|
-
:
|
|
320
|
-
press feedback
|
|
324
|
+
permanent and prominent. Hover fill is gated on any-hover (see the note in
|
|
325
|
+
10-primitives.css): on a bare touchscreen any-hover is none, so the purple
|
|
326
|
+
cannot linger after a tap and the press feedback comes from :active; a trackpad
|
|
327
|
+
or Pencil reports hover and gets the wash, which a (hover: hover) gate denied it.
|
|
328
|
+
*/
|
|
321
329
|
:where(.wt-root) .wt-switcher-new {
|
|
322
330
|
flex: none;
|
|
323
331
|
color: var(--accent);
|
|
324
332
|
}
|
|
325
333
|
|
|
326
|
-
@media (hover: hover) {
|
|
334
|
+
@media (any-hover: hover) {
|
|
327
335
|
:where(.wt-root) .wt-switcher-new:hover {
|
|
328
336
|
background: var(--tab-hover-bg);
|
|
329
337
|
}
|
|
@@ -413,7 +421,7 @@
|
|
|
413
421
|
overflow: hidden;
|
|
414
422
|
}
|
|
415
423
|
|
|
416
|
-
@media (hover: hover) {
|
|
424
|
+
@media (any-hover: hover) {
|
|
417
425
|
:where(.wt-root) .wt-switcher-row-select:hover {
|
|
418
426
|
background: color-mix(in oklch, var(--tab-bg), var(--text) 15%);
|
|
419
427
|
}
|
|
@@ -432,11 +440,16 @@
|
|
|
432
440
|
overflow: hidden;
|
|
433
441
|
text-overflow: ellipsis;
|
|
434
442
|
white-space: nowrap;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* Ink-centering trim + its mandatory descender allowance, same as .wt-tab-label
|
|
446
|
+
(see 30-tabs.css for why `ex`, why the padding is not optional, and why the
|
|
447
|
+
pair is @supports-gated rather than left to fall back). */
|
|
448
|
+
@supports (text-box: trim-both ex alphabetic) {
|
|
449
|
+
:where(.wt-root) .wt-switcher-row-label {
|
|
450
|
+
text-box: trim-both ex alphabetic;
|
|
451
|
+
padding-block: 0.5em;
|
|
452
|
+
}
|
|
440
453
|
}
|
|
441
454
|
|
|
442
455
|
/* The close sits flush at the row's right edge (no gap), vertically centered. */
|
|
@@ -448,10 +461,13 @@
|
|
|
448
461
|
color: color-mix(in oklch, var(--text) 55%, transparent);
|
|
449
462
|
}
|
|
450
463
|
|
|
451
|
-
/* Hover
|
|
452
|
-
when closing a tab shifts the list under the
|
|
453
|
-
|
|
454
|
-
|
|
464
|
+
/* Hover gated on any-hover (see the note in 10-primitives.css): on a bare
|
|
465
|
+
touchscreen any-hover is none, so when closing a tab shifts the list under the
|
|
466
|
+
finger the newly-under close cannot latch white, and the feedback comes from the
|
|
467
|
+
surrounding row's :active. A trackpad or Pencil reports hover and gets the
|
|
468
|
+
brighten, which a (hover: hover) gate denied it — this subtree only ever renders
|
|
469
|
+
on a coarse-primary device. */
|
|
470
|
+
@media (any-hover: hover) {
|
|
455
471
|
:where(.wt-root) .wt-switcher-row-close:hover {
|
|
456
472
|
color: var(--text);
|
|
457
473
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -2445,8 +2445,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2445
2445
|
void close_(activeId);
|
|
2446
2446
|
}
|
|
2447
2447
|
});
|
|
2448
|
-
// Dismiss the tab context menu on an outside click, on Escape, and on
|
|
2449
|
-
// scroll
|
|
2448
|
+
// Dismiss the tab context menu on an outside click, on Escape, and on a
|
|
2449
|
+
// scroll of the strip itself (which moves the chip the menu is anchored to).
|
|
2450
2450
|
const onDocClickMenu = (): void => {
|
|
2451
2451
|
// The trailing click of the long-press that just opened the menu is that
|
|
2452
2452
|
// gesture's own release, not a click-away.
|
|
@@ -2466,8 +2466,23 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2466
2466
|
}
|
|
2467
2467
|
};
|
|
2468
2468
|
document.addEventListener("contextmenu", onDocContextMenu);
|
|
2469
|
-
|
|
2470
|
-
|
|
2469
|
+
// Only a scroll that moves the menu's ANCHOR dismisses it, which is the
|
|
2470
|
+
// strip's own horizontal scroller and nothing else: the menu is placed
|
|
2471
|
+
// root-relative (menu-position.ts rebases onto the offsetParent), so any
|
|
2472
|
+
// scroll that moves .wt-root moves the menu with it, and the terminal's
|
|
2473
|
+
// scroller does not move the chip at all.
|
|
2474
|
+
//
|
|
2475
|
+
// The target check is load-bearing, not a micro-optimization. This listener
|
|
2476
|
+
// is capture-phase on window, so it sees EVERY scroll in the document —
|
|
2477
|
+
// including the terminal surface auto-scrolling to the bottom on each chunk
|
|
2478
|
+
// of output. Without the check, an agent printing into the TUI closed the
|
|
2479
|
+
// tab menu within a frame of it opening, which is unusable on exactly the
|
|
2480
|
+
// screen the menu exists for (a busy multi-agent strip).
|
|
2481
|
+
const onScrollMenu = (e: Event): void => {
|
|
2482
|
+
const target = e.target;
|
|
2483
|
+
if (target instanceof Element && target.closest(".wt-tab-scroll")) {
|
|
2484
|
+
hideTabMenu();
|
|
2485
|
+
}
|
|
2471
2486
|
};
|
|
2472
2487
|
window.addEventListener("scroll", onScrollMenu, true);
|
|
2473
2488
|
const offMenuKey = ctx.registerKeydown((ev) => {
|