Просмотр исходного кода

feat(app): add manage models icon to selector (per Figma request) (#9722)

Ronan Kearns 1 месяц назад
Родитель
Сommit
996eeb1f68

+ 25 - 2
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>

+ 35 - 3
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);

+ 26 - 21
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">