diff --git a/src/assets/images/backgrounds/bottom-left-wave1 .svg b/src/assets/images/backgrounds/bottom-left-wave1.svg similarity index 100% rename from src/assets/images/backgrounds/bottom-left-wave1 .svg rename to src/assets/images/backgrounds/bottom-left-wave1.svg diff --git a/src/components/auth/auth-button.tsx b/src/components/auth/auth-button.tsx index e1dc3c7..9fe3156 100644 --- a/src/components/auth/auth-button.tsx +++ b/src/components/auth/auth-button.tsx @@ -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({ diff --git a/src/components/auth/index.ts b/src/components/auth/index.ts index 0bda060..0958560 100644 --- a/src/components/auth/index.ts +++ b/src/components/auth/index.ts @@ -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'; diff --git a/src/components/auth/modal-icons.tsx b/src/components/auth/modal-icons.tsx new file mode 100644 index 0000000..da32acf --- /dev/null +++ b/src/components/auth/modal-icons.tsx @@ -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 ( + + + + ); +} + +/** + * 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 ( + + + + ); +} + diff --git a/src/components/auth/otp-input.tsx b/src/components/auth/otp-input.tsx new file mode 100644 index 0000000..c72d41a --- /dev/null +++ b/src/components/auth/otp-input.tsx @@ -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>; +} + +/** + * 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} event - Keyboard event + * @param {number} index - Current input index + */ + const handleKeyDown = (event: KeyboardEvent, 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} event - Clipboard event + */ + const handlePaste = (event: ClipboardEvent): 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 ( +
+ {otp.map((digit, index) => ( + { 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}`} + /> + ))} +
+ ); +} + diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index d9c93af..5cdbf28 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -11,7 +11,7 @@ export function Sidebar(): JSX.Element { return (