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

View File

@@ -7,6 +7,8 @@ describe("home page hierarchy", () => {
expect(source).toContain("useIsMobile"); expect(source).toContain("useIsMobile");
expect(source).toContain("grid-cols-[minmax(0,0.75fr)_minmax(0,1.25fr)]"); 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("AI capture");
expect(source).toContain("Event timeline"); expect(source).toContain("Event timeline");
}); });
@@ -25,4 +27,43 @@ describe("home page hierarchy", () => {
expect(source).toContain("Import"); expect(source).toContain("Import");
expect(source).toContain("Manual create"); 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"');
});
}); });