Frontend completo (Etapas 11-18) e extensões de backend (Etapas 13 e 18)
Adiciona o frontend Next.js em frontend/ (setup, design system, autenticação/tema, layout/navegação, área master, área tenant completa: clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas, equipe e perfil). Estende o backend de forma aditiva e autorizada: cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe + edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { NavItem } from '@/lib/layout/nav-items';
|
||||
|
||||
function isActive(pathname: string, href: string): boolean {
|
||||
return pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
function isGroupActive(pathname: string, item: NavItem): boolean {
|
||||
if (isActive(pathname, item.href)) return true;
|
||||
return item.children?.some((child) => isActive(pathname, child.href)) ?? false;
|
||||
}
|
||||
|
||||
interface NavListProps {
|
||||
items: NavItem[];
|
||||
/** Modo ícones-apenas (sidebar recolhida) — grupos viram link direto pro primeiro filho. */
|
||||
collapsed?: boolean;
|
||||
/** Disparado ao navegar (o Drawer usa isso para fechar). */
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista de navegação com estado de item/grupo ativo — compartilhada entre
|
||||
* `Sidebar` (desktop) e `Drawer` (mobile) para não duplicar essa lógica em
|
||||
* dois lugares.
|
||||
*/
|
||||
export function NavList({ items, collapsed = false, onNavigate }: NavListProps) {
|
||||
const pathname = usePathname();
|
||||
const [openHref, setOpenHref] = React.useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<nav className="flex flex-col gap-1">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = isGroupActive(pathname, item);
|
||||
|
||||
if (item.children && !collapsed) {
|
||||
const open = openHref === item.href || (openHref === null && active);
|
||||
return (
|
||||
<div key={item.href}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenHref(open ? '' : item.href)}
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-neutral-500 transition-colors hover:bg-muted hover:text-foreground',
|
||||
active && 'bg-primary/10 text-primary-on-light',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4.5 shrink-0" aria-hidden />
|
||||
<span className="flex-1 text-left">{item.label}</span>
|
||||
<ChevronDown
|
||||
className={cn('size-4 shrink-0 transition-transform', open && 'rotate-180')}
|
||||
aria-hidden
|
||||
/>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="ml-4 flex flex-col gap-0.5 border-l border-border py-1 pl-4">
|
||||
{item.children.map((child) => (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
onClick={onNavigate}
|
||||
aria-current={isActive(pathname, child.href) ? 'page' : undefined}
|
||||
className={cn(
|
||||
'rounded-lg px-3 py-2 text-sm text-neutral-500 transition-colors hover:bg-muted hover:text-foreground',
|
||||
isActive(pathname, child.href) && 'font-medium text-primary-on-light',
|
||||
)}
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={onNavigate}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
title={collapsed ? item.label : undefined}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-neutral-500 transition-colors hover:bg-muted hover:text-foreground',
|
||||
active && 'bg-primary/10 text-primary-on-light',
|
||||
collapsed && 'mx-auto size-11 justify-center px-0 py-0',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4.5 shrink-0" aria-hidden />
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user