From b1e6b9c7c9ebed944f8267bd0fb90404eba546fb Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 19 Sep 2025 06:05:05 -0500 Subject: wip: desktop work --- packages/app/src/components/select.tsx | 184 +++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 packages/app/src/components/select.tsx (limited to 'packages/app/src/components') diff --git a/packages/app/src/components/select.tsx b/packages/app/src/components/select.tsx new file mode 100644 index 000000000..6849f90f1 --- /dev/null +++ b/packages/app/src/components/select.tsx @@ -0,0 +1,184 @@ +import { Select as KobalteSelect } from "@kobalte/core/select" +import { createEffect, createMemo } 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 + options: T[] + current?: T + value?: (x: T) => string + label?: (x: T) => string + groupBy?: (x: T) => string + filterKeys: 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 + let contentRef: HTMLDivElement | undefined = undefined + const [store, setStore] = createStore({ + filter: "", + }) + const grouped = createMemo(() => { + const needle = store.filter.toLowerCase() + const result = pipe( + props.options, + (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: props.filterKeys }).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 ( + + allowDuplicateSelectionEvents={false} + disallowEmptySelection={true} + closeOnSelection={false} + 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 + }} + + + + + + + (contentRef = el)} + onKeyDown={(e) => { + 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="Filter models" + 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, + }} + /> +
+
+ + ) +} -- cgit v1.2.3