112 lines
3.7 KiB
TypeScript
112 lines
3.7 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
import { FiCheck, FiMinus } from 'react-icons/fi';
|
|
|
|
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
label?: React.ReactNode;
|
|
description?: string;
|
|
errorMessage?: string;
|
|
indeterminate?: boolean;
|
|
}
|
|
|
|
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);
|
|
|
|
useEffect(() => {
|
|
if (resolvedRef.current) {
|
|
resolvedRef.current.indeterminate = indeterminate;
|
|
}
|
|
}, [indeterminate, resolvedRef]);
|
|
|
|
const isChecked = Boolean(checked);
|
|
|
|
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' : ''
|
|
}`}
|
|
>
|
|
{/* 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}
|
|
/>
|
|
|
|
<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>
|
|
</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>
|
|
);
|
|
}
|
|
);
|
|
|
|
Checkbox.displayName = 'Checkbox';
|