refactor(ui): migrar componente Input a HeroUI
This commit is contained in:
1 parent
67b96442a3
commit
34e9896e15
1 file changed
+79
-84
+79
-84
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
import React, { useState } from 'react';
|
||||
import { FiEye, FiEyeOff, FiX, FiAlertCircle } from 'react-icons/fi';
|
||||
import React, { useState } from "react";
|
||||
|
||||
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||
import { Input as HeroInput, TextField, Label, Description, FieldError, InputGroup } from "@heroui/react";
|
||||
import { FiEye, FiEyeOff, FiX, FiAlertCircle } from "react-icons/fi";
|
||||
|
||||
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
|
||||
label?: string;
|
||||
helperText?: string;
|
||||
errorMessage?: string;
|
||||
@@ -10,28 +11,28 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
|
||||
rightIcon?: React.ReactNode;
|
||||
isClearable?: boolean;
|
||||
onClear?: () => void;
|
||||
sizeVariant?: 'sm' | 'md' | 'lg';
|
||||
sizeVariant?: "sm" | "md" | "lg";
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const sizeStyles = {
|
||||
sm: {
|
||||
input: 'py-2 text-xs rounded-lg',
|
||||
icon: 'text-sm',
|
||||
leftPad: 'pl-8',
|
||||
rightPad: 'pr-8',
|
||||
input: "py-2 text-xs rounded-lg",
|
||||
icon: "text-sm",
|
||||
leftPad: "pl-8",
|
||||
rightPad: "pr-8",
|
||||
},
|
||||
md: {
|
||||
input: 'py-2.5 text-sm rounded-xl',
|
||||
icon: 'text-base',
|
||||
leftPad: 'pl-10',
|
||||
rightPad: 'pr-10',
|
||||
input: "py-2.5 text-sm rounded-xl",
|
||||
icon: "text-base",
|
||||
leftPad: "pl-10",
|
||||
rightPad: "pr-10",
|
||||
},
|
||||
lg: {
|
||||
input: 'py-3.5 text-base rounded-2xl',
|
||||
icon: 'text-lg',
|
||||
leftPad: 'pl-12',
|
||||
rightPad: 'pr-12',
|
||||
input: "py-3.5 text-base rounded-2xl",
|
||||
icon: "text-lg",
|
||||
leftPad: "pl-12",
|
||||
rightPad: "pr-12",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,65 +46,73 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
rightIcon,
|
||||
isClearable = false,
|
||||
onClear,
|
||||
sizeVariant = 'md',
|
||||
sizeVariant = "md",
|
||||
fullWidth = true,
|
||||
type = 'text',
|
||||
type = "text",
|
||||
id,
|
||||
name,
|
||||
disabled,
|
||||
required,
|
||||
className = '',
|
||||
className = "",
|
||||
value,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const inputId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
|
||||
|
||||
const isPasswordType = type === 'password';
|
||||
const computedType = isPasswordType ? (showPassword ? 'text' : 'password') : type;
|
||||
const inputId = id || name || (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined);
|
||||
const isPasswordType = type === "password";
|
||||
const computedType = isPasswordType ? (showPassword ? "text" : "password") : type;
|
||||
const hasError = Boolean(errorMessage);
|
||||
const hasValue = value !== undefined && value !== '' && value !== null;
|
||||
|
||||
const hasValue = value !== undefined && value !== "" && value !== null;
|
||||
const currentSize = sizeStyles[sizeVariant];
|
||||
|
||||
return (
|
||||
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}>
|
||||
<TextField
|
||||
id={`${inputId}-field`}
|
||||
isInvalid={hasError}
|
||||
isDisabled={disabled}
|
||||
isRequired={required}
|
||||
className={`${fullWidth ? "w-full" : "inline-block"} space-y-1.5`}>
|
||||
|
||||
{/* Label */}
|
||||
{label && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
<Label
|
||||
htmlFor={inputId}
|
||||
className={`block text-xs font-semibold select-none transition-colors ${
|
||||
hasError
|
||||
? 'text-rose-600 dark:text-rose-400'
|
||||
: 'text-slate-700 dark:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
hasError ? "text-rose-600 dark:text-rose-400" : "text-slate-700 dark:text-slate-300"
|
||||
}`}>
|
||||
{label}
|
||||
|
||||
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
|
||||
</label>
|
||||
</div>
|
||||
</Label>
|
||||
)}
|
||||
|
||||
{/* Input Wrapper */}
|
||||
<div className="relative flex items-center">
|
||||
<div className="relative flex items-center w-full">
|
||||
<InputGroup
|
||||
className={`
|
||||
flex items-center w-full overflow-hidden transition-all duration-200 outline-none
|
||||
bg-white border border-slate-300 hover:border-slate-400
|
||||
focus-within:border-cyan-500 focus-within:ring-4 focus-within:ring-cyan-500/15
|
||||
dark:bg-slate-900/90 dark:border-slate-800 dark:hover:border-slate-700
|
||||
dark:focus-within:border-cyan-500 dark:focus-within:ring-cyan-500/25
|
||||
${disabled ? "opacity-60 bg-slate-100 dark:bg-slate-950 cursor-not-allowed" : ""}
|
||||
${hasError ? "border-rose-500! ring-rose-500/20! focus-within:border-rose-500! focus-within:ring-rose-500/20!" : ""}
|
||||
${currentSize.input} `}>
|
||||
|
||||
{/* Left Icon */}
|
||||
{leftIcon && (
|
||||
<div
|
||||
className={`absolute left-3.5 pointer-events-none flex items-center justify-center transition-colors ${
|
||||
hasError
|
||||
? 'text-rose-500'
|
||||
: 'text-slate-400 dark:text-slate-500'
|
||||
} ${currentSize.icon}`}
|
||||
>
|
||||
<InputGroup.Prefix
|
||||
className={`flex items-center justify-center pl-3.5 pr-2 shrink-0 pointer-events-none transition-colors ${
|
||||
hasError ? "text-rose-500" : "text-slate-400 dark:text-slate-500"
|
||||
} ${currentSize.icon}`}>
|
||||
{leftIcon}
|
||||
</div>
|
||||
</InputGroup.Prefix>
|
||||
)}
|
||||
|
||||
{/* Core Input Element */}
|
||||
<input
|
||||
<HeroInput
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
name={name}
|
||||
@@ -112,78 +121,64 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={`
|
||||
w-full px-3.5 font-normal transition-all duration-200 outline-none
|
||||
bg-white text-slate-900 placeholder:text-slate-400
|
||||
border border-slate-300
|
||||
hover:border-slate-400
|
||||
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
|
||||
dark:bg-slate-900/90 dark:text-slate-100 dark:placeholder:text-slate-500
|
||||
dark:border-slate-800 dark:hover:border-slate-700
|
||||
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
|
||||
disabled:opacity-60 disabled:bg-slate-100 disabled:dark:bg-slate-950 disabled:cursor-not-allowed
|
||||
${leftIcon ? currentSize.leftPad : ''}
|
||||
${isPasswordType || isClearable || rightIcon ? currentSize.rightPad : ''}
|
||||
${
|
||||
hasError
|
||||
? '!border-rose-500 !ring-rose-500/20 text-rose-900 dark:text-rose-200 focus:!border-rose-500 focus:!ring-rose-500/20'
|
||||
: ''
|
||||
}
|
||||
${currentSize.input}
|
||||
flex-1 w-full bg-transparent border-none outline-none focus:ring-0 p-0 m-0 shadow-none text-slate-900 placeholder:text-slate-400
|
||||
dark:text-slate-100 dark:placeholder:text-slate-500
|
||||
${hasError ? "text-rose-900 dark:text-rose-200" : ""}
|
||||
${!leftIcon ? "pl-3.5" : ""}
|
||||
${!rightIcon && !isClearable && !isPasswordType ? "pr-3.5" : ""}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{/* Right Action Icons (Clear, Password Toggle, Custom RightIcon) */}
|
||||
<div className="absolute right-3 flex items-center gap-1.5 text-slate-400 dark:text-slate-500">
|
||||
{/* Right Action Icons */}
|
||||
<InputGroup.Suffix className="flex items-center gap-1.5 pr-3 pl-1 shrink-0 text-slate-400 dark:text-slate-500">
|
||||
|
||||
{/* Clear */}
|
||||
{isClearable && hasValue && !disabled && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={onClear}
|
||||
aria-label="Limpiar campo"
|
||||
className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors"
|
||||
>
|
||||
className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors">
|
||||
<FiX className="text-sm" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Password Toggle */}
|
||||
{isPasswordType && !disabled && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
aria-label={showPassword ? 'Ocultar contraseña' : 'Ver contraseña'}
|
||||
className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors"
|
||||
>
|
||||
{showPassword ? (
|
||||
<FiEyeOff className={currentSize.icon} />
|
||||
) : (
|
||||
<FiEye className={currentSize.icon} />
|
||||
)}
|
||||
className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors">
|
||||
{showPassword ? <FiEyeOff className={currentSize.icon} /> : <FiEye className={currentSize.icon} />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Custom Right Icon */}
|
||||
{!isPasswordType && rightIcon && (
|
||||
<div className={`pointer-events-none flex items-center justify-center ${currentSize.icon}`}>
|
||||
{rightIcon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InputGroup.Suffix>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
{/* Error or Helper Message */}
|
||||
{hasError ? (
|
||||
<p className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn">
|
||||
<FieldError className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn">
|
||||
<FiAlertCircle className="shrink-0 text-xs" />
|
||||
|
||||
<span>{errorMessage}</span>
|
||||
</p>
|
||||
</FieldError>
|
||||
) : helperText ? (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{helperText}</p>
|
||||
<Description className="text-xs text-slate-500 dark:text-slate-400">{helperText}</Description>
|
||||
) : null}
|
||||
</div>
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
Input.displayName = "Input";
|
||||
Reference in new issue
Block a user