@cplieger/web-terminal-ui 5.4.0 → 5.4.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/30-tabs.css +0 -33
- package/package.json +4 -4
- package/src/features/tabs/index.ts +138 -146
- package/src/features/tabs/strip.ts +24 -17
package/css/30-tabs.css
CHANGED
|
@@ -495,39 +495,6 @@
|
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
/* ===== Pending drop edge (tabs feature, desktop drag) =====
|
|
499
|
-
The exact boundary the dragged tab will be inserted at, marked while the hold runs
|
|
500
|
-
and filling from nothing to full over the hold's own duration (the tabs feature
|
|
501
|
-
sets the duration inline from REORDER_DWELL_MS, so the bar and the timer cannot
|
|
502
|
-
drift apart).
|
|
503
|
-
|
|
504
|
-
It rides the neighbouring chip: the leading edge of the chip being pushed aside,
|
|
505
|
-
or the trailing edge of the last chip when the tab is heading past the end of the
|
|
506
|
-
strip (.wt-tab-dwell-end). The chip is already position: relative for its progress
|
|
507
|
-
bar, and its overflow clip keeps the rail inside the same rounded padding box.
|
|
508
|
-
|
|
509
|
-
3px of solid accent rather than a dashed treatment: the dashed vocabulary belongs
|
|
510
|
-
to the SLOT, which is a destination that has been decided. This is a boundary
|
|
511
|
-
still being chosen, so it reads as a caret. inset-block keeps it clear of the
|
|
512
|
-
chip's rounded corners without needing its own radius arithmetic. */
|
|
513
|
-
:where(.wt-root) .wt-tab-dwell {
|
|
514
|
-
position: absolute;
|
|
515
|
-
inset-block: var(--sp-1);
|
|
516
|
-
inset-inline-start: 0;
|
|
517
|
-
width: 3px;
|
|
518
|
-
border-radius: var(--radius-sm);
|
|
519
|
-
background: var(--accent);
|
|
520
|
-
/* Grows upward like a gauge. The feature writes scaleY inline; a transform on this
|
|
521
|
-
element cannot collide with the chip's own animations because it is a separate
|
|
522
|
-
element, which is the whole reason the rail is not a pseudo-element on the chip. */
|
|
523
|
-
transform-origin: bottom;
|
|
524
|
-
pointer-events: none;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
:where(.wt-root) .wt-tab-dwell-end {
|
|
528
|
-
inset-inline: auto 0;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
498
|
/* ===== Reorder drop slot (tabs feature, desktop drag) =====
|
|
532
499
|
The dragged chip stays in the strip's flow for the whole gesture and renders as
|
|
533
500
|
the SLOT it will land in: an empty dashed outline in the accent over a faint
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.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": "MPL-2.0",
|
|
@@ -50,19 +50,19 @@
|
|
|
50
50
|
"@stryker-mutator/core": "9.6.1",
|
|
51
51
|
"@stryker-mutator/typescript-checker": "9.6.1",
|
|
52
52
|
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
53
|
-
"@types/node": "
|
|
53
|
+
"@types/node": "26.2.0",
|
|
54
54
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
55
55
|
"@vitest/coverage-v8": "4.1.10",
|
|
56
56
|
"eslint": "10.8.1",
|
|
57
57
|
"fast-check": "4.9.0",
|
|
58
58
|
"happy-dom": "20.11.2",
|
|
59
59
|
"html-validate": "11.6.2",
|
|
60
|
-
"knip": "6.32.
|
|
60
|
+
"knip": "6.32.1",
|
|
61
61
|
"prettier": "3.9.6",
|
|
62
62
|
"stylelint": "17.14.1",
|
|
63
63
|
"stylelint-config-standard": "40.0.0",
|
|
64
64
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
65
|
-
"typescript-eslint": "8.
|
|
65
|
+
"typescript-eslint": "8.67.0",
|
|
66
66
|
"vitest": "4.1.10"
|
|
67
67
|
},
|
|
68
68
|
"publishConfig": {
|
|
@@ -43,8 +43,9 @@ import {
|
|
|
43
43
|
} from "./model.js";
|
|
44
44
|
import { browserNotifierEnv, createNotifier } from "./notify.js";
|
|
45
45
|
import {
|
|
46
|
-
REORDER_DWELL_MS,
|
|
47
46
|
REORDER_LEAN_PX,
|
|
47
|
+
REORDER_MOVE_EPS_PX,
|
|
48
|
+
REORDER_REST_MS,
|
|
48
49
|
REORDER_SETTLE_MS,
|
|
49
50
|
REORDER_SHIFT_TRANS,
|
|
50
51
|
REORDER_SLOT_FADE_MS,
|
|
@@ -672,21 +673,19 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
672
673
|
// to hand every one of them back to the stylesheet.
|
|
673
674
|
const shifted = new Set<HTMLElement>();
|
|
674
675
|
let shiftTimer: ReturnType<typeof setTimeout> | null = null;
|
|
675
|
-
// The
|
|
676
|
-
//
|
|
677
|
-
// value there meaning "past the last chip".
|
|
678
|
-
|
|
679
|
-
|
|
676
|
+
// The slot waiting to open and the rest window that will open it.
|
|
677
|
+
// `restTimer !== null` is the only "a slot is pending" flag — `restBefore` cannot
|
|
678
|
+
// be one, because null is a legitimate value there meaning "past the last chip".
|
|
679
|
+
// restX is the last pointer position seen, which is how movement is told from
|
|
680
|
+
// stillness (see trackRest).
|
|
681
|
+
let restTimer: ReturnType<typeof setTimeout> | null = null;
|
|
682
|
+
let restBefore: HTMLElement | null = null;
|
|
683
|
+
let restX: number | null = null;
|
|
680
684
|
// Whether a `drop` fired for the drag in flight. It is the exact signal for
|
|
681
685
|
// "the user released deliberately" as opposed to "the drag was abandoned":
|
|
682
686
|
// Escape and a refused release fire dragend with no drop at all. dragend
|
|
683
687
|
// without it reverts the preview, which is the cancel this reorder never had.
|
|
684
688
|
let dropped = false;
|
|
685
|
-
// The pending-drop rail: ONE element, moved to whichever edge the current hold
|
|
686
|
-
// is counting down on (see showDwellRail).
|
|
687
|
-
const dwellRail = document.createElement("span");
|
|
688
|
-
dwellRail.className = "wt-tab-dwell";
|
|
689
|
-
dwellRail.setAttribute("aria-hidden", "true");
|
|
690
689
|
// The chip mid slot-fade, and the timer that ends it.
|
|
691
690
|
let slotFadeEl: HTMLElement | null = null;
|
|
692
691
|
let slotFadeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -1826,7 +1825,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1826
1825
|
}
|
|
1827
1826
|
}
|
|
1828
1827
|
|
|
1829
|
-
// --- Desktop reorder preview:
|
|
1828
|
+
// --- Desktop reorder preview: sweep, rest, slide ------------------------
|
|
1830
1829
|
//
|
|
1831
1830
|
// The old reorder moved the dragged chip on EVERY dragover, so the strip
|
|
1832
1831
|
// rearranged itself continuously under the pointer and — every chip being
|
|
@@ -1834,22 +1833,33 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1834
1833
|
// in. The chip you were dragging looked like the slot it left, the slot it
|
|
1835
1834
|
// was over, and the slot it would end up in.
|
|
1836
1835
|
//
|
|
1837
|
-
// The
|
|
1838
|
-
// flow throughout as the slot it
|
|
1839
|
-
// 30-tabs.css renders as an empty
|
|
1840
|
-
//
|
|
1841
|
-
// of the two had to stop
|
|
1836
|
+
// The preview splits that into two different answers for two different
|
|
1837
|
+
// actions, and the dragged chip stays in the flow throughout as the slot it
|
|
1838
|
+
// will land in (`.wt-tab-dragging`, which 30-tabs.css renders as an empty
|
|
1839
|
+
// dashed outline rather than a dimmed copy of the tab — the solid copy is
|
|
1840
|
+
// the drag image under the pointer, and one of the two had to stop
|
|
1841
|
+
// pretending to be the tab):
|
|
1842
1842
|
//
|
|
1843
|
-
//
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
//
|
|
1848
|
-
//
|
|
1849
|
-
//
|
|
1843
|
+
// - SWEEPING across the strip only LEANS the chips a commit would displace,
|
|
1844
|
+
// a few px toward where they would go (leanToward). Instant, and it is
|
|
1845
|
+
// what says where the tab will fall while you are still looking for the
|
|
1846
|
+
// spot. Nothing is rearranged, so crossing five tabs does not reshuffle
|
|
1847
|
+
// the strip five times.
|
|
1848
|
+
// - COMING TO REST opens the slot for real (trackRest -> commitSlot): one
|
|
1849
|
+
// DOM move, the displaced chips slide from their old positions to their
|
|
1850
|
+
// new ones, and the slot fades in at its new home.
|
|
1850
1851
|
//
|
|
1851
|
-
//
|
|
1852
|
-
//
|
|
1852
|
+
// Rest is DETECTED, not waited out: REORDER_REST_MS is re-armed by movement,
|
|
1853
|
+
// so it expires a rest window after the last movement rather than a fixed
|
|
1854
|
+
// time after a decision. An earlier version made every commit serve a fixed
|
|
1855
|
+
// hold, which is the wrong shape for a direct manipulation — it delayed the
|
|
1856
|
+
// one case that should be instant (you found the spot and stopped) to
|
|
1857
|
+
// protect a case the lean already covers (you are still moving). It also
|
|
1858
|
+
// grew a progress bar to explain the wait, which is the tell that the wait
|
|
1859
|
+
// should not have been there.
|
|
1860
|
+
//
|
|
1861
|
+
// A release never waits either: `drop` commits whatever slot is pending
|
|
1862
|
+
// (flushRest), so dropping mid-sweep lands the tab where the lean said.
|
|
1853
1863
|
//
|
|
1854
1864
|
// dropTargetBefore returns the first tab whose horizontal midpoint is past
|
|
1855
1865
|
// x (the element the dragged tab should sit before), or null to drop at the
|
|
@@ -1933,9 +1943,13 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1933
1943
|
}
|
|
1934
1944
|
|
|
1935
1945
|
// leanToward tips the chips a commit would displace a few px toward their
|
|
1936
|
-
// destination, for as long as the
|
|
1937
|
-
// held state, not an animation with an end, and a timer that expired
|
|
1938
|
-
// would snap it away
|
|
1946
|
+
// destination, for as long as the pointer is still choosing. No settle timer: the
|
|
1947
|
+
// lean is a held state, not an animation with an end, and a timer that expired
|
|
1948
|
+
// under it would snap it away mid-sweep.
|
|
1949
|
+
//
|
|
1950
|
+
// This is the SWEEP preview and it is the only thing a moving pointer produces:
|
|
1951
|
+
// it says where the tab will fall without rearranging anything, which is what lets
|
|
1952
|
+
// a drag cross five tabs without the strip reshuffling five times.
|
|
1939
1953
|
//
|
|
1940
1954
|
// Chips that were leaning and no longer would be are written back to zero in
|
|
1941
1955
|
// the SAME pass, so a changed candidate eases from one lean into the other
|
|
@@ -1974,9 +1988,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1974
1988
|
applyShift(next, REORDER_SHIFT_TRANS);
|
|
1975
1989
|
}
|
|
1976
1990
|
// releaseLean eases every leaning chip back to rest and settles it, for when
|
|
1977
|
-
// a candidate is withdrawn (the pointer came back to the committed slot, or
|
|
1978
|
-
//
|
|
1979
|
-
//
|
|
1991
|
+
// a candidate is withdrawn (the pointer came back to the committed slot, or left
|
|
1992
|
+
// the strip). endShift would be wrong here: it strips the transition in the same
|
|
1993
|
+
// write, so the lean would snap home.
|
|
1980
1994
|
function releaseLean(): void {
|
|
1981
1995
|
if (shifted.size === 0) {
|
|
1982
1996
|
return;
|
|
@@ -1994,101 +2008,82 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1994
2008
|
applyShift(rest, REORDER_SHIFT_TRANS);
|
|
1995
2009
|
shiftTimer = setTimeout(endShift, REORDER_SETTLE_MS);
|
|
1996
2010
|
}
|
|
1997
|
-
//
|
|
1998
|
-
// of the chip beside it, and fills that mark over the hold's own duration.
|
|
2011
|
+
// trackRest is the whole gate, called on every dragover over the strip.
|
|
1999
2012
|
//
|
|
2000
|
-
//
|
|
2001
|
-
//
|
|
2002
|
-
//
|
|
2003
|
-
//
|
|
2004
|
-
//
|
|
2005
|
-
//
|
|
2006
|
-
// read as three ordered states: the tab under the pointer, the edge about to open,
|
|
2007
|
-
// then the gap that opened.
|
|
2013
|
+
// Sweeping and settling are DIFFERENT actions and get different answers. While the
|
|
2014
|
+
// pointer is still travelling the strip only LEANS the chips it would displace
|
|
2015
|
+
// (leanToward): instant, cheap, and it is what says where the tab will fall while
|
|
2016
|
+
// you are still looking for the spot. Nothing is rearranged, so crossing five tabs
|
|
2017
|
+
// does not rearrange the strip five times. The moment the pointer comes to REST the
|
|
2018
|
+
// slot opens for real.
|
|
2008
2019
|
//
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
const
|
|
2017
|
-
if (
|
|
2018
|
-
hideDwellRail();
|
|
2019
|
-
return;
|
|
2020
|
-
}
|
|
2021
|
-
dwellRail.classList.toggle("wt-tab-dwell-end", before === null);
|
|
2022
|
-
host.appendChild(dwellRail);
|
|
2023
|
-
if (prefersReduce()) {
|
|
2024
|
-
// No countdown, but the boundary still has to be legible, so show it filled.
|
|
2025
|
-
dwellRail.style.transition = "none";
|
|
2026
|
-
dwellRail.style.transform = "scaleY(1)";
|
|
2027
|
-
return;
|
|
2028
|
-
}
|
|
2029
|
-
dwellRail.style.transition = "none";
|
|
2030
|
-
dwellRail.style.transform = "scaleY(0)";
|
|
2031
|
-
dwellRail.getBoundingClientRect(); // commit the from-state before the fill
|
|
2032
|
-
dwellRail.style.transition = `transform ${String(REORDER_DWELL_MS)}ms linear`;
|
|
2033
|
-
dwellRail.style.transform = "scaleY(1)";
|
|
2034
|
-
}
|
|
2035
|
-
function hideDwellRail(): void {
|
|
2036
|
-
dwellRail.remove(); // idempotent: removing a detached node does nothing
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
// armDwell registers the slot a release would land in and starts (or keeps)
|
|
2040
|
-
// the hold that commits it. Called on every dragover, so it must be cheap
|
|
2041
|
-
// and idempotent for an unchanged candidate: restarting the timer on each
|
|
2042
|
-
// event would mean a hand that never holds perfectly still could never
|
|
2043
|
-
// commit anything, which is the bug a naive dwell always ships with.
|
|
2044
|
-
function armDwell(before: HTMLElement | null): void {
|
|
2045
|
-
if (!draggingEl) {
|
|
2020
|
+
// Rest is detected, not waited out. An earlier version made every commit wait a
|
|
2021
|
+
// fixed hold, which is the wrong shape for a direct manipulation: it delayed the
|
|
2022
|
+
// one case that should be instant (you have found the spot and stopped) in order to
|
|
2023
|
+
// protect the case that never needed protecting (you are still moving, and the lean
|
|
2024
|
+
// already covers it). The timer here is re-armed by MOVEMENT, so it expires a rest
|
|
2025
|
+
// window after the last movement rather than a fixed time after a decision.
|
|
2026
|
+
function trackRest(clientX: number): void {
|
|
2027
|
+
const dragged = draggingEl;
|
|
2028
|
+
if (!dragged) {
|
|
2046
2029
|
return;
|
|
2047
2030
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2031
|
+
const moved = restX === null || Math.abs(clientX - restX) > REORDER_MOVE_EPS_PX;
|
|
2032
|
+
restX = clientX;
|
|
2033
|
+
const before = dropTargetBefore(clientX);
|
|
2034
|
+
// Already the slot the tab holds, so there is nothing to preview or commit.
|
|
2035
|
+
// Withdraw anything pending: the pointer came back.
|
|
2036
|
+
if (before === dragged || before === dragged.nextElementSibling) {
|
|
2037
|
+
cancelRest();
|
|
2052
2038
|
return;
|
|
2053
2039
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2040
|
+
// Lean toward a NEW candidate. The test is guarded on a pending rest rather than
|
|
2041
|
+
// on `before !== restBefore` alone, because null is a legitimate candidate here
|
|
2042
|
+
// ("past the last chip") and would otherwise be indistinguishable from the
|
|
2043
|
+
// initial "nothing pending" state — which is exactly how the first sweep of a
|
|
2044
|
+
// drag toward the end of the strip ended up leaning nothing at all.
|
|
2045
|
+
if (restTimer === null || before !== restBefore) {
|
|
2046
|
+
restBefore = before;
|
|
2047
|
+
leanToward(before); // the sweep preview: where this will fall
|
|
2056
2048
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2049
|
+
// Movement pushes the commit out; stillness lets the armed timer run down to it.
|
|
2050
|
+
// A stationary pointer that has nothing armed still needs one — a browser may
|
|
2051
|
+
// deliver dragover repeatedly at an unchanged position, and the first of those is
|
|
2052
|
+
// the only signal that the pointer arrived and stopped.
|
|
2053
|
+
if (moved || restTimer === null) {
|
|
2054
|
+
if (restTimer !== null) {
|
|
2055
|
+
clearTimeout(restTimer);
|
|
2056
|
+
}
|
|
2057
|
+
restTimer = setTimeout(() => {
|
|
2058
|
+
restTimer = null;
|
|
2059
|
+
const target = restBefore;
|
|
2060
|
+
restBefore = null;
|
|
2061
|
+
commitSlot(target);
|
|
2062
|
+
}, REORDER_REST_MS);
|
|
2059
2063
|
}
|
|
2060
|
-
dwellBefore = before;
|
|
2061
|
-
leanToward(before);
|
|
2062
|
-
showDwellRail(before);
|
|
2063
|
-
dwellTimer = setTimeout(() => {
|
|
2064
|
-
dwellTimer = null;
|
|
2065
|
-
commitSlot(before);
|
|
2066
|
-
}, REORDER_DWELL_MS);
|
|
2067
2064
|
}
|
|
2068
|
-
function
|
|
2069
|
-
|
|
2070
|
-
if (dwellTimer === null) {
|
|
2065
|
+
function cancelRest(): void {
|
|
2066
|
+
if (restTimer === null) {
|
|
2071
2067
|
return;
|
|
2072
2068
|
}
|
|
2073
|
-
clearTimeout(
|
|
2074
|
-
|
|
2075
|
-
|
|
2069
|
+
clearTimeout(restTimer);
|
|
2070
|
+
restTimer = null;
|
|
2071
|
+
restBefore = null;
|
|
2076
2072
|
releaseLean();
|
|
2077
2073
|
}
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
if (dwellTimer === null) {
|
|
2074
|
+
// flushRest commits a pending slot at once, because a release is a decision. It is
|
|
2075
|
+
// what stops the rest window from ever being felt as a floor on the gesture: drop
|
|
2076
|
+
// mid-sweep and the tab lands where the lean said it would. Only a `drop` calls it;
|
|
2077
|
+
// an abandoned drag reaches dragend without one and reverts.
|
|
2078
|
+
function flushRest(): void {
|
|
2079
|
+
if (restTimer === null) {
|
|
2085
2080
|
return;
|
|
2086
2081
|
}
|
|
2087
|
-
const
|
|
2088
|
-
clearTimeout(
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
commitSlot(
|
|
2082
|
+
const target = restBefore;
|
|
2083
|
+
clearTimeout(restTimer);
|
|
2084
|
+
restTimer = null;
|
|
2085
|
+
restBefore = null;
|
|
2086
|
+
commitSlot(target);
|
|
2092
2087
|
}
|
|
2093
2088
|
|
|
2094
2089
|
// flipTo runs a rearranging mutation and animates its result: every chip that
|
|
@@ -2165,22 +2160,19 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2165
2160
|
// whatever the pointer is hovering.
|
|
2166
2161
|
//
|
|
2167
2162
|
// Every early return releases the lean, and that is not belt-and-braces. The
|
|
2168
|
-
// caller nulls `
|
|
2169
|
-
//
|
|
2170
|
-
//
|
|
2163
|
+
// caller nulls `restTimer` BEFORE calling in (both the rest timer and flushRest
|
|
2164
|
+
// do), so by the time control arrives here `cancelRest` has already become a no-op
|
|
2165
|
+
// and nothing else would ever hand those chips back.
|
|
2171
2166
|
function commitSlot(before: HTMLElement | null): void {
|
|
2172
|
-
// The countdown is over however this call ends: the rail's whole job was to
|
|
2173
|
-
// depict a hold that has now expired.
|
|
2174
|
-
hideDwellRail();
|
|
2175
2167
|
const dragged = draggingEl;
|
|
2176
2168
|
if (!dragged?.isConnected) {
|
|
2177
2169
|
releaseLean();
|
|
2178
|
-
return; // the dragged session was closed elsewhere mid-
|
|
2170
|
+
return; // the dragged session was closed elsewhere mid-gesture
|
|
2179
2171
|
}
|
|
2180
|
-
// The
|
|
2181
|
-
//
|
|
2182
|
-
//
|
|
2183
|
-
//
|
|
2172
|
+
// The pending slot was chosen a rest window ago, and a session closed in another
|
|
2173
|
+
// window (an SSE push, or the poll reconcile) removes chips through dropTab while
|
|
2174
|
+
// this gesture is still open — so the reference node may be gone by now, and
|
|
2175
|
+
// insertBefore throws NotFoundError on a reference that is no longer a
|
|
2184
2176
|
// child, which would abandon the drop mid-way and leave DOM order and tabList
|
|
2185
2177
|
// disagreeing. null stays legal: it means "past the last chip".
|
|
2186
2178
|
if (before !== null && before.parentNode !== scroller) {
|
|
@@ -2289,8 +2281,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2289
2281
|
// start from rest, and a torn-down feature may leave no inline style on an
|
|
2290
2282
|
// element it no longer owns.
|
|
2291
2283
|
function endReorderPreview(): void {
|
|
2292
|
-
|
|
2293
|
-
|
|
2284
|
+
cancelRest();
|
|
2285
|
+
restX = null;
|
|
2294
2286
|
endSlotFade();
|
|
2295
2287
|
}
|
|
2296
2288
|
|
|
@@ -2309,8 +2301,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2309
2301
|
// in progress" and so would preventDefault every unrelated file or text drop on
|
|
2310
2302
|
// the page from then on.
|
|
2311
2303
|
function abortReorderFor(removed: HTMLElement): void {
|
|
2312
|
-
if (removed ===
|
|
2313
|
-
|
|
2304
|
+
if (removed === restBefore) {
|
|
2305
|
+
cancelRest();
|
|
2314
2306
|
}
|
|
2315
2307
|
if (removed !== draggingEl) {
|
|
2316
2308
|
return;
|
|
@@ -3360,9 +3352,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3360
3352
|
}
|
|
3361
3353
|
return false;
|
|
3362
3354
|
});
|
|
3363
|
-
//
|
|
3364
|
-
//
|
|
3365
|
-
// hold that commits it (see the reorder-preview block above).
|
|
3355
|
+
// Drive the reorder preview while dragging a tab over the strip. A sweep only
|
|
3356
|
+
// leans the chips; the slot opens when the pointer comes to rest (trackRest).
|
|
3366
3357
|
bar.addEventListener("dragover", (e) => {
|
|
3367
3358
|
if (!draggingEl) {
|
|
3368
3359
|
return;
|
|
@@ -3371,7 +3362,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3371
3362
|
if (e.dataTransfer) {
|
|
3372
3363
|
e.dataTransfer.dropEffect = "move";
|
|
3373
3364
|
}
|
|
3374
|
-
|
|
3365
|
+
trackRest(e.clientX);
|
|
3375
3366
|
});
|
|
3376
3367
|
// Commit on drop — and, load-bearing, CANCEL the drop's default action. An
|
|
3377
3368
|
// uncancelled drop leaves the browser free to act on the drag payload
|
|
@@ -3386,16 +3377,17 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3386
3377
|
}
|
|
3387
3378
|
e.preventDefault();
|
|
3388
3379
|
dropped = true;
|
|
3389
|
-
|
|
3380
|
+
flushRest(); // a release decides: never make a drop wait on the rest window
|
|
3390
3381
|
syncOrderFromDom();
|
|
3391
3382
|
announceMoved(moved); // the ONE completed-move announcement per gesture
|
|
3392
3383
|
});
|
|
3393
|
-
// Leaving the strip
|
|
3394
|
-
//
|
|
3395
|
-
//
|
|
3396
|
-
//
|
|
3397
|
-
//
|
|
3398
|
-
//
|
|
3384
|
+
// Leaving the strip withdraws a pending slot. The document dragover below does
|
|
3385
|
+
// this too and covers more ground, but it cannot cover the case that matters most
|
|
3386
|
+
// here: the strip is docked at the viewport EDGE, so a pointer can leave it by
|
|
3387
|
+
// leaving the window entirely, and then no other element in the document receives a
|
|
3388
|
+
// dragover to notice with. A pointer that leaves the window is also, by definition,
|
|
3389
|
+
// one that has stopped moving over the strip — so without this the rest window
|
|
3390
|
+
// would run down and open a slot the pointer had already abandoned.
|
|
3399
3391
|
//
|
|
3400
3392
|
// relatedTarget is what makes dragleave usable at all: it fires on every
|
|
3401
3393
|
// child-to-child transition inside the bar (chip to label, label to close), and
|
|
@@ -3408,7 +3400,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3408
3400
|
}
|
|
3409
3401
|
const next = e.relatedTarget;
|
|
3410
3402
|
if (next === null || !bar.contains(next as Node)) {
|
|
3411
|
-
|
|
3403
|
+
cancelRest();
|
|
3412
3404
|
}
|
|
3413
3405
|
});
|
|
3414
3406
|
// A tab released anywhere OTHER than the strip — over the terminal, over
|
|
@@ -3435,17 +3427,17 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3435
3427
|
// refused release fell through to dragend and reverted. That distinction is
|
|
3436
3428
|
// invisible to the person doing the dragging.
|
|
3437
3429
|
//
|
|
3438
|
-
// So: leave `dropped` false,
|
|
3430
|
+
// So: leave `dropped` false, withdraw anything pending, and let dragend run the
|
|
3439
3431
|
// revert. Nothing is lost silently — the revert animates and announces, so a
|
|
3440
3432
|
// mis-release reads as "that did not take" rather than as a mystery.
|
|
3441
|
-
|
|
3433
|
+
cancelRest();
|
|
3442
3434
|
return;
|
|
3443
3435
|
}
|
|
3444
|
-
// dragover, off the strip: no candidate slot exists out here, so
|
|
3445
|
-
// pending
|
|
3446
|
-
// has already left. A slot already COMMITTED stays put; the pointer may yet
|
|
3447
|
-
//
|
|
3448
|
-
|
|
3436
|
+
// dragover, off the strip: no candidate slot exists out here, so withdraw a
|
|
3437
|
+
// pending one rather than let the rest window open a gap for a target the pointer
|
|
3438
|
+
// has already left. A slot already COMMITTED stays put; the pointer may yet come
|
|
3439
|
+
// back onto the strip and release there.
|
|
3440
|
+
cancelRest();
|
|
3449
3441
|
};
|
|
3450
3442
|
document.addEventListener("dragover", onDocTabDrop);
|
|
3451
3443
|
document.addEventListener("drop", onDocTabDrop);
|
|
@@ -21,28 +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
|
-
/**
|
|
25
|
-
* opens
|
|
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.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* This is a rest detector, not a hold. The difference is the whole interaction: a hold
|
|
28
|
+
* makes you wait a fixed time whatever you do, which is exactly the wrong shape here —
|
|
29
|
+
* a reorder is a direct manipulation and the answer to "when does it move" is "when you
|
|
30
|
+
* stop looking for the spot". Sweeping across the strip only leans the chips (cheap,
|
|
31
|
+
* instant, and it is what says where the tab will fall); coming to rest is what commits.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
33
|
+
* Sized to sit above the gap between two dragover events during a real sweep and below
|
|
34
|
+
* human perception of delay, so stopping reads as immediate. If a browser's dragover
|
|
35
|
+
* cadence is ever slower than this, the failure mode is benign: the strip commits the
|
|
36
|
+
* slot the pointer is over slightly eagerly, which is what it would have done anyway.
|
|
37
|
+
*
|
|
38
|
+
* A release never waits for it either — a drop commits whatever is pending (flushRest). */
|
|
39
|
+
export const REORDER_REST_MS = 120;
|
|
40
|
+
|
|
41
|
+
/** Movement (px) between two dragover events below which the pointer counts as still.
|
|
42
|
+
* A hand resting on a mouse is never perfectly still, and treating a 1px tremor as a
|
|
43
|
+
* sweep would keep pushing the commit out for as long as someone held the tab. */
|
|
44
|
+
export const REORDER_MOVE_EPS_PX = 3;
|
|
38
45
|
|
|
39
|
-
/** How far (px) each chip a commit would displace leans toward its destination
|
|
40
|
-
*
|
|
46
|
+
/** How far (px) each chip a commit would displace leans toward its destination while
|
|
47
|
+
* the pointer is still sweeping.
|
|
41
48
|
*
|
|
42
49
|
* Small enough to read as a lean rather than a move, and large enough to be
|
|
43
|
-
* unambiguous at a glance
|
|
44
|
-
*
|
|
45
|
-
*
|
|
50
|
+
* unambiguous at a glance: it is what answers "where will this fall" during the sweep,
|
|
51
|
+
* before anything has actually moved. The same purpose the mobile switcher's
|
|
52
|
+
* PREVIEW_PEEK_MAX serves for its live swipe. */
|
|
46
53
|
export const REORDER_LEAN_PX = 7;
|
|
47
54
|
|
|
48
55
|
/** The one transition both preview stages use (the lean, and the slide that
|