lead-to-policy/src/components/core/MultiSelect.tsx
Bhanu Prakash Sai Potteri 66d4c91f32 Initial import of lead-to-policy (dev env)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:28:12 +05:30

110 lines
3.8 KiB
TypeScript

import { useState } from 'react';
import { Check, Plus } from 'lucide-react';
import { cn } from '../../lib/cn';
export interface MultiSelectOption {
value: string;
label: string;
}
export interface MultiSelectProps {
label?: string;
options?: Array<string | MultiSelectOption>;
/** Controlled selected values. */
value?: string[];
defaultValue?: string[];
onChange?: (next: string[]) => void;
className?: string;
/** Render chips as read-only (no toggling) and show `hint` instead of being editable. */
disabled?: boolean;
/** Small muted helper line — used for the disabled-state explanation. */
hint?: string;
}
/** Chip-based multiselect. Click options to toggle; selected render as filled chips. */
export function MultiSelect({
label,
options = [],
value,
defaultValue = [],
onChange,
className,
disabled = false,
hint,
}: MultiSelectProps) {
const controlled = value !== undefined;
const [internal, setInternal] = useState<string[]>(defaultValue);
const selected = controlled ? value : internal;
const set = (next: string[]) => {
if (!controlled) setInternal(next);
onChange?.(next);
};
const toggle = (v: string) =>
set(selected.includes(v) ? selected.filter((x) => x !== v) : [...selected, v]);
return (
<div className={cn('flex flex-col gap-1.5 font-sans', className)}>
{label && <span className="text-sm font-medium text-muted">{label}</span>}
{disabled ? (
// Read-only: chips don't toggle and a hint explains why (no product yet,
// or the plan offers no riders). Muted so it reads as inactive.
<div className="flex flex-col gap-1.5">
{options.length > 0 && (
<div className="flex flex-wrap gap-2 opacity-60">
{options.map((o) => {
const val = typeof o === 'string' ? o : o.value;
const lab = typeof o === 'string' ? o : o.label;
const on = selected.includes(val);
return (
<span
key={val}
className={cn(
'inline-flex items-center gap-1.5 px-3 py-[7px] rounded-pill font-sans text-sm font-semibold cursor-default border',
on
? 'bg-sunrise-100 text-sunrise-600 border-transparent'
: 'bg-card text-body border-border-default',
)}
>
<span className={cn('flex', on ? 'opacity-100' : 'opacity-40')}>
{on ? <Check size={13} /> : <Plus size={13} />}
</span>
{lab}
</span>
);
})}
</div>
)}
{hint && <span className="text-xs text-faint">{hint}</span>}
</div>
) : (
<div className="flex flex-wrap gap-2">
{options.map((o) => {
const val = typeof o === 'string' ? o : o.value;
const lab = typeof o === 'string' ? o : o.label;
const on = selected.includes(val);
return (
<button
key={val}
type="button"
onClick={() => toggle(val)}
className={cn(
'inline-flex items-center gap-1.5 px-3 py-[7px] rounded-pill font-sans text-sm font-semibold cursor-pointer border transition-all duration-150',
on
? 'bg-sunrise-100 text-sunrise-600 border-transparent'
: 'bg-card text-body border-border-default',
)}
>
<span className={cn('flex', on ? 'opacity-100' : 'opacity-40')}>
{on ? <Check size={13} /> : <Plus size={13} />}
</span>
{lab}
</button>
);
})}
</div>
)}
</div>
);
}