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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user