A screenshot at phone width showed an "INVOICE | LINES | TOTAL | STATUS" header row while the data under LINES was money. The header and its data cell had drifted apart: the earlier pass hid the Lines <td> but not the matching <th>, because the loop that added the hide classes only matched text-left headers and Lines is text-right. So the Total values rendered under a heading that named a column which no longer existed. The same screenshot showed the totals clipped — "41,710.50" reading as "710,50". With only three columns left, the 520px min-width was still forcing a sideways scroll, and the opaque sticky first column then sat on top of the left of the money values. The mobile floor is gone, so the three remaining columns size to their content, and the sticky and status gutters are trimmed at mobile widths to let them fit 375px. Below sm the table is now exactly Invoice, Total, Status — and every header matches a visible cell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { ChevronDown, Menu, X } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export type NavLeaf = { href: string; label: string };
|
|
export type NavItem =
|
|
| { kind: "link"; href: string; label: string; icon: React.ReactNode }
|
|
| { kind: "group"; label: string; icon: React.ReactNode; children: NavLeaf[] };
|
|
|
|
export function Sidebar({ items }: { items: NavItem[] }) {
|
|
const pathname = usePathname();
|
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
|
|
// Close drawer on navigation
|
|
useEffect(() => {
|
|
setMobileOpen(false);
|
|
}, [pathname]);
|
|
|
|
// Lock body scroll when drawer is open
|
|
useEffect(() => {
|
|
document.body.style.overflow = mobileOpen ? "hidden" : "";
|
|
return () => {
|
|
document.body.style.overflow = "";
|
|
};
|
|
}, [mobileOpen]);
|
|
|
|
const renderNav = (onNavigate?: () => void) => (
|
|
<nav className="p-3 space-y-0.5 text-sm">
|
|
{items.map((item, i) =>
|
|
item.kind === "link" ? (
|
|
<LeafLink
|
|
key={i}
|
|
href={item.href}
|
|
label={item.label}
|
|
icon={item.icon}
|
|
active={pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href))}
|
|
onClick={onNavigate}
|
|
/>
|
|
) : (
|
|
<Group
|
|
key={i}
|
|
item={item}
|
|
active={pathname.startsWith(item.children[0]?.href.split("/").slice(0, 3).join("/") ?? "")}
|
|
/>
|
|
)
|
|
)}
|
|
</nav>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop: persistent sidebar (hidden on mobile) */}
|
|
<aside className="hidden lg:block w-64 shrink-0 bg-white dark:bg-surface-muted border-r border-gray-200 dark:border-border min-h-[calc(100vh-4rem)]">
|
|
{renderNav()}
|
|
</aside>
|
|
|
|
{/* Mobile: hamburger in the top bar triggers a drawer */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setMobileOpen(true)}
|
|
aria-label="Open navigation"
|
|
className="lg:hidden w-9 h-9 grid place-items-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 text-gray-500 dark:text-gray-400"
|
|
>
|
|
<Menu className="w-5 h-5" />
|
|
</button>
|
|
|
|
{mobileOpen && (
|
|
<div
|
|
className="lg:hidden fixed inset-0 z-40 bg-black/40"
|
|
onClick={() => setMobileOpen(false)}
|
|
/>
|
|
)}
|
|
<aside
|
|
className={cn(
|
|
"lg:hidden fixed top-0 left-0 bottom-0 z-50 w-72 bg-white dark:bg-surface-muted shadow-2xl transition-transform",
|
|
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between px-4 h-16 border-b border-gray-200 dark:border-border">
|
|
<div className="flex items-center gap-2">
|
|
<img src="/brand/logo.png" alt="Mega Plant" className="w-8 h-8 rounded-xl object-contain bg-white dark:bg-bg shadow-sm" />
|
|
<span className="text-sm font-semibold text-[#003D5B] dark:text-white">Mega Plant</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setMobileOpen(false)}
|
|
aria-label="Close navigation"
|
|
className="w-9 h-9 grid place-items-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 text-gray-500 dark:text-gray-400"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
{renderNav(() => setMobileOpen(false))}
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function LeafLink({
|
|
href, label, icon, active, onClick,
|
|
}: {
|
|
href: string; label: string; icon: React.ReactNode; active: boolean; onClick?: () => void;
|
|
}) {
|
|
return (
|
|
<Link
|
|
href={href}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"flex items-center gap-3 px-3 py-2 rounded-lg font-medium transition",
|
|
active
|
|
? "bg-gradient-to-r from-[#30638E]/10 to-[#00798C]/5 text-[#003D5B] dark:from-[#30638E]/20 dark:to-[#00798C]/10 dark:text-white"
|
|
: "text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white"
|
|
)}
|
|
>
|
|
<span className={cn(active ? "text-accent-ink" : "text-gray-400")}>{icon}</span>
|
|
<span>{label}</span>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
function Group({ item, active }: { item: Extract<NavItem, { kind: "group" }>; active: boolean }) {
|
|
const [open, setOpen] = useState(active);
|
|
const pathname = usePathname();
|
|
return (
|
|
<div>
|
|
<button
|
|
onClick={() => setOpen((o) => !o)}
|
|
className={cn(
|
|
"w-full flex items-center gap-3 px-3 py-2 rounded-lg font-medium transition",
|
|
active
|
|
? "text-[#003D5B] dark:text-white"
|
|
: "text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white"
|
|
)}
|
|
>
|
|
<span className={cn(active ? "text-accent-ink" : "text-gray-400")}>{item.icon}</span>
|
|
<span className="flex-1 text-left">{item.label}</span>
|
|
<ChevronDown className={cn("w-4 h-4 text-gray-400 transition-transform", open && "rotate-180")} />
|
|
</button>
|
|
{open && (
|
|
<div className="ml-7 mt-0.5 space-y-0.5 border-l-2 border-gray-100 dark:border-border pl-2">
|
|
{item.children.map((child) => {
|
|
const childActive = pathname === child.href;
|
|
return (
|
|
<Link
|
|
key={child.href}
|
|
href={child.href}
|
|
className={cn(
|
|
"block px-3 py-1.5 rounded-lg text-xs font-medium transition",
|
|
childActive
|
|
? "bg-[#30638E]/10 text-[#30638E] dark:bg-[#00798C]/15 dark:text-[#00798C]"
|
|
: "text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white"
|
|
)}
|
|
>
|
|
{child.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|