@cplieger/web-terminal-ui 5.4.4 → 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 +1 -1
- package/src/features/tabs/index.ts +98 -74
- package/src/features/tabs/strip.ts +1 -1
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.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
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
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).
|
|
@@ -744,26 +742,58 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
744
742
|
// name, `list()` reports the real name, and clearing the progress needs no
|
|
745
743
|
// cleanup — the next paint simply stops adding it.
|
|
746
744
|
function relabelAll(): void {
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
|
|
745
|
+
// De-duplicate identical labels with a "(k)" suffix, numbered by the session's
|
|
746
|
+
// AGE — never by its position in the strip.
|
|
747
|
+
//
|
|
748
|
+
// The suffix used to be assigned by encounter order while walking tabList, which
|
|
749
|
+
// made it a property of the SLOT: two tabs both called "workspace" always read
|
|
750
|
+
// "workspace" then "workspace (2)" whichever way round they sat, so the label
|
|
751
|
+
// text stayed put while the sessions moved underneath it. Dragging one of them
|
|
752
|
+
// changed nothing on screen, and the reorder — which had worked correctly all
|
|
753
|
+
// along, server included — was invisible. It also meant dragging tab A could
|
|
754
|
+
// renumber tab B, which is the clearest sign the number belonged to the wrong
|
|
755
|
+
// thing.
|
|
756
|
+
//
|
|
757
|
+
// A label answers "which session is this", so it has to be a property of the
|
|
758
|
+
// session: stable while other tabs move, and travelling with its own tab. Age is
|
|
759
|
+
// that key. createdAt (the server's, tie-broken by id) rather than the local
|
|
760
|
+
// `born` counter, so every device and every reload agrees on which "workspace"
|
|
761
|
+
// is (2) — `born` is adoption order, which races.
|
|
762
|
+
//
|
|
763
|
+
// Numbering is recomputed over the LIVE group each time, so it stays contiguous:
|
|
764
|
+
// closing "(2)" of three renumbers the survivor rather than leaving a hole. That
|
|
765
|
+
// is a renumber with a visible cause the user just performed, which is the one
|
|
766
|
+
// kind that reads as sensible.
|
|
767
|
+
const groups = new Map<string, Tab[]>();
|
|
750
768
|
for (const t of tabList) {
|
|
751
769
|
const { text, fallback } = baseLabel(t);
|
|
752
|
-
|
|
753
|
-
|
|
770
|
+
// Fallback labels are not real names: several untitled tabs all read
|
|
771
|
+
// "New tab" with no numbering, which is the pre-existing behaviour.
|
|
772
|
+
if (fallback) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const group = groups.get(text);
|
|
776
|
+
if (group) {
|
|
777
|
+
group.push(t);
|
|
778
|
+
} else {
|
|
779
|
+
groups.set(text, [t]);
|
|
754
780
|
}
|
|
755
781
|
}
|
|
756
|
-
const
|
|
757
|
-
for (const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (!fallback && (counts.get(text) ?? 0) > 1) {
|
|
761
|
-
const k = (seen.get(text) ?? 0) + 1;
|
|
762
|
-
seen.set(text, k);
|
|
763
|
-
if (k > 1) {
|
|
764
|
-
display = `${text} (${String(k)})`;
|
|
765
|
-
}
|
|
782
|
+
const ordinals = new Map<Tab, number>();
|
|
783
|
+
for (const group of groups.values()) {
|
|
784
|
+
if (group.length < 2) {
|
|
785
|
+
continue; // nothing to disambiguate from, so no suffix at all
|
|
766
786
|
}
|
|
787
|
+
[...group]
|
|
788
|
+
.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id))
|
|
789
|
+
.forEach((t, i) => {
|
|
790
|
+
ordinals.set(t, i + 1);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
for (const t of tabList) {
|
|
794
|
+
const { text } = baseLabel(t);
|
|
795
|
+
const k = ordinals.get(t);
|
|
796
|
+
const display = k !== undefined && k > 1 ? `${text} (${String(k)})` : text;
|
|
767
797
|
t.display = display;
|
|
768
798
|
t.label.textContent = display;
|
|
769
799
|
// The accessible name carries the state and the percentage as well as
|
|
@@ -1864,7 +1894,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1864
1894
|
// was too small to tell them apart).
|
|
1865
1895
|
//
|
|
1866
1896
|
// A release never waits either: `drop` commits whatever slot is pending
|
|
1867
|
-
// (
|
|
1897
|
+
// (the drop handler commits the slot under the pointer), so dropping mid-sweep
|
|
1898
|
+
// still lands the tab exactly where it was headed.
|
|
1868
1899
|
//
|
|
1869
1900
|
// dropTargetBefore returns the first tab whose horizontal midpoint is past
|
|
1870
1901
|
// x (the element the dragged tab should sit before), or null to drop at the
|
|
@@ -1971,21 +2002,30 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1971
2002
|
if (!dragged) {
|
|
1972
2003
|
return;
|
|
1973
2004
|
}
|
|
2005
|
+
const now = Date.now();
|
|
1974
2006
|
const moved = restX === null || Math.abs(clientX - restX) > REORDER_MOVE_EPS_PX;
|
|
1975
2007
|
restX = clientX;
|
|
1976
|
-
const before = dropTargetBefore(clientX);
|
|
1977
|
-
// Already the slot the tab holds, so there is nothing to preview or commit.
|
|
1978
|
-
// Withdraw anything pending: the pointer came back.
|
|
1979
|
-
if (before === dragged || before === dragged.nextElementSibling) {
|
|
1980
|
-
cancelRest();
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
restBefore = before;
|
|
1984
|
-
const now = Date.now();
|
|
1985
2008
|
if (moved) {
|
|
1986
|
-
// Still travelling. Push the net out and wait.
|
|
1987
2009
|
restMovedAt = now;
|
|
1988
|
-
|
|
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();
|
|
1989
2029
|
return;
|
|
1990
2030
|
}
|
|
1991
2031
|
// A dragover at an UNCHANGED position is positive evidence that the pointer has
|
|
@@ -1994,48 +2034,31 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1994
2034
|
// cadence. The elapsed check filters the coincidence where one event of a sweep
|
|
1995
2035
|
// lands within REORDER_MOVE_EPS_PX of the previous (a reversal, or a frame whose
|
|
1996
2036
|
// motion was almost all vertical); a real stop clears it on the next event.
|
|
1997
|
-
if (now - restMovedAt >= REORDER_STILL_MS) {
|
|
1998
|
-
|
|
2037
|
+
if (!moved && now - restMovedAt >= REORDER_STILL_MS) {
|
|
2038
|
+
endRestNet();
|
|
2039
|
+
commitSlot(before);
|
|
1999
2040
|
return;
|
|
2000
2041
|
}
|
|
2001
|
-
armRestNet();
|
|
2042
|
+
armRestNet(before);
|
|
2002
2043
|
}
|
|
2003
2044
|
// armRestNet is the no-events fallback: if dragover stops arriving altogether, no
|
|
2004
|
-
// stationary event will ever confirm the stop, so this commits anyway.
|
|
2005
|
-
//
|
|
2006
|
-
//
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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();
|
|
2011
2052
|
restTimer = setTimeout(() => {
|
|
2012
2053
|
restTimer = null;
|
|
2013
|
-
|
|
2014
|
-
restBefore = null;
|
|
2015
|
-
commitSlot(target);
|
|
2054
|
+
commitSlot(before);
|
|
2016
2055
|
}, REORDER_REST_MS);
|
|
2017
2056
|
}
|
|
2018
|
-
function
|
|
2019
|
-
if (restTimer
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
clearTimeout(restTimer);
|
|
2023
|
-
restTimer = null;
|
|
2024
|
-
restBefore = null;
|
|
2025
|
-
}
|
|
2026
|
-
// flushRest commits a pending slot at once, because a release is a decision. It is
|
|
2027
|
-
// what stops the rest window from ever being felt as a floor on the gesture: drop
|
|
2028
|
-
// mid-sweep and the tab still lands where it was headed. Only a `drop` calls it;
|
|
2029
|
-
// an abandoned drag reaches dragend without one and reverts.
|
|
2030
|
-
function flushRest(): void {
|
|
2031
|
-
if (restTimer === null) {
|
|
2032
|
-
return;
|
|
2057
|
+
function endRestNet(): void {
|
|
2058
|
+
if (restTimer !== null) {
|
|
2059
|
+
clearTimeout(restTimer);
|
|
2060
|
+
restTimer = null;
|
|
2033
2061
|
}
|
|
2034
|
-
const target = restBefore;
|
|
2035
|
-
clearTimeout(restTimer);
|
|
2036
|
-
restTimer = null;
|
|
2037
|
-
restBefore = null;
|
|
2038
|
-
commitSlot(target);
|
|
2039
2062
|
}
|
|
2040
2063
|
|
|
2041
2064
|
// flipTo runs a rearranging mutation and animates its result: every chip that
|
|
@@ -2231,7 +2254,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2231
2254
|
// start from rest, and a torn-down feature may leave no inline style on an
|
|
2232
2255
|
// element it no longer owns.
|
|
2233
2256
|
function endReorderPreview(): void {
|
|
2234
|
-
|
|
2257
|
+
endRestNet();
|
|
2235
2258
|
restX = null;
|
|
2236
2259
|
endSlotFade();
|
|
2237
2260
|
}
|
|
@@ -2251,9 +2274,6 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2251
2274
|
// in progress" and so would preventDefault every unrelated file or text drop on
|
|
2252
2275
|
// the page from then on.
|
|
2253
2276
|
function abortReorderFor(removed: HTMLElement): void {
|
|
2254
|
-
if (removed === restBefore) {
|
|
2255
|
-
cancelRest();
|
|
2256
|
-
}
|
|
2257
2277
|
if (removed !== draggingEl) {
|
|
2258
2278
|
return;
|
|
2259
2279
|
}
|
|
@@ -3327,7 +3347,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3327
3347
|
}
|
|
3328
3348
|
e.preventDefault();
|
|
3329
3349
|
dropped = true;
|
|
3330
|
-
|
|
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));
|
|
3331
3355
|
syncOrderFromDom();
|
|
3332
3356
|
announceMoved(moved); // the ONE completed-move announcement per gesture
|
|
3333
3357
|
});
|
|
@@ -3350,7 +3374,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3350
3374
|
}
|
|
3351
3375
|
const next = e.relatedTarget;
|
|
3352
3376
|
if (next === null || !bar.contains(next as Node)) {
|
|
3353
|
-
|
|
3377
|
+
endRestNet();
|
|
3354
3378
|
}
|
|
3355
3379
|
});
|
|
3356
3380
|
// A tab released anywhere OTHER than the strip — over the terminal, over
|
|
@@ -3380,14 +3404,14 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3380
3404
|
// So: leave `dropped` false, withdraw anything pending, and let dragend run the
|
|
3381
3405
|
// revert. Nothing is lost silently — the revert animates and announces, so a
|
|
3382
3406
|
// mis-release reads as "that did not take" rather than as a mystery.
|
|
3383
|
-
|
|
3407
|
+
endRestNet();
|
|
3384
3408
|
return;
|
|
3385
3409
|
}
|
|
3386
3410
|
// dragover, off the strip: no candidate slot exists out here, so withdraw a
|
|
3387
3411
|
// pending one rather than let the rest window open a gap for a target the pointer
|
|
3388
3412
|
// has already left. A slot already COMMITTED stays put; the pointer may yet come
|
|
3389
3413
|
// back onto the strip and release there.
|
|
3390
|
-
|
|
3414
|
+
endRestNet();
|
|
3391
3415
|
};
|
|
3392
3416
|
document.addEventListener("dragover", onDocTabDrop);
|
|
3393
3417
|
document.addEventListener("drop", onDocTabDrop);
|
|
@@ -31,7 +31,7 @@ import { statusPhrase, statusRevealsDot } from "./model.js";
|
|
|
31
31
|
* pointer announces itself; this only filters the coincidence where one event of a
|
|
32
32
|
* sweep happens to land within REORDER_MOVE_EPS_PX of the previous one (a direction
|
|
33
33
|
* reversal, or a frame where the motion was almost entirely vertical). */
|
|
34
|
-
export const REORDER_STILL_MS =
|
|
34
|
+
export const REORDER_STILL_MS = 50;
|
|
35
35
|
|
|
36
36
|
/** Fallback guard (ms): commit the pending slot this long after the last MOVEMENT, for
|
|
37
37
|
* the case where `dragover` stops arriving altogether and no stationary event ever
|