@cplieger/web-terminal-ui 4.7.0 → 4.8.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/css/00-tokens.css +18 -4
- package/css/30-tabs.css +104 -45
- package/css/31-switcher.css +10 -4
- package/package.json +1 -1
- package/src/features/context-menu.ts +9 -12
- package/src/features/menu-position.ts +29 -0
- package/src/features/tabs/index.ts +30 -1
package/css/00-tokens.css
CHANGED
|
@@ -45,10 +45,24 @@
|
|
|
45
45
|
); /* subtle edge: the fill lightened + slightly desaturated, matching the inactive chip's faint border rather than a vivid accent outline */
|
|
46
46
|
|
|
47
47
|
/* Status-dot palette (the activity vocabulary painted in 30-tabs.css; the
|
|
48
|
-
switcher notification dot reuses the same rules). The working and
|
|
49
|
-
|
|
50
|
-
recolors dot +
|
|
51
|
-
|
|
48
|
+
switcher notification dot reuses the same rules). The working wave and the
|
|
49
|
+
input ring each derive from their own token in 30-tabs.css, so one override
|
|
50
|
+
recolors dot + surround together.
|
|
51
|
+
|
|
52
|
+
The trio is the conventional progress scheme — blue in flight, green
|
|
53
|
+
finished, orange waiting on you — which is why working is a LITERAL copy of
|
|
54
|
+
the default --accent blue rather than var(--accent): a consumer that
|
|
55
|
+
rethemes its brand accent to (say) a red should not have its "working" dot
|
|
56
|
+
follow, and a status vocabulary that shifts hue per consumer stops being a
|
|
57
|
+
vocabulary. The value being equal to the default accent is a deliberate
|
|
58
|
+
coincidence of taste, not a dependency.
|
|
59
|
+
|
|
60
|
+
Working shares the accent's hue family, so on the ACTIVE tab it sits on an
|
|
61
|
+
accent-derived fill (--tab-active-bg): measured 5.2:1 against it, and 8.0:1
|
|
62
|
+
against the bar, both clear of the 3:1 non-text minimum. Hue still never
|
|
63
|
+
carries state on its own (the wave's motion, and the donut it becomes under
|
|
64
|
+
prefers-reduced-motion, are the non-hue cues per WCAG 1.4.1). */
|
|
65
|
+
--status-working: #52a9fe; /* agent working (waves): the same blue as the default --accent */
|
|
52
66
|
--status-done: #22c55e; /* turn finished: solid green */
|
|
53
67
|
--status-input: #fb923c; /* action required: solid orange + ring */
|
|
54
68
|
--radius: 0.75rem;
|
package/css/30-tabs.css
CHANGED
|
@@ -137,17 +137,37 @@
|
|
|
137
137
|
overflow: hidden;
|
|
138
138
|
white-space: nowrap;
|
|
139
139
|
text-overflow: ellipsis;
|
|
140
|
-
/* Center the INK, not the line box: UI fonts carry more ascent than
|
|
141
|
-
(Noto Sans at 13px: 14px over / 4px under
|
|
142
|
-
centered line box leaves the visible text ~1.5px
|
|
143
|
-
baseline-aligned with the activity dot's bottom instead of
|
|
144
|
-
Trimming the box
|
|
145
|
-
the
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
/* Center the INK, not the line box: UI fonts carry far more ascent than
|
|
141
|
+
descent (the -apple-system/Noto Sans stack at 13px: 14px over / 4px under
|
|
142
|
+
the baseline), so the flex-centered line box leaves the visible text ~1.5px
|
|
143
|
+
low — reading as baseline-aligned with the activity dot's bottom instead of
|
|
144
|
+
its center. Trimming the box makes flexbox center the glyph band instead of
|
|
145
|
+
the font's metrics box.
|
|
146
|
+
|
|
147
|
+
`ex`, not `cap`, is the over edge, because a tab title is sentence case and
|
|
148
|
+
lowercase-dominant ("workspace", "refactor the auth module"). Centering the
|
|
149
|
+
x-height/baseline band centers the total ink of such a string, since a
|
|
150
|
+
Latin font's ascender overshoot above x-height is about its descender depth
|
|
151
|
+
below the baseline (measured on this stack at 13px: 3.0px over vs 3.0px
|
|
152
|
+
under, ink center 0.02px off the chip's center). `cap alphabetic` — the
|
|
153
|
+
right choice for an ALL-CAPS label — leaves lowercase text 1.14px low,
|
|
154
|
+
which is the original complaint barely moved.
|
|
155
|
+
|
|
156
|
+
padding-block is LOAD-BEARING, not spacing: trimming puts the box's under
|
|
157
|
+
edge ON the baseline, and `overflow: hidden` clips at the padding edge, so
|
|
158
|
+
with no padding every descender and every tall diacritic is cut off (
|
|
159
|
+
measured: 3.3px of p/y/g/j tail and 2.3px of an acute accent, i.e. "pygmy
|
|
160
|
+
jaguar Ápple" rendered as "pyqmy iaquar Apple"). 0.5em restores the ink
|
|
161
|
+
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. The
|
|
163
|
+
grown box (~20px) still sits inside the close button's 1.5rem, so the tab
|
|
164
|
+
height is unchanged. A browser without text-box support keeps the untrimmed
|
|
165
|
+
box, where the padding is harmless slack.
|
|
166
|
+
|
|
167
|
+
Shared by all three chip labels (this one + the two switcher labels in
|
|
149
168
|
31-switcher.css). */
|
|
150
|
-
text-box: trim-both
|
|
169
|
+
text-box: trim-both ex alphabetic;
|
|
170
|
+
padding-block: 0.5em;
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
/* A little breathing room after the activity dot, before the label. */
|
|
@@ -535,17 +555,18 @@
|
|
|
535
555
|
/* Agent working: a solid disc with two animated cues (the status-badge
|
|
536
556
|
"processing" idiom — Ant Design's processing badge, the classic CSS
|
|
537
557
|
live-indicator ripple): the disc keeps full opacity and gently GLOWS (its
|
|
538
|
-
colors brighten mid-beat), while a ::after
|
|
558
|
+
colors brighten mid-beat), while a ::after WAVE travels outward off the
|
|
539
559
|
disc's edge and dissolves, re-emitted each cycle. No static ring at rest —
|
|
540
|
-
the disc + live
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
560
|
+
the disc + live wave ARE the working read. The wave's soft feathered edge is
|
|
561
|
+
the second axis that separates working from input: input is a HARD-edged 2px
|
|
562
|
+
ring, held still (see input below), so the pair differs by both motion and
|
|
563
|
+
edge quality, and the presence of any surround separates both from done's
|
|
564
|
+
bare disc. The old single-layer pulse faded disc + ring together (opacity on
|
|
565
|
+
the element), which read as a blink and coupled the layers — an element's
|
|
566
|
+
opacity always multiplies onto its pseudos, so true independence requires
|
|
567
|
+
animating disc COLORS, not element opacity. Server-side this is driven by the
|
|
568
|
+
OSC 9;4 progress signal, so it tracks agent activity, not the user typing at
|
|
569
|
+
the prompt. */
|
|
549
570
|
:where(.wt-root) .wt-status-dot[data-status="working"] {
|
|
550
571
|
position: relative;
|
|
551
572
|
background: var(--status-working);
|
|
@@ -553,40 +574,58 @@
|
|
|
553
574
|
animation: wt-working-glow 1.2s ease-in-out infinite;
|
|
554
575
|
}
|
|
555
576
|
|
|
556
|
-
/* The ripple layer:
|
|
557
|
-
-2px, the 9->13px
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
577
|
+
/* The ripple layer: a feathered radial BAND (not a hairline border) that starts
|
|
578
|
+
hugging the disc's edge (inset -2px, the 9->13px box) and then travels
|
|
579
|
+
outward, dissolving. The band is painted as a radial-gradient — soft on both
|
|
580
|
+
edges, so it reads as a wave of light leaving the disc rather than a ring
|
|
581
|
+
outline shrinking in opacity; scaling the layer scales the feather with it,
|
|
582
|
+
which is what makes the wave visibly dissipate as it grows. transform +
|
|
583
|
+
opacity only, so it composites without repaints (a gradient background is
|
|
584
|
+
rasterized once, unlike an animated box-shadow spread, which repaints every
|
|
585
|
+
frame). It exists only in the working state — no stale layer to pin when a
|
|
586
|
+
dot changes status. (The switcher notification dot never shows working —
|
|
587
|
+
input/done only — so its position: absolute placement never meets this
|
|
588
|
+
rule's position: relative.)
|
|
589
|
+
|
|
590
|
+
The final scale is deliberately capped at today's 27px footprint: the mobile
|
|
591
|
+
overview row (.wt-switcher-row-select) clips at overflow: hidden 12.5px from
|
|
592
|
+
the dot's center, and a wave wider than ~25px picks up a visible straight
|
|
593
|
+
edge there. Visibility comes from the band's weight and timing, not size. */
|
|
563
594
|
:where(.wt-root) .wt-status-dot[data-status="working"]::after {
|
|
564
595
|
content: "";
|
|
565
596
|
position: absolute;
|
|
566
597
|
inset: -2px;
|
|
567
|
-
border: 2px solid var(--status-working);
|
|
568
598
|
border-radius: 50%;
|
|
569
|
-
|
|
599
|
+
background: radial-gradient(
|
|
600
|
+
closest-side,
|
|
601
|
+
transparent 44%,
|
|
602
|
+
var(--status-working) 76%,
|
|
603
|
+
transparent 100%
|
|
604
|
+
);
|
|
605
|
+
animation: wt-working-ripple 1.2s linear infinite;
|
|
570
606
|
}
|
|
571
607
|
|
|
572
608
|
/* Agent done / ready: solid disc (--status-done), no motion. The default green
|
|
573
|
-
matches Kiro IDE's "Ready" (#22c55e)
|
|
574
|
-
|
|
575
|
-
motion
|
|
609
|
+
matches Kiro IDE's "Ready" (#22c55e). It sits at almost exactly working's
|
|
610
|
+
default lightness now that working is the accent blue (both ~72% OKLab L), so
|
|
611
|
+
the pair separates by hue plus the wave's motion rather than by lightness —
|
|
612
|
+
and under prefers-reduced-motion by shape, since working becomes a donut. */
|
|
576
613
|
:where(.wt-root) .wt-status-dot[data-status="done"] {
|
|
577
614
|
background: var(--status-done);
|
|
578
615
|
border-color: var(--status-done);
|
|
579
616
|
}
|
|
580
617
|
|
|
581
|
-
/* User action required (permission / approval):
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
618
|
+
/* User action required (permission / approval): a solid dot inside a static,
|
|
619
|
+
HARD-edged 2px ring, motionless (--status-input; the default orange is
|
|
620
|
+
shifted away from the old amber so it separates from both blue working and
|
|
621
|
+
green done). The stillness plus the hard edge is the message: the
|
|
622
|
+
agent's turn is mid-flight but blocked on you, and the surround is a fixed
|
|
623
|
+
outline rather than working's soft travelling wave. (Before the wave landed,
|
|
624
|
+
working's ripple was a 2px ring too and this state was literally that ping
|
|
625
|
+
frozen at its birth frame; the two now differ by edge quality as well as
|
|
626
|
+
motion, which reads more distinctly at 9px.) The ring derives from the same
|
|
627
|
+
token as the dot, so a theme override recolors both together. It persists
|
|
628
|
+
until you act on or switch into the tab. */
|
|
590
629
|
:where(.wt-root) .wt-status-dot[data-status="input"] {
|
|
591
630
|
background: var(--status-input);
|
|
592
631
|
border-color: var(--status-input);
|
|
@@ -621,13 +660,33 @@
|
|
|
621
660
|
}
|
|
622
661
|
|
|
623
662
|
/* The ripple wave: one emission per cycle, launching off the disc's edge and
|
|
624
|
-
dissolving by ~2.1x.
|
|
625
|
-
|
|
626
|
-
|
|
663
|
+
dissolving by ~2.1x. Two things are load-bearing and were both wrong in the
|
|
664
|
+
first version (the wave was there but nobody could see it):
|
|
665
|
+
|
|
666
|
+
- The travel is LINEAR while the opacity is shaped separately. An ease-in-out
|
|
667
|
+
shared by both properties spends the slow opening on top of the disc, where
|
|
668
|
+
the wave is indistinguishable from it, and then accelerates away exactly as
|
|
669
|
+
the opacity collapses — so the wave was only ever visible while it was
|
|
670
|
+
still merged with the disc. Constant-speed travel plus its own opacity
|
|
671
|
+
envelope keeps the wave legible across most of the cycle.
|
|
672
|
+
- The wave is BRIGHT at birth (0.95 after a 12% ramp-in, not 0.3) and holds
|
|
673
|
+
usable weight through the travel. The ramp-in is short enough to read as
|
|
674
|
+
instant while stopping the emission from popping in.
|
|
675
|
+
|
|
676
|
+
The disc's own glow shares this 1.2s period so the two cues stay phase-locked;
|
|
677
|
+
change both or neither. */
|
|
627
678
|
@keyframes wt-working-ripple {
|
|
628
679
|
0% {
|
|
629
680
|
transform: scale(1);
|
|
630
|
-
opacity: 0.
|
|
681
|
+
opacity: 0.15;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
12% {
|
|
685
|
+
opacity: 0.95;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
70% {
|
|
689
|
+
opacity: 0.45;
|
|
631
690
|
}
|
|
632
691
|
|
|
633
692
|
100% {
|
package/css/31-switcher.css
CHANGED
|
@@ -178,8 +178,11 @@
|
|
|
178
178
|
text-overflow: ellipsis;
|
|
179
179
|
white-space: nowrap;
|
|
180
180
|
text-align: left;
|
|
181
|
-
/* Ink-centering trim
|
|
182
|
-
|
|
181
|
+
/* Ink-centering trim + its mandatory descender allowance, same as
|
|
182
|
+
.wt-tab-label (see 30-tabs.css for why `ex` and why the padding is not
|
|
183
|
+
optional). */
|
|
184
|
+
text-box: trim-both ex alphabetic;
|
|
185
|
+
padding-block: 0.5em;
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
/* Keyboard button: hidden until a keyboardToggle is wired; filled while the key
|
|
@@ -429,8 +432,11 @@
|
|
|
429
432
|
overflow: hidden;
|
|
430
433
|
text-overflow: ellipsis;
|
|
431
434
|
white-space: nowrap;
|
|
432
|
-
/* Ink-centering trim
|
|
433
|
-
|
|
435
|
+
/* Ink-centering trim + its mandatory descender allowance, same as
|
|
436
|
+
.wt-tab-label (see 30-tabs.css for why `ex` and why the padding is not
|
|
437
|
+
optional). */
|
|
438
|
+
text-box: trim-both ex alphabetic;
|
|
439
|
+
padding-block: 0.5em;
|
|
434
440
|
}
|
|
435
441
|
|
|
436
442
|
/* The close sits flush at the row's right edge (no gap), vertically centered. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
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",
|
|
@@ -38,19 +38,16 @@
|
|
|
38
38
|
|
|
39
39
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
40
40
|
import type { ClipboardApi } from "./clipboard.js";
|
|
41
|
-
import { placeMenuAt } from "./menu-position.js";
|
|
41
|
+
import { createClickSwallow, placeMenuAt } from "./menu-position.js";
|
|
42
42
|
|
|
43
43
|
// Slightly longer than the browser's ~500ms native long-press so a native word
|
|
44
44
|
// selection (which we defer to) has registered — and fired selectionchange —
|
|
45
45
|
// before this fires.
|
|
46
46
|
const LONG_PRESS_MS = 550;
|
|
47
47
|
const LONG_PRESS_MOVE_PX = 10;
|
|
48
|
-
// Viewport clamping
|
|
49
|
-
// point-anchored
|
|
50
|
-
//
|
|
51
|
-
// the same gesture emits on finger-release for this window, so the menu does not
|
|
52
|
-
// immediately dismiss itself (the classic contextmenu-then-touchend race).
|
|
53
|
-
const SWALLOW_MS = 350;
|
|
48
|
+
// Viewport clamping, the flip-above-the-fingertip gap, and the trailing-click
|
|
49
|
+
// swallow all live in the shared point-anchored menu module (menu-position.ts),
|
|
50
|
+
// shared with the tab menu.
|
|
54
51
|
|
|
55
52
|
export interface ContextMenuOptions {
|
|
56
53
|
/** The clipboard feature value, so the menu can offer Copy/Paste through its
|
|
@@ -98,8 +95,8 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
98
95
|
// Computed once: on an Apple touch device, defer a touch contextmenu to
|
|
99
96
|
// native text selection (see isAppleTouchDevice).
|
|
100
97
|
const appleTouch = isAppleTouchDevice();
|
|
101
|
-
//
|
|
102
|
-
|
|
98
|
+
// Swallows the trailing click a touch long-press emits on release.
|
|
99
|
+
const swallow = createClickSwallow();
|
|
103
100
|
// A native selection was made during the current touch press (tracked via
|
|
104
101
|
// selectionchange). Gates the touch menu: a selected long-press belongs to
|
|
105
102
|
// the native callout, an empty one to us (Paste).
|
|
@@ -212,9 +209,9 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
212
209
|
menu.classList.add("visible");
|
|
213
210
|
placeMenuAt(menu, x, y);
|
|
214
211
|
// A touch long-press is immediately followed by a synthetic click on
|
|
215
|
-
// release; swallow it so the menu stays open
|
|
212
|
+
// release; swallow it so the menu stays open.
|
|
216
213
|
if (touch) {
|
|
217
|
-
|
|
214
|
+
swallow.arm();
|
|
218
215
|
}
|
|
219
216
|
}
|
|
220
217
|
|
|
@@ -295,7 +292,7 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
295
292
|
|
|
296
293
|
const onDocClick = (): void => {
|
|
297
294
|
// Swallow the release click of the long-press that just opened the menu.
|
|
298
|
-
if (
|
|
295
|
+
if (swallow.swallowing()) {
|
|
299
296
|
return;
|
|
300
297
|
}
|
|
301
298
|
hide();
|
|
@@ -13,6 +13,35 @@ const EDGE_MARGIN = 8;
|
|
|
13
13
|
* hidden under the fingertip that long-pressed near the bottom edge. */
|
|
14
14
|
const FLIP_GAP = 16;
|
|
15
15
|
|
|
16
|
+
/** After a touch long-press opens a menu, the SAME gesture emits a click on
|
|
17
|
+
* finger-release, and an outside-click dismiss reads that click as "the user
|
|
18
|
+
* clicked away" — the classic contextmenu-then-touchend race. Swallow document
|
|
19
|
+
* clicks for this window after a touch-opened menu. 350ms comfortably covers
|
|
20
|
+
* the release without eating a deliberate follow-up tap. */
|
|
21
|
+
const SWALLOW_MS = 350;
|
|
22
|
+
|
|
23
|
+
/** createClickSwallow returns the two halves a point-anchored menu needs to
|
|
24
|
+
* survive the contextmenu-then-touchend race: `arm()` right after a TOUCH-opened
|
|
25
|
+
* menu becomes visible, and `swallowing()` as the first guard in the
|
|
26
|
+
* outside-click dismiss.
|
|
27
|
+
*
|
|
28
|
+
* It lives here, beside the shared placement math, because both point-anchored
|
|
29
|
+
* menus need it and a hand-synced second copy is what this module was extracted
|
|
30
|
+
* to stop. The tab menu shipped without the guard entirely, so on iPadOS Safari
|
|
31
|
+
* — which fires `contextmenu` on a long-press, and where the desktop tab strip
|
|
32
|
+
* is the chrome that shows — a long-pressed tab menu dismissed itself on
|
|
33
|
+
* release. Arm it only for touch: a mouse right-click emits no trailing click,
|
|
34
|
+
* and swallowing there would eat a genuine dismiss. */
|
|
35
|
+
export function createClickSwallow(): { arm: () => void; swallowing: () => boolean } {
|
|
36
|
+
let until = 0;
|
|
37
|
+
return {
|
|
38
|
+
arm: (): void => {
|
|
39
|
+
until = performance.now() + SWALLOW_MS;
|
|
40
|
+
},
|
|
41
|
+
swallowing: (): boolean => performance.now() < until,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
16
45
|
/** Place an already-visible `menu` at viewport point (x, y): open just below
|
|
17
46
|
* the point, flip above it when that would overflow the visible bottom, and
|
|
18
47
|
* clamp into the visual viewport on both axes. The menu is absolute-positioned
|
|
@@ -13,7 +13,7 @@ import type { SessionRef, TerminalContext, TerminalFeature } from "../../kernel/
|
|
|
13
13
|
import type { ActivityMonitorApi } from "../activity-monitor.js";
|
|
14
14
|
import type { MobileToolbarApi } from "../mobile-toolbar.js";
|
|
15
15
|
import { fromHTML } from "../dom.js";
|
|
16
|
-
import { placeMenuAt } from "../menu-position.js";
|
|
16
|
+
import { createClickSwallow, placeMenuAt } from "../menu-position.js";
|
|
17
17
|
import type { SessionInfo, Tab } from "./model.js";
|
|
18
18
|
import {
|
|
19
19
|
ACTIVE_TAB_KEY,
|
|
@@ -2029,6 +2029,27 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2029
2029
|
// the items for the target tab (disabled states reflect its position) and
|
|
2030
2030
|
// clamps into the visible viewport, flipping above the pointer near the
|
|
2031
2031
|
// bottom edge (mirrors context-menu.ts).
|
|
2032
|
+
//
|
|
2033
|
+
// menuSwallow covers the contextmenu-then-touchend race: iPadOS Safari
|
|
2034
|
+
// raises a context menu from a LONG-PRESS, and the same gesture emits a
|
|
2035
|
+
// click on release which onDocClickMenu would read as a click-away. The
|
|
2036
|
+
// terminal menu has guarded this since it shipped; this one did not, so a
|
|
2037
|
+
// long-pressed tab menu closed itself on release — on the one platform
|
|
2038
|
+
// where the desktop strip is the chrome you touch. menuTouch records
|
|
2039
|
+
// whether the press that raised the menu was a finger, since only touch
|
|
2040
|
+
// emits that trailing click.
|
|
2041
|
+
const menuSwallow = createClickSwallow();
|
|
2042
|
+
let menuTouch = false;
|
|
2043
|
+
// One listener on the bar rather than one per chip: chips are created and
|
|
2044
|
+
// destroyed as sessions come and go, and the pointer type belongs to the
|
|
2045
|
+
// gesture, not to a chip.
|
|
2046
|
+
bar.addEventListener(
|
|
2047
|
+
"pointerdown",
|
|
2048
|
+
(e) => {
|
|
2049
|
+
menuTouch = e.pointerType === "touch";
|
|
2050
|
+
},
|
|
2051
|
+
{ passive: true },
|
|
2052
|
+
);
|
|
2032
2053
|
function hideTabMenu(): void {
|
|
2033
2054
|
tabMenu.classList.remove("visible");
|
|
2034
2055
|
tabMenu.replaceChildren();
|
|
@@ -2102,6 +2123,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2102
2123
|
// the visual viewport; flip above the pointer near the bottom edge).
|
|
2103
2124
|
tabMenu.classList.add("visible");
|
|
2104
2125
|
placeMenuAt(tabMenu, x, y);
|
|
2126
|
+
if (menuTouch) {
|
|
2127
|
+
menuSwallow.arm();
|
|
2128
|
+
}
|
|
2105
2129
|
}
|
|
2106
2130
|
|
|
2107
2131
|
// One-time "swipe to switch" hint on first multi-tab state, mobile only.
|
|
@@ -2336,6 +2360,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2336
2360
|
// Dismiss the tab context menu on an outside click, on Escape, and on
|
|
2337
2361
|
// scroll (it is anchored to a viewport point, so a scroll would detach it).
|
|
2338
2362
|
const onDocClickMenu = (): void => {
|
|
2363
|
+
// The trailing click of the long-press that just opened the menu is that
|
|
2364
|
+
// gesture's own release, not a click-away.
|
|
2365
|
+
if (menuSwallow.swallowing()) {
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2339
2368
|
hideTabMenu();
|
|
2340
2369
|
};
|
|
2341
2370
|
document.addEventListener("click", onDocClickMenu);
|