commit 26dcd2023550ef895c9522e818559e433f3e6282 truex <truex@equwal.com> 2026-09-26 19:21:35 -0700 Keep the status strip one line high, so the page does not move On the phone a long event message wrapped in the strip and pushed the page down 31.5 CSS px, and the page went up again when the message faded 6.6 s later. The strip is now one line of a fixed height (32 px). A long text ends with an ellipsis, and the title attribute of its part holds the full text. Links, buttons and "Follow paused" do not shrink, and the player text shrinks last. At 360 CSS px the top of the page stays at 84 px before, during and after a long message.
src/status.ts | 28 ++++++++++++++++++++++------ src/style.css | 46 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 12 deletions(-)
diff --git a/src/status.ts b/src/status.ts index ff9eb68..0b6fc4f 100644 --- a/src/status.ts +++ b/src/status.ts @@ -1,6 +1,10 @@ // The status strip under the top bar. It has three parts: the player, // the reading of the pages, and the last event. Only the event part is a // live region, because the other parts change often. +// +// The strip is one line of a fixed height, so the page under it never +// moves. A long text ends with an ellipsis, and its title attribute holds +// the full text. import type { ActionId, Message } from './messages'; @@ -23,11 +27,15 @@ const event = el('status'); let fadeTimer: ReturnType<typeof setTimeout> | undefined; /** - * Writes a message into `target`: the text, then the link or the button. A - * click on the button runs the handler of onAction. + * Writes a message into `target`: the text in its own element, so that the + * strip can cut it with an ellipsis, then the link or the button. A click + * on the button runs the handler of onAction. */ export function fill(target: HTMLElement, m: Message): void { - target.textContent = m.text; + const text = document.createElement('span'); + text.className = 'text'; + text.textContent = m.text; + target.replaceChildren(text); if (m.link) { const a = document.createElement('a'); a.href = m.link.href; @@ -46,6 +54,13 @@ export function fill(target: HTMLElement, m: Message): void { } } +/** Writes a message into a part of the strip, with the full text in the title attribute. */ +function fillPart(target: HTMLElement, m: Message): void { + fill(target, m); + if (m.text) target.title = m.text; + else target.removeAttribute('title'); +} + let runAction: ((id: ActionId) => void) | null = null; /** Sets the handler of the buttons that fill makes. */ @@ -70,24 +85,25 @@ export function showPlayer(m: Message | null): void { if (key === lastPlayer) return; lastPlayer = key; player.hidden = m === null; - fill(player, m ?? { text: '' }); + fillPart(player, m ?? { text: '' }); } /** Shows how far the reading of the pages is. Null hides this part. */ export function showReading(m: Message | string | null): void { reading.hidden = m === null; - fill(reading, typeof m === 'string' ? { text: m } : (m ?? { text: '' })); + fillPart(reading, typeof m === 'string' ? { text: m } : (m ?? { text: '' })); } /** Shows an event, for example a lookup or an error. It fades after EVENT_MS. */ export function say(m: Message | string): void { - fill(event, typeof m === 'string' ? { text: m } : m); + fillPart(event, typeof m === 'string' ? { text: m } : m); event.classList.remove('faded'); clearTimeout(fadeTimer); fadeTimer = setTimeout(() => { event.classList.add('faded'); fadeTimer = setTimeout(() => { event.replaceChildren(); + event.removeAttribute('title'); event.classList.remove('faded'); }, FADE_MS); }, EVENT_MS); diff --git a/src/style.css b/src/style.css index bf8ec96..929339e 100644 --- a/src/style.css +++ b/src/style.css @@ -152,24 +152,58 @@ body { text-align: center; } -/* The status strip: the player, the reading of the pages, the last event. */ +/* + * The status strip: the player, the reading of the pages, the last event. + * One line of a fixed height, so the page under it never moves: a long + * message wrapped and pushed the page down, and the page went up again + * when the message faded. A long text ends with an ellipsis. Its title + * attribute holds the full text (status.ts). + */ #strip { flex: none; box-sizing: border-box; - min-height: 1.5rem; + height: 2rem; display: flex; - flex-wrap: wrap; - align-items: baseline; + flex-wrap: nowrap; + align-items: stretch; column-gap: 0.75rem; - padding: 0.15rem 0.5rem; + padding: 0 0.5rem; font-size: 0.8rem; line-height: 1.3; + white-space: nowrap; + overflow: hidden; border-bottom: 1px solid #8884; - overflow-wrap: anywhere; } #strip > * { + display: flex; + align-items: center; + gap: 0.3rem; + min-width: 0; + flex: 0 1 auto; +} + +/* The player text is short and always there: the other parts shrink first. */ +#player-status { + flex-shrink: 0.3; +} + +#strip .text { min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +/* A link, a button and "Follow paused" do not shrink: a cut button cannot be read. */ +#strip a, +#strip button.action, +#follow-note { + flex: none; +} + +#strip button.action { + margin: 3px 0; + align-self: stretch; } /* A strip with no text takes no space. */