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>
This commit is contained in:
Auto
2026-01-26 18:25:55 +02:00
parent e45b5b064e
commit c917582a64
69 changed files with 4900 additions and 4287 deletions

View File

@@ -2,6 +2,9 @@ import { useState } from 'react'
import { ChevronDown, ChevronUp, Code, FlaskConical, Clock, Lock, Sparkles } from 'lucide-react'
import { OrchestratorAvatar } from './OrchestratorAvatar'
import type { OrchestratorStatus, OrchestratorState } from '../lib/types'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
interface OrchestratorStatusCardProps {
status: OrchestratorStatus
@@ -31,16 +34,16 @@ function getStateText(state: OrchestratorState): string {
function getStateColor(state: OrchestratorState): string {
switch (state) {
case 'complete':
return 'text-neo-done'
return 'text-primary'
case 'spawning':
return 'text-[#7C3AED]' // Violet
return 'text-violet-600 dark:text-violet-400'
case 'scheduling':
case 'monitoring':
return 'text-neo-progress'
return 'text-primary'
case 'initializing':
return 'text-neo-pending'
return 'text-yellow-600 dark:text-yellow-400'
default:
return 'text-neo-text-secondary'
return 'text-muted-foreground'
}
}
@@ -62,91 +65,95 @@ export function OrchestratorStatusCard({ status }: OrchestratorStatusCardProps)
const [showEvents, setShowEvents] = useState(false)
return (
<div className="neo-card p-4 bg-gradient-to-r from-[#EDE9FE] to-[#F3E8FF] border-[#7C3AED]/30 mb-4">
<div className="flex items-start gap-4">
{/* Avatar */}
<OrchestratorAvatar state={status.state} size="md" />
<Card className="mb-4 bg-gradient-to-r from-violet-50 to-purple-50 dark:from-violet-950/30 dark:to-purple-950/30 border-violet-200 dark:border-violet-800/50 py-4">
<CardContent className="p-4">
<div className="flex items-start gap-4">
{/* Avatar */}
<OrchestratorAvatar state={status.state} size="md" />
{/* Main content */}
<div className="flex-1 min-w-0">
{/* Header row */}
<div className="flex items-center gap-2 mb-1">
<span className="font-display font-bold text-lg text-[#7C3AED]">
Maestro
</span>
<span className={`text-sm font-medium ${getStateColor(status.state)}`}>
{getStateText(status.state)}
</span>
{/* Main content */}
<div className="flex-1 min-w-0">
{/* Header row */}
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-lg text-violet-700 dark:text-violet-300">
Maestro
</span>
<span className={`text-sm font-medium ${getStateColor(status.state)}`}>
{getStateText(status.state)}
</span>
</div>
{/* Current message */}
<p className="text-sm text-foreground mb-3 line-clamp-2">
{status.message}
</p>
{/* Status badges row */}
<div className="flex flex-wrap items-center gap-2">
{/* Coding agents badge */}
<Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700">
<Code size={12} />
Coding: {status.codingAgents}
</Badge>
{/* Testing agents badge */}
<Badge variant="outline" className="bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700">
<FlaskConical size={12} />
Testing: {status.testingAgents}
</Badge>
{/* Ready queue badge */}
<Badge variant="outline" className="bg-green-100 text-green-700 border-green-300 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700">
<Clock size={12} />
Ready: {status.readyCount}
</Badge>
{/* Blocked badge (only show if > 0) */}
{status.blockedCount > 0 && (
<Badge variant="outline" className="bg-amber-100 text-amber-700 border-amber-300 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-700">
<Lock size={12} />
Blocked: {status.blockedCount}
</Badge>
)}
</div>
</div>
{/* Current message */}
<p className="text-sm text-neo-text mb-3 line-clamp-2">
{status.message}
</p>
{/* Status badges row */}
<div className="flex flex-wrap items-center gap-2">
{/* Coding agents badge */}
<div className="inline-flex items-center gap-1.5 px-2 py-1 bg-blue-100 text-blue-700 rounded border border-blue-300 text-xs font-bold">
<Code size={12} />
<span>Coding: {status.codingAgents}</span>
</div>
{/* Testing agents badge */}
<div className="inline-flex items-center gap-1.5 px-2 py-1 bg-purple-100 text-purple-700 rounded border border-purple-300 text-xs font-bold">
<FlaskConical size={12} />
<span>Testing: {status.testingAgents}</span>
</div>
{/* Ready queue badge */}
<div className="inline-flex items-center gap-1.5 px-2 py-1 bg-green-100 text-green-700 rounded border border-green-300 text-xs font-bold">
<Clock size={12} />
<span>Ready: {status.readyCount}</span>
</div>
{/* Blocked badge (only show if > 0) */}
{status.blockedCount > 0 && (
<div className="inline-flex items-center gap-1.5 px-2 py-1 bg-amber-100 text-amber-700 rounded border border-amber-300 text-xs font-bold">
<Lock size={12} />
<span>Blocked: {status.blockedCount}</span>
</div>
)}
</div>
{/* Recent events toggle */}
{status.recentEvents.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => setShowEvents(!showEvents)}
className="text-violet-600 dark:text-violet-400 hover:bg-violet-100 dark:hover:bg-violet-900/30"
>
<Sparkles size={12} />
Activity
{showEvents ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</Button>
)}
</div>
{/* Recent events toggle */}
{status.recentEvents.length > 0 && (
<button
onClick={() => setShowEvents(!showEvents)}
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-[#7C3AED] hover:bg-[#7C3AED]/10 rounded transition-colors"
>
<Sparkles size={12} />
<span>Activity</span>
{showEvents ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
{/* Collapsible recent events */}
{showEvents && status.recentEvents.length > 0 && (
<div className="mt-3 pt-3 border-t border-violet-200 dark:border-violet-800/50">
<div className="space-y-1.5">
{status.recentEvents.map((event, idx) => (
<div
key={`${event.timestamp}-${idx}`}
className="flex items-start gap-2 text-xs"
>
<span className="text-violet-500 dark:text-violet-400 shrink-0 font-mono">
{formatRelativeTime(event.timestamp)}
</span>
<span className="text-foreground">
{event.message}
</span>
</div>
))}
</div>
</div>
)}
</div>
{/* Collapsible recent events */}
{showEvents && status.recentEvents.length > 0 && (
<div className="mt-3 pt-3 border-t border-[#7C3AED]/20">
<div className="space-y-1.5">
{status.recentEvents.map((event, idx) => (
<div
key={`${event.timestamp}-${idx}`}
className="flex items-start gap-2 text-xs"
>
<span className="text-[#A78BFA] shrink-0 font-mono">
{formatRelativeTime(event.timestamp)}
</span>
<span className="text-neo-text">
{event.message}
</span>
</div>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
)
}