import { Select as Kobalte } from "@kobalte/core/select" import { Show, createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js" import "./select-v2.css" function groupOptions(options: T[], groupBy?: (x: T) => string): { category: string; options: T[] }[] { if (!groupBy) { return [{ category: "", options }] } const map = new Map() for (const opt of options) { const key = groupBy(opt) const arr = map.get(key) if (arr) arr.push(opt) else map.set(key, [opt]) } return [...map.entries()].map(([category, opts]) => ({ category, options: opts })) } const ChevronDown = () => ( ) const CheckSmall = () => ( ) export type SelectV2Props = Omit< ComponentProps>, "value" | "onSelect" | "children" | "options" | "itemComponent" | "sectionComponent" | "defaultValue" | "multiple" > & { placeholder?: string options: T[] /** Selected option (single selection). */ current?: T value?: (x: T) => string label?: (x: T) => string groupBy?: (x: T) => string onSelect?: (value: T | null) => void onHighlight?: (value: T | undefined) => void | (() => void) /** `base` / `large` match text-input-v2; `inline` is a compact settings-row trigger. */ appearance?: "base" | "large" | "inline" invalid?: boolean numeric?: boolean children?: (item: T) => JSX.Element valueClass?: string } export function SelectV2(props: SelectV2Props) { const [local, others] = splitProps(props, [ "class", "classList", "placeholder", "options", "current", "value", "label", "groupBy", "onSelect", "onHighlight", "onOpenChange", "children", "appearance", "invalid", "numeric", "disabled", "valueClass", "placement", "gutter", "sameWidth", "flip", "slide", "fitViewport", ]) const inline = () => (local.appearance ?? "base") === "inline" const state: { key?: string; cleanup?: void | (() => void) } = {} const stop = () => { state.cleanup?.() state.cleanup = undefined state.key = undefined } const keyFor = (item: T) => (local.value ? local.value(item) : String(item as string)) const move = (item: T | undefined) => { if (!local.onHighlight) return if (!item) { stop() return } const key = keyFor(item) if (state.key === key) return state.cleanup?.() state.cleanup = local.onHighlight(item) state.key = key } onCleanup(stop) const grouped = createMemo(() => groupOptions(local.options, local.groupBy)) return ( {...others} multiple={false} disabled={local.disabled} data-component="select-v2-root" placement={local.placement ?? (inline() ? "bottom-end" : "bottom-start")} gutter={local.gutter ?? 4} sameWidth={local.sameWidth ?? !inline()} flip={local.flip ?? true} slide={local.slide ?? true} fitViewport={local.fitViewport ?? false} value={local.current} options={grouped()} optionValue={(x) => (local.value ? local.value(x) : String(x as string))} optionTextValue={(x) => (local.label ? local.label(x) : String(x as string))} optionGroupChildren="options" placeholder={local.placeholder} sectionComponent={(sectionProps) => (
{sectionProps.section.rawValue.category}
)} itemComponent={(itemProps) => ( move(itemProps.item.rawValue)} onPointerMove={() => move(itemProps.item.rawValue)} onFocus={() => move(itemProps.item.rawValue)} > {local.children ? local.children(itemProps.item.rawValue) : local.label ? local.label(itemProps.item.rawValue) : String(itemProps.item.rawValue as string)} )} onChange={(next) => { const v = next == null ? null : Array.isArray(next) ? ((next[0] as T) ?? null) : (next as T) local.onSelect?.(v) stop() }} onOpenChange={(open) => { local.onOpenChange?.(open) if (!open) stop() }} >
data-slot="select-v2-value-text" class={local.valueClass}> {(st) => { const selected = st.selectedOption() if (local.label && selected != null) return local.label(selected) return selected != null ? (selected as string) : "" }}
) }