refactor(header): improve mobile header layout with responsive action controls

This commit is contained in:
2026-04-21 23:23:43 -04:00
parent 7f7c945396
commit 88265678db
2 changed files with 112 additions and 36 deletions

View File

@@ -15,7 +15,6 @@ import { DragDropContainer } from "@/components/drag-drop-container";
import { EventDialog } from "@/components/event-dialog";
import { EventsList } from "@/components/events-list";
import { IcsFilePicker } from "@/components/ics-file-picker";
import { ModeToggle } from "@/components/mode-toggle";
import { SettingsPanel } from "@/components/settings-panel";
import SignIn from "@/components/sign-in";
import { Badge } from "@/components/ui/badge";
@@ -432,12 +431,21 @@ export default function HomePage() {
isMobile ? "px-4 pb-24 pt-4" : "px-8 py-4",
);
const appHeaderSurfaceClasses = getAppHeaderSurfaceClasses(isMobile);
const headerLayoutClasses = cn(
isMobile ? "flex-col items-start" : "items-center justify-between",
);
const appSectionSurfaceClasses = getAppSectionSurfaceClasses(isMobile);
const appNavSurfaceClasses = getAppNavSurfaceClasses(isMobile);
const mainContentClasses = cn(
"grid items-start gap-4",
isMobile ? "grid-cols-1" : "grid-cols-[minmax(0,0.75fr)_minmax(0,1.25fr)]",
);
const headerActionsClasses = isMobile
? "flex w-full items-center justify-between gap-2"
: "flex flex-wrap items-center justify-end gap-2";
const mobileUtilityActionsClasses = "flex items-center gap-2";
const desktopUtilityActionsClasses = "flex items-center gap-2";
const moreTriggerLabel = isMobile ? null : "More";
return (
<DragDropContainer
@@ -447,8 +455,13 @@ export default function HomePage() {
onImageDrop={(file) => handleImagesSelect([file])}
>
<div className={appFrameClasses}>
<header className={appHeaderSurfaceClasses}>
<div className="flex min-w-0 flex-col">
<header className={cn(appHeaderSurfaceClasses, headerLayoutClasses)}>
<div
className={cn(
"flex min-w-0 flex-col",
isMobile ? "w-full" : undefined,
)}
>
<p className="font-mono text-[11px] uppercase text-muted-foreground">
Local Calendar
</p>
@@ -456,7 +469,7 @@ export default function HomePage() {
Event timeline
</h1>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
<div className={headerActionsClasses}>
<Badge
variant="outline"
className={getConnectionBadgeClasses(isOnline)}
@@ -468,8 +481,15 @@ export default function HomePage() {
)}
<span>{isOnline ? "Online ready" : "Offline mode"}</span>
</Badge>
<div
className={
isMobile
? mobileUtilityActionsClasses
: desktopUtilityActionsClasses
}
>
<SignIn />
<ModeToggle />
{!isMobile && (
<Button
type="button"
variant="outline"
@@ -479,14 +499,28 @@ export default function HomePage() {
>
Export
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm">
<Button
type="button"
variant="outline"
size={isMobile ? "icon" : "sm"}
aria-label="More actions"
>
<MoreHorizontal className="h-4 w-4" />
More
{moreTriggerLabel}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{isMobile && (
<DropdownMenuItem
onClick={handleExport}
disabled={events.length === 0}
>
Export
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={openManualEventDialog}>
Manual create
</DropdownMenuItem>
@@ -504,6 +538,7 @@ export default function HomePage() {
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</header>
<main className="flex-1 space-y-4">

View File

@@ -7,6 +7,8 @@ describe("home page hierarchy", () => {
expect(source).toContain("useIsMobile");
expect(source).toContain("grid-cols-[minmax(0,0.75fr)_minmax(0,1.25fr)]");
expect(source).toContain('"items-center justify-between"');
expect(source).toContain("desktopUtilityActionsClasses");
expect(source).toContain("AI capture");
expect(source).toContain("Event timeline");
});
@@ -25,4 +27,43 @@ describe("home page hierarchy", () => {
expect(source).toContain("Import");
expect(source).toContain("Manual create");
});
test("mobile header gives controls their own full-width row so the title is not squeezed by actions", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain("isMobile ? \"w-full\" : undefined");
expect(source).toContain("headerActionsClasses");
expect(source).toContain('"flex w-full items-center justify-between gap-2"');
expect(source).toContain("flex flex-wrap items-center justify-end gap-2");
});
test("mobile header keeps export inside the More menu instead of a dedicated button", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain("!isMobile && (");
expect(source).toContain("Export");
expect(source).toContain("onClick={handleExport}");
});
test("mobile More trigger collapses to an icon button so utility actions fit on one row", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain('size={isMobile ? "icon" : "sm"}');
expect(source).toContain("moreTriggerLabel");
expect(source).toContain('isMobile ? null : "More"');
});
test("mobile header omits the theme toggle so auth and overflow actions have a stable hierarchy", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).not.toContain("<ModeToggle />");
});
test("mobile header uses a column layout with a dedicated action row instead of desktop justify-between framing", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain("headerLayoutClasses");
expect(source).toContain('isMobile ? "flex-col items-start"');
expect(source).toContain('"flex w-full items-center justify-between gap-2"');
});
});