// Dependencies: react ^18, lucide-react
import React, { useState } from 'react';
import { Check, Plus } from 'lucide-react';
function cn(...classes: (string | false | null | undefined)[]) {
return classes.filter(Boolean).join(' ');
}
export type Subtask = {
id: string;
label: string;
done: boolean;
};
type Props = {
title?: string;
initialSubtasks: Subtask[];
};
let nextId = 0;
const newId = () => `subtask-${Date.now()}-${++nextId}`;
export function SubtaskChecklist({ title = 'Subtasks', initialSubtasks }: Props) {
const [subtasks, setSubtasks] = useState<Subtask[]>(initialSubtasks);
const [draft, setDraft] = useState('');
const completedCount = subtasks.filter((s) => s.done).length;
const total = subtasks.length;
const percent = total === 0 ? 0 : Math.round((completedCount / total) * 100);
const toggle = (id: string) => {
setSubtasks((prev) => prev.map((s) => (s.id === id ? { ...s, done: !s.done } : s)));
};
const addSubtask = () => {
const label = draft.trim();
if (!label) return;
setSubtasks((prev) => [...prev, { id: newId(), label, done: false }]);
setDraft('');
};
return (
<div className="w-full max-w-sm bg-[#0a0a0a] border border-[#1a1a1a] rounded-xl overflow-hidden">
{/* Header */}
<div className="px-4 py-3 border-b border-[#1a1a1a]">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-white">{title}</span>
<span className="text-xs text-zinc-500">
{completedCount} of {total}
</span>
</div>
<div className="h-1 w-full bg-[#1e1e1e] rounded-full overflow-hidden">
<div
className="h-full bg-indigo-500 rounded-full transition-all duration-300"
style={{ width: `${percent}%` }}
/>
</div>
</div>
{/* Subtask rows */}
<div className="flex flex-col">
{subtasks.map((subtask) => (
<button
key={subtask.id}
onClick={() => toggle(subtask.id)}
className={cn(
'flex items-center gap-3 px-4 py-2.5 text-left border-b border-[#1a1a1a] last:border-0 hover:bg-white/[0.03] transition-colors group'
)}
>
<span
className={cn(
'shrink-0 w-4 h-4 rounded-full border flex items-center justify-center transition-colors',
subtask.done
? 'bg-indigo-500 border-indigo-500'
: 'border-zinc-600 group-hover:border-zinc-400'
)}
>
{subtask.done && <Check className="w-2.5 h-2.5 text-white" strokeWidth={3} />}
</span>
<span
className={cn(
'text-sm flex-1 truncate',
subtask.done ? 'text-zinc-600 line-through' : 'text-zinc-200'
)}
>
{subtask.label}
</span>
</button>
))}
</div>
{/* Add subtask */}
<div className="flex items-center gap-2 px-4 py-2.5 border-t border-[#1a1a1a]">
<Plus className="w-3.5 h-3.5 text-zinc-600 shrink-0" />
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') addSubtask();
}}
placeholder="Add subtask"
className="flex-1 bg-transparent text-sm text-zinc-200 placeholder:text-zinc-600 focus:outline-none"
/>
</div>
</div>
);
}
// Usage:
// const SUBTASKS = [
// { id: '1', label: 'Write spec', done: true },
// { id: '2', label: 'Design API', done: true },
// { id: '3', label: 'Implement backend', done: false },
// { id: '4', label: 'Add tests', done: false },
// { id: '5', label: 'Ship to staging', done: false },
// ];
// <SubtaskChecklist title="Subtasks" initialSubtasks={SUBTASKS} />