diff --git a/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx b/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx index 39a7b652..af63af32 100644 --- a/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx +++ b/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx @@ -1,6 +1,7 @@ -import { useEffect, useRef, useState, memo } from 'react'; +import { useEffect, useRef, useState, memo, useCallback } from 'react'; import type { LucideIcon } from 'lucide-react'; import { Edit2, Trash2, Palette, ChevronRight, Moon, Sun, Monitor } from 'lucide-react'; +import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { type ThemeMode, useAppStore } from '@/store/app-store'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; @@ -8,6 +9,9 @@ import type { Project } from '@/lib/electron'; import { PROJECT_DARK_THEMES, PROJECT_LIGHT_THEMES } from '@/components/layout/sidebar/constants'; import { useThemePreview } from '@/components/layout/sidebar/hooks'; +// Constant for "use global theme" option +const USE_GLOBAL_THEME = '' as const; + // Constants for z-index values const Z_INDEX = { CONTEXT_MENU: 100, @@ -124,19 +128,26 @@ export function ProjectContextMenu({ } = useAppStore(); const [showRemoveDialog, setShowRemoveDialog] = useState(false); const [showThemeSubmenu, setShowThemeSubmenu] = useState(false); + const [removeConfirmed, setRemoveConfirmed] = useState(false); const themeSubmenuRef = useRef(null); const { handlePreviewEnter, handlePreviewLeave } = useThemePreview({ setPreviewTheme }); useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + const handleClickOutside = (event: globalThis.MouseEvent) => { + // Don't close if a confirmation dialog is open (dialog is in a portal) + if (showRemoveDialog) return; + + if (menuRef.current && !menuRef.current.contains(event.target as globalThis.Node)) { setPreviewTheme(null); onClose(); } }; - const handleEscape = (event: KeyboardEvent) => { + const handleEscape = (event: globalThis.KeyboardEvent) => { + // Don't close if a confirmation dialog is open (let the dialog handle escape) + if (showRemoveDialog) return; + if (event.key === 'Escape') { setPreviewTheme(null); onClose(); @@ -150,7 +161,7 @@ export function ProjectContextMenu({ document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscape); }; - }, [onClose, setPreviewTheme]); + }, [onClose, setPreviewTheme, showRemoveDialog]); const handleEdit = () => { onEdit(project); @@ -160,166 +171,187 @@ export function ProjectContextMenu({ setShowRemoveDialog(true); }; - const handleThemeSelect = (value: ThemeMode | '') => { - setPreviewTheme(null); - if (value !== '') { - setTheme(value); - } else { - setTheme(globalTheme); - } - setProjectTheme(project.id, value === '' ? null : value); - setShowThemeSubmenu(false); - }; + const handleThemeSelect = useCallback( + (value: ThemeMode | typeof USE_GLOBAL_THEME) => { + setPreviewTheme(null); + const isUsingGlobal = value === USE_GLOBAL_THEME; + setTheme(isUsingGlobal ? globalTheme : value); + setProjectTheme(project.id, isUsingGlobal ? null : value); + setShowThemeSubmenu(false); + }, + [globalTheme, project.id, setPreviewTheme, setProjectTheme, setTheme] + ); - const handleConfirmRemove = () => { + const handleConfirmRemove = useCallback(() => { moveProjectToTrash(project.id); - onClose(); - }; + toast.success('Project removed', { + description: `${project.name} has been removed from your projects list`, + }); + setRemoveConfirmed(true); + }, [moveProjectToTrash, project.id, project.name]); + + const handleDialogClose = useCallback( + (isOpen: boolean) => { + setShowRemoveDialog(isOpen); + // Close the context menu when dialog closes (whether confirmed or cancelled) + // This prevents the context menu from reappearing after dialog interaction + if (!isOpen) { + // Reset confirmation state + setRemoveConfirmed(false); + // Always close the context menu when dialog closes + onClose(); + } + }, + [onClose] + ); return ( <> -
-
- - - {/* Theme Submenu Trigger */} -
setShowThemeSubmenu(true)} - onMouseLeave={() => { - setShowThemeSubmenu(false); - setPreviewTheme(null); - }} - > + {/* Hide context menu when confirm dialog is open */} + {!showRemoveDialog && ( +
+
- {/* Theme Submenu */} - {showThemeSubmenu && ( -
setShowThemeSubmenu(true)} + onMouseLeave={() => { + setShowThemeSubmenu(false); + setPreviewTheme(null); + }} + > + + + Project Theme + {project.theme && ( + + {project.theme} + + )} + + -
+ {/* Theme Submenu */} + {showThemeSubmenu && ( +
+
+ {/* Use Global Option */} + - {/* Two Column Layout - Using reusable ThemeColumn component */} -
- - +
+ + {/* Two Column Layout - Using reusable ThemeColumn component */} +
+ + +
-
- )} -
+ )} +
- + +
-
+ )}