fix(app): model selector ux
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
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 { useLocal } from "@/context/local"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
@@ -92,26 +93,103 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
triggerAs?: T
|
triggerAs?: T
|
||||||
triggerProps?: ComponentProps<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 dialog = useDialog()
|
||||||
|
|
||||||
const handleManage = () => {
|
const handleManage = () => {
|
||||||
setOpen(false)
|
setStore("open", false)
|
||||||
dialog.show(() => <DialogManageModels />)
|
dialog.show(() => <DialogManageModels />)
|
||||||
}
|
}
|
||||||
const language = useLanguage()
|
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 (
|
return (
|
||||||
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
|
<Kobalte
|
||||||
<Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
|
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}
|
{props.children}
|
||||||
</Kobalte.Trigger>
|
</Kobalte.Trigger>
|
||||||
<Kobalte.Portal>
|
<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>
|
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
|
||||||
<ModelList
|
<ModelList
|
||||||
provider={props.provider}
|
provider={props.provider}
|
||||||
onSelect={() => setOpen(false)}
|
onSelect={() => setStore("open", false)}
|
||||||
class="p-1"
|
class="p-1"
|
||||||
action={
|
action={
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
Reference in New Issue
Block a user