89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
'use client';
|
|
|
|
import { useId, type KeyboardEvent } from 'react';
|
|
import { Check } from 'lucide-react';
|
|
|
|
type CheckboxProps = {
|
|
checked: boolean;
|
|
onChange: (checked: boolean) => void;
|
|
disabled?: boolean;
|
|
label: string;
|
|
id?: string;
|
|
className?: string;
|
|
};
|
|
|
|
/**
|
|
* App design-system checkbox: primary fill when checked, rounded, focus-visible ring.
|
|
* Uses a button-like label toggle so mouse clicks do not focus a hidden input (scroll jumps).
|
|
*/
|
|
export function Checkbox({
|
|
checked,
|
|
onChange,
|
|
disabled = false,
|
|
label,
|
|
id,
|
|
className = '',
|
|
}: CheckboxProps) {
|
|
const genId = useId();
|
|
const inputId = id ?? genId;
|
|
|
|
const toggle = () => {
|
|
if (!disabled) onChange(!checked);
|
|
};
|
|
|
|
const onKeyDown = (event: KeyboardEvent<HTMLLabelElement>) => {
|
|
if (disabled) return;
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault();
|
|
toggle();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<label
|
|
id={inputId}
|
|
role="checkbox"
|
|
aria-checked={checked}
|
|
aria-disabled={disabled}
|
|
tabIndex={disabled ? -1 : 0}
|
|
onKeyDown={onKeyDown}
|
|
onClick={(event) => {
|
|
event.preventDefault();
|
|
toggle();
|
|
}}
|
|
onMouseDown={(event) => {
|
|
// Avoid focus-driven scroll-into-view in overflow panels.
|
|
if (event.button === 0) event.preventDefault();
|
|
}}
|
|
className={`
|
|
inline-flex items-center gap-2.5 cursor-pointer select-none rounded-[var(--radius-sm)] -m-0.5 p-0.5
|
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45 focus-visible:ring-offset-2
|
|
focus-visible:ring-offset-background-secondary
|
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
${className}
|
|
`}
|
|
>
|
|
<span
|
|
className={`
|
|
flex h-5 w-5 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border-2 transition-all duration-200
|
|
shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]
|
|
${
|
|
checked
|
|
? 'border-primary bg-primary shadow-[0_0_0_1px_rgba(9,169,188,0.25)]'
|
|
: 'border-border-strong bg-background-card/90 hover:border-border'
|
|
}
|
|
`}
|
|
aria-hidden
|
|
>
|
|
<Check
|
|
strokeWidth={3}
|
|
className={`h-3.5 w-3.5 text-primary-contrast transition-all duration-200 ${
|
|
checked ? 'scale-100 opacity-100' : 'scale-75 opacity-0'
|
|
}`}
|
|
/>
|
|
</span>
|
|
<span className="text-sm text-text-secondary">{label}</span>
|
|
</label>
|
|
);
|
|
}
|