@cplieger/web-terminal-ui 4.7.0 → 4.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/30-tabs.css +30 -10
- 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/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. */
|
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.7.
|
|
3
|
+
"version": "4.7.1",
|
|
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);
|