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 />}>
Con ícono
</Badge>
<Badge variant="primary" removable onRemove={() => {}}>
<Badge variant="primary" removable onRemove={() => { }}>
Removable
</Badge>
</Row>
@@ -279,15 +279,15 @@ export default function ComponentsContent() {
<Row>
<Checkbox
label="Recordar sesión"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
isSelected={checked}
onChange={(isSelected) => setChecked(isSelected)}
/>
<Checkbox label="Deshabilitado" disabled />
<Checkbox
label="Marcado fijo"
checked
disabled
onChange={() => {}}
onChange={() => { }}
/>
</Row>
<Row>
@@ -302,8 +302,8 @@ export default function ComponentsContent() {
<CodeBlock
code={`<Checkbox
label="Recordar sesión"
checked={checked}
onChange={e => setChecked(e.target.checked)}
isSelected={checked}
onChange={(isSelected) => setChecked(isSelected)}
/>
<Switch
+132 -96
View File
@@ -1,112 +1,148 @@
"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;
description?: string;
errorMessage?: string;
checked?: boolean;
disabled?: boolean;
indeterminate?: boolean;
className?: string;
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
(
{
label,
description,
errorMessage,
indeterminate = false,
checked,
disabled,
className = '',
id,
name,
...props
},
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);
export const Checkbox = ({
label,
description,
errorMessage,
indeterminate = false,
checked,
isSelected,
disabled = false,
className = "",
id,
name,
onChange,
...props
}: CheckboxProps) => {
return (
<HeroUICheckbox
{...props}
id={id}
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(() => {
if (resolvedRef.current) {
resolvedRef.current.indeterminate = indeterminate;
}
}, [indeterminate, resolvedRef]);
// Estado normal
"bg-white dark:bg-slate-900",
"border-slate-300 dark:border-slate-700",
const isChecked = Boolean(checked);
// Hover
"group-hover:border-slate-400",
"dark:group-hover:border-slate-500",
return (
<div className={`flex flex-col space-y-1 ${className}`}>
<label
htmlFor={checkboxId}
className={`inline-flex items-start gap-2.5 select-none cursor-pointer group ${
disabled ? 'opacity-50 !cursor-not-allowed' : ''
}`}
// 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(" ")}
>
{/* 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}
/>
<HeroUICheckbox.Indicator>
{({ isSelected, isIndeterminate }) => {
if (isIndeterminate) {
return <FiMinus className="text-xs stroke-3" />;
}
<div
className={`
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
peer-focus-visible:ring-cyan-500 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-950
${
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 ? (
<FiMinus className="text-xs stroke-3" />
) : isChecked ? (
<FiCheck className="text-xs stroke-3" />
) : null}
</div>
if (isSelected) {
return <FiCheck className="text-xs stroke-3" />;
}
return null;
}}
</HeroUICheckbox.Indicator>
</HeroUICheckbox.Control>
{(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 && (
<Description
className="
text-xs
text-slate-500 dark:text-slate-400
"
>
{description}
</Description>
)}
</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";