From 327f56fb757086f1e9c3f5752f443f988914f67a Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 26 Feb 2026 14:24:42 +0100 Subject: [PATCH 1/2] refactor(desktop): enhance project tile interaction with suppress hover functionality - Introduced `suppressHover` state to manage hover behavior during project tile interactions. - Updated event handlers to conditionally suppress hover effects based on the sidebar's open state. - Refactored state management for project tile visibility and menu interactions using `createStore` for better state handling. --- .../app/src/pages/layout/sidebar-project.tsx | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index e19e6f430f0..d37781d7c3a 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -1,4 +1,5 @@ -import { createEffect, createMemo, createSignal, For, Show, type Accessor, type JSX } from "solid-js" +import { createEffect, createMemo, For, on, Show, type Accessor, type JSX } from "solid-js" +import { createStore } from "solid-js/store" import { base64Encode } from "@opencode-ai/util/encode" import { Button } from "@opencode-ai/ui/button" 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 { Tooltip } from "@opencode-ai/ui/tooltip" 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 { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" @@ -60,6 +61,7 @@ const ProjectTile = (props: { selected: Accessor active: Accessor overlay: Accessor + suppressHover: Accessor dirs: Accessor onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseLeave: (worktree: string) => void @@ -71,9 +73,11 @@ const ProjectTile = (props: { closeProject: (directory: string) => void setMenu: (value: boolean) => void setOpen: (value: boolean) => void + setSuppressHover: (value: boolean) => void language: ReturnType }): JSX.Element => { const notification = useNotification() + const layout = useLayout() const unseenCount = createMemo(() => props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) @@ -107,17 +111,30 @@ const ProjectTile = (props: { }} onMouseEnter={(event: MouseEvent) => { if (!props.overlay()) return + if (props.suppressHover()) return props.onProjectMouseEnter(props.project.worktree, event) }} onMouseLeave={() => { if (!props.overlay()) return + if (props.suppressHover()) props.setSuppressHover(false) props.onProjectMouseLeave(props.project.worktree) }} onFocus={() => { if (!props.overlay()) return + if (props.suppressHover()) return props.onProjectFocus(props.project.worktree) }} - onClick={() => props.navigateToProject(props.project.worktree)} + onClick={() => { + if (props.selected()) { + const closing = layout.sidebar.opened() + // If the click is to togle to close we suppress the hover to prevent a flitch of closing then immediate opening based on the hover + if (closing) props.setSuppressHover(true) + layout.sidebar.toggle() + return + } + props.setSuppressHover(false) + props.navigateToProject(props.project.worktree) + }} onBlur={() => props.setOpen(false)} > @@ -278,26 +295,41 @@ export const SortableProject = (props: { const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) - const [open, setOpen] = createSignal(false) - const [menu, setMenu] = createSignal(false) + const [state, setState] = createStore({ + open: false, + menu: false, + suppressHover: false, + }) const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) const active = createMemo(() => projectTileActive({ - menu: menu(), + menu: state.menu, preview: preview(), - open: open(), + open: state.open, overlay: overlay(), hoverProject: props.ctx.hoverProject(), worktree: props.project.worktree, }), ) + createEffect( + on( + () => props.ctx.sidebarOpened(), + (opened, prev) => { + if (!opened) return + if (prev === undefined) return + if (!state.suppressHover) return + setState("suppressHover", false) + }, + ), + ) + createEffect(() => { if (preview()) return - if (!open()) return - setOpen(false) + if (!state.open) return + setState("open", false) }) const label = (directory: string) => { @@ -328,6 +360,7 @@ export const SortableProject = (props: { selected={selected} active={active} overlay={overlay} + suppressHover={() => state.suppressHover} dirs={dirs} onProjectMouseEnter={props.ctx.onProjectMouseEnter} onProjectMouseLeave={props.ctx.onProjectMouseLeave} @@ -337,8 +370,9 @@ export const SortableProject = (props: { toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces} workspacesEnabled={props.ctx.workspacesEnabled} closeProject={props.ctx.closeProject} - setMenu={setMenu} - setOpen={setOpen} + setMenu={(value) => setState("menu", value)} + setOpen={(value) => setState("open", value)} + setSuppressHover={(value) => setState("suppressHover", value)} language={language} /> ) @@ -348,15 +382,15 @@ export const SortableProject = (props: {
{ - if (menu()) return - setOpen(value) + if (state.menu) return + setState("open", value) if (value) props.ctx.setHoverSession(undefined) }} > @@ -371,7 +405,7 @@ export const SortableProject = (props: { projectChildren={projectChildren} workspaceSessions={workspaceSessions} workspaceChildren={workspaceChildren} - setOpen={setOpen} + setOpen={(value) => setState("open", value)} ctx={props.ctx} language={language} /> From 77d2b345478bde2c796c86086b9bd08300fb1e05 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 26 Feb 2026 14:42:32 +0100 Subject: [PATCH 2/2] feat(desktop): streamline project tile hover behavior and state management for hover card and temp sidebar --- .../app/src/pages/layout/sidebar-project.tsx | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index d37781d7c3a..3c3652e38f3 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, For, on, 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 { Button } from "@opencode-ai/ui/button" @@ -115,8 +115,8 @@ const ProjectTile = (props: { props.onProjectMouseEnter(props.project.worktree, event) }} onMouseLeave={() => { - if (!props.overlay()) return if (props.suppressHover()) props.setSuppressHover(false) + if (!props.overlay()) return props.onProjectMouseLeave(props.project.worktree) }} onFocus={() => { @@ -126,9 +126,7 @@ const ProjectTile = (props: { }} onClick={() => { if (props.selected()) { - const closing = layout.sidebar.opened() - // If the click is to togle to close we suppress the hover to prevent a flitch of closing then immediate opening based on the hover - if (closing) props.setSuppressHover(true) + props.setSuppressHover(true) layout.sidebar.toggle() return } @@ -314,24 +312,18 @@ export const SortableProject = (props: { }), ) - createEffect( - on( - () => props.ctx.sidebarOpened(), - (opened, prev) => { - if (!opened) return - if (prev === undefined) return - if (!state.suppressHover) return - setState("suppressHover", false) - }, - ), - ) - createEffect(() => { if (preview()) return if (!state.open) return setState("open", false) }) + createEffect(() => { + if (!selected()) return + if (!state.open) return + setState("open", false) + }) + const label = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) const kind = @@ -380,9 +372,9 @@ export const SortableProject = (props: { return ( // @ts-ignore
- + { if (state.menu) return + if (value && state.suppressHover) return setState("open", value) if (value) props.ctx.setHoverSession(undefined) }}