"use client"; import * as React from "react"; import { useMemo, useState } from "react"; import { ChevronsUpDown, Search } from "lucide-react"; import { cn } from "./utils"; import { Button } from "./button"; import { Input } from "./input"; import { Popover, PopoverContent, PopoverTrigger } from "./popover"; export type SearchableSelectOption = { value: string; label: string; }; /** 下拉默认可见行数,超出部分滚动 */ const DEFAULT_VISIBLE_LIST_ROWS = 5; /** 单行高度:py-2 + text-sm 行高 */ const LIST_ROW_PX = 36; export function SearchableSelect({ value, onValueChange, options, placeholder = "Select…", searchPlaceholder = "Search…", emptyText = "No matching results.", disabled, className, /** 顶栏/模板编辑器:与 SelectTrigger !h-7、text-xs 对齐 */ compact = false, /** 下拉列表可见行数(默认 5 行,其余滚动) */ visibleListRows = DEFAULT_VISIBLE_LIST_ROWS, }: { /** 空字符串表示未选择 */ value: string; onValueChange: (next: string) => void; options: SearchableSelectOption[]; placeholder?: string; searchPlaceholder?: string; emptyText?: string; disabled?: boolean; className?: string; compact?: boolean; visibleListRows?: number; }) { const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const hit = value ? options.find((o) => o.value === value) : undefined; const selectedLabel = value ? hit?.label ?? value : null; const listMaxHeightPx = Math.max(1, visibleListRows) * LIST_ROW_PX; const filtered = useMemo(() => { const s = q.trim().toLowerCase(); if (!s) return options; return options.filter( (o) => o.label.toLowerCase().includes(s) || o.value.toLowerCase().includes(s), ); }, [options, q]); const closePanel = () => { setOpen(false); setQ(""); }; return ( { setOpen(next); if (!next) setQ(""); }} >
setQ(e.target.value)} placeholder={searchPlaceholder} className="h-8 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0" />
{filtered.length === 0 ? (
{emptyText}
) : ( filtered.map((opt) => ( )) )}
{value ? (
) : null}
); }