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:
2026-03-21 08:33:08 +11:00
parent ef73a9cea0
commit 0a1f6b48a2
5 changed files with 120 additions and 21 deletions
+68 -4
View File
@@ -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>
</>
);
}