refactor(ui): migrar componente Input a HeroUI

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