100 lines
2.5 KiB
TypeScript
100 lines
2.5 KiB
TypeScript
|
|
import React, { ReactNode } from 'react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
import { AlertCircle } from 'lucide-react';
|
||
|
|
|
||
|
|
interface FormFieldWrapperProps {
|
||
|
|
id: string;
|
||
|
|
label: string;
|
||
|
|
icon?: ReactNode;
|
||
|
|
error?: string;
|
||
|
|
isFilled: boolean;
|
||
|
|
isFocused: boolean;
|
||
|
|
disabled?: boolean;
|
||
|
|
children: ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function FormFieldWrapper({
|
||
|
|
id,
|
||
|
|
label,
|
||
|
|
icon,
|
||
|
|
error,
|
||
|
|
isFilled,
|
||
|
|
isFocused,
|
||
|
|
disabled,
|
||
|
|
children,
|
||
|
|
}: FormFieldWrapperProps) {
|
||
|
|
const isFloated = isFocused || isFilled;
|
||
|
|
const hasError = !!error;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="w-full">
|
||
|
|
<div className="relative">
|
||
|
|
{/* Icon (if provided) */}
|
||
|
|
{icon && (
|
||
|
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-puffin-gray-label z-10">
|
||
|
|
{icon}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Floating Label */}
|
||
|
|
<motion.label
|
||
|
|
htmlFor={id}
|
||
|
|
className={`
|
||
|
|
absolute left-3 pointer-events-none origin-left z-10
|
||
|
|
transition-colors duration-200
|
||
|
|
${icon ? 'pl-7' : ''}
|
||
|
|
${hasError ? 'text-puffin-error' : isFloated ? 'text-puffin-blue-focus' : 'text-puffin-gray-label'}
|
||
|
|
${disabled ? 'opacity-50' : ''}
|
||
|
|
`}
|
||
|
|
initial={false}
|
||
|
|
animate={{
|
||
|
|
y: isFloated ? -24 : 12,
|
||
|
|
scale: isFloated ? 0.85 : 1,
|
||
|
|
}}
|
||
|
|
transition={{
|
||
|
|
type: 'tween',
|
||
|
|
ease: 'easeOut',
|
||
|
|
duration: 0.2,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{label}
|
||
|
|
</motion.label>
|
||
|
|
|
||
|
|
{/* Input Container with Border & Focus Effect */}
|
||
|
|
<div
|
||
|
|
className={`
|
||
|
|
relative rounded-lg transition-all duration-200
|
||
|
|
${hasError
|
||
|
|
? isFocused
|
||
|
|
? 'shadow-focus-red'
|
||
|
|
: ''
|
||
|
|
: isFocused
|
||
|
|
? 'shadow-focus-blue'
|
||
|
|
: ''
|
||
|
|
}
|
||
|
|
`}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Error Message */}
|
||
|
|
<AnimatePresence>
|
||
|
|
{hasError && (
|
||
|
|
<motion.div
|
||
|
|
id={`${id}-error`}
|
||
|
|
initial={{ opacity: 0, y: -10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
exit={{ opacity: 0, y: -10 }}
|
||
|
|
transition={{ duration: 0.2 }}
|
||
|
|
className="flex items-center gap-1.5 mt-1.5 text-sm text-puffin-error"
|
||
|
|
>
|
||
|
|
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||
|
|
<span>{error}</span>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|