+ // biome-ignore lint/a11y/noStaticElementInteractions: hover handlers drive a non-interactive popover, no new keyboard semantics
+
{(accentColor || isActive) && (
- }
isLocalWorkspace={hostType === "local-device"}
onCreateSection={handleCreateSection}
onMoveToSection={(targetSectionId) =>
@@ -181,22 +208,29 @@ export function DashboardSidebarWorkspaceItem({
}
const expandedContent = (
-
setIsDeleteDialogOpen(true)}
- onRenameValueChange={setRenameValue}
- onSubmitRename={submitRename}
- onCancelRename={cancelRename}
- />
+ // biome-ignore lint/a11y/noStaticElementInteractions: hover handlers drive a non-interactive popover, no new keyboard semantics
+
+ setIsDeleteDialogOpen(true)}
+ onRenameValueChange={setRenameValue}
+ onSubmitRename={submitRename}
+ onCancelRename={cancelRename}
+ />
+
);
return (
@@ -209,16 +243,6 @@ export function DashboardSidebarWorkspaceItem({
projectId={projectId}
isInSection={isInSection}
isUnread={isUnread}
- onHoverCardOpen={
- hostType === "local-device" ? onHoverCardOpen : undefined
- }
- hoverCardContent={
-
- }
onCreateSection={handleCreateSection}
onMoveToSection={(targetSectionId) =>
moveWorkspaceToSection(id, projectId, targetSectionId)
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx
index 8c74ff2ede4..c650ea308c1 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx
@@ -8,14 +8,8 @@ import {
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@superset/ui/context-menu";
-import {
- HoverCard,
- HoverCardContent,
- HoverCardTrigger,
-} from "@superset/ui/hover-card";
import { eq } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
-import { useState } from "react";
import {
LuArrowRightLeft,
LuArrowUp,
@@ -30,14 +24,13 @@ import {
LuX,
} from "react-icons/lu";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
+import { useDashboardSidebarHover } from "../../../../providers/DashboardSidebarHoverProvider";
interface DashboardSidebarWorkspaceContextMenuProps {
- hoverCardContent?: React.ReactNode;
projectId: string;
isInSection?: boolean;
isLocalWorkspace: boolean;
isUnread: boolean;
- onHoverCardOpen?: () => void;
onCreateSection: () => void;
onMoveToSection: (sectionId: string | null) => void;
onOpenInFinder: () => void;
@@ -55,8 +48,6 @@ export function DashboardSidebarWorkspaceContextMenu({
isInSection,
isLocalWorkspace,
isUnread,
- onHoverCardOpen,
- hoverCardContent,
onCreateSection,
onMoveToSection,
onOpenInFinder,
@@ -69,7 +60,7 @@ export function DashboardSidebarWorkspaceContextMenu({
children,
}: DashboardSidebarWorkspaceContextMenuProps) {
const collections = useCollections();
- const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
+ const { setContextMenuOpen } = useDashboardSidebarHover();
const { data: sections = [] } = useLiveQuery(
(q) =>
q
@@ -86,129 +77,100 @@ export function DashboardSidebarWorkspaceContextMenu({
[collections, projectId],
);
- const menuContent = (
- event.preventDefault()}>
-
-
- Rename
-
- {isLocalWorkspace && (
- <>
-
-
-
- Open in Finder
-
-
-
- Copy Path
-
- >
- )}
- {!isLocalWorkspace && }
-
-
- Copy Branch Name
-
-
-
- {isUnread ? (
- <>
-
- Mark as Read
- >
- ) : (
+ return (
+
+ {children}
+ event.preventDefault()}>
+
+
+ Rename
+
+ {isLocalWorkspace && (
<>
-
- Mark as Unread
+
+
+
+ Open in Finder
+
+
+
+ Copy Path
+
>
)}
-
-
-
-
- New group from workspace
-
- {(sections.length > 0 || isInSection) && }
- {sections.length > 0 && (
-
-
-
- Move to group
-
-
- {sections.map((section) => (
- onMoveToSection(section.id)}
- >
- {section.color && (
-
- )}
- {section.name}
-
- ))}
-
-
- )}
- {isInSection && (
- onMoveToSection(null)}>
-
- Ungroup
+ {!isLocalWorkspace && }
+
+
+ Copy Branch Name
+
+
+
+ {isUnread ? (
+ <>
+
+ Mark as Read
+ >
+ ) : (
+ <>
+
+ Mark as Unread
+ >
+ )}
+
+
+
+
+ New group from workspace
- )}
-
-
-
- Remove from Sidebar
-
- {onDelete ? (
+ {(sections.length > 0 || isInSection) && }
+ {sections.length > 0 && (
+
+
+
+ Move to group
+
+
+ {sections.map((section) => (
+ onMoveToSection(section.id)}
+ >
+ {section.color && (
+
+ )}
+ {section.name}
+
+ ))}
+
+
+ )}
+ {isInSection && (
+ onMoveToSection(null)}>
+
+ Ungroup
+
+ )}
+
-
- Delete
+
+ Remove from Sidebar
- ) : null}
-
- );
-
- if (!hoverCardContent) {
- return (
-
- {children}
- {menuContent}
-
- );
- }
-
- return (
- {
- if (open) {
- onHoverCardOpen?.();
- }
- }}
- >
-
-
- {children}
-
- {menuContent}
-
-
- {hoverCardContent}
-
-
+ {onDelete ? (
+
+
+ Delete
+
+ ) : null}
+
+
);
}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarHoverProvider/DashboardSidebarHoverProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarHoverProvider/DashboardSidebarHoverProvider.tsx
new file mode 100644
index 00000000000..017292f32e9
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarHoverProvider/DashboardSidebarHoverProvider.tsx
@@ -0,0 +1,185 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import type { DashboardSidebarWorkspace } from "../../types";
+
+const OPEN_DELAY_MS = 400;
+const CLOSE_DELAY_MS = 100;
+
+export interface DashboardSidebarHoverPayload {
+ workspace: DashboardSidebarWorkspace;
+ onEditBranchClick: (branchName: string) => void;
+}
+
+interface HoverState {
+ hoveredId: string | null;
+ anchorElement: HTMLElement | null;
+ payload: DashboardSidebarHoverPayload | null;
+}
+
+interface HoverContextValue {
+ hoveredId: string | null;
+ anchorElement: HTMLElement | null;
+ payload: DashboardSidebarHoverPayload | null;
+ contextMenuOpen: boolean;
+ requestOpen: (
+ id: string,
+ anchor: HTMLElement,
+ payload: DashboardSidebarHoverPayload,
+ ) => void;
+ requestClose: (id: string) => void;
+ cancelClose: () => void;
+ forceClose: () => void;
+ setContextMenuOpen: (open: boolean) => void;
+ syncIfHovered: (id: string, payload: DashboardSidebarHoverPayload) => void;
+}
+
+const HoverContext = createContext(null);
+
+export function DashboardSidebarHoverProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [state, setState] = useState({
+ hoveredId: null,
+ anchorElement: null,
+ payload: null,
+ });
+ const [contextMenuOpen, setContextMenuOpen] = useState(false);
+
+ const stateRef = useRef(state);
+ useEffect(() => {
+ stateRef.current = state;
+ }, [state]);
+
+ const openTimerRef = useRef | null>(null);
+ const closeTimerRef = useRef | null>(null);
+
+ const clearOpenTimer = useCallback(() => {
+ if (openTimerRef.current) {
+ clearTimeout(openTimerRef.current);
+ openTimerRef.current = null;
+ }
+ }, []);
+ const clearCloseTimer = useCallback(() => {
+ if (closeTimerRef.current) {
+ clearTimeout(closeTimerRef.current);
+ closeTimerRef.current = null;
+ }
+ }, []);
+
+ const requestOpen = useCallback(
+ (id, anchor, payload) => {
+ clearCloseTimer();
+ if (stateRef.current.hoveredId !== null) {
+ clearOpenTimer();
+ setState({ hoveredId: id, anchorElement: anchor, payload });
+ return;
+ }
+ clearOpenTimer();
+ openTimerRef.current = setTimeout(() => {
+ setState({ hoveredId: id, anchorElement: anchor, payload });
+ openTimerRef.current = null;
+ }, OPEN_DELAY_MS);
+ },
+ [clearCloseTimer, clearOpenTimer],
+ );
+
+ const requestClose = useCallback(
+ (id) => {
+ if (openTimerRef.current && stateRef.current.hoveredId === null) {
+ // Pending open for this id — cancel it.
+ clearOpenTimer();
+ return;
+ }
+ if (stateRef.current.hoveredId !== id) return;
+ clearCloseTimer();
+ closeTimerRef.current = setTimeout(() => {
+ setState({ hoveredId: null, anchorElement: null, payload: null });
+ closeTimerRef.current = null;
+ }, CLOSE_DELAY_MS);
+ },
+ [clearCloseTimer, clearOpenTimer],
+ );
+
+ const cancelClose = useCallback(() => {
+ clearCloseTimer();
+ }, [clearCloseTimer]);
+
+ const forceClose = useCallback(() => {
+ clearOpenTimer();
+ clearCloseTimer();
+ setState({ hoveredId: null, anchorElement: null, payload: null });
+ }, [clearCloseTimer, clearOpenTimer]);
+
+ const syncIfHovered = useCallback(
+ (id, payload) => {
+ setState((prev) => {
+ if (prev.hoveredId !== id) return prev;
+ if (
+ prev.payload?.workspace === payload.workspace &&
+ prev.payload.onEditBranchClick === payload.onEditBranchClick
+ ) {
+ return prev;
+ }
+ return { ...prev, payload };
+ });
+ },
+ [],
+ );
+
+ useEffect(
+ () => () => {
+ clearOpenTimer();
+ clearCloseTimer();
+ },
+ [clearCloseTimer, clearOpenTimer],
+ );
+
+ const value = useMemo(
+ () => ({
+ hoveredId: state.hoveredId,
+ anchorElement: state.anchorElement,
+ payload: state.payload,
+ contextMenuOpen,
+ requestOpen,
+ requestClose,
+ cancelClose,
+ forceClose,
+ setContextMenuOpen,
+ syncIfHovered,
+ }),
+ [
+ state.hoveredId,
+ state.anchorElement,
+ state.payload,
+ contextMenuOpen,
+ requestOpen,
+ requestClose,
+ cancelClose,
+ forceClose,
+ syncIfHovered,
+ ],
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useDashboardSidebarHover() {
+ const ctx = useContext(HoverContext);
+ if (!ctx) {
+ throw new Error(
+ "useDashboardSidebarHover must be used inside DashboardSidebarHoverProvider",
+ );
+ }
+ return ctx;
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarHoverProvider/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarHoverProvider/index.ts
new file mode 100644
index 00000000000..d8e00156972
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarHoverProvider/index.ts
@@ -0,0 +1,5 @@
+export {
+ type DashboardSidebarHoverPayload,
+ DashboardSidebarHoverProvider,
+ useDashboardSidebarHover,
+} from "./DashboardSidebarHoverProvider";
diff --git a/bun.lock b/bun.lock
index ed538233849..f68abeb726f 100644
--- a/bun.lock
+++ b/bun.lock
@@ -110,7 +110,7 @@
},
"apps/desktop": {
"name": "@superset/desktop",
- "version": "1.6.1",
+ "version": "1.6.2",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.43",
"@ai-sdk/openai": "3.0.36",