Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | 41x 41x 41x 1123x 1123x 1123x 1123x 1056x 1056x 2x 1054x 2x 1123x 530x 530x 1123x 1123x 50x 1073x 41x | /**
* Checkbox — a ruled 2px-radius box that fills with the laminate-teal field
* when ticked, with the mark printed in inverse ink.
*
* A real `<input type="checkbox">` under the paint: space toggles it, the
* platform reports it as a checkbox, and `indeterminate` is the DOM property
* (the only way to express a tri-state checkbox that assistive tech reads).
* Keyboard focus shows the portal focus ring. The mockups mark focus in
* index red, but that token has no Earthy override yet and
* `src/styles.test.ts` forbids a consumer before the reskin.
*
* Pass `children` for the common "box + label" row — the whole row becomes the
* hit target. Omit them for a bare box (a table's select-all, a photo tile),
* and give it an `aria-label`.
*/
import { cva, type VariantProps } from "class-variance-authority";
import { Check, Minus } from "lucide-react";
import {
forwardRef,
type InputHTMLAttributes,
type ReactNode,
type Ref,
useCallback,
useEffect,
useId,
useRef,
} from "react";
import { cn } from "../../lib/utils";
const checkboxVariants = cva(
"peer size-4 shrink-0 cursor-pointer appearance-none rounded-sm border bg-background-elevated transition-colors checked:border-primary-500 checked:bg-primary-500 indeterminate:border-primary-500 indeterminate:bg-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
invalid: {
true: "border-error",
// The portal's fields resolve their stroke through --border-strong,
// 3:1 against both grounds (WCAG 1.4.11, UI component boundary).
// --border-strong is 1.67:1 light / 1.91:1 dark and is NOT a legal
// field stroke; it was a placeholder while the Earthy override
// still shadowed the package tokens.
false: "border-input",
},
},
defaultVariants: {
invalid: false,
},
},
);
interface CheckboxProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "size">,
VariantProps<typeof checkboxVariants> {
/** Tri-state: sets the DOM `indeterminate` property and prints a dash. */
indeterminate?: boolean;
/** Label printed to the right of the box; makes the whole row clickable. */
children?: ReactNode;
}
const markClasses =
"pointer-events-none absolute size-3 text-foreground-inverse";
const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(
{
className,
invalid,
indeterminate = false,
children,
disabled,
id,
...props
},
ref,
) => {
const generatedId = useId();
// The label wraps the input AND points at it: nesting alone is enough
// for a browser, but the explicit pairing is what a screen reader and a
// static a11y check both read without guessing.
const inputId = id ?? generatedId;
const innerRef = useRef<HTMLInputElement | null>(null);
const setRefs = useCallback(
(node: HTMLInputElement | null) => {
innerRef.current = node;
if (typeof ref === "function") {
ref(node);
} else if (ref) {
(ref as { current: HTMLInputElement | null }).current = node;
}
},
[ref],
);
useEffect(() => {
Eif (innerRef.current) {
innerRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);
const box = (
<span className="relative inline-flex items-center justify-center">
<input
type="checkbox"
id={inputId}
ref={setRefs as Ref<HTMLInputElement>}
disabled={disabled}
aria-invalid={invalid ? true : undefined}
className={cn(checkboxVariants({ invalid }), className)}
{...props}
/>
{indeterminate ? (
<Minus aria-hidden="true" className={markClasses} />
) : (
<Check
aria-hidden="true"
className={cn(markClasses, "opacity-0 peer-checked:opacity-100")}
/>
)}
</span>
);
if (children === undefined) {
return box;
}
return (
<label
htmlFor={inputId}
className={cn(
"inline-flex items-center gap-2 text-sm text-foreground-default",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
)}
>
{box}
<span>{children}</span>
</label>
);
},
);
Checkbox.displayName = "Checkbox";
export type { CheckboxProps };
export { Checkbox, checkboxVariants };
|