Files
dyolink/frontend/src/components/ui/shared/Input.tsx

121 lines
3.0 KiB
TypeScript

'use client';
import React, { forwardRef, useId, useState } from 'react';
import { Eye, EyeOff } from 'lucide-react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
icon?: React.ReactNode;
endIcon?: React.ReactNode;
passwordToggleLabels?: {
show: string;
hide: string;
};
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
label,
error,
icon,
endIcon,
passwordToggleLabels,
className = '',
id,
type,
...props
},
ref,
) => {
const [showPassword, setShowPassword] = useState(false);
const genId = useId();
const inputId = id ?? genId;
const resolvedEndIcon =
endIcon ??
(passwordToggleLabels ? (
<button
type="button"
tabIndex={-1}
onClick={() => setShowPassword((visible) => !visible)}
className="rounded p-0.5 text-text-muted transition-colors hover:text-text-secondary"
aria-label={
showPassword ? passwordToggleLabels.hide : passwordToggleLabels.show
}
>
{showPassword ? (
<EyeOff className="h-5 w-5 icon-flat" />
) : (
<Eye className="h-5 w-5 icon-flat" />
)}
</button>
) : undefined);
const resolvedType = passwordToggleLabels
? showPassword
? 'text'
: 'password'
: type;
return (
<div className="w-full">
{label && (
<label
htmlFor={inputId}
className="block text-sm font-medium text-text-secondary mb-1"
>
{label}
</label>
)}
<div className="relative">
{icon && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center text-text-muted pointer-events-none">
{icon}
</div>
)}
<input
ref={ref}
id={inputId}
type={resolvedType}
className={`
w-full rounded-[var(--radius-md)] border
${error ? 'border-red-500' : 'border-border'}
bg-background-secondary/90 text-text-primary
${icon ? 'pl-10' : 'pl-4'} ${resolvedEndIcon ? 'pr-10' : 'pr-4'} py-2.5 sm:py-2 text-base sm:text-sm
placeholder:text-text-muted
focus:outline-none focus:ring-2 focus:ring-primary/35 focus:border-border-strong
disabled:opacity-50 disabled:cursor-not-allowed
transition-all duration-200 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]
${className}
`}
{...props}
/>
{resolvedEndIcon && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-text-muted">
{resolvedEndIcon}
</div>
)}
</div>
{error && (
<p className="mt-1 text-sm text-red-500">
{error}
</p>
)}
</div>
);
}
);
Input.displayName = 'Input';