refactor(ui): migrar componente Checkbox a HeroUI

This commit is contained in:
Miguel Ortega committed 2026-09-11 21:37:46 -05:00
1 parent e399f22007
commit 952f30ea19
2 files changed
+118 -82

No files matched your search

+6 -6
View File
@@ -163,7 +163,7 @@ export default function ComponentsContent() {
<Badge variant="danger" icon={<FiAlertCircle />}> <Badge variant="danger" icon={<FiAlertCircle />}>
Con ícono Con ícono
</Badge> </Badge>
<Badge variant="primary" removable onRemove={() => {}}> <Badge variant="primary" removable onRemove={() => { }}>
Removable Removable
</Badge> </Badge>
</Row> </Row>
@@ -279,15 +279,15 @@ export default function ComponentsContent() {
<Row> <Row>
<Checkbox <Checkbox
label="Recordar sesión" label="Recordar sesión"
checked={checked} isSelected={checked}
onChange={(e) => setChecked(e.target.checked)} onChange={(isSelected) => setChecked(isSelected)}
/> />
<Checkbox label="Deshabilitado" disabled /> <Checkbox label="Deshabilitado" disabled />
<Checkbox <Checkbox
label="Marcado fijo" label="Marcado fijo"
checked checked
disabled disabled
onChange={() => {}} onChange={() => { }}
/> />
</Row> </Row>
<Row> <Row>
@@ -302,8 +302,8 @@ export default function ComponentsContent() {
<CodeBlock <CodeBlock
code={`<Checkbox code={`<Checkbox
label="Recordar sesión" label="Recordar sesión"
checked={checked} isSelected={checked}
onChange={e => setChecked(e.target.checked)} onChange={(isSelected) => setChecked(isSelected)}
/> />
<Switch <Switch
+112 -76
View File
@@ -1,112 +1,148 @@
"use client"; "use client";
import React, { useEffect, useRef } from 'react';
import { FiCheck, FiMinus } from 'react-icons/fi';
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> { import React from "react";
import {
Checkbox as HeroUICheckbox,
Description,
FieldError,
} from "@heroui/react";
import { FiCheck, FiMinus } from "react-icons/fi";
export interface CheckboxProps
extends Omit<
React.ComponentProps<typeof HeroUICheckbox>,
"children" | "defaultSelected" | "isDisabled" | "isInvalid" | "isIndeterminate"
> {
label?: React.ReactNode; label?: React.ReactNode;
description?: string; description?: string;
errorMessage?: string; errorMessage?: string;
checked?: boolean;
disabled?: boolean;
indeterminate?: boolean; indeterminate?: boolean;
className?: string;
} }
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>( export const Checkbox = ({
(
{
label, label,
description, description,
errorMessage, errorMessage,
indeterminate = false, indeterminate = false,
checked, checked,
disabled, isSelected,
className = '', disabled = false,
className = "",
id, id,
name, name,
onChange,
...props ...props
}, }: CheckboxProps) => {
forwardedRef
) => {
const internalRef = useRef<HTMLInputElement>(null);
const resolvedRef = (forwardedRef as React.RefObject<HTMLInputElement>) || internalRef;
const checkboxId = id || name || (typeof label === 'string' ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
useEffect(() => {
if (resolvedRef.current) {
resolvedRef.current.indeterminate = indeterminate;
}
}, [indeterminate, resolvedRef]);
const isChecked = Boolean(checked);
return ( return (
<div className={`flex flex-col space-y-1 ${className}`}> <HeroUICheckbox
<label
htmlFor={checkboxId}
className={`inline-flex items-start gap-2.5 select-none cursor-pointer group ${
disabled ? 'opacity-50 !cursor-not-allowed' : ''
}`}
>
{/* Custom Styled Box */}
<div className="relative flex items-center justify-center mt-0.5 shrink-0">
<input
ref={resolvedRef}
id={checkboxId}
name={name}
type="checkbox"
checked={checked}
disabled={disabled}
className="peer sr-only"
{...props} {...props}
/> id={id}
name={name}
<div isSelected={checked}
className={` isDisabled={disabled}
w-4 h-4 rounded-md flex items-center justify-center transition-all duration-200 isIndeterminate={indeterminate}
border isInvalid={Boolean(errorMessage)}
peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 onChange={onChange}
peer-focus-visible:ring-cyan-500 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-950 className={className}
${
isChecked || indeterminate
? 'bg-cyan-500 border-cyan-500 text-slate-950 shadow-sm shadow-cyan-500/20'
: 'bg-white dark:bg-slate-900 border-slate-300 dark:border-slate-700 text-transparent group-hover:border-slate-400 dark:group-hover:border-slate-500'
}
${
errorMessage
? '!border-rose-500 !bg-rose-50 dark:!bg-rose-950/30'
: ''
}
`}
> >
{indeterminate ? ( <HeroUICheckbox.Content className="items-start gap-2.5">
<FiMinus className="text-xs stroke-3" /> <HeroUICheckbox.Control
) : isChecked ? ( className={[
<FiCheck className="text-xs stroke-3" /> "mt-0.5 shrink-0 size-4 rounded-md",
) : null} "border",
</div> "transition-all duration-200",
</div>
// Estado normal
"bg-white dark:bg-slate-900",
"border-slate-300 dark:border-slate-700",
// Hover
"group-hover:border-slate-400",
"dark:group-hover:border-slate-500",
// Focus
"data-[focus-visible=true]:ring-2",
"data-[focus-visible=true]:ring-cyan-500",
"data-[focus-visible=true]:ring-offset-2",
"data-[focus-visible=true]:ring-offset-white",
"dark:data-[focus-visible=true]:ring-offset-slate-950",
// Seleccionado
"data-[selected=true]:bg-cyan-500",
"data-[selected=true]:border-cyan-500",
"data-[selected=true]:text-slate-950",
"data-[selected=true]:shadow-sm",
"data-[selected=true]:shadow-cyan-500/20",
// Indeterminado
"data-[indeterminate=true]:bg-cyan-500",
"data-[indeterminate=true]:border-cyan-500",
"data-[indeterminate=true]:text-slate-950",
// Error
errorMessage
? "data-[invalid=true]:border-rose-500"
: "",
]
.filter(Boolean)
.join(" ")}
>
<HeroUICheckbox.Indicator>
{({ isSelected, isIndeterminate }) => {
if (isIndeterminate) {
return <FiMinus className="text-xs stroke-3" />;
}
if (isSelected) {
return <FiCheck className="text-xs stroke-3" />;
}
return null;
}}
</HeroUICheckbox.Indicator>
</HeroUICheckbox.Control>
{/* Label and Sub-description */}
{(label || description) && ( {(label || description) && (
<div className="flex flex-col text-left"> <div className="flex flex-col text-left">
{label && ( {label && (
<span className="text-xs sm:text-sm font-medium text-slate-800 dark:text-slate-200 group-hover:text-slate-950 dark:group-hover:text-white transition-colors"> <span
className="
text-xs sm:text-sm font-medium
text-slate-800 dark:text-slate-200
group-hover:text-slate-950
dark:group-hover:text-white
transition-colors
"
>
{label} {label}
</span> </span>
)} )}
{description && ( {description && (
<span className="text-xs text-slate-500 dark:text-slate-400"> <Description
className="
text-xs
text-slate-500 dark:text-slate-400
"
>
{description} {description}
</span> </Description>
)} )}
</div> </div>
)} )}
</label> </HeroUICheckbox.Content>
{errorMessage && ( {errorMessage && (
<p className="text-xs text-rose-600 dark:text-rose-400 pl-6.5">{errorMessage}</p> <FieldError className="pl-6.5 text-xs">
{errorMessage}
</FieldError>
)} )}
</div> </HeroUICheckbox>
); );
} };
);
Checkbox.displayName = 'Checkbox'; Checkbox.displayName = "Checkbox";