refactor(ui): migrar componente Select a HeroUI

This commit is contained in:
Miguel Ortega committed 2026-09-14 20:38:55 -05:00
1 parent 0beb12e6ad
commit b56fe10cbe
2 files changed
+137 -103

No files matched your search

+1 -2
View File
@@ -83,8 +83,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
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>}
{required}
</Label>
)}
+136 -101
View File
@@ -1,6 +1,9 @@
"use client";
import React from 'react';
import { FiChevronDown, FiAlertCircle } from 'react-icons/fi';
import React from "react";
import { Select as HeroSelect, Label, Description, FieldError, ListBox, ListBoxItem } from "@heroui/react";
import type { Key } from "@heroui/react";
import { FiChevronDown, FiAlertCircle } from "react-icons/fi";
export interface SelectOption {
value: string | number;
@@ -8,158 +11,190 @@ export interface SelectOption {
disabled?: boolean;
}
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
export interface SelectProps {
label?: string;
helperText?: string;
errorMessage?: string;
leftIcon?: React.ReactNode;
options?: SelectOption[];
sizeVariant?: 'sm' | 'md' | 'lg';
sizeVariant?: "sm" | "md" | "lg";
fullWidth?: boolean;
id?: string;
name?: string;
disabled?: boolean;
required?: boolean;
value?: string | number;
defaultValue?: string | number;
onChange?: (value: string | null) => void;
className?: string;
children?: React.ReactNode;
}
const sizeStyles = {
sm: {
select: 'py-2 text-xs rounded-lg',
icon: 'text-sm',
leftPad: 'pl-8',
rightPad: 'pr-8',
select: "py-2 text-xs rounded-lg",
icon: "text-sm",
leftPad: "pl-8",
rightPad: "pr-8",
},
md: {
select: 'py-2.5 text-sm rounded-xl',
icon: 'text-base',
leftPad: 'pl-10',
rightPad: 'pr-10',
select: "py-2.5 text-sm rounded-xl",
icon: "text-base",
leftPad: "pl-10",
rightPad: "pr-10",
},
lg: {
select: 'py-3.5 text-base rounded-2xl',
icon: 'text-lg',
leftPad: 'pl-12',
rightPad: 'pr-12',
select: "py-3.5 text-base rounded-2xl",
icon: "text-lg",
leftPad: "pl-12",
rightPad: "pr-12",
},
};
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
export const Select = React.forwardRef<HTMLDivElement, SelectProps>(
(
{
label,
helperText,
errorMessage,
leftIcon,
options,
sizeVariant = 'md',
options = [],
sizeVariant = "md",
fullWidth = true,
id,
name,
disabled,
required,
className = '',
className = "",
value,
defaultValue,
onChange,
children,
...props
},
ref
ref,
) => {
const selectId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
const selectId = id || name || (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined);
const hasError = Boolean(errorMessage);
const currentSize = sizeStyles[sizeVariant];
const selectedKey = value !== undefined && value !== null && value !== "" ? String(value) : undefined;
const defaultSelectedKey =
defaultValue !== undefined && defaultValue !== null && defaultValue !== "" ? String(defaultValue) : undefined;
const handleSelectionChange = (key: Key | null) => {
if (!onChange) return;
onChange(key !== null ? String(key) : null);
};
return (
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}>
{/* Label */}
{label && (
<div className="flex items-center justify-between">
<label
htmlFor={selectId}
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'
}`}
>
{label}
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
</label>
</div>
)}
{/* Select Wrapper */}
<div className="relative flex items-center">
{/* 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}`}
>
{leftIcon}
</div>
)}
{/* Core Select Element */}
<select
<div className={`${fullWidth ? "w-full" : "inline-block"} space-y-1.5`}>
<HeroSelect
ref={ref}
id={selectId}
name={name}
disabled={disabled}
required={required}
selectedKey={selectedKey}
defaultSelectedKey={defaultSelectedKey}
onSelectionChange={handleSelectionChange}
isDisabled={disabled}
isRequired={required}
isInvalid={hasError}
className="w-full">
{label && (
<Label
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"
}`}>
{label}
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
</Label>
)}
<HeroSelect.Trigger
className={`
w-full px-3.5 font-normal transition-all duration-200 outline-none appearance-none cursor-pointer
relative flex w-full items-center
font-normal transition-all duration-200
outline-none appearance-none cursor-pointer
bg-white text-slate-900
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: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 : ''}
${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'
: ''
}
focus:border-cyan-500
focus:ring-4
focus:ring-cyan-500/15
dark:bg-slate-900/90
dark:text-slate-100
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
${currentSize.select}
${leftIcon ? currentSize.leftPad : ""}
${currentSize.rightPad}
${hasError ? "border-rose-500! ring-rose-500/20! text-rose-900 dark:text-rose-200" : ""}
${className}
`}
{...props}
>
{options
? options.map((opt) => (
<option
key={opt.value}
value={opt.value}
disabled={opt.disabled}
className="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
>
{opt.label}
</option>
))
: children}
</select>
`}>
{leftIcon && (
<span
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}
</span>
)}
{/* Right Chevron Icon */}
<div
className={`absolute right-3.5 pointer-events-none flex items-center justify-center text-slate-400 dark:text-slate-500 ${currentSize.icon}`}
>
<HeroSelect.Value className="w-full text-left" />
<HeroSelect.Indicator
className={`
absolute right-3.5
pointer-events-none
flex items-center justify-center
text-slate-400 dark:text-slate-500
${currentSize.icon}
`}>
<FiChevronDown />
</div>
</div>
</HeroSelect.Indicator>
</HeroSelect.Trigger>
<HeroSelect.Popover className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-xl overflow-hidden z-50 min-w-(--trigger-width)">
<ListBox className="p-1 outline-none">
{options.length > 0
? options.map((option) => (
<ListBoxItem
key={String(option.value)}
id={String(option.value)}
textValue={option.label}
isDisabled={option.disabled}
className="px-3 py-2 text-sm rounded-lg cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 data-[selected=true]:bg-cyan-500/10 data-[selected=true]:text-cyan-600 dark:data-[selected=true]:text-cyan-400 outline-none transition-colors">
{option.label}
</ListBoxItem>
))
: (children as any)}
</ListBox>
</HeroSelect.Popover>
{/* 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">
<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}
</HeroSelect>
</div>
);
}
},
);
Select.displayName = 'Select';
Select.displayName = "Select";