Stop the bar dropping the one word it exists to say
The acting line names whoever the table is waiting on, and on a phone it
had stopped doing it. The name was set up as the half that gives way —
right rule, no floor under it. Flex shrinks the only item that can shrink
until the line fits, and with the clock button on the line there was no
fitting to be had: measured with five players nine-handed, mid-street,
360x640 "Christopher" wanted 62px, drawn 0. "Alexandra" 54 -> 0.
390x844 "Christopher" wanted 62px, drawn 0. "Alexandra" 54 -> 0.
430x932 "Christopher" 62 -> 31.
What the bar read on the first two was "· $1,200 behind" — a dangling
separator and a figure, at the moment the dealer is looking at it to say
whose turn it is out loud. The ellipsis the rule was written around never
appeared; there were no characters left to put one after.
The rule stays: a number read wrong is worse than a name read short, so
the stack is never truncated. It comes off the line whole instead, and
it is not lost — the same figure is on that player's own seat puck.
The stopwatch pays for the rest. It was on the line from the first second
of every decision, reading "0:07" in grey: 50px of the narrowest row in
the tool spent on a number nobody acts on. It is a prompt, not a readout,
and it now arrives when the button that acts on it arrives — the same
thirty-second threshold, asked once.
360x640 "Christopher" 62 -> 44 ("Christo…"), "Alexandra" 54 -> 44
390x844 both whole, where both were nothing
430x932 both whole
and "Mike from work" whole at 360 where it was cut to 68
with the bar 151px and the felt 309x463 either side of the thirty-second
mark, and 0px of seat movement across it.
Also: the space before the separator. These are two flex items and a flex
item's own leading whitespace is collapsed at its edge, so the bar has
read "Alexandra· $1,500 behind" since the sentence was split in two. The
gap is in the stylesheet now, where it can survive that.
1018 unit tests; 320 layout audits across 13 viewports, clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -3272,10 +3272,16 @@ body.hasActionBar #holdemApp {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* The gap is the space before the separator, and it is here rather than in the string
|
||||
because it cannot be in the string: these are two flex items, and a flex item's own
|
||||
leading whitespace is collapsed at its edge. The bar has been reading "Alexandra·
|
||||
$1,500 behind" ever since the name and the stack were split into two spans to decide
|
||||
which of them gives way. */
|
||||
.actionWho {
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--sp-1);
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -3289,13 +3295,26 @@ body.hasActionBar #holdemApp {
|
||||
|
||||
/* And the half that doesn't. The same rule the seat pucks keep: a number read wrong is
|
||||
a worse failure than a name read short, so the stack keeps every digit and the name
|
||||
is what loses characters. */
|
||||
is what loses characters.
|
||||
|
||||
With a floor under the name, which is what this was missing: shrinking the only
|
||||
shrinkable item until the line fits took "Christopher" to 0px on two of the three
|
||||
phones in the sweep, and what the bar then read was "· $1,200 behind". So when the
|
||||
name is being cut, this comes off the line instead — see ActionBar.fitActingLine.
|
||||
The figure is on that player's seat puck either way; the name is the thing only the
|
||||
bar is saying. */
|
||||
.actionWhoStack {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.actionWhoStack[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Stopwatch, not a shot clock: it colours up as a prompt to the dealer, and does
|
||||
nothing else. */
|
||||
nothing else — and a prompt that is on the line from the first second of every
|
||||
decision is not a prompt. It arrives when the button beside it does, half a minute
|
||||
in, which is also 50px of a 349px line handed back to the name. */
|
||||
.actionTank {
|
||||
flex: none;
|
||||
min-width: 3.1rem;
|
||||
@@ -3305,6 +3324,10 @@ body.hasActionBar #holdemApp {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.actionTank[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.actionTank.tankNudge {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
@@ -244,6 +244,9 @@ const ActionBar = {
|
||||
}
|
||||
|
||||
this.syncTankTimer();
|
||||
// After the tank timer, because whether the stopwatch and the clock button are
|
||||
// on the acting line is what decides how much room the name has left.
|
||||
this.fitActingLine();
|
||||
this.announce();
|
||||
},
|
||||
|
||||
@@ -420,16 +423,23 @@ const ActionBar = {
|
||||
|
||||
const whoStack = document.createElement("span");
|
||||
whoStack.className = "actionWhoStack";
|
||||
whoStack.textContent = ` · ${Money.format(Betting.stackOf(this.state, player))} behind`;
|
||||
// No leading space: this is a flex item and its own would be collapsed away
|
||||
// at the edge. .actionWho holds the two of them apart instead.
|
||||
whoStack.textContent = `· ${Money.format(Betting.stackOf(this.state, player))} behind`;
|
||||
who.appendChild(whoStack);
|
||||
|
||||
acting.appendChild(who);
|
||||
|
||||
// Built every render and shown by the tick, the same way the clock button
|
||||
// beside it is: a stopwatch reading "0:02" is 50px of the narrowest line in
|
||||
// the tool spent on a number nobody acts on. Starts hidden so a decision
|
||||
// that has only just landed on somebody never flashes one up for a frame.
|
||||
const tank = document.createElement("span");
|
||||
tank.className = "actionTank";
|
||||
tank.id = "actionTank";
|
||||
tank.title = "How long this player has been thinking";
|
||||
tank.textContent = "0:00";
|
||||
tank.hidden = true;
|
||||
acting.appendChild(tank);
|
||||
|
||||
// Rendered every time and shown by the tick once the wait is long enough
|
||||
@@ -475,6 +485,23 @@ const ActionBar = {
|
||||
// ellipsize, and only a real element can: as a bare text node the label
|
||||
// truncated *through* the keycap below it, which then sat squashed against
|
||||
// the cut-off text looking like a rendering fault.
|
||||
//
|
||||
// Known, measured, and not fixed here: on a phone this label has 73px and
|
||||
// four of the five labels a hand produces want more than that —
|
||||
//
|
||||
// ↶ dealing the hand 104 ↶ Christopher's fold 109
|
||||
// ↶ Mike from work's fold 128 ↶ Alexandra's fold 100
|
||||
// ↶ Dee's fold 69 (the only one that fits)
|
||||
//
|
||||
// so the pill reads "↶ dealing …" most of the night. Widening it is not
|
||||
// available — the room it would take is the room the name beside it needs,
|
||||
// and that is what fitActingLine is spending. The answer is the same shape
|
||||
// as the acting line's: the actor and the act as two spans, the actor
|
||||
// shrinking first, so "Mike from work's fold" degrades to "Mike f…'s fold"
|
||||
// rather than to "Mike from wor…" and the *act* — the half that says what
|
||||
// would come back — survives. That needs the two halves to come from the
|
||||
// call sites rather than be guessed out of a string with an apostrophe in
|
||||
// it, which is a change to what commit() is handed, and a round of its own.
|
||||
const label = document.createElement("span");
|
||||
label.className = "actionUndoLabel";
|
||||
label.textContent = `↶ ${undoLabel}`;
|
||||
@@ -626,12 +653,59 @@ const ActionBar = {
|
||||
}
|
||||
},
|
||||
|
||||
// Whether this decision has been going on long enough to be worth saying anything
|
||||
// about — which is the one question behind both the stopwatch and the button beside
|
||||
// it, so they ask it once and answer it together.
|
||||
//
|
||||
// The stopwatch used to be there from the first second of every decision, and for
|
||||
// the first half-minute it reads "0:07" in grey: a number nobody acts on, holding
|
||||
// 50px of a line that on a 390px phone has 349 to spend between a name, a stack, a
|
||||
// clock button and the undo pill. It is not a readout the dealer is watching; it is
|
||||
// a prompt, and a prompt that is always there is not one. See fitActingLine for
|
||||
// what the room it gives back is spent on.
|
||||
tankWorthShowing() {
|
||||
if (this.clockRunning()) return true;
|
||||
if (this.tankStartedAt === null) return false;
|
||||
// Nothing to time from — the action was already here when the page loaded. The
|
||||
// dash and its explanation are the answer to "why is there no clock on this
|
||||
// one", so it has to be visible to be asked.
|
||||
if (this.tankUnknownFor !== null && this.tankSeatIndex === this.tankUnknownFor) return true;
|
||||
return this.tankSeconds() >= this.NUDGE_AFTER_S;
|
||||
},
|
||||
|
||||
tankSeconds() {
|
||||
return Math.floor((Date.now() - this.tankStartedAt) / 1000);
|
||||
},
|
||||
|
||||
// Updates only the timer's own text, so the seconds ticking over never rebuilds the
|
||||
// buttons under the dealer's thumb.
|
||||
paintTank() {
|
||||
const el = document.getElementById("actionTank");
|
||||
if (!el || this.tankStartedAt === null) return;
|
||||
const nudge = document.getElementById("actionClockBtn");
|
||||
|
||||
const wasTank = el.hidden;
|
||||
const wasNudge = nudge ? nudge.hidden : null;
|
||||
|
||||
const worth = this.tankWorthShowing();
|
||||
el.hidden = !worth;
|
||||
// The button that starts a clock is offered once somebody has been thinking
|
||||
// long enough for the table to start asking. Before that it is a control nobody
|
||||
// needs, and this bar is docked over the seats. While a clock is already running
|
||||
// the button says "Stop the clock" and renderStatus decides whether it is there
|
||||
// at all — once the count has expired there is nothing left to stop.
|
||||
if (nudge && !this.clockRunning()) nudge.hidden = !worth;
|
||||
|
||||
if (!this.paintTankText(el)) return;
|
||||
|
||||
// Two things just left the line or joined it, so what is left has a different
|
||||
// amount of room than it did a second ago.
|
||||
if (el.hidden !== wasTank || (nudge && nudge.hidden !== wasNudge)) this.fitActingLine();
|
||||
},
|
||||
|
||||
// The readout itself. Returns false when it has asked for a full render instead, so
|
||||
// that nothing downstream measures a bar which is about to be rebuilt.
|
||||
paintTankText(el) {
|
||||
// A clock takes the readout over: the number the table is waiting on is no
|
||||
// longer how long this has taken but how long is left. At zero the bar has to
|
||||
// change shape — a ruling and the button that carries it out — so that one
|
||||
@@ -641,39 +715,60 @@ const ActionBar = {
|
||||
if (left === 0 && !this.clockExpiredShown) {
|
||||
this.clockExpiredShown = true;
|
||||
this.render();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
el.textContent = left <= this.CLOCK_COUNTDOWN_S ? `${left}` : this.formatTank(left);
|
||||
el.title = "Time left before the hand is decided";
|
||||
el.classList.toggle("tankNudge", left > this.CLOCK_COUNTDOWN_S);
|
||||
el.classList.toggle("tankSlow", left <= this.CLOCK_COUNTDOWN_S);
|
||||
return;
|
||||
}
|
||||
|
||||
// The button that starts a clock is offered once somebody has been thinking
|
||||
// long enough for the table to start asking — which is the same threshold the
|
||||
// stopwatch already marks. Before that it is a control nobody needs, and this
|
||||
// bar is docked over the seats.
|
||||
const nudge = document.getElementById("actionClockBtn");
|
||||
if (nudge && !this.clockRunning()) {
|
||||
const waited = Math.floor((Date.now() - this.tankStartedAt) / 1000);
|
||||
nudge.hidden = this.tankUnknownFor === this.tankSeatIndex
|
||||
? false
|
||||
: waited < this.NUDGE_AFTER_S;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.tankUnknownFor !== null && this.tankSeatIndex === this.tankUnknownFor) {
|
||||
el.textContent = "—";
|
||||
el.title = "The action was already on this seat when the page loaded, so there's nothing to time from yet. The clock starts when the action next moves.";
|
||||
el.classList.remove("tankNudge", "tankSlow");
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const seconds = Math.floor((Date.now() - this.tankStartedAt) / 1000);
|
||||
const seconds = this.tankSeconds();
|
||||
el.title = "How long this player has been thinking";
|
||||
el.textContent = this.formatTank(seconds);
|
||||
el.classList.toggle("tankNudge", seconds >= this.NUDGE_AFTER_S && seconds < this.SLOW_AFTER_S);
|
||||
el.classList.toggle("tankSlow", seconds >= this.SLOW_AFTER_S);
|
||||
return true;
|
||||
},
|
||||
|
||||
// Which half of "Christopher · $1,200 behind" survives when the line cannot hold
|
||||
// both of them.
|
||||
//
|
||||
// The name was set up to be the half that gives way, on the rule the seat pucks
|
||||
// keep: a number read wrong is a worse failure than a name read short. That rule is
|
||||
// right and it stays. What was missing under it was a floor. Flex shrinks the only
|
||||
// item that *can* shrink until the line fits, and with the clock button on the line
|
||||
// there is no fitting to be had — measured at 360x640 and again at 390x844, both
|
||||
// "Christopher" and "Alexandra" were drawn 0px wide, so the bar read
|
||||
//
|
||||
// · $1,200 behind
|
||||
//
|
||||
// a dangling separator and a figure, at the exact moment the dealer is looking at
|
||||
// the bar to say whose turn it is out loud. The ellipsis never appeared: there were
|
||||
// no characters left to put one after.
|
||||
//
|
||||
// So the stack comes off the line before the name gives up a single character. It is
|
||||
// not lost — it is on that player's own seat puck, a couple of inches up the same
|
||||
// screen, which is where a dealer counting somebody down is looking anyway. The name
|
||||
// is the thing the bar alone is saying.
|
||||
fitActingLine() {
|
||||
const name = document.querySelector("#actionBar .actionWhoName");
|
||||
const stack = document.querySelector("#actionBar .actionWhoStack");
|
||||
if (!name || !stack) return;
|
||||
// Always measured with the stack back on, so the answer is a property of the
|
||||
// line rather than of whatever the last render happened to leave behind.
|
||||
stack.hidden = false;
|
||||
// What the name wants against what flex gave it. A cut name reads as an
|
||||
// overflowing one even when the overflow is the whole word.
|
||||
if (name.scrollWidth > name.clientWidth + 1) stack.hidden = true;
|
||||
},
|
||||
|
||||
// "0:07", "4:01", and past the hour "1:05:03" rather than "65:03" — a table left
|
||||
|
||||
@@ -294,6 +294,176 @@ describe("the status block is the same number of lines whoever is acting", () =>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("and the name is the half that is still there at the end of it", () => {
|
||||
// Shrinking the name had no floor under it. Flex takes the only item that can give
|
||||
// way and takes it all the way, so with the clock button on the line there was
|
||||
// nothing left to give: measured with five players nine-handed, mid-street, forcing
|
||||
// the clock button on —
|
||||
//
|
||||
// 360x640 "Christopher" wanted 62px and was drawn 0. "Alexandra" 54 -> 0.
|
||||
// 390x844 "Christopher" wanted 62px and was drawn 0. "Alexandra" 54 -> 0.
|
||||
// 430x932 "Christopher" 62 -> 31.
|
||||
//
|
||||
// What the bar read at the first two was "· $1,200 behind" — the separator, the
|
||||
// figure, and no player. The ellipsis the rule was written around never appeared,
|
||||
// because there were no characters left to put one after.
|
||||
//
|
||||
// The rule itself is kept: a number read wrong is worse than a name read short, so
|
||||
// the stack is never truncated. It is taken off the line whole instead. Measured
|
||||
// again afterwards, with the same clock button on the same line:
|
||||
//
|
||||
// 360x640 "Christopher" 62 -> 44 ("Christo…"), "Alexandra" 54 -> 44
|
||||
// 390x844 both whole
|
||||
// 430x932 both whole
|
||||
//
|
||||
// and 0px of seat movement across the thirty-second mark at every one of them.
|
||||
const withLine = (name, stack, run) => {
|
||||
const el = (extra) => Object.assign({
|
||||
hidden: false, scrollWidth: 0, clientWidth: 0,
|
||||
}, extra);
|
||||
const nameEl = el(name);
|
||||
const stackEl = el(stack);
|
||||
global.document = {
|
||||
querySelector: sel => (sel.endsWith(".actionWhoName") ? nameEl
|
||||
: sel.endsWith(".actionWhoStack") ? stackEl : null),
|
||||
};
|
||||
try { run(nameEl, stackEl); } finally { delete global.document; }
|
||||
};
|
||||
|
||||
test("a name that fits keeps the stack beside it", () => {
|
||||
withLine({ scrollWidth: 54, clientWidth: 54 }, {}, (nameEl, stackEl) => {
|
||||
ActionBar.fitActingLine();
|
||||
assert.equal(stackEl.hidden, false);
|
||||
});
|
||||
});
|
||||
|
||||
test("a name being cut takes the stack off the line first", () => {
|
||||
withLine({ scrollWidth: 62, clientWidth: 0 }, {}, (nameEl, stackEl) => {
|
||||
ActionBar.fitActingLine();
|
||||
assert.equal(stackEl.hidden, true,
|
||||
"the stack is on that player's own seat puck; the name is only here");
|
||||
});
|
||||
});
|
||||
|
||||
test("and a stack already off it is put back before anything is measured", () => {
|
||||
// Otherwise the answer is a property of whatever the last render left behind
|
||||
// rather than of the line: a name that fits only *because* the stack is missing
|
||||
// would keep it missing for the rest of the night.
|
||||
withLine({ scrollWidth: 54, clientWidth: 54 }, { hidden: true }, (nameEl, stackEl) => {
|
||||
ActionBar.fitActingLine();
|
||||
assert.equal(stackEl.hidden, false);
|
||||
});
|
||||
});
|
||||
|
||||
test("a hair of rounding is not a cut", () => {
|
||||
withLine({ scrollWidth: 55, clientWidth: 54 }, {}, (nameEl, stackEl) => {
|
||||
ActionBar.fitActingLine();
|
||||
assert.equal(stackEl.hidden, false);
|
||||
});
|
||||
});
|
||||
|
||||
test("and a bar with nobody acting on it is left alone", () => {
|
||||
global.document = { querySelector: () => null };
|
||||
try { assert.doesNotThrow(() => ActionBar.fitActingLine()); }
|
||||
finally { delete global.document; }
|
||||
});
|
||||
|
||||
test("the stylesheet can actually take it off the line", () => {
|
||||
// `hidden` is a UA-stylesheet default, and .actionWhoStack sits inside a flex
|
||||
// container. Said out loud here so a later `display:` on the class can't quietly
|
||||
// put the figure back.
|
||||
assert.match(CSS, /\.actionWhoStack\[hidden\]\s*\{[^}]*display:\s*none/);
|
||||
assert.match(CSS, /\.actionTank\[hidden\]\s*\{[^}]*display:\s*none/);
|
||||
});
|
||||
|
||||
test("the space before the separator is in the stylesheet, not in the string", () => {
|
||||
// These are two flex items, and a flex item's own leading whitespace is collapsed
|
||||
// at its edge — so ` · $1,200 behind` drew as `Alexandra· $1,500 behind` from the
|
||||
// day the sentence was split in two.
|
||||
const who = CSS.match(/\n\.actionWho \{([^}]*)\}/);
|
||||
assert.match(who[1], /gap:\s*var\(--sp-1\)/);
|
||||
assert.match(CSS_JS, /whoStack\.textContent = `· \$\{/,
|
||||
"a leading space here is invisible and would double the gap if it weren't");
|
||||
});
|
||||
|
||||
test("and the fit is taken after the timer has decided whether it is on the line", () => {
|
||||
const render = CSS_JS.slice(CSS_JS.indexOf(" render() {"));
|
||||
const body = render.slice(0, render.indexOf("\n },"));
|
||||
const tank = body.indexOf("this.syncTankTimer()");
|
||||
const fit = body.indexOf("this.fitActingLine()");
|
||||
assert.ok(tank >= 0 && fit >= 0, "render() should do both");
|
||||
assert.ok(fit > tank,
|
||||
"measured before the stopwatch is hidden, the name is measured against 50px it has");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("a stopwatch that is only there when it is saying something", () => {
|
||||
// 50px of a 349px line, from the first second of every decision, reading "0:07" in
|
||||
// grey. It is a prompt rather than a readout — nothing happens when it grows — and a
|
||||
// prompt that is always on screen is not a prompt. It arrives when the button that
|
||||
// acts on it arrives, which is the same threshold, and the room goes to the name.
|
||||
const withClock = ({ seat = 0, started = null, unknown = null, clockAt = null }, run) => {
|
||||
const saved = {
|
||||
state: ActionBar.state, tankSeatIndex: ActionBar.tankSeatIndex,
|
||||
tankStartedAt: ActionBar.tankStartedAt, tankUnknownFor: ActionBar.tankUnknownFor,
|
||||
clockSeatIndex: ActionBar.clockSeatIndex, clockStartedAt: ActionBar.clockStartedAt,
|
||||
};
|
||||
ActionBar.state = { actingSeatIndex: seat };
|
||||
ActionBar.tankSeatIndex = seat;
|
||||
ActionBar.tankStartedAt = started;
|
||||
ActionBar.tankUnknownFor = unknown;
|
||||
ActionBar.clockSeatIndex = clockAt === null ? null : seat;
|
||||
ActionBar.clockStartedAt = clockAt;
|
||||
try { run(); } finally { Object.assign(ActionBar, saved); }
|
||||
};
|
||||
|
||||
test("a decision that has only just landed on somebody shows neither", () => {
|
||||
withClock({ started: Date.now() - 3000 }, () =>
|
||||
assert.equal(ActionBar.tankWorthShowing(), false));
|
||||
});
|
||||
|
||||
test("and both arrive together, at the threshold the stopwatch already marked", () => {
|
||||
withClock({ started: Date.now() - (ActionBar.NUDGE_AFTER_S * 1000) }, () =>
|
||||
assert.equal(ActionBar.tankWorthShowing(), true));
|
||||
});
|
||||
|
||||
test("a clock already running is always worth watching", () => {
|
||||
// The readout is a countdown by then, and the button beside it says "Stop the
|
||||
// clock" — there is no second at which either of those is not the point.
|
||||
withClock({ started: Date.now(), clockAt: Date.now() }, () =>
|
||||
assert.equal(ActionBar.tankWorthShowing(), true));
|
||||
});
|
||||
|
||||
test("so is the seat whose wait cannot be known", () => {
|
||||
// The action was already on them when the page loaded. The dash and the sentence
|
||||
// behind it are the answer to "why is there no time on this one", so the answer
|
||||
// has to be visible for the question to get asked.
|
||||
withClock({ started: Date.now(), unknown: 0 }, () =>
|
||||
assert.equal(ActionBar.tankWorthShowing(), true));
|
||||
});
|
||||
|
||||
test("and with nothing to time from at all, nothing is shown", () => {
|
||||
withClock({ started: null }, () =>
|
||||
assert.equal(ActionBar.tankWorthShowing(), false));
|
||||
});
|
||||
|
||||
test("it is built hidden, so a fresh decision never flashes one up", () => {
|
||||
assert.match(CSS_JS, /tank\.hidden = true;/);
|
||||
});
|
||||
|
||||
test("and the tick re-fits the line only when one of them actually moved", () => {
|
||||
// The line is measured by forcing a layout, and this runs once a second all
|
||||
// night. Nothing has changed on the seconds in between.
|
||||
const paint = CSS_JS.slice(CSS_JS.indexOf(" paintTank() {"));
|
||||
const body = paint.slice(0, paint.indexOf("\n },"));
|
||||
assert.match(body, /if \(el\.hidden !== wasTank \|\| \(nudge && nudge\.hidden !== wasNudge\)\) this\.fitActingLine\(\)/);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("how much of the bottom is spoken for", () => {
|
||||
// feltRoomFor asks one question — what is under the felt — and gets two different
|
||||
// answers depending on the layout. Under the bar it is the bar. Under the rail it is
|
||||
|
||||
Reference in New Issue
Block a user