style: refina base visual e navegação

This commit is contained in:
Felipe Coutinho
2026-03-15 23:23:00 +00:00
parent 5a78fd614c
commit 64eb29d807
20 changed files with 350 additions and 262 deletions

View File

@@ -3,20 +3,19 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme { @theme {
--spacing-custom-height-card: 30rem; --spacing-custom-height-card: 29rem;
--spacing-8xl: 88rem; --spacing-8xl: 90rem;
--spacing-9xl: 96rem;
} }
:root { :root {
--background: oklch(97.036% 0.00276 84.303); --background: oklch(97.412% 0.00332 67.032);
--foreground: oklch(27% 0.008 45); --foreground: oklch(27% 0.008 45);
--card: var(--background); --card: oklch(99% 0.002 67);
--card-foreground: var(--foreground); --card-foreground: var(--foreground);
--popover: oklch(100% 0 0); --popover: oklch(100% 0 0);
--popover-foreground: var(--foreground); --popover-foreground: var(--foreground);
--primary: oklch(72.085% 0.16286 50.705); --primary: oklch(72.069% 0.18335 44.069);
--primary-foreground: oklch(98% 0.008 80); --primary-foreground: oklch(98% 0.008 80);
--secondary: oklch(96.2% 0.005 70); --secondary: oklch(96.2% 0.005 70);
@@ -28,24 +27,23 @@
--accent: oklch(94.8% 0.009 65); --accent: oklch(94.8% 0.009 65);
--accent-foreground: var(--foreground); --accent-foreground: var(--foreground);
--success: oklch(61.654% 0.14385 157.131); --success: oklch(61.685% 0.13077 162.978);
--success-foreground: oklch(98% 0.01 150); --success-foreground: oklch(98% 0.01 150);
--warning: oklch(69.913% 0.1798 49.649); --warning: oklch(78.357% 0.15147 68.301);
--warning-foreground: oklch(20% 0.04 85); --warning-foreground: oklch(20% 0.04 85);
--info: oklch(55% 0.17 250); --info: oklch(55% 0.17 250);
--info-foreground: oklch(98% 0.01 250); --info-foreground: oklch(98% 0.01 250);
--destructive: oklch(55% 0.22 27); --destructive: oklch(55% 0.22 27);
--destructive-foreground: oklch(98% 0.005 30); --destructive-foreground: oklch(98% 0.005 30);
--border: oklch(84.567% 0.00583 84.468); --border: oklch(90.274% 0.01362 60.342);
--input: oklch(84.567% 0.00583 84.468); --input: var(--border);
--ring: var(--primary); --ring: var(--primary);
--chart-1: var(--color-emerald-500); --chart-1: var(--color-emerald-500);
--chart-2: var(--color-orange-500); --chart-2: var(--color-red-500);
--chart-3: var(--color-indigo-500); --chart-3: var(--color-amber-500);
--chart-4: var(--color-amber-500); --chart-4: var(--color-blue-500);
--chart-5: var(--color-pink-500); --chart-5: var(--color-pink-500);
--chart-6: var(--color-stone-500); --chart-6: var(--color-stone-500);
--chart-7: var(--color-teal-500); --chart-7: var(--color-teal-500);
@@ -62,7 +60,7 @@
--sidebar-border: oklch(91% 0.004 70); --sidebar-border: oklch(91% 0.004 70);
--sidebar-ring: var(--primary); --sidebar-ring: var(--primary);
--radius: 0.5rem; --radius: 0.625rem;
--shadow-2xs: 0 1px 2px 0px oklch(35% 0.02 45 / 0.04); --shadow-2xs: 0 1px 2px 0px oklch(35% 0.02 45 / 0.04);
--shadow-xs: 0 1px 3px 0px oklch(35% 0.02 45 / 0.06); --shadow-xs: 0 1px 3px 0px oklch(35% 0.02 45 / 0.06);
@@ -83,37 +81,36 @@
} }
.dark { .dark {
--background: oklch(20.5% 0.004 55); --background: oklch(22% 0.004 55);
--foreground: oklch(93% 0.008 80); --foreground: oklch(93% 0.008 80);
--card: oklch(23% 0.004 55); --card: oklch(25.5% 0.004 55);
--card-foreground: var(--foreground); --card-foreground: var(--foreground);
--popover: oklch(25% 0.004 55); --popover: oklch(28% 0.004 55);
--popover-foreground: var(--foreground); --popover-foreground: var(--foreground);
--primary: oklch(72.085% 0.16286 50.705); --primary: oklch(66% 0.15 45.139);
--primary-foreground: oklch(16% 0.004 60); --primary-foreground: oklch(16% 0.004 60);
--secondary: oklch(26% 0.004 55); --secondary: oklch(29% 0.004 55);
--secondary-foreground: var(--foreground); --secondary-foreground: var(--foreground);
--muted: oklch(28.5% 0.0035 55); --muted: oklch(32% 0.0035 55);
--muted-foreground: oklch(73% 0.006 75); --muted-foreground: oklch(73% 0.006 75);
--accent: oklch(30% 0.005 55); --accent: oklch(33% 0.005 55);
--accent-foreground: var(--foreground); --accent-foreground: var(--foreground);
--success: oklch(65% 0.19 150); --success: oklch(62% 0.16 150);
--success-foreground: oklch(15% 0.02 150); --success-foreground: oklch(15% 0.02 150);
--warning: oklch(69.913% 0.1798 49.649); --warning: oklch(69.913% 0.1798 49.649);
--warning-foreground: oklch(15% 0.04 85); --warning-foreground: oklch(15% 0.04 85);
--info: oklch(65% 0.17 250); --info: oklch(65% 0.17 250);
--info-foreground: oklch(15% 0.02 250); --info-foreground: oklch(15% 0.02 250);
--destructive: oklch(62% 0.2 28); --destructive: oklch(62% 0.2 28);
--destructive-foreground: oklch(98% 0.005 30); --destructive-foreground: oklch(98% 0.005 30);
--border: oklch(33% 0.004 55); --border: oklch(35% 0.004 55);
--input: oklch(30% 0.004 55); --input: var(--border);
--ring: var(--primary); --ring: var(--primary);
--chart-1: var(--color-emerald-500); --chart-1: var(--color-emerald-500);
@@ -127,16 +124,16 @@
--chart-9: var(--color-cyan-500); --chart-9: var(--color-cyan-500);
--chart-10: var(--color-lime-500); --chart-10: var(--color-lime-500);
--sidebar: oklch(18% 0.004 55); --sidebar: oklch(19.5% 0.004 55);
--sidebar-foreground: var(--foreground); --sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary); --sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground); --sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: oklch(27% 0.004 55); --sidebar-accent: oklch(30% 0.004 55);
--sidebar-accent-foreground: var(--foreground); --sidebar-accent-foreground: var(--foreground);
--sidebar-border: oklch(31% 0.004 55); --sidebar-border: oklch(34% 0.004 55);
--sidebar-ring: var(--primary); --sidebar-ring: var(--primary);
--radius: 0.5rem; --radius: 0.625rem;
--shadow-2xs: 0 1px 2px 0px oklch(0% 0 0 / 0.3); --shadow-2xs: 0 1px 2px 0px oklch(0% 0 0 / 0.3);
--shadow-xs: 0 1px 3px 0px oklch(0% 0 0 / 0.4); --shadow-xs: 0 1px 3px 0px oklch(0% 0 0 / 0.4);

View File

@@ -5,7 +5,7 @@ import AuthSidebar from "./auth-sidebar";
export function AuthCardShell({ children }: PropsWithChildren) { export function AuthCardShell({ children }: PropsWithChildren) {
return ( return (
<Card className="relative overflow-hidden rounded-2xl border-primary/10 bg-card p-0 shadow-none"> <Card className="relative overflow-hidden p-0">
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]"> <div className="pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]">
<DotPattern <DotPattern
width={17} width={17}

View File

@@ -26,7 +26,7 @@ function AuthSidebar() {
<h2 className="text-[2rem] font-semibold leading-[1.04] tracking-[-0.03em] text-black/84 lg:text-[2.35rem]"> <h2 className="text-[2rem] font-semibold leading-[1.04] tracking-[-0.03em] text-black/84 lg:text-[2.35rem]">
Controle suas finanças com clareza e foco diário. Controle suas finanças com clareza e foco diário.
</h2> </h2>
<p className="max-w-[18rem] text-sm leading-6 text-black/68"> <p className="max-w-2xs text-sm leading-6 text-black/68">
Centralize despesas, organize cartões e acompanhe metas mensais em Centralize despesas, organize cartões e acompanhe metas mensais em
um painel inteligente feito para o seu dia a dia. um painel inteligente feito para o seu dia a dia.
</p> </p>

View File

@@ -195,37 +195,41 @@ export function LoginForm({ className, ...props }: DivProps) {
</FieldSeparator> </FieldSeparator>
<Field> <Field>
<GoogleAuthButton <div
onClick={handleGoogle} className={cn(
loading={loadingGoogle} passkeySupported ? "grid grid-cols-2 gap-2" : "flex",
disabled={ )}
loadingEmail || >
loadingGoogle || <GoogleAuthButton
loadingPasskey || onClick={handleGoogle}
!isGoogleAvailable loading={loadingGoogle}
} disabled={
text="Entrar com Google" loadingEmail ||
/> loadingGoogle ||
</Field> loadingPasskey ||
!isGoogleAvailable
}
text="Google"
/>
{passkeySupported && ( {passkeySupported && (
<Field> <Button
<Button variant="outline"
variant="outline" type="button"
type="button" onClick={handlePasskey}
onClick={handlePasskey} disabled={loadingEmail || loadingGoogle || loadingPasskey}
disabled={loadingEmail || loadingGoogle || loadingPasskey} className="w-full gap-2"
className="w-full gap-2" >
> {loadingPasskey ? (
{loadingPasskey ? ( <RiLoader4Line className="h-4 w-4 animate-spin" />
<RiLoader4Line className="h-4 w-4 animate-spin" /> ) : (
) : ( <RiFingerprintLine className="h-5 w-5" />
<RiFingerprintLine className="h-5 w-5" /> )}
)} <span>Passkey</span>
<span>Entrar com passkey</span> </Button>
</Button> )}
</Field> </div>
)} </Field>
<FieldDescription className="pt-1 text-center"> <FieldDescription className="pt-1 text-center">
Não tem uma conta?{" "} Não tem uma conta?{" "}

View File

@@ -87,7 +87,7 @@ export const AnimatedThemeToggler = ({
buttonVariants({ variant: "ghost", size: "icon-sm" }), buttonVariants({ variant: "ghost", size: "icon-sm" }),
"group relative text-muted-foreground transition-all duration-200", "group relative text-muted-foreground transition-all duration-200",
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40", "hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
"data-[state=open]:bg-accent/60 data-[state=open]:text-foreground border", "data-[state=open]:bg-accent/60 data-[state=open]:text-foreground",
className, className,
)} )}
{...props} {...props}

View File

@@ -68,12 +68,13 @@ export function ExpandableWidgetCard({
hasOverflow ? ( hasOverflow ? (
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center bg-linear-to-t from-card to-transparent pt-12 pb-6"> <div className="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center bg-linear-to-t from-card to-transparent pt-12 pb-6">
<Button <Button
variant="secondary" variant="outline"
className="pointer-events-auto rounded-full text-xs backdrop-blur-sm bg-primary/10" className="pointer-events-auto text-xs"
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
aria-label="Expandir para ver todo o conteúdo" aria-label="Expandir para ver todo o conteúdo"
> >
Ver tudo <RiExpandDiagonalLine size={10} aria-hidden="true" /> Ver tudo{" "}
<RiExpandDiagonalLine className="size-3" aria-hidden="true" />
</Button> </Button>
</div> </div>
) : null ) : null

View File

@@ -42,7 +42,7 @@ export default function MonthNavigation() {
}; };
return ( return (
<Card className="sticky top-16 z-10 flex w-full flex-row p-4 backdrop-blur-md bg-card/5"> <Card className="sticky top-16 z-10 flex w-full flex-row p-4 backdrop-blur-sm bg-card/50">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<NavigationButton <NavigationButton
direction="left" direction="left"

View File

@@ -30,7 +30,7 @@ export function AppNavbar({
notificationsSnapshot, notificationsSnapshot,
}: AppNavbarProps) { }: AppNavbarProps) {
return ( return (
<header className="fixed top-0 left-0 right-0 z-50 flex h-16 shrink-0 items-center border-b border-black/6 bg-primary"> <header className="fixed top-0 left-0 right-0 z-50 flex h-16 shrink-0 items-center bg-primary">
<div className="pointer-events-none absolute inset-0 overflow-hidden"> <div className="pointer-events-none absolute inset-0 overflow-hidden">
<DotPattern <DotPattern
width={20} width={20}
@@ -38,7 +38,7 @@ export function AppNavbar({
cx={1.25} cx={1.25}
cy={1.25} cy={1.25}
cr={1.25} cr={1.25}
className="text-black/10 mask-[linear-gradient(to_right,transparent,black_6%,black_60%,transparent)]" className="text-black/5 mask-[linear-gradient(to_right,transparent,black_5%,black_55%,transparent)]"
/> />
<div className="absolute inset-0 bg-linear-to-b from-white/8 via-transparent to-black/6" /> <div className="absolute inset-0 bg-linear-to-b from-white/8 via-transparent to-black/6" />
</div> </div>

View File

@@ -12,6 +12,7 @@ type MobileLinkProps = {
onClick?: () => void; onClick?: () => void;
badge?: number; badge?: number;
preservePeriod?: boolean; preservePeriod?: boolean;
description?: string;
}; };
export function MobileLink({ export function MobileLink({
@@ -21,6 +22,7 @@ export function MobileLink({
onClick, onClick,
badge, badge,
preservePeriod, preservePeriod,
description,
}: MobileLinkProps) { }: MobileLinkProps) {
const pathname = usePathname(); const pathname = usePathname();
@@ -40,8 +42,22 @@ export function MobileLink({
isActive && "bg-primary/10 text-primary font-medium", isActive && "bg-primary/10 text-primary font-medium",
)} )}
> >
<span className="text-muted-foreground shrink-0">{icon}</span> <span
<span className="flex-1">{children}</span> className={cn(
"shrink-0",
isActive ? "text-primary" : "text-muted-foreground",
)}
>
{icon}
</span>
<span className="flex-1 flex flex-col gap-0.5">
<span>{children}</span>
{description && (
<span className="text-[11px] text-muted-foreground leading-snug">
{description}
</span>
)}
</span>
{badge && badge > 0 ? ( {badge && badge > 0 ? (
<Badge variant="secondary" className="text-xs px-1.5 py-0"> <Badge variant="secondary" className="text-xs px-1.5 py-0">
{badge} {badge}

View File

@@ -1,6 +1,8 @@
"use client"; "use client";
import { usePathname } from "next/navigation";
import { Badge } from "@/shared/components/ui/badge"; import { Badge } from "@/shared/components/ui/badge";
import { cn } from "@/shared/utils/ui";
import type { NavItem } from "./nav-items"; import type { NavItem } from "./nav-items";
import { NavLink } from "./nav-link"; import { NavLink } from "./nav-link";
@@ -9,28 +11,60 @@ type NavDropdownProps = {
}; };
export function NavDropdown({ items }: NavDropdownProps) { export function NavDropdown({ items }: NavDropdownProps) {
const pathname = usePathname();
return ( return (
<ul className="grid w-48 gap-0.5 p-2"> <ul className="grid w-72 gap-0.5 p-2">
{items.map((item) => ( {items.map((item) => {
<li key={item.href}> const isActive =
<NavLink pathname === item.href || pathname.startsWith(`${item.href}/`);
href={item.href}
preservePeriod={item.preservePeriod} return (
className="flex items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors" <li key={item.href}>
> <NavLink
<span className="text-muted-foreground shrink-0">{item.icon}</span> href={item.href}
<span className="flex-1">{item.label}</span> preservePeriod={item.preservePeriod}
{item.badge && item.badge > 0 ? ( className={cn(
<Badge "flex items-center gap-3 rounded-sm px-2 py-2 text-sm transition-colors",
variant="secondary" isActive
className="text-xs px-1.5 py-0 h-4 min-w-4 ml-auto" ? "bg-accent text-foreground"
: "text-foreground hover:bg-accent",
)}
>
<span
className={cn(
"shrink-0",
isActive
? (item.iconClass ?? "text-foreground")
: (item.iconClass ?? "text-muted-foreground"),
)}
> >
{item.badge} {item.icon}
</Badge> </span>
) : null} <span className="flex flex-col min-w-0">
</NavLink> <span
</li> className={cn("font-medium", isActive && "font-semibold")}
))} >
{item.label}
</span>
{item.description && (
<span className="text-xs text-muted-foreground truncate lowercase">
{item.description}
</span>
)}
</span>
{item.badge && item.badge > 0 ? (
<Badge
variant="secondary"
className="text-xs px-1.5 py-0 h-4 min-w-4 ml-auto shrink-0"
>
{item.badge}
</Badge>
) : null}
</NavLink>
</li>
);
})}
</ul> </ul>
); );
} }

View File

@@ -17,7 +17,9 @@ import {
export type NavItem = { export type NavItem = {
href: string; href: string;
label: string; label: string;
description?: string;
icon: React.ReactNode; icon: React.ReactNode;
iconClass?: string;
badge?: number; badge?: number;
preservePeriod?: boolean; preservePeriod?: boolean;
hideOnMobile?: boolean; hideOnMobile?: boolean;
@@ -35,18 +37,24 @@ export const NAV_SECTIONS: NavSection[] = [
{ {
href: "/transactions", href: "/transactions",
label: "lançamentos", label: "lançamentos",
description: "Registre e gerencie suas transações",
icon: <RiArrowLeftRightLine className="size-4" />, icon: <RiArrowLeftRightLine className="size-4" />,
iconClass: "text-primary",
preservePeriod: true, preservePeriod: true,
}, },
{ {
href: "/inbox", href: "/inbox",
label: "pré-lançamentos", label: "pré-lançamentos",
description: "Notificações capturadas pelo Companion",
icon: <RiAtLine className="size-4" />, icon: <RiAtLine className="size-4" />,
iconClass: "text-primary",
}, },
{ {
href: "/calendar", href: "/calendar",
label: "calendário", label: "calendário",
description: "Visualize lançamentos por dia",
icon: <RiCalendarEventLine className="size-4" />, icon: <RiCalendarEventLine className="size-4" />,
iconClass: "text-primary",
hideOnMobile: true, hideOnMobile: true,
}, },
], ],
@@ -57,17 +65,23 @@ export const NAV_SECTIONS: NavSection[] = [
{ {
href: "/cards", href: "/cards",
label: "cartões", label: "cartões",
description: "Faturas e limites dos seus cartões",
icon: <RiBankCard2Line className="size-4" />, icon: <RiBankCard2Line className="size-4" />,
iconClass: "text-primary",
}, },
{ {
href: "/accounts", href: "/accounts",
label: "contas", label: "contas",
description: "Saldos e extratos bancários",
icon: <RiBankLine className="size-4" />, icon: <RiBankLine className="size-4" />,
iconClass: "text-primary",
}, },
{ {
href: "/budgets", href: "/budgets",
label: "orçamentos", label: "orçamentos",
description: "Defina limites de gastos por categoria",
icon: <RiBarChart2Line className="size-4" />, icon: <RiBarChart2Line className="size-4" />,
iconClass: "text-primary",
preservePeriod: true, preservePeriod: true,
}, },
], ],
@@ -78,17 +92,23 @@ export const NAV_SECTIONS: NavSection[] = [
{ {
href: "/payers", href: "/payers",
label: "pagadores", label: "pagadores",
description: "Gerencie quem divide as despesas",
icon: <RiGroupLine className="size-4" />, icon: <RiGroupLine className="size-4" />,
iconClass: "text-primary",
}, },
{ {
href: "/categories", href: "/categories",
label: "categorias", label: "categorias",
description: "Agrupe seus lançamentos",
icon: <RiPriceTag3Line className="size-4" />, icon: <RiPriceTag3Line className="size-4" />,
iconClass: "text-primary",
}, },
{ {
href: "/notes", href: "/notes",
label: "anotações", label: "anotações",
description: "Guarde lembretes e observações",
icon: <RiTodoLine className="size-4" />, icon: <RiTodoLine className="size-4" />,
iconClass: "text-primary",
}, },
], ],
}, },
@@ -98,29 +118,39 @@ export const NAV_SECTIONS: NavSection[] = [
{ {
href: "/insights", href: "/insights",
label: "insights", label: "insights",
description: "Análises inteligentes dos seus dados",
icon: <RiSparklingLine className="size-4" />, icon: <RiSparklingLine className="size-4" />,
iconClass: "text-primary",
preservePeriod: true, preservePeriod: true,
}, },
{ {
href: "/reports/category-trends", href: "/reports/category-trends",
label: "tendências", label: "tendências",
description: "Evolução de gastos por categoria",
icon: <RiFileChartLine className="size-4" />, icon: <RiFileChartLine className="size-4" />,
iconClass: "text-primary",
}, },
{ {
href: "/reports/card-usage", href: "/reports/card-usage",
label: "uso de cartões", label: "uso de cartões",
description: "Resumo de gastos por cartão",
icon: <RiBankCard2Line className="size-4" />, icon: <RiBankCard2Line className="size-4" />,
iconClass: "text-primary",
preservePeriod: true, preservePeriod: true,
}, },
{ {
href: "/reports/installment-analysis", href: "/reports/installment-analysis",
label: "análise de parcelas", label: "análise de parcelas",
description: "Acompanhe parcelas em aberto",
icon: <RiSecurePaymentLine className="size-4" />, icon: <RiSecurePaymentLine className="size-4" />,
iconClass: "text-primary",
}, },
{ {
href: "/reports/establishments", href: "/reports/establishments",
label: "estabelecimentos", label: "estabelecimentos",
description: "Top gastos por estabelecimento",
icon: <RiStore2Line className="size-4" />, icon: <RiStore2Line className="size-4" />,
iconClass: "text-primary",
}, },
], ],
}, },

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { RiDashboardLine, RiMenuLine } from "@remixicon/react"; import { RiDashboardLine, RiMenuLine } from "@remixicon/react";
import { usePathname } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { CalculatorDialogContent } from "@/shared/components/calculator/calculator-dialog"; import { CalculatorDialogContent } from "@/shared/components/calculator/calculator-dialog";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
@@ -23,10 +24,11 @@ import { MobileLink, MobileSectionLabel } from "./mobile-link";
import { NavDropdown } from "./nav-dropdown"; import { NavDropdown } from "./nav-dropdown";
import { NAV_SECTIONS } from "./nav-items"; import { NAV_SECTIONS } from "./nav-items";
import { NavPill } from "./nav-pill"; import { NavPill } from "./nav-pill";
import { triggerClass } from "./nav-styles"; import { triggerActiveClass, triggerClass } from "./nav-styles";
import { MobileTools, NavToolsDropdown } from "./nav-tools"; import { MobileTools, NavToolsDropdown } from "./nav-tools";
export function NavMenu() { export function NavMenu() {
const pathname = usePathname();
const [sheetOpen, setSheetOpen] = useState(false); const [sheetOpen, setSheetOpen] = useState(false);
const [calculatorOpen, setCalculatorOpen] = useState(false); const [calculatorOpen, setCalculatorOpen] = useState(false);
const close = () => setSheetOpen(false); const close = () => setSheetOpen(false);
@@ -44,16 +46,25 @@ export function NavMenu() {
</NavPill> </NavPill>
</NavigationMenuItem> </NavigationMenuItem>
{NAV_SECTIONS.map((section) => ( {NAV_SECTIONS.map((section) => {
<NavigationMenuItem key={section.label}> const isSectionActive = section.items.some(
<NavigationMenuTrigger className={triggerClass}> (item) =>
{section.label} pathname === item.href ||
</NavigationMenuTrigger> pathname.startsWith(`${item.href}/`),
<NavigationMenuContent> );
<NavDropdown items={section.items} /> return (
</NavigationMenuContent> <NavigationMenuItem key={section.label}>
</NavigationMenuItem> <NavigationMenuTrigger
))} className={`${triggerClass} ${isSectionActive ? triggerActiveClass : ""}`}
>
{section.label}
</NavigationMenuTrigger>
<NavigationMenuContent>
<NavDropdown items={section.items} />
</NavigationMenuContent>
</NavigationMenuItem>
);
})}
<NavigationMenuItem> <NavigationMenuItem>
<NavigationMenuTrigger className={triggerClass}> <NavigationMenuTrigger className={triggerClass}>
@@ -109,6 +120,7 @@ export function NavMenu() {
onClick={close} onClick={close}
badge={item.badge} badge={item.badge}
preservePeriod={item.preservePeriod} preservePeriod={item.preservePeriod}
description={item.description}
> >
{item.label} {item.label}
</MobileLink> </MobileLink>

View File

@@ -1,14 +1,12 @@
// Base para links diretos e triggers — pill arredondado
export const linkBase = export const linkBase =
"inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium transition-all lowercase"; "inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium transition-all lowercase";
// Estado inativo: muted, hover suave sem underline
export const linkIdle = "text-black/75 hover:bg-black/10 hover:text-black"; export const linkIdle = "text-black/75 hover:bg-black/10 hover:text-black";
// Estado ativo: pill com cor primária export const linkActive = "bg-black/15 text-black";
export const linkActive = "bg-black/10 text-black";
export const triggerActiveClass = ["bg-black/15!", "text-black!"].join(" ");
// Trigger do NavigationMenu — espelha linkBase + linkIdle, remove estilos padrão
export const triggerClass = [ export const triggerClass = [
"h-8!", "h-8!",
"rounded-md!", "rounded-md!",

View File

@@ -3,7 +3,6 @@
import { RiCalculatorLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react"; import { RiCalculatorLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react";
import { usePrivacyMode } from "@/shared/components/providers/privacy-provider"; import { usePrivacyMode } from "@/shared/components/providers/privacy-provider";
import { Badge } from "@/shared/components/ui/badge"; import { Badge } from "@/shared/components/ui/badge";
import { cn } from "@/shared/utils/ui";
const itemClass = const itemClass =
"flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer"; "flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer";
@@ -16,29 +15,35 @@ export function NavToolsDropdown({ onOpenCalculator }: NavToolsDropdownProps) {
const { privacyMode, toggle } = usePrivacyMode(); const { privacyMode, toggle } = usePrivacyMode();
return ( return (
<ul className="grid w-52 gap-0.5 p-2"> <ul className="grid w-72 gap-0.5 p-2">
<li> <li>
<button <button type="button" className={itemClass} onClick={onOpenCalculator}>
type="button" <span className="text-primary shrink-0">
className={cn(itemClass)}
onClick={onOpenCalculator}
>
<span className="text-muted-foreground shrink-0">
<RiCalculatorLine className="size-4" /> <RiCalculatorLine className="size-4" />
</span> </span>
<span className="flex-1 text-left">calculadora</span> <span className="flex flex-col flex-1 text-left">
<span className="font-medium">calculadora</span>
<span className="text-xs text-muted-foreground lowercase">
Faça cálculos rápidos
</span>
</span>
</button> </button>
</li> </li>
<li> <li>
<button type="button" onClick={toggle} className={cn(itemClass)}> <button type="button" onClick={toggle} className={itemClass}>
<span className="text-muted-foreground shrink-0"> <span className="text-primary shrink-0">
{privacyMode ? ( {privacyMode ? (
<RiEyeOffLine className="size-4" /> <RiEyeOffLine className="size-4" />
) : ( ) : (
<RiEyeLine className="size-4" /> <RiEyeLine className="size-4" />
)} )}
</span> </span>
<span className="flex-1 text-left">privacidade</span> <span className="flex flex-col flex-1 text-left">
<span className="font-medium">privacidade</span>
<span className="text-xs text-muted-foreground lowercase">
Oculta valores na tela
</span>
</span>
{privacyMode && ( {privacyMode && (
<Badge <Badge
variant="secondary" variant="secondary"

View File

@@ -9,7 +9,7 @@ import {
RiCheckboxCircleFill, RiCheckboxCircleFill,
RiErrorWarningLine, RiErrorWarningLine,
RiFileListLine, RiFileListLine,
RiNotification3Line, RiNotification2Line,
RiTimeLine, RiTimeLine,
} from "@remixicon/react"; } from "@remixicon/react";
import Image from "next/image"; import Image from "next/image";
@@ -70,11 +70,58 @@ function SectionLabel({
return ( return (
<div className="flex items-center gap-1.5 px-3 pb-1 pt-3"> <div className="flex items-center gap-1.5 px-3 pb-1 pt-3">
<span className="text-muted-foreground">{icon}</span> <span className="text-muted-foreground">{icon}</span>
<span className="text-xs text-muted-foreground">{title}</span> <span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
{title}
</span>
</div> </div>
); );
} }
type NotificationItemProps = {
href: string;
icon: React.ReactNode;
isOverdue: boolean;
title: string;
detail: string;
onClose: () => void;
};
function NotificationItem({
href,
icon,
isOverdue,
title,
detail,
onClose,
}: NotificationItemProps) {
return (
<Link
href={href}
onClick={onClose}
className={cn(
"group mx-1 mb-0.5 flex items-start gap-2.5 rounded-md px-2 py-2 transition-colors hover:bg-accent/60",
isOverdue && "bg-destructive/5 hover:bg-destructive/10",
)}
>
<span className="mt-0.5 shrink-0">{icon}</span>
<span className="flex flex-1 flex-col gap-0.5 min-w-0">
<span
className={cn(
"text-[12px] font-medium leading-snug",
isOverdue ? "text-destructive" : "text-foreground",
)}
>
{title}
</span>
<span className="text-[11px] leading-snug text-muted-foreground">
{detail}
</span>
</span>
<RiArrowRightLine className="mt-0.5 size-3 shrink-0 text-muted-foreground/50 transition-transform group-hover:translate-x-0.5" />
</Link>
);
}
export function NotificationBell({ export function NotificationBell({
notifications, notifications,
totalCount, totalCount,
@@ -105,12 +152,13 @@ export function NotificationBell({
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }), buttonVariants({ variant: "ghost", size: "icon-sm" }),
"group relative border border-black/10 text-black/75 shadow-none transition-all duration-200", "group relative shadow-none transition-all duration-200",
"hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-2 focus-visible:ring-black/20", "hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-2 focus-visible:ring-black/20",
"data-[state=open]:bg-black/10 data-[state=open]:text-black", "data-[state=open]:bg-black/10 data-[state=open]:text-black",
hasNotifications ? "text-black" : "text-black/75",
)} )}
> >
<RiNotification3Line <RiNotification2Line
className={cn( className={cn(
"size-4 transition-transform duration-200", "size-4 transition-transform duration-200",
open ? "scale-90" : "scale-100", open ? "scale-90" : "scale-100",
@@ -120,7 +168,7 @@ export function NotificationBell({
<> <>
<span <span
aria-hidden aria-hidden
className="absolute -right-1.5 -top-1.5 inline-flex min-h-5 min-w-5 items-center justify-center rounded-full bg-destructive px-1 text-xs font-semibold text-destructive-foreground " className="absolute -right-1.5 -top-1.5 inline-flex min-h-5 min-w-5 items-center justify-center rounded-full bg-destructive px-1 text-[12px] font-semibold text-destructive-foreground"
> >
{displayCount} {displayCount}
</span> </span>
@@ -138,10 +186,10 @@ export function NotificationBell({
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
sideOffset={12} sideOffset={12}
className="w-76 overflow-hidden rounded-lg border border-border/60 bg-popover p-0 shadow-none" className="w-80 overflow-hidden rounded-lg border border-border/60 bg-popover p-0 shadow-none"
> >
{/* Header */} {/* Header */}
<DropdownMenuLabel className="flex items-center justify-between gap-2 border-b border-border/50 px-3 py-2.5 text-sm font-semibold"> <DropdownMenuLabel className="flex items-center justify-between gap-2 border-b border-border/50 px-3 py-2.5 text-[12px] font-semibold">
<span>Notificações</span> <span>Notificações</span>
{hasNotifications && ( {hasNotifications && (
<Badge variant="outline" className="text-[10px] font-semibold"> <Badge variant="outline" className="text-[10px] font-semibold">
@@ -172,19 +220,18 @@ export function NotificationBell({
icon={<RiAtLine className="size-3" />} icon={<RiAtLine className="size-3" />}
title="Pré-lançamentos" title="Pré-lançamentos"
/> />
<Link <NotificationItem
href="/inbox" href="/inbox"
onClick={() => setOpen(false)} isOverdue={false}
className="group mx-1 mb-1 flex items-center gap-2 rounded-md px-2 py-2 transition-colors hover:bg-accent/60" icon={<RiAtLine className="size-5 text-primary" />}
> title={
<RiAtLine className="size-6 shrink-0 text-primary" /> preLancamentosCount === 1
<p className="flex-1 text-xs leading-snug text-foreground"> ? "1 pré-lançamento pendente"
{preLancamentosCount === 1 : `${preLancamentosCount} pré-lançamentos pendentes`
? "1 pré-lançamento aguardando revisão" }
: `${preLancamentosCount} pré-lançamentos aguardando revisão`} detail="Aguardando revisão"
</p> onClose={() => setOpen(false)}
<RiArrowRightLine className="size-3 shrink-0 text-muted-foreground/50 transition-transform group-hover:translate-x-0.5" /> />
</Link>
</div> </div>
)} )}
@@ -195,48 +242,27 @@ export function NotificationBell({
icon={<RiBarChart2Line className="size-3" />} icon={<RiBarChart2Line className="size-3" />}
title="Orçamentos" title="Orçamentos"
/> />
<div className="mx-1 mb-1 overflow-hidden rounded-md"> {budgetNotifications.map((n) => (
{budgetNotifications.map((n) => ( <NotificationItem
<div key={n.id}
key={n.id} href="/budgets"
className="flex items-start gap-2 px-2 py-2" isOverdue={n.status === "exceeded"}
> icon={
{n.status === "exceeded" ? ( n.status === "exceeded" ? (
<RiAlertFill className="mt-0.5 size-6 shrink-0 text-destructive" /> <RiAlertFill className="size-5 text-destructive" />
) : ( ) : (
<RiErrorWarningLine className="mt-0.5 size-6 shrink-0 text-amber-500" /> <RiErrorWarningLine className="size-5 text-amber-500" />
)} )
<p className="text-xs leading-snug"> }
{n.status === "exceeded" ? ( title={n.categoryName}
<> detail={
Orçamento de <strong>{n.categoryName}</strong>{" "} n.status === "exceeded"
excedido {" "} ? `Excedido — ${formatCurrency(n.spentAmount)} de ${formatCurrency(n.budgetAmount)} (${formatPercentage(n.usedPercentage, { maximumFractionDigits: 0, minimumFractionDigits: 0 })})`
<strong>{formatCurrency(n.spentAmount)}</strong> de{" "} : `${formatPercentage(n.usedPercentage, { maximumFractionDigits: 0, minimumFractionDigits: 0 })} utilizado — ${formatCurrency(n.spentAmount)} de ${formatCurrency(n.budgetAmount)}`
{formatCurrency(n.budgetAmount)} ( }
{formatPercentage(n.usedPercentage, { onClose={() => setOpen(false)}
maximumFractionDigits: 0, />
minimumFractionDigits: 0, ))}
})}
)
</>
) : (
<>
<strong>{n.categoryName}</strong> atingiu{" "}
<strong>
{formatPercentage(n.usedPercentage, {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
})}
</strong>{" "}
do orçamento {" "}
<strong>{formatCurrency(n.spentAmount)}</strong> de{" "}
{formatCurrency(n.budgetAmount)}
</>
)}
</p>
</div>
))}
</div>
</div> </div>
)} )}
@@ -247,56 +273,38 @@ export function NotificationBell({
icon={<RiBankCardLine className="size-3" />} icon={<RiBankCardLine className="size-3" />}
title="Cartão de Crédito" title="Cartão de Crédito"
/> />
<div className="mx-1 mb-1 overflow-hidden rounded-md"> {invoiceNotifications.map((n) => {
{invoiceNotifications.map((n) => { const logo = resolveLogoSrc(n.cardLogo);
const logo = resolveLogoSrc(n.cardLogo); return (
return ( <NotificationItem
<div key={n.id}
key={n.id} href="/cards"
className="flex items-start gap-2 px-2 py-2" isOverdue={n.status === "overdue"}
> icon={
{logo ? ( logo ? (
<Image <Image
src={logo} src={logo}
alt="" alt=""
width={24} width={20}
height={24} height={20}
className="mt-0.5 size-6 shrink-0 rounded-sm object-contain" className="size-5 rounded-full object-contain"
/> />
) : n.status === "overdue" ? ( ) : n.status === "overdue" ? (
<RiAlertFill className="mt-0.5 size-3.5 shrink-0 text-destructive" /> <RiAlertFill className="size-5 text-destructive" />
) : ( ) : (
<RiTimeLine className="mt-0.5 size-3.5 shrink-0 text-amber-500" /> <RiTimeLine className="size-5 text-amber-500" />
)} )
<p className="text-xs leading-snug"> }
{n.status === "overdue" ? ( title={n.name}
<> detail={
A fatura de <strong>{n.name}</strong> venceu em{" "} n.status === "overdue"
{formatDate(n.dueDate)} ? `Venceu em ${formatDate(n.dueDate)}${n.showAmount && n.amount > 0 ? `${formatCurrency(n.amount)}` : ""}`
{n.showAmount && n.amount > 0 && ( : `Vence em ${formatDate(n.dueDate)}${n.showAmount && n.amount > 0 ? `${formatCurrency(n.amount)}` : ""}`
<> }
{" "} onClose={() => setOpen(false)}
<strong>{formatCurrency(n.amount)}</strong> />
</> );
)} })}
</>
) : (
<>
A fatura de <strong>{n.name}</strong> vence em{" "}
{formatDate(n.dueDate)}
{n.showAmount && n.amount > 0 && (
<>
{" "}
<strong>{formatCurrency(n.amount)}</strong>
</>
)}
</>
)}
</p>
</div>
);
})}
</div>
</div> </div>
)} )}
@@ -307,48 +315,30 @@ export function NotificationBell({
icon={<RiFileListLine className="size-3" />} icon={<RiFileListLine className="size-3" />}
title="Boletos" title="Boletos"
/> />
<div className="mx-1 mb-1 overflow-hidden rounded-md"> {boletoNotifications.map((n) => (
{boletoNotifications.map((n) => ( <NotificationItem
<div key={n.id}
key={n.id} href="/transactions"
className="flex items-start gap-2 px-2 py-2" isOverdue={n.status === "overdue"}
> icon={
<RiAlertFill <RiAlertFill
className={cn( className={cn(
"mt-0.5 size-6 shrink-0", "size-5",
n.status === "overdue" n.status === "overdue"
? "text-destructive" ? "text-destructive"
: "text-amber-500", : "text-amber-500",
)} )}
/> />
<p className="text-xs leading-snug"> }
{n.status === "overdue" ? ( title={n.name}
<> detail={
O boleto <strong>{n.name}</strong> n.status === "overdue"
{n.amount > 0 && ( ? `Venceu em ${formatDate(n.dueDate)}${n.amount > 0 ? `${formatCurrency(n.amount)}` : ""}`
<> : `Vence em ${formatDate(n.dueDate)}${n.amount > 0 ? `${formatCurrency(n.amount)}` : ""}`
{" "} }
<strong>{formatCurrency(n.amount)}</strong> onClose={() => setOpen(false)}
</> />
)}{" "} ))}
venceu em {formatDate(n.dueDate)}
</>
) : (
<>
O boleto <strong>{n.name}</strong>
{n.amount > 0 && (
<>
{" "}
<strong>{formatCurrency(n.amount)}</strong>
</>
)}{" "}
vence em {formatDate(n.dueDate)}
</>
)}
</p>
</div>
))}
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -38,7 +38,7 @@ export function RefreshPageButton({
className={cn( className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }), buttonVariants({ variant: "ghost", size: "icon-sm" }),
"size-8 text-muted-foreground transition-all duration-200", "size-8 text-muted-foreground transition-all duration-200",
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40 border", "hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
"disabled:pointer-events-none disabled:opacity-50", "disabled:pointer-events-none disabled:opacity-50",
className, className,
)} )}

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 border py-6 rounded-md hover:border-primary/50 transition-all ease-in-out duration-300", "bg-card text-card-foreground flex flex-col gap-4 border py-6 rounded-lg",
className, className,
)} )}
{...props} {...props}

View File

@@ -17,6 +17,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
offset={{ top: 60 }}
icons={{ icons={{
success: <RiCheckboxCircleLine className="size-4" />, success: <RiCheckboxCircleLine className="size-4" />,
info: <RiInformationLine className="size-4" />, info: <RiInformationLine className="size-4" />,

View File

@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b border-dashed transition-colors", "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className, className,
)} )}
{...props} {...props}

View File

@@ -37,7 +37,7 @@ export default function WidgetCard({
<CardHeader> <CardHeader>
<div className="flex w-full items-start justify-between"> <div className="flex w-full items-start justify-between">
<div> <div>
<CardTitle className="flex items-center gap-1 tracking-tighter lowercase"> <CardTitle className="flex items-center gap-1 tracking-tight lowercase">
<span className="size-4">{icon}</span> <span className="size-4">{icon}</span>
{title} {title}
</CardTitle> </CardTitle>