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,12 +2,13 @@
* Chat Message Component
*
* Displays a single message in the spec creation chat.
* Supports user, assistant, and system messages with neobrutalism styling.
* Supports user, assistant, and system messages with clean styling.
*/
import { memo } from 'react'
import { Bot, User, Info } from 'lucide-react'
import type { ChatMessage as ChatMessageType } from '../lib/types'
import { Card } from '@/components/ui/card'
interface ChatMessageProps {
message: ChatMessageType
@@ -25,37 +26,34 @@ export const ChatMessage = memo(function ChatMessage({ message }: ChatMessagePro
minute: '2-digit',
})
// Role-specific styling using CSS variables for theme consistency
// Role-specific styling
const roleConfig = {
user: {
icon: User,
bgColor: 'bg-[var(--color-neo-pending)]',
textColor: 'text-[var(--color-neo-text-on-bright)]',
borderColor: 'border-[var(--color-neo-border)]',
bgColor: 'bg-primary',
textColor: 'text-primary-foreground',
align: 'justify-end',
bubbleAlign: 'items-end',
iconBg: 'bg-[var(--color-neo-pending)]',
shadow: 'var(--shadow-neo-md)',
iconBg: 'bg-primary',
iconColor: 'text-primary-foreground',
},
assistant: {
icon: Bot,
bgColor: 'bg-[var(--color-neo-card)]',
textColor: 'text-[var(--color-neo-text)]',
borderColor: 'border-[var(--color-neo-border)]',
bgColor: 'bg-muted',
textColor: 'text-foreground',
align: 'justify-start',
bubbleAlign: 'items-start',
iconBg: 'bg-[var(--color-neo-progress)]',
shadow: 'var(--shadow-neo-md)',
iconBg: 'bg-secondary',
iconColor: 'text-secondary-foreground',
},
system: {
icon: Info,
bgColor: 'bg-[var(--color-neo-done)]',
textColor: 'text-[var(--color-neo-text-on-bright)]',
borderColor: 'border-[var(--color-neo-border)]',
bgColor: 'bg-green-100 dark:bg-green-900/30',
textColor: 'text-green-900 dark:text-green-100',
align: 'justify-center',
bubbleAlign: 'items-center',
iconBg: 'bg-[var(--color-neo-done)]',
shadow: 'var(--shadow-neo-sm)',
iconBg: 'bg-green-500',
iconColor: 'text-white',
},
}
@@ -66,15 +64,7 @@ export const ChatMessage = memo(function ChatMessage({ message }: ChatMessagePro
if (role === 'system') {
return (
<div className={`flex ${config.align} px-4 py-2`}>
<div
className={`
${config.bgColor}
border-2 ${config.borderColor}
px-4 py-2
text-sm font-mono text-[var(--color-neo-text-on-bright)]
`}
style={{ boxShadow: 'var(--shadow-neo-sm)' }}
>
<div className={`${config.bgColor} border border-border rounded-lg px-4 py-2 text-sm font-mono ${config.textColor}`}>
<span className="flex items-center gap-2">
<Icon size={14} />
{content}
@@ -90,28 +80,12 @@ export const ChatMessage = memo(function ChatMessage({ message }: ChatMessagePro
{/* Message bubble */}
<div className="flex items-start gap-2">
{role === 'assistant' && (
<div
className={`
${config.iconBg}
border-2 border-[var(--color-neo-border)]
p-1.5
flex-shrink-0
`}
style={{ boxShadow: 'var(--shadow-neo-sm)' }}
>
<Icon size={16} className="text-[var(--color-neo-text-on-bright)]" />
<div className={`${config.iconBg} p-1.5 rounded flex-shrink-0`}>
<Icon size={16} className={config.iconColor} />
</div>
)}
<div
className={`
${config.bgColor}
border-3 ${config.borderColor}
px-4 py-3
${isStreaming ? 'animate-pulse-neo' : ''}
`}
style={{ boxShadow: config.shadow }}
>
<Card className={`${config.bgColor} px-4 py-3 border ${isStreaming ? 'animate-pulse' : ''}`}>
{/* Parse content for basic markdown-like formatting */}
{content && (
<div className={`whitespace-pre-wrap text-sm leading-relaxed ${config.textColor}`}>
@@ -152,19 +126,15 @@ export const ChatMessage = memo(function ChatMessage({ message }: ChatMessagePro
{attachments && attachments.length > 0 && (
<div className={`flex flex-wrap gap-2 ${content ? 'mt-3' : ''}`}>
{attachments.map((attachment) => (
<div
key={attachment.id}
className="border-2 border-[var(--color-neo-border)] p-1 bg-[var(--color-neo-card)]"
style={{ boxShadow: 'var(--shadow-neo-sm)' }}
>
<div key={attachment.id} className="border border-border rounded p-1 bg-card">
<img
src={attachment.previewUrl}
alt={attachment.filename}
className="max-w-48 max-h-48 object-contain cursor-pointer hover:opacity-90 transition-opacity"
className="max-w-48 max-h-48 object-contain cursor-pointer hover:opacity-90 transition-opacity rounded"
onClick={() => window.open(attachment.previewUrl, '_blank')}
title={`${attachment.filename} (click to enlarge)`}
/>
<span className="text-xs text-[var(--color-neo-text-secondary)] block mt-1 text-center">
<span className="text-xs text-muted-foreground block mt-1 text-center">
{attachment.filename}
</span>
</div>
@@ -174,27 +144,19 @@ export const ChatMessage = memo(function ChatMessage({ message }: ChatMessagePro
{/* Streaming indicator */}
{isStreaming && (
<span className="inline-block w-2 h-4 bg-[var(--color-neo-accent)] ml-1 animate-pulse" />
<span className="inline-block w-2 h-4 bg-primary ml-1 animate-pulse rounded" />
)}
</div>
</Card>
{role === 'user' && (
<div
className={`
${config.iconBg}
border-2 border-[var(--color-neo-border)]
p-1.5
flex-shrink-0
`}
style={{ boxShadow: 'var(--shadow-neo-sm)' }}
>
<Icon size={16} className="text-[var(--color-neo-text-on-bright)]" />
<div className={`${config.iconBg} p-1.5 rounded flex-shrink-0`}>
<Icon size={16} className={config.iconColor} />
</div>
)}
</div>
{/* Timestamp */}
<span className="text-xs text-[var(--color-neo-text-secondary)] font-mono px-2">
<span className="text-xs text-muted-foreground font-mono px-2">
{timeString}
</span>
</div>