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

@@ -1,62 +1,25 @@
import { useEffect, useRef } from 'react'
import { X, Loader2, AlertCircle } from 'lucide-react'
import { Loader2, AlertCircle } from 'lucide-react'
import { useSettings, useUpdateSettings, useAvailableModels } from '../hooks/useProjects'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
interface SettingsModalProps {
isOpen: boolean
onClose: () => void
}
export function SettingsModal({ onClose }: SettingsModalProps) {
export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
const { data: settings, isLoading, isError, refetch } = useSettings()
const { data: modelsData } = useAvailableModels()
const updateSettings = useUpdateSettings()
const modalRef = useRef<HTMLDivElement>(null)
const closeButtonRef = useRef<HTMLButtonElement>(null)
// Focus trap - keep focus within modal
useEffect(() => {
const modal = modalRef.current
if (!modal) return
// Focus the close button when modal opens
closeButtonRef.current?.focus()
const focusableElements = modal.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault()
lastElement?.focus()
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault()
firstElement?.focus()
}
}
}
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
}
}
document.addEventListener('keydown', handleTabKey)
document.addEventListener('keydown', handleEscape)
return () => {
document.removeEventListener('keydown', handleTabKey)
document.removeEventListener('keydown', handleEscape)
}
}, [onClose])
const handleYoloToggle = () => {
if (settings && !updateSettings.isPending) {
@@ -80,36 +43,14 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
const isSaving = updateSettings.isPending
return (
<div
className="neo-modal-backdrop"
onClick={onClose}
role="presentation"
>
<div
ref={modalRef}
className="neo-modal w-full max-w-sm p-6"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-labelledby="settings-title"
aria-modal="true"
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 id="settings-title" className="font-display text-xl font-bold">
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Settings
{isSaving && (
<Loader2 className="inline-block ml-2 animate-spin" size={16} />
)}
</h2>
<button
ref={closeButtonRef}
onClick={onClose}
className="neo-btn neo-btn-ghost p-2"
aria-label="Close settings"
>
<X size={20} />
</button>
</div>
{isSaving && <Loader2 className="animate-spin" size={16} />}
</DialogTitle>
</DialogHeader>
{/* Loading State */}
{isLoading && (
@@ -121,82 +62,55 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
{/* Error State */}
{isError && (
<div className="p-4 bg-[var(--color-neo-error-bg)] text-[var(--color-neo-error-text)] border-3 border-[var(--color-neo-error-border)] mb-4">
<div className="flex items-center gap-2">
<AlertCircle size={18} />
<span>Failed to load settings</span>
</div>
<button
onClick={() => refetch()}
className="mt-2 underline text-sm hover:opacity-70 transition-opacity"
>
Retry
</button>
</div>
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Failed to load settings
<Button
variant="link"
onClick={() => refetch()}
className="ml-2 p-0 h-auto"
>
Retry
</Button>
</AlertDescription>
</Alert>
)}
{/* Settings Content */}
{settings && !isLoading && (
<div className="space-y-6">
{/* YOLO Mode Toggle */}
<div>
<div className="flex items-center justify-between">
<div>
<label
id="yolo-label"
className="font-display font-bold text-base"
>
YOLO Mode
</label>
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
Skip testing for rapid prototyping
</p>
</div>
<button
onClick={handleYoloToggle}
disabled={isSaving}
className={`relative w-14 h-8 rounded-none border-3 border-[var(--color-neo-border)] transition-colors ${
settings.yolo_mode
? 'bg-[var(--color-neo-pending)]'
: 'bg-[var(--color-neo-card)]'
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
role="switch"
aria-checked={settings.yolo_mode}
aria-labelledby="yolo-label"
>
<span
className={`absolute top-1 w-5 h-5 bg-[var(--color-neo-border)] transition-transform ${
settings.yolo_mode ? 'left-7' : 'left-1'
}`}
/>
</button>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="yolo-mode" className="font-medium">
YOLO Mode
</Label>
<p className="text-sm text-muted-foreground">
Skip testing for rapid prototyping
</p>
</div>
<Switch
id="yolo-mode"
checked={settings.yolo_mode}
onCheckedChange={handleYoloToggle}
disabled={isSaving}
/>
</div>
{/* Model Selection - Radio Group */}
<div>
<label
id="model-label"
className="font-display font-bold text-base block mb-2"
>
Model
</label>
<div
className="flex border-3 border-[var(--color-neo-border)]"
role="radiogroup"
aria-labelledby="model-label"
>
{/* Model Selection */}
<div className="space-y-2">
<Label className="font-medium">Model</Label>
<div className="flex rounded-lg border overflow-hidden">
{models.map((model) => (
<button
key={model.id}
onClick={() => handleModelChange(model.id)}
disabled={isSaving}
role="radio"
aria-checked={settings.model === model.id}
className={`flex-1 py-3 px-4 font-display font-bold text-sm transition-colors ${
className={`flex-1 py-2 px-3 text-sm font-medium transition-colors ${
settings.model === model.id
? 'bg-[var(--color-neo-accent)] text-[var(--color-neo-text-on-bright)]'
: 'bg-[var(--color-neo-card)] text-[var(--color-neo-text)] hover:bg-[var(--color-neo-hover-subtle)]'
? 'bg-primary text-primary-foreground'
: 'bg-background text-foreground hover:bg-muted'
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{model.name}
@@ -206,32 +120,21 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
</div>
{/* Regression Agents */}
<div>
<label
id="testing-ratio-label"
className="font-display font-bold text-base block mb-1"
>
Regression Agents
</label>
<p className="text-sm text-[var(--color-neo-text-secondary)] mb-2">
<div className="space-y-2">
<Label className="font-medium">Regression Agents</Label>
<p className="text-sm text-muted-foreground">
Number of regression testing agents (0 = disabled)
</p>
<div
className="flex border-3 border-[var(--color-neo-border)]"
role="radiogroup"
aria-labelledby="testing-ratio-label"
>
<div className="flex rounded-lg border overflow-hidden">
{[0, 1, 2, 3].map((ratio) => (
<button
key={ratio}
onClick={() => handleTestingRatioChange(ratio)}
disabled={isSaving}
role="radio"
aria-checked={settings.testing_agent_ratio === ratio}
className={`flex-1 py-2 px-3 font-display font-bold text-sm transition-colors ${
className={`flex-1 py-2 px-3 text-sm font-medium transition-colors ${
settings.testing_agent_ratio === ratio
? 'bg-[var(--color-neo-progress)] text-[var(--color-neo-text)]'
: 'bg-[var(--color-neo-card)] text-[var(--color-neo-text)] hover:bg-[var(--color-neo-hover-subtle)]'
? 'bg-primary text-primary-foreground'
: 'bg-background text-foreground hover:bg-muted'
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{ratio}
@@ -242,13 +145,15 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
{/* Update Error */}
{updateSettings.isError && (
<div className="p-3 bg-[var(--color-neo-error-bg)] border-3 border-[var(--color-neo-error-border)] text-[var(--color-neo-error-text)] text-sm">
Failed to save settings. Please try again.
</div>
<Alert variant="destructive">
<AlertDescription>
Failed to save settings. Please try again.
</AlertDescription>
</Alert>
)}
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}