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> <DragOverlay>
{activeTask ? ( {activeTask ? (
<div className="rotate-2 scale-105"> <div className="rotate-1 scale-[1.03] opacity-90">
<TaskCard task={activeTask} onClick={() => {}} /> <TaskCard task={activeTask} onClick={() => {}} />
</div> </div>
) : null} ) : null}

View File

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

View File

@@ -1,6 +1,6 @@
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { CheckSquare, Clock } from 'lucide-react' import { Clock } from 'lucide-react'
import { WorkTaskStatus } from '../types/index.ts' import { WorkTaskStatus } from '../types/index.ts'
import type { WorkTask } from '../types/index.ts' import type { WorkTask } from '../types/index.ts'
import { CATEGORY_COLORS } from '../lib/constants.ts' import { CATEGORY_COLORS } from '../lib/constants.ts'
@@ -36,7 +36,7 @@ export default function TaskCard({ task, onClick }: TaskCardProps) {
const style = { const style = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
opacity: isDragging ? 0.5 : 1, opacity: isDragging ? 0.4 : 1,
} }
const categoryColor = CATEGORY_COLORS[task.category ?? 'Unknown'] ?? CATEGORY_COLORS['Unknown'] const categoryColor = CATEGORY_COLORS[task.category ?? 'Unknown'] ?? CATEGORY_COLORS['Unknown']
@@ -48,14 +48,6 @@ export default function TaskCard({ task, onClick }: TaskCardProps) {
).length ?? 0 ).length ?? 0
const totalSubTasks = task.subTasks?.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 ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
@@ -64,62 +56,59 @@ export default function TaskCard({ task, onClick }: TaskCardProps) {
{...listeners} {...listeners}
onClick={() => onClick(task.id)} onClick={() => onClick(task.id)}
className={` className={`
relative rounded-lg cursor-grab active:cursor-grabbing card-glow rounded-xl cursor-grab active:cursor-grabbing
bg-[#1a1d27] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-indigo-500/5 bg-[var(--color-surface)] border transition-all duration-200
transition-all duration-200 hover:-translate-y-0.5
${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'} ${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="px-3.5 py-3">
<div {/* Title row */}
className="absolute left-0 top-0 bottom-0 w-1 rounded-l-lg" <div className="flex items-start gap-2 mb-1.5">
style={{ backgroundColor: categoryColor }} {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"> {/* Meta row */}
{/* Title */} <div className="flex items-center gap-2 text-[11px] text-[var(--color-text-secondary)]">
<p className="text-sm font-medium text-white leading-snug mb-2 truncate"> {task.category && (
{task.title} <span className="flex items-center gap-1">
</p> <span
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: categoryColor }}
/>
{task.category}
</span>
)}
{/* Category badge */} {elapsed && (
{task.category && ( <span className="flex items-center gap-1">
<span <Clock size={10} />
className="inline-block text-[11px] font-medium px-2 py-0.5 rounded-full mb-2" {elapsed}
style={{ </span>
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>
{totalSubTasks > 0 && ( {totalSubTasks > 0 && (
<span className="flex items-center gap-1"> <span className="ml-auto flex items-center gap-1">
<CheckSquare size={12} /> {Array.from({ length: totalSubTasks }, (_, i) => (
{completedSubTasks}/{totalSubTasks} <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> </span>
)} )}
</div> </div>