summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/select.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-10-14 07:14:26 -0500
committerAdam <[email protected]>2025-10-14 07:16:24 -0500
commit37e6c8342f1f63a95918a71ac78130b9d10d1103 (patch)
treef5baccd7d4d97a0171eb224c9f7d88ec62b14aac /packages/ui/src/components/select.tsx
parentc04e8929911be2433063e9a53cccf8d2dfd232b8 (diff)
downloadopencode-37e6c8342f1f63a95918a71ac78130b9d10d1103.tar.gz
opencode-37e6c8342f1f63a95918a71ac78130b9d10d1103.zip
wip: css and ui packages
Diffstat (limited to 'packages/ui/src/components/select.tsx')
-rw-r--r--packages/ui/src/components/select.tsx92
1 files changed, 92 insertions, 0 deletions
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx
new file mode 100644
index 000000000..c4d5443c1
--- /dev/null
+++ b/packages/ui/src/components/select.tsx
@@ -0,0 +1,92 @@
+import { Select as Kobalte } from "@kobalte/core/select"
+import { createMemo, type ComponentProps } from "solid-js"
+import { Icon, Button, type ButtonProps } from "@opencode-ai/ui"
+import { pipe, groupBy, entries, map } from "remeda"
+
+export interface SelectProps<T> {
+ placeholder?: string
+ options: T[]
+ current?: T
+ value?: (x: T) => string
+ label?: (x: T) => string
+ groupBy?: (x: T) => string
+ onSelect?: (value: T | undefined) => void
+ class?: ComponentProps<"div">["class"]
+ classList?: ComponentProps<"div">["classList"]
+}
+
+export function Select<T>(props: SelectProps<T> & ButtonProps) {
+ const grouped = createMemo(() => {
+ const result = pipe(
+ props.options,
+ 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
+ })
+
+ return (
+ <Kobalte<T, { category: string; options: T[] }>
+ data-component="select"
+ 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) => (
+ <Kobalte.Section data-slot="section">{props.section.rawValue.category}</Kobalte.Section>
+ )}
+ itemComponent={(itemProps) => (
+ <Kobalte.Item
+ data-slot="item"
+ classList={{
+ ...(props.classList ?? {}),
+ [props.class ?? ""]: !!props.class,
+ }}
+ {...itemProps}
+ >
+ <Kobalte.ItemLabel data-slot="item-label">
+ {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
+ </Kobalte.ItemLabel>
+ <Kobalte.ItemIndicator data-slot="item-indicator">
+ <Icon name="checkmark" size={16} />
+ </Kobalte.ItemIndicator>
+ </Kobalte.Item>
+ )}
+ onChange={(v) => {
+ props.onSelect?.(v ?? undefined)
+ }}
+ >
+ <Kobalte.Trigger
+ data-slot="trigger"
+ as={Button}
+ size={props.size}
+ variant={props.variant}
+ classList={{
+ ...(props.classList ?? {}),
+ [props.class ?? ""]: !!props.class,
+ }}
+ >
+ <Kobalte.Value<T> data-slot="value">
+ {(state) => {
+ const selected = state.selectedOption() ?? props.current
+ if (!selected) return props.placeholder || ""
+ if (props.label) return props.label(selected)
+ return selected as string
+ }}
+ </Kobalte.Value>
+ <Kobalte.Icon data-slot="icon">
+ <Icon name="chevron-down" size={16} class="-my-2 group-data-[expanded]:rotate-180" />
+ </Kobalte.Icon>
+ </Kobalte.Trigger>
+ <Kobalte.Portal>
+ <Kobalte.Content data-component="select-content">
+ <Kobalte.Listbox data-slot="list" />
+ </Kobalte.Content>
+ </Kobalte.Portal>
+ </Kobalte>
+ )
+}