From ff53f31bfc7515ef2aad2427fd245837aae4c6f9 Mon Sep 17 00:00:00 2001 From: Kev Date: Fri, 17 Jul 2026 01:15:24 -0400 Subject: [PATCH] =?UTF-8?q?P0-4=20fix:=20mobile=20header=20collapses=20to?= =?UTF-8?q?=20ONE=20line=20=E2=80=94=20logo=20+=20clock=20+=20sync=20dot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phone audit: at 390px we still rendered the full desktop 3-row header (nav + TOP MOVES ticker + SYNC line) eating ~20% of the viewport, and its height clipped page titles under it (MY READS tabs, HEAD TO HEAD). Implemented Design's mobile app bar <768px: - New MobileSyncClock (extracted from HeartbeatBar) lives in the Nav's right cluster — wall clock rests, amber/STALE reacts off the shared freshness tier. - <768px: the ticker row (.nav-ticker) AND the whole heartbeat bar are hidden; only the nav row shows (logo + clock + search). main padding-top → 62px and the Slate sticky tabs → top:60px, so nothing clips under the bar. - Locked in vyndrParityQA (P0-4): ticker+heartbeat hidden, nav clock shown, paddings collapsed. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/unit/vyndrParityQA.test.js | 19 ++++++++++ web/src/app/globals.css | 14 ++++++++ web/src/components/Nav.tsx | 11 ++++-- web/src/components/Slate.tsx | 6 +++- web/src/components/vyndr/LiveLayer.tsx | 50 ++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 4 deletions(-) diff --git a/tests/unit/vyndrParityQA.test.js b/tests/unit/vyndrParityQA.test.js index 8eb0dc1..33574e1 100644 --- a/tests/unit/vyndrParityQA.test.js +++ b/tests/unit/vyndrParityQA.test.js @@ -246,3 +246,22 @@ describe('P0-1 — grade badges are token-derived; blue-B / amber-C banished', ( expect(css).toMatch(/\.grade-c-bg\s*\{[^}]*color-mix\(in srgb, var\(--grade-c\)/); }); }); + +// ── P0-4 lock — mobile header collapses to ONE line ────────────────────── +describe('P0-4 — mobile app bar is one line (ticker + heartbeat hidden)', () => { + const css = read('app/globals.css'); + it('<768px hides the ticker row + the whole heartbeat bar; nav clock shows', () => { + expect(css).toMatch(/@media\s*\(max-width:\s*767px\)\s*\{[^]*?\.nav-ticker\s*\{[^}]*display:\s*none/); + expect(css).toMatch(/@media\s*\(max-width:\s*767px\)\s*\{[^]*?\.heartbeat-bar\s*\{[^}]*display:\s*none/); + expect(css).toMatch(/\.nav-mobile-clock\s*\{\s*display:\s*none/); // desktop hides it + expect(css).toMatch(/@media[^]*?\.nav-mobile-clock\s*\{[^}]*display:\s*inline-flex/); + // the clock is wired into the nav, freshness-driven (never silently stale) + const nav = read('components/Nav.tsx'); + expect(nav).toMatch(/MobileSyncClock/); + expect(nav).toMatch(/nav-mobile-clock/); + }); + it('main top padding + slate sticky drop to the 60px bar on mobile (no clipping)', () => { + expect(css).toMatch(/@media[^]*?main\s*\{[^}]*padding-top:\s*62px/); + expect(css).toMatch(/\.slate-sticky-head\s*\{\s*top:\s*60px/); + }); +}); diff --git a/web/src/app/globals.css b/web/src/app/globals.css index d98cb24..160661e 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -1371,6 +1371,20 @@ html[data-font="readable"] .wm::after { opacity: 0.45 !important; } .gl-full.gl-expanded { margin-top: 10px; } } + +/* P0-4 — MOBILE APP BAR: collapse the 3-row header (nav + ticker + heartbeat) + to ONE line <768px. The ticker row + the whole heartbeat bar are hidden; the + nav's right cluster shows the app-bar clock (nav-mobile-clock). main's top + padding drops to the nav height alone so page titles stop clipping under it. */ +.nav-mobile-clock { display: none; } +@media (max-width: 767px) { + .nav-ticker { display: none !important; } + .heartbeat-bar { display: none !important; } + .nav-mobile-clock { display: inline-flex !important; } + main { padding-top: 62px !important; } + .slate-sticky-head { top: 60px !important; } /* sticky tabs clear the 60px app bar, not 122 */ +} + /* ============================================================ DESIGN COMPLETION TRAIN — M1 MOBILE FOUNDATION (2026-07-16) Now RECONCILED against Design's drawn mobile (specs/design-reference/ diff --git a/web/src/components/Nav.tsx b/web/src/components/Nav.tsx index af3097e..3c6206a 100644 --- a/web/src/components/Nav.tsx +++ b/web/src/components/Nav.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { usePathname } from 'next/navigation'; import { useAuth } from '@/contexts/AuthContext'; import { Wordmark, Ticker } from '@/components/vyndr'; -import { HeartbeatBar } from '@/components/vyndr/LiveLayer'; +import { HeartbeatBar, MobileSyncClock } from '@/components/vyndr/LiveLayer'; import NotificationBell from '@/components/NotificationBell'; // Nav labels are English literals for now; nav-string i18n lands in Phase G // (Session 38) once the locale dictionaries carry slate/terminal/etc. keys. @@ -190,6 +190,9 @@ export default function Nav() { the "›Query" pill was a duplicate link to /scan — deleted. A real ⌘K palette is a later nicety (spec §4), not a nav link. */}
+ {/* P0-4 — the mobile app-bar clock. Shown ONLY <768px (nav-mobile-clock); + the ticker + heartbeat bar are hidden there, so the header is ONE line. */} + {/* S6 (A1 board) — global search (players + teams). ⌘K opens the same modal; this icon is the mobile/mouse path (window.__search registered by GlobalHosts). */} @@ -412,8 +415,10 @@ export default function Nav() { {/* Ticker + heartbeat under the bar (§8 living layer). No fallback - items — real snapshot exhaust or nothing. */} - + items — real snapshot exhaust or nothing. P0-4: both hidden <768px + (mobile header is ONE line — the nav-mobile-clock in the bar carries + freshness; the ticker's own JS still runs but the row is display:none). */} +