From 7ba25c6afb3bc1db1ed38f66121b9c95e988e1f0 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:33:45 -0600 Subject: [PATCH] fix(app): model selector ux --- .../src/components/dialog-select-model.tsx | 92 +++++++++++++++++-- 1 file 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(props: { triggerAs?: T triggerProps?: ComponentProps }) { - 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(() => ) } 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 ( - - + { + if (next) setStore("dismiss", null) + setStore("open", next) + }} + placement="top-start" + gutter={8} + > + setStore("trigger", el)} + as={props.triggerAs ?? "div"} + {...(props.triggerProps as any)} + > {props.children} - + 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) + }} + > {language.t("dialog.model.select.title")} setOpen(false)} + onSelect={() => setStore("open", false)} class="p-1" action={