@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.
@@ -38,7 +38,11 @@
38
38
  user-select: none;
39
39
  }
40
40
 
41
- :where(.wt-root) .wt-btn:active {
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
- :where(.wt-root) .wt-scroll-bottom:active {
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
- :where(.wt-root) .wt-tab-new:active {
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
 
@@ -339,7 +339,10 @@
339
339
  }
340
340
  }
341
341
 
342
- :where(.wt-root) .wt-switcher-new:active {
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.0",
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.29.0",
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",
@@ -1,7 +1,8 @@
1
- // Small shared helper for features: build a chrome element from a static,
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. No interpolation ever passes through
4
- // here, so it carries no injection surface.
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 show press
43
- // feedback; preventDefault keeps focus on the terminal. click is kept for
44
- // keyboard activation. jump is idempotent, so the pair is safe.
45
- const onDown = (e: PointerEvent): void => {
46
- e.preventDefault();
47
- jump();
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
- // Keep the hidden terminal textarea focused: a bar button needs no focus
340
- // of its own, and letting the tap shift focus makes iOS/iPadOS consume
341
- // the FIRST tap to blur the input (so "+" only fired create on the second
342
- // tap — the reported double-tap). preventDefault on pointerdown fires the
343
- // action on the first tap; switchTo then re-focuses the input for the new
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
- // First-tap focus retention (see makeNewButton): keep the terminal
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 preventDefaults pointerdown for first-tap focus
379
- // retention (keeps the terminal textarea focused so it fires on the first
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.addEventListener("pointerdown", (e) => {
385
- e.preventDefault();
386
- });
373
+ holdFocusOnPress(btn);
387
374
  btn.addEventListener("click", () => {
388
375
  toggleSwitcher();
389
376
  });