@cplieger/web-terminal-ui 5.2.0 → 5.2.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/10-primitives.css
CHANGED
|
@@ -38,7 +38,11 @@
|
|
|
38
38
|
user-select: none;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/* Press. The pointerdown-cancelling bar buttons (holdFocusOnPress in
|
|
42
|
+
features/dom.ts) never enter :active in Firefox, so they paint .wt-pressed on
|
|
43
|
+
themselves and every press rule in this package matches either. */
|
|
44
|
+
:where(.wt-root) .wt-btn:active,
|
|
45
|
+
:where(.wt-root) .wt-btn.wt-pressed {
|
|
42
46
|
background: var(--text-active);
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -38,7 +38,11 @@
|
|
|
38
38
|
border-color: color-mix(in oklch, var(--text) 35%, transparent);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/* Press fills the background. .wt-pressed rides along: this button cancels its
|
|
42
|
+
pointerdown default and so never enters :active in Firefox (holdFocusOnPress
|
|
43
|
+
in features/dom.ts). */
|
|
44
|
+
:where(.wt-root) .wt-scroll-bottom:active,
|
|
45
|
+
:where(.wt-root) .wt-scroll-bottom.wt-pressed {
|
|
42
46
|
background: var(--text-active);
|
|
43
47
|
}
|
|
44
48
|
|
package/css/30-tabs.css
CHANGED
|
@@ -347,8 +347,11 @@
|
|
|
347
347
|
|
|
348
348
|
/* Press: brighten the hover fill with the active-tab fg (the
|
|
349
349
|
.wt-switcher-current press recipe — a lighter press over an already-filled
|
|
350
|
-
control), keeping the accent glyph.
|
|
351
|
-
|
|
350
|
+
control), keeping the accent glyph. .wt-pressed rides along because this
|
|
351
|
+
button cancels its pointerdown default and so never enters :active in Firefox
|
|
352
|
+
(holdFocusOnPress in features/dom.ts). */
|
|
353
|
+
:where(.wt-root) .wt-tab-new:active,
|
|
354
|
+
:where(.wt-root) .wt-tab-new.wt-pressed {
|
|
352
355
|
background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 12%);
|
|
353
356
|
}
|
|
354
357
|
|
package/css/31-switcher.css
CHANGED
|
@@ -339,7 +339,10 @@
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
|
|
342
|
+
/* Press. .wt-pressed rides along: this button cancels its pointerdown default
|
|
343
|
+
and so never enters :active in Firefox (holdFocusOnPress in features/dom.ts). */
|
|
344
|
+
:where(.wt-root) .wt-switcher-new:active,
|
|
345
|
+
:where(.wt-root) .wt-switcher-new.wt-pressed {
|
|
343
346
|
background: var(--tab-hover-bg);
|
|
344
347
|
}
|
|
345
348
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.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",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"fast-check": "4.9.0",
|
|
58
58
|
"happy-dom": "20.11.1",
|
|
59
59
|
"html-validate": "11.6.0",
|
|
60
|
-
"knip": "6.
|
|
60
|
+
"knip": "6.30.0",
|
|
61
61
|
"prettier": "3.9.6",
|
|
62
62
|
"stylelint": "17.14.1",
|
|
63
63
|
"stylelint-config-standard": "40.0.0",
|
package/src/features/dom.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Small shared
|
|
1
|
+
// Small shared helpers for features: building a chrome element from a static,
|
|
2
2
|
// trusted HTML string (icons, buttons) via a <template> clone, the same pattern
|
|
3
|
-
// the kernel uses for its core subtree
|
|
4
|
-
// here, so it carries no injection
|
|
3
|
+
// the kernel uses for its core subtree, and the press wiring the bar buttons
|
|
4
|
+
// share. No interpolation ever passes through here, so it carries no injection
|
|
5
|
+
// surface.
|
|
5
6
|
|
|
6
7
|
/** Parse a static HTML string and return its first element. */
|
|
7
8
|
export function fromHTML(html: string): HTMLElement {
|
|
@@ -13,3 +14,62 @@ export function fromHTML(html: string): HTMLElement {
|
|
|
13
14
|
}
|
|
14
15
|
return el;
|
|
15
16
|
}
|
|
17
|
+
|
|
18
|
+
/** The press class the chrome buttons wired by holdFocusOnPress paint on
|
|
19
|
+
* themselves. Every `:active` press rule for such a button pairs with it (see
|
|
20
|
+
* the CSS contract test), because those buttons never enter `:active` in
|
|
21
|
+
* Firefox. */
|
|
22
|
+
export const PRESSED_CLASS = "wt-pressed";
|
|
23
|
+
|
|
24
|
+
/** holdFocusOnPress wires a chrome button that must NOT take the keyboard off
|
|
25
|
+
* the terminal, and paints its own press state because of it.
|
|
26
|
+
*
|
|
27
|
+
* Focus: the hidden terminal textarea stays focused. A bar button needs no
|
|
28
|
+
* focus of its own, and letting the press shift focus makes iOS/iPadOS consume
|
|
29
|
+
* the FIRST tap to blur the input (so "+" only fired on the second tap — the
|
|
30
|
+
* reported double-tap), while on a desktop it parks the keyboard on the button,
|
|
31
|
+
* where the strip's own keydown handling eats arrows as "switch tab" and Delete
|
|
32
|
+
* as "close tab". Cancelling pointerdown's default prevents both.
|
|
33
|
+
*
|
|
34
|
+
* Press feedback: cancelling that default is also what suppresses the browser's
|
|
35
|
+
* OWN activation state — Firefox ties `:active` to the mousedown default action,
|
|
36
|
+
* and preventDefault on pointerdown suppresses the compatibility mousedown
|
|
37
|
+
* entirely (measured on Firefox 140: no mousedown event, `matches(":active")`
|
|
38
|
+
* false, background unchanged for as long as the button is held; Chromium and
|
|
39
|
+
* WebKit still apply it). So these buttons — and ONLY these — carry the press
|
|
40
|
+
* class, and their CSS press rules match either. That is why the two jobs live
|
|
41
|
+
* in one helper: a caller that cancels the default without painting the class
|
|
42
|
+
* ships a control with no press feedback in one engine, which is exactly the
|
|
43
|
+
* defect this replaced.
|
|
44
|
+
*
|
|
45
|
+
* The class is dropped on release, on cancel, and when the pointer leaves while
|
|
46
|
+
* held (a press dragged off the button is not an activation), mirroring what
|
|
47
|
+
* `:active` does on its own. A non-primary button press paints nothing, also
|
|
48
|
+
* matching it. The release listeners live on `window` for the life of the press
|
|
49
|
+
* (an `AbortController`, the pattern the switcher gesture uses) rather than on
|
|
50
|
+
* the button: `.wt-scroll-bottom` acts on the press and hides itself, so by the
|
|
51
|
+
* time the release lands the button is `display: none` and receives no
|
|
52
|
+
* `pointerup` at all — bound to the button, the class would stay painted and the
|
|
53
|
+
* control would come back already lit. */
|
|
54
|
+
export function holdFocusOnPress(btn: HTMLElement): void {
|
|
55
|
+
let release: (() => void) | null = null;
|
|
56
|
+
btn.addEventListener("pointerdown", (e) => {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
if (e.button !== 0 || release !== null) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
btn.classList.add(PRESSED_CLASS);
|
|
62
|
+
const held = new AbortController();
|
|
63
|
+
release = () => {
|
|
64
|
+
held.abort();
|
|
65
|
+
release = null;
|
|
66
|
+
btn.classList.remove(PRESSED_CLASS);
|
|
67
|
+
};
|
|
68
|
+
const end = (): void => {
|
|
69
|
+
release?.();
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener("pointerup", end, { signal: held.signal });
|
|
72
|
+
window.addEventListener("pointercancel", end, { signal: held.signal });
|
|
73
|
+
btn.addEventListener("pointerleave", end, { signal: held.signal });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// scroll.scrollToBottom.
|
|
6
6
|
|
|
7
7
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
8
|
-
import { fromHTML } from "./dom.js";
|
|
8
|
+
import { fromHTML, holdFocusOnPress } from "./dom.js";
|
|
9
9
|
|
|
10
10
|
const BUTTON_HTML = `
|
|
11
11
|
<button type="button" class="wt-btn wt-scroll-bottom" aria-label="Scroll to bottom">
|
|
@@ -39,14 +39,13 @@ export function scrollToBottom(): TerminalFeature {
|
|
|
39
39
|
surface.scrollTo({ top: surface.scrollHeight, behavior: "smooth" });
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
// pointerdown (like the toolbar keys) so touch devices
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
btn.addEventListener("pointerdown", onDown);
|
|
42
|
+
// pointerdown (like the toolbar keys) so touch devices get the jump on the
|
|
43
|
+
// press rather than the release; click is kept for keyboard activation.
|
|
44
|
+
// jump is idempotent, so the pair is safe. holdFocusOnPress keeps the
|
|
45
|
+
// keyboard on the terminal across the press and paints the press class the
|
|
46
|
+
// cancelled default costs this button.
|
|
47
|
+
holdFocusOnPress(btn);
|
|
48
|
+
btn.addEventListener("pointerdown", jump);
|
|
50
49
|
btn.addEventListener("click", jump);
|
|
51
50
|
|
|
52
51
|
// Visible only while scrolled up; the region reflects it via a class.
|
|
@@ -12,7 +12,7 @@ import { LineStore, modes } from "@cplieger/web-terminal-engine";
|
|
|
12
12
|
import type { SessionRef, TerminalContext, TerminalFeature } from "../../kernel/types.js";
|
|
13
13
|
import type { ActivityMonitorApi } from "../activity-monitor.js";
|
|
14
14
|
import type { MobileToolbarApi } from "../mobile-toolbar.js";
|
|
15
|
-
import { fromHTML } from "../dom.js";
|
|
15
|
+
import { fromHTML, holdFocusOnPress } from "../dom.js";
|
|
16
16
|
import { createClickSwallow, placeMenuAt } from "../menu-position.js";
|
|
17
17
|
import type { CueStatus, SessionInfo, StatusRecord, Tab } from "./model.js";
|
|
18
18
|
import {
|
|
@@ -336,16 +336,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
336
336
|
// and reflects its open state on every keyboard button.
|
|
337
337
|
function makeNewButton(cls: string): HTMLElement {
|
|
338
338
|
const btn = fromHTML(newButtonHTML(cls));
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
// tab (see focusAfterSwitch). It does NOT make one press one activation —
|
|
345
|
-
// that is create()'s in-flight coalescing, for the opposite failure.
|
|
346
|
-
btn.addEventListener("pointerdown", (e) => {
|
|
347
|
-
e.preventDefault();
|
|
348
|
-
});
|
|
339
|
+
// Keeps the keyboard on the terminal and paints its own press state;
|
|
340
|
+
// holdFocusOnPress explains why those two are one job. It does NOT make
|
|
341
|
+
// one press one activation — that is create()'s in-flight coalescing,
|
|
342
|
+
// for the opposite failure.
|
|
343
|
+
holdFocusOnPress(btn);
|
|
349
344
|
btn.addEventListener("click", () => {
|
|
350
345
|
void create();
|
|
351
346
|
});
|
|
@@ -353,11 +348,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
353
348
|
}
|
|
354
349
|
function makeKbButton(cls: string): HTMLElement {
|
|
355
350
|
const btn = fromHTML(kbButtonHTML(cls));
|
|
356
|
-
|
|
357
|
-
// textarea focused so the toggle fires on the first tap on iOS/iPadOS.
|
|
358
|
-
btn.addEventListener("pointerdown", (e) => {
|
|
359
|
-
e.preventDefault();
|
|
360
|
-
});
|
|
351
|
+
holdFocusOnPress(btn);
|
|
361
352
|
btn.addEventListener("click", () => {
|
|
362
353
|
const kb = opts.keyboardToggle ? ctx.use(opts.keyboardToggle) : undefined;
|
|
363
354
|
if (!kb) {
|
|
@@ -375,15 +366,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
375
366
|
}
|
|
376
367
|
// makeSwitchButton builds the mobile switcher's dedicated open/close
|
|
377
368
|
// button (its notification dot is painted by paintSwitchDot). Like the
|
|
378
|
-
// other bar buttons it
|
|
379
|
-
//
|
|
380
|
-
// tap on iOS/iPadOS); its click toggles the list (toggleSwitcher opens when
|
|
381
|
-
// collapsed, closes when expanded).
|
|
369
|
+
// other bar buttons it goes through holdFocusOnPress; its click toggles the
|
|
370
|
+
// list (toggleSwitcher opens when collapsed, closes when expanded).
|
|
382
371
|
function makeSwitchButton(): HTMLElement {
|
|
383
372
|
const btn = fromHTML(switchButtonHTML("wt-switcher-switch wt-btn"));
|
|
384
|
-
btn
|
|
385
|
-
e.preventDefault();
|
|
386
|
-
});
|
|
373
|
+
holdFocusOnPress(btn);
|
|
387
374
|
btn.addEventListener("click", () => {
|
|
388
375
|
toggleSwitcher();
|
|
389
376
|
});
|