From a9f73f7a45562cab10a7ffec15c6040df89e1114 Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Sun, 22 Feb 2026 20:28:57 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20implementar=20topbar=20como=20experimen?= =?UTF-8?q?to=20de=20navega=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitui a sidebar pela topbar com NavigationMenu do shadcn/ui. - Logo à esquerda com invert para bg primário - Links diretos: Dashboard, Calendário, Cartões, Contas - Dropdowns: Lançamentos, Organização, Análise - Mobile: Sheet lateral com hamburger - Ações à direita: notificações, calculadora, tema, etc. - User avatar com dropdown de ajustes/logout - CSS vars overrideados na área de ações para cor primária Co-Authored-By: Claude Sonnet 4.6 --- app/(dashboard)/layout.tsx | 39 +-- components/topbar/app-topbar.tsx | 91 +++++++ components/topbar/top-nav-menu.tsx | 385 +++++++++++++++++++++++++++++ components/topbar/topbar-user.tsx | 82 ++++++ components/ui/navigation-menu.tsx | 167 +++++++++++++ 5 files changed, 738 insertions(+), 26 deletions(-) create mode 100644 components/topbar/app-topbar.tsx create mode 100644 components/topbar/top-nav-menu.tsx create mode 100644 components/topbar/topbar-user.tsx create mode 100644 components/ui/navigation-menu.tsx diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 2df5167..f305926 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,8 +1,6 @@ import { FontProvider } from "@/components/font-provider"; -import { SiteHeader } from "@/components/header-dashboard"; import { PrivacyProvider } from "@/components/privacy-provider"; -import { AppSidebar } from "@/components/sidebar/app-sidebar"; -import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import { AppTopbar } from "@/components/topbar/app-topbar"; import { getUserSession } from "@/lib/auth/server"; import { fetchDashboardNotifications } from "@/lib/dashboard/notifications"; import { fetchPagadoresWithAccess } from "@/lib/pagadores/access"; @@ -55,30 +53,19 @@ export default async function DashboardLayout({ moneyFont={fontPrefs.moneyFont} > - - ({ - id: item.id, - name: item.name, - avatarUrl: item.avatarUrl, - canEdit: item.canEdit, - }))} - preLancamentosCount={preLancamentosCount} - variant="sidebar" - /> - - -
-
-
- {children} -
-
+ +
+
+
+ {children}
- - +
+
); diff --git a/components/topbar/app-topbar.tsx b/components/topbar/app-topbar.tsx new file mode 100644 index 0000000..054196c --- /dev/null +++ b/components/topbar/app-topbar.tsx @@ -0,0 +1,91 @@ +import Image from "next/image"; +import Link from "next/link"; +import { AnimatedThemeToggler } from "@/components/animated-theme-toggler"; +import { CalculatorDialogButton } from "@/components/calculadora/calculator-dialog"; +import { FeedbackDialog } from "@/components/feedback/feedback-dialog"; +import { NotificationBell } from "@/components/notificacoes/notification-bell"; +import { PrivacyModeToggle } from "@/components/privacy-mode-toggle"; +import { RefreshPageButton } from "@/components/refresh-page-button"; +import type { DashboardNotificationsSnapshot } from "@/lib/dashboard/notifications"; +import { TopNavMenu } from "./top-nav-menu"; +import { TopbarUser } from "./topbar-user"; + +type AppTopbarProps = { + user: { + id: string; + name: string; + email: string; + image: string | null; + }; + pagadorAvatarUrl: string | null; + preLancamentosCount?: number; + notificationsSnapshot: DashboardNotificationsSnapshot; +}; + +export function AppTopbar({ + user, + pagadorAvatarUrl, + preLancamentosCount = 0, + notificationsSnapshot, +}: AppTopbarProps) { + return ( +
+ {/* Logo */} + + OpenMonetis + OpenMonetis + + + {/* Navigation */} + + + {/* Right-side actions — CSS vars overridden so icons render light on primary bg */} +
+ + + + + + + +
+ + {/* User avatar — outside the var-override div so dropdown stays normal */} + +
+ ); +} diff --git a/components/topbar/top-nav-menu.tsx b/components/topbar/top-nav-menu.tsx new file mode 100644 index 0000000..ecd3e0e --- /dev/null +++ b/components/topbar/top-nav-menu.tsx @@ -0,0 +1,385 @@ +"use client"; + +import { + RiArrowLeftRightLine, + RiBankCard2Line, + RiBankLine, + RiCalendarEventLine, + RiDashboardLine, + RiFileChartLine, + RiFundsLine, + RiGroupLine, + RiInboxLine, + RiMenuLine, + RiPriceTag3Line, + RiSparklingLine, + RiTodoLine, +} from "@remixicon/react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuList, + NavigationMenuTrigger, +} from "@/components/ui/navigation-menu"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { cn } from "@/lib/utils/ui"; + +type TopNavMenuProps = { + preLancamentosCount?: number; +}; + +const triggerClass = + "!bg-transparent !text-primary-foreground/90 hover:!bg-primary-foreground/15 hover:!text-primary-foreground focus:!bg-primary-foreground/15 focus:!text-primary-foreground data-[state=open]:!bg-primary-foreground/20 data-[state=open]:!text-primary-foreground"; + +function SimpleNavLink({ + href, + children, +}: { + href: string; + children: React.ReactNode; +}) { + const pathname = usePathname(); + const isActive = + href === "/dashboard" + ? pathname === href + : pathname === href || pathname.startsWith(`${href}/`); + + return ( + + {children} + + ); +} + +type DropdownLinkItem = { + href: string; + label: string; + icon: React.ReactNode; + badge?: number; +}; + +function DropdownLinkList({ items }: { items: DropdownLinkItem[] }) { + return ( +
    + {items.map((item) => ( +
  • + + {item.icon} + {item.label} + {item.badge && item.badge > 0 ? ( + + {item.badge} + + ) : null} + +
  • + ))} +
+ ); +} + +function MobileNavLink({ + href, + icon, + children, + onClick, + badge, +}: { + href: string; + icon: React.ReactNode; + children: React.ReactNode; + onClick?: () => void; + badge?: number; +}) { + const pathname = usePathname(); + const isActive = + href === "/dashboard" + ? pathname === href + : pathname === href || pathname.startsWith(`${href}/`); + + return ( + + {icon} + {children} + {badge && badge > 0 ? ( + + {badge} + + ) : null} + + ); +} + +function MobileSectionLabel({ label }: { label: string }) { + return ( +

+ {label} +

+ ); +} + +export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) { + const [sheetOpen, setSheetOpen] = useState(false); + const close = () => setSheetOpen(false); + + const lancamentosItems: DropdownLinkItem[] = [ + { + href: "/lancamentos", + label: "Lançamentos", + icon: , + }, + { + href: "/pre-lancamentos", + label: "Pré-Lançamentos", + icon: , + badge: preLancamentosCount, + }, + ]; + + const organizacaoItems: DropdownLinkItem[] = [ + { + href: "/orcamentos", + label: "Orçamentos", + icon: , + }, + { + href: "/pagadores", + label: "Pagadores", + icon: , + }, + { + href: "/categorias", + label: "Categorias", + icon: , + }, + { + href: "/anotacoes", + label: "Anotações", + icon: , + }, + ]; + + const analiseItems: DropdownLinkItem[] = [ + { + href: "/insights", + label: "Insights", + icon: , + }, + { + href: "/relatorios/tendencias", + label: "Tendências", + icon: , + }, + { + href: "/relatorios/uso-cartoes", + label: "Uso de Cartões", + icon: , + }, + ]; + + return ( + <> + {/* Desktop nav */} + + + {/* Mobile hamburger */} + + + + + + + Menu + + + + + + ); +} diff --git a/components/topbar/topbar-user.tsx b/components/topbar/topbar-user.tsx new file mode 100644 index 0000000..6e988f8 --- /dev/null +++ b/components/topbar/topbar-user.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { RiSettings2Line } from "@remixicon/react"; +import Image from "next/image"; +import Link from "next/link"; +import { useMemo } from "react"; +import LogoutButton from "@/components/auth/logout-button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { getAvatarSrc } from "@/lib/pagadores/utils"; + +type TopbarUserProps = { + user: { + id: string; + name: string; + email: string; + image: string | null; + }; + pagadorAvatarUrl: string | null; +}; + +export function TopbarUser({ user, pagadorAvatarUrl }: TopbarUserProps) { + const avatarSrc = useMemo(() => { + if (pagadorAvatarUrl) return getAvatarSrc(pagadorAvatarUrl); + if (user.image) return user.image; + return getAvatarSrc(null); + }, [user.image, pagadorAvatarUrl]); + + return ( + + + + + + + {user.name} +
+ {user.name} + + {user.email} + +
+
+ +
+ + + Ajustes + +
+ +
+
+
+
+ ); +} diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..36cb1c9 --- /dev/null +++ b/components/ui/navigation-menu.tsx @@ -0,0 +1,167 @@ +import { RiArrowDownBoxFill } from "@remixicon/react"; +import { cva } from "class-variance-authority"; +import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui"; +import type * as React from "react"; +import { cn } from "@/lib/utils"; + +function NavigationMenu({ + className, + children, + viewport = true, + ...props +}: React.ComponentProps & { + viewport?: boolean; +}) { + return ( + + {children} + {viewport && } + + ); +} + +function NavigationMenuList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function NavigationMenuItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +const navigationMenuTriggerStyle = cva( + "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1", +); + +function NavigationMenuTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + {children}{" "} + + ); +} + +function NavigationMenuContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function NavigationMenuViewport({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ +
+ ); +} + +function NavigationMenuLink({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function NavigationMenuIndicator({ + className, + ...props +}: React.ComponentProps) { + return ( + +
+ + ); +} + +export { + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, + navigationMenuTriggerStyle, +};