Files
autocoder/ui/src/components/ProjectSelector.tsx
Auto c917582a64 refactor(ui): migrate to shadcn/ui components and fix scroll issues
Migrate UI component library from custom implementations to shadcn/ui:
- Add shadcn/ui primitives (Button, Card, Dialog, Input, etc.)
- Replace custom styles with Tailwind CSS v4 theme configuration
- Remove custom-theme.css in favor of globals.css with @theme directive

Fix scroll overflow issues in multiple components:
- ProjectSelector: "New Project" button no longer overlays project list
- FolderBrowser: folder list now scrolls properly within modal
- AgentCard: log modal content stays within bounds
- ConversationHistory: conversation list scrolls correctly
- KanbanColumn: feature cards scroll within fixed height
- ScheduleModal: schedule form content scrolls properly

Key technical changes:
- Replace ScrollArea component with native overflow-y-auto divs
- Add min-h-0 to flex containers to allow proper shrinking
- Restructure dropdown layouts with flex-col for fixed footers

New files:
- ui/components.json (shadcn/ui configuration)
- ui/src/components/ui/* (20 UI primitive components)
- ui/src/lib/utils.ts (cn utility for class merging)
- ui/tsconfig.app.json (app-specific TypeScript config)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 18:25:55 +02:00

176 lines
5.7 KiB
TypeScript

import { useState } from 'react'
import { ChevronDown, Plus, FolderOpen, Loader2, Trash2 } from 'lucide-react'
import type { ProjectSummary } from '../lib/types'
import { NewProjectModal } from './NewProjectModal'
import { ConfirmDialog } from './ConfirmDialog'
import { useDeleteProject } from '../hooks/useProjects'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface ProjectSelectorProps {
projects: ProjectSummary[]
selectedProject: string | null
onSelectProject: (name: string | null) => void
isLoading: boolean
onSpecCreatingChange?: (isCreating: boolean) => void
}
export function ProjectSelector({
projects,
selectedProject,
onSelectProject,
isLoading,
onSpecCreatingChange,
}: ProjectSelectorProps) {
const [isOpen, setIsOpen] = useState(false)
const [showNewProjectModal, setShowNewProjectModal] = useState(false)
const [projectToDelete, setProjectToDelete] = useState<string | null>(null)
const deleteProject = useDeleteProject()
const handleProjectCreated = (projectName: string) => {
onSelectProject(projectName)
setIsOpen(false)
}
const handleDeleteClick = (e: React.MouseEvent, projectName: string) => {
e.stopPropagation()
e.preventDefault()
setProjectToDelete(projectName)
}
const handleConfirmDelete = async () => {
if (!projectToDelete) return
try {
await deleteProject.mutateAsync(projectToDelete)
if (selectedProject === projectToDelete) {
onSelectProject(null)
}
setProjectToDelete(null)
} catch (error) {
console.error('Failed to delete project:', error)
setProjectToDelete(null)
}
}
const handleCancelDelete = () => {
setProjectToDelete(null)
}
const selectedProjectData = projects.find(p => p.name === selectedProject)
return (
<div className="relative">
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="min-w-[200px] justify-between"
disabled={isLoading}
>
{isLoading ? (
<Loader2 size={18} className="animate-spin" />
) : selectedProject ? (
<>
<span className="flex items-center gap-2">
<FolderOpen size={18} />
{selectedProject}
</span>
{selectedProjectData && selectedProjectData.stats.total > 0 && (
<Badge className="ml-2">{selectedProjectData.stats.percentage}%</Badge>
)}
</>
) : (
<span className="text-muted-foreground">Select Project</span>
)}
<ChevronDown size={18} className={`transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[280px] p-0 flex flex-col">
{projects.length > 0 ? (
<div className="max-h-[300px] overflow-y-auto p-1">
{projects.map(project => (
<DropdownMenuItem
key={project.name}
className={`flex items-center justify-between cursor-pointer ${
project.name === selectedProject ? 'bg-primary/10' : ''
}`}
onSelect={() => {
onSelectProject(project.name)
}}
>
<span className="flex items-center gap-2 flex-1">
<FolderOpen size={16} />
{project.name}
{project.stats.total > 0 && (
<span className="text-sm font-mono text-muted-foreground ml-auto">
{project.stats.passing}/{project.stats.total}
</span>
)}
</span>
<Button
variant="ghost"
size="icon-xs"
onClick={(e) => handleDeleteClick(e, project.name)}
className="text-muted-foreground hover:text-destructive"
>
<Trash2 size={14} />
</Button>
</DropdownMenuItem>
))}
</div>
) : (
<div className="p-4 text-center text-muted-foreground">
No projects yet
</div>
)}
<DropdownMenuSeparator className="my-0" />
<div className="p-1">
<DropdownMenuItem
onSelect={() => {
setShowNewProjectModal(true)
}}
className="cursor-pointer font-semibold"
>
<Plus size={16} />
New Project
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
{/* New Project Modal */}
<NewProjectModal
isOpen={showNewProjectModal}
onClose={() => setShowNewProjectModal(false)}
onProjectCreated={handleProjectCreated}
onStepChange={(step) => onSpecCreatingChange?.(step === 'chat')}
/>
{/* Delete Confirmation Dialog */}
<ConfirmDialog
isOpen={projectToDelete !== null}
title="Delete Project"
message={`Are you sure you want to remove "${projectToDelete}" from the registry? This will unregister the project but preserve its files on disk.`}
confirmLabel="Delete"
cancelLabel="Cancel"
variant="danger"
isLoading={deleteProject.isPending}
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
/>
</div>
)
}