feat(web): add mobile responsive layout with bottom navigation

Add useMobile hook for viewport detection (<768px). Update AppLayout
with hamburger menu, slide-over sidebar overlay, and fixed bottom
navigation bar (Tools, Automate, Files, Settings) for mobile. Update
Sidebar with expanded mode for mobile overlay. Update ToolPage to
stack settings above dropzone on mobile with collapsible panel. Add
new routes for /automate and /fullscreen. Integrate Settings dialog
and keyboard shortcuts at the App root level.
This commit is contained in:
Siddharth Kumar Sah
2026-03-22 04:42:31 +08:00
parent 6d67b106ca
commit 580208de83
5 changed files with 271 additions and 17 deletions
+124 -8
View File
@@ -1,8 +1,20 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import {
LayoutGrid,
Workflow,
FolderOpen,
Settings as SettingsIcon,
Menu,
X,
} from "lucide-react";
import { Sidebar } from "./sidebar";
import { ToolPanel } from "./tool-panel";
import { Footer } from "./footer";
import { Dropzone } from "../common/dropzone";
import { SettingsDialog } from "../settings/settings-dialog";
import { useMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
interface AppLayoutProps {
children?: React.ReactNode;
@@ -10,21 +22,125 @@ interface AppLayoutProps {
}
export function AppLayout({ children, showToolPanel = true }: AppLayoutProps) {
const [_settingsOpen, setSettingsOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const isMobile = useMobile();
return (
<div className="flex h-screen bg-background text-foreground overflow-hidden">
<Sidebar onSettingsClick={() => setSettingsOpen(true)} />
{showToolPanel && <ToolPanel />}
<main className="flex-1 flex flex-col overflow-hidden">
{/* Desktop sidebar */}
{!isMobile && (
<Sidebar onSettingsClick={() => setSettingsOpen(true)} />
)}
{/* Mobile sidebar overlay */}
{isMobile && mobileSidebarOpen && (
<>
<div
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"
onClick={() => setMobileSidebarOpen(false)}
/>
<div className="fixed inset-y-0 left-0 z-50 w-64 bg-background border-r border-border shadow-xl animate-in slide-in-from-left">
<div className="flex items-center justify-between p-3 border-b border-border">
<span className="text-sm font-bold text-foreground">
Stirling <span className="text-primary">Image</span>
</span>
<button
onClick={() => setMobileSidebarOpen(false)}
className="p-1.5 rounded-lg hover:bg-muted"
>
<X className="h-4 w-4" />
</button>
</div>
<div onClick={() => setMobileSidebarOpen(false)}>
<Sidebar
onSettingsClick={() => {
setMobileSidebarOpen(false);
setSettingsOpen(true);
}}
expanded
/>
</div>
</div>
</>
)}
{/* Mobile top bar */}
{isMobile && (
<div className="fixed top-0 left-0 right-0 z-30 bg-background/95 backdrop-blur-sm border-b border-border px-3 py-2 flex items-center gap-3">
<button
onClick={() => setMobileSidebarOpen(true)}
className="p-1.5 rounded-lg hover:bg-muted"
>
<Menu className="h-5 w-5" />
</button>
<span className="text-sm font-bold text-foreground">
Stirling <span className="text-primary">Image</span>
</span>
</div>
)}
{showToolPanel && !isMobile && <ToolPanel />}
<main
className={cn(
"flex-1 flex flex-col overflow-hidden",
isMobile && "pt-12 pb-16"
)}
>
<div className="flex-1 overflow-y-auto p-6 flex items-center justify-center">
{children || <Dropzone />}
</div>
<div className="text-center text-xs text-muted-foreground py-2 border-t border-border">
Privacy Policy
</div>
{!isMobile && (
<div className="text-center text-xs text-muted-foreground py-2 border-t border-border">
Privacy Policy
</div>
)}
</main>
<Footer />
{!isMobile && <Footer />}
{/* Mobile bottom nav */}
{isMobile && (
<nav className="fixed bottom-0 left-0 right-0 z-30 bg-background/95 backdrop-blur-sm border-t border-border flex items-center justify-around px-2 py-1.5">
<MobileNavItem icon={LayoutGrid} label="Tools" href="/" />
<MobileNavItem icon={Workflow} label="Automate" href="/automate" />
<MobileNavItem icon={FolderOpen} label="Files" href="/files" />
<button
onClick={() => setSettingsOpen(true)}
className="flex flex-col items-center gap-0.5 px-3 py-1 text-muted-foreground"
>
<SettingsIcon className="h-5 w-5" />
<span className="text-[10px]">Settings</span>
</button>
</nav>
)}
{/* Settings dialog */}
<SettingsDialog
open={settingsOpen}
onClose={() => setSettingsOpen(false)}
/>
</div>
);
}
function MobileNavItem({
icon: Icon,
label,
href,
}: {
icon: React.ComponentType<{ className?: string }>;
label: string;
href: string;
}) {
return (
<Link
to={href}
className="flex flex-col items-center gap-0.5 px-3 py-1 text-muted-foreground hover:text-foreground transition-colors"
>
<Icon className="h-5 w-5" />
<span className="text-[10px]">{label}</span>
</Link>
);
}
+30 -3
View File
@@ -7,6 +7,7 @@ import {
FolderOpen,
HelpCircle,
Settings,
Grid3x3,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
@@ -18,7 +19,7 @@ interface SidebarItem {
const topItems: SidebarItem[] = [
{ icon: LayoutGrid, label: "Tools", href: "/" },
{ icon: BookOpen, label: "Reader", href: "/reader" },
{ icon: Grid3x3, label: "Grid", href: "/fullscreen" },
{ icon: Workflow, label: "Automate", href: "/automate" },
{ icon: FolderOpen, label: "Files", href: "/files" },
];
@@ -30,13 +31,27 @@ const bottomItems: SidebarItem[] = [
interface SidebarProps {
onSettingsClick: () => void;
/** When true, renders in expanded mode (for mobile overlay). */
expanded?: boolean;
}
export function Sidebar({ onSettingsClick }: SidebarProps) {
export function Sidebar({ onSettingsClick, expanded = false }: SidebarProps) {
const location = useLocation();
const renderItem = (item: SidebarItem, isActive: boolean) => {
const content = (
const content = expanded ? (
<div
className={cn(
"flex items-center gap-3 px-4 py-2.5 rounded-lg cursor-pointer transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<item.icon className="h-5 w-5" />
<span className="text-sm font-medium">{item.label}</span>
</div>
) : (
<div
className={cn(
"flex flex-col items-center gap-1 p-2 rounded-lg cursor-pointer transition-colors",
@@ -64,6 +79,18 @@ export function Sidebar({ onSettingsClick }: SidebarProps) {
);
};
if (expanded) {
return (
<div className="flex flex-col p-3 gap-1">
{topItems.map((item) =>
renderItem(item, location.pathname === item.href)
)}
<div className="border-t border-border my-2" />
{bottomItems.map((item) => renderItem(item, false))}
</div>
);
}
return (
<aside className="flex flex-col items-center w-16 bg-sidebar border-r border-border py-3 gap-1 shrink-0">
<div className="flex flex-col gap-1 flex-1">