refactor(ProjectSelector): improve project selection logic and UI/UX

This commit is contained in:
Illia Filippov
2025-12-23 13:44:09 +01:00
parent d4d4b8fb3d
commit 5a5165818e
2 changed files with 25 additions and 21 deletions

View File

@@ -1,3 +1,4 @@
import { useCallback } from 'react';
import { import {
Folder, Folder,
ChevronDown, ChevronDown,
@@ -47,7 +48,6 @@ export function ProjectSelectorWithOptions({
setIsProjectPickerOpen, setIsProjectPickerOpen,
setShowDeleteProjectDialog, setShowDeleteProjectDialog,
}: ProjectSelectorWithOptionsProps) { }: ProjectSelectorWithOptionsProps) {
// Get data from store
const { const {
projects, projects,
currentProject, currentProject,
@@ -59,7 +59,6 @@ export function ProjectSelectorWithOptions({
clearProjectHistory, clearProjectHistory,
} = useAppStore(); } = useAppStore();
// Get keyboard shortcuts
const shortcuts = useKeyboardShortcutsConfig(); const shortcuts = useKeyboardShortcutsConfig();
const { const {
projectSearchQuery, projectSearchQuery,
@@ -75,10 +74,16 @@ export function ProjectSelectorWithOptions({
setCurrentProject, setCurrentProject,
}); });
// Drag-and-drop handlers
const { sensors, handleDragEnd } = useDragAndDrop({ projects, reorderProjects }); const { sensors, handleDragEnd } = useDragAndDrop({ projects, reorderProjects });
// Theme management const handleProjectSelect = useCallback(
(project: (typeof projects)[number]) => {
setCurrentProject(project);
setIsProjectPickerOpen(false);
},
[setCurrentProject, setIsProjectPickerOpen]
);
const { const {
globalTheme, globalTheme,
setTheme, setTheme,
@@ -107,7 +112,6 @@ export function ProjectSelectorWithOptions({
'shadow-sm shadow-black/5', 'shadow-sm shadow-black/5',
'text-foreground titlebar-no-drag min-w-0', 'text-foreground titlebar-no-drag min-w-0',
'transition-all duration-200 ease-out', 'transition-all duration-200 ease-out',
'hover:scale-[1.01] active:scale-[0.99]',
isProjectPickerOpen && isProjectPickerOpen &&
'from-brand-500/10 to-brand-600/5 border-brand-500/30 ring-2 ring-brand-500/20 shadow-lg shadow-brand-500/5' 'from-brand-500/10 to-brand-600/5 border-brand-500/30 ring-2 ring-brand-500/20 shadow-lg shadow-brand-500/5'
)} )}
@@ -140,7 +144,7 @@ export function ProjectSelectorWithOptions({
align="start" align="start"
data-testid="project-picker-dropdown" data-testid="project-picker-dropdown"
> >
{/* Search input for type-ahead filtering */} {/* Search input */}
<div className="px-1 pb-2"> <div className="px-1 pb-2">
<div className="relative"> <div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
@@ -151,10 +155,10 @@ export function ProjectSelectorWithOptions({
value={projectSearchQuery} value={projectSearchQuery}
onChange={(e) => setProjectSearchQuery(e.target.value)} onChange={(e) => setProjectSearchQuery(e.target.value)}
className={cn( className={cn(
'w-full h-9 pl-8 pr-3 text-sm rounded-lg', 'w-full h-8 pl-8 pr-3 text-sm rounded-lg',
'border border-border bg-background/50', 'border border-border bg-background/50',
'text-foreground placeholder:text-muted-foreground', 'text-foreground placeholder:text-muted-foreground',
'focus:outline-none focus:ring-2 focus:ring-brand-500/30 focus:border-brand-500/50', 'focus:outline-none focus:ring-1 focus:ring-brand-500/30 focus:border-brand-500/50',
'transition-all duration-200' 'transition-all duration-200'
)} )}
data-testid="project-search-input" data-testid="project-search-input"
@@ -176,17 +180,14 @@ export function ProjectSelectorWithOptions({
items={filteredProjects.map((p) => p.id)} items={filteredProjects.map((p) => p.id)}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
<div className="space-y-0.5 max-h-64 overflow-y-auto"> <div className="space-y-0.5 max-h-64 overflow-y-auto overflow-x-hidden">
{filteredProjects.map((project, index) => ( {filteredProjects.map((project, index) => (
<SortableProjectItem <SortableProjectItem
key={project.id} key={project.id}
project={project} project={project}
currentProjectId={currentProject?.id} currentProjectId={currentProject?.id}
isHighlighted={index === selectedProjectIndex} isHighlighted={index === selectedProjectIndex}
onSelect={(p) => { onSelect={handleProjectSelect}
setCurrentProject(p);
setIsProjectPickerOpen(false);
}}
/> />
))} ))}
</div> </div>
@@ -197,9 +198,9 @@ export function ProjectSelectorWithOptions({
{/* Keyboard hint */} {/* Keyboard hint */}
<div className="px-2 pt-2 mt-1.5 border-t border-border/50"> <div className="px-2 pt-2 mt-1.5 border-t border-border/50">
<p className="text-[10px] text-muted-foreground text-center tracking-wide"> <p className="text-[10px] text-muted-foreground text-center tracking-wide">
<span className="text-foreground/60">arrow</span> navigate{' '} <span className="text-foreground/60"></span> navigate{' '}
<span className="mx-1 text-foreground/30">|</span>{' '} <span className="mx-1 text-foreground/30">|</span>{' '}
<span className="text-foreground/60">enter</span> select{' '} <span className="text-foreground/60"></span> select{' '}
<span className="mx-1 text-foreground/30">|</span>{' '} <span className="mx-1 text-foreground/30">|</span>{' '}
<span className="text-foreground/60">esc</span> close <span className="text-foreground/60">esc</span> close
</p> </p>
@@ -207,7 +208,7 @@ export function ProjectSelectorWithOptions({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* Project Options Menu - theme and history */} {/* Project Options Menu */}
{currentProject && ( {currentProject && (
<DropdownMenu <DropdownMenu
onOpenChange={(open) => { onOpenChange={(open) => {
@@ -224,8 +225,7 @@ export function ProjectSelectorWithOptions({
'text-muted-foreground hover:text-foreground', 'text-muted-foreground hover:text-foreground',
'bg-transparent hover:bg-accent/60', 'bg-transparent hover:bg-accent/60',
'border border-border/50 hover:border-border', 'border border-border/50 hover:border-border',
'transition-all duration-200 ease-out titlebar-no-drag', 'transition-all duration-200 ease-out titlebar-no-drag'
'hover:scale-[1.02] active:scale-[0.98]'
)} )}
title="Project options" title="Project options"
data-testid="project-options-menu" data-testid="project-options-menu"
@@ -253,7 +253,6 @@ export function ProjectSelectorWithOptions({
setPreviewTheme(null); setPreviewTheme(null);
}} }}
> >
{/* Use Global Option */}
<DropdownMenuRadioGroup <DropdownMenuRadioGroup
value={currentProject.theme || ''} value={currentProject.theme || ''}
onValueChange={(value) => { onValueChange={(value) => {
@@ -329,7 +328,7 @@ export function ProjectSelectorWithOptions({
</DropdownMenuSubContent> </DropdownMenuSubContent>
</DropdownMenuSub> </DropdownMenuSub>
{/* Project History Section - only show when there's history */} {/* Project History Section */}
{projectHistory.length > 1 && ( {projectHistory.length > 1 && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />

View File

@@ -45,7 +45,12 @@ export function SortableProjectItem({
{/* Project content - clickable area */} {/* Project content - clickable area */}
<div className="flex items-center gap-2.5 flex-1 min-w-0" onClick={() => onSelect(project)}> <div className="flex items-center gap-2.5 flex-1 min-w-0" onClick={() => onSelect(project)}>
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" /> <Folder
className={cn(
'h-4 w-4 shrink-0',
currentProjectId === project.id ? 'text-brand-500' : 'text-muted-foreground'
)}
/>
<span className="flex-1 truncate text-sm font-medium">{project.name}</span> <span className="flex-1 truncate text-sm font-medium">{project.name}</span>
{currentProjectId === project.id && <Check className="h-4 w-4 text-brand-500 shrink-0" />} {currentProjectId === project.id && <Check className="h-4 w-4 text-brand-500 shrink-0" />}
</div> </div>