feat(ui): redesign kanban columns and task cards — borderless columns, glow cards, live dots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 00:05:05 -05:00
parent 320ef51c74
commit 5ec4ca9a62
3 changed files with 76 additions and 87 deletions

View File

@@ -143,7 +143,7 @@ export default function KanbanBoard({ tasks, isLoading, onTaskClick }: KanbanBoa
<DragOverlay>
{activeTask ? (
<div className="rotate-2 scale-105">
<div className="rotate-1 scale-[1.03] opacity-90">
<TaskCard task={activeTask} onClick={() => {}} />
</div>
) : null}

View File

@@ -28,56 +28,56 @@ export default function KanbanColumn({
const taskIds = tasks.map((t) => t.id)
return (
<div
className={`
flex flex-col min-h-[400px] rounded-xl
bg-white/[0.02] border transition-all duration-200
${isOver ? 'border-indigo-500/30 bg-white/[0.04] shadow-lg shadow-indigo-500/5' : 'border-white/5'}
`}
>
<div className="flex flex-col min-h-[300px]">
{/* Column header */}
<div className="px-4 pt-4 pb-3">
<div className="flex items-center justify-between mb-2">
<h2 className="text-sm font-semibold text-white">{label}</h2>
<span
className="text-[11px] font-medium px-2 py-0.5 rounded-full"
style={{
backgroundColor: color + '20',
color,
}}
>
<div className="mb-3">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-[11px] font-semibold uppercase tracking-wider text-[var(--color-text-secondary)]">
{label}
</h2>
<span className="text-[11px] text-[var(--color-text-tertiary)]">
{tasks.length}
</span>
</div>
<div className="h-0.5 rounded-full" style={{ backgroundColor: color }} />
<div className="h-[2px] rounded-full" style={{ backgroundColor: color }} />
</div>
{/* Cards area */}
<div
ref={setNodeRef}
className="flex-1 flex flex-col gap-2 px-3 pb-3 overflow-y-auto"
className={`flex-1 flex flex-col gap-2 rounded-lg transition-colors duration-200 py-1 ${
isOver ? 'bg-white/[0.02]' : ''
}`}
>
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
{tasks.map((task) => (
<TaskCard key={task.id} task={task} onClick={onTaskClick} />
))}
</SortableContext>
{/* Empty state */}
{tasks.length === 0 && !showForm && (
<div className="flex-1 flex items-center justify-center min-h-[80px] rounded-lg border border-dashed border-white/[0.06]">
<span className="text-[11px] text-[var(--color-text-tertiary)]">No tasks</span>
</div>
)}
</div>
{/* Add task form / button (Pending column only) */}
{/* Add task (Pending column only) */}
{status === WorkTaskStatus.Pending && (
<div className="px-3 pb-3">
<div className="mt-2">
{showForm ? (
<CreateTaskForm onClose={() => setShowForm(false)} />
) : (
<button
onClick={() => setShowForm(true)}
className="flex items-center justify-center gap-1.5 w-full py-2 rounded-lg
text-xs text-[#64748b] border border-dashed border-white/10
hover:text-white hover:border-white/20 transition-all duration-200"
className="flex items-center gap-1.5 w-full py-2 rounded-lg
text-[11px] text-[var(--color-text-tertiary)]
hover:text-[var(--color-text-secondary)] hover:bg-white/[0.02]
transition-colors"
>
<Plus size={14} />
Add Task
<Plus size={13} />
New task
</button>
)}
</div>

View File

@@ -1,6 +1,6 @@
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { CheckSquare, Clock } from 'lucide-react'
import { Clock } from 'lucide-react'
import { WorkTaskStatus } from '../types/index.ts'
import type { WorkTask } from '../types/index.ts'
import { CATEGORY_COLORS } from '../lib/constants.ts'
@@ -36,7 +36,7 @@ export default function TaskCard({ task, onClick }: TaskCardProps) {
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
opacity: isDragging ? 0.4 : 1,
}
const categoryColor = CATEGORY_COLORS[task.category ?? 'Unknown'] ?? CATEGORY_COLORS['Unknown']
@@ -48,14 +48,6 @@ export default function TaskCard({ task, onClick }: TaskCardProps) {
).length ?? 0
const totalSubTasks = task.subTasks?.length ?? 0
let progressPercent: number | null = null
if (task.estimatedMinutes && task.startedAt) {
const start = new Date(task.startedAt).getTime()
const end = task.completedAt ? new Date(task.completedAt).getTime() : Date.now()
const elapsedMins = (end - start) / 60_000
progressPercent = Math.min(100, (elapsedMins / task.estimatedMinutes) * 100)
}
return (
<div
ref={setNodeRef}
@@ -64,62 +56,59 @@ export default function TaskCard({ task, onClick }: TaskCardProps) {
{...listeners}
onClick={() => onClick(task.id)}
className={`
relative rounded-lg cursor-grab active:cursor-grabbing
bg-[#1a1d27] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-indigo-500/5
transition-all duration-200
${isActive ? 'ring-1 ring-cyan-400/60 shadow-[0_0_12px_rgba(6,182,212,0.25)] animate-pulse-glow' : 'shadow-md shadow-black/20'}
card-glow rounded-xl cursor-grab active:cursor-grabbing
bg-[var(--color-surface)] border transition-all duration-200
hover:-translate-y-0.5
${isActive
? 'border-[var(--color-status-active)]/30 animate-pulse-glow'
: 'border-[var(--color-border)] hover:border-[var(--color-border-hover)]'
}
${isDragging ? 'shadow-xl shadow-black/40' : ''}
`}
>
{/* Category left border */}
<div
className="absolute left-0 top-0 bottom-0 w-1 rounded-l-lg"
style={{ backgroundColor: categoryColor }}
/>
<div className="px-3.5 py-3">
{/* Title row */}
<div className="flex items-start gap-2 mb-1.5">
{isActive && (
<span className="shrink-0 mt-1.5 w-1.5 h-1.5 rounded-full bg-[var(--color-status-active)] animate-live-dot" />
)}
<p className="text-[13px] font-medium text-[var(--color-text-primary)] leading-snug flex-1">
{task.title}
</p>
</div>
<div className="pl-4 pr-3 py-3">
{/* Title */}
<p className="text-sm font-medium text-white leading-snug mb-2 truncate">
{task.title}
</p>
{/* Meta row */}
<div className="flex items-center gap-2 text-[11px] text-[var(--color-text-secondary)]">
{task.category && (
<span className="flex items-center gap-1">
<span
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: categoryColor }}
/>
{task.category}
</span>
)}
{/* Category badge */}
{task.category && (
<span
className="inline-block text-[11px] font-medium px-2 py-0.5 rounded-full mb-2"
style={{
backgroundColor: categoryColor + '20',
color: categoryColor,
}}
>
{task.category}
</span>
)}
{/* Progress bar */}
{progressPercent !== null && (
<div className="h-1 w-full bg-white/5 rounded-full mb-2 overflow-hidden">
<div
className="h-full rounded-full bg-indigo-500 transition-all duration-300"
style={{ width: `${progressPercent}%` }}
/>
</div>
)}
{/* Footer row */}
<div className="flex items-center justify-between text-[11px] text-[#64748b]">
<div className="flex items-center gap-2">
{elapsed && (
<span className="flex items-center gap-1">
<Clock size={12} />
{elapsed}
</span>
)}
</div>
{elapsed && (
<span className="flex items-center gap-1">
<Clock size={10} />
{elapsed}
</span>
)}
{totalSubTasks > 0 && (
<span className="flex items-center gap-1">
<CheckSquare size={12} />
{completedSubTasks}/{totalSubTasks}
<span className="ml-auto flex items-center gap-1">
{Array.from({ length: totalSubTasks }, (_, i) => (
<span
key={i}
className={`w-1 h-1 rounded-full ${
i < completedSubTasks
? 'bg-[var(--color-status-completed)]'
: 'bg-white/10'
}`}
/>
))}
<span className="ml-0.5">{completedSubTasks}/{totalSubTasks}</span>
</span>
)}
</div>