From aa3d26f3149e277621fb3f38c5c4dcd9ce070485 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Tue, 5 May 2026 01:36:08 -0700 Subject: [PATCH] fix(nav): stop nav bar from jumping when Live is selected (#1046) (#1078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The `🔴 Live` nav link could wrap onto two lines at certain viewport widths once it became the `.active` link, which grew `.nav-link`'s height and made the whole `.top-nav` "hop" the instant Live was selected (issue #1046). Adding `white-space: nowrap` to the base `.nav-link` rule keeps every nav label on a single line at every breakpoint (default desktop + the 768–1279px and <768px responsive overrides), eliminating the jump. ## Changes - `public/style.css` — `white-space: nowrap` on `.nav-link`. - `test-e2e-playwright.js` — new assertion at viewport 1115px (the width in the issue screenshots) that: - computed `white-space` prevents wrapping - the Live link renders on a single line in both states - `.top-nav` height does not change when `.active` is toggled ## TDD - Red commit `ba906a5` — test added, fails because base `.nav-link` has no `white-space` rule (default `normal`). - Green commit `51906cb` — single-line CSS fix makes the test pass. Fixes #1046 --------- Co-authored-by: corescope-bot --- public/style.css | 1 + test-e2e-playwright.js | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/public/style.css b/public/style.css index 081bfa82..9719bba5 100644 --- a/public/style.css +++ b/public/style.css @@ -398,6 +398,7 @@ input[type="week"] { border-bottom: 2px solid transparent; transition: all .15s; background: none; border-top: none; border-left: none; border-right: none; cursor: pointer; font-family: var(--font); + white-space: nowrap; /* #1046: never wrap labels — wrapping makes the nav bar grow taller */ } .nav-link:hover { color: var(--nav-text); } .nav-link.active { diff --git a/test-e2e-playwright.js b/test-e2e-playwright.js index 9c5393ed..9ed6fb42 100644 --- a/test-e2e-playwright.js +++ b/test-e2e-playwright.js @@ -832,6 +832,57 @@ async function run() { // --- Group: Live page --- + // Test (issue #1046): Activating the Live nav link MUST NOT cause the + // "🔴 Live" label to wrap onto two lines, which makes the whole top + // nav bar grow taller and "hop". The label has to stay on one line in + // every state, and the nav bar height must be identical with/without + // the .active class. + await test('Live nav-link does not wrap or change nav height when active (#1046)', async () => { + // Use the exact viewport width from the issue screenshots. + await page.setViewportSize({ width: 1115, height: 800 }); + await page.goto(BASE, { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('a.nav-link[data-route="live"]'); + + const measure = await page.evaluate(() => { + const link = document.querySelector('a.nav-link[data-route="live"]'); + const nav = document.querySelector('.top-nav'); + const ws = getComputedStyle(link).whiteSpace; + // Force inactive state. + const wasActive = link.classList.contains('active'); + link.classList.remove('active'); + const inactive = { + navH: nav.getBoundingClientRect().height, + lines: link.getClientRects().length, + }; + // Force active state. + link.classList.add('active'); + const active = { + navH: nav.getBoundingClientRect().height, + lines: link.getClientRects().length, + }; + // Restore. + link.classList.toggle('active', wasActive); + return { ws, inactive, active }; + }); + + assert( + ['nowrap', 'pre', 'pre-wrap'].includes(measure.ws), + `Live nav-link must not wrap; computed white-space=${measure.ws}`, + ); + assert( + measure.inactive.lines === 1, + `Live nav-link must render on one line when inactive (got ${measure.inactive.lines})`, + ); + assert( + measure.active.lines === 1, + `Live nav-link must render on one line when active (got ${measure.active.lines})`, + ); + assert( + measure.active.navH === measure.inactive.navH, + `Top nav height must not change when Live becomes active (inactive=${measure.inactive.navH}, active=${measure.active.navH})`, + ); + }); + // Test: Live page loads with map and stats await test('Live page loads with map and stats', async () => { await page.goto(`${BASE}/#/live`, { waitUntil: 'domcontentloaded' });