@cplieger/web-terminal-ui 5.4.2 → 5.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "5.4.2",
3
+ "version": "5.4.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",
@@ -45,6 +45,7 @@ import { browserNotifierEnv, createNotifier } from "./notify.js";
45
45
  import {
46
46
  REORDER_MOVE_EPS_PX,
47
47
  REORDER_REST_MS,
48
+ REORDER_STILL_MS,
48
49
  REORDER_SETTLE_MS,
49
50
  REORDER_SHIFT_TRANS,
50
51
  REORDER_SLOT_FADE_MS,
@@ -679,6 +680,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
679
680
  let restTimer: ReturnType<typeof setTimeout> | null = null;
680
681
  let restBefore: HTMLElement | null = null;
681
682
  let restX: number | null = null;
683
+ // When the pointer last actually MOVED. A stationary dragover this long after it
684
+ // is believed as a stop (see trackRest).
685
+ let restMovedAt = 0;
682
686
  // Whether a `drop` fired for the drag in flight. It is the exact signal for
683
687
  // "the user released deliberately" as opposed to "the drag was abandoned":
684
688
  // Escape and a refused release fire dragend with no drop at all. dragend
@@ -1943,12 +1947,19 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1943
1947
  // travelling pointer rearranges nothing at all; the moment it comes to REST the
1944
1948
  // slot opens.
1945
1949
  //
1946
- // Rest is detected, not waited out: the timer is re-armed by MOVEMENT, so it
1947
- // expires a rest window after the last movement rather than a fixed time after a
1948
- // decision. The window has a hard FLOOR set by the platform rather than by taste —
1949
- // HTML5 drag-and-drop only guarantees a `dragover` every 350ms, so a window at or
1950
- // below that cadence expires between two events of a fast sweep and commits every
1951
- // slot the pointer crosses (see REORDER_REST_MS).
1950
+ // Rest is detected from TWO signals, and separating them is what lets the slot
1951
+ // open promptly without a fast sweep committing every slot it crosses:
1952
+ //
1953
+ // - a `dragover` at an unchanged position is POSITIVE evidence of a stop, so it
1954
+ // commits after a short REORDER_STILL_MS confirmation. This is the signal that
1955
+ // normally decides, and the reason the delay is short.
1956
+ // - the absence of events is only a FALLBACK (armRestNet), for a browser that
1957
+ // stops delivering dragover entirely. That one has to out-wait the drag loop's
1958
+ // 350ms cadence, so it is long — and it is rarely what decides.
1959
+ //
1960
+ // One quiet window had to serve both jobs before this, which is why it could not be
1961
+ // shortened: the timer deciding responsiveness was the same one that had to survive
1962
+ // the cadence.
1952
1963
  function trackRest(clientX: number): void {
1953
1964
  const dragged = draggingEl;
1954
1965
  if (!dragged) {
@@ -1964,21 +1975,39 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1964
1975
  return;
1965
1976
  }
1966
1977
  restBefore = before;
1967
- // Movement pushes the commit out; stillness lets the armed timer run down to it.
1968
- // A stationary pointer that has nothing armed still needs one — a browser may
1969
- // deliver dragover repeatedly at an unchanged position, and the first of those is
1970
- // the only signal that the pointer arrived and stopped.
1971
- if (moved || restTimer === null) {
1972
- if (restTimer !== null) {
1973
- clearTimeout(restTimer);
1974
- }
1975
- restTimer = setTimeout(() => {
1976
- restTimer = null;
1977
- const target = restBefore;
1978
- restBefore = null;
1979
- commitSlot(target);
1980
- }, REORDER_REST_MS);
1978
+ const now = Date.now();
1979
+ if (moved) {
1980
+ // Still travelling. Push the net out and wait.
1981
+ restMovedAt = now;
1982
+ armRestNet();
1983
+ return;
1984
+ }
1985
+ // A dragover at an UNCHANGED position is positive evidence that the pointer has
1986
+ // stopped, which is a far stronger signal than the absence of events — and it is
1987
+ // what lets the slot open promptly instead of out-waiting the drag loop's 350ms
1988
+ // cadence. The elapsed check filters the coincidence where one event of a sweep
1989
+ // lands within REORDER_MOVE_EPS_PX of the previous (a reversal, or a frame whose
1990
+ // motion was almost all vertical); a real stop clears it on the next event.
1991
+ if (now - restMovedAt >= REORDER_STILL_MS) {
1992
+ flushRest();
1993
+ return;
1981
1994
  }
1995
+ armRestNet(); // too soon to believe; keep the net alive
1996
+ }
1997
+ // armRestNet is the no-events fallback: if dragover stops arriving altogether, no
1998
+ // stationary event will ever confirm the stop, so this commits anyway. Sized to
1999
+ // out-wait the cadence (see REORDER_REST_MS) because it is the one timer that a
2000
+ // fast sweep could otherwise expire between two of its own events.
2001
+ function armRestNet(): void {
2002
+ if (restTimer !== null) {
2003
+ clearTimeout(restTimer);
2004
+ }
2005
+ restTimer = setTimeout(() => {
2006
+ restTimer = null;
2007
+ const target = restBefore;
2008
+ restBefore = null;
2009
+ commitSlot(target);
2010
+ }, REORDER_REST_MS);
1982
2011
  }
1983
2012
  function cancelRest(): void {
1984
2013
  if (restTimer === null) {
@@ -21,24 +21,35 @@ import { statusPhrase, statusRevealsDot } from "./model.js";
21
21
  // motion is spelled in — index.ts owns the mechanism, strip.ts owns the values,
22
22
  // the same split switcher.ts keeps for the mobile swipe.
23
23
 
24
- /** The quiet window (ms) that means the pointer has STOPPED. When it elapses with no
25
- * movement, the strip opens the slot the pointer is over.
24
+ /** How long (ms) the pointer must have been un-moved before a stationary `dragover` is
25
+ * believed as "stopped".
26
26
  *
27
- * This is a rest detector, not a hold: it is re-armed by MOVEMENT, so it expires a
28
- * rest window after the last movement rather than a fixed time after a decision.
29
- * Sweeping the strip rearranges nothing; stopping is what commits.
27
+ * This is the number that decides how fast the slot opens, and it is small because a
28
+ * `dragover` at an UNCHANGED position is positive evidence of rest — far better
29
+ * evidence than the absence of events, which is all the fallback below has. The drag
30
+ * loop keeps delivering events while the pointer is held still, so a genuinely stopped
31
+ * pointer announces itself; this only filters the coincidence where one event of a
32
+ * sweep happens to land within REORDER_MOVE_EPS_PX of the previous one (a direction
33
+ * reversal, or a frame where the motion was almost entirely vertical). */
34
+ export const REORDER_STILL_MS = 90;
35
+
36
+ /** Fallback guard (ms): commit the pending slot this long after the last MOVEMENT, for
37
+ * the case where `dragover` stops arriving altogether and no stationary event ever
38
+ * confirms the stop.
30
39
  *
31
- * **The floor is set by the drag loop, not by taste.** HTML5 drag-and-drop only
32
- * guarantees a `dragover` every 350ms — the processing model runs on that cadence,
33
- * not per mouse movement — so any window at or below it expires BETWEEN two events
34
- * of a fast sweep, and the strip commits every slot the pointer crosses. That is
35
- * exactly what 120ms did in production: a quick pass over five tabs moved all five.
36
- * 500ms clears the cadence with margin and is the reason this number is not smaller.
40
+ * **Its floor is set by the platform, not by taste, and this is the timer that floor
41
+ * applies to.** HTML5 drag-and-drop only guarantees a `dragover` every 350ms — the drag
42
+ * loop runs on that cadence, not per mouse movement — so a guard at or below it can
43
+ * expire BETWEEN two events of a fast sweep, and the strip then commits every slot the
44
+ * pointer crosses. Measured in production: at 120ms a quick pass over five tabs moved
45
+ * all five.
37
46
  *
38
- * It costs a decisive user nothing, which is what makes the margin affordable: a
39
- * release commits whatever slot is pending at once (flushRest), so the window is only
40
- * ever felt by a pointer that has stopped and is waiting to see the gap open. */
41
- export const REORDER_REST_MS = 500;
47
+ * A pure quiet-window design has to make this ONE number carry both jobs, which is why
48
+ * it could not be shortened — the same timer that decides responsiveness was the one
49
+ * that had to out-wait the cadence, so 500ms was safe and felt slow. Splitting rest
50
+ * DETECTION (the stationary event above) from the no-events NET (here) lets each be
51
+ * sized for its own job. This one is rarely the deciding signal at all. */
52
+ export const REORDER_REST_MS = 450;
42
53
 
43
54
  /** Movement (px) between two dragover events below which the pointer counts as still.
44
55
  * A hand resting on a mouse is never perfectly still, and treating a 1px tremor as a