feat(ui): mobile-responsive sidebar + rules improvements
- Sidebar: hidden on mobile, opens as slide-out drawer with hamburger toggle; auto-closes on navigation; desktop layout unchanged - Layout: responsive padding accounting for mobile header bar - Rules: add tag as a condition field (has/not-has tag) - Rules: apply a single rule via per-rule Apply button - Rules: splits-from defaults to 2026-01-09
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: "/transactions", label: "Transactions", icon: "receipt" },
|
||||
@@ -60,11 +61,34 @@ const ICONS: Record<string, React.ReactNode> = {
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<aside className="w-56 bg-zinc-900 border-r border-zinc-800 flex flex-col min-h-screen">
|
||||
<div className="p-4 border-b border-zinc-800">
|
||||
// Close drawer on route change
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
// Prevent body scroll when drawer is open
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => { document.body.style.overflow = ""; };
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const navContent = (
|
||||
<>
|
||||
<div className="p-4 border-b border-zinc-800 flex items-center justify-between">
|
||||
<h1 className="text-lg font-semibold text-white">Finance</h1>
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="md:hidden p-1 rounded text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav className="flex-1 p-2">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
@@ -85,6 +109,46 @@ export function Sidebar() {
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile header bar */}
|
||||
<div className="md:hidden fixed top-0 left-0 right-0 z-40 bg-zinc-900 border-b border-zinc-800 flex items-center px-4 h-14">
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="p-1.5 -ml-1.5 rounded text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="ml-3 text-base font-semibold text-white">Finance</span>
|
||||
</div>
|
||||
|
||||
{/* Mobile drawer overlay */}
|
||||
{open && (
|
||||
<div
|
||||
className="md:hidden fixed inset-0 z-50 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile drawer */}
|
||||
<aside
|
||||
className={`md:hidden fixed top-0 left-0 z-50 w-64 h-full bg-zinc-900 border-r border-zinc-800 flex flex-col transform transition-transform duration-200 ease-in-out ${
|
||||
open ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
{navContent}
|
||||
</aside>
|
||||
|
||||
{/* Desktop sidebar — unchanged */}
|
||||
<aside className="hidden md:flex w-56 bg-zinc-900 border-r border-zinc-800 flex-col min-h-screen">
|
||||
{navContent}
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user