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 */}
+
+
+
+
+
+
+ )}
+
+