feat(app): search through sessions
This commit is contained in:
@@ -27,6 +27,7 @@ import { Button } from "@opencode-ai/ui/button"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||||
|
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
||||||
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||||
@@ -2682,6 +2683,14 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean }) => {
|
const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean }) => {
|
||||||
|
type SearchItem = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
directory: string
|
||||||
|
label: string
|
||||||
|
archived?: number
|
||||||
|
}
|
||||||
|
|
||||||
const projectName = createMemo(() => {
|
const projectName = createMemo(() => {
|
||||||
const project = panelProps.project
|
const project = panelProps.project
|
||||||
if (!project) return ""
|
if (!project) return ""
|
||||||
@@ -2697,6 +2706,107 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
const homedir = createMemo(() => globalSync.data.path.home)
|
const homedir = createMemo(() => globalSync.data.path.home)
|
||||||
|
|
||||||
|
const [search, setSearch] = createStore({
|
||||||
|
value: "",
|
||||||
|
})
|
||||||
|
const searching = createMemo(() => search.value.trim().length > 0)
|
||||||
|
let searchRef: HTMLInputElement | undefined
|
||||||
|
let listRef: ListRef | undefined
|
||||||
|
|
||||||
|
const token = { value: 0 }
|
||||||
|
let inflight: Promise<SearchItem[]> | undefined
|
||||||
|
let all: SearchItem[] | undefined
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
token.value += 1
|
||||||
|
inflight = undefined
|
||||||
|
all = undefined
|
||||||
|
setSearch({ value: "" })
|
||||||
|
listRef = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = (item: SearchItem | undefined) => {
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
const href = `/${base64Encode(item.directory)}/session/${item.id}`
|
||||||
|
if (!layout.sidebar.opened()) {
|
||||||
|
setState("hoverSession", undefined)
|
||||||
|
setState("hoverProject", undefined)
|
||||||
|
}
|
||||||
|
reset()
|
||||||
|
navigate(href)
|
||||||
|
layout.mobileSidebar.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = (filter: string) => {
|
||||||
|
const query = filter.trim()
|
||||||
|
if (!query) {
|
||||||
|
token.value += 1
|
||||||
|
inflight = undefined
|
||||||
|
all = undefined
|
||||||
|
return [] as SearchItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = panelProps.project
|
||||||
|
if (!project) return [] as SearchItem[]
|
||||||
|
if (all) return all
|
||||||
|
if (inflight) return inflight
|
||||||
|
|
||||||
|
const current = token.value
|
||||||
|
const dirs = workspaceIds(project)
|
||||||
|
inflight = Promise.all(
|
||||||
|
dirs.map((input) => {
|
||||||
|
const directory = workspaceKey(input)
|
||||||
|
const [workspaceStore] = globalSync.child(directory, { bootstrap: false })
|
||||||
|
const kind =
|
||||||
|
directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
||||||
|
const name = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
|
||||||
|
const label = `${kind} : ${name}`
|
||||||
|
return globalSDK.client.session
|
||||||
|
.list({ directory, roots: true })
|
||||||
|
.then((x) =>
|
||||||
|
(x.data ?? [])
|
||||||
|
.filter((s) => !!s?.id)
|
||||||
|
.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title ?? language.t("command.session.new"),
|
||||||
|
directory,
|
||||||
|
label,
|
||||||
|
archived: s.time?.archived,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.catch(() => [] as SearchItem[])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((results) => {
|
||||||
|
if (token.value !== current) return [] as SearchItem[]
|
||||||
|
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const next = results.flat().filter((item) => {
|
||||||
|
const key = `${item.directory}:${item.id}`
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
all = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
.catch(() => [] as SearchItem[])
|
||||||
|
.finally(() => {
|
||||||
|
inflight = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
return inflight
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => panelProps.project?.worktree,
|
||||||
|
() => reset(),
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
@@ -2705,7 +2815,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}}
|
}}
|
||||||
style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
|
style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
|
||||||
>
|
>
|
||||||
<Show when={panelProps.project} keyed>
|
<Show when={panelProps.project}>
|
||||||
{(p) => (
|
{(p) => (
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 px-2 py-1">
|
<div class="shrink-0 px-2 py-1">
|
||||||
@@ -2714,7 +2824,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<InlineEditor
|
<InlineEditor
|
||||||
id={`project:${projectId()}`}
|
id={`project:${projectId()}`}
|
||||||
value={projectName}
|
value={projectName}
|
||||||
onSave={(next) => renameProject(p, next)}
|
onSave={(next) => renameProject(p(), next)}
|
||||||
class="text-16-medium text-text-strong truncate"
|
class="text-16-medium text-text-strong truncate"
|
||||||
displayClass="text-16-medium text-text-strong truncate"
|
displayClass="text-16-medium text-text-strong truncate"
|
||||||
stopPropagation
|
stopPropagation
|
||||||
@@ -2723,7 +2833,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
gutter={2}
|
gutter={2}
|
||||||
value={p.worktree}
|
value={p().worktree}
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
"max-width": "640px",
|
"max-width": "640px",
|
||||||
@@ -2731,7 +2841,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="text-12-regular text-text-base truncate select-text">
|
<span class="text-12-regular text-text-base truncate select-text">
|
||||||
{p.worktree.replace(homedir(), "~")}
|
{p().worktree.replace(homedir(), "~")}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -2742,31 +2852,31 @@ export default function Layout(props: ParentProps) {
|
|||||||
icon="dot-grid"
|
icon="dot-grid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
data-action="project-menu"
|
data-action="project-menu"
|
||||||
data-project={base64Encode(p.worktree)}
|
data-project={base64Encode(p().worktree)}
|
||||||
class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
|
class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
|
||||||
aria-label={language.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu.Portal mount={!panelProps.mobile ? state.nav : undefined}>
|
<DropdownMenu.Portal mount={!panelProps.mobile ? state.nav : undefined}>
|
||||||
<DropdownMenu.Content class="mt-1">
|
<DropdownMenu.Content class="mt-1">
|
||||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}>
|
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p()} />)}>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
data-action="project-workspaces-toggle"
|
data-action="project-workspaces-toggle"
|
||||||
data-project={base64Encode(p.worktree)}
|
data-project={base64Encode(p().worktree)}
|
||||||
disabled={p.vcs !== "git" && !layout.sidebar.workspaces(p.worktree)()}
|
disabled={p().vcs !== "git" && !layout.sidebar.workspaces(p().worktree)()}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const enabled = layout.sidebar.workspaces(p.worktree)()
|
const enabled = layout.sidebar.workspaces(p().worktree)()
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
layout.sidebar.toggleWorkspaces(p.worktree)
|
layout.sidebar.toggleWorkspaces(p().worktree)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (p.vcs !== "git") return
|
if (p().vcs !== "git") return
|
||||||
layout.sidebar.toggleWorkspaces(p.worktree)
|
layout.sidebar.toggleWorkspaces(p().worktree)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>
|
||||||
{layout.sidebar.workspaces(p.worktree)()
|
{layout.sidebar.workspaces(p().worktree)()
|
||||||
? language.t("sidebar.workspaces.disable")
|
? language.t("sidebar.workspaces.disable")
|
||||||
: language.t("sidebar.workspaces.enable")}
|
: language.t("sidebar.workspaces.enable")}
|
||||||
</DropdownMenu.ItemLabel>
|
</DropdownMenu.ItemLabel>
|
||||||
@@ -2774,8 +2884,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
data-action="project-close-menu"
|
data-action="project-close-menu"
|
||||||
data-project={base64Encode(p.worktree)}
|
data-project={base64Encode(p().worktree)}
|
||||||
onSelect={() => closeProject(p.worktree)}
|
onSelect={() => closeProject(p().worktree)}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
@@ -2785,103 +2895,207 @@ export default function Layout(props: ParentProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show
|
<div class="shrink-0 px-2 pt-2">
|
||||||
when={workspacesEnabled()}
|
<div
|
||||||
fallback={
|
class="flex items-center gap-2 p-2 rounded-md bg-surface-base shadow-xs-border-base focus-within:shadow-xs-border-select"
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
const target = event.target
|
||||||
|
if (!(target instanceof Element)) return
|
||||||
|
if (target.closest("input, textarea, [contenteditable='true']")) return
|
||||||
|
searchRef?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="magnifying-glass" />
|
||||||
|
<InlineInput
|
||||||
|
ref={(el) => {
|
||||||
|
searchRef = el
|
||||||
|
}}
|
||||||
|
class="flex-1 min-w-0 text-14-regular text-text-strong placeholder:text-text-weak"
|
||||||
|
style={{ "box-shadow": "none" }}
|
||||||
|
value={search.value}
|
||||||
|
onInput={(event) => setSearch("value", event.currentTarget.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault()
|
||||||
|
setSearch("value", "")
|
||||||
|
queueMicrotask(() => searchRef?.focus())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searching()) return
|
||||||
|
|
||||||
|
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter") {
|
||||||
|
const ref = listRef
|
||||||
|
if (!ref) return
|
||||||
|
event.stopPropagation()
|
||||||
|
ref.onKeyDown(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
|
||||||
|
if (event.key === "n" || event.key === "p") {
|
||||||
|
const ref = listRef
|
||||||
|
if (!ref) return
|
||||||
|
event.stopPropagation()
|
||||||
|
ref.onKeyDown(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={language.t("session.header.search.placeholder", { project: projectName() })}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
/>
|
||||||
|
<Show when={search.value}>
|
||||||
|
<IconButton
|
||||||
|
icon="circle-x"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-5"
|
||||||
|
aria-label={language.t("common.close")}
|
||||||
|
onClick={() => {
|
||||||
|
setSearch("value", "")
|
||||||
|
queueMicrotask(() => searchRef?.focus())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={searching()}>
|
||||||
|
<List
|
||||||
|
class="flex-1 min-h-0 pb-2 pt-2 !px-2 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
||||||
|
items={items}
|
||||||
|
filter={search.value}
|
||||||
|
filterKeys={["title", "label", "id"]}
|
||||||
|
key={(item) => `${item.directory}:${item.id}`}
|
||||||
|
onSelect={open}
|
||||||
|
ref={(ref) => {
|
||||||
|
listRef = ref
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item) => (
|
||||||
|
<div class="flex flex-col gap-0.5 min-w-0 pr-2 text-left">
|
||||||
|
<span
|
||||||
|
class="text-14-medium text-text-strong truncate"
|
||||||
|
classList={{ "opacity-70": !!item.archived }}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="text-12-regular text-text-weak truncate"
|
||||||
|
classList={{ "opacity-70": !!item.archived }}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="flex-1 min-h-0 flex flex-col" classList={{ hidden: searching() }}>
|
||||||
|
<Show
|
||||||
|
when={workspacesEnabled()}
|
||||||
|
fallback={
|
||||||
|
<>
|
||||||
|
<div class="shrink-0 py-4 px-3">
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("command.session.new")}
|
||||||
|
keybind={command.keybind("session.new")}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
icon="plus-small"
|
||||||
|
class="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
if (!layout.sidebar.opened()) {
|
||||||
|
setState("hoverSession", undefined)
|
||||||
|
setState("hoverProject", undefined)
|
||||||
|
}
|
||||||
|
navigate(`/${base64Encode(p().worktree)}/session`)
|
||||||
|
layout.mobileSidebar.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("command.session.new")}
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-h-0">
|
||||||
|
<LocalWorkspace project={p()} mobile={panelProps.mobile} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<div class="py-4 px-3">
|
<div class="shrink-0 py-4 px-3">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
title={language.t("command.session.new")}
|
title={language.t("workspace.new")}
|
||||||
keybind={command.keybind("session.new")}
|
keybind={command.keybind("workspace.new")}
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<Button
|
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
||||||
size="large"
|
{language.t("workspace.new")}
|
||||||
icon="plus-small"
|
|
||||||
class="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
if (!layout.sidebar.opened()) {
|
|
||||||
setState("hoverSession", undefined)
|
|
||||||
setState("hoverProject", undefined)
|
|
||||||
}
|
|
||||||
navigate(`/${base64Encode(p.worktree)}/session`)
|
|
||||||
layout.mobileSidebar.hide()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("command.session.new")}
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0">
|
<div class="relative flex-1 min-h-0">
|
||||||
<LocalWorkspace project={p} mobile={panelProps.mobile} />
|
<DragDropProvider
|
||||||
|
onDragStart={handleWorkspaceDragStart}
|
||||||
|
onDragEnd={handleWorkspaceDragEnd}
|
||||||
|
onDragOver={handleWorkspaceDragOver}
|
||||||
|
collisionDetector={closestCenter}
|
||||||
|
>
|
||||||
|
<DragDropSensors />
|
||||||
|
<ConstrainDragXAxis />
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
if (!panelProps.mobile) scrollContainerRef = el
|
||||||
|
}}
|
||||||
|
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||||
|
>
|
||||||
|
<SortableProvider ids={workspaces()}>
|
||||||
|
<For each={workspaces()}>
|
||||||
|
{(directory) => (
|
||||||
|
<SortableWorkspace directory={directory} project={p()} mobile={panelProps.mobile} />
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</SortableProvider>
|
||||||
|
</div>
|
||||||
|
<DragOverlay>
|
||||||
|
<WorkspaceDragOverlay />
|
||||||
|
</DragOverlay>
|
||||||
|
</DragDropProvider>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
</Show>
|
||||||
>
|
</div>
|
||||||
<>
|
|
||||||
<div class="py-4 px-3">
|
|
||||||
<TooltipKeybind
|
|
||||||
title={language.t("workspace.new")}
|
|
||||||
keybind={command.keybind("workspace.new")}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p)}>
|
|
||||||
{language.t("workspace.new")}
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
|
||||||
<div class="relative flex-1 min-h-0">
|
|
||||||
<DragDropProvider
|
|
||||||
onDragStart={handleWorkspaceDragStart}
|
|
||||||
onDragEnd={handleWorkspaceDragEnd}
|
|
||||||
onDragOver={handleWorkspaceDragOver}
|
|
||||||
collisionDetector={closestCenter}
|
|
||||||
>
|
|
||||||
<DragDropSensors />
|
|
||||||
<ConstrainDragXAxis />
|
|
||||||
<div
|
|
||||||
ref={(el) => {
|
|
||||||
if (!panelProps.mobile) scrollContainerRef = el
|
|
||||||
}}
|
|
||||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
|
||||||
>
|
|
||||||
<SortableProvider ids={workspaces()}>
|
|
||||||
<For each={workspaces()}>
|
|
||||||
{(directory) => (
|
|
||||||
<SortableWorkspace directory={directory} project={p} mobile={panelProps.mobile} />
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</SortableProvider>
|
|
||||||
</div>
|
|
||||||
<DragOverlay>
|
|
||||||
<WorkspaceDragOverlay />
|
|
||||||
</DragOverlay>
|
|
||||||
</DragDropProvider>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Show>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={providers.all().length > 0 && providers.paid().length === 0}>
|
|
||||||
<div class="shrink-0 px-2 py-3 border-t border-border-weak-base">
|
<div
|
||||||
<div class="rounded-md bg-background-base shadow-xs-border-base">
|
class="shrink-0 px-2 py-3 border-t border-border-weak-base"
|
||||||
<div class="p-3 flex flex-col gap-2">
|
classList={{
|
||||||
<div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
|
hidden: searching() || !(providers.all().length > 0 && providers.paid().length === 0),
|
||||||
<div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
|
}}
|
||||||
<div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
|
>
|
||||||
</div>
|
<div class="rounded-md bg-background-base shadow-xs-border-base">
|
||||||
<Button
|
<div class="p-3 flex flex-col gap-2">
|
||||||
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3"
|
<div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
|
||||||
size="large"
|
<div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
|
||||||
icon="plus"
|
<div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
|
||||||
onClick={connectProvider}
|
|
||||||
>
|
|
||||||
{language.t("command.provider.connect")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3"
|
||||||
|
size="large"
|
||||||
|
icon="plus"
|
||||||
|
onClick={connectProvider}
|
||||||
|
>
|
||||||
|
{language.t("command.provider.connect")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | undefined>(undefined)
|
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | undefined>(undefined)
|
||||||
const [internalFilter, setInternalFilter] = createSignal("")
|
const [internalFilter, setInternalFilter] = createSignal("")
|
||||||
|
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
mouseActive: false,
|
mouseActive: false,
|
||||||
})
|
})
|
||||||
@@ -176,6 +177,14 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
if (e.key === "Enter" && !e.isComposing) {
|
if (e.key === "Enter" && !e.isComposing) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (selected) handleSelect(selected, index)
|
if (selected) handleSelect(selected, index)
|
||||||
|
} else if (props.search) {
|
||||||
|
if (e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && (e.key === "n" || e.key === "p")) {
|
||||||
|
onKeyDown(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||||
|
onKeyDown(e)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onKeyDown(e)
|
onKeyDown(e)
|
||||||
}
|
}
|
||||||
@@ -247,7 +256,21 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
<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-wrapper">
|
<div data-slot="list-search-wrapper">
|
||||||
<div data-slot="list-search" classList={{ [searchProps().class ?? ""]: !!searchProps().class }}>
|
<div
|
||||||
|
data-slot="list-search"
|
||||||
|
classList={{ [searchProps().class ?? ""]: !!searchProps().class }}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
const container = event.currentTarget
|
||||||
|
if (!(container instanceof HTMLElement)) return
|
||||||
|
|
||||||
|
const node = container.querySelector("input, textarea")
|
||||||
|
const input = node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement ? node : inputRef
|
||||||
|
input?.focus()
|
||||||
|
|
||||||
|
// Prevent global listeners (e.g. dnd sensors) from cancelling focus.
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div data-slot="list-search-container">
|
<div data-slot="list-search-container">
|
||||||
<Show when={!searchProps().hideIcon}>
|
<Show when={!searchProps().hideIcon}>
|
||||||
<Icon name="magnifying-glass" />
|
<Icon name="magnifying-glass" />
|
||||||
@@ -257,6 +280,9 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
data-slot="list-search-input"
|
data-slot="list-search-input"
|
||||||
type="text"
|
type="text"
|
||||||
|
ref={(el: HTMLInputElement | HTMLTextAreaElement) => {
|
||||||
|
inputRef = el
|
||||||
|
}}
|
||||||
value={internalFilter()}
|
value={internalFilter()}
|
||||||
onChange={(value) => applyFilter(value)}
|
onChange={(value) => applyFilter(value)}
|
||||||
onKeyDown={handleKey}
|
onKeyDown={handleKey}
|
||||||
@@ -271,7 +297,10 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon="circle-x"
|
icon="circle-x"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => applyFilter("")}
|
onClick={() => {
|
||||||
|
setInternalFilter("")
|
||||||
|
queueMicrotask(() => inputRef?.focus())
|
||||||
|
}}
|
||||||
aria-label={i18n.t("ui.list.clearFilter")}
|
aria-label={i18n.t("ui.list.clearFilter")}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
Reference in New Issue
Block a user