refactor(app): use dropdown for project selector (#33984)
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { Popover } from "@kobalte/core/popover"
|
|
||||||
import { For, Show, splitProps, type Accessor, type ComponentProps } from "solid-js"
|
import { For, Show, splitProps, type Accessor, type ComponentProps } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { getProjectAvatarVariant } from "@/context/layout"
|
import { getProjectAvatarVariant } from "@/context/layout"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -25,12 +26,23 @@ export type PromptProjectControls = {
|
|||||||
add: (title: string, server?: string) => void
|
add: (title: string, server?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const actionPrefix = "action:"
|
||||||
|
const projectPrefix = "project:"
|
||||||
|
|
||||||
|
function projectKey(project: PromptProject) {
|
||||||
|
return `${projectPrefix}${encodeURIComponent(project.server?.key ?? "")}:${encodeURIComponent(project.worktree)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionKey(server?: string) {
|
||||||
|
return `${actionPrefix}${encodeURIComponent(server ?? "")}`
|
||||||
|
}
|
||||||
|
|
||||||
export function createPromptProjectController(input: {
|
export function createPromptProjectController(input: {
|
||||||
controls: Accessor<PromptProjectControls>
|
controls: Accessor<PromptProjectControls>
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [store, setStore] = createStore({ open: false, search: "" })
|
const [store, setStore] = createStore({ open: false, search: "", active: "" })
|
||||||
let searchRef: HTMLInputElement | undefined
|
let searchRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
const selected = () => {
|
const selected = () => {
|
||||||
@@ -53,8 +65,25 @@ export function createPromptProjectController(input: {
|
|||||||
.controls()
|
.controls()
|
||||||
.available.map((project) => project.server)
|
.available.map((project) => project.server)
|
||||||
.filter((server, index, all) => server && all.findIndex((item) => item?.key === server.key) === index)
|
.filter((server, index, all) => server && all.findIndex((item) => item?.key === server.key) === index)
|
||||||
|
const keys = () => {
|
||||||
|
if (servers().length <= 1) {
|
||||||
|
return [...projects().map(projectKey), actionKey(servers()[0]?.key)]
|
||||||
|
}
|
||||||
|
return servers().flatMap((server) => [
|
||||||
|
...projects()
|
||||||
|
.filter((project) => project.server?.key === server!.key)
|
||||||
|
.map(projectKey),
|
||||||
|
actionKey(server!.key),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
const initialActive = () => {
|
||||||
|
const selectedKey = selected() ? projectKey(selected()!) : undefined
|
||||||
|
const options = keys()
|
||||||
|
if (selectedKey && options.includes(selectedKey)) return selectedKey
|
||||||
|
return options[0] ?? ""
|
||||||
|
}
|
||||||
const close = () => {
|
const close = () => {
|
||||||
setStore({ open: false, search: "" })
|
setStore({ open: false, search: "", active: "" })
|
||||||
input.onDone()
|
input.onDone()
|
||||||
}
|
}
|
||||||
const select = (project: PromptProject) => {
|
const select = (project: PromptProject) => {
|
||||||
@@ -67,7 +96,7 @@ export function createPromptProjectController(input: {
|
|||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
const add = (server?: string) => {
|
const add = (server?: string) => {
|
||||||
setStore("open", false)
|
setStore({ open: false, search: "", active: "" })
|
||||||
input.controls().add(language.t("command.project.open"), server)
|
input.controls().add(language.t("command.project.open"), server)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +104,11 @@ export function createPromptProjectController(input: {
|
|||||||
selected,
|
selected,
|
||||||
projects,
|
projects,
|
||||||
servers,
|
servers,
|
||||||
|
projectKey,
|
||||||
|
actionKey,
|
||||||
open: () => store.open,
|
open: () => store.open,
|
||||||
search: () => store.search,
|
search: () => store.search,
|
||||||
|
active: () => store.active,
|
||||||
labels: {
|
labels: {
|
||||||
add: () => language.t("session.new.project.add"),
|
add: () => language.t("session.new.project.add"),
|
||||||
clear: () => language.t("common.clear"),
|
clear: () => language.t("common.clear"),
|
||||||
@@ -86,50 +118,188 @@ export function createPromptProjectController(input: {
|
|||||||
add,
|
add,
|
||||||
select,
|
select,
|
||||||
setOpen(open: boolean) {
|
setOpen(open: boolean) {
|
||||||
setStore("open", open)
|
if (open) {
|
||||||
if (open) requestAnimationFrame(() => searchRef?.focus())
|
setStore({ open: true, active: initialActive() })
|
||||||
|
setTimeout(() => requestAnimationFrame(() => searchRef?.focus()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore({ open: false, search: "", active: "" })
|
||||||
},
|
},
|
||||||
setSearch(value: string) {
|
setSearch(value: string) {
|
||||||
setStore("search", value)
|
const search = value.trim().toLowerCase()
|
||||||
|
const first = input
|
||||||
|
.controls()
|
||||||
|
.available.find((project) => !search || displayName(project).toLowerCase().includes(search))
|
||||||
|
setStore({ search: value, active: first ? projectKey(first) : actionKey(servers()[0]?.key) })
|
||||||
|
},
|
||||||
|
clearSearch() {
|
||||||
|
setStore({ search: "", active: initialActive() })
|
||||||
|
setTimeout(() => searchRef?.focus())
|
||||||
|
},
|
||||||
|
setActive(key: string) {
|
||||||
|
setStore("active", key)
|
||||||
|
},
|
||||||
|
moveActive(delta: number) {
|
||||||
|
const options = keys()
|
||||||
|
if (options.length === 0) return
|
||||||
|
const index = options.indexOf(store.active)
|
||||||
|
const start = index === -1 ? 0 : index
|
||||||
|
setStore("active", options[(start + delta + options.length) % options.length])
|
||||||
|
},
|
||||||
|
activeProject() {
|
||||||
|
return store.active.startsWith(projectPrefix)
|
||||||
|
? projects().find((project) => projectKey(project) === store.active)
|
||||||
|
: undefined
|
||||||
|
},
|
||||||
|
activeServer() {
|
||||||
|
return store.active.startsWith(actionPrefix)
|
||||||
|
? decodeURIComponent(store.active.slice(actionPrefix.length)) || undefined
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
setSearchRef(el: HTMLInputElement) {
|
setSearchRef(el: HTMLInputElement) {
|
||||||
searchRef = el
|
searchRef = el
|
||||||
},
|
},
|
||||||
|
focusSearch() {
|
||||||
|
setTimeout(() => requestAnimationFrame(() => searchRef?.focus()))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptProjectController = ReturnType<typeof createPromptProjectController>
|
export type PromptProjectController = ReturnType<typeof createPromptProjectController>
|
||||||
|
|
||||||
export function PromptProjectSelector(props: { controller: PromptProjectController }) {
|
export function PromptProjectSelector(props: { controller: PromptProjectController }) {
|
||||||
|
let contentRef: HTMLDivElement | undefined
|
||||||
|
let restoreTrigger = true
|
||||||
|
|
||||||
|
const activeItem = () =>
|
||||||
|
props.controller.active()
|
||||||
|
? contentRef?.querySelector<HTMLElement>(`[data-option-key="${CSS.escape(props.controller.active())}"]`)
|
||||||
|
: undefined
|
||||||
|
const afterClose = (callback: () => void) => {
|
||||||
|
const complete = () => {
|
||||||
|
if (contentRef?.isConnected) {
|
||||||
|
requestAnimationFrame(complete)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(callback))
|
||||||
|
}
|
||||||
|
requestAnimationFrame(complete)
|
||||||
|
}
|
||||||
|
const selectProject = (project: PromptProject) => {
|
||||||
|
restoreTrigger = false
|
||||||
|
props.controller.setOpen(false)
|
||||||
|
afterClose(() => props.controller.select(project))
|
||||||
|
}
|
||||||
|
const selectAction = (server?: string) => {
|
||||||
|
restoreTrigger = false
|
||||||
|
props.controller.setOpen(false)
|
||||||
|
afterClose(() => props.controller.add(server))
|
||||||
|
}
|
||||||
|
const selectActive = () => {
|
||||||
|
const project = props.controller.activeProject()
|
||||||
|
if (project) {
|
||||||
|
selectProject(project)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectAction(props.controller.activeServer())
|
||||||
|
}
|
||||||
|
const moveActive = (delta: number) => {
|
||||||
|
props.controller.moveActive(delta)
|
||||||
|
queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" }))
|
||||||
|
}
|
||||||
|
const focusPreviousControl = () => {
|
||||||
|
const target = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>(
|
||||||
|
'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter((element) => !contentRef?.contains(element) && !element.hasAttribute("data-focus-trap"))
|
||||||
|
.findLast((element) => element.offsetParent !== null)
|
||||||
|
restoreTrigger = false
|
||||||
|
target?.focus()
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (props.controller.open()) props.controller.setOpen(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const selectedValue = () => {
|
||||||
|
const project = props.controller.selected()
|
||||||
|
return project ? props.controller.projectKey(project) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<DropdownMenu
|
||||||
open={props.controller.open()}
|
open={props.controller.open()}
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
|
shift={-6}
|
||||||
modal={false}
|
modal={false}
|
||||||
onOpenChange={(open) => props.controller.setOpen(open)}
|
onOpenChange={(open) => props.controller.setOpen(open)}
|
||||||
>
|
>
|
||||||
<Popover.Trigger as={ProjectTrigger} controller={props.controller} />
|
<DropdownMenu.Trigger as={ProjectTrigger} controller={props.controller} />
|
||||||
<Popover.Portal>
|
<DropdownMenu.Portal>
|
||||||
<Popover.Content
|
<DropdownMenu.Content
|
||||||
class="w-[243px] overflow-hidden rounded-md bg-v2-background-bg-layer-01 shadow-[var(--v2-elevation-floating)] focus:outline-none"
|
ref={contentRef}
|
||||||
|
id="prompt-project-menu"
|
||||||
|
class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none [&[data-closed]]:!animate-none"
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
|
onPointerDownOutside={() => (restoreTrigger = false)}
|
||||||
|
onFocusOutside={() => (restoreTrigger = false)}
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
if (!restoreTrigger) event.preventDefault()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col p-0.5">
|
<div class="flex flex-col p-0.5">
|
||||||
<div class="flex h-7 items-center gap-2 rounded px-3 text-v2-icon-icon-muted">
|
<div class="flex h-7 items-center gap-2 rounded-sm pl-3 pr-2.5 text-v2-icon-icon-muted">
|
||||||
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
||||||
<input
|
<input
|
||||||
ref={(el) => props.controller.setSearchRef(el)}
|
ref={(el) => props.controller.setSearchRef(el)}
|
||||||
value={props.controller.search()}
|
value={props.controller.search()}
|
||||||
placeholder={props.controller.labels.search()}
|
placeholder={props.controller.labels.search()}
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-controls="prompt-project-menu"
|
||||||
|
aria-activedescendant={props.controller.active() || undefined}
|
||||||
class="h-7 min-w-0 flex-1 border-0 bg-transparent text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint"
|
class="h-7 min-w-0 flex-1 border-0 bg-transparent text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint"
|
||||||
onInput={(event) => props.controller.setSearch(event.currentTarget.value)}
|
onInput={(event) => props.controller.setSearch(event.currentTarget.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (event.shiftKey) {
|
||||||
|
focusPreviousControl()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeItem()?.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.stopPropagation()
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault()
|
||||||
|
props.controller.setOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.altKey || event.metaKey) return
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
event.preventDefault()
|
||||||
|
moveActive(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
event.preventDefault()
|
||||||
|
moveActive(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "Enter" && !event.isComposing) {
|
||||||
|
event.preventDefault()
|
||||||
|
selectActive()
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Show when={props.controller.search().trim()}>
|
<Show when={props.controller.search().trim()}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex size-5 items-center justify-center rounded text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
|
class="flex size-5 items-center justify-center rounded-sm text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
|
||||||
onClick={() => props.controller.setSearch("")}
|
onPointerDown={(event) => event.preventDefault()}
|
||||||
|
onClick={() => props.controller.clearSearch()}
|
||||||
aria-label={props.controller.labels.clear()}
|
aria-label={props.controller.labels.clear()}
|
||||||
>
|
>
|
||||||
<Icon name="close-small" size="small" />
|
<Icon name="close-small" size="small" />
|
||||||
@@ -139,15 +309,13 @@ export function PromptProjectSelector(props: { controller: PromptProjectControll
|
|||||||
<Show
|
<Show
|
||||||
when={props.controller.servers().length > 1}
|
when={props.controller.servers().length > 1}
|
||||||
fallback={
|
fallback={
|
||||||
<For each={props.controller.projects()}>
|
<DropdownMenu.RadioGroup value={selectedValue()}>
|
||||||
{(project) => (
|
<For each={props.controller.projects()}>
|
||||||
<ProjectItem
|
{(project) => (
|
||||||
project={project}
|
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
|
||||||
selected={props.controller.selected()}
|
)}
|
||||||
onSelect={props.controller.select}
|
</For>
|
||||||
/>
|
</DropdownMenu.RadioGroup>
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For each={props.controller.servers()}>
|
<For each={props.controller.servers()}>
|
||||||
@@ -156,19 +324,14 @@ export function PromptProjectSelector(props: { controller: PromptProjectControll
|
|||||||
<div class="flex h-7 select-none items-center pl-1.5 pr-3 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-faint">
|
<div class="flex h-7 select-none items-center pl-1.5 pr-3 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-faint">
|
||||||
{server!.name}
|
{server!.name}
|
||||||
</div>
|
</div>
|
||||||
<For each={props.controller.projects().filter((project) => project.server?.key === server!.key)}>
|
<DropdownMenu.RadioGroup value={selectedValue()}>
|
||||||
{(project) => (
|
<For each={props.controller.projects().filter((project) => project.server?.key === server!.key)}>
|
||||||
<ProjectItem
|
{(project) => (
|
||||||
project={project}
|
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
|
||||||
selected={props.controller.selected()}
|
)}
|
||||||
onSelect={props.controller.select}
|
</For>
|
||||||
/>
|
</DropdownMenu.RadioGroup>
|
||||||
)}
|
<ProjectAction server={server!.key} controller={props.controller} onSelect={selectAction} />
|
||||||
</For>
|
|
||||||
<ProjectAction
|
|
||||||
label={props.controller.labels.add()}
|
|
||||||
onSelect={() => props.controller.add(server!.key)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -178,14 +341,15 @@ export function PromptProjectSelector(props: { controller: PromptProjectControll
|
|||||||
<div class="h-px bg-v2-border-border-muted" />
|
<div class="h-px bg-v2-border-border-muted" />
|
||||||
<div class="flex flex-col p-0.5">
|
<div class="flex flex-col p-0.5">
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
label={props.controller.labels.add()}
|
server={props.controller.servers()[0]?.key}
|
||||||
onSelect={() => props.controller.add(props.controller.servers()[0]?.key)}
|
controller={props.controller}
|
||||||
|
onSelect={selectAction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Popover.Content>
|
</DropdownMenu.Content>
|
||||||
</Popover.Portal>
|
</DropdownMenu.Portal>
|
||||||
</Popover>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,15 +369,28 @@ export function PromptProjectAddButton(props: { controller: PromptProjectControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ProjectTrigger(props: ComponentProps<"button"> & { controller: PromptProjectController }) {
|
function ProjectTrigger(props: ComponentProps<"button"> & { controller: PromptProjectController }) {
|
||||||
const [local, rest] = splitProps(props, ["controller", "class", "onClick"])
|
const [local, rest] = splitProps(props, ["controller", "class", "classList", "onClick", "onKeyDown"])
|
||||||
const project = () => local.controller.selected()
|
const project = () => local.controller.selected()
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...rest}
|
{...rest}
|
||||||
data-action="prompt-project"
|
data-action="prompt-project"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex h-7 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
class="flex h-7 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
||||||
onClick={() => local.controller.setOpen(true)}
|
classList={{
|
||||||
|
...local.classList,
|
||||||
|
"hover:bg-v2-overlay-simple-overlay-hover": !local.controller.open(),
|
||||||
|
"bg-v2-overlay-simple-overlay-pressed": local.controller.open(),
|
||||||
|
}}
|
||||||
|
onClick={local.onClick ?? (() => local.controller.setOpen(true))}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (!local.controller.open() && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof local.onKeyDown === "function") local.onKeyDown(event)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={project()}
|
when={project()}
|
||||||
@@ -237,42 +414,77 @@ function ProjectTrigger(props: ComponentProps<"button"> & { controller: PromptPr
|
|||||||
|
|
||||||
function ProjectItem(props: {
|
function ProjectItem(props: {
|
||||||
project: PromptProject
|
project: PromptProject
|
||||||
selected?: PromptProject
|
controller: PromptProjectController
|
||||||
onSelect: (project: PromptProject) => void
|
onSelect: (project: PromptProject) => void
|
||||||
}) {
|
}) {
|
||||||
|
const key = () => props.controller.projectKey(props.project)
|
||||||
return (
|
return (
|
||||||
<button
|
<DropdownMenu.RadioItem
|
||||||
type="button"
|
id={key()}
|
||||||
class="flex h-7 w-full items-center gap-2 rounded-sm px-3 text-left text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
value={key()}
|
||||||
onClick={() => props.onSelect(props.project)}
|
data-option-key={key()}
|
||||||
|
class="h-7 gap-2 rounded-sm px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base data-[highlighted]:!bg-v2-overlay-simple-overlay-hover"
|
||||||
|
classList={{ "!bg-v2-overlay-simple-overlay-hover": props.controller.active() === key() }}
|
||||||
|
style={{
|
||||||
|
"font-family": "var(--v2-font-family-sans)",
|
||||||
|
"font-size": "13px",
|
||||||
|
"font-weight": 440,
|
||||||
|
"line-height": "20px",
|
||||||
|
"letter-spacing": "-0.04px",
|
||||||
|
color: "var(--v2-text-text-base)",
|
||||||
|
padding: "0 12px",
|
||||||
|
}}
|
||||||
|
closeOnSelect
|
||||||
|
onMouseEnter={() => {
|
||||||
|
props.controller.setActive(key())
|
||||||
|
props.controller.focusSearch()
|
||||||
|
}}
|
||||||
|
onSelect={() => props.onSelect(props.project)}
|
||||||
>
|
>
|
||||||
<ProjectAvatar
|
<ProjectAvatar
|
||||||
fallback={displayName(props.project)}
|
fallback={displayName(props.project)}
|
||||||
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
||||||
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
||||||
/>
|
/>
|
||||||
<span class="min-w-0 flex-1 truncate leading-5">{displayName(props.project)}</span>
|
<DropdownMenu.ItemLabel class="min-w-0 truncate leading-5">{displayName(props.project)}</DropdownMenu.ItemLabel>
|
||||||
<Show
|
<DropdownMenu.ItemIndicator style={{ width: "14px", height: "14px", right: "12px" }}>
|
||||||
when={
|
<IconV2 name="check" size="small" class="shrink-0 text-v2-icon-icon-base" />
|
||||||
props.selected?.worktree === props.project.worktree &&
|
</DropdownMenu.ItemIndicator>
|
||||||
props.selected?.server?.key === props.project.server?.key
|
</DropdownMenu.RadioItem>
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon name="check-small" size="small" class="shrink-0 text-v2-icon-icon-base" />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectAction(props: { label: string; onSelect: () => void }) {
|
function ProjectAction(props: {
|
||||||
|
server?: string
|
||||||
|
controller: PromptProjectController
|
||||||
|
onSelect: (server?: string) => void
|
||||||
|
}) {
|
||||||
|
const key = () => props.controller.actionKey(props.server)
|
||||||
return (
|
return (
|
||||||
<button
|
<DropdownMenu.Item
|
||||||
type="button"
|
id={key()}
|
||||||
class="flex h-7 w-full items-center gap-2 rounded-sm px-3 text-left text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
data-option-key={key()}
|
||||||
onClick={props.onSelect}
|
class="h-7 gap-2 rounded-sm px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base data-[highlighted]:!bg-v2-overlay-simple-overlay-hover"
|
||||||
|
classList={{ "!bg-v2-overlay-simple-overlay-hover": props.controller.active() === key() }}
|
||||||
|
style={{
|
||||||
|
"font-family": "var(--v2-font-family-sans)",
|
||||||
|
"font-size": "13px",
|
||||||
|
"font-weight": 440,
|
||||||
|
"line-height": "20px",
|
||||||
|
"letter-spacing": "-0.04px",
|
||||||
|
color: "var(--v2-text-text-base)",
|
||||||
|
padding: "0 12px",
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
props.controller.setActive(key())
|
||||||
|
props.controller.focusSearch()
|
||||||
|
}}
|
||||||
|
onSelect={() => props.onSelect(props.server)}
|
||||||
>
|
>
|
||||||
<Icon name="plus" size="small" />
|
<Icon name="plus" size="small" />
|
||||||
<span class="min-w-0 flex-1 truncate leading-5">{props.label}</span>
|
<DropdownMenu.ItemLabel class="min-w-0 truncate leading-5">
|
||||||
</button>
|
{props.controller.labels.add()}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ const icons = {
|
|||||||
viewBox: "0 0 16 16",
|
viewBox: "0 0 16 16",
|
||||||
body: `<path d="M5 6.5L8 9.5L11 6.5" stroke="currentColor"/>`,
|
body: `<path d="M5 6.5L8 9.5L11 6.5" stroke="currentColor"/>`,
|
||||||
},
|
},
|
||||||
|
check: {
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
body: `<path d="M3.53613 8.17857L6.39328 11.75L12.4647 4.25" stroke="currentColor"/>`,
|
||||||
|
},
|
||||||
close: {
|
close: {
|
||||||
viewBox: "0 0 20 20",
|
viewBox: "0 0 20 20",
|
||||||
body: `<path d="M14.4446 5.55566L5.55566 14.4446M5.55566 5.55566L14.4446 14.4446" stroke="currentColor" stroke-linejoin="round"/>`,
|
body: `<path d="M14.4446 5.55566L5.55566 14.4446M5.55566 5.55566L14.4446 14.4446" stroke="currentColor" stroke-linejoin="round"/>`,
|
||||||
|
|||||||
Reference in New Issue
Block a user