diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/app/src/components/dialog-select-model.tsx | 27 | ||||
| -rw-r--r-- | packages/ui/src/components/list.css | 38 | ||||
| -rw-r--r-- | packages/ui/src/components/list.tsx | 47 |
3 files changed, 86 insertions, 26 deletions
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index cdb299c79..8288a8255 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -4,6 +4,7 @@ import { useLocal } from "@/context/local" import { useDialog } from "@opencode-ai/ui/context/dialog" import { popularProviders } from "@/hooks/use-providers" import { Button } from "@opencode-ai/ui/button" +import { IconButton } from "@opencode-ai/ui/icon-button" import { Tag } from "@opencode-ai/ui/tag" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" @@ -15,6 +16,7 @@ const ModelList: Component<{ provider?: string class?: string onSelect: () => void + action?: JSX.Element }> = (props) => { const local = useLocal() const language = useLanguage() @@ -29,7 +31,7 @@ const ModelList: Component<{ return ( <List 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")} key={(x) => `${x.provider.id}:${x.id}`} items={models} @@ -71,6 +73,12 @@ export const ModelSelectorPopover: Component<{ children: JSX.Element }> = (props) => { const [open, setOpen] = createSignal(false) + const dialog = useDialog() + + const handleManage = () => { + setOpen(false) + dialog.show(() => <DialogManageModels />) + } const language = useLanguage() return ( @@ -79,7 +87,22 @@ export const ModelSelectorPopover: Component<{ <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.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.Portal> </Kobalte> diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css index ee9be422c..95641bb20 100644 --- a/packages/ui/src/components/list.css +++ b/packages/ui/src/components/list.css @@ -23,14 +23,46 @@ overflow: hidden; padding: 0 12px; - [data-slot="list-search"] { + [data-slot="list-search-wrapper"] { display: flex; flex-shrink: 0; - padding: 8px; align-items: center; - gap: 12px; + 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; + align-items: center; + gap: 12px; border-radius: var(--radius-md); background: var(--surface-base); diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 066363502..fc9fa5405 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -11,6 +11,7 @@ export interface ListSearchProps { autofocus?: boolean hideIcon?: boolean class?: string + action?: JSX.Element } 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 searchProps = () => (typeof props.search === "object" ? props.search : {}) + const searchAction = () => searchProps().action 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 ( <div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}> <Show when={!!props.search}> - <div data-slot="list-search" classList={{ [searchProps().class ?? ""]: !!searchProps().class }}> - <div data-slot="list-search-container"> - <Show when={!searchProps().hideIcon}> - <Icon name="magnifying-glass" /> + <div data-slot="list-search-wrapper"> + <div data-slot="list-search" classList={{ [searchProps().class ?? ""]: !!searchProps().class }}> + <div data-slot="list-search-container"> + <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> - <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> + {searchAction()} </div> </Show> <div ref={setScrollRef} data-slot="list-scroll"> |
