Files
2026-07-13 13:32:57 +08:00

200 lines
6.7 KiB
TypeScript

import * as React from "react";
import { forwardRef, useEffect, useState } from "react";
import { cn } from "~/utils/cn";
import { Badge } from "./Badge";
import { Paragraph } from "./Paragraph";
const variants = {
"simple/small": {
button: "w-fit pr-4",
label: "text-sm text-text-bright mt-0.5 select-none",
description: "text-text-dimmed",
inputPosition: "mt-1",
isChecked: "",
isDisabled: "opacity-70",
},
simple: {
button: "w-fit pr-4",
label: "text-text-bright select-none",
description: "text-text-dimmed",
inputPosition: "mt-1",
isChecked: "",
isDisabled: "opacity-70",
},
"button/small": {
button:
"flex items-center w-fit h-8 pl-2 pr-3 rounded border border-border-bright hover:bg-background-dimmed hover:border-border-brightest transition",
label: "text-sm text-text-bright select-none",
description: "text-text-dimmed",
inputPosition: "mt-0",
isChecked: "bg-background-dimmed border-grid-dimmed hover:bg-background-dimmed!",
isDisabled: "opacity-70 hover:bg-transparent",
},
button: {
button:
"w-fit py-2 pl-3 pr-4 rounded border border-border-bright hover:bg-background-dimmed hover:border-border-brightest transition",
label: "text-text-bright select-none",
description: "text-text-dimmed",
inputPosition: "mt-1",
isChecked: "bg-background-dimmed border-grid-dimmed hover:bg-background-dimmed!",
isDisabled: "opacity-70 hover:bg-transparent",
},
description: {
button: "w-full py-2 pl-3 pr-4 checked:hover:bg-background-dimmed transition",
label: "text-text-bright font-semibold",
description: "text-text-dimmed",
inputPosition: "mt-1",
isChecked: "bg-background-dimmed",
isDisabled: "opacity-70",
},
};
export type CheckboxProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"checked" | "onChange"
> & {
id?: string;
name?: string;
value?: string;
variant?: keyof typeof variants;
label: React.ReactNode;
description?: string;
badges?: string[];
className?: string;
labelClassName?: string;
onChange?: (isChecked: boolean) => void;
};
export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProps>(
(
{
id,
name,
value,
variant = "simple",
type,
label,
description,
defaultChecked,
badges,
disabled,
className,
labelClassName: externalLabelClassName,
...props
},
ref
) => {
const [isChecked, setIsChecked] = useState<boolean>(defaultChecked ?? false);
const [isDisabled, setIsDisabled] = useState<boolean>(disabled ?? false);
const buttonClassName = variants[variant].button;
const labelClassName = variants[variant].label;
const descriptionClassName = variants[variant].description;
const isCheckedClassName = variants[variant].isChecked;
const isDisabledClassName = variants[variant].isDisabled;
const inputPositionClasses = variants[variant].inputPosition;
useEffect(() => {
setIsDisabled(disabled ?? false);
}, [disabled]);
useEffect(() => {
if (props.onChange) {
props.onChange(isChecked);
}
}, [isChecked]);
useEffect(() => {
setIsChecked(defaultChecked ?? false);
}, [defaultChecked]);
return (
<div
className={cn(
"group flex items-start gap-x-2 transition ",
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
buttonClassName,
isChecked && isCheckedClassName,
(isDisabled || props.readOnly) && isDisabledClassName,
className
)}
onClick={(e) => {
//returning false is not setting the state to false, it stops the event from bubbling up
if (isDisabled || props.readOnly === true) return false;
setIsChecked((c) => !c);
}}
>
<input
{...props}
name={name}
type="checkbox"
value={value}
checked={isChecked}
onChange={(e) => {
//returning false is not setting the state to false, it stops the event from bubbling up
if (isDisabled || props.readOnly === true) return false;
setIsChecked(!isChecked);
}}
disabled={isDisabled}
className={cn(
inputPositionClasses,
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
"read-only:border-border-bright disabled:border-border-bright rounded-sm border border-border-bright bg-transparent transition checked:bg-indigo-500! read-only:bg-background-raised! group-hover:bg-background-deep checked:group-hover:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-hidden focus-visible:ring-indigo-500 disabled:bg-background-raised!"
)}
id={id}
ref={ref}
/>
<div>
<div className="flex items-center gap-x-2">
<label
htmlFor={id}
className={cn(
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
labelClassName,
externalLabelClassName
)}
onClick={(e) => e.preventDefault()}
>
{label}
</label>
{badges && (
<span className="-mr-2 flex gap-x-1.5">
{badges.map((badge) => (
<Badge key={badge}>{badge}</Badge>
))}
</span>
)}
</div>
{variant === "description" && (
<Paragraph variant="small" className={cn("mt-0.5", descriptionClassName)}>
{description}
</Paragraph>
)}
</div>
</div>
);
}
);
type SimpleCheckboxProps = Omit<React.ComponentProps<"input">, "type">;
export const Checkbox = forwardRef<HTMLInputElement, SimpleCheckboxProps>(
({ className, ...props }: SimpleCheckboxProps, ref) => {
return (
<input
type="checkbox"
className={cn(
props.disabled
? "cursor-not-allowed"
: props.readOnly
? "cursor-default"
: "cursor-pointer",
"read-only:border-border-bright disabled:border-border-bright disabled:opacity-50 rounded-sm border border-border-bright bg-transparent transition checked:bg-indigo-500! read-only:bg-background-raised! group-hover:bg-background-deep checked:group-hover:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-hidden focus-visible:ring-indigo-500 disabled:bg-background-raised!"
)}
{...props}
ref={ref}
/>
);
}
);