P0-4 fix: mobile header collapses to ONE line — logo + clock + sync dot
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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. */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
{/* 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. */}
|
||||
<MobileSyncClock className="nav-mobile-clock" />
|
||||
{/* 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() {
|
||||
</nav>
|
||||
|
||||
{/* Ticker + heartbeat under the bar (§8 living layer). No fallback
|
||||
items — real snapshot exhaust or nothing. */}
|
||||
<Ticker items={[]} height={32} />
|
||||
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). */}
|
||||
<div className="nav-ticker"><Ticker items={[]} height={32} /></div>
|
||||
<HeartbeatBar />
|
||||
|
||||
<style jsx>{`
|
||||
|
||||
@@ -918,10 +918,14 @@ export default function Slate({ initialTab = 'all', tier = 'free', preferredBook
|
||||
<div style={{ display: 'grid', gap: 24, paddingBottom: 24 }}>
|
||||
{/* Sticky header — search + tabs */}
|
||||
<div
|
||||
className="slate-sticky-head"
|
||||
style={{
|
||||
position: 'sticky',
|
||||
// Clears nav (60) + heartbeat (30) + the dynamic ticker (Session 57:
|
||||
// --ticker-h is 32px only when the ticker has ≥4 real items).
|
||||
// --ticker-h is 32px only when the ticker has ≥4 real items). P0-4:
|
||||
// on mobile the header collapses to the 60px app bar (ticker +
|
||||
// heartbeat hidden), so .slate-sticky-head is overridden to top:60px
|
||||
// in globals.css — otherwise it stuck 62px below the bar.
|
||||
top: 'calc(90px + var(--ticker-h, 0px))',
|
||||
zIndex: 5,
|
||||
background: 'var(--bg-0, #0A0A0F)',
|
||||
|
||||
@@ -188,3 +188,53 @@ export function HeartbeatBar() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* MobileSyncClock (P0-4) — the mobile APP-BAR clock. Rendered in the Nav's right
|
||||
* cluster and shown ONLY <768px (where the ticker + the whole heartbeat bar are
|
||||
* hidden), so the mobile header collapses to ONE line: logo + clock + sync dot.
|
||||
* Wall clock rests (the stillness); amber → red STALE reacts off the shared
|
||||
* freshness tier (refreshed_at vs expected_interval_s). Never silently stale.
|
||||
*/
|
||||
export function MobileSyncClock({ className = '' }: { className?: string }) {
|
||||
const live = useLive();
|
||||
const [summary, setSummary] = useState<SnapshotSummary | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
const r = await fetch('/api/snapshot/summary', { cache: 'no-store' });
|
||||
if (!r.ok) return;
|
||||
const d = await r.json();
|
||||
if (active && typeof d?.graded === 'number') {
|
||||
setSummary({
|
||||
graded: d.graded,
|
||||
updated_at: d.updated_at ?? null,
|
||||
refreshed_at: d.refreshed_at ?? d.updated_at ?? null,
|
||||
expected_interval_s: Number(d.expected_interval_s) > 0 ? Number(d.expected_interval_s) : undefined,
|
||||
});
|
||||
}
|
||||
} catch { /* keep last known — never invent */ }
|
||||
};
|
||||
load();
|
||||
const id = setInterval(load, SUMMARY_POLL_MS);
|
||||
return () => { active = false; clearInterval(id); };
|
||||
}, []);
|
||||
void live.tick;
|
||||
const freshAt = summary?.refreshed_at ?? summary?.updated_at ?? null;
|
||||
const { tier, elapsedMs } = freshnessTier(freshAt ? Date.parse(freshAt) : NaN, summary?.expected_interval_s ?? 0, Date.now());
|
||||
const wall = mounted ? new Date().toLocaleTimeString('en-US', { hour12: false }) : '--:--:--';
|
||||
const view = tier === 'stale'
|
||||
? { label: `STALE ${elapsedMs !== null ? syncLabel(elapsedMs) : ''}`.trim(), color: 'var(--miss)', weight: 700 }
|
||||
: tier === 'amber'
|
||||
? { label: `SYNC ${elapsedMs !== null ? syncLabel(elapsedMs) : '—'}`, color: 'var(--amber)', weight: 700 }
|
||||
: { label: wall, color: 'var(--text-1)', weight: 400 };
|
||||
return (
|
||||
<span className={`mono ${className}`} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 10.5, color: view.color, fontWeight: view.weight, letterSpacing: '0.04em', flexShrink: 0 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: view.color, flexShrink: 0 }} />
|
||||
{view.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user