feat(app): update manage servers dialog styling and behavior

This commit is contained in:
David Hill
2026-01-24 20:51:53 +00:00
parent a98add29d1
commit 02aea77e92
3 changed files with 79 additions and 35 deletions

View File

@@ -12,6 +12,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { Popover } from "@opencode-ai/ui/popover" import { Popover } from "@opencode-ai/ui/popover"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSDK } from "@/context/global-sdk"
type ServerStatus = { healthy: boolean; version?: string } type ServerStatus = { healthy: boolean; version?: string }
@@ -52,16 +53,16 @@ async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>
function AddRow(props: AddRowProps) { function AddRow(props: AddRowProps) {
return ( return (
<div class="flex items-center gap-3 px-4 min-w-0 flex-1"> <div class="flex items-center px-3 h-14 min-w-0 flex-1">
<div <div class="relative flex-1 min-w-0">
classList={{ <div
"size-1.5 rounded-full shrink-0": true, classList={{
"bg-icon-success-base": props.status === true, "size-1.5 rounded-full absolute left-3 top-1/2 -translate-y-1/2": true,
"bg-icon-critical-base": props.status === false, "bg-icon-success-base": props.status === true,
"bg-border-weak-base": props.status === undefined, "bg-icon-critical-base": props.status === false,
}} "bg-border-weak-base": props.status === undefined,
/> }}
<div class="flex-1 min-w-0"> />
<TextField <TextField
type="text" type="text"
hideLabel hideLabel
@@ -74,6 +75,7 @@ function AddRow(props: AddRowProps) {
onChange={props.onChange} onChange={props.onChange}
onKeyDown={props.onKeyDown} onKeyDown={props.onKeyDown}
onBlur={props.onBlur} onBlur={props.onBlur}
class="pl-7"
/> />
</div> </div>
</div> </div>
@@ -344,9 +346,10 @@ export function DialogSelectServer() {
return ( return (
<Dialog title={language.t("dialog.server.title")}> <Dialog title={language.t("dialog.server.title")}>
<div class="flex flex-col gap-2 pb-4"> <div class="flex flex-col gap-2 pb-5">
<List <List
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }} search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
noInitialSelection
emptyMessage={language.t("dialog.server.empty")} emptyMessage={language.t("dialog.server.empty")}
items={sortedItems} items={sortedItems}
key={(x) => x} key={(x) => x}
@@ -354,7 +357,7 @@ export function DialogSelectServer() {
if (x) select(x) if (x) select(x)
}} }}
divider={true} divider={true}
class="[&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:py-3" class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3"
add={ add={
store.addServer.showForm store.addServer.showForm
? { ? {
@@ -376,6 +379,35 @@ export function DialogSelectServer() {
> >
{(i) => { {(i) => {
const [popoverOpen, setPopoverOpen] = createSignal(false) const [popoverOpen, setPopoverOpen] = createSignal(false)
const [truncated, setTruncated] = createSignal(false)
let nameRef: HTMLSpanElement | undefined
let versionRef: HTMLSpanElement | undefined
const check = () => {
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false
setTruncated(nameTruncated || versionTruncated)
}
createEffect(() => {
check()
window.addEventListener("resize", check)
onCleanup(() => window.removeEventListener("resize", check))
})
const tooltipValue = () => {
const name = serverDisplayName(i)
const version = store.status[i]?.version
return (
<span class="flex items-center gap-2">
<span>{name}</span>
<Show when={version}>
<span class="text-text-invert-base">{version}</span>
</Show>
</span>
)
}
return ( return (
<div class="flex items-center gap-3 min-w-0 flex-1 group/item"> <div class="flex items-center gap-3 min-w-0 flex-1 group/item">
<Show <Show
@@ -393,28 +425,34 @@ export function DialogSelectServer() {
/> />
} }
> >
<div <Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
class="flex items-center gap-3 px-4 min-w-0 flex-1"
classList={{ "opacity-50": store.status[i]?.healthy === false }}
>
<div <div
classList={{ class="flex items-center gap-3 px-4 min-w-0 flex-1"
"size-1.5 rounded-full shrink-0": true, classList={{ "opacity-50": store.status[i]?.healthy === false }}
"bg-icon-success-base": store.status[i]?.healthy === true, >
"bg-icon-critical-base": store.status[i]?.healthy === false, <div
"bg-border-weak-base": store.status[i] === undefined, classList={{
}} "size-1.5 rounded-full shrink-0": true,
/> "bg-icon-success-base": store.status[i]?.healthy === true,
<span class="truncate">{serverDisplayName(i)}</span> "bg-icon-critical-base": store.status[i]?.healthy === false,
<Show when={store.status[i]?.version}> "bg-border-weak-base": store.status[i] === undefined,
<span class="text-text-weak text-14-regular">{store.status[i]?.version}</span> }}
</Show> />
<Show when={defaultUrl() === i}> <span ref={nameRef} class="truncate">
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs"> {serverDisplayName(i)}
{language.t("dialog.server.status.default")}
</span> </span>
</Show> <Show when={store.status[i]?.version}>
</div> <span ref={versionRef} class="text-text-weak text-14-regular truncate">
{store.status[i]?.version}
</span>
</Show>
<Show when={defaultUrl() === i}>
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
{language.t("dialog.server.status.default")}
</span>
</Show>
</div>
</Tooltip>
</Show> </Show>
<Show when={store.editServer.id !== i}> <Show when={store.editServer.id !== i}>
<div class="flex items-center justify-center gap-5 px-4"> <div class="flex items-center justify-center gap-5 px-4">
@@ -508,7 +546,7 @@ export function DialogSelectServer() {
}} }}
</List> </List>
<div class="px-6"> <div class="px-5">
<Button <Button
variant="secondary" variant="secondary"
icon="plus-small" icon="plus-small"

View File

@@ -66,7 +66,7 @@
[data-slot="dialog-header"] { [data-slot="dialog-header"] {
display: flex; display: flex;
padding: 16px 16px 16px 24px; padding: 20px;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;

View File

@@ -13,6 +13,7 @@ export interface FilteredListProps<T> {
sortBy?: (a: T, b: T) => number sortBy?: (a: T, b: T) => number
sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
onSelect?: (value: T | undefined, index: number) => void onSelect?: (value: T | undefined, index: number) => void
noInitialSelection?: boolean
} }
export function useFilteredList<T>(props: FilteredListProps<T>) { export function useFilteredList<T>(props: FilteredListProps<T>) {
@@ -57,6 +58,7 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
}) })
function initialActive() { function initialActive() {
if (props.noInitialSelection) return ""
if (props.current) return props.key(props.current) if (props.current) return props.key(props.current)
const items = flat() const items = flat()
@@ -71,6 +73,10 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
}) })
const reset = () => { const reset = () => {
if (props.noInitialSelection) {
list.setActive("")
return
}
const all = flat() const all = flat()
if (all.length === 0) return if (all.length === 0) return
list.setActive(props.key(all[0])) list.setActive(props.key(all[0]))