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 32a6315d..39a7b652 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,9 +1,105 @@ -import { useEffect, useRef, useState } from 'react'; -import { Edit2, Trash2 } from 'lucide-react'; +import { useEffect, useRef, useState, memo } from 'react'; +import type { LucideIcon } from 'lucide-react'; +import { Edit2, Trash2, Palette, ChevronRight, Moon, Sun, Monitor } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { useAppStore } from '@/store/app-store'; +import { type ThemeMode, useAppStore } from '@/store/app-store'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; 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'; + +// Constants for z-index values +const Z_INDEX = { + CONTEXT_MENU: 100, + THEME_SUBMENU: 101, +} as const; + +// Theme option type - using ThemeMode for type safety +interface ThemeOption { + value: ThemeMode; + label: string; + icon: LucideIcon; + color: string; +} + +// Reusable theme button component to avoid duplication (DRY principle) +interface ThemeButtonProps { + option: ThemeOption; + isSelected: boolean; + onPointerEnter: () => void; + onPointerLeave: (e: React.PointerEvent) => void; + onClick: () => void; +} + +const ThemeButton = memo(function ThemeButton({ + option, + isSelected, + onPointerEnter, + onPointerLeave, + onClick, +}: ThemeButtonProps) { + const Icon = option.icon; + return ( + + ); +}); + +// Reusable theme column component +interface ThemeColumnProps { + title: string; + icon: LucideIcon; + themes: ThemeOption[]; + selectedTheme: ThemeMode | null; + onPreviewEnter: (value: ThemeMode) => void; + onPreviewLeave: (e: React.PointerEvent) => void; + onSelect: (value: ThemeMode) => void; +} + +const ThemeColumn = memo(function ThemeColumn({ + title, + icon: Icon, + themes, + selectedTheme, + onPreviewEnter, + onPreviewLeave, + onSelect, +}: ThemeColumnProps) { + return ( +
+
+ + {title} +
+
+ {themes.map((option) => ( + onPreviewEnter(option.value)} + onPointerLeave={onPreviewLeave} + onClick={() => onSelect(option.value)} + /> + ))} +
+
+ ); +}); interface ProjectContextMenuProps { project: Project; @@ -19,18 +115,30 @@ export function ProjectContextMenu({ onEdit, }: ProjectContextMenuProps) { const menuRef = useRef(null); - const { moveProjectToTrash } = useAppStore(); + const { + moveProjectToTrash, + theme: globalTheme, + setTheme, + setProjectTheme, + setPreviewTheme, + } = useAppStore(); const [showRemoveDialog, setShowRemoveDialog] = useState(false); + const [showThemeSubmenu, setShowThemeSubmenu] = 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)) { + setPreviewTheme(null); onClose(); } }; const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { + setPreviewTheme(null); onClose(); } }; @@ -42,7 +150,7 @@ export function ProjectContextMenu({ document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscape); }; - }, [onClose]); + }, [onClose, setPreviewTheme]); const handleEdit = () => { onEdit(project); @@ -52,6 +160,17 @@ 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 handleConfirmRemove = () => { moveProjectToTrash(project.id); onClose(); @@ -62,7 +181,7 @@ export function ProjectContextMenu({
@@ -88,6 +208,98 @@ export function ProjectContextMenu({ Edit Name & Icon + {/* Theme Submenu Trigger */} +
setShowThemeSubmenu(true)} + onMouseLeave={() => { + setShowThemeSubmenu(false); + setPreviewTheme(null); + }} + > + + + {/* Theme Submenu */} + {showThemeSubmenu && ( +
+
+ {/* Use Global Option */} + + +
+ + {/* Two Column Layout - Using reusable ThemeColumn component */} +
+ + +
+
+
+ )} +
+