summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/list.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-10 15:16:57 -0600
committerAdam <[email protected]>2025-12-10 15:17:04 -0600
commit58e66dd3d1dfd975195dac916fb4b23093404243 (patch)
treedff8b9aaf7026a4b74fd3f519bdf18374697f062 /packages/ui/src/components/list.tsx
parent190fa4c87aa2b3f954a419f716add1fc29e4011e (diff)
downloadopencode-58e66dd3d1dfd975195dac916fb4b23093404243.tar.gz
opencode-58e66dd3d1dfd975195dac916fb4b23093404243.zip
wip(desktop): progress
Diffstat (limited to 'packages/ui/src/components/list.tsx')
-rw-r--r--packages/ui/src/components/list.tsx141
1 files changed, 141 insertions, 0 deletions
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
new file mode 100644
index 000000000..3fbeb35f6
--- /dev/null
+++ b/packages/ui/src/components/list.tsx
@@ -0,0 +1,141 @@
+import { createEffect, Show, For, type JSX, createSignal } from "solid-js"
+import { createStore } from "solid-js/store"
+import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
+import { Icon, IconProps } from "./icon"
+
+export interface ListProps<T> extends FilteredListProps<T> {
+ children: (item: T) => JSX.Element
+ emptyMessage?: string
+ onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
+ activeIcon?: IconProps["name"]
+ filter?: string
+}
+
+export interface ListRef {
+ onKeyDown: (e: KeyboardEvent) => void
+ setScrollRef: (el: HTMLDivElement | undefined) => void
+}
+
+export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) {
+ const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | undefined>(undefined)
+ const [store, setStore] = createStore({
+ mouseActive: false,
+ })
+
+ const { filter, grouped, flat, reset, active, setActive, onKeyDown, onInput } = useFilteredList<T>({
+ items: props.items,
+ key: props.key,
+ filterKeys: props.filterKeys,
+ current: props.current,
+ groupBy: props.groupBy,
+ sortBy: props.sortBy,
+ sortGroupsBy: props.sortGroupsBy,
+ })
+
+ createEffect(() => {
+ if (props.filter === undefined) return
+ onInput(props.filter)
+ })
+
+ createEffect(() => {
+ filter()
+ scrollRef()?.scrollTo(0, 0)
+ reset()
+ })
+
+ createEffect(() => {
+ if (!scrollRef()) return
+ if (!props.current) return
+ const key = props.key(props.current)
+ requestAnimationFrame(() => {
+ const element = scrollRef()!.querySelector(`[data-key="${key}"]`)
+ element?.scrollIntoView({ block: "center" })
+ })
+ })
+
+ createEffect(() => {
+ const all = flat()
+ if (store.mouseActive || all.length === 0) return
+ if (active() === props.key(all[0])) {
+ scrollRef()?.scrollTo(0, 0)
+ return
+ }
+ const element = scrollRef()?.querySelector(`[data-key="${active()}"]`)
+ element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
+ })
+
+ const handleSelect = (item: T | undefined) => {
+ props.onSelect?.(item)
+ }
+
+ const handleKey = (e: KeyboardEvent) => {
+ setStore("mouseActive", false)
+ if (e.key === "Escape") return
+
+ const all = flat()
+ const selected = all.find((x) => props.key(x) === active())
+ props.onKeyEvent?.(e, selected)
+
+ if (e.key === "Enter") {
+ e.preventDefault()
+ if (selected) handleSelect(selected)
+ } else {
+ onKeyDown(e)
+ }
+ }
+
+ props.ref?.({
+ onKeyDown: handleKey,
+ setScrollRef,
+ })
+
+ return (
+ <div ref={setScrollRef} data-component="list">
+ <Show
+ when={flat().length > 0}
+ fallback={
+ <div data-slot="list-empty-state">
+ <div data-slot="list-message">
+ {props.emptyMessage ?? "No results"} for <span data-slot="list-filter">&quot;{filter()}&quot;</span>
+ </div>
+ </div>
+ }
+ >
+ <For each={grouped()}>
+ {(group) => (
+ <div data-slot="list-group">
+ <Show when={group.category}>
+ <div data-slot="list-header">{group.category}</div>
+ </Show>
+ <div data-slot="list-items">
+ <For each={group.items}>
+ {(item) => (
+ <button
+ data-slot="list-item"
+ data-key={props.key(item)}
+ data-active={props.key(item) === active()}
+ data-selected={item === props.current}
+ onClick={() => handleSelect(item)}
+ onMouseMove={() => {
+ setStore("mouseActive", true)
+ setActive(props.key(item))
+ }}
+ >
+ {props.children(item)}
+ <Show when={item === props.current}>
+ <Icon data-slot="list-item-selected-icon" name="check-small" />
+ </Show>
+ <Show when={props.activeIcon}>
+ {(icon) => <Icon data-slot="list-item-active-icon" name={icon()} />}
+ </Show>
+ </button>
+ )}
+ </For>
+ </div>
+ </div>
+ )}
+ </For>
+ </Show>
+ </div>
+ )
+}