mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
Refina tema global e experiencia visual de auth
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
27
src/features/auth/components/auth-card-shell.tsx
Normal file
27
src/features/auth/components/auth-card-shell.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
Já 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">
|
||||||
Já 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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!",
|
||||||
|
|||||||
49
src/shared/components/ui/dot-pattern.tsx
Normal file
49
src/shared/components/ui/dot-pattern.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user