import { Select as KobalteSelect } from "@kobalte/core/select" import { createEffect, createMemo, Show } from "solid-js" import type { ComponentProps } from "solid-js" import { Icon } from "@/ui/icon" import fuzzysort from "fuzzysort" import { pipe, groupBy, entries, map } from "remeda" import { createStore } from "solid-js/store" export interface SelectProps { variant?: "default" | "outline" size?: "sm" | "md" | "lg" placeholder?: string filter?: | false | { placeholder?: string keys: string[] } options: T[] current?: T value?: (x: T) => string label?: (x: T) => string groupBy?: (x: T) => string onFilter?: (query: string) => void onSelect?: (value: T | undefined) => void class?: ComponentProps<"div">["class"] classList?: ComponentProps<"div">["classList"] } export function Select(props: SelectProps) { let inputRef: HTMLInputElement | undefined = undefined let listboxRef: HTMLUListElement | undefined = undefined const [store, setStore] = createStore({ filter: "", }) const grouped = createMemo(() => { const needle = store.filter.toLowerCase() const result = pipe( props.options, (x) => !needle || !props.filter ? x : fuzzysort.go(needle, x, { keys: props.filter && props.filter.keys }).map((x) => x.obj), groupBy((x) => (props.groupBy ? props.groupBy(x) : "")), // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))), entries(), map(([k, v]) => ({ category: k, options: v })), ) return result }) // const flat = createMemo(() => { // return pipe( // grouped(), // flatMap(({ options }) => options), // ) // }) createEffect(() => { store.filter listboxRef?.scrollTo(0, 0) // setStore("selected", 0) // scroll.scrollTo(0) }) return ( value={props.current} options={grouped()} optionValue={(x) => (props.value ? props.value(x) : (x as string))} optionTextValue={(x) => (props.label ? props.label(x) : (x as string))} optionGroupChildren="options" placeholder={props.placeholder} sectionComponent={(props) => ( {props.section.rawValue.category} )} itemComponent={(itemProps) => ( {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)} )} onChange={(v) => { if (props.onSelect) props.onSelect(v ?? undefined) if (v !== null) { // close the select } }} onOpenChange={(v) => v || setStore("filter", "")} > > {(state) => { const selected = state.selectedOption() ?? props.current if (!selected) return props.placeholder || "" if (props.label) return props.label(selected) return selected as string }} { if (!props.filter) return if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape") { return } inputRef?.focus() }} classList={{ "min-w-32 overflow-hidden rounded-md border border-border-subtle/40": true, "bg-background-panel p-1 shadow-md z-50": true, "data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95": true, "data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true, }} > (inputRef = el)} id="select-filter" type="text" placeholder={props.filter ? props.filter.placeholder : "Filter items"} value={store.filter} onInput={(e) => setStore("filter", e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape") { e.preventDefault() e.stopPropagation() listboxRef?.focus() } }} classList={{ "w-full": true, "px-2 pb-2 text-text font-light placeholder-text-muted/70 text-xs focus:outline-none": true, }} /> (listboxRef = el)} classList={{ "overflow-y-auto max-h-48 no-scrollbar": true, }} /> ) }