mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
style: refina base visual e navegação
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -195,6 +195,11 @@ export function LoginForm({ className, ...props }: DivProps) {
|
|||||||
</FieldSeparator>
|
</FieldSeparator>
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
passkeySupported ? "grid grid-cols-2 gap-2" : "flex",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<GoogleAuthButton
|
<GoogleAuthButton
|
||||||
onClick={handleGoogle}
|
onClick={handleGoogle}
|
||||||
loading={loadingGoogle}
|
loading={loadingGoogle}
|
||||||
@@ -204,12 +209,10 @@ export function LoginForm({ className, ...props }: DivProps) {
|
|||||||
loadingPasskey ||
|
loadingPasskey ||
|
||||||
!isGoogleAvailable
|
!isGoogleAvailable
|
||||||
}
|
}
|
||||||
text="Entrar com Google"
|
text="Google"
|
||||||
/>
|
/>
|
||||||
</Field>
|
|
||||||
|
|
||||||
{passkeySupported && (
|
{passkeySupported && (
|
||||||
<Field>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -222,10 +225,11 @@ export function LoginForm({ className, ...props }: DivProps) {
|
|||||||
) : (
|
) : (
|
||||||
<RiFingerprintLine className="h-5 w-5" />
|
<RiFingerprintLine className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
<span>Entrar com passkey</span>
|
<span>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?{" "}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<ul className="grid w-72 gap-0.5 p-2">
|
||||||
|
{items.map((item) => {
|
||||||
|
const isActive =
|
||||||
|
pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="grid w-48 gap-0.5 p-2">
|
|
||||||
{items.map((item) => (
|
|
||||||
<li key={item.href}>
|
<li key={item.href}>
|
||||||
<NavLink
|
<NavLink
|
||||||
href={item.href}
|
href={item.href}
|
||||||
preservePeriod={item.preservePeriod}
|
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"
|
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="text-muted-foreground shrink-0">{item.icon}</span>
|
<span
|
||||||
<span className="flex-1">{item.label}</span>
|
className={cn(
|
||||||
|
"shrink-0",
|
||||||
|
isActive
|
||||||
|
? (item.iconClass ?? "text-foreground")
|
||||||
|
: (item.iconClass ?? "text-muted-foreground"),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{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 ? (
|
{item.badge && item.badge > 0 ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="text-xs px-1.5 py-0 h-4 min-w-4 ml-auto"
|
className="text-xs px-1.5 py-0 h-4 min-w-4 ml-auto shrink-0"
|
||||||
>
|
>
|
||||||
{item.badge}
|
{item.badge}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
const isSectionActive = section.items.some(
|
||||||
|
(item) =>
|
||||||
|
pathname === item.href ||
|
||||||
|
pathname.startsWith(`${item.href}/`),
|
||||||
|
);
|
||||||
|
return (
|
||||||
<NavigationMenuItem key={section.label}>
|
<NavigationMenuItem key={section.label}>
|
||||||
<NavigationMenuTrigger className={triggerClass}>
|
<NavigationMenuTrigger
|
||||||
|
className={`${triggerClass} ${isSectionActive ? triggerActiveClass : ""}`}
|
||||||
|
>
|
||||||
{section.label}
|
{section.label}
|
||||||
</NavigationMenuTrigger>
|
</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<NavDropdown items={section.items} />
|
<NavDropdown items={section.items} />
|
||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</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>
|
||||||
|
|||||||
@@ -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!",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,49 +242,28 @@ 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) => (
|
||||||
<div
|
<NotificationItem
|
||||||
key={n.id}
|
key={n.id}
|
||||||
className="flex items-start gap-2 px-2 py-2"
|
href="/budgets"
|
||||||
>
|
isOverdue={n.status === "exceeded"}
|
||||||
{n.status === "exceeded" ? (
|
icon={
|
||||||
<RiAlertFill className="mt-0.5 size-6 shrink-0 text-destructive" />
|
n.status === "exceeded" ? (
|
||||||
|
<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" ? (
|
|
||||||
<>
|
|
||||||
Orçamento de <strong>{n.categoryName}</strong>{" "}
|
|
||||||
excedido —{" "}
|
|
||||||
<strong>{formatCurrency(n.spentAmount)}</strong> de{" "}
|
|
||||||
{formatCurrency(n.budgetAmount)} (
|
|
||||||
{formatPercentage(n.usedPercentage, {
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
})}
|
|
||||||
)
|
)
|
||||||
</>
|
}
|
||||||
) : (
|
title={n.categoryName}
|
||||||
<>
|
detail={
|
||||||
<strong>{n.categoryName}</strong> atingiu{" "}
|
n.status === "exceeded"
|
||||||
<strong>
|
? `Excedido — ${formatCurrency(n.spentAmount)} de ${formatCurrency(n.budgetAmount)} (${formatPercentage(n.usedPercentage, { maximumFractionDigits: 0, minimumFractionDigits: 0 })})`
|
||||||
{formatPercentage(n.usedPercentage, {
|
: `${formatPercentage(n.usedPercentage, { maximumFractionDigits: 0, minimumFractionDigits: 0 })} utilizado — ${formatCurrency(n.spentAmount)} de ${formatCurrency(n.budgetAmount)}`
|
||||||
maximumFractionDigits: 0,
|
}
|
||||||
minimumFractionDigits: 0,
|
onClose={() => setOpen(false)}
|
||||||
})}
|
/>
|
||||||
</strong>{" "}
|
|
||||||
do orçamento —{" "}
|
|
||||||
<strong>{formatCurrency(n.spentAmount)}</strong> de{" "}
|
|
||||||
{formatCurrency(n.budgetAmount)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cartão de Crédito */}
|
{/* Cartão de Crédito */}
|
||||||
@@ -247,57 +273,39 @@ 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 (
|
||||||
<div
|
<NotificationItem
|
||||||
key={n.id}
|
key={n.id}
|
||||||
className="flex items-start gap-2 px-2 py-2"
|
href="/cards"
|
||||||
>
|
isOverdue={n.status === "overdue"}
|
||||||
{logo ? (
|
icon={
|
||||||
|
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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Boletos */}
|
{/* Boletos */}
|
||||||
@@ -307,49 +315,31 @@ 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) => (
|
||||||
<div
|
<NotificationItem
|
||||||
key={n.id}
|
key={n.id}
|
||||||
className="flex items-start gap-2 px-2 py-2"
|
href="/transactions"
|
||||||
>
|
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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" />,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user