From 3faf312c471df9a949af46d2ce560fe5ac520b54 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:04:32 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/ui/searchable-select.tsx | 127 ++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/components/ui/searchable-select.tsx diff --git a/src/components/ui/searchable-select.tsx b/src/components/ui/searchable-select.tsx new file mode 100644 index 0000000..de7a756 --- /dev/null +++ b/src/components/ui/searchable-select.tsx @@ -0,0 +1,127 @@ +import * as React from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +export interface SearchableSelectOption { + value: string; + /** Visible label in the selected trigger */ + label: string; + /** Custom render for the option row (defaults to label) */ + render?: React.ReactNode; + /** Extra text used for fuzzy matching (case number, email, etc.) */ + keywords?: string; + disabled?: boolean; +} + +interface SearchableSelectProps { + value?: string | null; + onValueChange: (value: string) => void; + options: SearchableSelectOption[]; + placeholder?: string; + searchPlaceholder?: string; + emptyText?: string; + disabled?: boolean; + className?: string; + triggerClassName?: string; + /** Width of popover content. Default: trigger width via "w-[--radix-popover-trigger-width]" */ + contentClassName?: string; + /** Optional element rendered above the list (e.g. "Add new") */ + footer?: React.ReactNode; +} + +export function SearchableSelect({ + value, + onValueChange, + options, + placeholder = "Select…", + searchPlaceholder = "Search…", + emptyText = "No results.", + disabled, + className, + triggerClassName, + contentClassName, + footer, +}: SearchableSelectProps) { + const [open, setOpen] = React.useState(false); + const selected = options.find((o) => o.value === value); + + return ( +
+ + + + + + { + const opt = options.find((o) => o.value === itemValue); + if (!opt) return 0; + const hay = `${opt.label} ${opt.keywords ?? ""}`.toLowerCase(); + return hay.includes(search.toLowerCase()) ? 1 : 0; + }} + > + + + {emptyText} + + {options.map((opt) => ( + { + onValueChange(v); + setOpen(false); + }} + > + + {opt.render ?? opt.label} + + ))} + + {footer} + + + + +
+ ); +}