From 3380d4c22514037aba8b2601d11b1c286298a857 Mon Sep 17 00:00:00 2001 From: Jim Bethancourt Date: Fri, 16 Oct 2026 13:22:26 -0500 Subject: [PATCH] Make menu surface edge-to-edge and align with header --- app/globals.css | 49 +++++++++++++++++++------------- tests/unit/layout-styles.test.js | 34 ++++++++++++++++++---- tests/unit/theme-css.test.js | 11 +++---- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/app/globals.css b/app/globals.css index 49b1578..5eac59b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -55,7 +55,8 @@ --shadow: 0 1px 4px rgba(0, 0, 0, 0.12); /* mvp.css caps header/footer/main at --width-content (1080px by default). Report pages and listings should fill the view: content uses 95% of the - viewport width instead. */ + viewport width instead. (The top menu opts out entirely — #top-menu + sets max-width: none so the menu surface reaches the page edges.) */ --width-content: min(3840px, 95vw); } @@ -289,6 +290,9 @@ pre { whole rendered box (padding contributes outside max-height). */ padding: 0; max-height: var(--menu-max-height); + /* mvp.css also caps every header at --width-content; the menu surface + must reach the left and right edges of the page instead. */ + max-width: none; } .menu-bar { @@ -296,7 +300,14 @@ pre { align-items: center; gap: 0.4rem; flex-wrap: nowrap; - max-width: 1200px; + /* The menu surface (#top-menu) spans the full viewport, but its content + keeps the centered --width-content column. border-box makes the cap + bound the rendered box (padding inside), matching .theme-bar so the + two rows' content edges align. The margin: 0 auto both centers the + column and resets mvp.css's 7rem nav margin — without the reset the + bar's bottom margin pushes #top-menu past its max-height cap. */ + max-width: var(--width-content); + box-sizing: border-box; margin: 0 auto; padding: 0.3rem 0.8rem; min-height: 48px; @@ -382,12 +393,12 @@ pre { /* ---- Breadcrumbs (user/report trail under the top menu) ---- */ .breadcrumbs { /* Lives on the .theme-bar row (rendered by app/layout.jsx), sharing the - line with the color theme toggle: the row owns the menu-column - alignment formula (max-width: var(--width-content) + 0.8rem horizontal - padding, see .theme-bar above), so the trail must add no box-model - offsets of its own — flex-grow fills the row's left side, keeping the - first crumb's glyph at the menu bar's left content edge while the - toggle pins to the right edge, both vertically centered. */ + line with the color theme toggle: the row carries the menu-column + alignment formula (max-width: var(--width-content) + 0.8rem horizontal + padding, see .theme-bar below), so the trail must add no box-model + offsets of its own — flex-grow fills the row's left side, keeping the + first crumb's glyph at the menu bar's left content edge while the + toggle pins to the right edge, both vertically centered. */ flex: 1 1 auto; min-width: 0; /* mvp.css gives bare nav a 7rem bottom margin, bold text and a spaced flex @@ -459,9 +470,9 @@ pre { .hero-search { position: relative; - /* Exactly half the rendered width of the top menu (#top-menu is capped by - mvp.css at --width-content = 1080px, full width below that) and centered - under it. */ + /* Exactly half the menu content column (.menu-bar is capped at + --width-content = 1080px, full width below that) and centered under + the edge-to-edge header. */ width: min(540px, 50vw); margin: 1rem auto; text-align: left; @@ -716,14 +727,14 @@ pre { /* ---- Color theme toggle (three radio modes, pure-CSS switching) ---- */ /* Row below the site header (rendered by app/layout.jsx): the breadcrumb trail (when the route has one) on the left and the toggle on the right, - vertically centered on one line. The alignment formula .breadcrumbs used - to carry lives here now: --width-content column, 0.8rem horizontal - padding, border-box — keeping the first crumb's glyph at the menu bar's - left content edge and the toggle at its right content edge at every - viewport, the two edges mirroring each other. #top-menu is capped by - mvp.css at --width-content, so the row and the menu bar can never drift - apart. Crumb routes grow the trail (flex: 1 1 auto); crumb-less routes - leave only the toggle, pinned right by flex-end. */ + vertically centered on one line. The row shares the menu bar's centered + --width-content content column (the #top-menu surface around it is edge + to edge, but the bar keeps the column): 0.8rem horizontal padding, + border-box — keeping the first crumb's glyph at the menu bar's left + content edge and the toggle at its right content edge at every viewport, + the two edges mirroring each other, so the row and the menu bar can + never drift apart. Crumb routes grow the trail (flex: 1 1 auto); + crumb-less routes leave only the toggle, pinned right by flex-end. */ .theme-bar { display: flex; align-items: center; diff --git a/tests/unit/layout-styles.test.js b/tests/unit/layout-styles.test.js index 540ed9a..1c55dec 100644 --- a/tests/unit/layout-styles.test.js +++ b/tests/unit/layout-styles.test.js @@ -56,13 +56,37 @@ describe('layout styles (mvp.css section-flex reset)', () => { expect(body).toMatch(/margin:\s*0\s+auto\s*;/); }); + test('the menu surface spans the viewport, its content stays centered', () => { + // mvp.css caps every header (and footer/main) at --width-content; the + // menu surface must reach the page's left/right edges instead, while + // the inner bar keeps the centered --width-content content column and + // the margin reset of mvp.css's 7rem nav margin. The .theme-bar row + // below mirrors the bar's content edges (guarded in + // tests/unit/theme-css.test.js and the E2E alignment checks). + // ruleMap keeps only the last block per selector (#top-menu and + // .menu-bar also appear inside the 900px media query), so scan every + // block in the raw source instead. + const menuBlocks = [...css.matchAll(/#top-menu\s*\{([^}]*)\}/g)] + .map((m) => m[1]); + expect(menuBlocks.length).toBeGreaterThan(0); + expect(menuBlocks.some((b) => /max-width:\s*none/.test(b))).toBe(true); + const barBlocks = [...css.matchAll(/\.menu-bar\s*\{([^}]*)\}/g)] + .map((m) => m[1]); + expect(barBlocks.length).toBeGreaterThan(0); + expect(barBlocks.some((b) => /max-width:\s*var\(--width-content\)/.test(b))).toBe(true); + // border-box keeps the padding inside the capped column, matching + // .theme-bar's geometry (guarded in tests/unit/theme-css.test.js). + expect(barBlocks.some((b) => /box-sizing:\s*border-box/.test(b))).toBe(true); + expect(barBlocks.some((b) => /margin:\s*0\s+auto/.test(b))).toBe(true); + }); + test('the breadcrumb trail stretches inside the theme bar row (menu column)', () => { // The trail now shares the .theme-bar row with the color theme toggle: - // the row carries the alignment formula (max-width: var(--width-content) - // + 0.8rem horizontal padding, guarded in tests/unit/theme-css.test.js), - // so .breadcrumbs must not add its own box-model offsets — it grows to - // fill the row's left side, keeping the first crumb's glyph at the menu - // column's left content edge while the toggle pins to the right edge. + // the row is edge to edge like the menu bar (guarded in + // tests/unit/theme-css.test.js), so .breadcrumbs must not add its own + // box-model offsets — it grows to fill the row's left side, keeping the + // first crumb's glyph at the menu bar's left content edge while the + // toggle pins to the right edge. const body = rules.get('.breadcrumbs'); expect(body).toBeTruthy(); expect(body).toMatch(/flex:\s*1\s+1\s+auto/); diff --git a/tests/unit/theme-css.test.js b/tests/unit/theme-css.test.js index c18efb1..026e9be 100644 --- a/tests/unit/theme-css.test.js +++ b/tests/unit/theme-css.test.js @@ -289,11 +289,12 @@ describe('toggle styles', () => { expect(height).toBeGreaterThanOrEqual(1.5); }); - it('the row below the header mirrors the breadcrumb alignment formula', () => { - // Same invariant layout-styles.test.js pinned for the standalone - // .breadcrumbs bar: the row shares #top-menu's --width-content column - // and the 0.8rem horizontal padding insets its children to the menu - // bar's content edges at every viewport — the toggle on the right, the + it('the row below the header mirrors the menu content column', () => { + // The top menu surface is edge to edge (globals.css overrides mvp.css's + // --width-content cap on #top-menu), but the .menu-bar content inside + // keeps the centered --width-content column; the row must share it so + // its 0.8rem horizontal padding insets its children to the menu bar's + // content edges at every viewport — the toggle on the right, the // breadcrumb trail on the left (flex-grow), vertically centered. const row = rules.get('.theme-bar'); expect(row).toBeTruthy();