File size: 3,684 Bytes
eb846d0 | 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 | import React, { ReactNode } from 'react';
import { cn } from '@/utils/cn';
interface ToggleGroupItemProps {
value: string;
isSelected: boolean;
onClick: () => void;
children: ReactNode;
}
export const ToggleGroupItem: React.FC<ToggleGroupItemProps> = ({
value,
isSelected,
onClick,
children
}) => {
return (
<button
type="button"
role="checkbox"
aria-checked={isSelected}
className={cn(
"flex w-full items-center justify-between p-2 rounded transition-colors cursor-pointer",
isSelected
? "bg-blue-50 text-blue-700 hover:bg-blue-100 border-l-4 border-blue-500"
: "hover:bg-gray-50 text-gray-700"
)}
onClick={onClick}
>
<span className="flex items-center">
{children}
</span>
{isSelected && (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5 text-blue-500">
<path fillRule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clipRule="evenodd" />
</svg>
)}
</button>
);
};
interface ToggleGroupProps {
label: string;
helpText?: string;
noOptionsText?: string;
values: string[];
options: { value: string; label: string }[];
onChange: (values: string[]) => void;
className?: string;
}
export const ToggleGroup: React.FC<ToggleGroupProps> = ({
label,
helpText,
noOptionsText = "No options available",
values,
options,
onChange,
className
}) => {
const handleToggle = (value: string) => {
const isSelected = values.includes(value);
if (isSelected) {
onChange(values.filter(v => v !== value));
} else {
onChange([...values, value]);
}
};
return (
<div className={className}>
<label className="block text-gray-700 text-sm font-bold mb-2">
{label}
</label>
<div className="border rounded shadow max-h-60 overflow-y-auto">
{options.length === 0 ? (
<p className="text-gray-500 text-sm p-3">{noOptionsText}</p>
) : (
<div className="space-y-1 p-1">
{options.map(option => (
<ToggleGroupItem
key={option.value}
value={option.value}
isSelected={values.includes(option.value)}
onClick={() => handleToggle(option.value)}
>
{option.label}
</ToggleGroupItem>
))}
</div>
)}
</div>
{helpText && (
<p className="text-xs text-gray-500 mt-1">
{helpText}
</p>
)}
</div>
);
};
interface SwitchProps {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
disabled?: boolean;
}
export const Switch: React.FC<SwitchProps> = ({
checked,
onCheckedChange,
disabled = false
}) => {
return (
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500",
checked ? "bg-blue-600" : "bg-gray-200",
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
)}
onClick={() => !disabled && onCheckedChange(!checked)}
>
<span
className={cn(
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
checked ? "translate-x-6" : "translate-x-1"
)}
/>
</button>
);
}; |