feat(app): add manage models icon to selector (per Figma request) (#9722)
This commit is contained in:
@@ -4,6 +4,7 @@ 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"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tag } from "@opencode-ai/ui/tag"
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
@@ -15,6 +16,7 @@ const ModelList: Component<{
|
|||||||
provider?: string
|
provider?: string
|
||||||
class?: string
|
class?: string
|
||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
|
action?: JSX.Element
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -29,7 +31,7 @@ const ModelList: Component<{
|
|||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
|
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
|
||||||
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true, action: props.action }}
|
||||||
emptyMessage={language.t("dialog.model.empty")}
|
emptyMessage={language.t("dialog.model.empty")}
|
||||||
key={(x) => `${x.provider.id}:${x.id}`}
|
key={(x) => `${x.provider.id}:${x.id}`}
|
||||||
items={models}
|
items={models}
|
||||||
@@ -71,6 +73,12 @@ export const ModelSelectorPopover: Component<{
|
|||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const [open, setOpen] = createSignal(false)
|
const [open, setOpen] = createSignal(false)
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const handleManage = () => {
|
||||||
|
setOpen(false)
|
||||||
|
dialog.show(() => <DialogManageModels />)
|
||||||
|
}
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,7 +87,22 @@ export const ModelSelectorPopover: Component<{
|
|||||||
<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 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.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 provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
|
<ModelList
|
||||||
|
provider={props.provider}
|
||||||
|
onSelect={() => setOpen(false)}
|
||||||
|
class="p-1"
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
icon="sliders"
|
||||||
|
variant="ghost"
|
||||||
|
iconSize="normal"
|
||||||
|
class="size-6"
|
||||||
|
aria-label="Manage models"
|
||||||
|
title="Manage models"
|
||||||
|
onClick={handleManage}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Kobalte.Content>
|
</Kobalte.Content>
|
||||||
</Kobalte.Portal>
|
</Kobalte.Portal>
|
||||||
</Kobalte>
|
</Kobalte>
|
||||||
|
|||||||
@@ -23,14 +23,46 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
[data-slot="list-search"] {
|
[data-slot="list-search-wrapper"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
|
||||||
|
> [data-component="icon-button"] {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled),
|
||||||
|
&:focus:not(:disabled),
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) [data-slot="icon-svg"] {
|
||||||
|
color: var(--icon-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) [data-slot="icon-svg"] {
|
||||||
|
color: var(--icon-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="list-search"] {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-self: stretch;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
|
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: var(--surface-base);
|
background: var(--surface-base);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface ListSearchProps {
|
|||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
class?: string
|
class?: string
|
||||||
|
action?: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListProps<T> extends FilteredListProps<T> {
|
export interface ListProps<T> extends FilteredListProps<T> {
|
||||||
@@ -60,6 +61,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
const { filter, grouped, flat, active, setActive, onKeyDown, onInput } = useFilteredList<T>(props)
|
const { filter, grouped, flat, active, setActive, onKeyDown, onInput } = useFilteredList<T>(props)
|
||||||
|
|
||||||
const searchProps = () => (typeof props.search === "object" ? props.search : {})
|
const searchProps = () => (typeof props.search === "object" ? props.search : {})
|
||||||
|
const searchAction = () => searchProps().action
|
||||||
|
|
||||||
const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0
|
const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0
|
||||||
|
|
||||||
@@ -198,29 +200,32 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
return (
|
return (
|
||||||
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
|
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
|
||||||
<Show when={!!props.search}>
|
<Show when={!!props.search}>
|
||||||
<div data-slot="list-search" classList={{ [searchProps().class ?? ""]: !!searchProps().class }}>
|
<div data-slot="list-search-wrapper">
|
||||||
<div data-slot="list-search-container">
|
<div data-slot="list-search" classList={{ [searchProps().class ?? ""]: !!searchProps().class }}>
|
||||||
<Show when={!searchProps().hideIcon}>
|
<div data-slot="list-search-container">
|
||||||
<Icon name="magnifying-glass" />
|
<Show when={!searchProps().hideIcon}>
|
||||||
|
<Icon name="magnifying-glass" />
|
||||||
|
</Show>
|
||||||
|
<TextField
|
||||||
|
autofocus={searchProps().autofocus}
|
||||||
|
variant="ghost"
|
||||||
|
data-slot="list-search-input"
|
||||||
|
type="text"
|
||||||
|
value={internalFilter()}
|
||||||
|
onChange={setInternalFilter}
|
||||||
|
onKeyDown={handleKey}
|
||||||
|
placeholder={searchProps().placeholder}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Show when={internalFilter()}>
|
||||||
|
<IconButton icon="circle-x" variant="ghost" onClick={() => setInternalFilter("")} />
|
||||||
</Show>
|
</Show>
|
||||||
<TextField
|
|
||||||
autofocus={searchProps().autofocus}
|
|
||||||
variant="ghost"
|
|
||||||
data-slot="list-search-input"
|
|
||||||
type="text"
|
|
||||||
value={internalFilter()}
|
|
||||||
onChange={setInternalFilter}
|
|
||||||
onKeyDown={handleKey}
|
|
||||||
placeholder={searchProps().placeholder}
|
|
||||||
spellcheck={false}
|
|
||||||
autocorrect="off"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Show when={internalFilter()}>
|
{searchAction()}
|
||||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setInternalFilter("")} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div ref={setScrollRef} data-slot="list-scroll">
|
<div ref={setScrollRef} data-slot="list-scroll">
|
||||||
|
|||||||
Reference in New Issue
Block a user