Spaces:
Running
Running
| import React, { | |
| createContext, | |
| useContext, | |
| useState, | |
| useRef, | |
| useEffect, | |
| } from "react"; | |
| import { cn } from "@/lib/utils"; | |
| const SelectContext = createContext(null); | |
| export function Select({ value, onValueChange, children }) { | |
| const [open, setOpen] = useState(false); | |
| const [items, setItems] = useState({}); | |
| const triggerRef = useRef(null); | |
| // Close on outside click | |
| useEffect(() => { | |
| if (!open) return; | |
| function handleClick(e) { | |
| if (!triggerRef.current) return; | |
| if (!triggerRef.current.parentElement.contains(e.target)) { | |
| setOpen(false); | |
| } | |
| } | |
| document.addEventListener("mousedown", handleClick); | |
| return () => document.removeEventListener("mousedown", handleClick); | |
| }, [open]); | |
| const registerItem = (val, label) => { | |
| setItems((prev) => ({ ...prev, [val]: label })); | |
| }; | |
| return ( | |
| <SelectContext.Provider | |
| value={{ | |
| value, | |
| onValueChange, | |
| open, | |
| setOpen, | |
| items, | |
| registerItem, | |
| triggerRef, | |
| }} | |
| > | |
| <div className="relative inline-block">{children}</div> | |
| </SelectContext.Provider> | |
| ); | |
| } | |
| export function SelectTrigger({ className, children }) { | |
| const { setOpen, triggerRef } = useContext(SelectContext); | |
| return ( | |
| <button | |
| type="button" | |
| ref={triggerRef} | |
| onClick={() => setOpen((o) => !o)} | |
| className={cn( | |
| "flex items-center justify-between rounded-md border bg-white px-3 py-2 text-sm text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500", | |
| className | |
| )} | |
| > | |
| {children} | |
| </button> | |
| ); | |
| } | |
| export function SelectValue({ placeholder }) { | |
| const { value, items } = useContext(SelectContext); | |
| const label = value ? items[value] : null; | |
| return ( | |
| <span className={cn("truncate text-sm", !label && "text-slate-400")}> | |
| {label || placeholder} | |
| </span> | |
| ); | |
| } | |
| export function SelectContent({ className, children }) { | |
| const { open } = useContext(SelectContext); | |
| if (!open) return null; | |
| return ( | |
| <div | |
| className={cn( | |
| "absolute z-50 mt-2 min-w-[8rem] rounded-md border border-slate-200 bg-white shadow-lg", | |
| className | |
| )} | |
| > | |
| {children} | |
| </div> | |
| ); | |
| } | |
| export function SelectItem({ value, children, className }) { | |
| const { onValueChange, setOpen, registerItem } = useContext(SelectContext); | |
| useEffect(() => { | |
| registerItem(value, typeof children === "string" ? children : String(children)); | |
| }, [value, children, registerItem]); | |
| const handleClick = () => { | |
| onValueChange?.(value); | |
| setOpen(false); | |
| }; | |
| return ( | |
| <div | |
| onClick={handleClick} | |
| className={cn( | |
| "cursor-pointer select-none px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100", | |
| className | |
| )} | |
| > | |
| {children} | |
| </div> | |
| ); | |
| } | |