Skip to content

Dock the start-failure banner above the footer, not inside it - #2366

Merged
mholzi merged 1 commit into
mainfrom
fix/2365-banner-anchor
Aug 23, 2026
Merged

Dock the start-failure banner above the footer, not inside it#2366
mholzi merged 1 commit into
mainfrom
fix/2365-banner-anchor

Conversation

@mholzi

@mholzi mholzi commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Closes #2365.

The bug

Pressing Start with no players joined rendered "Cannot start" as a narrow column between the two footer buttons, wrapping mid-word — beitrete / n, scanne / n. Reported from a phone on v4.3.1-rc1.

The cause

The banner was fine. The anchor was wrong.

showBanner inserts its node before the anchor, inside the anchor's parent. Handing it the Start button therefore did not put the banner above the button — it put it into the button's row:

.home-cta-bar { display: flex; … }
.home-cta-start { flex: 1; }

The banner became a third flex item, the Start button claimed the free space, and the banner was left at its min-content width — one word for a sentence, one character for a long word.

"Above the button" and "before the button in a horizontal row" are the same DOM operation and two very different layouts. showBanner's own docstring already said what was meant: "docks directly above the primary action".

The change

1. bannerAnchorFor() in admin/util.js resolves the Start button to its footer row. Falls back to the button when there is no row, and to null when there is no button — showSetupError reads that null to fall back to a toast, so it has to survive. Extracted rather than inlined so the decision that caused the bug is unit-testable; the file's existing helpers set that precedent.

2. A guard on .beatify-banner: flex-basis: 100%. No effect outside a flex parent, so it costs nothing in the normal case. Should a future call site anchor a banner inside a flex row again, it claims its own line rather than being squeezed.

3. flex-wrap: wrap on .home-cta-bar — without it the guard above cannot take effect, because a 100% basis still shares a line in a non-wrapping row. It also lets the buttons themselves wrap on a narrow phone rather than squeeze.

Scope of the exposure

Checked, as asked: there is exactly one production call site for showBannershowSetupError in admin.js — and all four of its callers funnel through it. The test files call it directly, which is fine. So this is the whole exposure, not a first instance.

Tests

Four in banner-anchor-2365.test.js. Verified against the broken behaviour rather than only the fixed one: with the anchor returning the button, the first test fails and the three guard tests stay green — which is exactly what they are for.

Full suites: vitest 645 in 66 files, pytest 2083 passed / 2 skipped, eslint 0 errors. npm run build ran (the bundles are in the diff).

🤖 Generated with Claude Code

https://claude.ai/code/session_012W9Pj7vqs7Yv31j19B4vRt

…e it

Closes #2365.

Pressing Start with no players joined rendered "Cannot start" as a narrow
column between the two footer buttons, wrapping mid-word — `beitrete` / `n`,
`scanne` / `n`. Reported from a phone on v4.3.1-rc1.

The banner was fine; the anchor was wrong. `showBanner` inserts its node
*before the anchor, inside the anchor's parent*, so handing it the Start
button put the banner **into** `.home-cta-bar`. That bar is a flex row and
`.home-cta-start` claims the free space with `flex: 1`, leaving the banner at
its min-content width — one word for a sentence, one character for a long
word.

"Above the button" and "before the button in a horizontal row" are the same
DOM operation and two very different layouts. `showBanner`'s own docstring
already said what was meant: "docks directly above the primary action".

Three changes:

* `bannerAnchorFor()` in admin/util.js resolves the Start button to its
  footer row, falling back to the button when there is no row and to null
  when there is no button (showSetupError reads that null to fall back to a
  toast). Extracted rather than inlined so the decision that caused the bug
  is unit-testable.
* A guard on `.beatify-banner`: `flex-basis: 100%`. It has no effect outside
  a flex parent, so it costs nothing normally; should a future call site
  anchor a banner inside a flex row again, it claims its own line instead of
  being squeezed.
* `flex-wrap: wrap` on `.home-cta-bar`, without which that guard cannot take
  effect. It also lets the buttons themselves wrap on a narrow phone rather
  than squeeze.

Only one production call site reaches showBanner (showSetupError), and all
four of its callers go through it, so this is the whole exposure.

Tests: four in banner-anchor-2365.test.js. Verified against the broken
behaviour — with the anchor returning the button, the first test fails and the
three guard tests stay green, which is what they are for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012W9Pj7vqs7Yv31j19B4vRt
@mholzi
mholzi merged commit e1d4f66 into main Aug 23, 2026
11 checks passed
@mholzi
mholzi deleted the fix/2365-banner-anchor branch August 23, 2026 19:58
mholzi added a commit that referenced this pull request Aug 23, 2026
Bumps the manifest to 4.3.1-rc2, adds the changelog entry for what landed
since the previous build, and adds the pre-release notes.

Two changes since the last tag: the Tidal name fallback (#2364) and the
start-failure banner layout fix (#2365 / #2366). The dead YouTube links
repaired in #2362 are catalogue maintenance and stay out of the user-facing
notes, as pre-release notes always do.

The notes file is force-added — `docs/` is gitignored and every previous
release-notes file was added the same way.


Claude-Session: https://claude.ai/code/session_012W9Pj7vqs7Yv31j19B4vRt

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Start-error banner is squeezed into the button row and wraps one character per line

2 participants