@cplieger/web-terminal-ui 5.4.2 → 5.4.4

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.4",
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,
@@ -232,11 +233,17 @@ async function createSessionHonouringRetry(
232
233
  // First refusal always speaks; after that only every
233
234
  // CREATE_RETRY_REANNOUNCE_MS, so a multi-minute wait is neither silent nor
234
235
  // a toast storm.
236
+ //
237
+ // The suffix says the page is WAITING, not recovering from a failure. It
238
+ // used to say "retrying", which read as an error loop when the server had
239
+ // only said "not yet" -- and the first announcement fires before any retry
240
+ // has happened at all, so a user two seconds into a boot was told
241
+ // "retrying" about a request that had been made exactly once.
235
242
  if (lastAnnouncedAt === 0 || elapsed - lastAnnouncedAt >= CREATE_RETRY_REANNOUNCE_MS) {
236
243
  lastAnnouncedAt = elapsed;
237
- const waiting = err.serverMessage ?? "Server is not ready yet";
238
- ctx.toast(`${waiting}; retrying`, 8000);
239
- ctx.announce(`${waiting}; retrying`);
244
+ const reason = err.serverMessage ?? "Server is not ready yet";
245
+ ctx.toast(`${reason}; waiting…`, 8000);
246
+ ctx.announce(`${reason}; waiting…`);
240
247
  }
241
248
  // Separate from the throttled pair above, and unthrottled: this writes the
242
249
  // reason onto the loading OVERLAY, which is the only surface a user can
@@ -245,7 +252,7 @@ async function createSessionHonouringRetry(
245
252
  // stacking notifications, so there is no storm to throttle, and repeating
246
253
  // the same string is idempotent. This is what turns the black screen of a
247
254
  // twenty-minute tools install into a screen that says why it is waiting.
248
- ctx.loadingReason(`${err.serverMessage ?? "Server is not ready yet"} — retrying…`);
255
+ ctx.loadingReason(`${err.serverMessage ?? "Server is not ready yet"}; waiting…`);
249
256
  await new Promise((resolve) => {
250
257
  window.setTimeout(resolve, err.retryAfterMs ?? CREATE_RETRY_FALLBACK_MS);
251
258
  });
@@ -679,6 +686,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
679
686
  let restTimer: ReturnType<typeof setTimeout> | null = null;
680
687
  let restBefore: HTMLElement | null = null;
681
688
  let restX: number | null = null;
689
+ // When the pointer last actually MOVED. A stationary dragover this long after it
690
+ // is believed as a stop (see trackRest).
691
+ let restMovedAt = 0;
682
692
  // Whether a `drop` fired for the drag in flight. It is the exact signal for
683
693
  // "the user released deliberately" as opposed to "the drag was abandoned":
684
694
  // Escape and a refused release fire dragend with no drop at all. dragend
@@ -1943,12 +1953,19 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1943
1953
  // travelling pointer rearranges nothing at all; the moment it comes to REST the
1944
1954
  // slot opens.
1945
1955
  //
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).
1956
+ // Rest is detected from TWO signals, and separating them is what lets the slot
1957
+ // open promptly without a fast sweep committing every slot it crosses:
1958
+ //
1959
+ // - a `dragover` at an unchanged position is POSITIVE evidence of a stop, so it
1960
+ // commits after a short REORDER_STILL_MS confirmation. This is the signal that
1961
+ // normally decides, and the reason the delay is short.
1962
+ // - the absence of events is only a FALLBACK (armRestNet), for a browser that
1963
+ // stops delivering dragover entirely. That one has to out-wait the drag loop's
1964
+ // 350ms cadence, so it is long — and it is rarely what decides.
1965
+ //
1966
+ // One quiet window had to serve both jobs before this, which is why it could not be
1967
+ // shortened: the timer deciding responsiveness was the same one that had to survive
1968
+ // the cadence.
1952
1969
  function trackRest(clientX: number): void {
1953
1970
  const dragged = draggingEl;
1954
1971
  if (!dragged) {
@@ -1964,21 +1981,39 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1964
1981
  return;
1965
1982
  }
1966
1983
  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);
1984
+ const now = Date.now();
1985
+ if (moved) {
1986
+ // Still travelling. Push the net out and wait.
1987
+ restMovedAt = now;
1988
+ armRestNet();
1989
+ return;
1990
+ }
1991
+ // A dragover at an UNCHANGED position is positive evidence that the pointer has
1992
+ // stopped, which is a far stronger signal than the absence of events — and it is
1993
+ // what lets the slot open promptly instead of out-waiting the drag loop's 350ms
1994
+ // cadence. The elapsed check filters the coincidence where one event of a sweep
1995
+ // lands within REORDER_MOVE_EPS_PX of the previous (a reversal, or a frame whose
1996
+ // motion was almost all vertical); a real stop clears it on the next event.
1997
+ if (now - restMovedAt >= REORDER_STILL_MS) {
1998
+ flushRest();
1999
+ return;
1981
2000
  }
2001
+ armRestNet(); // too soon to believe; keep the net alive
2002
+ }
2003
+ // armRestNet is the no-events fallback: if dragover stops arriving altogether, no
2004
+ // stationary event will ever confirm the stop, so this commits anyway. Sized to
2005
+ // out-wait the cadence (see REORDER_REST_MS) because it is the one timer that a
2006
+ // fast sweep could otherwise expire between two of its own events.
2007
+ function armRestNet(): void {
2008
+ if (restTimer !== null) {
2009
+ clearTimeout(restTimer);
2010
+ }
2011
+ restTimer = setTimeout(() => {
2012
+ restTimer = null;
2013
+ const target = restBefore;
2014
+ restBefore = null;
2015
+ commitSlot(target);
2016
+ }, REORDER_REST_MS);
1982
2017
  }
1983
2018
  function cancelRest(): void {
1984
2019
  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