From ef5a30c7983affaa4fb0ae0a6bd77adaa9d3858c Mon Sep 17 00:00:00 2001 From: davd-gzl <60177543+davd-gzl@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:33:58 +0900 Subject: [PATCH] Header: every control in the phone row is the same size On phones Settings was 42x44 while Install was 34x35 and the menu 38x33: the only sizing rule matched .icon-ctl inside .header-quick, which Install does not carry and the menu sits outside. One --hdr-ctl size now applies to the logo, the pass select and the three buttons: 44px from 360px up, shrinking below so MAX SENIOR still fits beside the select's arrow. npm run verify now fails when those controls differ by more than 1px at 360 and 390px. --- scripts/verify-render.mjs | 28 +++++++++++++++++++++++++++- src/styles.css | 17 +++++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/scripts/verify-render.mjs b/scripts/verify-render.mjs index f11cc91..d79ee7d 100644 --- a/scripts/verify-render.mjs +++ b/scripts/verify-render.mjs @@ -3,7 +3,8 @@ * * Serves ./dist and loads it in headless Chromium (home + a deep-link). Fails * (exit 1) if #app stays effectively empty or any uncaught page error fires, so - * a build that would show a blank page can never reach production. + * a build that would show a blank page can never reach production. It also + * fails when the controls in the phone header row differ in size. * * npm run build && npm run verify */ @@ -76,6 +77,31 @@ for (const { name, url } of pages) { await page.close(); } +// Phone header: every visible control in the one-line row shares one height, and +// every control but the pass select shares one width. +const phone = await browser.newPage(); +for (const width of [360, 390]) { + // Only a change of isMobile/hasTouch reloads the page; a width change reflows it. + await phone.setViewport({ width, height: 844, isMobile: true, hasTouch: true }); + if (width === 360) { + await phone.goto(BASE, { waitUntil: "load", timeout: 45000 }); + await phone.waitForSelector(".header-quick select"); + } + const ctls = await phone.evaluate(() => + [...document.querySelectorAll(".site-header .logo, .header-quick > *, .header-nav > .menu-btn")] + .map((e) => ({ e, r: e.getBoundingClientRect() })) + .filter(({ r }) => r.width > 0 && r.height > 0) + .map(({ e, r }) => ({ name: e.className || e.tagName, select: e.tagName === "SELECT", w: r.width, h: r.height })), + ); + const spread = (xs) => Math.max(...xs) - Math.min(...xs); + const icons = ctls.filter((c) => !c.select); + const sizes = ctls.map((c) => `${c.name} ${c.w.toFixed(1)}x${c.h.toFixed(1)}`).join(", "); + if (spread(ctls.map((c) => c.h)) > 1) failures.push(`[header ${width}px] control heights differ: ${sizes}`); + if (spread(icons.map((c) => c.w)) > 1) failures.push(`[header ${width}px] icon button widths differ: ${sizes}`); + console.log(` header ${width}px: ${sizes}`); +} +await phone.close(); + await browser.close(); server.close(); diff --git a/src/styles.css b/src/styles.css index a734ab5..d441cc9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4366,6 +4366,9 @@ select.input::-ms-expand { .site-header { flex-wrap: nowrap; gap: 0.5rem; + /* One square size for the row. 184px is 53px of padding and gaps plus the + 131px that fits "MAX SENIOR"; below 360px the squares shrink instead. */ + --hdr-ctl: clamp(34px, (100vw - 184px) / 4, 44px); } .brand-badge { display: none; @@ -4392,13 +4395,19 @@ select.input::-ms-expand { .header-quick select.ctl { flex: 1; min-width: 0; - min-height: 44px; + height: var(--hdr-ctl); font-size: 0.95rem; font-weight: 600; } - .header-quick .icon-ctl { - width: 42px; - min-height: 44px; + /* Install carries no .icon-ctl and the menu sits outside .header-quick, so + each is named here beside Settings. */ + .site-header .logo, + .header-quick .icon-ctl, + .header-quick .install-btn, + .header-nav > .menu-btn { + width: var(--hdr-ctl); + height: var(--hdr-ctl); + padding: 0; flex: none; } .header-quick .install-label {