refactor(header): improve mobile header layout with responsive action controls
This commit is contained in:
@@ -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,8 +481,15 @@ export default function HomePage() {
|
|||||||
)}
|
)}
|
||||||
<span>{isOnline ? "Online ready" : "Offline mode"}</span>
|
<span>{isOnline ? "Online ready" : "Offline mode"}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isMobile
|
||||||
|
? mobileUtilityActionsClasses
|
||||||
|
: desktopUtilityActionsClasses
|
||||||
|
}
|
||||||
|
>
|
||||||
<SignIn />
|
<SignIn />
|
||||||
<ModeToggle />
|
{!isMobile && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -479,14 +499,28 @@ export default function HomePage() {
|
|||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
More
|
{moreTriggerLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
|
{isMobile && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={events.length === 0}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<DropdownMenuItem onClick={openManualEventDialog}>
|
<DropdownMenuItem onClick={openManualEventDialog}>
|
||||||
Manual create
|
Manual create
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -504,6 +538,7 @@ export default function HomePage() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 space-y-4">
|
<main className="flex-1 space-y-4">
|
||||||
|
|||||||
@@ -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"');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user