new-personal-site/components/lib/form/Checkbox.tsx
Benjamin Toby a0a0ab8ee4 Updates
2025-07-20 10:35:54 +01:00

138 lines
4.2 KiB
TypeScript

import React, {
ComponentProps,
DetailedHTMLProps,
HTMLAttributes,
ReactNode,
} from "react";
import { twMerge } from "tailwind-merge";
import CheckMarkSVG from "../svgs/CheckMarkSVG";
import Stack from "../layout/Stack";
import Row from "../layout/Row";
import { Info } from "lucide-react";
import Span from "../layout/Span";
export type CheckboxProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
wrapperProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
label?: string | ReactNode;
labelProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
defaultChecked?: boolean;
wrapperClassName?: string;
setChecked?: React.Dispatch<React.SetStateAction<boolean>>;
checked?: boolean;
readOnly?: boolean;
size?: number;
changeHandler?: (value: boolean) => void;
info?: string | ReactNode;
wrapperWrapperProps?: ComponentProps<typeof Stack>;
};
/**
* # Checkbox Component
* @className twui-checkbox
* @className twui-checkbox-checked
* @className twui-checkbox-unchecked
*/
export default function Checkbox({
wrapperProps,
label,
labelProps,
size,
wrapperClassName,
defaultChecked,
setChecked: externalSetChecked,
readOnly,
checked: externalChecked,
changeHandler,
info,
wrapperWrapperProps,
...props
}: CheckboxProps) {
const finalSize = size || 20;
const [checked, setChecked] = React.useState(
defaultChecked || externalChecked || false
);
const finalTitle = props.title
? props.title
: `Checkbox-${Math.round(Math.random() * 100000)}`;
React.useEffect(() => {
if (typeof externalChecked == "undefined") return;
setChecked(externalChecked);
}, [externalChecked]);
React.useEffect(() => {
changeHandler?.(checked);
}, [checked]);
return (
<Stack
{...wrapperWrapperProps}
className={twMerge("gap-1.5", wrapperWrapperProps?.className)}
>
<div
{...wrapperProps}
className={twMerge(
"flex items-start md:items-center gap-2 flex-wrap md:flex-nowrap",
readOnly ? "opacity-70 pointer-events-none" : "",
wrapperClassName,
wrapperProps?.className
)}
onClick={() => {
setChecked(!checked);
externalSetChecked?.(!checked);
}}
>
<div
{...props}
className={twMerge(
"flex items-center justify-center p-[3px] rounded-default",
checked
? "bg-primary twui-checkbox-checked text-white outline-slate-400"
: "dark:outline-white/50 outline-2 -outline-offset-2 twui-checkbox-unchecked",
"twui-checkbox",
props.className
)}
style={{
minWidth: finalSize + "px",
width: finalSize + "px",
height: finalSize + "px",
...props.style,
}}
>
{checked && <CheckMarkSVG />}
</div>
<Stack className="gap-0.5">
<div
{...labelProps}
className={twMerge(
"select-none whitespace-normal md:whitespace-nowrap",
labelProps?.className
)}
>
{label || finalTitle}
</div>
</Stack>
</div>
{info && (
<Row className="gap-1" title={info.toString()}>
<Info size={12} className="opacity-40" />
<Span size="smaller" className="opacity-70">
{info}
</Span>
</Row>
)}
</Stack>
);
}