Files
CIAgent/apps/web/components/ui/select.tsx
T
saksham 1a4c80958f Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
2026-08-05 10:48:20 -04:00

85 lines
3.2 KiB
TypeScript

"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import clsx from "clsx";
import { Check, ChevronDown } from "lucide-react";
export interface SelectOption {
value: string;
label: string;
}
// Radix disallows an empty-string Item value (it's reserved to mean "no
// selection" internally) - but several call sites here use "" to mean "all"
// / "no filter". This sentinel maps "" <-> a real string at the Radix
// boundary only, so callers can keep using "" as usual.
const EMPTY_VALUE_SENTINEL = "__select-empty__";
/**
* Custom dropdown replacing native <select> everywhere in the app. Native
* select option lists are rendered by the OS/browser itself and cannot be
* animated with CSS/JS in any browser - this is a real component (Radix's
* accessible, keyboard-navigable primitive) so the open/close transition
* (see select-in/select-out in tailwind.config.ts) actually applies.
*/
export function Select({
value,
onValueChange,
options,
placeholder,
name,
ariaLabel,
triggerClassName,
}: {
value: string;
onValueChange: (value: string) => void;
options: SelectOption[];
placeholder?: string;
name?: string;
ariaLabel?: string;
triggerClassName?: string;
}) {
return (
<SelectPrimitive.Root
value={value === "" ? EMPTY_VALUE_SENTINEL : value}
onValueChange={(v) => onValueChange(v === EMPTY_VALUE_SENTINEL ? "" : v)}
name={name}
>
<SelectPrimitive.Trigger
aria-label={ariaLabel}
className={clsx(
"focus-ring flex items-center justify-between gap-2 rounded-md border border-slate-300 bg-white text-left shadow-sm outline-none data-[placeholder]:text-slate-400",
triggerClassName ?? "px-3 py-2 text-sm",
)}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Icon>
<ChevronDown className="h-4 w-4 shrink-0 text-slate-400" aria-hidden />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
position="popper"
sideOffset={4}
className="z-50 overflow-hidden rounded-md border border-slate-200 bg-white shadow-lg data-[state=closed]:animate-select-out data-[state=open]:animate-select-in"
>
<SelectPrimitive.Viewport className="p-1">
{options.map((option) => (
<SelectPrimitive.Item
key={option.value}
value={option.value === "" ? EMPTY_VALUE_SENTINEL : option.value}
className="relative flex cursor-pointer select-none items-center rounded px-2 py-1.5 pl-7 text-sm text-slate-700 outline-none data-[highlighted]:bg-brand-50 data-[highlighted]:text-brand-700"
>
<SelectPrimitive.ItemIndicator className="absolute left-2 inline-flex items-center">
<Check className="h-3.5 w-3.5" aria-hidden />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
);
}