@cplieger/web-terminal-ui 8.3.1 → 8.3.3
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 +1 -1
- package/package.json +4 -4
- package/src/composition.ts +1 -1
- package/src/features/context-menu.ts +3 -8
- package/src/features/predictive-echo.ts +0 -1
- package/src/features/tabs/index.ts +67 -108
- package/src/features/tabs/model.ts +1 -7
- package/src/kernel/a11y.ts +10 -5
- package/src/kernel/conn-state.ts +3 -9
- package/src/kernel/loading-status.ts +2 -10
- package/src/kernel/pane.ts +4 -10
- package/src/kernel/scrollback-storage.ts +0 -5
- package/src/kernel/selection.ts +0 -3
- package/src/kernel/shell.ts +2 -27
- package/src/kernel/split-handle.ts +0 -3
- package/src/predict.ts +2 -11
- package/src/viewport.ts +9 -1
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ The IME module follows the design of [xterm.js](https://github.com/xtermjs/xterm
|
|
|
97
97
|
|
|
98
98
|
## Contributing
|
|
99
99
|
|
|
100
|
-
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
100
|
+
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
101
101
|
|
|
102
102
|
## Disclaimer
|
|
103
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "8.3.
|
|
3
|
+
"version": "8.3.3",
|
|
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": "MPL-2.0",
|
|
@@ -50,18 +50,18 @@
|
|
|
50
50
|
"@stryker-mutator/core": "10.0.0",
|
|
51
51
|
"@stryker-mutator/typescript-checker": "10.0.0",
|
|
52
52
|
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
53
|
-
"@types/node": "26.6.
|
|
53
|
+
"@types/node": "26.6.4",
|
|
54
54
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
55
55
|
"@vitest/browser": "5.0.3",
|
|
56
56
|
"@vitest/browser-playwright": "5.0.3",
|
|
57
57
|
"@vitest/coverage-v8": "5.0.3",
|
|
58
|
-
"eslint": "10.
|
|
58
|
+
"eslint": "10.12.0",
|
|
59
59
|
"fast-check": "4.10.2",
|
|
60
60
|
"html-validate": "11.16.1",
|
|
61
61
|
"knip": "6.39.0",
|
|
62
62
|
"playwright": "1.63.0",
|
|
63
63
|
"prettier": "3.9.9",
|
|
64
|
-
"stylelint": "17.
|
|
64
|
+
"stylelint": "17.16.0",
|
|
65
65
|
"stylelint-config-standard": "40.0.0",
|
|
66
66
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
67
67
|
"typescript-eslint": "8.71.0",
|
package/src/composition.ts
CHANGED
|
@@ -190,7 +190,7 @@ export function createComposition(opts: CompositionOptions): Composition {
|
|
|
190
190
|
|
|
191
191
|
return {
|
|
192
192
|
isComposing,
|
|
193
|
-
isCompositionOpen: () => composing
|
|
193
|
+
isCompositionOpen: () => composing,
|
|
194
194
|
cancelComposition,
|
|
195
195
|
positionCompositionView,
|
|
196
196
|
teardown,
|
|
@@ -226,9 +226,6 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
226
226
|
pressSelection = selectionTextWithin(surface);
|
|
227
227
|
};
|
|
228
228
|
const onTouchMove = (e: TouchEvent): void => {
|
|
229
|
-
if (!pressLive) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
229
|
const t = e.touches.length === 1 ? e.touches[0] : undefined;
|
|
233
230
|
if (!t) {
|
|
234
231
|
pressLive = false;
|
|
@@ -258,11 +255,9 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
258
255
|
return; // this press selected text; the OS callout owns it
|
|
259
256
|
}
|
|
260
257
|
show(pressX, pressY);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
swallow.arm();
|
|
265
|
-
}
|
|
258
|
+
// Armed at the release edge, so the trailing click this same gesture
|
|
259
|
+
// emits is always inside the window.
|
|
260
|
+
swallow.arm();
|
|
266
261
|
};
|
|
267
262
|
const onTouchCancel = (): void => {
|
|
268
263
|
pressLive = false;
|
|
@@ -545,11 +545,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
545
545
|
}
|
|
546
546
|
// forgetCueSeen drops an acknowledgement, so the session's NEXT input/done
|
|
547
547
|
// is a fresh cue: called when its status moves off the acknowledged value
|
|
548
|
-
// (a new working phase, an exit) and when the session goes away.
|
|
548
|
+
// (a new working phase, an exit) and when the session goes away. Every
|
|
549
|
+
// caller ends in syncChrome, which repaints the out-of-page surfaces.
|
|
549
550
|
function forgetCueSeen(id: string): void {
|
|
550
551
|
if (cueSeen.delete(id)) {
|
|
551
552
|
writeCueSeen(win, cueSeen);
|
|
552
|
-
paintAttention();
|
|
553
553
|
}
|
|
554
554
|
}
|
|
555
555
|
// The out-of-page surfaces render the SAME unseen-cue set the switch dot
|
|
@@ -600,9 +600,6 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
600
600
|
swSwitchDot.dataset["status"] = switchNotify;
|
|
601
601
|
swSwitchDot.title = statusPhrase(switchNotify);
|
|
602
602
|
}
|
|
603
|
-
// The out-of-page surfaces answer to the same set, so they repaint
|
|
604
|
-
// wherever this does: a raise, and every clear through clearSwitchNotify.
|
|
605
|
-
paintAttention();
|
|
606
603
|
}
|
|
607
604
|
function clearSwitchNotify(): void {
|
|
608
605
|
switchNotify = "";
|
|
@@ -929,9 +926,21 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
929
926
|
const el = termInput();
|
|
930
927
|
inputFocusedAtPress = el !== null && doc.activeElement === el;
|
|
931
928
|
}
|
|
929
|
+
// Read at the button's own pointerdown, before the press moves focus onto it.
|
|
930
|
+
function keyboardAtPressOf(btn: HTMLElement): () => boolean {
|
|
931
|
+
let held = false;
|
|
932
|
+
btn.addEventListener(
|
|
933
|
+
"pointerdown",
|
|
934
|
+
() => {
|
|
935
|
+
held = doc.activeElement === termInput();
|
|
936
|
+
},
|
|
937
|
+
{ passive: true },
|
|
938
|
+
);
|
|
939
|
+
return () => held;
|
|
940
|
+
}
|
|
932
941
|
function keyboardParkedOnChrome(): boolean {
|
|
933
942
|
const active = doc.activeElement;
|
|
934
|
-
return
|
|
943
|
+
return bar.contains(active) || switcher.contains(active);
|
|
935
944
|
}
|
|
936
945
|
function keyboardInPaneInput(): boolean {
|
|
937
946
|
const active = doc.activeElement;
|
|
@@ -1021,9 +1030,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1021
1030
|
collapseSwitcher();
|
|
1022
1031
|
switchTo(t.id);
|
|
1023
1032
|
});
|
|
1024
|
-
pick(row, ".wt-switcher-row-close")
|
|
1033
|
+
const rowClose = pick(row, ".wt-switcher-row-close");
|
|
1034
|
+
const heldKeyboard = keyboardAtPressOf(rowClose);
|
|
1035
|
+
rowClose.addEventListener("click", (e) => {
|
|
1025
1036
|
e.stopPropagation();
|
|
1026
|
-
void
|
|
1037
|
+
void dropTab(t.id, true, heldKeyboard());
|
|
1027
1038
|
});
|
|
1028
1039
|
return row;
|
|
1029
1040
|
}
|
|
@@ -1254,8 +1265,6 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1254
1265
|
for (const tab of tabList) {
|
|
1255
1266
|
scroller.appendChild(tab.el);
|
|
1256
1267
|
}
|
|
1257
|
-
paintActive();
|
|
1258
|
-
syncMobile();
|
|
1259
1268
|
}
|
|
1260
1269
|
|
|
1261
1270
|
// closeKeyGrid closes the mobile key grid (if a keyboardToggle is wired and
|
|
@@ -1298,9 +1307,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1298
1307
|
}
|
|
1299
1308
|
collapseClearTimer = win.setTimeout(() => {
|
|
1300
1309
|
collapseClearTimer = null;
|
|
1301
|
-
|
|
1302
|
-
clearRows();
|
|
1303
|
-
}
|
|
1310
|
+
clearRows();
|
|
1304
1311
|
}, 260);
|
|
1305
1312
|
}
|
|
1306
1313
|
}
|
|
@@ -1414,10 +1421,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1414
1421
|
activityCount: tab.activityCount,
|
|
1415
1422
|
}),
|
|
1416
1423
|
);
|
|
1417
|
-
el.addEventListener("click", (
|
|
1418
|
-
if ((e.target as HTMLElement).closest(".wt-tab-close")) {
|
|
1419
|
-
return; // handled by the close button
|
|
1420
|
-
}
|
|
1424
|
+
el.addEventListener("click", () => {
|
|
1421
1425
|
if (editingId === tab.id) {
|
|
1422
1426
|
return; // the rename field owns this chip
|
|
1423
1427
|
}
|
|
@@ -1486,9 +1490,10 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1486
1490
|
switchTo(target.id);
|
|
1487
1491
|
target.el.focus();
|
|
1488
1492
|
});
|
|
1493
|
+
const heldKeyboard = keyboardAtPressOf(close);
|
|
1489
1494
|
close.addEventListener("click", (e) => {
|
|
1490
1495
|
e.stopPropagation();
|
|
1491
|
-
void
|
|
1496
|
+
void dropTab(tab.id, true, heldKeyboard());
|
|
1492
1497
|
});
|
|
1493
1498
|
// Middle-click closes the tab (#8). Suppress the middle-click default on
|
|
1494
1499
|
// mousedown so the browser's autoscroll/paste affordance does not fire.
|
|
@@ -1983,10 +1988,6 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1983
1988
|
// would ignore an inline transform and refuse to move. `translate` composes
|
|
1984
1989
|
// with it instead, so the two can own one chip at the same time.
|
|
1985
1990
|
function applyShift(px: ReadonlyMap<HTMLElement, number>, trans: string): void {
|
|
1986
|
-
if (shiftTimer !== null) {
|
|
1987
|
-
win.clearTimeout(shiftTimer);
|
|
1988
|
-
shiftTimer = null;
|
|
1989
|
-
}
|
|
1990
1991
|
for (const [el, dx] of px) {
|
|
1991
1992
|
el.style.transition = trans;
|
|
1992
1993
|
el.style.translate = `${String(Math.round(dx))}px`;
|
|
@@ -2105,7 +2106,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2105
2106
|
if (before !== null && before.parentNode !== scroller) {
|
|
2106
2107
|
return;
|
|
2107
2108
|
}
|
|
2108
|
-
if (before === dragged
|
|
2109
|
+
if (before === dragged.nextElementSibling) {
|
|
2109
2110
|
return;
|
|
2110
2111
|
}
|
|
2111
2112
|
flipTo(() => {
|
|
@@ -2162,16 +2163,12 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2162
2163
|
return [...scroller.querySelectorAll<HTMLElement>(".wt-tab")].indexOf(el) + 1;
|
|
2163
2164
|
}
|
|
2164
2165
|
function announceTarget(el: HTMLElement): void {
|
|
2165
|
-
|
|
2166
|
-
if (at > 0) {
|
|
2167
|
-
ctx.announce(`Drop position ${String(at)}`);
|
|
2168
|
-
}
|
|
2166
|
+
ctx.announce(`Drop position ${String(slotPosition(el))}`);
|
|
2169
2167
|
}
|
|
2170
2168
|
function announceMoved(el: HTMLElement): void {
|
|
2171
2169
|
const tab = tabList.find((t) => t.el === el);
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
ctx.announce(`Moved ${tab.display} to position ${String(at)}`);
|
|
2170
|
+
if (tab) {
|
|
2171
|
+
ctx.announce(`Moved ${tab.display} to position ${String(slotPosition(el))}`);
|
|
2175
2172
|
}
|
|
2176
2173
|
}
|
|
2177
2174
|
|
|
@@ -2222,10 +2219,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2222
2219
|
if (removed !== draggingEl) {
|
|
2223
2220
|
return;
|
|
2224
2221
|
}
|
|
2225
|
-
removed.classList.remove("wt-tab-dragging");
|
|
2226
2222
|
draggingEl = null;
|
|
2227
|
-
dropped = false;
|
|
2228
|
-
endReorderPreview();
|
|
2229
2223
|
endShift();
|
|
2230
2224
|
clearDragGhost();
|
|
2231
2225
|
}
|
|
@@ -2238,14 +2232,12 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2238
2232
|
order.push(t);
|
|
2239
2233
|
}
|
|
2240
2234
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
syncChrome();
|
|
2248
|
-
}
|
|
2235
|
+
tabList.length = 0;
|
|
2236
|
+
tabList.push(...order);
|
|
2237
|
+
// Before syncChrome: it ends in applyServerOrder, which would undo this
|
|
2238
|
+
// move while the tabs still carry their old positions.
|
|
2239
|
+
publishOrder();
|
|
2240
|
+
syncChrome();
|
|
2249
2241
|
}
|
|
2250
2242
|
|
|
2251
2243
|
// The write half of tab-order sync, called only from the paths that COMMIT
|
|
@@ -2254,9 +2246,6 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2254
2246
|
// the strip straight back. A 409 means this client's list is stale, so the
|
|
2255
2247
|
// server's word is taken; any other failure leaves the arrangement local.
|
|
2256
2248
|
function publishOrder(): void {
|
|
2257
|
-
if (tabList.length === 0) {
|
|
2258
|
-
return;
|
|
2259
|
-
}
|
|
2260
2249
|
tabList.forEach((tab, i) => {
|
|
2261
2250
|
tab.order = i;
|
|
2262
2251
|
});
|
|
@@ -2475,8 +2464,10 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2475
2464
|
// its pane shows next. remote=true also DELETEs the server session (a user
|
|
2476
2465
|
// close); remote=false is a local drop for a session the server already
|
|
2477
2466
|
// ended (an SSE removed event or a poll that no longer lists it), so no
|
|
2478
|
-
// redundant DELETE is sent.
|
|
2479
|
-
|
|
2467
|
+
// redundant DELETE is sent. keyboardHeld: the terminal input held the
|
|
2468
|
+
// keyboard when the press on this tab's close button began, so focus goes
|
|
2469
|
+
// back to it once the button that took it is gone.
|
|
2470
|
+
async function dropTab(id: string, remote: boolean, keyboardHeld = false): Promise<void> {
|
|
2480
2471
|
const idx = tabList.findIndex((t) => t.id === id);
|
|
2481
2472
|
if (idx < 0) {
|
|
2482
2473
|
return;
|
|
@@ -2486,7 +2477,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2486
2477
|
// far left while the create POST is in flight. The replacement lands as any
|
|
2487
2478
|
// new tab does, and dropping the old tab is then an ordinary non-last
|
|
2488
2479
|
// close, so this intercept does not re-fire. A user close only.
|
|
2489
|
-
if (remote && tabList.length === 1
|
|
2480
|
+
if (remote && tabList.length === 1) {
|
|
2490
2481
|
await create();
|
|
2491
2482
|
// A failed create adds nothing (and toasts): keep the existing tab
|
|
2492
2483
|
// rather than stranding the user on an empty strip.
|
|
@@ -2565,7 +2556,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2565
2556
|
replacement = create();
|
|
2566
2557
|
}
|
|
2567
2558
|
}
|
|
2568
|
-
if (typing) {
|
|
2559
|
+
if (typing || keyboardHeld) {
|
|
2569
2560
|
focusInput();
|
|
2570
2561
|
}
|
|
2571
2562
|
if (remote) {
|
|
@@ -2687,10 +2678,6 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2687
2678
|
// Recorded at entry because the exit paths cannot tell a keyboard-started
|
|
2688
2679
|
// edit (focus returns to the chip) from a pointer one (to the terminal).
|
|
2689
2680
|
let editFrom: "keyboard" | "pointer" = "pointer";
|
|
2690
|
-
// Pinned-name requests in flight, COUNTED per id: two renames of one tab can
|
|
2691
|
-
// be out at once, and a bare Set let the first completion clear the marker
|
|
2692
|
-
// while the second's PUT was still open, so that rename failed silently.
|
|
2693
|
-
const namesInFlight = new Map<string, number>();
|
|
2694
2681
|
|
|
2695
2682
|
/** restoreFocusAfterEdit sends focus where the entry path implies. The
|
|
2696
2683
|
* terminal branch is gated exactly like switchTo's focus-on-switch: on a
|
|
@@ -2745,8 +2732,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2745
2732
|
}
|
|
2746
2733
|
// Optimistic: paint it now, guarded by a monotonic counter AND the tab's
|
|
2747
2734
|
// birth epoch so a slow response cannot roll back a newer rename, a later
|
|
2748
|
-
// clear,
|
|
2749
|
-
//
|
|
2735
|
+
// clear, or — if this id were ever reused by a fresh session — a
|
|
2736
|
+
// different tab entirely.
|
|
2750
2737
|
const seq = ++t.nameSeq;
|
|
2751
2738
|
const born = t.born;
|
|
2752
2739
|
t.pinnedTitle = name;
|
|
@@ -2761,28 +2748,15 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2761
2748
|
ctx.announce(fallback ? "Custom name removed" : `Using automatic name: ${text}`);
|
|
2762
2749
|
}
|
|
2763
2750
|
const request = name === "" ? api.clearPinnedTitle(id) : api.setPinnedTitle(id, name);
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
.
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
}
|
|
2774
|
-
cur.pinnedTitle = before;
|
|
2775
|
-
syncChrome();
|
|
2776
|
-
ctx.toast("Could not save the terminal name");
|
|
2777
|
-
})
|
|
2778
|
-
.finally(() => {
|
|
2779
|
-
const left = (namesInFlight.get(id) ?? 0) - 1;
|
|
2780
|
-
if (left > 0) {
|
|
2781
|
-
namesInFlight.set(id, left);
|
|
2782
|
-
} else {
|
|
2783
|
-
namesInFlight.delete(id);
|
|
2784
|
-
}
|
|
2785
|
-
});
|
|
2751
|
+
void request.catch(() => {
|
|
2752
|
+
const cur = tabList.find((x) => x.id === id);
|
|
2753
|
+
if (cur?.born !== born || cur.nameSeq !== seq) {
|
|
2754
|
+
return; // gone, reused, or superseded: the newer state stands
|
|
2755
|
+
}
|
|
2756
|
+
cur.pinnedTitle = before;
|
|
2757
|
+
syncChrome();
|
|
2758
|
+
ctx.toast("Could not save the terminal name");
|
|
2759
|
+
});
|
|
2786
2760
|
return true;
|
|
2787
2761
|
}
|
|
2788
2762
|
|
|
@@ -2973,7 +2947,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2973
2947
|
hideTabMenu();
|
|
2974
2948
|
const idx = tabList.findIndex((t) => t.id === id);
|
|
2975
2949
|
const target = tabList[idx];
|
|
2976
|
-
if (
|
|
2950
|
+
if (!target) {
|
|
2977
2951
|
return;
|
|
2978
2952
|
}
|
|
2979
2953
|
const n = tabList.length;
|
|
@@ -3134,17 +3108,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3134
3108
|
t.title = rec.title;
|
|
3135
3109
|
}
|
|
3136
3110
|
// "" is meaningful here: it is how a clear made in ANOTHER browser reaches
|
|
3137
|
-
// this one.
|
|
3138
|
-
//
|
|
3139
|
-
//
|
|
3140
|
-
if (rec.pinnedTitle !== undefined
|
|
3111
|
+
// this one. It never bumps nameSeq: SSE delivery and REST mutation are not
|
|
3112
|
+
// one total order, so a record arriving during a pending request may
|
|
3113
|
+
// predate our own PUT, and a bump would suppress that request's rollback.
|
|
3114
|
+
if (rec.pinnedTitle !== undefined) {
|
|
3141
3115
|
t.pinnedTitle = rec.pinnedTitle;
|
|
3142
|
-
// SSE delivery and REST mutation are not one total order: during a
|
|
3143
|
-
// pending request the record may predate our own PUT, and bumping would
|
|
3144
|
-
// suppress that request's rollback and its failure toast.
|
|
3145
|
-
if (!namesInFlight.has(rec.id)) {
|
|
3146
|
-
t.nameSeq++;
|
|
3147
|
-
}
|
|
3148
3116
|
}
|
|
3149
3117
|
}
|
|
3150
3118
|
|
|
@@ -3350,20 +3318,13 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3350
3318
|
if (!draggingEl) {
|
|
3351
3319
|
return;
|
|
3352
3320
|
}
|
|
3353
|
-
e.preventDefault();
|
|
3354
3321
|
if (e.dataTransfer) {
|
|
3355
3322
|
e.dataTransfer.dropEffect = "move";
|
|
3356
3323
|
}
|
|
3357
3324
|
trackRest(e.clientX);
|
|
3358
3325
|
});
|
|
3359
|
-
//
|
|
3360
|
-
// drop is to LOAD the payload as a URL, so dropping a tab on iPadOS
|
|
3361
|
-
// navigated the page to /<session-id>.
|
|
3326
|
+
// onDocTabDrop cancels both events here too, as they bubble.
|
|
3362
3327
|
bar.addEventListener("drop", (e) => {
|
|
3363
|
-
if (!draggingEl) {
|
|
3364
|
-
return;
|
|
3365
|
-
}
|
|
3366
|
-
e.preventDefault();
|
|
3367
3328
|
dropOnStrip(e.clientX);
|
|
3368
3329
|
});
|
|
3369
3330
|
function dropOnStrip(clientX: number): void {
|
|
@@ -3388,7 +3349,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3388
3349
|
return;
|
|
3389
3350
|
}
|
|
3390
3351
|
const next = e.relatedTarget;
|
|
3391
|
-
if (
|
|
3352
|
+
if (!bar.contains(next as Node | null)) {
|
|
3392
3353
|
endRestNet();
|
|
3393
3354
|
}
|
|
3394
3355
|
});
|
|
@@ -3412,10 +3373,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3412
3373
|
ctx.shell.root.classList.toggle("wt-drop-left", half === "left");
|
|
3413
3374
|
ctx.shell.root.classList.toggle("wt-drop-right", half === "right");
|
|
3414
3375
|
}
|
|
3415
|
-
// A tab released anywhere
|
|
3416
|
-
//
|
|
3417
|
-
//
|
|
3418
|
-
//
|
|
3376
|
+
// A tab released anywhere must be inert, not a navigation: WebKit's default
|
|
3377
|
+
// for an uncancelled drop is to LOAD the payload as a URL, which sent iPadOS
|
|
3378
|
+
// to /<session-id>. So the whole document, the strip included, is a drop
|
|
3379
|
+
// target for the life of a tab drag; over a half of the pane row it is a
|
|
3380
|
+
// snap. Gated on draggingEl so a file dropped on the page is the browser's.
|
|
3419
3381
|
const onDocTabDrop = (e: DragEvent): void => {
|
|
3420
3382
|
if (!draggingEl) {
|
|
3421
3383
|
return;
|
|
@@ -3499,7 +3461,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3499
3461
|
const rule = pointerDragActivation(e.pointerType);
|
|
3500
3462
|
const el =
|
|
3501
3463
|
e.target instanceof win.Element ? e.target.closest<HTMLElement>(".wt-tab") : null;
|
|
3502
|
-
if (rule === null || !e.isPrimary ||
|
|
3464
|
+
if (rule === null || !e.isPrimary || el?.parentNode !== scroller) {
|
|
3503
3465
|
return;
|
|
3504
3466
|
}
|
|
3505
3467
|
const tab = tabList.find((t) => t.el === el);
|
|
@@ -3529,7 +3491,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3529
3491
|
win.addEventListener("pointermove", onPressMove, opts);
|
|
3530
3492
|
win.addEventListener("pointerup", onPressUp, opts);
|
|
3531
3493
|
win.addEventListener("pointercancel", onPressCancel, opts);
|
|
3532
|
-
|
|
3494
|
+
// Registered after this press's own pointerdown has passed the window, so
|
|
3495
|
+
// any pointerdown it sees is another pointer.
|
|
3496
|
+
win.addEventListener("pointerdown", cancelPointerPress, opts);
|
|
3533
3497
|
win.addEventListener("keydown", onPressKey, opts);
|
|
3534
3498
|
win.addEventListener("blur", cancelPointerPress, { signal: off.signal });
|
|
3535
3499
|
doc.addEventListener("visibilitychange", onPressVisibility, { signal: off.signal });
|
|
@@ -3608,11 +3572,6 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3608
3572
|
cancelPointerPress();
|
|
3609
3573
|
}
|
|
3610
3574
|
}
|
|
3611
|
-
function onPressSecond(e: PointerEvent): void {
|
|
3612
|
-
if (press !== null && press.id !== e.pointerId) {
|
|
3613
|
-
cancelPointerPress();
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
3575
|
// lostpointercapture bubbles, and the chip's implicit touch capture ending
|
|
3617
3576
|
// when the bar takes it over is not a loss.
|
|
3618
3577
|
function onPressLostCapture(e: PointerEvent): void {
|
|
@@ -3657,7 +3616,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3657
3616
|
return;
|
|
3658
3617
|
}
|
|
3659
3618
|
const target = doc.elementFromPoint(x, y);
|
|
3660
|
-
if (
|
|
3619
|
+
if (bar.contains(target)) {
|
|
3661
3620
|
paintDropHalf(null);
|
|
3662
3621
|
if (release) {
|
|
3663
3622
|
dropOnStrip(x);
|
|
@@ -293,7 +293,7 @@ export const MAX_PERSISTED_CUE_SEEN = 200;
|
|
|
293
293
|
* lost acknowledgement only re-lights a dot the user can dismiss again. */
|
|
294
294
|
export function parseCueSeen(raw: string | null): Map<string, CueStatus> {
|
|
295
295
|
const out = new Map<string, CueStatus>();
|
|
296
|
-
if (raw === null
|
|
296
|
+
if (raw === null) {
|
|
297
297
|
return out;
|
|
298
298
|
}
|
|
299
299
|
let data: unknown;
|
|
@@ -608,9 +608,6 @@ function parseRetryAfter(header: string | null, now: number): number | undefined
|
|
|
608
608
|
return undefined;
|
|
609
609
|
}
|
|
610
610
|
const raw = header.trim();
|
|
611
|
-
if (raw === "") {
|
|
612
|
-
return undefined;
|
|
613
|
-
}
|
|
614
611
|
if (/^\d+$/.test(raw)) {
|
|
615
612
|
return Math.min(Number(raw) * 1000, RETRY_AFTER_MAX_MS);
|
|
616
613
|
}
|
|
@@ -627,9 +624,6 @@ function parseRetryAfter(header: string | null, now: number): number | undefined
|
|
|
627
624
|
async function readServerMessage(r: Response): Promise<string | undefined> {
|
|
628
625
|
try {
|
|
629
626
|
const body: unknown = await r.json();
|
|
630
|
-
if (typeof body !== "object" || body === null) {
|
|
631
|
-
return undefined;
|
|
632
|
-
}
|
|
633
627
|
const fields = body as { error?: unknown; message?: unknown };
|
|
634
628
|
for (const candidate of [fields.error, fields.message]) {
|
|
635
629
|
if (typeof candidate === "string" && candidate.trim() !== "") {
|
package/src/kernel/a11y.ts
CHANGED
|
@@ -29,7 +29,15 @@ export interface Announcer {
|
|
|
29
29
|
/** Delay before a cleared live region is re-set with the new message (ms).
|
|
30
30
|
* Long enough for assistive tech to register the clear and the set as two
|
|
31
31
|
* distinct mutations (a sub-frame gap is not). */
|
|
32
|
-
|
|
32
|
+
let REANNOUNCE_DELAY_MS = 100;
|
|
33
|
+
|
|
34
|
+
/** @internal Test seam: re-set every later announcement after `ms`; returns the
|
|
35
|
+
* previous value so the caller can restore it. */
|
|
36
|
+
export function setReannounceDelayMs(ms: number): number {
|
|
37
|
+
const previous = REANNOUNCE_DELAY_MS;
|
|
38
|
+
REANNOUNCE_DELAY_MS = ms;
|
|
39
|
+
return previous;
|
|
40
|
+
}
|
|
33
41
|
|
|
34
42
|
/** Build the single pair of live regions inside root. */
|
|
35
43
|
export function createAnnouncer(root: HTMLElement): Announcer {
|
|
@@ -58,10 +66,7 @@ export function createAnnouncer(root: HTMLElement): Announcer {
|
|
|
58
66
|
// distinct mutations — the same rationale ui-primitives' announce()
|
|
59
67
|
// documents for its shared region.
|
|
60
68
|
el.textContent = "";
|
|
61
|
-
|
|
62
|
-
if (pending !== undefined) {
|
|
63
|
-
win.clearTimeout(pending);
|
|
64
|
-
}
|
|
69
|
+
win.clearTimeout(timers.get(el));
|
|
65
70
|
timers.set(
|
|
66
71
|
el,
|
|
67
72
|
win.setTimeout(() => {
|
package/src/kernel/conn-state.ts
CHANGED
|
@@ -62,12 +62,10 @@ export function createConnState(opts: {
|
|
|
62
62
|
// Suppress transient states until the initial load is over (the loading
|
|
63
63
|
// overlay owns the screen). Terminal states ("ended" and "incompatible")
|
|
64
64
|
// must pass through because no later frame or reconnect will explain the
|
|
65
|
-
// failure;
|
|
65
|
+
// failure; offline, which before load is reached only at the initial-failure
|
|
66
|
+
// limit (closed() returns before it until then), passes for the same reason.
|
|
66
67
|
// connectionBanner renders "open" as hidden.
|
|
67
|
-
const passesLoadingGate =
|
|
68
|
-
state === "ended" ||
|
|
69
|
-
state === "incompatible" ||
|
|
70
|
-
(state === "offline" && consecutiveFailures >= INITIAL_FAILURE_LIMIT);
|
|
68
|
+
const passesLoadingGate = state === "ended" || state === "incompatible" || state === "offline";
|
|
71
69
|
if (!loaded && !passesLoadingGate) {
|
|
72
70
|
opts.onState("open"); // nothing to show yet
|
|
73
71
|
return;
|
|
@@ -140,10 +138,6 @@ export function createConnState(opts: {
|
|
|
140
138
|
},
|
|
141
139
|
idle(): void {
|
|
142
140
|
consecutiveFailures = 0;
|
|
143
|
-
if (restartedTimer !== null) {
|
|
144
|
-
timers.clearTimeout(restartedTimer);
|
|
145
|
-
restartedTimer = null;
|
|
146
|
-
}
|
|
147
141
|
setState("idle", 0);
|
|
148
142
|
},
|
|
149
143
|
setLoaded(): void {
|
|
@@ -165,10 +165,7 @@ export function attachLoadingStatus(
|
|
|
165
165
|
}
|
|
166
166
|
current = text;
|
|
167
167
|
pinned = true;
|
|
168
|
-
|
|
169
|
-
win.clearInterval(rotation);
|
|
170
|
-
rotation = undefined;
|
|
171
|
-
}
|
|
168
|
+
win.clearInterval(rotation);
|
|
172
169
|
show(text);
|
|
173
170
|
announce(text);
|
|
174
171
|
},
|
|
@@ -179,16 +176,11 @@ export function attachLoadingStatus(
|
|
|
179
176
|
// cancellable callback, can run, and it is the one reader of `stopped`.
|
|
180
177
|
// show() and announce() do not re-test it; a timer armed outside later()
|
|
181
178
|
// is what the "stop() leaves no timer armed" test catches.
|
|
182
|
-
if (stopped) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
179
|
stopped = true;
|
|
186
180
|
for (const t of timers) {
|
|
187
181
|
win.clearTimeout(t);
|
|
188
182
|
}
|
|
189
|
-
|
|
190
|
-
win.clearInterval(rotation);
|
|
191
|
-
}
|
|
183
|
+
win.clearInterval(rotation);
|
|
192
184
|
visible.remove();
|
|
193
185
|
live.remove();
|
|
194
186
|
},
|
package/src/kernel/pane.ts
CHANGED
|
@@ -220,7 +220,8 @@ export interface PaneKernel extends PaneHandle {
|
|
|
220
220
|
* gets when the closing transition ends. */
|
|
221
221
|
setHidden(value: boolean): void;
|
|
222
222
|
/** The shell is about to slide this pane's box: nothing it measures from now
|
|
223
|
-
* until its viewport settles is final, and the settle sends the size.
|
|
223
|
+
* until its viewport settles is final, and the settle sends the size. Live
|
|
224
|
+
* panes only: a failed pane is torn down before the split next opens. */
|
|
224
225
|
holdGeometry(): void;
|
|
225
226
|
/** Whether this pane's input is a stop in the document's Tab order. On while
|
|
226
227
|
* the split is open and the pane shows a tab, so Tab runs left input, divider,
|
|
@@ -437,15 +438,10 @@ function buildPaneInto(
|
|
|
437
438
|
let hidden = false;
|
|
438
439
|
let rootReleased = false;
|
|
439
440
|
|
|
440
|
-
let ready = false;
|
|
441
441
|
let firstFrameRendered = false;
|
|
442
442
|
let fontsLoaded = false;
|
|
443
443
|
let wsOpen = false;
|
|
444
444
|
function markReady(): void {
|
|
445
|
-
if (ready) {
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
ready = true;
|
|
449
445
|
connState.setLoaded();
|
|
450
446
|
services.loading.firstFrame();
|
|
451
447
|
}
|
|
@@ -981,7 +977,7 @@ function buildPaneInto(
|
|
|
981
977
|
}
|
|
982
978
|
// Nobody owns the keyboard: focus in a shadow root, a dialog or an ARIA
|
|
983
979
|
// widget all report non-body and all bail.
|
|
984
|
-
if (doc.activeElement !==
|
|
980
|
+
if (doc.activeElement !== doc.body) {
|
|
985
981
|
return;
|
|
986
982
|
}
|
|
987
983
|
if (composition.isComposing()) {
|
|
@@ -1514,9 +1510,7 @@ function buildPaneInto(
|
|
|
1514
1510
|
hidden = value;
|
|
1515
1511
|
},
|
|
1516
1512
|
holdGeometry() {
|
|
1517
|
-
|
|
1518
|
-
viewport.beginTransition();
|
|
1519
|
-
}
|
|
1513
|
+
viewport.beginTransition();
|
|
1520
1514
|
},
|
|
1521
1515
|
setTabStop(value) {
|
|
1522
1516
|
input.tabIndex = value ? 0 : -1;
|
|
@@ -214,7 +214,6 @@ export function localScrollbackStorage(
|
|
|
214
214
|
function sweep(): void {
|
|
215
215
|
const now = Date.now();
|
|
216
216
|
const live: { key: string; savedAt: number; chars: number }[] = [];
|
|
217
|
-
let total = 0;
|
|
218
217
|
for (const entry of ownEntries()) {
|
|
219
218
|
// An unreadable timestamp is treated as expired: the kernel would refuse
|
|
220
219
|
// the entry anyway, so keeping it only consumes quota.
|
|
@@ -223,10 +222,6 @@ export function localScrollbackStorage(
|
|
|
223
222
|
continue;
|
|
224
223
|
}
|
|
225
224
|
live.push({ key: entry.key, savedAt: entry.savedAt, chars: entry.chars });
|
|
226
|
-
total += entry.chars;
|
|
227
|
-
}
|
|
228
|
-
if (total <= maxBytes) {
|
|
229
|
-
return;
|
|
230
225
|
}
|
|
231
226
|
// Newest first, then keep entries while they fit and drop the rest: an entry
|
|
232
227
|
// the user last saw a minute ago is worth more than one from this morning.
|
package/src/kernel/selection.ts
CHANGED
|
@@ -12,9 +12,6 @@ export function selectionTextWithin(container: Element): string {
|
|
|
12
12
|
return "";
|
|
13
13
|
}
|
|
14
14
|
const text = sel.toString();
|
|
15
|
-
if (text.length === 0) {
|
|
16
|
-
return "";
|
|
17
|
-
}
|
|
18
15
|
for (let i = 0; i < sel.rangeCount; i++) {
|
|
19
16
|
const range = sel.getRangeAt(i);
|
|
20
17
|
if (!container.contains(range.startContainer) || !container.contains(range.endContainer)) {
|
package/src/kernel/shell.ts
CHANGED
|
@@ -549,9 +549,6 @@ function createShellInto(
|
|
|
549
549
|
applyRatioVar();
|
|
550
550
|
}
|
|
551
551
|
function fireSplitChange(): void {
|
|
552
|
-
if (changeListeners.size === 0) {
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
552
|
const state = splitState();
|
|
556
553
|
for (const cb of [...changeListeners]) {
|
|
557
554
|
cb(state);
|
|
@@ -608,9 +605,6 @@ function createShellInto(
|
|
|
608
605
|
// may rest on the empty pane the replacement is headed for, so the handle faces
|
|
609
606
|
// where the new tab will land.
|
|
610
607
|
function select(side: PaneSide): boolean {
|
|
611
|
-
if (!isPaneSide(side)) {
|
|
612
|
-
return false;
|
|
613
|
-
}
|
|
614
608
|
const state = paneAt(side)?.state();
|
|
615
609
|
if (state !== "shown" && !(state === "empty" && !anyShown())) {
|
|
616
610
|
return false;
|
|
@@ -1064,7 +1058,7 @@ function createShellInto(
|
|
|
1064
1058
|
return;
|
|
1065
1059
|
}
|
|
1066
1060
|
for (const feature of features) {
|
|
1067
|
-
if (
|
|
1061
|
+
if (kernel.isDestroyed()) {
|
|
1068
1062
|
return;
|
|
1069
1063
|
}
|
|
1070
1064
|
const outcome = await kernel.setupFeature(feature);
|
|
@@ -1072,9 +1066,6 @@ function createShellInto(
|
|
|
1072
1066
|
failPane(slot, { phase: "feature-setup", feature: outcome.feature, cause: outcome.cause });
|
|
1073
1067
|
return;
|
|
1074
1068
|
}
|
|
1075
|
-
if (outcome.status === "aborted") {
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
1069
|
}
|
|
1079
1070
|
}
|
|
1080
1071
|
function buildSecondary(slot: Slot): void {
|
|
@@ -1231,7 +1222,7 @@ function createShellInto(
|
|
|
1231
1222
|
return true;
|
|
1232
1223
|
}
|
|
1233
1224
|
function closeSide(side: PaneSide): boolean {
|
|
1234
|
-
if (!
|
|
1225
|
+
if (!splitOpen || destroyed) {
|
|
1235
1226
|
return false;
|
|
1236
1227
|
}
|
|
1237
1228
|
const squeezed = slotAt(side);
|
|
@@ -1266,9 +1257,6 @@ function createShellInto(
|
|
|
1266
1257
|
);
|
|
1267
1258
|
}
|
|
1268
1259
|
function onShellResize(): void {
|
|
1269
|
-
if (destroyed) {
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
1260
|
paintShellNarrow();
|
|
1273
1261
|
for (const p of builtPanes()) {
|
|
1274
1262
|
p.updateNarrow();
|
|
@@ -1427,9 +1415,6 @@ function createShellInto(
|
|
|
1427
1415
|
|
|
1428
1416
|
let rootReleased = false;
|
|
1429
1417
|
function cleanupRuntime(): void {
|
|
1430
|
-
if (destroyed) {
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
1418
|
destroyed = true;
|
|
1434
1419
|
// The tracked stores are about to become unreachable, and the connections
|
|
1435
1420
|
// whose epochs the save is filed under go with the drain.
|
|
@@ -1441,7 +1426,6 @@ function createShellInto(
|
|
|
1441
1426
|
if (destroyed) {
|
|
1442
1427
|
return;
|
|
1443
1428
|
}
|
|
1444
|
-
primaryKernel.markFailed();
|
|
1445
1429
|
cleanupRuntime();
|
|
1446
1430
|
dismissLoadingOverlay();
|
|
1447
1431
|
let handled = false;
|
|
@@ -1517,21 +1501,12 @@ function createShellInto(
|
|
|
1517
1501
|
selectedPane().focus();
|
|
1518
1502
|
},
|
|
1519
1503
|
send(bytes) {
|
|
1520
|
-
if (destroyed) {
|
|
1521
|
-
return;
|
|
1522
|
-
}
|
|
1523
1504
|
selectedPane().send(bytes);
|
|
1524
1505
|
},
|
|
1525
1506
|
reset() {
|
|
1526
|
-
if (destroyed) {
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
1507
|
selectedPane().reset();
|
|
1530
1508
|
},
|
|
1531
1509
|
reattach() {
|
|
1532
|
-
if (destroyed) {
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
1510
|
selectedPane().reattach();
|
|
1536
1511
|
},
|
|
1537
1512
|
destroy() {
|
|
@@ -237,9 +237,6 @@ export function createSplitHandle(shell: SplitHandleShell): SplitHandle {
|
|
|
237
237
|
return;
|
|
238
238
|
}
|
|
239
239
|
const state = split.state();
|
|
240
|
-
if (!state.open || state.collapsed) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
240
|
ev.preventDefault();
|
|
244
241
|
const span = root.clientWidth - SPLIT_GUTTER_PX;
|
|
245
242
|
const { min, max } = bounds();
|
package/src/predict.ts
CHANGED
|
@@ -31,7 +31,6 @@ export function createPredictor(): Predictor {
|
|
|
31
31
|
let predCol = 0;
|
|
32
32
|
let predActive = false;
|
|
33
33
|
let predPendingWrap = false;
|
|
34
|
-
let predFrozen = false;
|
|
35
34
|
let cols = 80;
|
|
36
35
|
let rows = 30;
|
|
37
36
|
let onChange: (() => void) | null = null;
|
|
@@ -40,8 +39,6 @@ export function createPredictor(): Predictor {
|
|
|
40
39
|
predRow = 0;
|
|
41
40
|
predCol = 0;
|
|
42
41
|
predActive = false;
|
|
43
|
-
predPendingWrap = false;
|
|
44
|
-
predFrozen = false;
|
|
45
42
|
onChange?.();
|
|
46
43
|
}
|
|
47
44
|
|
|
@@ -73,23 +70,17 @@ export function createPredictor(): Predictor {
|
|
|
73
70
|
predRow = serverRow;
|
|
74
71
|
predCol = serverCol;
|
|
75
72
|
predPendingWrap = false;
|
|
76
|
-
|
|
77
|
-
predActive = !predFrozen;
|
|
73
|
+
predActive = !(cursorHidden ?? false);
|
|
78
74
|
onChange?.();
|
|
79
75
|
},
|
|
80
76
|
applyInput(bytes) {
|
|
81
|
-
if (!predActive
|
|
77
|
+
if (!predActive) {
|
|
82
78
|
return;
|
|
83
79
|
}
|
|
84
80
|
let i = 0;
|
|
85
81
|
while (i < bytes.length) {
|
|
86
82
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- index checked by loop condition
|
|
87
83
|
const b = bytes[i]!;
|
|
88
|
-
if (b === 0x1b) {
|
|
89
|
-
predActive = false;
|
|
90
|
-
onChange?.();
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
84
|
if (b === 0x08 || b === 0x7f) {
|
|
94
85
|
predPendingWrap = false;
|
|
95
86
|
if (predCol > 0) {
|
package/src/viewport.ts
CHANGED
|
@@ -9,7 +9,15 @@ import { windowOf } from "./kernel/realm.js";
|
|
|
9
9
|
|
|
10
10
|
// Long enough to bridge the iOS keyboard slide (~250ms) with margin for fonts
|
|
11
11
|
// and reflow.
|
|
12
|
-
|
|
12
|
+
let SETTLE_MS = 350;
|
|
13
|
+
|
|
14
|
+
/** @internal Test seam: arm every settle started from now on at `ms`; returns the
|
|
15
|
+
* previous value so the caller can restore it. */
|
|
16
|
+
export function setSettleMs(ms: number): number {
|
|
17
|
+
const previous = SETTLE_MS;
|
|
18
|
+
SETTLE_MS = ms;
|
|
19
|
+
return previous;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
/** The visual viewport against the layout viewport, in whole CSS px. */
|
|
15
23
|
interface KeyboardGeometry {
|