g1t/apps/web/app/components/ui/checkbox.tsx
| 1 | import { Check, Minus } from "lucide-react"; |
| 2 | import { Checkbox as Primitive } from "radix-ui"; |
| 3 | import { type ComponentProps, type ReactNode, useId } from "react"; |
| 4 | |
| 5 | import { cn } from "../../lib/cn"; |
| 6 | |
| 7 | // shadcn/ui's checkbox, styled with g1t's tokens. |
| 8 | // |
| 9 | // Inside a <Form>, a `name` makes Radix render a hidden checkbox beside it, |
| 10 | // so it posts `value` (default "on") when checked and nothing when not, |
| 11 | // exactly as a native one does. Wrap it and its text in a <label>, or point |
| 12 | // a <Label htmlFor> at its `id`. |
| 13 | |
| 14 | /** A checkbox with its label, and optionally a line under it. */ |
| 15 | export function CheckboxOption({ |
| 16 | label, |
| 17 | description, |
| 18 | className, |
| 19 | labelClassName, |
| 20 | ...props |
| 21 | }: ComponentProps<typeof Primitive.Root> & { label: ReactNode; description?: ReactNode; labelClassName?: string }) { |
| 22 | const generated = useId(); |
| 23 | const id = props.id ?? generated; |
| 24 | return ( |
| 25 | <div className={cn("flex items-start gap-2.5", props.disabled && "opacity-60", className)}> |
| 26 | <Checkbox id={id} className={className?.includes("items-center") ? undefined : "mt-0.5"} aria-describedby={description ? `${id}-d` : undefined} {...props} /> |
| 27 | <label htmlFor={id} className={cn("min-w-0 text-sm text-fg", props.disabled ? "cursor-not-allowed" : "cursor-pointer", labelClassName)}> |
| 28 | {label} |
| 29 | {description && ( |
| 30 | <span id={`${id}-d`} className="mt-0.5 block text-xs text-faint"> |
| 31 | {description} |
| 32 | </span> |
| 33 | )} |
| 34 | </label> |
| 35 | </div> |
| 36 | ); |
| 37 | } |
| 38 | |
| 39 | export function Checkbox({ className, ...props }: ComponentProps<typeof Primitive.Root>) { |
| 40 | return ( |
| 41 | <Primitive.Root |
| 42 | className={cn( |
| 43 | "peer inline-flex size-4 shrink-0 items-center justify-center rounded-[5px] border border-line-strong bg-bg text-bg outline-none transition-colors", |
| 44 | "hover:border-faint focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:ring-offset-1 focus-visible:ring-offset-bg focus-visible:outline-none", |
| 45 | "data-[state=checked]:border-accent data-[state=checked]:bg-accent data-[state=indeterminate]:border-accent data-[state=indeterminate]:bg-accent", |
| 46 | "disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-danger/70", |
| 47 | className, |
| 48 | )} |
| 49 | {...props} |
| 50 | > |
| 51 | <Primitive.Indicator className="flex items-center justify-center data-[state=checked]:animate-pop-in motion-reduce:animate-none"> |
| 52 | {props.checked === "indeterminate" ? ( |
| 53 | <Minus size={12} strokeWidth={3} /> |
| 54 | ) : ( |
| 55 | <Check size={12} strokeWidth={3} /> |
| 56 | )} |
| 57 | </Primitive.Indicator> |
| 58 | </Primitive.Root> |
| 59 | ); |
| 60 | } |