style(auth): modernize login, setup, and onboarding screens

This commit is contained in:
Haileyesus
2026-06-30 11:45:14 +03:00
parent 7d5bd753d4
commit 1a7f0291b2
11 changed files with 163 additions and 91 deletions

View File

@@ -1,3 +1,5 @@
import { AlertCircle } from 'lucide-react';
type AuthErrorAlertProps = {
errorMessage: string;
};
@@ -8,8 +10,9 @@ export default function AuthErrorAlert({ errorMessage }: AuthErrorAlertProps) {
}
return (
<div className="rounded-md border border-red-300 bg-red-100 p-3 dark:border-red-800 dark:bg-red-900/20">
<p className="text-sm text-red-700 dark:text-red-400">{errorMessage}</p>
<div className="flex items-start gap-2.5 rounded-xl border border-destructive/30 bg-destructive/10 p-3 text-destructive">
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
<p className="text-sm leading-relaxed">{errorMessage}</p>
</div>
);
}

View File

@@ -1,3 +1,7 @@
import { useState } from 'react';
import type { ComponentType } from 'react';
import { Eye, EyeOff } from 'lucide-react';
type AuthInputFieldProps = {
id: string;
label: string;
@@ -8,13 +12,14 @@ type AuthInputFieldProps = {
type?: 'text' | 'password' | 'email';
name?: string;
autoComplete?: string;
icon?: ComponentType<{ className?: string }>;
};
/**
* A labelled input field for authentication forms.
* Renders a `<label>` / `<input>` pair and forwards browser autofill hints
* (`name`, `autoComplete`) so that password managers can identify and fill
* the field correctly.
* the field correctly. Password fields gain a show/hide visibility toggle.
*/
export default function AuthInputField({
id,
@@ -26,24 +31,49 @@ export default function AuthInputField({
type = 'text',
name,
autoComplete,
icon: Icon,
}: AuthInputFieldProps) {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const isPasswordField = type === 'password';
const resolvedType = isPasswordField && isPasswordVisible ? 'text' : type;
return (
<div>
<label htmlFor={id} className="mb-1 block text-sm font-medium text-foreground">
<label htmlFor={id} className="mb-1.5 block text-sm font-medium text-foreground">
{label}
</label>
<input
id={id}
type={type}
name={name ?? id}
autoComplete={autoComplete}
value={value}
onChange={(event) => onChange(event.target.value)}
className="w-full rounded-md border border-border bg-background px-3 py-2 text-foreground focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={placeholder}
required
disabled={isDisabled}
/>
<div className="group relative">
{Icon && (
<Icon className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground transition-colors group-focus-within:text-primary" />
)}
<input
id={id}
type={resolvedType}
name={name ?? id}
autoComplete={autoComplete}
value={value}
onChange={(event) => onChange(event.target.value)}
className={`w-full rounded-xl border border-border bg-background/60 py-2.5 text-foreground shadow-sm transition-colors placeholder:text-muted-foreground/60 hover:border-foreground/20 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-60 ${
Icon ? 'pl-10' : 'pl-3.5'
} ${isPasswordField ? 'pr-11' : 'pr-3.5'}`}
placeholder={placeholder}
required
disabled={isDisabled}
/>
{isPasswordField && (
<button
type="button"
onClick={() => setIsPasswordVisible((previous) => !previous)}
disabled={isDisabled}
tabIndex={-1}
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
className="absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-60"
>
{isPasswordVisible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
)}
</div>
</div>
);
}

View File

@@ -1,30 +1,30 @@
import { MessageSquare } from 'lucide-react';
const loadingDotAnimationDelays = ['0s', '0.1s', '0.2s'];
const loadingDotAnimationDelays = ['0s', '0.15s', '0.3s'];
export default function AuthLoadingScreen() {
return (
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="text-center">
<div className="mb-4 flex justify-center">
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-primary shadow-sm">
<MessageSquare className="h-8 w-8 text-primary-foreground" />
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-4">
<div aria-hidden className="pointer-events-none absolute inset-0">
<div className="absolute -top-40 left-1/2 h-[36rem] w-[36rem] -translate-x-1/2 rounded-full bg-primary/10 blur-3xl" />
</div>
<div className="relative text-center">
<div className="mb-5 flex justify-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-primary/80 shadow-lg shadow-primary/25 ring-1 ring-inset ring-white/20">
<img src="/logo.svg" alt="CloudCLI" className="h-9 w-9" />
</div>
</div>
<h1 className="mb-2 text-2xl font-bold text-foreground">CloudCLI</h1>
<h1 className="mb-4 font-serif text-2xl font-bold tracking-tight text-foreground">CloudCLI</h1>
<div className="flex items-center justify-center space-x-2">
<div className="flex items-center justify-center gap-2">
{loadingDotAnimationDelays.map((delay) => (
<div
key={delay}
className="h-2 w-2 animate-bounce rounded-full bg-blue-500"
className="h-2 w-2 animate-bounce rounded-full bg-primary"
style={{ animationDelay: delay }}
/>
))}
</div>
<p className="mt-2 text-muted-foreground">Loading...</p>
</div>
</div>
);

View File

@@ -1,5 +1,4 @@
import type { ReactNode } from 'react';
import { MessageSquare } from 'lucide-react';
import { IS_PLATFORM } from '../../../constants/config';
type AuthScreenLayoutProps = {
@@ -18,29 +17,38 @@ export default function AuthScreenLayout({
logo,
}: AuthScreenLayoutProps) {
return (
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-md">
<div className="space-y-6 rounded-lg border border-border bg-card p-8 shadow-lg">
<div className="relative h-screen overflow-y-auto bg-background">
{/* Ambient, on-brand backdrop that gives the screen depth without
competing with the card content. Fixed so it stays put while the
form scrolls on short viewports. */}
<div aria-hidden className="pointer-events-none fixed inset-0">
<div className="absolute -top-40 left-1/2 h-[36rem] w-[36rem] -translate-x-1/2 rounded-full bg-primary/10 blur-3xl" />
<div className="absolute -bottom-32 -left-24 h-[26rem] w-[26rem] rounded-full bg-primary/5 blur-3xl" />
<div className="absolute inset-0 bg-[radial-gradient(hsl(var(--foreground)/0.04)_1px,transparent_1px)] [background-size:22px_22px] opacity-60" />
</div>
<div className="relative mx-auto flex min-h-full w-full max-w-md items-center justify-center p-4 py-8">
<div className="w-full rounded-2xl border border-border/70 bg-card/90 p-8 shadow-[0_24px_60px_-20px_hsl(var(--foreground)/0.18)] ring-1 ring-foreground/5 backdrop-blur-xl sm:p-10">
<div className="text-center">
<div className="mb-4 flex justify-center">
<div className="mb-5 flex justify-center">
{logo ?? (
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-primary shadow-sm">
<MessageSquare className="h-8 w-8 text-primary-foreground" />
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-primary/80 shadow-lg shadow-primary/25 ring-1 ring-inset ring-white/20">
<img src="/logo.svg" alt="CloudCLI" className="h-9 w-9" />
</div>
)}
</div>
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
<p className="mt-2 text-muted-foreground">{description}</p>
<h1 className="font-serif text-3xl font-bold tracking-tight text-foreground">{title}</h1>
<p className="mx-auto mt-2 max-w-xs text-sm leading-relaxed text-muted-foreground">{description}</p>
</div>
{children}
<div className="mt-8">{children}</div>
<div className="text-center">
<p className="text-sm text-muted-foreground">{footerText}</p>
<div className="mt-6 border-t border-border/60 pt-5 text-center">
<p className="text-xs leading-relaxed text-muted-foreground">{footerText}</p>
</div>
{!IS_PLATFORM && (
<div className="flex items-center justify-center gap-1.5 pt-2">
<div className="mt-4 flex items-center justify-center gap-1.5">
<svg className="h-3.5 w-3.5 text-muted-foreground/50" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
</svg>

View File

@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import type { FormEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { Loader2, Lock, User } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import AuthErrorAlert from './AuthErrorAlert';
import AuthInputField from './AuthInputField';
@@ -69,6 +70,7 @@ export default function LoginForm() {
placeholder={t('login.placeholders.username')}
isDisabled={isSubmitting}
autoComplete="username"
icon={User}
/>
<AuthInputField
@@ -80,6 +82,7 @@ export default function LoginForm() {
isDisabled={isSubmitting}
type="password"
autoComplete="current-password"
icon={Lock}
/>
<AuthErrorAlert errorMessage={errorMessage} />
@@ -87,9 +90,16 @@ export default function LoginForm() {
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition-colors duration-200 hover:bg-blue-700 disabled:bg-blue-400"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-primary px-4 py-2.5 font-medium text-primary-foreground shadow-lg shadow-primary/25 transition-all duration-200 hover:brightness-110 hover:shadow-primary/30 focus:outline-none focus:ring-2 focus:ring-primary/40 focus:ring-offset-2 focus:ring-offset-card active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60"
>
{isSubmitting ? t('login.loading') : t('login.submit')}
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{t('login.loading')}
</>
) : (
t('login.submit')
)}
</button>
</form>
</AuthScreenLayout>

View File

@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import type { FormEvent } from 'react';
import { Loader2, Lock, ShieldCheck, User } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import AuthErrorAlert from './AuthErrorAlert';
import AuthInputField from './AuthInputField';
@@ -85,7 +86,6 @@ export default function SetupForm() {
title="Welcome to CloudCLI"
description="Set up your account to get started"
footerText="This is a single-user system. Only one account can be created."
logo={<img src="/logo.svg" alt="CloudCLI" className="h-16 w-16" />}
>
<form onSubmit={handleSubmit} className="space-y-4">
<AuthInputField
@@ -94,9 +94,10 @@ export default function SetupForm() {
label="Username"
value={formState.username}
onChange={(value) => updateField('username', value)}
placeholder="Enter your username"
placeholder="Choose a username"
isDisabled={isSubmitting}
autoComplete="username"
icon={User}
/>
<AuthInputField
@@ -105,10 +106,11 @@ export default function SetupForm() {
label="Password"
value={formState.password}
onChange={(value) => updateField('password', value)}
placeholder="Enter your password"
placeholder="Create a password"
isDisabled={isSubmitting}
type="password"
autoComplete="new-password"
icon={Lock}
/>
<AuthInputField
@@ -117,20 +119,33 @@ export default function SetupForm() {
label="Confirm Password"
value={formState.confirmPassword}
onChange={(value) => updateField('confirmPassword', value)}
placeholder="Confirm your password"
placeholder="Re-enter your password"
isDisabled={isSubmitting}
type="password"
autoComplete="new-password"
icon={ShieldCheck}
/>
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<ShieldCheck className="h-3.5 w-3.5" />
At least 3 characters for username, 6 for password.
</p>
<AuthErrorAlert errorMessage={errorMessage} />
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition-colors duration-200 hover:bg-blue-700 disabled:bg-blue-400"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-primary px-4 py-2.5 font-medium text-primary-foreground shadow-lg shadow-primary/25 transition-all duration-200 hover:brightness-110 hover:shadow-primary/30 focus:outline-none focus:ring-2 focus:ring-primary/40 focus:ring-offset-2 focus:ring-offset-card active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60"
>
{isSubmitting ? 'Setting up...' : 'Create Account'}
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Setting up...
</>
) : (
'Create Account'
)}
</button>
</form>
</AuthScreenLayout>