Compare commits

..

3 Commits

Author SHA1 Message Date
d5d8f2b6d3 auth changes 2025-12-24 11:12:21 +05:30
8932c3d502 added knowledge base 2025-12-24 11:05:39 +05:30
ab3bf2038b feat: improve responsiveness and refactor auth components
- Fix responsiveness at md (768px) and lg (1024px) breakpoints across all pages
- Reduce forgot-password-modal.tsx from 472 to 319 lines by extracting components
- Extract OTPInput and modal icons into separate files for better code organization
- Optimize card sizes and text scaling for lg (1024px) breakpoint
- Adjust login page layout: smaller card and reduced text sizes at lg breakpoint
- Improve responsive typography, spacing, and touch targets across all components
- Update all pages (login, sign-up, dashboard) with proper mobile-first responsive design
2025-12-24 11:02:55 +05:30
14 changed files with 515 additions and 296 deletions

View File

Before

Width:  |  Height:  |  Size: 536 B

After

Width:  |  Height:  |  Size: 536 B

View File

@ -100,13 +100,13 @@ export function AuthButton({
disabled={disabled || isLoading}
className={`
flex items-center justify-center
h-[52px]
px-[18px] py-[13px]
min-h-[44px] h-11 md:h-[52px]
px-4 md:px-[18px] py-2.5 md:py-[13px]
bg-[#00E2E0]
border border-[rgba(255,255,255,0.2)]
rounded-[12px]
rounded-lg md:rounded-[12px]
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
text-[16px] font-semibold leading-6 text-black
text-sm md:text-base md:text-[16px] font-semibold leading-5 md:leading-6 text-black
cursor-pointer
hover:bg-[#00D4D2]
active:bg-[#00C6C4]
@ -165,16 +165,17 @@ export function SocialButton({
<button
type="button"
className={`
flex items-center justify-center gap-3
px-4 py-2.5
flex items-center justify-center gap-2 md:gap-3
px-3 md:px-4 py-2 md:py-2.5
bg-white
rounded-xl
rounded-lg md:rounded-xl
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
text-sm font-semibold leading-6 text-black
text-xs md:text-sm font-semibold leading-5 md:leading-6 text-black
cursor-pointer
hover:bg-gray-50
active:bg-gray-100
transition-colors duration-200
min-h-[44px]
${className}
`}
{...buttonProps}

View File

@ -53,11 +53,11 @@ export function AuthCard({ children, className = '', variant = 'signin' }: AuthC
return (
<div
className={`
flex flex-col items-center gap-8
w-[520px] max-w-full
rounded-[32px]
flex flex-col items-center gap-5 md:gap-6 lg:gap-6 xl:gap-8
w-full max-w-[520px]
rounded-2xl md:rounded-3xl lg:rounded-2xl xl:rounded-[32px]
shadow-[0px_4px_24px_0px_rgba(0,0,0,0.15)]
${isRegister ? 'px-[42px] py-[64px]' : 'px-[60px] py-[80px]'}
${isRegister ? 'px-6 py-8 md:px-8 md:py-12 lg:px-8 lg:py-10 xl:px-[42px] xl:py-[64px]' : 'px-6 py-8 md:px-10 md:py-12 lg:px-10 lg:py-12 xl:px-[60px] xl:py-[80px]'}
${className}
`}
style={{
@ -79,11 +79,11 @@ export function AuthCardHeader({ title, subtitle }: AuthCardHeaderProps): JSX.El
return (
<div className="flex flex-col items-center w-full">
<div className="flex flex-col items-center gap-2 w-full text-center">
<h2 className="text-[30px] font-semibold leading-[38px] text-white w-full">
<h2 className="text-2xl md:text-2xl lg:text-2xl xl:text-3xl 2xl:text-[30px] font-semibold leading-tight md:leading-tight lg:leading-tight xl:leading-[38px] text-white w-full">
{title}
</h2>
{subtitle && (
<p className="text-[18px] font-medium leading-6 text-[rgba(255,255,255,0.75)]">
<p className="text-sm md:text-base lg:text-sm xl:text-lg 2xl:text-[18px] font-medium leading-5 md:leading-5 lg:leading-5 xl:leading-6 text-[rgba(255,255,255,0.75)]">
{subtitle}
</p>
)}

View File

@ -19,6 +19,26 @@ import {
AzureIcon,
} from './index';
/** Animation timing constants (in milliseconds) */
const ANIMATION_TIMING = {
/** Duration for simulating form submission */
FORM_SUBMIT_DELAY: 1000,
/** Duration for name fields expand/collapse */
NAME_FIELDS_DURATION: 0.2,
/** Duration for forgot password link animation */
FORGOT_PASSWORD_DURATION: 0.15,
/** Opacity fade delay */
OPACITY_DELAY: 0.05,
/** Opacity fade duration */
OPACITY_DURATION: 0.15,
/** Margin animation bottom value */
NAME_FIELDS_MARGIN: 16,
/** Margin animation top value */
FORGOT_PASSWORD_MARGIN: 8,
/** Layout animation duration */
LAYOUT_DURATION: 0.3,
} as const;
/**
* Auth form mode type
*/
@ -72,19 +92,19 @@ const nameFieldsVariants = {
opacity: 0,
marginBottom: 0,
transition: {
height: { ...springTransition, duration: 0.2 },
height: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
opacity: { duration: 0.1 },
marginBottom: { ...springTransition, duration: 0.2 },
marginBottom: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
},
},
visible: {
height: 'auto',
opacity: 1,
marginBottom: 16,
marginBottom: ANIMATION_TIMING.NAME_FIELDS_MARGIN,
transition: {
height: { ...springTransition, duration: 0.2 },
opacity: { duration: 0.15, delay: 0.05 },
marginBottom: { ...springTransition, duration: 0.2 },
height: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
opacity: { duration: ANIMATION_TIMING.OPACITY_DURATION, delay: ANIMATION_TIMING.OPACITY_DELAY },
marginBottom: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
},
},
};
@ -98,19 +118,19 @@ const forgotPasswordVariants = {
opacity: 0,
marginTop: 0,
transition: {
height: { ...springTransition, duration: 0.15 },
height: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
opacity: { duration: 0.1 },
marginTop: { ...springTransition, duration: 0.15 },
marginTop: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
},
},
visible: {
height: 'auto',
opacity: 1,
marginTop: 8,
marginTop: ANIMATION_TIMING.FORGOT_PASSWORD_MARGIN,
transition: {
height: { ...springTransition, duration: 0.15 },
opacity: { duration: 0.15, delay: 0.05 },
marginTop: { ...springTransition, duration: 0.15 },
height: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
opacity: { duration: ANIMATION_TIMING.OPACITY_DURATION, delay: ANIMATION_TIMING.OPACITY_DELAY },
marginTop: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
},
},
};
@ -118,6 +138,7 @@ const forgotPasswordVariants = {
/**
* AuthFormCard component
* @description Unified authentication card with animated transitions between Sign In and Register modes.
* Features smooth expand/collapse animations for form fields using framer-motion.
* @param {AuthFormCardProps} props - Component props
* @returns {JSX.Element} AuthFormCard element
*/
@ -140,10 +161,10 @@ export function AuthFormCard({
/**
* Handle form submission based on current mode
* @param {FormEvent<HTMLFormElement>} e - Form event
* @param {FormEvent<HTMLFormElement>} event - Form submit event
*/
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
setIsLoading(true);
if (isRegister) {
@ -152,11 +173,12 @@ export function AuthFormCard({
onSignIn?.(email, password);
}
setTimeout(() => setIsLoading(false), 1000);
setTimeout(() => setIsLoading(false), ANIMATION_TIMING.FORM_SUBMIT_DELAY);
};
/**
* Toggle between Sign In and Register modes
* Clears all form fields when switching modes
*/
const toggleMode = (): void => {
setMode((prev) => (prev === 'signin' ? 'register' : 'signin'));
@ -169,8 +191,8 @@ export function AuthFormCard({
return (
<motion.div
layout
transition={{ layout: { ...springTransition, duration: 0.3 } }}
className="w-[520px] max-w-full"
transition={{ layout: { ...springTransition, duration: ANIMATION_TIMING.LAYOUT_DURATION } }}
className="w-full max-w-[460px] lg:max-w-[440px] xl:max-w-[520px]"
style={{ willChange: 'transform' }}
>
<AuthCard variant={isRegister ? 'register' : 'signin'}>
@ -186,7 +208,7 @@ export function AuthFormCard({
{/* Form Content */}
<AuthCardContent>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
<form onSubmit={handleSubmit} className="flex flex-col gap-3 md:gap-4 lg:gap-3 xl:gap-4 w-full">
{/* Collapsible Name Fields Row */}
<AnimatePresence initial={false}>
{isRegister && (

View File

@ -134,7 +134,7 @@ export const AuthInput = forwardRef<HTMLInputElement, AuthInputProps>(
className="
flex-1 min-w-0
bg-transparent
text-base font-medium leading-6
text-sm md:text-base lg:text-sm xl:text-base font-medium leading-5 md:leading-6 lg:leading-5 xl:leading-6
text-white
placeholder:text-[rgba(255,255,255,0.5)]
outline-none
@ -149,7 +149,7 @@ export const AuthInput = forwardRef<HTMLInputElement, AuthInputProps>(
<button
type="button"
onClick={togglePasswordVisibility}
className="flex-shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
className="shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOpenIcon /> : <EyeClosedIcon />}

View File

@ -7,9 +7,22 @@
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useState, useRef, useEffect, type FormEvent, type KeyboardEvent, type ClipboardEvent } from 'react';
import { useState, type FormEvent } from 'react';
import { motion, AnimatePresence, type Easing } from 'framer-motion';
import { AuthInput, AuthButton } from './index';
import { OTPInput } from './otp-input';
import { CloseIcon, ArrowLeftIcon } from './modal-icons';
/** Animation timing constants (in milliseconds) */
const ANIMATION_TIMING = {
MODAL_RESET_DELAY: 250,
EMAIL_SUBMIT_DELAY: 600,
OTP_VERIFY_DELAY: 800,
TRANSITION_DURATION: 0.25,
LAYOUT_DURATION: 0.3,
HEADER_DURATION: 0.2,
OTP_LENGTH: 6,
} as const;
/**
* Modal step type - determines which view to show
@ -30,126 +43,16 @@ interface ForgotPasswordModalProps {
onVerify?: (otp: string) => void;
}
/**
* Close icon component
*/
function CloseIcon(): JSX.Element {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/**
* Left arrow icon component
*/
function ArrowLeftIcon(): JSX.Element {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/**
* Smooth easing curve for animations (cubic-bezier)
* Material Design standard easing
*/
const smoothEasing: Easing = [0.4, 0, 0.2, 1];
/**
* OTP Input component for 6-digit verification code
*/
function OTPInput({
otp,
setOtp,
}: {
otp: string[];
setOtp: React.Dispatch<React.SetStateAction<string[]>>;
}): JSX.Element {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
// Auto-focus first input when component mounts
useEffect(() => {
const timer = setTimeout(() => {
inputRefs.current[0]?.focus();
}, 300);
return () => clearTimeout(timer);
}, []);
const handleChange = (index: number, value: string): void => {
if (value.length > 1) value = value.slice(-1);
if (value && !/^\d$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
if (value && index < 5) {
inputRefs.current[index + 1]?.focus();
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>, index: number): void => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
const handlePaste = (e: ClipboardEvent<HTMLInputElement>): void => {
e.preventDefault();
const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6);
const newOtp = [...otp];
for (let i = 0; i < pastedData.length; i++) {
newOtp[i] = pastedData[i];
}
setOtp(newOtp);
const nextIndex = Math.min(pastedData.length, 5);
inputRefs.current[nextIndex]?.focus();
};
return (
<div className="flex gap-3 items-center justify-center">
{otp.map((digit, index) => (
<motion.input
key={index}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.25,
delay: index * 0.04,
ease: smoothEasing,
}}
ref={(el) => { inputRefs.current[index] = el; }}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, index)}
onPaste={handlePaste}
className="
w-[44px] h-[44px]
bg-[rgba(255,255,255,0.15)]
border border-[rgba(255,255,255,0.1)]
rounded-[8px]
text-center text-white text-[18px] font-medium
outline-none
focus:border-[rgba(255,255,255,0.4)]
focus:bg-[rgba(255,255,255,0.2)]
transition-all duration-200
"
placeholder="•"
aria-label={`Digit ${index + 1}`}
/>
))}
</div>
);
}
/**
* ForgotPasswordModal component
* @description Modal with smooth transitions between email input and OTP verification.
* Features animated backdrop, scale/fade modal entrance, and sliding form content.
* @param {ForgotPasswordModalProps} props - Component props
* @returns {JSX.Element} ForgotPasswordModal element
*/
@ -164,6 +67,9 @@ export function ForgotPasswordModal({
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
const [isLoading, setIsLoading] = useState(false);
/**
* Handle modal close and reset state
*/
const handleClose = (): void => {
onClose();
setTimeout(() => {
@ -171,49 +77,58 @@ export function ForgotPasswordModal({
setEmail('');
setOtp(['', '', '', '', '', '']);
setIsLoading(false);
}, 250);
}, ANIMATION_TIMING.MODAL_RESET_DELAY);
};
const handleEmailSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
/**
* Handle email form submission
* @param {FormEvent<HTMLFormElement>} event - Form submit event
*/
const handleEmailSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
setIsLoading(true);
onSubmit?.(email);
setTimeout(() => {
setIsLoading(false);
setStep('verify');
}, 600);
}, ANIMATION_TIMING.EMAIL_SUBMIT_DELAY);
};
const handleVerifySubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
/**
* Handle OTP verification form submission
* @param {FormEvent<HTMLFormElement>} event - Form submit event
*/
const handleVerifySubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
const otpString = otp.join('');
if (otpString.length !== 6) return;
if (otpString.length !== ANIMATION_TIMING.OTP_LENGTH) return;
setIsLoading(true);
onVerify?.(otpString);
setTimeout(() => {
setIsLoading(false);
handleClose();
}, 800);
}, ANIMATION_TIMING.OTP_VERIFY_DELAY);
};
/**
* Handle resend OTP code
* @description Resends the OTP verification code to the user's email
* @throws {Error} When resend request fails
* Handle resend code request
*/
const handleResendCode = async (): Promise<void> => {
try {
// TODO: Implement resend OTP API call
// await resendOtpCode(email);
} catch (error) {
// Error handling will be implemented with proper logging service
throw error;
}
const handleResendCode = (): void => {
// TODO(AUTH-007): Implement resend OTP API call
onSubmit?.(email);
};
/**
* Handle backdrop click to close modal
*/
const handleBackdropClick = (): void => handleClose();
const handleModalClick = (e: React.MouseEvent): void => e.stopPropagation();
/**
* Prevent modal card clicks from closing modal
* @param {React.MouseEvent} event - Mouse event
*/
const handleModalClick = (event: React.MouseEvent): void => event.stopPropagation();
const isEmailStep = step === 'email';
@ -227,7 +142,7 @@ export function ForgotPasswordModal({
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25, ease: smoothEasing }}
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
onClick={handleBackdropClick}
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm"
/>
@ -238,21 +153,22 @@ export function ForgotPasswordModal({
initial={{ opacity: 0, scale: 0.96, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: 10 }}
transition={{ duration: 0.25, ease: smoothEasing }}
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
onClick={handleBackdropClick}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
>
{/* Modal Card */}
<motion.div
layout
transition={{ duration: 0.3, ease: smoothEasing }}
transition={{ duration: ANIMATION_TIMING.LAYOUT_DURATION, ease: smoothEasing }}
onClick={handleModalClick}
className="
flex flex-col items-center gap-8
w-[520px] max-w-full
p-[42px]
rounded-[32px]
flex flex-col items-center gap-6 md:gap-7 lg:gap-8
w-full max-w-[520px]
p-6 md:p-8 lg:p-[42px]
rounded-2xl md:rounded-3xl lg:rounded-[32px]
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
mx-4
"
style={{
background: 'linear-gradient(180deg, #00B8B7 0%, #001C8E 100%)',
@ -268,14 +184,14 @@ export function ForgotPasswordModal({
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.2, ease: smoothEasing }}
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
>
<h2 className="text-[24px] font-semibold leading-[38px] text-white">
<h2 className="text-xl md:text-xl lg:text-2xl xl:text-[24px] font-semibold leading-tight md:leading-tight lg:leading-[38px] text-white">
{isEmailStep ? 'Forgot your password' : 'Verify Your Email'}
</h2>
<p className={isEmailStep
? "text-[24px] font-semibold leading-[38px] text-white"
: "text-[18px] font-medium leading-6 text-[rgba(255,255,255,0.75)] mt-1"
? "text-xl md:text-xl lg:text-2xl xl:text-[24px] font-semibold leading-tight md:leading-tight lg:leading-[38px] text-white"
: "text-base md:text-base lg:text-lg xl:text-[18px] font-medium leading-5 md:leading-5 lg:leading-6 text-[rgba(255,255,255,0.75)] mt-1"
}>
{isEmailStep ? 'and continue' : 'Enter the 6-Digit Verification Code'}
</p>
@ -289,11 +205,11 @@ export function ForgotPasswordModal({
onClick={handleClose}
className="
flex items-center justify-center
w-8 h-8 rounded-full
w-8 h-8 md:w-10 md:h-10 rounded-full
bg-[rgba(255,255,255,0.1)]
hover:bg-[rgba(255,255,255,0.2)]
transition-colors duration-200
cursor-pointer shrink-0
cursor-pointer shrink-0 min-w-[44px] min-h-[44px]
"
aria-label="Close modal"
>
@ -310,7 +226,7 @@ export function ForgotPasswordModal({
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.25, ease: smoothEasing }}
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
onSubmit={handleEmailSubmit}
className="flex flex-col gap-6 w-full"
>
@ -333,14 +249,14 @@ export function ForgotPasswordModal({
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.25, ease: smoothEasing }}
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
onSubmit={handleVerifySubmit}
className="flex flex-col gap-6 w-full items-center"
>
<OTPInput otp={otp} setOtp={setOtp} />
<AuthButton
isLoading={isLoading}
disabled={otp.join('').length !== 6}
disabled={otp.join('').length !== ANIMATION_TIMING.OTP_LENGTH}
fullWidth
>
Continue
@ -358,7 +274,7 @@ export function ForgotPasswordModal({
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: smoothEasing }}
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
type="button"
onClick={handleClose}
className="
@ -366,6 +282,7 @@ export function ForgotPasswordModal({
text-sm font-semibold text-white
cursor-pointer hover:opacity-80
transition-opacity duration-200
min-h-[44px]
"
>
<ArrowLeftIcon />
@ -377,16 +294,16 @@ export function ForgotPasswordModal({
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: smoothEasing }}
className="flex items-center justify-center gap-1"
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
className="flex flex-col sm:flex-row items-center justify-center gap-1 text-center sm:text-left"
>
<span className="text-sm font-normal text-[rgba(255,255,255,0.5)]">
<span className="text-xs sm:text-sm font-normal text-[rgba(255,255,255,0.5)]">
Didn't you receive any code?
</span>
<button
type="button"
onClick={handleResendCode}
className="text-sm font-semibold text-white cursor-pointer hover:underline"
className="text-xs sm:text-sm font-semibold text-white cursor-pointer hover:underline min-h-[44px]"
>
Resend Code
</button>

View File

@ -9,4 +9,6 @@ export { AuthInput } from './auth-input';
export { AuthButton, SocialButton, TextButton, GoogleIcon, AzureIcon } from './auth-button';
export { AuthFormCard } from './auth-form-card';
export { ForgotPasswordModal } from './forgot-password-modal';
export { OTPInput } from './otp-input';
export { CloseIcon, ArrowLeftIcon } from './modal-icons';

View File

@ -0,0 +1,32 @@
/**
* Modal Icons Component
* @description Icon components for modal dialogs
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
/**
* Close icon component
* @description Renders an X icon for closing the modal
* @returns {JSX.Element} Close icon SVG
*/
export function CloseIcon(): JSX.Element {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/**
* Left arrow icon component
* @description Renders a left-pointing arrow for back navigation
* @returns {JSX.Element} Arrow left icon SVG
*/
export function ArrowLeftIcon(): JSX.Element {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}

View File

@ -0,0 +1,139 @@
/**
* OTP Input Component
* @description Six individual input boxes for OTP entry with auto-focus,
* backspace navigation, and paste support.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useRef, useEffect, type KeyboardEvent, type ClipboardEvent } from 'react';
import { motion, type Easing } from 'framer-motion';
/** Animation timing constants */
const ANIMATION_TIMING = {
OTP_AUTOFOCUS_DELAY: 300,
TRANSITION_DURATION: 0.25,
OTP_STAGGER_DELAY: 0.04,
OTP_LENGTH: 6,
OTP_MAX_INDEX: 5,
} as const;
/**
* Props for OTPInput component
*/
interface OTPInputProps {
/** Current OTP values array */
otp: string[];
/** Setter function for OTP state */
setOtp: React.Dispatch<React.SetStateAction<string[]>>;
}
/**
* Smooth easing curve for animations
*/
const smoothEasing: Easing = [0.4, 0, 0.2, 1];
/**
* OTP Input component
* @description Six individual input boxes for OTP entry with auto-focus,
* backspace navigation, and paste support.
* @param {OTPInputProps} props - Component props
* @returns {JSX.Element} OTP input element
*/
export function OTPInput({ otp, setOtp }: OTPInputProps): JSX.Element {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
useEffect(() => {
const timer = setTimeout(() => {
inputRefs.current[0]?.focus();
}, ANIMATION_TIMING.OTP_AUTOFOCUS_DELAY);
return () => clearTimeout(timer);
}, []);
/**
* Handle input value change
* @param {number} index - Input index
* @param {string} value - New input value
*/
const handleChange = (index: number, value: string): void => {
if (value.length > 1) value = value.slice(-1);
if (value && !/^\d$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
if (value && index < ANIMATION_TIMING.OTP_MAX_INDEX) {
inputRefs.current[index + 1]?.focus();
}
};
/**
* Handle keyboard navigation
* @param {KeyboardEvent<HTMLInputElement>} event - Keyboard event
* @param {number} index - Current input index
*/
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>, index: number): void => {
if (event.key === 'Backspace' && !otp[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
/**
* Handle paste event for OTP auto-fill
* @param {ClipboardEvent<HTMLInputElement>} event - Clipboard event
*/
const handlePaste = (event: ClipboardEvent<HTMLInputElement>): void => {
event.preventDefault();
const pastedData = event.clipboardData
.getData('text')
.replace(/\D/g, '')
.slice(0, ANIMATION_TIMING.OTP_LENGTH);
const newOtp = [...otp];
for (let i = 0; i < pastedData.length; i++) {
newOtp[i] = pastedData[i];
}
setOtp(newOtp);
const nextIndex = Math.min(pastedData.length, ANIMATION_TIMING.OTP_MAX_INDEX);
inputRefs.current[nextIndex]?.focus();
};
return (
<div className="flex gap-2 md:gap-3 items-center justify-center w-full">
{otp.map((digit, index) => (
<motion.input
key={index}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: ANIMATION_TIMING.TRANSITION_DURATION,
delay: index * ANIMATION_TIMING.OTP_STAGGER_DELAY,
ease: smoothEasing,
}}
ref={(el) => { inputRefs.current[index] = el; }}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, index)}
onPaste={handlePaste}
className="
w-10 h-10 md:w-[44px] md:h-[44px]
bg-[rgba(255,255,255,0.15)]
border border-[rgba(255,255,255,0.1)]
rounded-lg md:rounded-[8px]
text-center text-white text-base md:text-[18px] font-medium
outline-none
focus:border-[rgba(255,255,255,0.4)]
focus:bg-[rgba(255,255,255,0.2)]
transition-all duration-200
flex-1 max-w-[44px]
"
placeholder="•"
aria-label={`Digit ${index + 1}`}
/>
))}
</div>
);
}

View File

@ -11,7 +11,7 @@ export function Sidebar(): JSX.Element {
return (
<aside className="fixed left-4 top-20 bottom-4 w-14 md:left-6 md:top-24 md:bottom-6 md:w-16 bg-gradient-to-b from-[#00A1A0] to-[#00166D] flex flex-col items-center py-4 md:py-6 gap-4 md:gap-6 z-50 rounded-xl md:rounded-2xl shadow-xl hidden md:flex">
{/* Navigation Icons */}
<nav className="flex flex-col items-center gap-5 flex-1 w-full">
<nav className="flex flex-col items-center gap-3 sm:gap-4 md:gap-5 flex-1 w-full">
{/* Home - Active State */}
<Link
to={APP_PATHS.dashboard}

View File

@ -0,0 +1,131 @@
/**
* Configure Knowledge Base Dialog Component
* @description Dialog for managing knowledge base collections
*/
import { useEscapeKey, useBodyScrollLock } from '@/hooks';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogClose,
} from './dialog';
/**
* ConfigureKnowledgeBaseDialog component props
*/
interface ConfigureKnowledgeBaseDialogProps {
/** Whether the dialog is open */
open: boolean;
/** Callback when dialog open state changes */
onOpenChange: (open: boolean) => void;
/** Callback when Add Knowledge Base button is clicked */
onAddKnowledgeBase?: () => void;
}
/**
* ConfigureKnowledgeBaseDialog component
* @description Modal dialog for managing knowledge base collections with empty state
* @param props - Component props
* @returns {JSX.Element} ConfigureKnowledgeBaseDialog element
*/
export function ConfigureKnowledgeBaseDialog({
open,
onOpenChange,
onAddKnowledgeBase,
}: ConfigureKnowledgeBaseDialogProps): JSX.Element {
useEscapeKey(() => onOpenChange(false), open);
useBodyScrollLock(open);
/**
* Handle Add Knowledge Base button click
* @description Triggers the onAddKnowledgeBase callback and closes dialog
*/
const handleAddKnowledgeBase = (): void => {
if (onAddKnowledgeBase) {
onAddKnowledgeBase();
}
onOpenChange(false);
};
/**
* Handle keyboard events on Add Knowledge Base button
* @description Handles Enter and Space key presses
* @param e - Keyboard event
*/
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAddKnowledgeBase();
}
};
// Icon from Figma design - database icon with exclamation
const emptyStateIcon = 'https://www.figma.com/api/mcp/asset/31344752-2e10-42c5-9678-5d2cb0b3dadb';
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[620px]">
<DialogHeader>
<div className="flex flex-col gap-0.5">
<DialogTitle className="font-semibold text-[22px] text-black leading-normal">
Add Knowledge Base
</DialogTitle>
<DialogDescription className="font-normal text-sm text-[rgba(0,0,0,0.75)] max-w-[484px]">
Manage your document collections and knowledge sources
</DialogDescription>
</div>
<DialogClose onClick={() => onOpenChange(false)} />
</DialogHeader>
<div className="px-8 pt-6 pb-8">
<div className="flex flex-col gap-8 items-end relative shrink-0">
{/* Empty State Content */}
<div className="flex flex-col items-center justify-center relative shrink-0 w-full">
<div className="flex flex-col gap-4 items-center relative shrink-0">
{/* Database Icon */}
<div className="h-[61px] relative shrink-0 w-[59px]">
<img
alt="Database icon"
className="block max-w-none size-full"
src={emptyStateIcon}
/>
</div>
{/* Empty State Messages */}
<div className="flex flex-col gap-[11px] items-center not-italic relative shrink-0 text-center">
<div className="flex flex-col font-semibold justify-center leading-normal min-w-full relative shrink-0 text-[#252526] text-base tracking-[0.25px] w-[min-content]">
<p className="leading-normal whitespace-pre-wrap">
No knowledge bases found
</p>
</div>
<p className="font-normal leading-normal relative shrink-0 text-[#6d6b70] text-sm w-[266px] whitespace-pre-wrap">
Get started by adding your first knowledge base
</p>
</div>
</div>
</div>
{/* Add Knowledge Base Button */}
<div className="flex items-center justify-center relative shrink-0 w-full">
<button
type="button"
onClick={handleAddKnowledgeBase}
onKeyDown={handleKeyDown}
className="bg-[#03f] flex flex-1 items-center justify-center px-[42px] py-[14px] relative rounded-[12px] hover:bg-[#002BCC] active:bg-[#0025AA] transition-colors cursor-pointer focus:outline-none focus:ring-2 focus:ring-[#0033FF] focus:ring-offset-2"
aria-label="Add Knowledge Base"
>
<p className="capitalize font-medium leading-[18px] not-italic relative shrink-0 text-sm text-center text-white">
Add Knowledge base
</p>
</button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -4,6 +4,7 @@ import { Info, ChevronDown } from 'lucide-react';
import { APP_PATHS } from '@/routes';
import { AddToolDialog } from '@/components/ui/add-tool-dialog';
import { ConfigureMemoryDialog } from '@/components/ui/configure-memory-dialog';
import { ConfigureKnowledgeBaseDialog } from '@/components/ui/configure-knowledge-base-dialog';
import { ToolChip } from '@/components/ui/tool-chip';
import type { Tool } from '@/types';
@ -39,6 +40,7 @@ export function AgentCreatePage(): JSX.Element {
const [enableMemory, setEnableMemory] = useState(true);
const [isToolDialogOpen, setIsToolDialogOpen] = useState(false);
const [isMemoryDialogOpen, setIsMemoryDialogOpen] = useState(false);
const [isKnowledgeBaseDialogOpen, setIsKnowledgeBaseDialogOpen] = useState(false);
const [memoryValue, setMemoryValue] = useState(2);
const [editingTool, setEditingTool] = useState<Tool | null>(null);
const [tools, setTools] = useState<Tool[]>([
@ -250,7 +252,10 @@ export function AgentCreatePage(): JSX.Element {
</div>
{enableKb && (
<div className="flex items-center justify-between">
<button className="flex items-center gap-0.5 text-xs font-medium text-[#03f] leading-[1.5] hover:underline">
<button
onClick={() => setIsKnowledgeBaseDialogOpen(true)}
className="flex items-center gap-0.5 text-xs font-medium text-[#03f] leading-[1.5] hover:underline"
>
Configure
<img src="/Agent/redirect.svg" alt="Redirect" className="size-3.5 text-[#03f]" />
</button>
@ -382,6 +387,16 @@ export function AgentCreatePage(): JSX.Element {
initialValue={memoryValue}
onSave={setMemoryValue}
/>
{/* Configure Knowledge Base Dialog */}
<ConfigureKnowledgeBaseDialog
open={isKnowledgeBaseDialogOpen}
onOpenChange={setIsKnowledgeBaseDialogOpen}
onAddKnowledgeBase={() => {
// Handle add knowledge base action
console.log('Add Knowledge Base clicked');
}}
/>
</div>
);
}

View File

@ -10,11 +10,21 @@
import { useState } from 'react';
import agenticiqLogo from '@/assets/images/logo/AgenticIQLogo.svg';
import topRightGlow from '@/assets/images/backgrounds/top-right-glow.svg';
import bottomLeftWave1 from '@/assets/images/backgrounds/bottom-left-wave1 .svg';
import bottomLeftWave1 from '@/assets/images/backgrounds/bottom-left-wave1.svg';
import bottomLeftWave2 from '@/assets/images/backgrounds/bottom-left-wave2.svg';
import logoGlowBg from '@/assets/images/backgrounds/logo-glow-bg.svg';
import { AuthFormCard, ForgotPasswordModal } from '@/components/auth';
/**
* Register form data interface
*/
interface RegisterFormData {
firstName: string;
lastName: string;
email: string;
password: string;
}
/**
* LoginPage component
* @description Full-page login layout with responsive 2-column grid structure.
@ -26,118 +36,67 @@ export function LoginPage(): JSX.Element {
/**
* Handle sign in form submission
* @description Processes user sign in credentials
* @param email - User email address
* @param password - User password
* @throws {Error} When authentication fails
* @param {string} email - User email address
* @param {string} password - User password
*/
const handleSignIn = async (email: string, password: string): Promise<void> => {
try {
// TODO: Implement actual authentication API call
// await signInUser({ email, password });
} catch (error) {
// Error handling will be implemented with proper logging service
throw error;
}
const handleSignIn = (email: string, password: string): void => {
// TODO(AUTH-001): Implement actual sign in API call
void Promise.resolve({ email, password });
};
/**
* Handle user registration
* @description Processes new user registration
* @param data - Registration form data
* @throws {Error} When registration fails
* Handle register form submission
* @param {RegisterFormData} data - Registration form data
*/
const handleRegister = async (data: {
firstName: string;
lastName: string;
email: string;
password: string;
}): Promise<void> => {
try {
// TODO: Implement actual registration API call
// await registerUser(data);
} catch (error) {
// Error handling will be implemented with proper logging service
throw error;
}
const handleRegister = (data: RegisterFormData): void => {
// TODO(AUTH-002): Implement actual registration API call
void Promise.resolve(data);
};
/**
* Handle Google authentication
* @description Initiates Google OAuth flow
* @throws {Error} When OAuth flow fails
* Handle Google OAuth authentication
*/
const handleGoogleAuth = async (): Promise<void> => {
try {
// TODO: Implement Google OAuth integration
// await authenticateWithGoogle();
} catch (error) {
// Error handling will be implemented with proper logging service
throw error;
}
const handleGoogleAuth = (): void => {
// TODO(AUTH-003): Implement Google OAuth flow
};
/**
* Handle Azure authentication
* @description Initiates Azure AD authentication flow
* @throws {Error} When Azure authentication fails
* Handle Azure OAuth authentication
*/
const handleAzureAuth = async (): Promise<void> => {
try {
// TODO: Implement Azure AD authentication
// await authenticateWithAzure();
} catch (error) {
// Error handling will be implemented with proper logging service
throw error;
}
const handleAzureAuth = (): void => {
// TODO(AUTH-004): Implement Azure OAuth flow
};
/**
* Handle forgot password click
* @description Opens the forgot password modal
* Open forgot password modal
*/
const handleForgotPasswordClick = (): void => {
setShowForgotPwd(true);
};
/**
* Handle forgot password modal close
* @description Closes the forgot password modal
* Close forgot password modal
*/
const handleForgotPasswordClose = (): void => {
setShowForgotPwd(false);
};
/**
* Handle forgot password form submission
* @description Sends password recovery email
* @param email - User email address for password recovery
* @throws {Error} When password recovery request fails
* Handle forgot password email submission
* @param {string} email - Email for password recovery
*/
const handleForgotPasswordSubmit = async (email: string): Promise<void> => {
try {
// TODO: Implement password recovery API call
// await requestPasswordRecovery(email);
} catch (error) {
// Error handling will be implemented with proper logging service
throw error;
}
const handleForgotPasswordSubmit = (email: string): void => {
// TODO(AUTH-005): Implement password recovery API call
void Promise.resolve(email);
};
/**
* Handle OTP verification
* @description Verifies the OTP code for password recovery
* @param otp - 6-digit OTP code
* @throws {Error} When OTP verification fails
* Handle OTP verification submission
* @param {string} otp - 6-digit OTP code
*/
const handleOtpVerify = async (otp: string): Promise<void> => {
try {
// TODO: Implement OTP verification API call
// await verifyOtp(otp);
} catch (error) {
// Error handling will be implemented with proper logging service
throw error;
}
const handleOtpVerify = (otp: string): void => {
// TODO(AUTH-006): Implement OTP verification API call
void Promise.resolve(otp);
};
return (
@ -147,11 +106,11 @@ export function LoginPage(): JSX.Element {
<BackgroundDecorations />
{/* AgenticIQ Logo */}
<div className="absolute left-[80px] top-[54px] z-20">
<div className="absolute left-4 top-4 md:left-8 lg:left-12 xl:left-[80px] md:top-8 lg:top-8 xl:top-[54px] z-20">
<img
src={agenticiqLogo}
alt="AgenticIQ Logo"
className="h-[117px] w-auto"
className="h-16 w-auto md:h-24 lg:h-20 xl:h-[117px]"
/>
</div>
@ -159,12 +118,12 @@ export function LoginPage(): JSX.Element {
<LeftColumnText />
{/* Main Content Grid */}
<div className="relative z-10 grid min-h-screen grid-cols-1 lg:grid-cols-[1fr_600px] xl:grid-cols-[1fr_620px]">
<div className="relative z-10 grid min-h-screen grid-cols-1 md:grid-cols-1 lg:grid-cols-[1fr_480px] xl:grid-cols-[1fr_600px] 2xl:grid-cols-[1fr_620px]">
{/* Left Column - Branding/Marketing Content */}
<LeftColumn />
{/* Right Column - Auth Form Card */}
<div className="flex items-center justify-start px-6 py-12 lg:px-0 lg:py-16">
<div className="flex items-center justify-center md:justify-center lg:justify-start px-4 py-8 md:px-6 md:py-12 lg:px-4 lg:py-12 xl:px-0 xl:py-16">
<AuthFormCard
initialMode="signin"
onSignIn={handleSignIn}
@ -191,6 +150,7 @@ export function LoginPage(): JSX.Element {
/**
* BackgroundDecorations component
* @description Renders decorative background images matching Figma design.
* All images are purely decorative and hidden from screen readers.
* @returns {JSX.Element} Background decoration elements
*/
function BackgroundDecorations(): JSX.Element {
@ -200,7 +160,7 @@ function BackgroundDecorations(): JSX.Element {
<img
src={logoGlowBg}
alt=""
className="absolute left-0 top-0 h-auto w-auto opacity-[0.08]"
className="absolute left-0 top-0 h-auto w-auto opacity-[0.08] hidden md:block"
style={{ width: '550px', height: 'auto', maxHeight: '350px' }}
/>
@ -208,7 +168,7 @@ function BackgroundDecorations(): JSX.Element {
<img
src={topRightGlow}
alt=""
className="absolute right-0 top-0 h-auto w-auto opacity-[0.15]"
className="absolute right-0 top-0 h-auto w-auto opacity-[0.15] hidden md:block"
style={{ width: '515px', height: 'auto', maxHeight: '614px' }}
/>
@ -216,7 +176,7 @@ function BackgroundDecorations(): JSX.Element {
<img
src={bottomLeftWave1}
alt=""
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15]"
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15] hidden md:block"
style={{ width: '559px', height: 'auto', maxHeight: '613px' }}
/>
@ -224,7 +184,7 @@ function BackgroundDecorations(): JSX.Element {
<img
src={bottomLeftWave2}
alt=""
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15]"
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15] hidden md:block"
style={{ width: '350px', height: 'auto', maxHeight: '500px' }}
/>
</div>
@ -233,21 +193,20 @@ function BackgroundDecorations(): JSX.Element {
/**
* LeftColumnText component
* @description Left side text content (heading and description).
* @description Left side text content with heading and description.
* Positioned absolutely to match Figma design specifications.
* @returns {JSX.Element} Text elements positioned as per Figma
*/
function LeftColumnText(): JSX.Element {
return (
<>
<h1
className="absolute text-[36px] font-semibold text-black leading-normal whitespace-pre-wrap z-20"
style={{ left: '110px', top: '348px', width: '513px' }}
className="hidden lg:block absolute text-2xl md:text-3xl lg:text-2xl xl:text-[36px] font-semibold text-black leading-tight lg:leading-tight xl:leading-normal whitespace-pre-wrap z-20 lg:left-12 lg:top-64 lg:w-80 xl:left-[110px] xl:top-[348px] xl:w-[513px]"
>
Engineering the Future with Intelligent Agents
</h1>
<p
className="absolute text-[24px] font-normal text-[rgba(0,0,0,0.75)] leading-normal whitespace-pre-wrap z-20"
style={{ left: '110px', top: '482px', width: '607px' }}
className="hidden lg:block absolute text-sm md:text-base lg:text-sm xl:text-[24px] font-normal text-[rgba(0,0,0,0.75)] leading-relaxed lg:leading-relaxed xl:leading-normal whitespace-pre-wrap z-20 lg:left-12 lg:top-96 lg:w-80 xl:left-[110px] xl:top-[482px] xl:w-[607px]"
>
Deploy intelligent agents that automate complex workflows, enhance decision-making, and scale your operations. Built for enterprise reliability with seamless integration capabilities.
</p>
@ -257,12 +216,13 @@ function LeftColumnText(): JSX.Element {
/**
* LeftColumn component
* @description Empty container for left side content.
* @description Empty container for left side content in the grid layout.
* Text content is positioned absolutely on the page level for precise control.
* @returns {JSX.Element} Left column container
*/
function LeftColumn(): JSX.Element {
return (
<div className="flex flex-col justify-center px-6 py-12 sm:px-12 md:px-16 lg:px-20 xl:px-28">
<div className="hidden md:hidden lg:flex flex-col justify-center px-6 py-12 md:px-12 lg:px-8 xl:px-20 2xl:px-28">
{/* Text content is positioned absolutely on page level */}
</div>
);

View File

@ -7,12 +7,12 @@ import { Link } from '@tanstack/react-router';
export function SignUpPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#C8FAFF] via-[#F5F8FF] to-[#C6D7FE] px-4 py-12">
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#C8FAFF] via-[#F5F8FF] to-[#C6D7FE] px-4 py-8 sm:py-12">
<div className="w-full max-w-md rounded-xl md:rounded-2xl bg-white p-6 sm:p-8 shadow-xl">
<div className="mb-6 text-center">
<img src="/Logo.png" alt="AgenticIQ Logo" className="mx-auto h-10 w-auto" />
<h1 className="mt-4 text-2xl font-semibold text-gray-900">Create your account</h1>
<p className="mt-1 text-sm text-gray-500">Join AgenticIQ to manage your agents and workflows.</p>
<img src="/Logo.png" alt="AgenticIQ Logo" className="mx-auto h-8 w-auto sm:h-10" />
<h1 className="mt-4 text-xl sm:text-2xl font-semibold text-gray-900">Create your account</h1>
<p className="mt-1 text-xs sm:text-sm text-gray-500">Join AgenticIQ to manage your agents and workflows.</p>
</div>
<form className="space-y-4">
@ -20,7 +20,7 @@ export function SignUpPage() {
<label className="mb-1 block text-sm font-medium text-gray-700">Full name</label>
<input
type="text"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
placeholder="Jane Doe"
/>
</div>
@ -28,7 +28,7 @@ export function SignUpPage() {
<label className="mb-1 block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
placeholder="you@example.com"
/>
</div>
@ -36,19 +36,19 @@ export function SignUpPage() {
<label className="mb-1 block text-sm font-medium text-gray-700">Password</label>
<input
type="password"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
placeholder="••••••••"
/>
</div>
<button
type="submit"
className="w-full rounded-lg bg-[#0033FF] px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-blue-500/20 transition hover:bg-[#0042CC]"
className="w-full rounded-lg bg-[#0033FF] px-4 py-3 sm:py-2.5 text-sm font-semibold text-white shadow-lg shadow-blue-500/20 transition hover:bg-[#0042CC] min-h-[44px]"
>
Sign Up
</button>
</form>
<p className="mt-4 text-center text-sm text-gray-600">
<p className="mt-4 text-center text-xs sm:text-sm text-gray-600">
Already have an account?{' '}
<Link to="/" className="font-semibold text-[#0033FF] hover:underline">
Sign in