Refina tema global e experiencia visual de auth

This commit is contained in:
Felipe Coutinho
2026-03-14 18:35:39 +00:00
parent 9fb3cc5ecd
commit 1e8e6e0d3d
14 changed files with 419 additions and 334 deletions

View File

@@ -7,6 +7,11 @@ export const america = localFont({
weight: "400", weight: "400",
style: "normal", style: "normal",
}, },
// {
// path: "./america-medium.woff2",
// weight: "500",
// style: "normal",
// },
], ],
display: "swap", display: "swap",
variable: "--font-america", variable: "--font-america",

View File

@@ -2,8 +2,8 @@ import { LoginForm } from "@/features/auth/components/login-form";
export default function LoginPage() { export default function LoginPage() {
return ( return (
<div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10"> <div className="flex min-h-svh flex-col items-center justify-center bg-linear-to-b from-background via-background to-muted/20 px-5 py-8 md:px-8 md:py-10">
<div className="w-full max-w-sm md:max-w-4xl"> <div className="w-full max-w-sm md:max-w-5xl">
<LoginForm /> <LoginForm />
</div> </div>
</div> </div>

View File

@@ -2,8 +2,8 @@ import { SignupForm } from "@/features/auth/components/signup-form";
export default function Page() { export default function Page() {
return ( return (
<div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10"> <div className="flex min-h-svh flex-col items-center justify-center bg-linear-to-b from-background via-background to-muted/20 px-5 py-8 md:px-8 md:py-10">
<div className="w-full max-w-sm md:max-w-4xl"> <div className="w-full max-w-sm md:max-w-5xl">
<SignupForm /> <SignupForm />
</div> </div>
</div> </div>

View File

@@ -1,5 +1,4 @@
import { import {
RiArrowRightSLine,
RiBankCard2Line, RiBankCard2Line,
RiBarChartBoxLine, RiBarChartBoxLine,
RiCalendarLine, RiCalendarLine,
@@ -211,7 +210,6 @@ export default async function Page() {
<Link href="/signup"> <Link href="/signup">
<Button size="sm" className="gap-2"> <Button size="sm" className="gap-2">
Começar Começar
<RiArrowRightSLine size={16} />
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@@ -4,53 +4,44 @@
@theme { @theme {
--spacing-custom-height-card: 30rem; --spacing-custom-height-card: 30rem;
--spacing-8xl: 88rem; /* 1408px */ --spacing-8xl: 88rem;
--spacing-9xl: 96rem; /* 1536px */ --spacing-9xl: 96rem;
} }
:root { :root {
/* Base surfaces - warm cream with subtle orange undertone */ --background: oklch(97.036% 0.00276 84.303);
--background: oklch(98.01% 0.00331 67.026); --foreground: oklch(27% 0.008 45);
--foreground: #201207;
--card: var(--background); --card: var(--background);
--card-foreground: #201207; --card-foreground: var(--foreground);
--popover: oklch(99.5% 0.004 80); --popover: oklch(100% 0 0);
--popover-foreground: oklch(18% 0.02 45); --popover-foreground: var(--foreground);
/* Primary - rich terracotta orange */ --primary: oklch(72.085% 0.16286 50.705);
--primary: #f17a35;
--primary-foreground: oklch(98% 0.008 80); --primary-foreground: oklch(98% 0.008 80);
/* Secondary - warm stone with subtle saturation */ --secondary: oklch(96.2% 0.005 70);
--secondary: oklch(94% 0.018 70); --secondary-foreground: oklch(30% 0.01 45);
--secondary-foreground: oklch(25% 0.025 45);
/* Muted - softer background variant */ --muted: oklch(95% 0.0035 70);
--muted: oklch(94.5% 0.014 75); --muted-foreground: oklch(50% 0.007 50);
--muted-foreground: #44413c;
/* Accent - complementary warm tone */ --accent: oklch(94.8% 0.009 65);
--accent: oklch(94% 0.01 70); --accent-foreground: var(--foreground);
--accent-foreground: #44413c;
/* Semantic states */ --success: oklch(61.654% 0.14385 157.131);
--success: oklch(55.87% 0.12943 157.517);
--success-foreground: oklch(98% 0.01 150); --success-foreground: oklch(98% 0.01 150);
--warning: oklch(69.913% 0.1798 49.649); --warning: oklch(69.913% 0.1798 49.649);
--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 - accessible red */
--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);
/* Borders and inputs - defined but subtle */ --border: oklch(84.567% 0.00583 84.468);
--border: oklch(82% 0.012 75); --input: oklch(84.567% 0.00583 84.468);
--input: oklch(82% 0.012 75); --ring: var(--primary);
--ring: oklch(69.18% 0.18855 38.353);
/* Charts - 10 harmonious, distinct, accessible colors */
--chart-1: var(--color-emerald-500); --chart-1: var(--color-emerald-500);
--chart-2: var(--color-orange-500); --chart-2: var(--color-orange-500);
--chart-3: var(--color-indigo-500); --chart-3: var(--color-indigo-500);
@@ -62,20 +53,17 @@
--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 - slight elevation from background */ --sidebar: oklch(99.3% 0.0015 75);
--sidebar: oklch(100% 0 0); --sidebar-foreground: var(--foreground);
--sidebar-foreground: oklch(20% 0.02 45); --sidebar-primary: var(--primary);
--sidebar-primary: oklch(25% 0.025 45); --sidebar-primary-foreground: var(--primary-foreground);
--sidebar-primary-foreground: oklch(98% 0.008 80); --sidebar-accent: oklch(96.5% 0.004 70);
--sidebar-accent: oklch(96.563% 0.00504 67.275); --sidebar-accent-foreground: var(--foreground);
--sidebar-accent-foreground: oklch(22% 0.025 45); --sidebar-border: oklch(91% 0.004 70);
--sidebar-border: oklch(69.18% 0.18855 38.353); --sidebar-ring: var(--primary);
--sidebar-ring: oklch(69.18% 0.18855 38.353);
/* Layout */
--radius: 0.5rem; --radius: 0.5rem;
/* Shadows - warm tinted for cohesion */
--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);
--shadow-sm: 0 1px 3px 0px oklch(35% 0.02 45 / 0.08), --shadow-sm: 0 1px 3px 0px oklch(35% 0.02 45 / 0.08),
@@ -95,31 +83,25 @@
} }
.dark { .dark {
/* Base surfaces - warm dark with consistent hue family */ --background: oklch(20.5% 0.004 55);
--background: oklch(18.5% 0.002 70); --foreground: oklch(93% 0.008 80);
--foreground: oklch(92% 0.015 80); --card: oklch(23% 0.004 55);
--card: var(--background); --card-foreground: var(--foreground);
--card-foreground: oklch(92% 0.015 80); --popover: oklch(25% 0.004 55);
--popover: oklch(24% 0.003 70); --popover-foreground: var(--foreground);
--popover-foreground: oklch(92% 0.015 80);
/* Primary - vibrant terracotta stands out on dark */ --primary: oklch(72.085% 0.16286 50.705);
--primary: #fa6c26; --primary-foreground: oklch(16% 0.004 60);
--primary-foreground: oklch(20% 0.002 70);
/* Secondary - elevated surface */ --secondary: oklch(26% 0.004 55);
--secondary: oklch(22% 0.004 70); --secondary-foreground: var(--foreground);
--secondary-foreground: oklch(92% 0.015 80);
/* Muted - subtle surface variant */ --muted: oklch(28.5% 0.0035 55);
--muted: oklch(33.5% 0.005 70); --muted-foreground: oklch(73% 0.006 75);
--muted-foreground: oklch(72% 0.004 70);
/* Accent - subtle highlight */ --accent: oklch(30% 0.005 55);
--accent: oklch(27% 0.004 70); --accent-foreground: var(--foreground);
--accent-foreground: oklch(92% 0.015 80);
/* Semantic states */
--success: oklch(65% 0.19 150); --success: oklch(65% 0.19 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);
@@ -127,16 +109,13 @@
--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 - accessible red for dark */
--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);
/* Borders and inputs - visible but subtle */ --border: oklch(33% 0.004 55);
--border: oklch(37% 0.01 70); --input: oklch(30% 0.004 55);
--input: oklch(32% 0.005 70); --ring: var(--primary);
--ring: oklch(69.18% 0.18855 38.353);
/* Charts - bright and distinct on dark */
--chart-1: var(--color-emerald-500); --chart-1: var(--color-emerald-500);
--chart-2: var(--color-orange-500); --chart-2: var(--color-orange-500);
--chart-3: var(--color-indigo-500); --chart-3: var(--color-indigo-500);
@@ -148,20 +127,17 @@
--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 - slight separation from main */ --sidebar: oklch(18% 0.004 55);
--sidebar: oklch(24% 0.003 70); --sidebar-foreground: var(--foreground);
--sidebar-foreground: oklch(92% 0.015 80); --sidebar-primary: var(--primary);
--sidebar-primary: oklch(69.18% 0.18855 38.353); --sidebar-primary-foreground: var(--primary-foreground);
--sidebar-primary-foreground: oklch(13% 0.006 70); --sidebar-accent: oklch(27% 0.004 55);
--sidebar-accent: oklch(32% 0.004 70); --sidebar-accent-foreground: var(--foreground);
--sidebar-accent-foreground: oklch(92% 0.015 80); --sidebar-border: oklch(31% 0.004 55);
--sidebar-border: oklch(26% 0.004 70); --sidebar-ring: var(--primary);
--sidebar-ring: oklch(69.18% 0.18855 38.353);
/* Layout */
--radius: 0.5rem; --radius: 0.5rem;
/* Shadows - deeper for dark mode */
--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);
--shadow-sm: 0 1px 3px 0px oklch(0% 0 0 / 0.45), --shadow-sm: 0 1px 3px 0px oklch(0% 0 0 / 0.45),
@@ -254,7 +230,6 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-family: var(--font-america), sans-serif;
} }
*::selection { *::selection {
@@ -283,7 +258,6 @@
mix-blend-mode: normal; mix-blend-mode: normal;
} }
/* Dialog animations */
@keyframes dialog-in { @keyframes dialog-in {
from { from {
opacity: 0; opacity: 0;
@@ -332,7 +306,6 @@
animation: dialog-out 0.15s ease-in; animation: dialog-out 0.15s ease-in;
} }
/* Overdue blink: alternates two stacked labels with a smooth crossfade */
@keyframes blink-in { @keyframes blink-in {
0%, 40% { opacity: 1; } 0%, 40% { opacity: 1; }
50%, 90% { opacity: 0; } 50%, 90% { opacity: 0; }

View File

@@ -0,0 +1,27 @@
import type { PropsWithChildren } from "react";
import { Card, CardContent } from "@/shared/components/ui/card";
import { DotPattern } from "@/shared/components/ui/dot-pattern";
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">
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]">
<DotPattern
width={17}
height={17}
cx={1.3}
cy={1.3}
cr={1.3}
className="text-primary/8 mask-[linear-gradient(to_bottom,black,transparent_84%)]"
/>
<div className="absolute inset-0 bg-linear-to-br from-primary/6 via-transparent to-transparent" />
</div>
<CardContent className="relative z-10 grid gap-0 p-0 md:min-h-[640px] md:grid-cols-[1.05fr_0.95fr]">
<div className="flex bg-card/92 backdrop-blur-[1px]">{children}</div>
<AuthSidebar />
</CardContent>
</Card>
);
}

View File

@@ -2,14 +2,20 @@ import { cn } from "@/shared/utils/ui";
interface AuthHeaderProps { interface AuthHeaderProps {
title: string; title: string;
description?: string;
} }
export function AuthHeader({ title }: AuthHeaderProps) { export function AuthHeader({ title, description }: AuthHeaderProps) {
return ( return (
<div className={cn("flex flex-col gap-1.5")}> <div className={cn("flex flex-col gap-2")}>
<h1 className="text-xl font-semibold tracking-tight text-card-foreground"> <h1 className="text-2xl font-semibold tracking-tight text-card-foreground">
{title} {title}
</h1> </h1>
{description ? (
<p className="max-w-md text-sm leading-6 text-muted-foreground">
{description}
</p>
) : null}
</div> </div>
); );
} }

View File

@@ -1,12 +1,32 @@
import { Logo } from "@/shared/components/logo";
import { DotPattern } from "@/shared/components/ui/dot-pattern";
function AuthSidebar() { function AuthSidebar() {
return ( return (
<div className="relative hidden flex-col overflow-hidden bg-primary md:flex"> <div className="relative hidden flex-col overflow-hidden bg-primary md:flex">
<div className="relative flex flex-1 flex-col justify-between p-8"> <div className="pointer-events-none absolute inset-0">
<div className="space-y-4"> <DotPattern
<h2 className="text-3xl font-semibold leading-tight"> width={18}
height={18}
cx={1.15}
cy={1.15}
cr={1.15}
className="text-black/10 mask-[radial-gradient(circle_at_top_left,black,transparent_80%)]"
/>
<div className="absolute inset-0 bg-linear-to-br from-white/9 via-transparent to-black/7" />
</div>
<div className="relative flex flex-1 flex-col justify-between p-10 lg:p-12">
<Logo
variant="compact"
invertTextOnDark={false}
className="opacity-92 [&_img]:brightness-0 [&_img]:saturate-0"
/>
<div className="max-w-sm space-y-4.5">
<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="text-sm opacity-90"> <p className="max-w-[18rem] text-sm leading-6 text-black/68">
Centralize despesas, organize cartões e acompanhe metas mensais em Centralize despesas, organize cartões e acompanhe metas mensais em
um painel inteligente feito para o seu dia a dia. um painel inteligente feito para o seu dia a dia.
</p> </p>

View File

@@ -3,9 +3,7 @@ import { RiFingerprintLine, RiLoader4Line } from "@remixicon/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { type FormEvent, useEffect, useState } from "react"; import { type FormEvent, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Logo } from "@/shared/components/logo";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card";
import { import {
Field, Field,
FieldDescription, FieldDescription,
@@ -16,13 +14,16 @@ import {
import { Input } from "@/shared/components/ui/input"; import { Input } from "@/shared/components/ui/input";
import { authClient, googleSignInAvailable } from "@/shared/lib/auth/client"; import { authClient, googleSignInAvailable } from "@/shared/lib/auth/client";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
import { AuthCardShell } from "./auth-card-shell";
import { AuthErrorAlert } from "./auth-error-alert"; import { AuthErrorAlert } from "./auth-error-alert";
import { AuthHeader } from "./auth-header"; import { AuthHeader } from "./auth-header";
import AuthSidebar from "./auth-sidebar";
import { GoogleAuthButton } from "./google-auth-button"; import { GoogleAuthButton } from "./google-auth-button";
type DivProps = React.ComponentProps<"div">; type DivProps = React.ComponentProps<"div">;
const authLinkClassName =
"font-medium text-foreground/88 underline decoration-border underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground/30 focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40";
export function LoginForm({ className, ...props }: DivProps) { export function LoginForm({ className, ...props }: DivProps) {
const router = useRouter(); const router = useRouter();
const isGoogleAvailable = googleSignInAvailable; const isGoogleAvailable = googleSignInAvailable;
@@ -130,119 +131,117 @@ export function LoginForm({ className, ...props }: DivProps) {
} }
return ( return (
<div className={cn("flex flex-col gap-6", className)} {...props}> <div className={cn("flex flex-col gap-5", className)} {...props}>
<Logo className="mb-2" /> <AuthCardShell>
<Card className="overflow-hidden p-0"> <form
<CardContent className="grid gap-0 p-0 md:grid-cols-[1.05fr_0.95fr]"> className="flex w-full items-center px-6 py-7 md:px-10 md:py-9"
<form onSubmit={handleSubmit}
className="flex flex-col gap-6 p-6 md:p-8" noValidate
onSubmit={handleSubmit} >
noValidate <FieldGroup className="mx-auto w-full max-w-md gap-5">
> <AuthHeader
<FieldGroup className="gap-4"> title="Entrar no OpenMonetis"
<AuthHeader title="Entrar no OpenMonetis" /> description="Acesse sua conta para acompanhar cartões, lançamentos e metas em um só lugar."
/>
<AuthErrorAlert error={error} /> <AuthErrorAlert error={error} />
<Field> <Field>
<FieldLabel htmlFor="email">E-mail</FieldLabel> <FieldLabel htmlFor="email">E-mail</FieldLabel>
<Input <Input
id="email" id="email"
type="email" type="email"
placeholder="Digite seu e-mail" placeholder="Digite seu e-mail"
autoComplete="username webauthn" autoComplete="username webauthn"
required required
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
aria-invalid={!!error} aria-invalid={!!error}
/> />
</Field> </Field>
<Field> <Field>
<div className="flex items-center"> <div className="flex items-center">
<FieldLabel htmlFor="password">Senha</FieldLabel> <FieldLabel htmlFor="password">Senha</FieldLabel>
</div> </div>
<Input <Input
id="password" id="password"
type="password" type="password"
required required
placeholder="Digite sua senha" placeholder="Digite sua senha"
autoComplete="current-password" autoComplete="current-password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
aria-invalid={!!error} aria-invalid={!!error}
/> />
</Field> </Field>
<Field>
<Button
type="submit"
disabled={loadingEmail || loadingGoogle || loadingPasskey}
className="w-full"
>
{loadingEmail ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
) : (
"Entrar"
)}
</Button>
</Field>
<FieldSeparator className="my-1.5 *:data-[slot=field-separator-content]:bg-card">
Ou continue com
</FieldSeparator>
<Field>
<GoogleAuthButton
onClick={handleGoogle}
loading={loadingGoogle}
disabled={
loadingEmail ||
loadingGoogle ||
loadingPasskey ||
!isGoogleAvailable
}
text="Entrar com Google"
/>
</Field>
{passkeySupported && (
<Field> <Field>
<Button <Button
type="submit" variant="outline"
type="button"
onClick={handlePasskey}
disabled={loadingEmail || loadingGoogle || loadingPasskey} disabled={loadingEmail || loadingGoogle || loadingPasskey}
className="w-full" className="w-full gap-2"
> >
{loadingEmail ? ( {loadingPasskey ? (
<RiLoader4Line className="h-4 w-4 animate-spin" /> <RiLoader4Line className="h-4 w-4 animate-spin" />
) : ( ) : (
"Entrar" <RiFingerprintLine className="h-5 w-5" />
)} )}
<span>Entrar com passkey</span>
</Button> </Button>
</Field> </Field>
)}
<FieldSeparator className="my-2 *:data-[slot=field-separator-content]:bg-card"> <FieldDescription className="pt-1 text-center">
Ou continue com Não tem uma conta?{" "}
</FieldSeparator> <a href="/signup" className={authLinkClassName}>
Inscreva-se
</a>
</FieldDescription>
<Field> <FieldDescription className="text-center text-[13px] text-muted-foreground">
<GoogleAuthButton <a href="/" className={authLinkClassName}>
onClick={handleGoogle} Voltar para a página inicial
loading={loadingGoogle} </a>
disabled={ </FieldDescription>
loadingEmail || </FieldGroup>
loadingGoogle || </form>
loadingPasskey || </AuthCardShell>
!isGoogleAvailable
}
text="Entrar com Google"
/>
</Field>
{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>
)}
<FieldDescription className="text-center">
Não tem uma conta?{" "}
<a href="/signup" className="underline underline-offset-4">
Inscreva-se
</a>
</FieldDescription>
</FieldGroup>
</form>
<AuthSidebar />
</CardContent>
</Card>
<FieldDescription className="text-center">
<a href="/" className="underline underline-offset-4">
Voltar para o site
</a>
</FieldDescription>
</div> </div>
); );
} }

View File

@@ -3,9 +3,7 @@ import { RiCheckLine, RiCloseLine, RiLoader4Line } from "@remixicon/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { type FormEvent, useState } from "react"; import { type FormEvent, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Logo } from "@/shared/components/logo";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card";
import { import {
Field, Field,
FieldDescription, FieldDescription,
@@ -16,9 +14,9 @@ import {
import { Input } from "@/shared/components/ui/input"; import { Input } from "@/shared/components/ui/input";
import { authClient, googleSignInAvailable } from "@/shared/lib/auth/client"; import { authClient, googleSignInAvailable } from "@/shared/lib/auth/client";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
import { AuthCardShell } from "./auth-card-shell";
import { AuthErrorAlert } from "./auth-error-alert"; import { AuthErrorAlert } from "./auth-error-alert";
import { AuthHeader } from "./auth-header"; import { AuthHeader } from "./auth-header";
import AuthSidebar from "./auth-sidebar";
import { GoogleAuthButton } from "./google-auth-button"; import { GoogleAuthButton } from "./google-auth-button";
interface PasswordValidation { interface PasswordValidation {
@@ -76,6 +74,9 @@ function PasswordRequirement({ met, label }: { met: boolean; label: string }) {
type DivProps = React.ComponentProps<"div">; type DivProps = React.ComponentProps<"div">;
const authLinkClassName =
"font-medium text-foreground/88 underline decoration-border underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground/30 focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40";
export function SignupForm({ className, ...props }: DivProps) { export function SignupForm({ className, ...props }: DivProps) {
const router = useRouter(); const router = useRouter();
const isGoogleAvailable = googleSignInAvailable; const isGoogleAvailable = googleSignInAvailable;
@@ -149,143 +150,141 @@ export function SignupForm({ className, ...props }: DivProps) {
} }
return ( return (
<div className={cn("flex flex-col gap-6", className)} {...props}> <div className={cn("flex flex-col gap-5", className)} {...props}>
<Logo className="mb-2" /> <AuthCardShell>
<Card className="overflow-hidden p-0"> <form
<CardContent className="grid gap-0 p-0 md:grid-cols-[1.05fr_0.95fr]"> className="flex w-full items-center px-6 py-7 md:px-10 md:py-9"
<form onSubmit={handleSubmit}
className="flex flex-col gap-6 p-6 md:p-8" noValidate
onSubmit={handleSubmit} >
noValidate <FieldGroup className="mx-auto w-full max-w-md gap-5">
> <AuthHeader
<FieldGroup className="gap-4"> title="Criar sua conta"
<AuthHeader title="Criar sua conta" /> description="Comece com uma base organizada para acompanhar despesas, cartões e objetivos mensais."
/>
<AuthErrorAlert error={error} /> <AuthErrorAlert error={error} />
<Field> <Field>
<FieldLabel htmlFor="name">Nome completo</FieldLabel> <FieldLabel htmlFor="name">Nome completo</FieldLabel>
<Input <Input
id="name" id="name"
type="text" type="text"
placeholder="Digite seu nome" placeholder="Digite seu nome"
autoComplete="name" autoComplete="name"
required required
value={fullname} value={fullname}
onChange={(e) => setFullname(e.target.value)} onChange={(e) => setFullname(e.target.value)}
aria-invalid={!!error} aria-invalid={!!error}
/> />
</Field> </Field>
<Field> <Field>
<FieldLabel htmlFor="email">E-mail</FieldLabel> <FieldLabel htmlFor="email">E-mail</FieldLabel>
<Input <Input
id="email" id="email"
type="email" type="email"
placeholder="Digite seu e-mail" placeholder="Digite seu e-mail"
autoComplete="email" autoComplete="email"
required required
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
aria-invalid={!!error} aria-invalid={!!error}
/> />
</Field> </Field>
<Field> <Field>
<FieldLabel htmlFor="password">Senha</FieldLabel> <FieldLabel htmlFor="password">Senha</FieldLabel>
<Input <Input
id="password" id="password"
type="password" type="password"
required required
autoComplete="new-password" autoComplete="new-password"
placeholder="Crie uma senha forte" placeholder="Crie uma senha forte"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
aria-invalid={ aria-invalid={
!!error || !!error ||
(password.length > 0 && !passwordValidation.isValid) (password.length > 0 && !passwordValidation.isValid)
} }
maxLength={23} maxLength={23}
/> />
{password.length > 0 && ( {password.length > 0 && (
<div className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1"> <div className="mt-3 grid grid-cols-2 gap-x-4 gap-y-1.5 rounded-xl bg-muted/35 p-3">
<PasswordRequirement <PasswordRequirement
met={passwordValidation.hasMinLength} met={passwordValidation.hasMinLength}
label="Mínimo 7 caracteres" label="Mínimo 7 caracteres"
/> />
<PasswordRequirement <PasswordRequirement
met={passwordValidation.hasMaxLength} met={passwordValidation.hasMaxLength}
label="Máximo 23 caracteres" label="Máximo 23 caracteres"
/> />
<PasswordRequirement <PasswordRequirement
met={passwordValidation.hasLowercase} met={passwordValidation.hasLowercase}
label="Letra minúscula" label="Letra minúscula"
/> />
<PasswordRequirement <PasswordRequirement
met={passwordValidation.hasUppercase} met={passwordValidation.hasUppercase}
label="Letra maiúscula" label="Letra maiúscula"
/> />
<PasswordRequirement <PasswordRequirement
met={passwordValidation.hasNumber} met={passwordValidation.hasNumber}
label="Número" label="Número"
/> />
<PasswordRequirement <PasswordRequirement
met={passwordValidation.hasSpecial} met={passwordValidation.hasSpecial}
label="Caractere especial" label="Caractere especial"
/> />
</div> </div>
)}
</Field>
<Field>
<Button
type="submit"
disabled={
loadingEmail ||
loadingGoogle ||
(password.length > 0 && !passwordValidation.isValid)
}
className="w-full"
>
{loadingEmail ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
) : (
"Criar conta"
)} )}
</Field> </Button>
</Field>
<Field> <FieldSeparator className="my-1.5 *:data-[slot=field-separator-content]:bg-card">
<Button Ou continue com
type="submit" </FieldSeparator>
disabled={
loadingEmail ||
loadingGoogle ||
(password.length > 0 && !passwordValidation.isValid)
}
className="w-full"
>
{loadingEmail ? (
<RiLoader4Line className="h-4 w-4 animate-spin" />
) : (
"Criar conta"
)}
</Button>
</Field>
<FieldSeparator className="my-2 *:data-[slot=field-separator-content]:bg-card"> <Field>
Ou continue com <GoogleAuthButton
</FieldSeparator> onClick={handleGoogle}
loading={loadingGoogle}
disabled={loadingEmail || loadingGoogle || !isGoogleAvailable}
text="Continuar com Google"
/>
</Field>
<Field> <FieldDescription className="pt-1 text-center">
<GoogleAuthButton tem uma conta?{" "}
onClick={handleGoogle} <a href="/login" className={authLinkClassName}>
loading={loadingGoogle} Entrar
disabled={loadingEmail || loadingGoogle || !isGoogleAvailable} </a>
text="Continuar com Google" </FieldDescription>
/>
</Field>
<FieldDescription className="text-center"> <FieldDescription className="text-center text-[13px] text-muted-foreground">
tem uma conta?{" "} <a href="/" className={authLinkClassName}>
<a href="/login" className="underline underline-offset-4"> Voltar para a página inicial
Entrar </a>
</a> </FieldDescription>
</FieldDescription> </FieldGroup>
</FieldGroup> </form>
</form> </AuthCardShell>
<AuthSidebar />
</CardContent>
</Card>
<FieldDescription className="text-center">
<a href="/" className="underline underline-offset-4">
Voltar para o site
</a>
</FieldDescription>
</div> </div>
); );
} }

View File

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

View File

@@ -4,6 +4,7 @@ import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler
import { Logo } from "@/shared/components/logo"; import { Logo } from "@/shared/components/logo";
import { NotificationBell } from "@/shared/components/navigation/navbar/notification-bell"; import { NotificationBell } from "@/shared/components/navigation/navbar/notification-bell";
import { RefreshPageButton } from "@/shared/components/refresh-page-button"; import { RefreshPageButton } from "@/shared/components/refresh-page-button";
import { DotPattern } from "@/shared/components/ui/dot-pattern";
import { NavMenu } from "./nav-menu"; import { NavMenu } from "./nav-menu";
import { NavbarUser } from "./navbar-user"; import { NavbarUser } from "./navbar-user";
@@ -29,17 +30,26 @@ 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 bg-primary"> <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">
<div className="w-full max-w-8xl mx-auto px-4 flex items-center gap-4 h-full"> <div className="pointer-events-none absolute inset-0 overflow-hidden">
{/* Logo */} <DotPattern
width={20}
height={20}
cx={1.25}
cy={1.25}
cr={1.25}
className="text-black/10 mask-[linear-gradient(to_right,transparent,black_6%,black_60%,transparent)]"
/>
<div className="absolute inset-0 bg-linear-to-b from-white/8 via-transparent to-black/6" />
</div>
<div className="relative z-10 mx-auto flex h-full w-full max-w-8xl items-center gap-4 px-4">
<Link href="/dashboard" className="shrink-0 mr-1"> <Link href="/dashboard" className="shrink-0 mr-1">
<Logo variant="compact" invertTextOnDark={false} /> <Logo variant="compact" invertTextOnDark={false} />
</Link> </Link>
{/* Navigation */}
<NavMenu /> <NavMenu />
{/* Right-side actions */}
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
<NotificationBell <NotificationBell
notifications={notificationsSnapshot.notifications} notifications={notificationsSnapshot.notifications}
@@ -51,7 +61,6 @@ export function AppNavbar({
<AnimatedThemeToggler className={navbarActionClassName} /> <AnimatedThemeToggler className={navbarActionClassName} />
</div> </div>
{/* User avatar */}
<NavbarUser user={user} pagadorAvatarUrl={pagadorAvatarUrl} /> <NavbarUser user={user} pagadorAvatarUrl={pagadorAvatarUrl} />
</div> </div>
</header> </header>

View File

@@ -1,6 +1,6 @@
// Base para links diretos e triggers — pill arredondado // Base para links diretos e triggers — pill arredondado
export const linkBase = export const linkBase =
"inline-flex h-8 items-center justify-center rounded-full px-3 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 // 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";
@@ -11,8 +11,8 @@ export const linkActive = "bg-black/10 text-black";
// Trigger do NavigationMenu — espelha linkBase + linkIdle, remove estilos padrão // Trigger do NavigationMenu — espelha linkBase + linkIdle, remove estilos padrão
export const triggerClass = [ export const triggerClass = [
"h-8!", "h-8!",
"rounded-full!", "rounded-md!",
"px-3!", "px-2!",
"py-0!", "py-0!",
"text-sm!", "text-sm!",
"font-medium!", "font-medium!",

View File

@@ -0,0 +1,49 @@
import type { ComponentProps } from "react";
import { cn } from "@/shared/utils/ui";
type DotPatternProps = ComponentProps<"svg"> & {
width?: number;
height?: number;
x?: number;
y?: number;
cx?: number;
cy?: number;
cr?: number;
};
export function DotPattern({
className,
width = 18,
height = 18,
x = 0,
y = 0,
cx = 1.5,
cy = 1.5,
cr = 1.5,
...props
}: DotPatternProps) {
const patternId = `dot-pattern-${width}-${height}-${x}-${y}-${cx}-${cy}-${cr}`;
return (
<svg
aria-hidden
className={cn("absolute inset-0 h-full w-full", className)}
{...props}
>
<title>Dot pattern background</title>
<defs>
<pattern
id={patternId}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle cx={cx} cy={cy} r={cr} fill="currentColor" />
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${patternId})`} />
</svg>
);
}