mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 02:51:46 +00:00
style: refina base visual e navegação
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?{" "}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user