@cplieger/web-terminal-ui 5.4.1 → 5.4.2
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 +54 -139
- package/src/features/tabs/strip.ts +13 -20
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.2",
|
|
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",
|
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
} from "./model.js";
|
|
44
44
|
import { browserNotifierEnv, createNotifier } from "./notify.js";
|
|
45
45
|
import {
|
|
46
|
-
REORDER_LEAN_PX,
|
|
47
46
|
REORDER_MOVE_EPS_PX,
|
|
48
47
|
REORDER_REST_MS,
|
|
49
48
|
REORDER_SETTLE_MS,
|
|
@@ -667,10 +666,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
667
666
|
let activeId: string | null = null;
|
|
668
667
|
let draggingEl: HTMLElement | null = null;
|
|
669
668
|
// --- Desktop reorder preview state (mechanism below, near dragTargetBefore) ---
|
|
670
|
-
// The chips currently carrying an inline
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
// to hand every one of them back to the stylesheet.
|
|
669
|
+
// The chips currently carrying an inline displacement from the commit slide.
|
|
670
|
+
// A set plus one settle function, so whatever a slide wrote can always be handed
|
|
671
|
+
// back to the stylesheet — including when a second drag interrupts the first.
|
|
674
672
|
const shifted = new Set<HTMLElement>();
|
|
675
673
|
let shiftTimer: ReturnType<typeof setTimeout> | null = null;
|
|
676
674
|
// The slot waiting to open and the rest window that will open it.
|
|
@@ -1833,33 +1831,30 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1833
1831
|
// in. The chip you were dragging looked like the slot it left, the slot it
|
|
1834
1832
|
// was over, and the slot it would end up in.
|
|
1835
1833
|
//
|
|
1836
|
-
// The preview
|
|
1837
|
-
//
|
|
1838
|
-
//
|
|
1839
|
-
//
|
|
1840
|
-
//
|
|
1841
|
-
// pretending to be the tab):
|
|
1834
|
+
// The preview answers a MOVING pointer and a STOPPED one differently, and the
|
|
1835
|
+
// dragged chip stays in the flow throughout as the slot it will land in
|
|
1836
|
+
// (`.wt-tab-dragging`, which 30-tabs.css renders as an empty dashed outline
|
|
1837
|
+
// rather than a dimmed copy of the tab — the solid copy is the drag image
|
|
1838
|
+
// under the pointer, and one of the two had to stop pretending to be the tab):
|
|
1842
1839
|
//
|
|
1843
|
-
// - SWEEPING across the strip
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
// the
|
|
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.
|
|
1840
|
+
// - SWEEPING across the strip rearranges NOTHING. Crossing five tabs used to
|
|
1841
|
+
// move all five, which is the whole complaint.
|
|
1842
|
+
// - COMING TO REST opens the slot (trackRest -> commitSlot): one DOM move,
|
|
1843
|
+
// the displaced chips slide from their old positions to their new ones, and
|
|
1844
|
+
// the slot fades in at its new home.
|
|
1851
1845
|
//
|
|
1852
|
-
// Rest is DETECTED, not waited out: REORDER_REST_MS is re-armed by movement,
|
|
1853
|
-
//
|
|
1854
|
-
//
|
|
1855
|
-
//
|
|
1856
|
-
// one case that should be instant
|
|
1857
|
-
//
|
|
1858
|
-
//
|
|
1859
|
-
//
|
|
1846
|
+
// Rest is DETECTED, not waited out: REORDER_REST_MS is re-armed by movement, so
|
|
1847
|
+
// it expires a rest window after the last MOVEMENT rather than a fixed time
|
|
1848
|
+
// after a decision. Two shapes were tried and removed before this one, and both
|
|
1849
|
+
// failures are worth keeping: a fixed one-second HOLD (wrong shape for a direct
|
|
1850
|
+
// manipulation — it delayed the one case that should be instant, and grew a
|
|
1851
|
+
// progress bar to explain the wait, which is the tell that the wait should not
|
|
1852
|
+
// have been there), and a sweep-time LEAN of the chips a commit would displace
|
|
1853
|
+
// (it read as a second, competing preview and its distance from the real move
|
|
1854
|
+
// was too small to tell them apart).
|
|
1860
1855
|
//
|
|
1861
1856
|
// A release never waits either: `drop` commits whatever slot is pending
|
|
1862
|
-
// (flushRest), so dropping mid-sweep lands the tab where
|
|
1857
|
+
// (flushRest), so dropping mid-sweep still lands the tab where it was headed.
|
|
1863
1858
|
//
|
|
1864
1859
|
// dropTargetBefore returns the first tab whose horizontal midpoint is past
|
|
1865
1860
|
// x (the element the dragged tab should sit before), or null to drop at the
|
|
@@ -1869,8 +1864,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1869
1864
|
//
|
|
1870
1865
|
// It hit-tests LAYOUT geometry (offsetLeft/offsetWidth), never
|
|
1871
1866
|
// getBoundingClientRect. That is not an optimisation, it is what makes an
|
|
1872
|
-
// animated reorder hit-testable at all: a rect read while a chip is mid-
|
|
1873
|
-
//
|
|
1867
|
+
// animated reorder hit-testable at all: a rect read while a chip is mid-slide
|
|
1868
|
+
// returns the INTERPOLATED position, so the preview's own motion feeds
|
|
1874
1869
|
// back into the decision that produced it and the strip oscillates between two
|
|
1875
1870
|
// slots for as long as the pointer sits near a boundary. Layout offsets are the
|
|
1876
1871
|
// chip's position in the FLOW, which a transform does not affect at all.
|
|
@@ -1942,87 +1937,18 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1942
1937
|
shifted.clear();
|
|
1943
1938
|
}
|
|
1944
1939
|
|
|
1945
|
-
// leanToward tips the chips a commit would displace a few px toward their
|
|
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.
|
|
1953
|
-
//
|
|
1954
|
-
// Chips that were leaning and no longer would be are written back to zero in
|
|
1955
|
-
// the SAME pass, so a changed candidate eases from one lean into the other
|
|
1956
|
-
// rather than snapping through rest first.
|
|
1957
|
-
function leanToward(before: HTMLElement | null): void {
|
|
1958
|
-
if (!draggingEl) {
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
// The preference is read live, so it can flip mid-gesture. Clearing here rather
|
|
1962
|
-
// than returning bare is what honours a mid-hold opt-in: a bare return would
|
|
1963
|
-
// leave the lean already applied under the previous preference sitting there.
|
|
1964
|
-
if (prefersReduce()) {
|
|
1965
|
-
endShift();
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
const chips = [...scroller.querySelectorAll<HTMLElement>(".wt-tab")];
|
|
1969
|
-
const from = chips.indexOf(draggingEl);
|
|
1970
|
-
const to = before === null ? chips.length : chips.indexOf(before);
|
|
1971
|
-
if (from < 0 || to < 0) {
|
|
1972
|
-
return;
|
|
1973
|
-
}
|
|
1974
|
-
// Moving right past the chips in between shifts them LEFT, and moving left
|
|
1975
|
-
// shifts the chips it displaces RIGHT — the direction the user reads as
|
|
1976
|
-
// "that tab is getting out of the way".
|
|
1977
|
-
const movers = from < to ? chips.slice(from + 1, to) : chips.slice(to, from);
|
|
1978
|
-
const dx = from < to ? -REORDER_LEAN_PX : REORDER_LEAN_PX;
|
|
1979
|
-
const next = new Map<HTMLElement, number>();
|
|
1980
|
-
for (const el of movers) {
|
|
1981
|
-
next.set(el, dx);
|
|
1982
|
-
}
|
|
1983
|
-
for (const el of shifted) {
|
|
1984
|
-
if (!next.has(el)) {
|
|
1985
|
-
next.set(el, 0);
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
applyShift(next, REORDER_SHIFT_TRANS);
|
|
1989
|
-
}
|
|
1990
|
-
// releaseLean eases every leaning chip back to rest and settles it, for when
|
|
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.
|
|
1994
|
-
function releaseLean(): void {
|
|
1995
|
-
if (shifted.size === 0) {
|
|
1996
|
-
return;
|
|
1997
|
-
}
|
|
1998
|
-
// ...except under reduced motion, where snapping IS the requested behaviour and
|
|
1999
|
-
// easing back would animate the very motion the preference opted out of.
|
|
2000
|
-
if (prefersReduce()) {
|
|
2001
|
-
endShift();
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
const rest = new Map<HTMLElement, number>();
|
|
2005
|
-
for (const el of shifted) {
|
|
2006
|
-
rest.set(el, 0);
|
|
2007
|
-
}
|
|
2008
|
-
applyShift(rest, REORDER_SHIFT_TRANS);
|
|
2009
|
-
shiftTimer = setTimeout(endShift, REORDER_SETTLE_MS);
|
|
2010
|
-
}
|
|
2011
1940
|
// trackRest is the whole gate, called on every dragover over the strip.
|
|
2012
1941
|
//
|
|
2013
|
-
// Sweeping and settling are DIFFERENT actions and get different answers.
|
|
2014
|
-
// pointer
|
|
2015
|
-
//
|
|
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.
|
|
1942
|
+
// Sweeping and settling are DIFFERENT actions and get different answers. A
|
|
1943
|
+
// travelling pointer rearranges nothing at all; the moment it comes to REST the
|
|
1944
|
+
// slot opens.
|
|
2019
1945
|
//
|
|
2020
|
-
// Rest is detected, not waited out
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2023
|
-
//
|
|
2024
|
-
//
|
|
2025
|
-
//
|
|
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).
|
|
2026
1952
|
function trackRest(clientX: number): void {
|
|
2027
1953
|
const dragged = draggingEl;
|
|
2028
1954
|
if (!dragged) {
|
|
@@ -2037,15 +1963,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2037
1963
|
cancelRest();
|
|
2038
1964
|
return;
|
|
2039
1965
|
}
|
|
2040
|
-
|
|
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
|
|
2048
|
-
}
|
|
1966
|
+
restBefore = before;
|
|
2049
1967
|
// Movement pushes the commit out; stillness lets the armed timer run down to it.
|
|
2050
1968
|
// A stationary pointer that has nothing armed still needs one — a browser may
|
|
2051
1969
|
// deliver dragover repeatedly at an unchanged position, and the first of those is
|
|
@@ -2069,11 +1987,10 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2069
1987
|
clearTimeout(restTimer);
|
|
2070
1988
|
restTimer = null;
|
|
2071
1989
|
restBefore = null;
|
|
2072
|
-
releaseLean();
|
|
2073
1990
|
}
|
|
2074
1991
|
// flushRest commits a pending slot at once, because a release is a decision. It is
|
|
2075
1992
|
// what stops the rest window from ever being felt as a floor on the gesture: drop
|
|
2076
|
-
// mid-sweep and the tab lands where
|
|
1993
|
+
// mid-sweep and the tab still lands where it was headed. Only a `drop` calls it;
|
|
2077
1994
|
// an abandoned drag reaches dragend without one and reverts.
|
|
2078
1995
|
function flushRest(): void {
|
|
2079
1996
|
if (restTimer === null) {
|
|
@@ -2103,18 +2020,18 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2103
2020
|
// belongs HERE and not only in the CSS, because these transitions are written
|
|
2104
2021
|
// inline and no stylesheet gate can reach them; the switcher's release reel
|
|
2105
2022
|
// guards its own call site the same way. Read live, so toggling the OS setting
|
|
2106
|
-
// mid-drag takes effect on the next commit
|
|
2023
|
+
// mid-drag takes effect on the very next commit.
|
|
2107
2024
|
if (prefersReduce()) {
|
|
2108
2025
|
endShift();
|
|
2109
2026
|
mutate();
|
|
2110
2027
|
return;
|
|
2111
2028
|
}
|
|
2112
|
-
// FIRST — where each chip is right now,
|
|
2113
|
-
//
|
|
2114
|
-
// release reel captures its live swipe preview the
|
|
2115
|
-
// same reason. Rects, not layout offsets: this is VISUAL
|
|
2116
|
-
// what the animation interpolates (hit-testing wants the
|
|
2117
|
-
// dropTargetBefore).
|
|
2029
|
+
// FIRST — where each chip is right now, MID-SLIDE INCLUDED, so a second commit
|
|
2030
|
+
// continues from wherever the first one got to instead of jumping back through
|
|
2031
|
+
// rest first. The switcher's release reel captures its live swipe preview the
|
|
2032
|
+
// same way, and for the same reason. Rects, not layout offsets: this is VISUAL
|
|
2033
|
+
// position, which is what the animation interpolates (hit-testing wants the
|
|
2034
|
+
// opposite, see dropTargetBefore).
|
|
2118
2035
|
const first = new Map<HTMLElement, number>();
|
|
2119
2036
|
for (const el of scroller.querySelectorAll<HTMLElement>(".wt-tab")) {
|
|
2120
2037
|
if (el !== hold) {
|
|
@@ -2159,14 +2076,14 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2159
2076
|
// an OSC title) renders the committed order rather than flickering through
|
|
2160
2077
|
// whatever the pointer is hovering.
|
|
2161
2078
|
//
|
|
2162
|
-
//
|
|
2163
|
-
//
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2079
|
+
// The caller nulls `restTimer` BEFORE calling in (both the rest timer and
|
|
2080
|
+
// flushRest do), so `cancelRest` is already a no-op by the time control arrives
|
|
2081
|
+
// here: an early return must leave nothing behind that only cancelRest would
|
|
2082
|
+
// Nothing exists pre-commit for it to have cleared, and a slide already in flight
|
|
2083
|
+
// is owned by its own settle timer.
|
|
2166
2084
|
function commitSlot(before: HTMLElement | null): void {
|
|
2167
2085
|
const dragged = draggingEl;
|
|
2168
2086
|
if (!dragged?.isConnected) {
|
|
2169
|
-
releaseLean();
|
|
2170
2087
|
return; // the dragged session was closed elsewhere mid-gesture
|
|
2171
2088
|
}
|
|
2172
2089
|
// The pending slot was chosen a rest window ago, and a session closed in another
|
|
@@ -2176,11 +2093,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2176
2093
|
// child, which would abandon the drop mid-way and leave DOM order and tabList
|
|
2177
2094
|
// disagreeing. null stays legal: it means "past the last chip".
|
|
2178
2095
|
if (before !== null && before.parentNode !== scroller) {
|
|
2179
|
-
releaseLean();
|
|
2180
2096
|
return;
|
|
2181
2097
|
}
|
|
2182
2098
|
if (before === dragged || before === dragged.nextElementSibling) {
|
|
2183
|
-
releaseLean();
|
|
2184
2099
|
return; // already in that slot
|
|
2185
2100
|
}
|
|
2186
2101
|
flipTo(() => {
|
|
@@ -2270,8 +2185,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2270
2185
|
}
|
|
2271
2186
|
}
|
|
2272
2187
|
|
|
2273
|
-
// endReorderPreview ends the GESTURE's state: the pending
|
|
2274
|
-
//
|
|
2188
|
+
// endReorderPreview ends the GESTURE's state: the pending rest and the slot's
|
|
2189
|
+
// fade.
|
|
2275
2190
|
//
|
|
2276
2191
|
// It deliberately does NOT stop a slide that is already playing. A slide is an
|
|
2277
2192
|
// animation with its own settle timer, and cutting it here is exactly what
|
|
@@ -2291,9 +2206,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2291
2206
|
// sweep). Two removals matter, for different reasons:
|
|
2292
2207
|
//
|
|
2293
2208
|
// - the pending reference chip: commitSlot would hand a detached node to
|
|
2294
|
-
// insertBefore. It guards that too, but
|
|
2295
|
-
//
|
|
2296
|
-
//
|
|
2209
|
+
// insertBefore. It guards that too, but withdrawing the pending slot here means
|
|
2210
|
+
// a fresh dragover arms against a chip that still exists rather than waiting out
|
|
2211
|
+
// a rest window against one that does not.
|
|
2297
2212
|
// - the DRAGGED chip: the gesture is over and there is no source left to deliver
|
|
2298
2213
|
// a dragend. A browser is not obliged to fire one for a removed source, and
|
|
2299
2214
|
// without this the feature would keep `draggingEl` set to a detached node for
|
|
@@ -3353,7 +3268,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
3353
3268
|
return false;
|
|
3354
3269
|
});
|
|
3355
3270
|
// Drive the reorder preview while dragging a tab over the strip. A sweep only
|
|
3356
|
-
//
|
|
3271
|
+
// rearranges nothing; the slot opens when the pointer comes to rest (trackRest).
|
|
3357
3272
|
bar.addEventListener("dragover", (e) => {
|
|
3358
3273
|
if (!draggingEl) {
|
|
3359
3274
|
return;
|
|
@@ -24,34 +24,27 @@ import { statusPhrase, statusRevealsDot } from "./model.js";
|
|
|
24
24
|
/** The quiet window (ms) that means the pointer has STOPPED. When it elapses with no
|
|
25
25
|
* movement, the strip opens the slot the pointer is over.
|
|
26
26
|
*
|
|
27
|
-
* This is a rest detector, not a hold
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
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.
|
|
32
30
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
|
|
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;
|
|
40
42
|
|
|
41
43
|
/** Movement (px) between two dragover events below which the pointer counts as still.
|
|
42
44
|
* A hand resting on a mouse is never perfectly still, and treating a 1px tremor as a
|
|
43
45
|
* sweep would keep pushing the commit out for as long as someone held the tab. */
|
|
44
46
|
export const REORDER_MOVE_EPS_PX = 3;
|
|
45
47
|
|
|
46
|
-
/** How far (px) each chip a commit would displace leans toward its destination while
|
|
47
|
-
* the pointer is still sweeping.
|
|
48
|
-
*
|
|
49
|
-
* Small enough to read as a lean rather than a move, and large enough to be
|
|
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. */
|
|
53
|
-
export const REORDER_LEAN_PX = 7;
|
|
54
|
-
|
|
55
48
|
/** The one transition both preview stages use (the lean, and the slide that
|
|
56
49
|
* commits it).
|
|
57
50
|
*
|