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 *));
@theme {
--spacing-custom-height-card: 30rem;
--spacing-8xl: 88rem;
--spacing-9xl: 96rem;
--spacing-custom-height-card: 29rem;
--spacing-8xl: 90rem;
}
:root {
--background: oklch(97.036% 0.00276 84.303);
--background: oklch(97.412% 0.00332 67.032);
--foreground: oklch(27% 0.008 45);
--card: var(--background);
--card: oklch(99% 0.002 67);
--card-foreground: var(--foreground);
--popover: oklch(100% 0 0);
--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);
--secondary: oklch(96.2% 0.005 70);
@@ -28,24 +27,23 @@
--accent: oklch(94.8% 0.009 65);
--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);
--warning: oklch(69.913% 0.1798 49.649);
--warning: oklch(78.357% 0.15147 68.301);
--warning-foreground: oklch(20% 0.04 85);
--info: oklch(55% 0.17 250);
--info-foreground: oklch(98% 0.01 250);
--destructive: oklch(55% 0.22 27);
--destructive-foreground: oklch(98% 0.005 30);
--border: oklch(84.567% 0.00583 84.468);
--input: oklch(84.567% 0.00583 84.468);
--border: oklch(90.274% 0.01362 60.342);
--input: var(--border);
--ring: var(--primary);
--chart-1: var(--color-emerald-500);
--chart-2: var(--color-orange-500);
--chart-3: var(--color-indigo-500);
--chart-4: var(--color-amber-500);
--chart-2: var(--color-red-500);
--chart-3: var(--color-amber-500);
--chart-4: var(--color-blue-500);
--chart-5: var(--color-pink-500);
--chart-6: var(--color-stone-500);
--chart-7: var(--color-teal-500);
@@ -62,7 +60,7 @@
--sidebar-border: oklch(91% 0.004 70);
--sidebar-ring: var(--primary);
--radius: 0.5rem;
--radius: 0.625rem;
--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);
@@ -83,37 +81,36 @@
}
.dark {
--background: oklch(20.5% 0.004 55);
--background: oklch(22% 0.004 55);
--foreground: oklch(93% 0.008 80);
--card: oklch(23% 0.004 55);
--card: oklch(25.5% 0.004 55);
--card-foreground: var(--foreground);
--popover: oklch(25% 0.004 55);
--popover: oklch(28% 0.004 55);
--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);
--secondary: oklch(26% 0.004 55);
--secondary: oklch(29% 0.004 55);
--secondary-foreground: var(--foreground);
--muted: oklch(28.5% 0.0035 55);
--muted: oklch(32% 0.0035 55);
--muted-foreground: oklch(73% 0.006 75);
--accent: oklch(30% 0.005 55);
--accent: oklch(33% 0.005 55);
--accent-foreground: var(--foreground);
--success: oklch(65% 0.19 150);
--success: oklch(62% 0.16 150);
--success-foreground: oklch(15% 0.02 150);
--warning: oklch(69.913% 0.1798 49.649);
--warning-foreground: oklch(15% 0.04 85);
--info: oklch(65% 0.17 250);
--info-foreground: oklch(15% 0.02 250);
--destructive: oklch(62% 0.2 28);
--destructive-foreground: oklch(98% 0.005 30);
--border: oklch(33% 0.004 55);
--input: oklch(30% 0.004 55);
--border: oklch(35% 0.004 55);
--input: var(--border);
--ring: var(--primary);
--chart-1: var(--color-emerald-500);
@@ -127,16 +124,16 @@
--chart-9: var(--color-cyan-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-primary: var(--primary);
--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-border: oklch(31% 0.004 55);
--sidebar-border: oklch(34% 0.004 55);
--sidebar-ring: var(--primary);
--radius: 0.5rem;
--radius: 0.625rem;
--shadow-2xs: 0 1px 2px 0px oklch(0% 0 0 / 0.3);
--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) {
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]">
<DotPattern
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]">
Controle suas finanças com clareza e foco diário.
</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
um painel inteligente feito para o seu dia a dia.
</p>

View File

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

View File

@@ -87,7 +87,7 @@ export const AnimatedThemeToggler = ({
buttonVariants({ variant: "ghost", size: "icon-sm" }),
"group relative text-muted-foreground transition-all duration-200",
"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,
)}
{...props}

View File

@@ -68,12 +68,13 @@ export function ExpandableWidgetCard({
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">
<Button
variant="secondary"
className="pointer-events-auto rounded-full text-xs backdrop-blur-sm bg-primary/10"
variant="outline"
className="pointer-events-auto text-xs"
onClick={() => setIsOpen(true)}
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>
</div>
) : null

View File

@@ -42,7 +42,7 @@ export default function MonthNavigation() {
};
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">
<NavigationButton
direction="left"

View File

@@ -30,7 +30,7 @@ export function AppNavbar({
notificationsSnapshot,
}: AppNavbarProps) {
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">
<DotPattern
width={20}
@@ -38,7 +38,7 @@ export function AppNavbar({
cx={1.25}
cy={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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,12 @@
// Base para links diretos e triggers — pill arredondado
export const linkBase =
"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";
// Estado ativo: pill com cor primária
export const linkActive = "bg-black/10 text-black";
export const linkActive = "bg-black/15 text-black";
export const triggerActiveClass = ["bg-black/15!", "text-black!"].join(" ");
// Trigger do NavigationMenu — espelha linkBase + linkIdle, remove estilos padrão
export const triggerClass = [
"h-8!",
"rounded-md!",

View File

@@ -3,7 +3,6 @@
import { RiCalculatorLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react";
import { usePrivacyMode } from "@/shared/components/providers/privacy-provider";
import { Badge } from "@/shared/components/ui/badge";
import { cn } from "@/shared/utils/ui";
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";
@@ -16,29 +15,35 @@ export function NavToolsDropdown({ onOpenCalculator }: NavToolsDropdownProps) {
const { privacyMode, toggle } = usePrivacyMode();
return (
<ul className="grid w-52 gap-0.5 p-2">
<ul className="grid w-72 gap-0.5 p-2">
<li>
<button
type="button"
className={cn(itemClass)}
onClick={onOpenCalculator}
>
<span className="text-muted-foreground shrink-0">
<button type="button" className={itemClass} onClick={onOpenCalculator}>
<span className="text-primary shrink-0">
<RiCalculatorLine className="size-4" />
</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>
</li>
<li>
<button type="button" onClick={toggle} className={cn(itemClass)}>
<span className="text-muted-foreground shrink-0">
<button type="button" onClick={toggle} className={itemClass}>
<span className="text-primary shrink-0">
{privacyMode ? (
<RiEyeOffLine className="size-4" />
) : (
<RiEyeLine className="size-4" />
)}
</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 && (
<Badge
variant="secondary"

View File

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

View File

@@ -38,7 +38,7 @@ export function RefreshPageButton({
className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }),
"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",
className,
)}

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
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,
)}
{...props}

View File

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

View File

@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr
data-slot="table-row"
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,
)}
{...props}

View File

@@ -37,7 +37,7 @@ export default function WidgetCard({
<CardHeader>
<div className="flex w-full items-start justify-between">
<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>
{title}
</CardTitle>