Add mobile bottom tab bar for small screen navigation

- New MobileTabBar component with Home, Search, and Account tabs
- Animated indicator follows active tab (spring layoutId transition)
- Shown only below sm breakpoint where desktop nav links are hidden
- Account tab provides quick sign-out access
- Safe area padding for devices with home indicators
- Added bottom padding to layout to prevent content overlap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 11:33:52 -05:00
co-authored by Claude Opus 4.6
parent f7d5179d0f
commit 30d074e2ca
2 changed files with 81 additions and 1 deletions
+3 -1
View File
@@ -3,6 +3,7 @@
import { CommandPalette } from "@/components/command-palette";
import { KeyboardHelpDialog } from "@/components/keyboard-help-dialog";
import { KeyboardProvider } from "@/components/keyboard-provider";
import { MobileTabBar } from "@/components/mobile-tab-bar";
import { NavBar } from "@/components/nav-bar";
import { Toaster } from "@/components/ui/sonner";
@@ -13,7 +14,7 @@ export default function PagesLayout({
}) {
return (
<KeyboardProvider>
<div className="min-h-screen">
<div className="min-h-screen pb-14 sm:pb-0">
<NavBar />
{/* Ambient glow */}
<div className="pointer-events-none fixed left-1/2 top-1/4 -translate-x-1/2 -translate-y-1/2 h-[600px] w-[800px] rounded-full bg-primary/3 blur-[200px]" />
@@ -21,6 +22,7 @@ export default function PagesLayout({
{children}
</main>
</div>
<MobileTabBar />
<CommandPalette />
<KeyboardHelpDialog />
<Toaster position="bottom-right" />
+78
View File
@@ -0,0 +1,78 @@
"use client";
import { IconHome, IconSearch, IconUser } from "@tabler/icons-react";
import { motion } from "motion/react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { signOut, useSession } from "@/lib/auth/client";
const tabs = [
{ href: "/dashboard", label: "Home", icon: IconHome },
{ href: "/search", label: "Search", icon: IconSearch },
] as const;
export function MobileTabBar() {
const { data: session } = useSession();
const pathname = usePathname();
const router = useRouter();
if (!session?.user) return null;
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t border-border/50 bg-background/90 backdrop-blur-xl sm:hidden">
<div className="flex h-14 items-stretch">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive =
tab.href === "/dashboard"
? pathname === "/dashboard"
: pathname.startsWith(tab.href);
return (
<Link
key={tab.href}
href={tab.href}
className="relative flex flex-1 flex-col items-center justify-center gap-0.5"
>
<Icon
size={20}
className={isActive ? "text-primary" : "text-muted-foreground"}
/>
<span
className={`text-[10px] font-medium ${isActive ? "text-primary" : "text-muted-foreground"}`}
>
{tab.label}
</span>
{isActive && (
<motion.div
layoutId="mobile-tab-indicator"
className="absolute top-0 h-0.5 w-8 rounded-full bg-primary"
transition={{
type: "spring",
stiffness: 380,
damping: 30,
}}
/>
)}
</Link>
);
})}
<button
type="button"
onClick={async () => {
await signOut();
router.push("/");
router.refresh();
}}
className="flex flex-1 flex-col items-center justify-center gap-0.5"
>
<IconUser size={20} className="text-muted-foreground" />
<span className="text-[10px] font-medium text-muted-foreground">
{session.user.name?.split(" ")[0] ?? "Account"}
</span>
</button>
</div>
{/* Safe area for devices with home indicator */}
<div className="h-[env(safe-area-inset-bottom)]" />
</nav>
);
}