RoleImpersonationRadio.tsx76 lines · main
1import { Check, Minus } from 'lucide-react'
2import { cn } from 'ui'
3
4export interface RoleImpersonationRadioProps<T extends string> {
5 label?: string
6 description?: string
7 value: T
8 isSelected: boolean | 'partially'
9 onSelectedChange: (value: T) => void
10 icon?: React.ReactNode
11 fullWidth?: boolean
12}
13
14export function RoleImpersonationRadio<T extends string>({
15 label,
16 description,
17 value,
18 isSelected,
19 onSelectedChange,
20 icon,
21 fullWidth = false,
22}: RoleImpersonationRadioProps<T>) {
23 return (
24 <label
25 className={cn(
26 'border border-default rounded-md bg-surface-200 hover:bg-overlay-hover hover:border-control px-4 py-3 cursor-pointer transition-colors',
27 fullWidth ? 'w-full' : 'w-44',
28 isSelected && 'border-foreground-muted hover:border-foreground-muted bg-surface-300'
29 )}
30 tabIndex={0}
31 onKeyDown={(e) => {
32 if (e.key === 'Enter' || e.key === ' ') {
33 onSelectedChange(value)
34 }
35 }}
36 htmlFor={`role-${value}`}
37 >
38 <div className="flex justify-between items-center mb-2">
39 {icon && <div>{icon}</div>}
40
41 {isSelected && (
42 <div className="flex items-center justify-center p-0.5 bg-foreground text-background rounded-full">
43 {typeof isSelected === 'boolean' && (
44 <Check size={12} strokeWidth="4" className="text-background" />
45 )}
46 {isSelected === 'partially' && (
47 <Minus size={12} strokeWidth="4" className="text-background" />
48 )}
49 </div>
50 )}
51 </div>
52
53 <input
54 id={`role-${value}`}
55 type="radio"
56 name="role"
57 value={value}
58 checked={Boolean(isSelected)}
59 onChange={(e) => {
60 onSelectedChange(e.target.value as T)
61 }}
62 className="invisible h-0 w-0 border-0"
63 />
64 <span
65 className={cn(
66 'text-sm text-foreground-light whitespace-nowrap select-none transition-colors',
67 isSelected && 'text-foreground'
68 )}
69 >
70 {label ?? value}
71 </span>
72
73 {description && <p className="text-foreground-lighter text-xs">{description}</p>}
74 </label>
75 )
76}