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. */}