Deliver role-aware shell (sidebar, breadcrumbs, quick search, tables, detail/create layouts), locale-ready pages, shared backend/feature docs, and Vercel project config so HR, finance, and members can demo against typed mocks. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { useTranslations } from "next-intl";
|
|
import { Link, usePathname } from "@/i18n/routing";
|
|
import { brand } from "@/data/brand";
|
|
import { navForRole } from "@/lib/auth/nav";
|
|
import { useAuth } from "@/providers/auth-provider";
|
|
import { cn } from "@/lib/utils";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { ChevronLeft, ChevronRight, X } from "lucide-react";
|
|
|
|
export function AppSidebar({
|
|
collapsed,
|
|
onToggle,
|
|
onNavigate,
|
|
showClose = false,
|
|
}: {
|
|
collapsed: boolean;
|
|
onToggle: () => void;
|
|
onNavigate?: () => void;
|
|
showClose?: boolean;
|
|
}) {
|
|
const t = useTranslations();
|
|
const pathname = usePathname();
|
|
const { user } = useAuth();
|
|
const items = user ? navForRole(user.activeRole) : [];
|
|
|
|
return (
|
|
<aside
|
|
className={cn(
|
|
"group/sidebar flex h-full flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width,padding] duration-300",
|
|
collapsed ? "w-[88px] px-2 py-5" : "w-[264px] px-4 py-5",
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-2 px-2",
|
|
collapsed ? "justify-center" : "justify-between",
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"flex min-w-0 items-center gap-3",
|
|
collapsed && "justify-center",
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"relative flex shrink-0 items-center justify-center overflow-hidden rounded-xl bg-secondary",
|
|
collapsed ? "size-10" : "size-11",
|
|
)}
|
|
>
|
|
<Image
|
|
src={brand.logo}
|
|
alt={brand.shortName}
|
|
width={44}
|
|
height={44}
|
|
className="size-full object-contain p-1.5"
|
|
priority
|
|
/>
|
|
</span>
|
|
{!collapsed && (
|
|
<div className="min-w-0">
|
|
<p className="truncate font-display text-sm font-semibold tracking-tight">
|
|
{t("common.appName")}
|
|
</p>
|
|
<p className="truncate text-[11px] leading-snug text-muted-foreground">
|
|
{brand.tagline}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showClose ? (
|
|
<button
|
|
type="button"
|
|
className="grid size-8 shrink-0 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
|
onClick={onToggle}
|
|
aria-label="Close sidebar"
|
|
>
|
|
<X className="size-5" />
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
"hidden size-8 shrink-0 place-items-center rounded-lg text-muted-foreground transition-all hover:bg-secondary hover:text-foreground md:grid",
|
|
"md:pointer-events-none md:opacity-0 md:group-hover/sidebar:pointer-events-auto md:group-hover/sidebar:opacity-100",
|
|
"focus-visible:pointer-events-auto focus-visible:opacity-100",
|
|
collapsed && "translate-x-1",
|
|
)}
|
|
onClick={onToggle}
|
|
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
>
|
|
{collapsed ? (
|
|
<ChevronRight className="size-5" />
|
|
) : (
|
|
<ChevronLeft className="size-5" />
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<ScrollArea className="mt-6 flex-1">
|
|
<nav className="space-y-1 pr-1">
|
|
{items.map((item) => {
|
|
const active =
|
|
item.href === "/"
|
|
? pathname === "/"
|
|
: pathname === item.href || pathname.startsWith(`${item.href}/`);
|
|
const Icon = item.icon;
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
onClick={onNavigate}
|
|
className={cn(
|
|
"group flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground transition-colors",
|
|
"hover:bg-secondary hover:text-brand-deep",
|
|
active &&
|
|
"bg-brand/10 text-brand-deep shadow-[0_1px_0_rgba(0,0,0,0.02)] ring-1 ring-brand/20",
|
|
collapsed && "justify-center px-2",
|
|
)}
|
|
title={t(item.labelKey)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"grid size-8 shrink-0 place-items-center rounded-lg bg-secondary text-muted-foreground transition-colors",
|
|
"group-hover:bg-brand/20 group-hover:text-brand-deep",
|
|
active && "bg-brand text-forest shadow-sm",
|
|
)}
|
|
>
|
|
<Icon className="size-4" />
|
|
</span>
|
|
{!collapsed && (
|
|
<>
|
|
<span className="truncate">{t(item.labelKey)}</span>
|
|
{active && (
|
|
<span className="ml-auto h-6 w-1 shrink-0 rounded-full bg-brand" />
|
|
)}
|
|
</>
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
</ScrollArea>
|
|
</aside>
|
|
);
|
|
}
|