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
+138 -102

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
+132 -96
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,
{ description,
label, errorMessage,
description, indeterminate = false,
errorMessage, checked,
indeterminate = false, isSelected,
checked, disabled = false,
disabled, className = "",
className = '', id,
id, name,
name, onChange,
...props ...props
}, }: CheckboxProps) => {
forwardedRef return (
) => { <HeroUICheckbox
const internalRef = useRef<HTMLInputElement>(null); {...props}
const resolvedRef = (forwardedRef as React.RefObject<HTMLInputElement>) || internalRef; id={id}
const checkboxId = id || name || (typeof label === 'string' ? label.toLowerCase().replace(/\s+/g, '-') : undefined); name={name}
isSelected={checked}
isDisabled={disabled}
isIndeterminate={indeterminate}
isInvalid={Boolean(errorMessage)}
onChange={onChange}
className={className}
>
<HeroUICheckbox.Content className="items-start gap-2.5">
<HeroUICheckbox.Control
className={[
"mt-0.5 shrink-0 size-4 rounded-md",
"border",
"transition-all duration-200",
useEffect(() => { // Estado normal
if (resolvedRef.current) { "bg-white dark:bg-slate-900",
resolvedRef.current.indeterminate = indeterminate; "border-slate-300 dark:border-slate-700",
}
}, [indeterminate, resolvedRef]);
const isChecked = Boolean(checked); // Hover
"group-hover:border-slate-400",
"dark:group-hover:border-slate-500",
return ( // Focus
<div className={`flex flex-col space-y-1 ${className}`}> "data-[focus-visible=true]:ring-2",
<label "data-[focus-visible=true]:ring-cyan-500",
htmlFor={checkboxId} "data-[focus-visible=true]:ring-offset-2",
className={`inline-flex items-start gap-2.5 select-none cursor-pointer group ${ "data-[focus-visible=true]:ring-offset-white",
disabled ? 'opacity-50 !cursor-not-allowed' : '' "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(" ")}
> >
{/* Custom Styled Box */} <HeroUICheckbox.Indicator>
<div className="relative flex items-center justify-center mt-0.5 shrink-0"> {({ isSelected, isIndeterminate }) => {
<input if (isIndeterminate) {
ref={resolvedRef} return <FiMinus className="text-xs stroke-3" />;
id={checkboxId} }
name={name}
type="checkbox"
checked={checked}
disabled={disabled}
className="peer sr-only"
{...props}
/>
<div if (isSelected) {
className={` return <FiCheck className="text-xs stroke-3" />;
w-4 h-4 rounded-md flex items-center justify-center transition-all duration-200 }
border
peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 return null;
peer-focus-visible:ring-cyan-500 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-950 }}
${ </HeroUICheckbox.Indicator>
isChecked || indeterminate </HeroUICheckbox.Control>
? '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' {(label || description) && (
} <div className="flex flex-col text-left">
${ {label && (
errorMessage <span
? '!border-rose-500 !bg-rose-50 dark:!bg-rose-950/30' 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
{indeterminate ? ( transition-colors
<FiMinus className="text-xs stroke-3" /> "
) : isChecked ? ( >
<FiCheck className="text-xs stroke-3" /> {label}
) : null} </span>
</div> )}
{description && (
<Description
className="
text-xs
text-slate-500 dark:text-slate-400
"
>
{description}
</Description>
)}
</div> </div>
{/* Label and Sub-description */}
{(label || description) && (
<div className="flex flex-col text-left">
{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">
{label}
</span>
)}
{description && (
<span className="text-xs text-slate-500 dark:text-slate-400">
{description}
</span>
)}
</div>
)}
</label>
{errorMessage && (
<p className="text-xs text-rose-600 dark:text-rose-400 pl-6.5">{errorMessage}</p>
)} )}
</div> </HeroUICheckbox.Content>
);
}
);
Checkbox.displayName = 'Checkbox'; {errorMessage && (
<FieldError className="pl-6.5 text-xs">
{errorMessage}
</FieldError>
)}
</HeroUICheckbox>
);
};
Checkbox.displayName = "Checkbox";