kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { motion } from "framer-motion";
2import { useTranslation } from "react-i18next";
3import TaskCardContextMenuContent from "@/components/kanban-board/task-card-context-menu/task-card-context-menu-content";
4import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
5import { Checkbox } from "@/components/ui/checkbox";
6import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
7import { getColumnIcon } from "@/lib/column";
8import type Task from "@/types/task";
9import SubtaskAssigneePopover from "./subtask-assignee-popover";
10import SubtaskStatusPopover from "./subtask-status-popover";
11
12type SubtaskRowProps = {
13 task: Task;
14 tasks: Task[];
15 projectId: string;
16 workspaceId: string;
17 isSelected: boolean;
18 isFocused: boolean;
19 selectionRadius: string;
20 assignee: {
21 user?: { image?: string | null; name?: string | null } | null;
22 } | null;
23 onToggleSelection: () => void;
24 onNavigate: () => void;
25 onDeleteClick: () => void;
26};
27
28export default function SubtaskRow({
29 task,
30 tasks,
31 projectId,
32 workspaceId,
33 isSelected,
34 isFocused,
35 selectionRadius,
36 assignee,
37 onToggleSelection,
38 onNavigate,
39 onDeleteClick,
40}: SubtaskRowProps) {
41 const { t } = useTranslation();
42
43 return (
44 <motion.div
45 layout
46 initial={{ opacity: 0, height: 0 }}
47 animate={{ opacity: 1, height: "auto" }}
48 exit={{ opacity: 0, height: 0 }}
49 transition={{ duration: 0.2, ease: "easeOut" }}
50 >
51 <ContextMenu>
52 <ContextMenuTrigger asChild>
53 <div
54 className={`group flex items-center gap-2 py-1 px-2 ${selectionRadius} transition-colors cursor-default ${isSelected ? "bg-primary/10 hover:bg-primary/15" : "hover:bg-accent/50"} ${isFocused ? "ring-1 ring-inset ring-ring/50" : ""}`}
55 >
56 <Checkbox
57 checked={isSelected}
58 onCheckedChange={onToggleSelection}
59 />
60
61 <SubtaskStatusPopover tasks={tasks} projectId={projectId}>
62 <button
63 type="button"
64 className="shrink-0 flex items-center justify-center rounded p-0.5 transition-colors outline-none [&_svg]:text-muted-foreground hover:[&_svg]:text-foreground"
65 >
66 {getColumnIcon(task.status, false)}
67 </button>
68 </SubtaskStatusPopover>
69
70 <button
71 type="button"
72 className="flex-1 min-w-0 text-left outline-none"
73 onClick={onNavigate}
74 >
75 <span
76 className={`text-sm truncate block ${task.status === "done" ? "line-through text-muted-foreground" : "text-foreground/90"}`}
77 >
78 {task.title}
79 </span>
80 </button>
81
82 <SubtaskAssigneePopover tasks={tasks} workspaceId={workspaceId}>
83 <button
84 type="button"
85 className="shrink-0 flex items-center justify-center rounded p-0.5 transition-colors outline-none"
86 >
87 {task.userId && assignee ? (
88 <Avatar className="h-5 w-5">
89 <AvatarImage
90 src={assignee?.user?.image ?? ""}
91 alt={assignee?.user?.name || ""}
92 />
93 <AvatarFallback className="text-[9px] font-medium border border-border/30">
94 {assignee?.user?.name?.charAt(0).toUpperCase()}
95 </AvatarFallback>
96 </Avatar>
97 ) : (
98 <div
99 className="flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-border/70"
100 title={t("tasks:popover.assignee.unassigned")}
101 >
102 <span className="text-[9px] font-medium text-muted-foreground">
103 ?
104 </span>
105 </div>
106 )}
107 </button>
108 </SubtaskAssigneePopover>
109 </div>
110 </ContextMenuTrigger>
111
112 <TaskCardContextMenuContent
113 task={task}
114 taskCardContext={{
115 projectId,
116 worskpaceId: workspaceId,
117 }}
118 onDeleteClick={onDeleteClick}
119 />
120 </ContextMenu>
121 </motion.div>
122 );
123}