feat: add task detail slide-over panel with inline editing, subtasks, and notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 22:22:57 -05:00
parent f0bcc01993
commit af205b367d
5 changed files with 702 additions and 4 deletions

View File

@@ -0,0 +1,108 @@
import { useState, useRef, useEffect } from 'react'
import { Plus, Square, CheckSquare } from 'lucide-react'
import { WorkTaskStatus } from '../types/index.ts'
import type { WorkTask } from '../types/index.ts'
import { useCreateTask, useCompleteTask } from '../api/tasks.ts'
interface SubtaskListProps {
taskId: number
subtasks: WorkTask[]
}
export default function SubtaskList({ taskId, subtasks }: SubtaskListProps) {
const [showInput, setShowInput] = useState(false)
const [inputValue, setInputValue] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const createTask = useCreateTask()
const completeTask = useCompleteTask()
useEffect(() => {
if (showInput) inputRef.current?.focus()
}, [showInput])
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter' && inputValue.trim()) {
createTask.mutate(
{ title: inputValue.trim(), parentTaskId: taskId },
{
onSuccess: () => {
setInputValue('')
},
}
)
}
if (e.key === 'Escape') {
setShowInput(false)
setInputValue('')
}
}
function handleToggle(subtask: WorkTask) {
if (subtask.status !== WorkTaskStatus.Completed) {
completeTask.mutate(subtask.id)
}
}
return (
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold uppercase tracking-wider text-[#64748b]">
Subtasks
</h3>
<button
onClick={() => setShowInput(true)}
className="p-1 rounded hover:bg-white/5 text-[#64748b] hover:text-white transition-colors"
>
<Plus size={14} />
</button>
</div>
<div className="space-y-1">
{subtasks.map((subtask) => {
const isCompleted = subtask.status === WorkTaskStatus.Completed
return (
<div
key={subtask.id}
className="flex items-center gap-2 py-1.5 px-1 rounded hover:bg-white/5 cursor-pointer group"
onClick={() => handleToggle(subtask)}
>
{isCompleted ? (
<CheckSquare size={16} className="text-emerald-400 flex-shrink-0" />
) : (
<Square size={16} className="text-[#64748b] group-hover:text-white flex-shrink-0" />
)}
<span
className={`text-sm ${
isCompleted ? 'line-through text-[#64748b]' : 'text-white'
}`}
>
{subtask.title}
</span>
</div>
)
})}
{showInput && (
<div className="flex items-center gap-2 py-1.5 px-1">
<Square size={16} className="text-[#64748b] flex-shrink-0" />
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => {
if (!inputValue.trim()) {
setShowInput(false)
setInputValue('')
}
}}
placeholder="New subtask..."
className="flex-1 bg-[#0f1117] text-sm text-white px-2 py-1 rounded border border-transparent focus:border-indigo-500 outline-none placeholder-[#64748b]"
/>
</div>
)}
</div>
</div>
)
}