Modernize UI with improved animations and design system

This commit is contained in:
Jarian Cottingham 2026-02-24 22:27:24 -06:00
parent 0ed08e894d
commit 7044ffa751
5 changed files with 137 additions and 63 deletions

View File

@ -7,7 +7,7 @@ app.use(cors())
app.use(express.json()) app.use(express.json())
app.post('/api/execute', (req, res) => { app.post('/api/execute', (req, res) => {
const { description, validation } = req.body const { description } = req.body
const child = spawn('opencode', ['run', description], { const child = spawn('opencode', ['run', description], {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],

View File

@ -4,15 +4,18 @@ function App() {
return ( return (
<div className="min-h-screen text-white p-6"> <div className="min-h-screen text-white p-6">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8 animate-fade-in">
<div> <div>
<h1 className="text-5xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent"> <h1 className="text-5xl font-bold bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent bg-gradient-to-r bg-[length:200%_auto] animate-gradient-text">
WorkBoard WorkBoard
</h1> </h1>
<p className="text-slate-400 mt-2">AI-powered task management</p> <p className="text-slate-400 mt-2 text-lg">AI-powered task management</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-3 animate-slide-in">
<div className="relative">
<div className="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></div> <div className="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="absolute inset-0 rounded-full bg-emerald-500 animate-ping opacity-75"></span>
</div>
<span className="text-sm font-medium text-slate-300">AI Agent Ready</span> <span className="text-sm font-medium text-slate-300">AI Agent Ready</span>
</div> </div>
</div> </div>

View File

@ -41,38 +41,43 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
const colorStyles = { const colorStyles = {
blue: { blue: {
header: 'bg-gradient-to-r from-blue-500 to-cyan-500', header: 'bg-gradient-to-r from-blue-600 via-blue-500 to-cyan-500',
bg: 'bg-gradient-to-br from-blue-500/10 to-cyan-500/5', bg: 'from-blue-500/10 via-cyan-500/5 to-blue-500/10',
border: 'border-blue-500/20', border: 'border-blue-500/30',
button: 'bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700', button: 'bg-gradient-to-r from-blue-600 via-blue-500 to-cyan-500 hover:from-blue-700 hover:via-blue-600 hover:to-cyan-600',
pulse: 'from-blue-500/20 to-cyan-500/20', pulse: 'bg-gradient-to-r from-blue-500/20 via-cyan-500/20 to-blue-500/20',
glow: 'shadow-blue-500/30',
}, },
purple: { purple: {
header: 'bg-gradient-to-r from-purple-500 to-pink-500', header: 'bg-gradient-to-r from-purple-600 via-purple-500 to-pink-500',
bg: 'bg-gradient-to-br from-purple-500/10 to-pink-500/5', bg: 'from-purple-500/10 via-pink-500/5 to-purple-500/10',
border: 'border-purple-500/20', border: 'border-purple-500/30',
button: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700', button: 'bg-gradient-to-r from-purple-600 via-purple-500 to-pink-500 hover:from-purple-700 hover:via-purple-600 hover:to-pink-600',
pulse: 'from-purple-500/20 to-pink-500/20', pulse: 'bg-gradient-to-r from-purple-500/20 via-pink-500/20 to-purple-500/20',
glow: 'shadow-purple-500/30',
}, },
green: { green: {
header: 'bg-gradient-to-r from-emerald-500 to-teal-500', header: 'bg-gradient-to-r from-emerald-600 via-emerald-500 to-teal-500',
bg: 'bg-gradient-to-br from-emerald-500/10 to-teal-500/5', bg: 'from-emerald-500/10 via-teal-500/5 to-emerald-500/10',
border: 'border-emerald-500/20', border: 'border-emerald-500/30',
button: 'bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700', button: 'bg-gradient-to-r from-emerald-600 via-emerald-500 to-teal-500 hover:from-emerald-700 hover:via-emerald-600 hover:to-teal-600',
pulse: 'from-emerald-500/20 to-teal-500/20', pulse: 'bg-gradient-to-r from-emerald-500/20 via-teal-500/20 to-emerald-500/20',
glow: 'shadow-emerald-500/30',
}, },
} }
const styles = colorStyles[color] const styles = colorStyles[color]
return ( return (
<div className={`flex-1 rounded-2xl p-5 ${styles.bg} border ${styles.border} backdrop-blur-sm transition-all duration-300 hover:shadow-2xl hover:shadow-${color}-500/10`}> <div className={`flex-1 rounded-2xl p-5 ${styles.bg} border ${styles.border} backdrop-blur-sm transition-all duration-300 hover:shadow-2xl hover:${styles.glow}`}>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6 animate-slide-in">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${color === 'blue' ? 'bg-blue-400' : color === 'purple' ? 'bg-purple-400' : 'bg-emerald-400'} animate-pulse`}></div> <div className={`relative w-3 h-3 rounded-full ${color === 'blue' ? 'bg-blue-400' : color === 'purple' ? 'bg-purple-400' : 'bg-emerald-400'} animate-pulse`}>
<h2 className="text-2xl font-bold text-white">{title}</h2> <span className={`absolute inset-0 rounded-full animate-ping opacity-75 ${color === 'blue' ? 'bg-blue-400' : color === 'purple' ? 'bg-purple-400' : 'bg-emerald-400'}`}></span>
</div> </div>
<span className={`px-3 py-1 rounded-full text-sm font-medium bg-white/10 text-white/80 backdrop-blur-md`}> <h2 className="text-2xl font-bold text-white tracking-tight">{title}</h2>
</div>
<span className="px-3 py-1.5 rounded-full text-sm font-medium bg-white/10 text-white/90 backdrop-blur-md border border-white/5 animate-fade-in">
{items.length} {items.length}
</span> </span>
</div> </div>
@ -86,9 +91,9 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
/> />
))} ))}
{status === 'active' && items.some(i => i.status === 'active') && ( {status === 'active' && items.some(i => i.status === 'active') && (
<div className={`p-4 rounded-xl bg-gradient-to-r ${styles.pulse} border border-white/5 animate-pulse`}> <div className="p-4 rounded-xl animate-fade-in border border-white/5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-white"></div> <div className="w-2 h-2 rounded-full bg-white animate-pulse"></div>
<p className="text-sm font-medium text-white/90">AI agent is working on this task...</p> <p className="text-sm font-medium text-white/90">AI agent is working on this task...</p>
</div> </div>
</div> </div>
@ -97,16 +102,16 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
{status === 'queued' && ( {status === 'queued' && (
<button <button
onClick={() => setIsModalOpen(true)} onClick={() => setIsModalOpen(true)}
className="w-full py-4 border-2 border-dashed border-white/20 rounded-xl text-white/60 hover:border-white/40 hover:text-white/90 hover:bg-white/5 transition-all duration-200 flex items-center justify-center gap-2 group" className="w-full py-4 border-2 border-dashed border-white/20 rounded-xl text-white/60 hover:border-white/40 hover:text-white/90 hover:bg-white/5 transition-all duration-200 flex items-center justify-center gap-2 group animate-fade-in"
> >
<span className="group-hover:scale-110 transition-transform">+</span> <span className="group-hover:scale-110 group-hover:rotate-90 transition-all duration-300 transform">+</span>
<span className="font-medium">Add Work Item</span> <span className="font-medium">Add Work Item</span>
</button> </button>
)} )}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-fade-in">
<div className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl p-6 w-full max-w-md border border-white/10 shadow-2xl"> <div className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl p-6 w-full max-w-md border border-white/10 shadow-2xl animate-slide-in">
<h3 className="text-2xl font-bold text-white mb-6">New Work Item</h3> <h3 className="text-2xl font-bold text-white mb-6 tracking-tight">New Work Item</h3>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-slate-300 mb-2"> <label className="block text-sm font-medium text-slate-300 mb-2">
@ -118,7 +123,8 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, title: e.target.value }) setFormData({ ...formData, title: e.target.value })
} }
className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white placeholder-slate-600"
placeholder="Enter task title"
required required
/> />
</div> </div>
@ -131,8 +137,9 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, description: e.target.value }) setFormData({ ...formData, description: e.target.value })
} }
className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white placeholder-slate-600"
rows={3} rows={3}
placeholder="Describe the task in detail"
required required
/> />
</div> </div>
@ -145,15 +152,16 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, validation: e.target.value }) setFormData({ ...formData, validation: e.target.value })
} }
className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white placeholder-slate-600"
rows={3} rows={3}
placeholder="Define validation conditions"
required required
/> />
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
type="submit" type="submit"
className={`flex-1 px-6 py-3 rounded-xl ${styles.button} text-white transition-all duration-200 shadow-lg shadow-blue-500/20`} className={`flex-1 px-6 py-3 rounded-xl ${styles.button} text-white transition-all duration-200 shadow-lg ${styles.glow}`}
> >
Create Create
</button> </button>

View File

@ -37,23 +37,23 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
} }
return ( return (
<div className={`glass-card p-5 transition-all duration-300 hover:shadow-xl hover:shadow-white/5 ${workItem.failedAttempts >= 3 ? 'border-red-500/50' : ''}`}> <div className={`glass-card p-5 transition-all duration-300 ${isExpanded ? 'ring-2 ring-blue-500/50' : ''} ${workItem.failedAttempts >= 3 ? 'border-red-500/50' : ''}`}>
{!isExpanded ? ( {!isExpanded ? (
<> <>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0 animate-slide-in">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className={`w-2 h-2 rounded-full ${getStatusColor(workItem.status)}`}></span> <span className={`w-2.5 h-2.5 rounded-full ${getStatusColor(workItem.status)} transition-transform duration-300`}></span>
<h3 className="text-lg font-semibold text-white truncate">{workItem.title}</h3> <h3 className="text-lg font-semibold text-white truncate transition-colors duration-200">{workItem.title}</h3>
</div> </div>
<p className="text-sm text-slate-400 line-clamp-2 mb-2">{workItem.description}</p> <p className="text-sm text-slate-400 line-clamp-2 mb-2 transition-colors duration-200">{workItem.description}</p>
{workItem.failedAttempts > 0 && ( {workItem.failedAttempts > 0 && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 animate-fade-in">
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${getFailedColor()} text-white`}> <span className={`px-2 py-1 rounded-lg text-xs font-medium ${getFailedColor()} text-white transition-all duration-300`}>
{workItem.failedAttempts}/3 attempts {workItem.failedAttempts}/3 attempts
</span> </span>
{workItem.errorMessage && ( {workItem.errorMessage && (
<span className="text-xs text-red-400 truncate max-w-xs"> <span className="text-xs text-red-400 truncate max-w-xs transition-colors duration-200">
{workItem.errorMessage} {workItem.errorMessage}
</span> </span>
)} )}
@ -63,11 +63,11 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
<button <button
onClick={() => onUpdate(workItem.id, { status: 'queued' })} onClick={() => onUpdate(workItem.id, { status: 'queued' })}
disabled={workItem.status === 'complete' || workItem.failedAttempts >= 3} disabled={workItem.status === 'complete' || workItem.failedAttempts >= 3}
className="ml-4 bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white rounded-full w-10 h-10 flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-200 shadow-lg shadow-blue-500/20" className="ml-4 bg-gradient-to-r from-blue-600 via-blue-500 to-cyan-500 hover:from-blue-700 hover:via-blue-600 hover:to-cyan-600 text-white rounded-full w-10 h-10 flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-200 shadow-lg shadow-blue-500/20 hover:shadow-blue-500/40 hover:scale-110 active:scale-95"
title="Queue for AI execution" title="Queue for AI execution"
> >
<svg <svg
className="w-5 h-5 fill-current" className="w-5 h-5 fill-current transition-transform duration-300 group-hover:scale-110"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
@ -77,7 +77,7 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
</div> </div>
</> </>
) : ( ) : (
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4 animate-fade-in">
<div> <div>
<label className="block text-sm font-medium text-slate-300 mb-2"> <label className="block text-sm font-medium text-slate-300 mb-2">
Title Title
@ -88,7 +88,7 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, title: e.target.value }) setFormData({ ...formData, title: e.target.value })
} }
className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white" className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white placeholder-slate-600"
required required
/> />
</div> </div>
@ -101,7 +101,7 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, description: e.target.value }) setFormData({ ...formData, description: e.target.value })
} }
className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white" className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white placeholder-slate-600"
rows={3} rows={3}
required required
/> />
@ -115,7 +115,7 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, validation: e.target.value }) setFormData({ ...formData, validation: e.target.value })
} }
className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white" className="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-white placeholder-slate-600"
rows={3} rows={3}
required required
/> />
@ -138,7 +138,7 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
</p> </p>
</div> </div>
{workItem.errorMessage && ( {workItem.errorMessage && (
<div className="bg-red-900/20 border border-red-500/30 rounded-xl p-4"> <div className="bg-red-900/20 border border-red-500/30 rounded-xl p-4 animate-fade-in">
<p className="text-red-400 text-sm"> <p className="text-red-400 text-sm">
<span className="font-semibold">Error:</span> {workItem.errorMessage} <span className="font-semibold">Error:</span> {workItem.errorMessage}
</p> </p>
@ -147,21 +147,21 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
<div className="flex gap-3 pt-2"> <div className="flex gap-3 pt-2">
<button <button
type="submit" type="submit"
className="flex-1 px-6 py-3 bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white rounded-xl transition-all duration-200 shadow-lg shadow-blue-500/20 font-medium" className="flex-1 px-6 py-3 bg-gradient-to-r from-blue-600 via-blue-500 to-cyan-500 hover:from-blue-700 hover:via-blue-600 hover:to-cyan-600 text-white rounded-xl transition-all duration-200 shadow-lg shadow-blue-500/20 font-medium hover:shadow-blue-500/40 active:scale-98"
> >
Save Changes Save Changes
</button> </button>
<button <button
type="button" type="button"
onClick={() => setIsExpanded(false)} onClick={() => setIsExpanded(false)}
className="flex-1 px-6 py-3 bg-slate-700/50 hover:bg-slate-700 text-white rounded-xl transition-colors duration-200 font-medium" className="flex-1 px-6 py-3 bg-slate-700/50 hover:bg-slate-700 text-white rounded-xl transition-colors duration-200 font-medium hover:scale-105 active:scale-98"
> >
Collapse Collapse
</button> </button>
<button <button
type="button" type="button"
onClick={() => onDelete(workItem.id)} onClick={() => onDelete(workItem.id)}
className="flex-1 px-6 py-3 bg-red-600/10 hover:bg-red-600/20 text-red-400 hover:text-red-300 rounded-xl transition-colors duration-200 font-medium" className="flex-1 px-6 py-3 bg-red-600/10 hover:bg-red-600/20 text-red-400 hover:text-red-300 rounded-xl transition-colors duration-200 font-medium hover:scale-105 active:scale-98"
> >
Delete Delete
</button> </button>

View File

@ -9,31 +9,94 @@ body {
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
background-size: 200% 200%;
animation: gradientShift 15s ease infinite;
min-height: 100vh; min-height: 100vh;
} }
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }
.glass-card { .glass-card {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(12px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px; border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-card:hover {
background: rgba(255, 255, 255, 0.05);
transform: translateY(-2px);
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.12);
} }
.glass-btn { .glass-btn {
transition: all 0.2s ease; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
.glass-btn:hover { .glass-btn:hover {
transform: translateY(-1px); transform: translateY(-2px);
box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.3);
} }
.glass-btn:active { .glass-btn:active {
transform: scale(0.95); transform: scale(0.98);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes gradientText {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.animate-fade-in {
animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.animate-slide-in {
animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.animate-gradient-text {
animation: gradientText 3s ease infinite;
} }