diff --git a/app/globals.css b/app/globals.css index e6bb464..47ade7b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -7,115 +7,165 @@ } :root { - --background: oklch(95.657% 0.00898 78.134); - --foreground: oklch(0.1448 0 0); - --card: oklch(98.531% 0.00274 84.298); - --card-foreground: oklch(0.1448 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.1448 0 0); - --primary: oklch(63.198% 0.16941 37.263); - --primary-foreground: oklch(0.9851 0 0); - --secondary: oklch(0.9702 0 0); - --secondary-foreground: oklch(0.2046 0 0); - --muted: var(--background); - --muted-foreground: oklch(0.5555 0 0); - --accent: oklch(0.9702 0 0); - --accent-foreground: oklch(0.2046 0 0); - --destructive: oklch(0.583 0.2387 28.4765); - --destructive-foreground: oklch(1 0 0); - --border: oklch(89.814% 0.00805 114.524); - --input: oklch(70.84% 0.00279 106.916); - --ring: oklch(76.109% 0.15119 44.68); - --chart-1: oklch(70.734% 0.16977 153.383); - --chart-2: oklch(62.464% 0.20395 25.32); - --chart-3: oklch(58.831% 0.22222 298.916); - --chart-4: oklch(0.4893 0.2202 264.0405); - --chart-5: oklch(0.421 0.1792 266.0094); - --sidebar: oklch(91.118% 0.01317 82.34); - --sidebar-foreground: oklch(0.1448 0 0); - --sidebar-primary: oklch(0.2046 0 0); - --sidebar-primary-foreground: oklch(0.9851 0 0); - --sidebar-accent: oklch(93.199% 0.00336 67.072); - --sidebar-accent-foreground: oklch(0.2046 0 0); - --sidebar-border: var(--primary); - --sidebar-ring: oklch(0.709 0 0); + /* Base surfaces - warm cream with subtle orange undertone */ + --background: oklch(97.512% 0.00674 67.377); + --foreground: oklch(18% 0.02 45); + --card: oklch(99% 0.006 80); + --card-foreground: oklch(18% 0.02 45); + --popover: oklch(99.5% 0.004 80); + --popover-foreground: oklch(18% 0.02 45); + + /* Primary - rich terracotta orange */ + --primary: oklch(69.18% 0.18855 38.353); + --primary-foreground: oklch(98% 0.008 80); + + /* Secondary - warm stone with subtle saturation */ + --secondary: oklch(94% 0.018 70); + --secondary-foreground: oklch(25% 0.025 45); + + /* Muted - softer background variant */ + --muted: oklch(94.5% 0.014 75); + --muted-foreground: oklch(45% 0.015 60); + + /* Accent - complementary warm tone */ + --accent: oklch(93.996% 0.01787 64.782); + --accent-foreground: oklch(22% 0.025 45); + + /* Destructive - accessible red */ + --destructive: oklch(55% 0.22 27); + --destructive-foreground: oklch(98% 0.005 30); + + /* Borders and inputs - defined but subtle */ + --border: oklch(88% 0.015 80); + --input: oklch(82% 0.012 75); + --ring: oklch(69.18% 0.18855 38.353); + + /* Charts - harmonious, distinct, accessible */ + --chart-1: oklch(65% 0.18 160); + --chart-2: oklch(60% 0.2 28); + --chart-3: oklch(58% 0.19 295); + --chart-4: oklch(55% 0.2 260); + --chart-5: oklch(68% 0.16 85); + + /* Sidebar - slight elevation from background */ + --sidebar: oklch(94.637% 0.00925 62.27); + --sidebar-foreground: oklch(20% 0.02 45); + --sidebar-primary: oklch(25% 0.025 45); + --sidebar-primary-foreground: oklch(98% 0.008 80); + --sidebar-accent: oklch(88.94% 0.02161 65.18); + --sidebar-accent-foreground: oklch(22% 0.025 45); + --sidebar-border: oklch(58.814% 0.15852 38.26); + --sidebar-ring: oklch(69.18% 0.18855 38.353); + + /* Layout */ --radius: 0.8rem; - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), - 0 1px 2px -1px hsl(0 0% 0% / 0.1); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), - 0 2px 4px -1px hsl(0 0% 0% / 0.1); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), - 0 4px 6px -1px hsl(0 0% 0% / 0.1); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), - 0 8px 10px -1px hsl(0 0% 0% / 0.1); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + + /* Shadows - warm tinted for cohesion */ + --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-sm: 0 1px 3px 0px oklch(35% 0.02 45 / 0.08), + 0 1px 2px -1px oklch(35% 0.02 45 / 0.08); + --shadow: 0 2px 4px 0px oklch(35% 0.02 45 / 0.08), + 0 1px 2px -1px oklch(35% 0.02 45 / 0.06); + --shadow-md: 0 4px 6px -1px oklch(35% 0.02 45 / 0.1), + 0 2px 4px -2px oklch(35% 0.02 45 / 0.08); + --shadow-lg: 0 10px 15px -3px oklch(35% 0.02 45 / 0.1), + 0 4px 6px -4px oklch(35% 0.02 45 / 0.08); + --shadow-xl: 0 20px 25px -5px oklch(35% 0.02 45 / 0.1), + 0 8px 10px -6px oklch(35% 0.02 45 / 0.08); + --shadow-2xl: 0 25px 50px -12px oklch(35% 0.02 45 / 0.2); + --tracking-normal: 0em; --spacing: 0.25rem; - --month-picker: oklch(89.296% 0.0234 143.556); - --month-picker-foreground: oklch(28% 0.035 143.556); - --dark: oklch(27.171% 0.00927 294.877); - --dark-foreground: oklch(91.3% 0.00281 84.324); + + /* Special components */ + --month-picker: oklch(92.929% 0.01274 63.703); + --month-picker-foreground: oklch(22% 0.015 45); + --dark: oklch(22% 0.015 45); + --dark-foreground: oklch(94% 0.008 80); --welcome-banner: var(--primary); - --welcome-banner-foreground: oklch(98% 0.005 35.01); + --welcome-banner-foreground: oklch(98% 0.008 80); } .dark { - --background: oklch(18.5% 0.008 67.284); - --foreground: oklch(96.5% 0.002 67.284); - --card: oklch(22.8% 0.009 67.284); - --card-foreground: oklch(96.5% 0.002 67.284); - --popover: oklch(24.5% 0.01 67.284); - --popover-foreground: oklch(96.5% 0.002 67.284); - --primary: oklch(63.198% 0.16941 37.263); - --primary-foreground: oklch(98% 0.001 67.284); - --secondary: oklch(26.5% 0.008 67.284); - --secondary-foreground: oklch(96.5% 0.002 67.284); - --muted: oklch(25.2% 0.008 67.284); - --muted-foreground: oklch(68% 0.004 67.284); - --accent: oklch(30.5% 0.012 67.284); - --accent-foreground: oklch(96.5% 0.002 67.284); - --destructive: oklch(62.5% 0.218 28.4765); - --destructive-foreground: oklch(98% 0.001 67.284); - --border: oklch(32.5% 0.01 114.524); - --input: oklch(38.5% 0.012 106.916); - --ring: oklch(68% 0.135 35.01); - --chart-1: oklch(70.734% 0.16977 153.383); - --chart-2: oklch(62.464% 0.20395 25.32); - --chart-3: oklch(63.656% 0.19467 301.166); - --chart-4: oklch(60% 0.19 264.0405); - --chart-5: oklch(56% 0.16 266.0094); - --sidebar: oklch(20.2% 0.009 67.484); - --sidebar-foreground: oklch(96.5% 0.002 67.284); - --sidebar-primary: oklch(65.5% 0.148 35.01); - --sidebar-primary-foreground: oklch(98% 0.001 67.284); - --sidebar-accent: oklch(28.5% 0.011 67.072); - --sidebar-accent-foreground: oklch(96.5% 0.002 67.284); - --sidebar-border: oklch(30% 0.01 67.484); - --sidebar-ring: oklch(68% 0.135 35.01); + /* Base surfaces - true dark with minimal saturation */ + --background: oklch(14% 0.004 285); + --foreground: oklch(95% 0.003 285); + --card: oklch(18% 0.005 285); + --card-foreground: oklch(95% 0.003 285); + --popover: oklch(20% 0.006 285); + --popover-foreground: oklch(95% 0.003 285); + + /* Primary - vibrant terracotta stands out on dark */ + --primary: oklch(69.18% 0.18855 38.353); + --primary-foreground: oklch(12% 0.008 285); + + /* Secondary - elevated surface */ + --secondary: oklch(22% 0.004 285); + --secondary-foreground: oklch(93% 0.003 285); + + /* Muted - subtle surface variant */ + --muted: oklch(20% 0.004 285); + --muted-foreground: oklch(60% 0.003 285); + + /* Accent - subtle highlight */ + --accent: oklch(26% 0.006 285); + --accent-foreground: oklch(95% 0.003 285); + + /* Destructive - accessible red for dark */ + --destructive: oklch(62% 0.2 28); + --destructive-foreground: oklch(98% 0.005 30); + + /* Borders and inputs - visible but subtle */ + --border: oklch(28% 0.004 285); + --input: oklch(32% 0.005 285); + --ring: oklch(69.18% 0.18855 38.353); + + /* Charts - bright and distinct on dark */ + --chart-1: oklch(72% 0.17 158); + --chart-2: oklch(68% 0.19 30); + --chart-3: oklch(68% 0.18 298); + --chart-4: oklch(65% 0.18 262); + --chart-5: oklch(74% 0.15 88); + + /* Sidebar - slight separation from main */ + --sidebar: oklch(16% 0.004 285); + --sidebar-foreground: oklch(95% 0.003 285); + --sidebar-primary: oklch(69.18% 0.18855 38.353); + --sidebar-primary-foreground: oklch(12% 0.008 285); + --sidebar-accent: oklch(24% 0.005 285); + --sidebar-accent-foreground: oklch(95% 0.003 285); + --sidebar-border: oklch(26% 0.004 285); + --sidebar-ring: oklch(69.18% 0.18855 38.353); + + /* Layout */ --radius: 0.8rem; - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.15); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.2); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.25), - 0 1px 2px -1px hsl(0 0% 0% / 0.25); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.3), 0 1px 2px -1px hsl(0 0% 0% / 0.3); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.35), - 0 2px 4px -1px hsl(0 0% 0% / 0.35); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.4), - 0 4px 6px -1px hsl(0 0% 0% / 0.4); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.45), - 0 8px 10px -1px hsl(0 0% 0% / 0.45); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.5); + + /* Shadows - deeper for dark mode */ + --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-sm: 0 1px 3px 0px oklch(0% 0 0 / 0.45), + 0 1px 2px -1px oklch(0% 0 0 / 0.45); + --shadow: 0 2px 4px 0px oklch(0% 0 0 / 0.5), + 0 1px 2px -1px oklch(0% 0 0 / 0.4); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.55), + 0 2px 4px -2px oklch(0% 0 0 / 0.45); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.55), + 0 4px 6px -4px oklch(0% 0 0 / 0.45); + --shadow-xl: 0 20px 25px -5px oklch(0% 0 0 / 0.6), + 0 8px 10px -6px oklch(0% 0 0 / 0.5); + --shadow-2xl: 0 25px 50px -12px oklch(0% 0 0 / 0.7); + --tracking-normal: 0em; --spacing: 0.25rem; - --month-picker: var(--card); - --month-picker-foreground: var(--foreground); - --dark: oklch(91.3% 0.00281 84.324); - --dark-foreground: oklch(23.649% 0.00484 67.469); - --welcome-banner: var(--card); - --welcome-banner-foreground: --dark; + + /* Special components */ + --month-picker: oklch(22% 0.006 285); + --month-picker-foreground: oklch(85% 0.02 315); + --dark: oklch(93% 0.003 285); + --dark-foreground: oklch(18% 0.005 285); + --welcome-banner: oklch(22% 0.006 285); + --welcome-banner-foreground: oklch(95% 0.003 285); } @theme inline { @@ -188,11 +238,11 @@ } *::selection { - @apply bg-violet-400 text-foreground; + @apply bg-primary/25 text-foreground; } .dark *::selection { - @apply bg-orange-700 text-foreground; + @apply bg-primary/30 text-foreground; } button:not(:disabled), diff --git a/components/ajustes/update-password-form.tsx b/components/ajustes/update-password-form.tsx index 6103a68..5bb3824 100644 --- a/components/ajustes/update-password-form.tsx +++ b/components/ajustes/update-password-form.tsx @@ -4,10 +4,70 @@ import { updatePasswordAction } from "@/app/(dashboard)/ajustes/actions"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils/ui"; import { RiEyeLine, RiEyeOffLine, RiCheckLine, RiCloseLine, RiAlertLine } from "@remixicon/react"; import { useState, useTransition, useMemo } from "react"; import { toast } from "sonner"; +interface PasswordValidation { + hasLowercase: boolean; + hasUppercase: boolean; + hasNumber: boolean; + hasSpecial: boolean; + hasMinLength: boolean; + hasMaxLength: boolean; + isValid: boolean; +} + +function validatePassword(password: string): PasswordValidation { + const hasLowercase = /[a-z]/.test(password); + const hasUppercase = /[A-Z]/.test(password); + const hasNumber = /\d/.test(password); + const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]/.test(password); + const hasMinLength = password.length >= 7; + const hasMaxLength = password.length <= 23; + + return { + hasLowercase, + hasUppercase, + hasNumber, + hasSpecial, + hasMinLength, + hasMaxLength, + isValid: + hasLowercase && + hasUppercase && + hasNumber && + hasSpecial && + hasMinLength && + hasMaxLength, + }; +} + +function PasswordRequirement({ + met, + label, +}: { + met: boolean; + label: string; +}) { + return ( +
+ {met ? ( + + ) : ( + + )} + {label} +
+ ); +} + type UpdatePasswordFormProps = { authProvider?: string; // 'google' | 'credential' | undefined }; @@ -30,30 +90,23 @@ export function UpdatePasswordForm({ authProvider }: UpdatePasswordFormProps) { return newPassword === confirmPassword; }, [newPassword, confirmPassword]); - // Indicador de força da senha (básico) - const passwordStrength = useMemo(() => { - if (!newPassword) return null; - if (newPassword.length < 6) return "weak"; - if (newPassword.length >= 12 && /[A-Z]/.test(newPassword) && /[0-9]/.test(newPassword) && /[^A-Za-z0-9]/.test(newPassword)) { - return "strong"; - } - if (newPassword.length >= 8 && (/[A-Z]/.test(newPassword) || /[0-9]/.test(newPassword))) { - return "medium"; - } - return "weak"; - }, [newPassword]); + // Validação de requisitos da senha + const passwordValidation = useMemo( + () => validatePassword(newPassword), + [newPassword] + ); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Validação frontend antes de enviar - if (newPassword !== confirmPassword) { - toast.error("As senhas não coincidem"); + if (!passwordValidation.isValid) { + toast.error("A senha não atende aos requisitos de segurança"); return; } - if (newPassword.length < 6) { - toast.error("A senha deve ter no mínimo 6 caracteres"); + if (newPassword !== confirmPassword) { + toast.error("As senhas não coincidem"); return; } @@ -145,12 +198,13 @@ export function UpdatePasswordForm({ authProvider }: UpdatePasswordFormProps) { value={newPassword} onChange={(e) => setNewPassword(e.target.value)} disabled={isPending} - placeholder="Mínimo de 6 caracteres" + placeholder="Crie uma senha forte" required - minLength={6} + minLength={7} + maxLength={23} aria-required="true" aria-describedby="new-password-help" - aria-invalid={newPassword.length > 0 && newPassword.length < 6} + aria-invalid={newPassword.length > 0 && !passwordValidation.isValid} /> diff --git a/components/auth/auth-header.tsx b/components/auth/auth-header.tsx index 4a8addb..6d846be 100644 --- a/components/auth/auth-header.tsx +++ b/components/auth/auth-header.tsx @@ -2,16 +2,14 @@ import { cn } from "@/lib/utils/ui"; interface AuthHeaderProps { title: string; - description: string; } -export function AuthHeader({ title, description }: AuthHeaderProps) { +export function AuthHeader({ title }: AuthHeaderProps) { return (

{title}

-

{description}

); } diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index f543fed..5b8811d 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -99,10 +99,7 @@ export function LoginForm({ className, ...props }: DivProps) { noValidate > - + diff --git a/components/auth/signup-form.tsx b/components/auth/signup-form.tsx index 898cb3e..62145ed 100644 --- a/components/auth/signup-form.tsx +++ b/components/auth/signup-form.tsx @@ -13,13 +13,73 @@ import { authClient, googleSignInAvailable } from "@/lib/auth/client"; import { cn } from "@/lib/utils/ui"; import { RiLoader4Line } from "@remixicon/react"; import { useRouter } from "next/navigation"; -import { useState, type FormEvent } from "react"; +import { useState, useMemo, type FormEvent } from "react"; import { toast } from "sonner"; import { Logo } from "../logo"; import { AuthErrorAlert } from "./auth-error-alert"; import { AuthHeader } from "./auth-header"; import AuthSidebar from "./auth-sidebar"; import { GoogleAuthButton } from "./google-auth-button"; +import { RiCheckLine, RiCloseLine } from "@remixicon/react"; + +interface PasswordValidation { + hasLowercase: boolean; + hasUppercase: boolean; + hasNumber: boolean; + hasSpecial: boolean; + hasMinLength: boolean; + hasMaxLength: boolean; + isValid: boolean; +} + +function validatePassword(password: string): PasswordValidation { + const hasLowercase = /[a-z]/.test(password); + const hasUppercase = /[A-Z]/.test(password); + const hasNumber = /\d/.test(password); + const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]/.test(password); + const hasMinLength = password.length >= 7; + const hasMaxLength = password.length <= 23; + + return { + hasLowercase, + hasUppercase, + hasNumber, + hasSpecial, + hasMinLength, + hasMaxLength, + isValid: + hasLowercase && + hasUppercase && + hasNumber && + hasSpecial && + hasMinLength && + hasMaxLength, + }; +} + +function PasswordRequirement({ + met, + label, +}: { + met: boolean; + label: string; +}) { + return ( +
+ {met ? ( + + ) : ( + + )} + {label} +
+ ); +} type DivProps = React.ComponentProps<"div">; @@ -35,9 +95,19 @@ export function SignupForm({ className, ...props }: DivProps) { const [loadingEmail, setLoadingEmail] = useState(false); const [loadingGoogle, setLoadingGoogle] = useState(false); + const passwordValidation = useMemo( + () => validatePassword(password), + [password] + ); + async function handleSubmit(e: FormEvent) { e.preventDefault(); + if (!passwordValidation.isValid) { + setError("A senha não atende aos requisitos de segurança."); + return; + } + await authClient.signUp.email( { email, @@ -99,10 +169,7 @@ export function SignupForm({ className, ...props }: DivProps) { noValidate > - + @@ -144,14 +211,43 @@ export function SignupForm({ className, ...props }: DivProps) { placeholder="Crie uma senha forte" value={password} onChange={(e) => setPassword(e.target.value)} - aria-invalid={!!error} + aria-invalid={!!error || (password.length > 0 && !passwordValidation.isValid)} + maxLength={23} /> + {password.length > 0 && ( +
+ + + + + + +
+ )}