diff options
| author | adamelmore <[email protected]> | 2026-01-24 13:33:45 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-24 15:01:05 -0600 |
| commit | 7ba25c6afb3bc1db1ed38f66121b9c95e988e1f0 (patch) | |
| tree | 266685dc54106783e9d450d208bb8f7711111328 | |
| parent | b951187a6e4e78d577f0aac2a9533b381fa719a8 (diff) | |
| download | opencode-7ba25c6afb3bc1db1ed38f66121b9c95e988e1f0.tar.gz opencode-7ba25c6afb3bc1db1ed38f66121b9c95e988e1f0.zip | |
fix(app): model selector ux
| -rw-r--r-- | packages/app/src/components/dialog-select-model.tsx | 92 |
1 files changed, 85 insertions, 7 deletions
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 5569d7780..4d2646e8f 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -1,5 +1,6 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js" +import { Component, ComponentProps, createEffect, createMemo, JSX, onCleanup, Show, ValidComponent } from "solid-js" +import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { useDialog } from "@opencode-ai/ui/context/dialog" import { popularProviders } from "@/hooks/use-providers" @@ -92,26 +93,103 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: { triggerAs?: T triggerProps?: ComponentProps<T> }) { - const [open, setOpen] = createSignal(false) + const [store, setStore] = createStore<{ + open: boolean + dismiss: "escape" | "outside" | null + trigger?: HTMLElement + content?: HTMLElement + }>({ + open: false, + dismiss: null, + trigger: undefined, + content: undefined, + }) const dialog = useDialog() const handleManage = () => { - setOpen(false) + setStore("open", false) dialog.show(() => <DialogManageModels />) } const language = useLanguage() + createEffect(() => { + if (!store.open) return + + const inside = (node: Node | null | undefined) => { + if (!node) return false + const el = store.content + if (el && el.contains(node)) return true + const anchor = store.trigger + if (anchor && anchor.contains(node)) return true + return false + } + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return + setStore("dismiss", "escape") + setStore("open", false) + event.preventDefault() + event.stopPropagation() + } + + const onPointerDown = (event: PointerEvent) => { + const target = event.target + if (!(target instanceof Node)) return + if (inside(target)) return + setStore("dismiss", "outside") + setStore("open", false) + } + + const onFocusIn = (event: FocusEvent) => { + if (!store.content) return + const target = event.target + if (!(target instanceof Node)) return + if (inside(target)) return + setStore("dismiss", "outside") + setStore("open", false) + } + + window.addEventListener("keydown", onKeyDown, true) + window.addEventListener("pointerdown", onPointerDown, true) + window.addEventListener("focusin", onFocusIn, true) + + onCleanup(() => { + window.removeEventListener("keydown", onKeyDown, true) + window.removeEventListener("pointerdown", onPointerDown, true) + window.removeEventListener("focusin", onFocusIn, true) + }) + }) + return ( - <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}> - <Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}> + <Kobalte + open={store.open} + onOpenChange={(next) => { + if (next) setStore("dismiss", null) + setStore("open", next) + }} + placement="top-start" + gutter={8} + > + <Kobalte.Trigger + ref={(el) => setStore("trigger", el)} + as={props.triggerAs ?? "div"} + {...(props.triggerProps as any)} + > {props.children} </Kobalte.Trigger> <Kobalte.Portal> - <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"> + <Kobalte.Content + ref={(el) => setStore("content", el)} + class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden" + onCloseAutoFocus={(event) => { + if (store.dismiss === "outside") event.preventDefault() + setStore("dismiss", null) + }} + > <Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title> <ModelList provider={props.provider} - onSelect={() => setOpen(false)} + onSelect={() => setStore("open", false)} class="p-1" action={ <IconButton |
