Fix the header on phones: credits and clock no longer overlap
CI / check (push) Successful in 4m37s Details

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-04 23:17:41 -04:00
parent 754242dde6
commit 621eecb737
6 changed files with 27 additions and 10 deletions

View File

@ -1059,3 +1059,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **Game-time spans read "4.3 game h"** so they can't be mistaken for real hours next to a real countdown.
- **Reduced motion has two sources**: the OS media query (unless the player turned motion back on) and the player's override. The map keeps moving ships but stills its background.
- **Pure helpers read preferences directly** (`displayPreferences()`) where they format strings outside React (the dispatch sheet's problem lines); those re-render every second with the clock anyway.
## Fix: header on phones (user report during Phase 10K)
- **User report:** "On a phone screen the credits and time line can't be read" (screenshot at about 430 px): the credits chip was drawn over the DEADHEAD wordmark. The wordmark showed from 420 px, and 10K.2's gear plus 10K.3's device-locale clock ("2145-07-11 7:18 PM") made the right side wider than the space left.
- **Fix:** the wordmark now shows from 640 px (`sm`), the logo side clips instead of overflowing, the game clock shows only the time below 640 px (the date returns above), chips sit a little closer, and the gear is 32 px on phones. The gear icon is now a cog (it read as a sun).
- **Checked** in headless Chrome at 375, 390 and 430 px with a 1,234,567 cr balance and a 12-hour clock: no overlapping header items and no horizontal scroll (`docs/screenshots/header-phone-390.png`).

View File

@ -10,5 +10,12 @@ export { formatGameTime };
export function GameClock() {
const now = useClock((state) => state.now);
const clock = usePreferences((p) => p.clock);
return <time>{formatGameTime(realToGame(now, content.config), clock)}</time>;
const [date, ...time] = formatGameTime(realToGame(now, content.config), clock).split(" ");
// Phones show the time only; the date joins it from 640 px (it no longer fit).
return (
<time>
<span className="hidden sm:inline">{date} </span>
{time.join(" ")}
</time>
);
}

View File

@ -28,16 +28,17 @@ export function Layout({
return (
<div className="flex min-h-dvh flex-col text-ink-50 [--header-h:3.5rem] [--nav-h:calc(4.25rem+env(safe-area-inset-bottom))] md:pl-24 md:[--nav-h:0px]">
<header className="sticky top-0 z-10 flex h-(--header-h) items-center justify-between gap-2 border-b border-space-700/80 bg-space-950/80 px-3 backdrop-blur-xl sm:px-4">
<div className="flex min-w-0 items-center gap-2">
{/* The wordmark only fits beside the chips from 640 px; on phones the mark alone. */}
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<LogoMark />
<span className="hidden font-display text-lg font-bold tracking-[0.28em] min-[420px]:inline">
<span className="hidden font-display text-lg font-bold tracking-[0.28em] sm:inline">
DEADHEAD
</span>
<span className="hidden font-display text-[10px] tracking-[0.3em] text-cyan-300/70 uppercase lg:inline">
Space Freight
</span>
</div>
<div className="flex shrink-0 items-center gap-2">
<div className="flex shrink-0 items-center gap-1.5 sm:gap-2">
<div className="flex items-center gap-1.5 rounded-lg border border-amber-400/45 bg-amber-500/10 px-2.5 py-1 text-amber-200 shadow-glow-amber">
<Icon name="credits" className="size-4 text-amber-300" />
<span aria-label="Credits" className="font-display text-sm font-bold tabular-nums">
@ -53,7 +54,7 @@ export function Layout({
<Link
to="/settings"
aria-label="Settings"
className="grid size-9 place-items-center rounded-lg border border-space-600 bg-space-900/80 text-ink-300 transition hover:border-amber-400/60 hover:text-amber-200"
className="grid size-8 place-items-center rounded-lg border border-space-600 bg-space-900/80 text-ink-300 transition hover:border-amber-400/60 hover:text-amber-200 sm:size-9"
>
<Icon name="gear" className="size-5" />
</Link>

View File

@ -82,10 +82,11 @@ const PATHS = {
</>
),
close: <path d="M6 6l12 12M18 6 6 18" />,
// A cog (outline after Lucide "settings", ISC licence).
gear: (
<>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />
<path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2.1 2.1M16.6 16.6l2.1 2.1M5.3 18.7l2.1-2.1M16.6 7.4l2.1-2.1" />
</>
),
plus: <path d="M12 5v14M5 12h14" />,

View File

@ -1,6 +1,6 @@
import { content } from "@deadhead/content";
import { realToGame } from "@deadhead/sim";
import { act, render, screen } from "@testing-library/react";
import { act, render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ApiClient } from "../src/api/client";
import { serverNow, startClockTicker, syncClock, useClock } from "../src/clock/clock";
@ -51,14 +51,16 @@ describe("server-synced clock", () => {
it("shows the in-game time in the game clock and keeps it ticking", async () => {
await syncClock(serverAhead(0));
const stop = startClockTicker();
render(<GameClock />);
const { container } = render(<GameClock />);
// Date and time are separate spans (the date hides on phones).
const clock = () => container.querySelector("time")!;
// The default clock follows the device locale (CR-011).
const at = (real: number) => formatGameTime(realToGame(real, content.config), "locale");
expect(screen.getByText(at(DEVICE_NOW))).toBeInTheDocument();
expect(clock()).toHaveTextContent(at(DEVICE_NOW));
// One real minute is one game hour.
act(() => vi.advanceTimersByTime(60_000));
expect(screen.getByText(at(DEVICE_NOW + 60_000))).toBeInTheDocument();
expect(clock()).toHaveTextContent(at(DEVICE_NOW + 60_000));
stop();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB