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,18 @@ import { useState, useId } from 'react'
import { X, Save, Plus, Trash2, Loader2, AlertCircle } from 'lucide-react'
import { useUpdateFeature } from '../hooks/useProjects'
import type { Feature } from '../lib/types'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Alert, AlertDescription } from '@/components/ui/alert'
interface Step {
id: string
@@ -83,149 +95,135 @@ export function EditFeatureForm({ feature, projectName, onClose, onSaved }: Edit
JSON.stringify(currentSteps) !== JSON.stringify(feature.steps)
return (
<div className="neo-modal-backdrop" onClick={onClose}>
<div
className="neo-modal w-full max-w-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b-3 border-[var(--color-neo-border)]">
<h2 className="font-display text-2xl font-bold">
Edit Feature
</h2>
<button
onClick={onClose}
className="neo-btn neo-btn-ghost p-2"
>
<X size={24} />
</button>
</div>
<Dialog open={true} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Feature</DialogTitle>
</DialogHeader>
{/* Form */}
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<form onSubmit={handleSubmit} className="space-y-4">
{/* Error Message */}
{error && (
<div className="flex items-center gap-3 p-4 bg-[var(--color-neo-error-bg)] text-[var(--color-neo-error-text)] border-3 border-[var(--color-neo-error-border)]">
<AlertCircle size={20} />
<span>{error}</span>
<button
type="button"
onClick={() => setError(null)}
className="ml-auto hover:opacity-70 transition-opacity"
>
<X size={16} />
</button>
</div>
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="flex items-center justify-between">
<span>{error}</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
onClick={() => setError(null)}
>
<X size={14} />
</Button>
</AlertDescription>
</Alert>
)}
{/* Category & Priority Row */}
<div className="flex gap-4">
<div className="flex-1">
<label className="block font-display font-bold mb-2 uppercase text-sm">
Category
</label>
<input
<div className="flex-1 space-y-2">
<Label htmlFor="category">Category</Label>
<Input
id="category"
type="text"
value={category}
onChange={(e) => setCategory(e.target.value)}
placeholder="e.g., Authentication, UI, API"
className="neo-input"
required
/>
</div>
<div className="w-32">
<label className="block font-display font-bold mb-2 uppercase text-sm">
Priority
</label>
<input
<div className="w-32 space-y-2">
<Label htmlFor="priority">Priority</Label>
<Input
id="priority"
type="number"
value={priority}
onChange={(e) => setPriority(e.target.value)}
min="1"
className="neo-input"
required
/>
</div>
</div>
{/* Name */}
<div>
<label className="block font-display font-bold mb-2 uppercase text-sm">
Feature Name
</label>
<input
<div className="space-y-2">
<Label htmlFor="name">Feature Name</Label>
<Input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., User login form"
className="neo-input"
required
/>
</div>
{/* Description */}
<div>
<label className="block font-display font-bold mb-2 uppercase text-sm">
Description
</label>
<textarea
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Describe what this feature should do..."
className="neo-input min-h-[100px] resize-y"
className="min-h-[100px] resize-y"
required
/>
</div>
{/* Steps */}
<div>
<label className="block font-display font-bold mb-2 uppercase text-sm">
Test Steps
</label>
<div className="space-y-2">
<Label>Test Steps</Label>
<div className="space-y-2">
{steps.map((step, index) => (
<div key={step.id} className="flex gap-2 items-center">
<span
className="w-10 h-10 flex-shrink-0 flex items-center justify-center font-mono font-bold text-sm border-3 border-[var(--color-neo-border)] bg-[var(--color-neo-bg)] text-[var(--color-neo-text-secondary)]"
style={{ boxShadow: 'var(--shadow-neo-sm)' }}
>
<span className="w-10 h-10 flex-shrink-0 flex items-center justify-center font-mono font-semibold text-sm border rounded-md bg-muted text-muted-foreground">
{index + 1}
</span>
<input
<Input
type="text"
value={step.value}
onChange={(e) => handleStepChange(step.id, e.target.value)}
placeholder="Describe this step..."
className="neo-input flex-1"
className="flex-1"
/>
{steps.length > 1 && (
<button
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemoveStep(step.id)}
className="neo-btn neo-btn-ghost p-2"
>
<Trash2 size={18} />
</button>
</Button>
)}
</div>
))}
</div>
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleAddStep}
className="neo-btn neo-btn-ghost mt-2 text-sm"
>
<Plus size={16} />
Add Step
</button>
</Button>
</div>
{/* Actions */}
<div className="flex gap-3 pt-4 border-t-3 border-[var(--color-neo-border)]">
<button
<DialogFooter className="pt-4 border-t">
<Button
type="button"
variant="outline"
onClick={onClose}
>
Cancel
</Button>
<Button
type="submit"
disabled={!isValid || !hasChanges || updateFeature.isPending}
className="neo-btn neo-btn-success flex-1"
>
{updateFeature.isPending ? (
<Loader2 size={18} className="animate-spin" />
@@ -235,17 +233,10 @@ export function EditFeatureForm({ feature, projectName, onClose, onSaved }: Edit
Save Changes
</>
)}
</button>
<button
type="button"
onClick={onClose}
className="neo-btn neo-btn-ghost"
>
Cancel
</button>
</div>
</Button>
</DialogFooter>
</form>
</div>
</div>
</DialogContent>
</Dialog>
)
}