refactor: Enhance project management features and UI components

- Updated create-pr.ts to improve commit error handling and logging.
- Enhanced project-switcher.tsx with new folder opening functionality and state management for project setup.
- Expanded icon-picker.tsx to include a comprehensive list of icons organized by category.
- Replaced dialog components with popover components for auto mode and plan settings, improving UI responsiveness.
- Refactored board-view components to streamline feature management and enhance user experience.
- Removed outdated dialog components and replaced them with popover alternatives for better accessibility.

These changes aim to improve the overall usability and functionality of the project management interface.
This commit is contained in:
webdevcody
2026-01-13 22:35:45 -05:00
parent ee13bf9a8f
commit 5ec5fe82e6
21 changed files with 989 additions and 469 deletions

View File

@@ -10,43 +10,434 @@ interface IconPickerProps {
onSelectIcon: (icon: string | null) => void;
}
// Popular project-related icons
// Comprehensive list of project-related icons from Lucide
// Organized by category for easier browsing
const POPULAR_ICONS = [
// Folders & Files
'Folder',
'FolderOpen',
'FolderCode',
'FolderGit',
'FolderKanban',
'Package',
'Box',
'Boxes',
'FolderTree',
'FolderInput',
'FolderOutput',
'FolderPlus',
'File',
'FileCode',
'FileText',
'FileJson',
'FileImage',
'FileVideo',
'FileAudio',
'FileSpreadsheet',
'Files',
'Archive',
// Code & Development
'Code',
'Code2',
'Braces',
'FileCode',
'Brackets',
'Terminal',
'Globe',
'Server',
'Database',
'TerminalSquare',
'Command',
'GitBranch',
'GitCommit',
'GitMerge',
'GitPullRequest',
'GitCompare',
'GitFork',
'GitHub',
'Gitlab',
'Bitbucket',
'Vscode',
// Packages & Containers
'Package',
'PackageSearch',
'PackageCheck',
'PackageX',
'Box',
'Boxes',
'Container',
// UI & Design
'Layout',
'LayoutGrid',
'LayoutList',
'LayoutDashboard',
'LayoutTemplate',
'Layers',
'Layers2',
'Layers3',
'Blocks',
'Component',
'Puzzle',
'Palette',
'Paintbrush',
'Brush',
'PenTool',
'Ruler',
'Grid',
'Grid3x3',
'Square',
'RectangleHorizontal',
'RectangleVertical',
'Circle',
// Tools & Settings
'Cog',
'Settings',
'Settings2',
'Wrench',
'Hammer',
'Screwdriver',
'WrenchIcon',
'Tool',
'ScrewdriverWrench',
'Sliders',
'SlidersHorizontal',
'Filter',
'FilterX',
// Technology & Infrastructure
'Server',
'ServerCrash',
'ServerCog',
'Database',
'DatabaseBackup',
'CloudUpload',
'CloudDownload',
'CloudOff',
'Globe',
'Globe2',
'Network',
'Wifi',
'WifiOff',
'Router',
'Cpu',
'MemoryStick',
'HardDrive',
'HardDriveIcon',
'CircuitBoard',
'Microchip',
'Monitor',
'MonitorSpeaker',
'Laptop',
'Smartphone',
'Tablet',
'Mouse',
'Keyboard',
'Headphones',
'Printer',
'Scanner',
// Workflow & Process
'Workflow',
'Zap',
'Rocket',
'Sparkles',
'Star',
'Heart',
'Flame',
'Lightning',
'Bolt',
'Target',
'Flag',
'FlagTriangleRight',
'CheckCircle',
'CheckCircle2',
'XCircle',
'AlertCircle',
'Info',
'HelpCircle',
'Clock',
'Timer',
'Stopwatch',
'Calendar',
'CalendarDays',
'CalendarCheck',
'CalendarClock',
// Security & Access
'Shield',
'ShieldCheck',
'ShieldAlert',
'ShieldOff',
'Lock',
'Unlock',
'Key',
'Cpu',
'CircuitBoard',
'Workflow',
'KeyRound',
'Eye',
'EyeOff',
'User',
'Users',
'UserCheck',
'UserX',
'UserPlus',
'UserCog',
// Business & Finance
'Briefcase',
'Building',
'Building2',
'Store',
'ShoppingCart',
'ShoppingBag',
'CreditCard',
'Wallet',
'DollarSign',
'Euro',
'PoundSterling',
'Yen',
'Coins',
'Receipt',
'ChartBar',
'ChartLine',
'ChartPie',
'TrendingUp',
'TrendingDown',
'Activity',
'BarChart',
'LineChart',
'PieChart',
// Communication & Media
'MessageSquare',
'MessageCircle',
'Mail',
'MailOpen',
'Send',
'Inbox',
'Phone',
'PhoneCall',
'Video',
'VideoOff',
'Camera',
'CameraOff',
'Image',
'ImageIcon',
'Film',
'Music',
'Mic',
'MicOff',
'Volume',
'Volume2',
'VolumeX',
'Radio',
'Podcast',
// Social & Community
'Heart',
'HeartHandshake',
'Star',
'StarOff',
'ThumbsUp',
'ThumbsDown',
'Share',
'Share2',
'Link',
'Link2',
'ExternalLink',
'AtSign',
'Hash',
'Hashtag',
'Tag',
'Tags',
// Navigation & Location
'Compass',
'Map',
'MapPin',
'Navigation',
'Navigation2',
'Route',
'Plane',
'Car',
'Bike',
'Ship',
'Train',
'Bus',
// Science & Education
'FlaskConical',
'FlaskRound',
'Beaker',
'TestTube',
'TestTube2',
'Microscope',
'Atom',
'Brain',
'GraduationCap',
'Book',
'BookOpen',
'BookMarked',
'Library',
'School',
'University',
// Food & Health
'Coffee',
'Utensils',
'UtensilsCrossed',
'Apple',
'Cherry',
'Cookie',
'Cake',
'Pizza',
'Beer',
'Wine',
'HeartPulse',
'Dumbbell',
'Running',
// Nature & Weather
'Tree',
'TreePine',
'Leaf',
'Flower',
'Flower2',
'Sun',
'Moon',
'CloudRain',
'CloudSnow',
'CloudLightning',
'Droplet',
'Wind',
'Snowflake',
'Umbrella',
// Objects & Symbols
'Puzzle',
'PuzzleIcon',
'Gamepad',
'Gamepad2',
'Dice',
'Dice1',
'Dice6',
'Gem',
'Crown',
'Trophy',
'Medal',
'Award',
'Gift',
'GiftIcon',
'Bell',
'BellOff',
'BellRing',
'Home',
'House',
'DoorOpen',
'DoorClosed',
'Window',
'Lightbulb',
'LightbulbOff',
'Candle',
'Flashlight',
'FlashlightOff',
'Battery',
'BatteryFull',
'BatteryLow',
'BatteryCharging',
'Plug',
'PlugZap',
'Power',
'PowerOff',
// Arrows & Directions
'ArrowRight',
'ArrowLeft',
'ArrowUp',
'ArrowDown',
'ArrowUpRight',
'ArrowDownRight',
'ArrowDownLeft',
'ArrowUpLeft',
'ChevronRight',
'ChevronLeft',
'ChevronUp',
'ChevronDown',
'Move',
'MoveUp',
'MoveDown',
'MoveLeft',
'MoveRight',
'RotateCw',
'RotateCcw',
'RefreshCw',
'RefreshCcw',
// Shapes & Symbols
'Diamond',
'Pentagon',
'Cross',
'Plus',
'Minus',
'X',
'Check',
'Divide',
'Equal',
'Infinity',
'Percent',
// Miscellaneous
'Bot',
'Wand',
'Wand2',
'Magic',
'Stars',
'Comet',
'Satellite',
'SatelliteDish',
'Radar',
'RadarIcon',
'Scan',
'ScanLine',
'QrCode',
'Barcode',
'ScanSearch',
'Search',
'SearchX',
'ZoomIn',
'ZoomOut',
'Maximize',
'Minimize',
'Maximize2',
'Minimize2',
'Expand',
'Shrink',
'Copy',
'CopyCheck',
'Clipboard',
'ClipboardCheck',
'ClipboardCopy',
'ClipboardList',
'ClipboardPaste',
'Scissors',
'Cut',
'FileEdit',
'Pen',
'Pencil',
'Eraser',
'Trash',
'Trash2',
'Delete',
'ArchiveRestore',
'Download',
'Upload',
'Save',
'SaveAll',
'FilePlus',
'FileMinus',
'FileX',
'FileCheck',
'FileQuestion',
'FileWarning',
'FileSearch',
'FolderSearch',
'FolderX',
'FolderCheck',
'FolderMinus',
'FolderSync',
'FolderUp',
'FolderDown',
];
export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) {
@@ -94,7 +485,7 @@ export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) {
)}
{/* Icons Grid */}
<ScrollArea className="h-64 rounded-md border">
<ScrollArea className="h-96 rounded-md border">
<div className="grid grid-cols-6 gap-1 p-2">
{filteredIcons.map((iconName) => {
const IconComponent = getIconComponent(iconName);

View File

@@ -1,7 +1,8 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Edit2, Trash2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import type { Project } from '@/lib/electron';
interface ProjectContextMenuProps {
@@ -19,6 +20,7 @@ export function ProjectContextMenu({
}: ProjectContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
const { moveProjectToTrash } = useAppStore();
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -47,57 +49,73 @@ export function ProjectContextMenu({
};
const handleRemove = () => {
if (confirm(`Remove "${project.name}" from the project list?`)) {
moveProjectToTrash(project.id);
}
setShowRemoveDialog(true);
};
const handleConfirmRemove = () => {
moveProjectToTrash(project.id);
onClose();
};
return (
<div
ref={menuRef}
className={cn(
'fixed z-[100] min-w-48 rounded-lg',
'bg-popover text-popover-foreground',
'border border-border shadow-lg',
'animate-in fade-in zoom-in-95 duration-100'
)}
style={{
top: position.y,
left: position.x,
}}
data-testid="project-context-menu"
>
<div className="p-1">
<button
onClick={handleEdit}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left',
'hover:bg-accent transition-colors',
'focus:outline-none focus:bg-accent'
)}
data-testid="edit-project-button"
>
<Edit2 className="w-4 h-4" />
<span>Edit Name & Icon</span>
</button>
<>
<div
ref={menuRef}
className={cn(
'fixed z-[100] min-w-48 rounded-lg',
'bg-popover text-popover-foreground',
'border border-border shadow-lg',
'animate-in fade-in zoom-in-95 duration-100'
)}
style={{
top: position.y,
left: position.x,
}}
data-testid="project-context-menu"
>
<div className="p-1">
<button
onClick={handleEdit}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left',
'hover:bg-accent transition-colors',
'focus:outline-none focus:bg-accent'
)}
data-testid="edit-project-button"
>
<Edit2 className="w-4 h-4" />
<span>Edit Name & Icon</span>
</button>
<button
onClick={handleRemove}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left',
'text-destructive hover:bg-destructive/10',
'transition-colors',
'focus:outline-none focus:bg-destructive/10'
)}
data-testid="remove-project-button"
>
<Trash2 className="w-4 h-4" />
<span>Remove Project</span>
</button>
<button
onClick={handleRemove}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left',
'text-destructive hover:bg-destructive/10',
'transition-colors',
'focus:outline-none focus:bg-destructive/10'
)}
data-testid="remove-project-button"
>
<Trash2 className="w-4 h-4" />
<span>Remove Project</span>
</button>
</div>
</div>
</div>
<ConfirmDialog
open={showRemoveDialog}
onOpenChange={setShowRemoveDialog}
onConfirm={handleConfirmRemove}
title="Remove Project"
description={`Are you sure you want to remove "${project.name}" from the project list? This won't delete any files on disk.`}
icon={Trash2}
iconClassName="text-destructive"
confirmText="Remove"
confirmVariant="destructive"
/>
</>
);
}

View File

@@ -1,8 +1,8 @@
import { useState, useCallback, useEffect } from 'react';
import { Plus, Bug } from 'lucide-react';
import { Plus, Bug, FolderOpen } from 'lucide-react';
import { useNavigate } from '@tanstack/react-router';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { useAppStore, type ThemeMode } from '@/store/app-store';
import { useOSDetection } from '@/hooks/use-os-detection';
import { ProjectSwitcherItem } from './components/project-switcher-item';
import { ProjectContextMenu } from './components/project-context-menu';
@@ -12,6 +12,9 @@ import { OnboardingDialog } from '@/components/layout/sidebar/dialogs';
import { useProjectCreation, useProjectTheme } from '@/components/layout/sidebar/hooks';
import type { Project } from '@/lib/electron';
import { getElectronAPI } from '@/lib/electron';
import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init';
import { toast } from 'sonner';
import { CreateSpecDialog } from '@/components/views/spec-view/dialogs';
function getOSAbbreviation(os: string): string {
switch (os) {
@@ -34,6 +37,8 @@ export function ProjectSwitcher() {
setCurrentProject,
trashedProjects,
upsertAndSetCurrentProject,
specCreatingForProject,
setSpecCreatingForProject,
} = useAppStore();
const [contextMenuProject, setContextMenuProject] = useState<Project | null>(null);
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(
@@ -41,6 +46,17 @@ export function ProjectSwitcher() {
);
const [editDialogProject, setEditDialogProject] = useState<Project | null>(null);
// Setup dialog state for opening existing projects
const [showSetupDialog, setShowSetupDialog] = useState(false);
const [setupProjectPath, setSetupProjectPath] = useState<string | null>(null);
const [projectOverview, setProjectOverview] = useState('');
const [generateFeatures, setGenerateFeatures] = useState(true);
const [analyzeProject, setAnalyzeProject] = useState(true);
const [featureCount, setFeatureCount] = useState(5);
// Derive isCreatingSpec from store state
const isCreatingSpec = specCreatingForProject !== null;
// Version info
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0';
const { os } = useOSDetection();
@@ -108,6 +124,109 @@ export function ProjectSwitcher() {
api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues');
}, []);
/**
* Opens the system folder selection dialog and initializes the selected project.
*/
const handleOpenFolder = useCallback(async () => {
const api = getElectronAPI();
const result = await api.openDirectory();
if (!result.canceled && result.filePaths[0]) {
const path = result.filePaths[0];
// Extract folder name from path (works on both Windows and Mac/Linux)
const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project';
try {
// Check if this is a brand new project (no .automaker directory)
const hadAutomakerDir = await hasAutomakerDir(path);
// Initialize the .automaker directory structure
const initResult = await initializeProject(path);
if (!initResult.success) {
toast.error('Failed to initialize project', {
description: initResult.error || 'Unknown error occurred',
});
return;
}
// Upsert project and set as current (handles both create and update cases)
// Theme preservation is handled by the store action
const trashedProject = trashedProjects.find((p) => p.path === path);
const effectiveTheme =
(trashedProject?.theme as ThemeMode | undefined) ||
(currentProject?.theme as ThemeMode | undefined) ||
globalTheme;
upsertAndSetCurrentProject(path, name, effectiveTheme);
// Check if app_spec.txt exists
const specExists = await hasAppSpec(path);
if (!hadAutomakerDir && !specExists) {
// This is a brand new project - show setup dialog
setSetupProjectPath(path);
setShowSetupDialog(true);
toast.success('Project opened', {
description: `Opened ${name}. Let's set up your app specification!`,
});
} else if (initResult.createdFiles && initResult.createdFiles.length > 0) {
toast.success(initResult.isNewProject ? 'Project initialized' : 'Project updated', {
description: `Set up ${initResult.createdFiles.length} file(s) in .automaker`,
});
} else {
toast.success('Project opened', {
description: `Opened ${name}`,
});
}
// Navigate to board view
navigate({ to: '/board' });
} catch (error) {
console.error('Failed to open project:', error);
toast.error('Failed to open project', {
description: error instanceof Error ? error.message : 'Unknown error',
});
}
}
}, [trashedProjects, upsertAndSetCurrentProject, currentProject, globalTheme, navigate]);
// Handler for creating initial spec from the setup dialog
const handleCreateInitialSpec = useCallback(async () => {
if (!setupProjectPath) return;
setSpecCreatingForProject(setupProjectPath);
setShowSetupDialog(false);
try {
const api = getElectronAPI();
await api.generateAppSpec({
projectPath: setupProjectPath,
projectOverview,
generateFeatures,
analyzeProject,
featureCount,
});
} catch (error) {
console.error('Failed to generate spec:', error);
toast.error('Failed to generate spec', {
description: error instanceof Error ? error.message : 'Unknown error',
});
setSpecCreatingForProject(null);
}
}, [
setupProjectPath,
projectOverview,
generateFeatures,
analyzeProject,
featureCount,
setSpecCreatingForProject,
]);
const handleSkipSetup = useCallback(() => {
setShowSetupDialog(false);
setSetupProjectPath(null);
}, []);
// Keyboard shortcuts for project switching (1-9, 0)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -204,7 +323,7 @@ export function ProjectSwitcher() {
</div>
{/* Projects List */}
<div className="flex-1 overflow-y-auto py-3 px-2 space-y-2">
<div className="flex-1 overflow-y-auto pt-1 pb-3 px-2 space-y-2">
{projects.map((project, index) => (
<ProjectSwitcherItem
key={project.id}
@@ -219,7 +338,7 @@ export function ProjectSwitcher() {
{/* Horizontal rule and Add Project Button - only show if there are projects */}
{projects.length > 0 && (
<>
<div className="w-full h-px bg-border/40 my-2" />
<div className="w-full h-px bg-border my-2" />
<button
onClick={handleNewProject}
className={cn(
@@ -234,25 +353,55 @@ export function ProjectSwitcher() {
>
<Plus className="w-5 h-5" />
</button>
<button
onClick={handleOpenFolder}
className={cn(
'w-full aspect-square rounded-xl flex items-center justify-center',
'transition-all duration-200 ease-out',
'text-muted-foreground hover:text-foreground',
'hover:bg-accent/50 border border-transparent hover:border-border/40',
'hover:shadow-sm hover:scale-105 active:scale-95'
)}
title="Open Project"
data-testid="open-project-button"
>
<FolderOpen className="w-5 h-5" />
</button>
</>
)}
{/* Add Project Button - when no projects, show without rule */}
{projects.length === 0 && (
<button
onClick={handleNewProject}
className={cn(
'w-full aspect-square rounded-xl flex items-center justify-center',
'transition-all duration-200 ease-out',
'text-muted-foreground hover:text-foreground',
'hover:bg-accent/50 border border-transparent hover:border-border/40',
'hover:shadow-sm hover:scale-105 active:scale-95'
)}
title="New Project"
data-testid="new-project-button"
>
<Plus className="w-5 h-5" />
</button>
<>
<button
onClick={handleNewProject}
className={cn(
'w-full aspect-square rounded-xl flex items-center justify-center',
'transition-all duration-200 ease-out',
'text-muted-foreground hover:text-foreground',
'hover:bg-accent/50 border border-transparent hover:border-border/40',
'hover:shadow-sm hover:scale-105 active:scale-95'
)}
title="New Project"
data-testid="new-project-button"
>
<Plus className="w-5 h-5" />
</button>
<button
onClick={handleOpenFolder}
className={cn(
'w-full aspect-square rounded-xl flex items-center justify-center',
'transition-all duration-200 ease-out',
'text-muted-foreground hover:text-foreground',
'hover:bg-accent/50 border border-transparent hover:border-border/40',
'hover:shadow-sm hover:scale-105 active:scale-95'
)}
title="Open Project"
data-testid="open-project-button"
>
<FolderOpen className="w-5 h-5" />
</button>
</>
)}
</div>
@@ -312,6 +461,26 @@ export function ProjectSwitcher() {
onSkip={handleOnboardingSkip}
onGenerateSpec={handleOnboardingSkip}
/>
{/* Setup Dialog for Open Project */}
<CreateSpecDialog
open={showSetupDialog}
onOpenChange={setShowSetupDialog}
projectOverview={projectOverview}
onProjectOverviewChange={setProjectOverview}
generateFeatures={generateFeatures}
onGenerateFeaturesChange={setGenerateFeatures}
analyzeProject={analyzeProject}
onAnalyzeProjectChange={setAnalyzeProject}
featureCount={featureCount}
onFeatureCountChange={setFeatureCount}
onCreateSpec={handleCreateInitialSpec}
onSkip={handleSkipSetup}
isCreatingSpec={isCreatingSpec}
showSkipButton={true}
title="Set Up Your Project"
description="We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt to help describe your project for our system. We'll analyze your project's tech stack and create a comprehensive specification."
/>
</>
);
}

View File

@@ -2,7 +2,7 @@ import { Folder, LucideIcon } from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import { cn, isMac } from '@/lib/utils';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
import type { Project } from '@/lib/electron';
import { isElectron, type Project } from '@/lib/electron';
interface SidebarHeaderProps {
sidebarOpen: boolean;
@@ -25,14 +25,17 @@ export function SidebarHeader({ sidebarOpen, currentProject }: SidebarHeaderProp
<div
className={cn(
'shrink-0 flex flex-col',
// Add minimal padding on macOS for traffic light buttons
isMac && 'pt-2'
// Add padding on macOS Electron for traffic light buttons
isMac && isElectron() && 'pt-[10px]'
)}
>
{/* Project name and icon display */}
{currentProject && (
<div
className={cn('flex items-center gap-3 px-4 py-3', !sidebarOpen && 'justify-center px-2')}
className={cn(
'flex items-center gap-3 px-4 pt-3 pb-1',
!sidebarOpen && 'justify-center px-2'
)}
>
{/* Project Icon */}
<div className="shrink-0">

View File

@@ -21,7 +21,7 @@ export function SidebarNavigation({
navigate,
}: SidebarNavigationProps) {
return (
<nav className={cn('flex-1 overflow-y-auto px-3 pb-2', sidebarOpen ? 'mt-5' : 'mt-1')}>
<nav className={cn('flex-1 overflow-y-auto px-3 pb-2', sidebarOpen ? 'mt-1' : 'mt-1')}>
{!currentProject && sidebarOpen ? (
// Placeholder when no project is selected (only in expanded state)
<div className="flex items-center justify-center h-full px-4">