refactor(desktop): enhance project tile interaction with suppress hover functionality (#15214)

This commit is contained in:
OpeOginni
2026-02-26 18:00:11 +01:00
committed by GitHub
parent 08f056d412
commit 5745ee87ba

View File

@@ -1,4 +1,5 @@
import { createEffect, createMemo, createSignal, For, Show, type Accessor, type JSX } from "solid-js" import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { base64Encode } from "@opencode-ai/util/encode" import { base64Encode } from "@opencode-ai/util/encode"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { ContextMenu } from "@opencode-ai/ui/context-menu" import { ContextMenu } from "@opencode-ai/ui/context-menu"
@@ -7,7 +8,7 @@ import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { createSortable } from "@thisbeyond/solid-dnd" import { createSortable } from "@thisbeyond/solid-dnd"
import { type LocalProject } from "@/context/layout" import { useLayout, type LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification" import { useNotification } from "@/context/notification"
@@ -60,6 +61,7 @@ const ProjectTile = (props: {
selected: Accessor<boolean> selected: Accessor<boolean>
active: Accessor<boolean> active: Accessor<boolean>
overlay: Accessor<boolean> overlay: Accessor<boolean>
suppressHover: Accessor<boolean>
dirs: Accessor<string[]> dirs: Accessor<string[]>
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
onProjectMouseLeave: (worktree: string) => void onProjectMouseLeave: (worktree: string) => void
@@ -71,9 +73,11 @@ const ProjectTile = (props: {
closeProject: (directory: string) => void closeProject: (directory: string) => void
setMenu: (value: boolean) => void setMenu: (value: boolean) => void
setOpen: (value: boolean) => void setOpen: (value: boolean) => void
setSuppressHover: (value: boolean) => void
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
}): JSX.Element => { }): JSX.Element => {
const notification = useNotification() const notification = useNotification()
const layout = useLayout()
const unseenCount = createMemo(() => const unseenCount = createMemo(() =>
props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
) )
@@ -107,17 +111,28 @@ const ProjectTile = (props: {
}} }}
onMouseEnter={(event: MouseEvent) => { onMouseEnter={(event: MouseEvent) => {
if (!props.overlay()) return if (!props.overlay()) return
if (props.suppressHover()) return
props.onProjectMouseEnter(props.project.worktree, event) props.onProjectMouseEnter(props.project.worktree, event)
}} }}
onMouseLeave={() => { onMouseLeave={() => {
if (props.suppressHover()) props.setSuppressHover(false)
if (!props.overlay()) return if (!props.overlay()) return
props.onProjectMouseLeave(props.project.worktree) props.onProjectMouseLeave(props.project.worktree)
}} }}
onFocus={() => { onFocus={() => {
if (!props.overlay()) return if (!props.overlay()) return
if (props.suppressHover()) return
props.onProjectFocus(props.project.worktree) props.onProjectFocus(props.project.worktree)
}} }}
onClick={() => props.navigateToProject(props.project.worktree)} onClick={() => {
if (props.selected()) {
props.setSuppressHover(true)
layout.sidebar.toggle()
return
}
props.setSuppressHover(false)
props.navigateToProject(props.project.worktree)
}}
onBlur={() => props.setOpen(false)} onBlur={() => props.setOpen(false)}
> >
<ProjectIcon project={props.project} notify /> <ProjectIcon project={props.project} notify />
@@ -278,16 +293,19 @@ export const SortableProject = (props: {
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
const [open, setOpen] = createSignal(false) const [state, setState] = createStore({
const [menu, setMenu] = createSignal(false) open: false,
menu: false,
suppressHover: false,
})
const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened())
const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened())
const active = createMemo(() => const active = createMemo(() =>
projectTileActive({ projectTileActive({
menu: menu(), menu: state.menu,
preview: preview(), preview: preview(),
open: open(), open: state.open,
overlay: overlay(), overlay: overlay(),
hoverProject: props.ctx.hoverProject(), hoverProject: props.ctx.hoverProject(),
worktree: props.project.worktree, worktree: props.project.worktree,
@@ -296,8 +314,14 @@ export const SortableProject = (props: {
createEffect(() => { createEffect(() => {
if (preview()) return if (preview()) return
if (!open()) return if (!state.open) return
setOpen(false) setState("open", false)
})
createEffect(() => {
if (!selected()) return
if (!state.open) return
setState("open", false)
}) })
const label = (directory: string) => { const label = (directory: string) => {
@@ -328,6 +352,7 @@ export const SortableProject = (props: {
selected={selected} selected={selected}
active={active} active={active}
overlay={overlay} overlay={overlay}
suppressHover={() => state.suppressHover}
dirs={dirs} dirs={dirs}
onProjectMouseEnter={props.ctx.onProjectMouseEnter} onProjectMouseEnter={props.ctx.onProjectMouseEnter}
onProjectMouseLeave={props.ctx.onProjectMouseLeave} onProjectMouseLeave={props.ctx.onProjectMouseLeave}
@@ -337,8 +362,9 @@ export const SortableProject = (props: {
toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces} toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces}
workspacesEnabled={props.ctx.workspacesEnabled} workspacesEnabled={props.ctx.workspacesEnabled}
closeProject={props.ctx.closeProject} closeProject={props.ctx.closeProject}
setMenu={setMenu} setMenu={(value) => setState("menu", value)}
setOpen={setOpen} setOpen={(value) => setState("open", value)}
setSuppressHover={(value) => setState("suppressHover", value)}
language={language} language={language}
/> />
) )
@@ -346,17 +372,18 @@ export const SortableProject = (props: {
return ( return (
// @ts-ignore // @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}> <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Show when={preview()} fallback={tile()}> <Show when={preview() && !selected()} fallback={tile()}>
<HoverCard <HoverCard
open={open() && !menu()} open={!state.suppressHover && state.open && !state.menu}
openDelay={0} openDelay={0}
closeDelay={0} closeDelay={0}
placement="right-start" placement="right-start"
gutter={6} gutter={6}
trigger={tile()} trigger={tile()}
onOpenChange={(value) => { onOpenChange={(value) => {
if (menu()) return if (state.menu) return
setOpen(value) if (value && state.suppressHover) return
setState("open", value)
if (value) props.ctx.setHoverSession(undefined) if (value) props.ctx.setHoverSession(undefined)
}} }}
> >
@@ -371,7 +398,7 @@ export const SortableProject = (props: {
projectChildren={projectChildren} projectChildren={projectChildren}
workspaceSessions={workspaceSessions} workspaceSessions={workspaceSessions}
workspaceChildren={workspaceChildren} workspaceChildren={workspaceChildren}
setOpen={setOpen} setOpen={(value) => setState("open", value)}
ctx={props.ctx} ctx={props.ctx}
language={language} language={language}
/> />