"use client"; import { CalendarIcon, CheckSquareIcon, GripVerticalIcon, MessageSquareIcon, PlusIcon, SearchIcon, XIcon, } from "lucide-react"; import { AnimatePresence, LayoutGroup, animate, motion, useMotionValue, useReducedMotion } from "motion/react"; import { type FormEvent, type KeyboardEvent, type PointerEvent, useEffect, useId, useRef, useState, } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; export type LabelTone = "gray" | "blue" | "green" | "amber" | "red"; export type TaskPriority = "low" | "medium" | "high" | "urgent"; export interface BoardLabel { id: string; name: string; tone: LabelTone; } export interface BoardColumn { id: string; title: string; tone?: LabelTone; } export interface BoardTask { id: string; title: string; column: string; labels: string[]; priority?: TaskPriority; assignee?: string; due?: string; description?: string; checklist?: { id: string; text: string; done: boolean }[]; comments?: number; } export interface KanbanBoardProps { columns: BoardColumn[]; labels: BoardLabel[]; tasks: BoardTask[]; onTasksChange?: (tasks: BoardTask[]) => void; className?: string; } const toneBg: Record = { gray: "bg-muted-foreground/60", blue: "bg-info", green: "bg-success", amber: "bg-warning", red: "bg-destructive", }; const toneSoft: Record = { gray: "bg-muted-foreground/12", blue: "bg-info/12", green: "bg-success/12", amber: "bg-warning/16", red: "bg-destructive/12", }; const avatarTones = [ "bg-info/16 text-info-foreground", "bg-success/16 text-success-foreground", "bg-warning/14 text-warning-foreground", "bg-destructive/14 text-destructive-foreground", ]; const priorities: TaskPriority[] = ["low", "medium", "high", "urgent"]; const spring = { type: "spring", stiffness: 520, damping: 40 } as const; const glide = { type: "spring", stiffness: 380, damping: 39 } as const; const hit = "relative after:absolute after:-inset-y-1 after:inset-x-0 after:content-['']"; function initials(name: string) { return name .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase(); } function avatarTone(name: string) { let hash = 0; for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) >>> 0; return avatarTones[hash % avatarTones.length]; } function Avatar({ name, className }: { name: string; className?: string }) { return ( {initials(name)} ); } function PriorityBars({ priority }: { priority: TaskPriority }) { if (priority === "urgent") { return ( ! ); } const level = priorities.indexOf(priority) + 1; return ( {[1, 2, 3].map((bar) => ( ))} ); } function CardBody({ task, labels, compact, }: { task: BoardTask; labels: BoardLabel[]; compact: boolean; }) { const taskLabels = task.labels .map((id) => labels.find((l) => l.id === id)) .filter((l): l is BoardLabel => Boolean(l)); const done = task.checklist?.filter((c) => c.done).length ?? 0; const total = task.checklist?.length ?? 0; if (compact) { return ( {taskLabels.map((l) => ( ))} {task.title} {task.priority && } {task.assignee && } ); } return ( {taskLabels.length > 0 && ( {taskLabels.map((l) => ( {l.name} ))} )} {task.title} {task.priority && } {task.due && ( {task.due} )} {total > 0 && ( {done}/{total} )} {!!task.comments && ( {task.comments} )} {task.assignee && } ); } function TaskDetail({ task, columns, labels, onUpdate, onMove, onClose, }: { task: BoardTask; columns: BoardColumn[]; labels: BoardLabel[]; onUpdate: (patch: Partial) => void; onMove: (column: string) => void; onClose: () => void; }) { const reduce = useReducedMotion(); const closeRef = useRef(null); const [newItem, setNewItem] = useState(""); const checklist = task.checklist ?? []; const done = checklist.filter((c) => c.done).length; useEffect(() => { closeRef.current?.focus(); }, []); const addItem = (event: FormEvent) => { event.preventDefault(); const text = newItem.trim(); if (!text) return; onUpdate({ checklist: [...checklist, { id: `${task.id}-${Date.now()}`, text, done: false }] }); setNewItem(""); }; return ( { if (e.key === "Escape") { e.stopPropagation(); onClose(); } }} role="dialog" transition={reduce ? { duration: 0 } : glide} >
{task.id.toUpperCase()}