@cplieger/web-terminal-ui 5.4.5 → 5.4.6

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.5",
3
+ "version": "5.4.6",
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",
@@ -678,13 +678,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
678
678
  // back to the stylesheet — including when a second drag interrupts the first.
679
679
  const shifted = new Set<HTMLElement>();
680
680
  let shiftTimer: ReturnType<typeof setTimeout> | null = null;
681
- // The slot waiting to open and the rest window that will open it.
682
- // `restTimer !== null` is the only "a slot is pending" flag — `restBefore` cannot
683
- // be one, because null is a legitimate value there meaning "past the last chip".
684
- // restX is the last pointer position seen, which is how movement is told from
685
- // stillness (see trackRest).
681
+ // The no-events fallback timer. It carries its own target, so there is
682
+ // deliberately NO pending-slot field here: one was the cause of the reorder
683
+ // dropping commits (see trackRest). restX is the last pointer position seen, which
684
+ // is how movement is told from stillness.
686
685
  let restTimer: ReturnType<typeof setTimeout> | null = null;
687
- let restBefore: HTMLElement | null = null;
688
686
  let restX: number | null = null;
689
687
  // When the pointer last actually MOVED. A stationary dragover this long after it
690
688
  // is believed as a stop (see trackRest).
@@ -1896,7 +1894,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1896
1894
  // was too small to tell them apart).
1897
1895
  //
1898
1896
  // A release never waits either: `drop` commits whatever slot is pending
1899
- // (flushRest), so dropping mid-sweep still lands the tab where it was headed.
1897
+ // (the drop handler commits the slot under the pointer), so dropping mid-sweep
1898
+ // still lands the tab exactly where it was headed.
1900
1899
  //
1901
1900
  // dropTargetBefore returns the first tab whose horizontal midpoint is past
1902
1901
  // x (the element the dragged tab should sit before), or null to drop at the
@@ -2003,21 +2002,30 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2003
2002
  if (!dragged) {
2004
2003
  return;
2005
2004
  }
2005
+ const now = Date.now();
2006
2006
  const moved = restX === null || Math.abs(clientX - restX) > REORDER_MOVE_EPS_PX;
2007
2007
  restX = clientX;
2008
- const before = dropTargetBefore(clientX);
2009
- // Already the slot the tab holds, so there is nothing to preview or commit.
2010
- // Withdraw anything pending: the pointer came back.
2011
- if (before === dragged || before === dragged.nextElementSibling) {
2012
- cancelRest();
2013
- return;
2014
- }
2015
- restBefore = before;
2016
- const now = Date.now();
2017
2008
  if (moved) {
2018
- // Still travelling. Push the net out and wait.
2019
2009
  restMovedAt = now;
2020
- armRestNet();
2010
+ }
2011
+ // The candidate is recomputed from THIS event, every time, and the commit below
2012
+ // uses that value rather than anything stored. That is the fix for a real
2013
+ // unreliability, so it must not be refactored back into a pending-target field:
2014
+ //
2015
+ // There used to be a `restBefore` holding the pending slot, and both the
2016
+ // already-there branch and the leave-the-strip paths nulled it. So a drag that
2017
+ // passed through "already there" on its way somewhere else lost the pending
2018
+ // state, and the stillness branch below — which then early-returned because no
2019
+ // timer was armed — committed NOTHING when the pointer stopped. The tab stayed on
2020
+ // its previous slot until the user jiggled the mouse to re-arm, which is exactly
2021
+ // the reported "it does not let go of the old drop spot". It hit leftward drags
2022
+ // hardest, because after each commit the dragged chip sits immediately left of
2023
+ // where the pointer is heading, so `nextElementSibling` matches constantly.
2024
+ const before = dropTargetBefore(clientX);
2025
+ // Already the slot it would land in, so there is nothing to commit. Only the net
2026
+ // is dropped; there is no pending target left to lose.
2027
+ if (before === dragged.nextElementSibling) {
2028
+ endRestNet();
2021
2029
  return;
2022
2030
  }
2023
2031
  // A dragover at an UNCHANGED position is positive evidence that the pointer has
@@ -2026,48 +2034,31 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2026
2034
  // cadence. The elapsed check filters the coincidence where one event of a sweep
2027
2035
  // lands within REORDER_MOVE_EPS_PX of the previous (a reversal, or a frame whose
2028
2036
  // motion was almost all vertical); a real stop clears it on the next event.
2029
- if (now - restMovedAt >= REORDER_STILL_MS) {
2030
- flushRest();
2037
+ if (!moved && now - restMovedAt >= REORDER_STILL_MS) {
2038
+ endRestNet();
2039
+ commitSlot(before);
2031
2040
  return;
2032
2041
  }
2033
- armRestNet(); // too soon to believe; keep the net alive
2042
+ armRestNet(before);
2034
2043
  }
2035
2044
  // armRestNet is the no-events fallback: if dragover stops arriving altogether, no
2036
- // stationary event will ever confirm the stop, so this commits anyway. Sized to
2037
- // out-wait the cadence (see REORDER_REST_MS) because it is the one timer that a
2038
- // fast sweep could otherwise expire between two of its own events.
2039
- function armRestNet(): void {
2040
- if (restTimer !== null) {
2041
- clearTimeout(restTimer);
2042
- }
2045
+ // stationary event will ever confirm the stop, so this commits anyway. It carries
2046
+ // its own target for the same reason trackRest recomputes one — a net whose target
2047
+ // lived in shared mutable state was how the old pending slot got lost. Sized to
2048
+ // out-wait the cadence (see REORDER_REST_MS) because it is the one timer a fast
2049
+ // sweep could otherwise expire between two of its own events.
2050
+ function armRestNet(before: HTMLElement | null): void {
2051
+ endRestNet();
2043
2052
  restTimer = setTimeout(() => {
2044
2053
  restTimer = null;
2045
- const target = restBefore;
2046
- restBefore = null;
2047
- commitSlot(target);
2054
+ commitSlot(before);
2048
2055
  }, REORDER_REST_MS);
2049
2056
  }
2050
- function cancelRest(): void {
2051
- if (restTimer === null) {
2052
- return;
2053
- }
2054
- clearTimeout(restTimer);
2055
- restTimer = null;
2056
- restBefore = null;
2057
- }
2058
- // flushRest commits a pending slot at once, because a release is a decision. It is
2059
- // what stops the rest window from ever being felt as a floor on the gesture: drop
2060
- // mid-sweep and the tab still lands where it was headed. Only a `drop` calls it;
2061
- // an abandoned drag reaches dragend without one and reverts.
2062
- function flushRest(): void {
2063
- if (restTimer === null) {
2064
- return;
2057
+ function endRestNet(): void {
2058
+ if (restTimer !== null) {
2059
+ clearTimeout(restTimer);
2060
+ restTimer = null;
2065
2061
  }
2066
- const target = restBefore;
2067
- clearTimeout(restTimer);
2068
- restTimer = null;
2069
- restBefore = null;
2070
- commitSlot(target);
2071
2062
  }
2072
2063
 
2073
2064
  // flipTo runs a rearranging mutation and animates its result: every chip that
@@ -2263,7 +2254,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2263
2254
  // start from rest, and a torn-down feature may leave no inline style on an
2264
2255
  // element it no longer owns.
2265
2256
  function endReorderPreview(): void {
2266
- cancelRest();
2257
+ endRestNet();
2267
2258
  restX = null;
2268
2259
  endSlotFade();
2269
2260
  }
@@ -2283,9 +2274,6 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2283
2274
  // in progress" and so would preventDefault every unrelated file or text drop on
2284
2275
  // the page from then on.
2285
2276
  function abortReorderFor(removed: HTMLElement): void {
2286
- if (removed === restBefore) {
2287
- cancelRest();
2288
- }
2289
2277
  if (removed !== draggingEl) {
2290
2278
  return;
2291
2279
  }
@@ -3359,7 +3347,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
3359
3347
  }
3360
3348
  e.preventDefault();
3361
3349
  dropped = true;
3362
- flushRest(); // a release decides: never make a drop wait on the rest window
3350
+ // A release decides, and it decides by POSITION: commit the slot the pointer is
3351
+ // actually over rather than whatever a timer had pending. commitSlot no-ops when
3352
+ // the tab already sits there, so a drop with nothing to move costs nothing.
3353
+ endRestNet();
3354
+ commitSlot(dropTargetBefore(e.clientX));
3363
3355
  syncOrderFromDom();
3364
3356
  announceMoved(moved); // the ONE completed-move announcement per gesture
3365
3357
  });
@@ -3382,7 +3374,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
3382
3374
  }
3383
3375
  const next = e.relatedTarget;
3384
3376
  if (next === null || !bar.contains(next as Node)) {
3385
- cancelRest();
3377
+ endRestNet();
3386
3378
  }
3387
3379
  });
3388
3380
  // A tab released anywhere OTHER than the strip — over the terminal, over
@@ -3412,14 +3404,14 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
3412
3404
  // So: leave `dropped` false, withdraw anything pending, and let dragend run the
3413
3405
  // revert. Nothing is lost silently — the revert animates and announces, so a
3414
3406
  // mis-release reads as "that did not take" rather than as a mystery.
3415
- cancelRest();
3407
+ endRestNet();
3416
3408
  return;
3417
3409
  }
3418
3410
  // dragover, off the strip: no candidate slot exists out here, so withdraw a
3419
3411
  // pending one rather than let the rest window open a gap for a target the pointer
3420
3412
  // has already left. A slot already COMMITTED stays put; the pointer may yet come
3421
3413
  // back onto the strip and release there.
3422
- cancelRest();
3414
+ endRestNet();
3423
3415
  };
3424
3416
  document.addEventListener("dragover", onDocTabDrop);
3425
3417
  document.addEventListener("drop", onDocTabDrop);