@emberai-engg/task-board 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2259 @@
1
+ // src/components/TaskBoard.tsx
2
+ import { useState as useState9, useEffect as useEffect8, useCallback as useCallback7 } from "react";
3
+ import { DragDropContext } from "@hello-pangea/dnd";
4
+
5
+ // src/context/TaskBoardProvider.tsx
6
+ import { createContext, useContext, useMemo } from "react";
7
+
8
+ // src/services/taskBoardService.ts
9
+ function createTaskBoardService(apiClient, basePath = "/api/v1/taskboard") {
10
+ return {
11
+ // ─── Tasks ───
12
+ async listTasks(projectSlug, perColumn = 10) {
13
+ const { data } = await apiClient.get(
14
+ `${basePath}/tasks?project_slug=${encodeURIComponent(projectSlug)}&per_column=${perColumn}`
15
+ );
16
+ return data;
17
+ },
18
+ async listColumnTasks(projectSlug, statusKey, offset, limit) {
19
+ const { data } = await apiClient.get(
20
+ `${basePath}/tasks/column?project_slug=${encodeURIComponent(projectSlug)}&status_key=${encodeURIComponent(statusKey)}&offset=${offset}&limit=${limit}`
21
+ );
22
+ return data;
23
+ },
24
+ async getTask(taskId) {
25
+ const { data } = await apiClient.get(`${basePath}/tasks/${taskId}`);
26
+ return data;
27
+ },
28
+ async createTask(payload) {
29
+ const { data } = await apiClient.post(`${basePath}/tasks`, payload);
30
+ return data;
31
+ },
32
+ async updateTask(taskId, payload) {
33
+ const { data } = await apiClient.patch(`${basePath}/tasks/${taskId}`, payload);
34
+ return data;
35
+ },
36
+ async deleteTask(taskId) {
37
+ await apiClient.delete(`${basePath}/tasks/${taskId}`);
38
+ },
39
+ async markTaskRead(taskId) {
40
+ await apiClient.post(`${basePath}/tasks/${taskId}/read`);
41
+ },
42
+ // ─── Comments ───
43
+ async listComments(taskId) {
44
+ const { data } = await apiClient.get(`${basePath}/tasks/${taskId}/comments`);
45
+ return data;
46
+ },
47
+ async addComment(taskId, payload) {
48
+ const { data } = await apiClient.post(`${basePath}/tasks/${taskId}/comments`, payload);
49
+ return data;
50
+ },
51
+ async editComment(taskId, commentId, payload) {
52
+ const { data } = await apiClient.patch(
53
+ `${basePath}/tasks/${taskId}/comments/${commentId}`,
54
+ payload
55
+ );
56
+ return data;
57
+ },
58
+ async deleteComment(taskId, commentId) {
59
+ await apiClient.delete(`${basePath}/tasks/${taskId}/comments/${commentId}`);
60
+ },
61
+ // ─── Mentions ───
62
+ async searchMentionUsers(query) {
63
+ const { data } = await apiClient.get(
64
+ `${basePath}/mentions/users?q=${encodeURIComponent(query)}`
65
+ );
66
+ return data;
67
+ },
68
+ // ─── Notifications ───
69
+ async getNotificationCount() {
70
+ const { data } = await apiClient.get(
71
+ `${basePath}/notifications/count`
72
+ );
73
+ return data.count;
74
+ },
75
+ async listNotifications(limit = 30) {
76
+ const { data } = await apiClient.get(
77
+ `${basePath}/notifications?limit=${limit}`
78
+ );
79
+ return data;
80
+ },
81
+ async markNotificationRead(notificationId) {
82
+ await apiClient.patch(`${basePath}/notifications/${notificationId}/read`);
83
+ },
84
+ async markAllNotificationsRead() {
85
+ await apiClient.post(`${basePath}/notifications/read-all`);
86
+ }
87
+ };
88
+ }
89
+
90
+ // src/utils/constants.ts
91
+ var DEFAULT_COLUMNS = [
92
+ { key: "backlog", label: "Backlog", color: "bg-neutral-400", description: "Tasks not yet scheduled" },
93
+ { key: "blocked", label: "Blocked", color: "bg-red-500", description: "Waiting on a dependency" },
94
+ { key: "queued", label: "Queued", color: "bg-blue-500", description: "Scheduled for this sprint" },
95
+ { key: "in_progress", label: "In Progress", color: "bg-amber-500", description: "Actively being worked on" },
96
+ { key: "in_testing", label: "In Testing", color: "bg-teal-500", description: "Under QA and validation" },
97
+ { key: "client_review", label: "Client Review", color: "bg-purple-500", description: "Live & ready for client review" },
98
+ { key: "changes_requested", label: "Changes Requested", color: "bg-orange-500", description: "Revisions needed from review" },
99
+ { key: "approved", label: "Approved", color: "bg-green-500", description: "Signed off and complete" }
100
+ ];
101
+ var DEFAULT_PRIORITIES = [
102
+ { value: "urgent", label: "Critical", className: "bg-red-50 text-red-600 border-red-200" },
103
+ { value: "high", label: "High", className: "bg-orange-50 text-orange-600 border-orange-200" },
104
+ { value: "medium", label: "Medium", className: "bg-amber-50 text-amber-600 border-amber-200" },
105
+ { value: "low", label: "Low", className: "bg-neutral-100 text-neutral-500 border-neutral-200" }
106
+ ];
107
+ var PREDEFINED_TAGS = [
108
+ { value: "traceability", label: "Traceability", className: "bg-blue-50 text-blue-600 border-blue-200" },
109
+ { value: "info-architecture", label: "Info Architecture", className: "bg-purple-50 text-purple-600 border-purple-200" },
110
+ { value: "ui-ux", label: "UI/UX", className: "bg-pink-50 text-pink-600 border-pink-200" },
111
+ { value: "workflow-logic", label: "Workflow Logic", className: "bg-teal-50 text-teal-600 border-teal-200" },
112
+ { value: "legal-reasoning", label: "Legal Reasoning", className: "bg-amber-50 text-amber-600 border-amber-200" },
113
+ { value: "bug-fix", label: "Bug Fix", className: "bg-red-50 text-red-600 border-red-200" }
114
+ ];
115
+ var DESCRIPTION_SECTIONS = [
116
+ { key: "problem", label: "Problem" },
117
+ { key: "user_story", label: "User Story" },
118
+ { key: "proposed_behavior", label: "Proposed Behavior" },
119
+ { key: "acceptance_criteria", label: "Acceptance Criteria" },
120
+ { key: "open_questions", label: "Open Questions" }
121
+ ];
122
+ var EMPTY_DESCRIPTION = {
123
+ problem: "",
124
+ user_story: "",
125
+ proposed_behavior: "",
126
+ acceptance_criteria: "",
127
+ open_questions: ""
128
+ };
129
+ var POSITION_GAP = 1e3;
130
+ var DEFAULT_PAGE_SIZE = 10;
131
+ var NOTIFICATION_POLL_INTERVAL = 3e4;
132
+
133
+ // src/context/TaskBoardProvider.tsx
134
+ import { jsx } from "react/jsx-runtime";
135
+ var TaskBoardContext = createContext(null);
136
+ function useTaskBoardContext() {
137
+ const ctx = useContext(TaskBoardContext);
138
+ if (!ctx) {
139
+ throw new Error("useTaskBoardContext must be used within a <TaskBoardProvider>");
140
+ }
141
+ return ctx;
142
+ }
143
+ function TaskBoardProvider({
144
+ children,
145
+ ...config
146
+ }) {
147
+ const service = useMemo(
148
+ () => createTaskBoardService(config.apiClient, config.apiBasePath),
149
+ [config.apiClient, config.apiBasePath]
150
+ );
151
+ const features = useMemo(
152
+ () => ({
153
+ dragAndDrop: config.features?.dragAndDrop ?? true,
154
+ comments: config.features?.comments ?? true,
155
+ mentions: config.features?.mentions ?? true,
156
+ notifications: config.features?.notifications ?? true,
157
+ internalComments: config.features?.internalComments ?? true,
158
+ tags: config.features?.tags ?? true,
159
+ sharing: config.features?.sharing ?? true,
160
+ filters: config.features?.filters ?? true,
161
+ unreadIndicators: config.features?.unreadIndicators ?? true
162
+ }),
163
+ [config.features]
164
+ );
165
+ const value = useMemo(
166
+ () => ({
167
+ service,
168
+ user: config.user,
169
+ projects: config.projects ?? [],
170
+ columns: config.columns ?? DEFAULT_COLUMNS,
171
+ priorities: config.priorities ?? DEFAULT_PRIORITIES,
172
+ tags: config.tags ?? PREDEFINED_TAGS,
173
+ config,
174
+ features
175
+ }),
176
+ [service, config, features]
177
+ );
178
+ return /* @__PURE__ */ jsx(TaskBoardContext.Provider, { value, children });
179
+ }
180
+
181
+ // src/hooks/useTaskBoard.ts
182
+ import { useState, useEffect, useCallback, useMemo as useMemo2, useRef } from "react";
183
+
184
+ // src/utils/helpers.ts
185
+ function getPriorityStyle(priority) {
186
+ return DEFAULT_PRIORITIES.find((p) => p.value === priority) ?? DEFAULT_PRIORITIES[2];
187
+ }
188
+ function getTagStyle(tag) {
189
+ const predefined = PREDEFINED_TAGS.find((t) => t.value === tag);
190
+ if (predefined) return predefined;
191
+ const label = tag.charAt(0).toUpperCase() + tag.slice(1).replace(/-/g, " ");
192
+ return { value: tag, label, className: "bg-neutral-100 text-neutral-500 border-neutral-200" };
193
+ }
194
+ function getInitials(name) {
195
+ return name.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
196
+ }
197
+ function parseDate(dateStr) {
198
+ if (!dateStr) return /* @__PURE__ */ new Date();
199
+ const d = new Date(dateStr);
200
+ if (isNaN(d.getTime()) && !dateStr.endsWith("Z") && !dateStr.includes("+")) {
201
+ return /* @__PURE__ */ new Date(dateStr + "Z");
202
+ }
203
+ return d;
204
+ }
205
+ function formatDate(dateStr) {
206
+ if (!dateStr) return "";
207
+ const d = parseDate(dateStr);
208
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
209
+ }
210
+ function formatDateTime(dateStr) {
211
+ if (!dateStr) return "";
212
+ const d = parseDate(dateStr);
213
+ return d.toLocaleDateString("en-US", {
214
+ month: "short",
215
+ day: "numeric",
216
+ hour: "numeric",
217
+ minute: "2-digit"
218
+ });
219
+ }
220
+ function getDescriptionPreview(desc) {
221
+ if (!desc) return "";
222
+ if (typeof desc === "string") return desc;
223
+ for (const section of DESCRIPTION_SECTIONS) {
224
+ if (desc[section.key]?.trim()) return desc[section.key].trim();
225
+ }
226
+ return "";
227
+ }
228
+ function hasDescription(desc) {
229
+ if (!desc) return false;
230
+ if (typeof desc === "string") return desc.trim().length > 0;
231
+ return DESCRIPTION_SECTIONS.some((s) => desc[s.key]?.trim());
232
+ }
233
+ function getUserProjects(apps, allProjects) {
234
+ if (apps.includes("all")) return allProjects;
235
+ return allProjects.filter((p) => apps.includes(p.slug));
236
+ }
237
+
238
+ // src/hooks/useTaskBoard.ts
239
+ function useTaskBoard() {
240
+ const { service, user, projects: configProjects, columns, config } = useTaskBoardContext();
241
+ const projects = useMemo2(
242
+ () => configProjects.length > 0 ? configProjects : getUserProjects(user.apps, []),
243
+ [configProjects, user.apps]
244
+ );
245
+ const [selectedProject, setSelectedProject] = useState("");
246
+ const [tasks, setTasks] = useState({});
247
+ const [columnTotals, setColumnTotals] = useState({});
248
+ const [columnUnreads, setColumnUnreads] = useState({});
249
+ const [boardLoading, setBoardLoading] = useState(false);
250
+ const [loadingMore, setLoadingMore] = useState({});
251
+ const [error, setError] = useState("");
252
+ const [successMessage, setSuccessMessage] = useState("");
253
+ useEffect(() => {
254
+ if (selectedProject || projects.length === 0) return;
255
+ if (typeof window !== "undefined") {
256
+ const params = new URLSearchParams(window.location.search);
257
+ const urlProject = params.get("project");
258
+ if (urlProject && projects.find((p) => p.slug === urlProject)) {
259
+ setSelectedProject(urlProject);
260
+ return;
261
+ }
262
+ }
263
+ setSelectedProject(projects[0].slug);
264
+ }, [projects, selectedProject]);
265
+ const fetchTasks = useCallback(async () => {
266
+ if (!selectedProject) return;
267
+ setBoardLoading(true);
268
+ try {
269
+ const data = await service.listTasks(selectedProject, DEFAULT_PAGE_SIZE);
270
+ const newTasks = {};
271
+ const newTotals = {};
272
+ const newUnreads = {};
273
+ for (const key of columns.map((c) => c.key)) {
274
+ const col = data[key];
275
+ if (col) {
276
+ newTasks[key] = col.tasks || [];
277
+ newTotals[key] = col.total || 0;
278
+ newUnreads[key] = col.unread || 0;
279
+ } else {
280
+ newTasks[key] = [];
281
+ newTotals[key] = 0;
282
+ newUnreads[key] = 0;
283
+ }
284
+ }
285
+ setTasks(newTasks);
286
+ setColumnTotals(newTotals);
287
+ setColumnUnreads(newUnreads);
288
+ } catch {
289
+ setError("Failed to load tasks");
290
+ } finally {
291
+ setBoardLoading(false);
292
+ }
293
+ }, [selectedProject, service, columns]);
294
+ useEffect(() => {
295
+ fetchTasks();
296
+ }, [fetchTasks]);
297
+ const loadMoreTasks = useCallback(async (statusKey) => {
298
+ if (!selectedProject || loadingMore[statusKey]) return;
299
+ const current = tasks[statusKey]?.length || 0;
300
+ const total = columnTotals[statusKey] || 0;
301
+ if (current >= total) return;
302
+ setLoadingMore((prev) => ({ ...prev, [statusKey]: true }));
303
+ try {
304
+ const newTasks = await service.listColumnTasks(
305
+ selectedProject,
306
+ statusKey,
307
+ current,
308
+ DEFAULT_PAGE_SIZE
309
+ );
310
+ setTasks((prev) => ({
311
+ ...prev,
312
+ [statusKey]: [...prev[statusKey] || [], ...newTasks]
313
+ }));
314
+ } catch (err) {
315
+ config.onError?.(err instanceof Error ? err : new Error(String(err)));
316
+ } finally {
317
+ setLoadingMore((prev) => ({ ...prev, [statusKey]: false }));
318
+ }
319
+ }, [selectedProject, tasks, columnTotals, loadingMore, service, config]);
320
+ const successTimeoutRef = useRef(null);
321
+ useEffect(() => {
322
+ return () => {
323
+ if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
324
+ };
325
+ }, []);
326
+ const showSuccess = (msg) => {
327
+ setSuccessMessage(msg);
328
+ if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
329
+ successTimeoutRef.current = setTimeout(() => setSuccessMessage(""), 3e3);
330
+ };
331
+ return {
332
+ projects,
333
+ selectedProject,
334
+ setSelectedProject,
335
+ tasks,
336
+ setTasks,
337
+ columnTotals,
338
+ columnUnreads,
339
+ setColumnUnreads,
340
+ boardLoading,
341
+ loadingMore,
342
+ error,
343
+ setError,
344
+ successMessage,
345
+ showSuccess,
346
+ fetchTasks,
347
+ loadMoreTasks
348
+ };
349
+ }
350
+
351
+ // src/hooks/useTaskActions.ts
352
+ import { useCallback as useCallback2, useRef as useRef2 } from "react";
353
+ function useTaskActions(tasks, setTasks, fetchTasks) {
354
+ const { service, config } = useTaskBoardContext();
355
+ const tasksRef = useRef2(tasks);
356
+ tasksRef.current = tasks;
357
+ const createTask = useCallback2(async (data) => {
358
+ const task = await service.createTask(data);
359
+ config.onTaskCreate?.(task);
360
+ await fetchTasks();
361
+ return task;
362
+ }, [service, config, fetchTasks]);
363
+ const updateTask = useCallback2(async (taskId, data) => {
364
+ const task = await service.updateTask(taskId, data);
365
+ config.onTaskUpdate?.(task);
366
+ await fetchTasks();
367
+ return task;
368
+ }, [service, config, fetchTasks]);
369
+ const deleteTask = useCallback2(async (taskId) => {
370
+ await service.deleteTask(taskId);
371
+ config.onTaskDelete?.(taskId);
372
+ await fetchTasks();
373
+ }, [service, config, fetchTasks]);
374
+ const markTaskRead = useCallback2(async (taskId) => {
375
+ service.markTaskRead(taskId).catch(() => {
376
+ });
377
+ }, [service]);
378
+ const moveTask = useCallback2(async (taskId, sourceStatus, destStatus, sourceIndex, destIndex) => {
379
+ const currentTasks = tasksRef.current;
380
+ const sourceCol = [...currentTasks[sourceStatus] || []];
381
+ const destCol = sourceStatus === destStatus ? sourceCol : [...currentTasks[destStatus] || []];
382
+ const [movedTask] = sourceCol.splice(sourceIndex, 1);
383
+ if (!movedTask) return;
384
+ const updatedTask = { ...movedTask, status: destStatus };
385
+ destCol.splice(destIndex, 0, updatedTask);
386
+ let newPosition;
387
+ if (destCol.length === 1) {
388
+ newPosition = POSITION_GAP;
389
+ } else if (destIndex === 0) {
390
+ newPosition = (destCol[1]?.position ?? POSITION_GAP) - POSITION_GAP;
391
+ } else if (destIndex === destCol.length - 1) {
392
+ newPosition = (destCol[destCol.length - 2]?.position ?? 0) + POSITION_GAP;
393
+ } else {
394
+ const above = destCol[destIndex - 1]?.position ?? 0;
395
+ const below = destCol[destIndex + 1]?.position ?? above + POSITION_GAP * 2;
396
+ newPosition = (above + below) / 2;
397
+ }
398
+ updatedTask.position = newPosition;
399
+ const newTasks = { ...currentTasks };
400
+ newTasks[sourceStatus] = sourceCol;
401
+ if (sourceStatus !== destStatus) {
402
+ newTasks[destStatus] = destCol;
403
+ }
404
+ setTasks(newTasks);
405
+ try {
406
+ await service.updateTask(taskId, { status: destStatus, position: newPosition });
407
+ } catch {
408
+ fetchTasks();
409
+ }
410
+ }, [setTasks, service, fetchTasks]);
411
+ return { createTask, updateTask, deleteTask, markTaskRead, moveTask };
412
+ }
413
+
414
+ // src/hooks/useShareLink.ts
415
+ import { useState as useState2, useCallback as useCallback3 } from "react";
416
+ function useShareLink() {
417
+ const [copiedTaskId, setCopiedTaskId] = useState2(null);
418
+ const copyShareLink = useCallback3((taskId, projectSlug) => {
419
+ if (typeof window === "undefined") return;
420
+ const url = new URL(window.location.origin + window.location.pathname);
421
+ url.searchParams.set("project", projectSlug);
422
+ url.searchParams.set("task", taskId);
423
+ navigator.clipboard.writeText(url.toString()).then(() => {
424
+ setCopiedTaskId(taskId);
425
+ setTimeout(() => setCopiedTaskId(null), 2e3);
426
+ });
427
+ }, []);
428
+ return { copiedTaskId, copyShareLink };
429
+ }
430
+
431
+ // src/components/SkeletonPulse.tsx
432
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
433
+ function SkeletonPulse({ className = "" }) {
434
+ return /* @__PURE__ */ jsx2("div", { className: `animate-pulse rounded bg-neutral-200/60 ${className}` });
435
+ }
436
+ function SkeletonCard() {
437
+ return /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg border border-neutral-200 p-3 space-y-2.5", children: [
438
+ /* @__PURE__ */ jsx2(SkeletonPulse, { className: "h-4 w-3/4" }),
439
+ /* @__PURE__ */ jsx2(SkeletonPulse, { className: "h-4 w-16 rounded-sm" }),
440
+ /* @__PURE__ */ jsx2(SkeletonPulse, { className: "h-3 w-full" }),
441
+ /* @__PURE__ */ jsx2(SkeletonPulse, { className: "h-3 w-2/3" }),
442
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pt-1", children: [
443
+ /* @__PURE__ */ jsx2(SkeletonPulse, { className: "h-2.5 w-12" }),
444
+ /* @__PURE__ */ jsx2(SkeletonPulse, { className: "h-2.5 w-8" })
445
+ ] })
446
+ ] });
447
+ }
448
+ function BoardSkeleton() {
449
+ const cardCounts = [3, 2, 2, 1, 1, 0, 1];
450
+ return /* @__PURE__ */ jsx2("div", { className: "flex-1 min-h-0 overflow-hidden pb-4", children: /* @__PURE__ */ jsx2("div", { className: "flex gap-4 min-w-max h-full", children: cardCounts.map((count, i) => /* @__PURE__ */ jsxs("div", { className: "w-[280px] flex flex-col shrink-0", children: [
451
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-3 px-1", children: [
452
+ /* @__PURE__ */ jsx2(SkeletonPulse, { className: "w-2 h-2 rounded-full" }),
453
+ /* @__PURE__ */ jsx2(SkeletonPulse, { className: "h-3 w-20" })
454
+ ] }),
455
+ /* @__PURE__ */ jsx2("div", { className: "flex-1 rounded-xl bg-neutral-100/50 p-2 space-y-2", children: Array.from({ length: count }).map((_, j) => /* @__PURE__ */ jsx2(SkeletonCard, {}, j)) })
456
+ ] }, i)) }) });
457
+ }
458
+
459
+ // src/components/KanbanColumn.tsx
460
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
461
+ import { Droppable } from "@hello-pangea/dnd";
462
+
463
+ // src/components/TaskCard.tsx
464
+ import { memo } from "react";
465
+ import { Draggable } from "@hello-pangea/dnd";
466
+
467
+ // src/components/PriorityBadge.tsx
468
+ import { jsx as jsx3 } from "react/jsx-runtime";
469
+ function PriorityBadge({ priority, size = "sm" }) {
470
+ const style = getPriorityStyle(priority);
471
+ const sizeClass = size === "sm" ? "px-1.5 py-0.5 text-[9px]" : "px-2.5 py-1 text-[10px]";
472
+ return /* @__PURE__ */ jsx3("span", { className: `inline-flex items-center font-semibold uppercase tracking-wide rounded border ${style.className} ${sizeClass}`, children: style.label });
473
+ }
474
+
475
+ // src/icons/index.tsx
476
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
477
+ var PlusIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsxs2("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: [
478
+ /* @__PURE__ */ jsx4("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
479
+ /* @__PURE__ */ jsx4("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
480
+ ] });
481
+ var XIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsxs2("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: [
482
+ /* @__PURE__ */ jsx4("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
483
+ /* @__PURE__ */ jsx4("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
484
+ ] });
485
+ var ChevronDownIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsx4("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx4("polyline", { points: "6 9 12 15 18 9" }) });
486
+ var MessageSquareIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsx4("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx4("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
487
+ var KanbanIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsxs2("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: [
488
+ /* @__PURE__ */ jsx4("rect", { width: "8", height: "4", x: "8", y: "2", rx: "1", ry: "1" }),
489
+ /* @__PURE__ */ jsx4("path", { d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" }),
490
+ /* @__PURE__ */ jsx4("path", { d: "m9 14 2 2 4-4" })
491
+ ] });
492
+ var LinkIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsxs2("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: [
493
+ /* @__PURE__ */ jsx4("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
494
+ /* @__PURE__ */ jsx4("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
495
+ ] });
496
+ var CheckIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsx4("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx4("path", { d: "M20 6 9 17l-5-5" }) });
497
+ var BellIcon = ({ className = "", size = 24 }) => /* @__PURE__ */ jsx4("svg", { className, width: size, height: size, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.75, children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" }) });
498
+ var FilterIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsx4("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx4("polygon", { points: "22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" }) });
499
+ var PencilIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsx4("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: /* @__PURE__ */ jsx4("path", { d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" }) });
500
+ var TrashIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsxs2("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: [
501
+ /* @__PURE__ */ jsx4("path", { d: "M3 6h18" }),
502
+ /* @__PURE__ */ jsx4("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
503
+ /* @__PURE__ */ jsx4("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
504
+ ] });
505
+ var LockIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsxs2("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: [
506
+ /* @__PURE__ */ jsx4("rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2" }),
507
+ /* @__PURE__ */ jsx4("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })
508
+ ] });
509
+ var FeedbackIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */ jsxs2("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", className, children: [
510
+ /* @__PURE__ */ jsx4("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }),
511
+ /* @__PURE__ */ jsx4("path", { d: "M8 12h.01" }),
512
+ /* @__PURE__ */ jsx4("path", { d: "M12 12h.01" }),
513
+ /* @__PURE__ */ jsx4("path", { d: "M16 12h.01" })
514
+ ] });
515
+
516
+ // src/components/TagBadge.tsx
517
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
518
+ function TagBadge({ tag, onRemove, size = "sm" }) {
519
+ const style = getTagStyle(tag);
520
+ const sizeClass = size === "sm" ? "px-1.5 py-px text-[8px]" : "px-2 py-0.5 text-[10px]";
521
+ return /* @__PURE__ */ jsxs3("span", { className: `inline-flex items-center gap-1 font-medium rounded border ${style.className} ${sizeClass}`, children: [
522
+ style.label,
523
+ onRemove && /* @__PURE__ */ jsx5("button", { onClick: onRemove, className: "opacity-50 hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsx5(XIcon, { size: size === "sm" ? 10 : 12 }) })
524
+ ] });
525
+ }
526
+
527
+ // src/components/UserAvatar.tsx
528
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
529
+ var SIZES = {
530
+ xs: { container: "w-5 h-5", text: "text-[8px]" },
531
+ sm: { container: "w-6 h-6", text: "text-[9px]" },
532
+ md: { container: "w-7 h-7", text: "text-[10px]" }
533
+ };
534
+ function UserAvatar({ name, size = "xs", showTooltip = false, className = "" }) {
535
+ const s = SIZES[size];
536
+ const initials = getInitials(name || "?");
537
+ return /* @__PURE__ */ jsxs4("div", { className: `relative ${showTooltip ? "group/avatar" : ""} ${className}`, children: [
538
+ /* @__PURE__ */ jsx6("div", { className: `${s.container} rounded-full bg-[#FF5E00] flex items-center justify-center`, children: /* @__PURE__ */ jsx6("span", { className: `${s.text} font-medium text-white leading-none`, children: initials }) }),
539
+ showTooltip && /* @__PURE__ */ jsx6("div", { className: "absolute bottom-full right-0 mb-1.5 px-2 py-1 text-[10px] font-medium text-white bg-neutral-800 rounded whitespace-nowrap opacity-0 pointer-events-none group-hover/avatar:opacity-100 transition-opacity duration-75", children: name })
540
+ ] });
541
+ }
542
+
543
+ // src/components/TaskCard.tsx
544
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
545
+ var TaskCard = memo(function TaskCard2({ task, index, onClick, onShare, copied }) {
546
+ return /* @__PURE__ */ jsx7(Draggable, { draggableId: task.id, index, children: (provided, snapshot) => /* @__PURE__ */ jsxs5(
547
+ "div",
548
+ {
549
+ ref: provided.innerRef,
550
+ ...provided.draggableProps,
551
+ ...provided.dragHandleProps,
552
+ onClick,
553
+ className: `relative bg-white rounded-lg border p-3.5 cursor-pointer transition-all hover:shadow-md group/card ${snapshot.isDragging ? "shadow-lg ring-2 ring-[#FF5E00]/20" : ""} border-neutral-200`,
554
+ children: [
555
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-start justify-between gap-2.5", children: [
556
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-start gap-1.5 flex-1 min-w-0", children: [
557
+ task.has_unread && /* @__PURE__ */ jsx7("span", { className: "relative group/unread mt-[5px] w-1.5 h-1.5 rounded-full bg-[#FF5E00] shrink-0 cursor-default", children: /* @__PURE__ */ jsx7("span", { className: "absolute left-1/2 -translate-x-1/2 bottom-full mb-1.5 px-2 py-1 text-[10px] font-medium text-white bg-neutral-800 rounded whitespace-nowrap opacity-0 pointer-events-none group-hover/unread:opacity-100 transition-opacity duration-75", children: "New activity" }) }),
558
+ /* @__PURE__ */ jsx7("h4", { className: "text-[13px] font-medium text-neutral-900 leading-snug line-clamp-2", children: task.title })
559
+ ] }),
560
+ /* @__PURE__ */ jsx7(PriorityBadge, { priority: task.priority })
561
+ ] }),
562
+ task.tags && task.tags.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "mt-1.5 flex flex-wrap gap-1", children: [
563
+ task.tags.slice(0, 3).map((tag) => /* @__PURE__ */ jsx7(TagBadge, { tag }, tag)),
564
+ task.tags.length > 3 && /* @__PURE__ */ jsxs5("span", { className: "text-[8px] text-neutral-400 px-1 py-px", children: [
565
+ "+",
566
+ task.tags.length - 3
567
+ ] })
568
+ ] }),
569
+ hasDescription(task.description) && /* @__PURE__ */ jsx7("p", { className: "mt-2 text-[11px] text-neutral-400 leading-relaxed line-clamp-2", children: getDescriptionPreview(task.description) }),
570
+ /* @__PURE__ */ jsxs5("div", { className: "mt-3 flex items-center justify-between pt-2.5 border-t border-neutral-100", children: [
571
+ /* @__PURE__ */ jsx7("span", { className: "text-[10px] text-neutral-400", children: formatDate(task.created_at) }),
572
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1.5", children: [
573
+ /* @__PURE__ */ jsx7(
574
+ "button",
575
+ {
576
+ onClick: onShare,
577
+ className: "opacity-0 group-hover/card:opacity-100 transition-opacity p-0.5 rounded text-neutral-300 hover:text-neutral-500 cursor-pointer",
578
+ title: copied ? "Link copied!" : "Copy link",
579
+ children: copied ? /* @__PURE__ */ jsx7(CheckIcon, { size: 12 }) : /* @__PURE__ */ jsx7(LinkIcon, { size: 12 })
580
+ }
581
+ ),
582
+ task.comment_count > 0 && /* @__PURE__ */ jsxs5("span", { className: "flex items-center gap-0.5 text-[10px] text-neutral-400", children: [
583
+ /* @__PURE__ */ jsx7(MessageSquareIcon, { size: 12 }),
584
+ task.comment_count
585
+ ] }),
586
+ task.created_by_name && /* @__PURE__ */ jsx7(UserAvatar, { name: task.created_by_name, size: "xs", showTooltip: true })
587
+ ] })
588
+ ] })
589
+ ]
590
+ }
591
+ ) });
592
+ });
593
+
594
+ // src/components/KanbanColumn.tsx
595
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
596
+ function LoadMoreSentinel({ loading, onLoadMore, remaining }) {
597
+ const sentinelRef = useRef3(null);
598
+ useEffect2(() => {
599
+ const el = sentinelRef.current;
600
+ if (!el) return;
601
+ const observer = new IntersectionObserver(
602
+ ([entry]) => {
603
+ if (entry.isIntersecting && !loading) onLoadMore();
604
+ },
605
+ { threshold: 0.1 }
606
+ );
607
+ observer.observe(el);
608
+ return () => observer.disconnect();
609
+ }, [loading, onLoadMore]);
610
+ const skeletonCount = loading ? Math.min(remaining, 10) : 0;
611
+ return /* @__PURE__ */ jsx8("div", { ref: sentinelRef, className: "space-y-2 pt-2", children: Array.from({ length: skeletonCount }).map((_, i) => /* @__PURE__ */ jsx8(SkeletonCard, {}, i)) });
612
+ }
613
+ function KanbanColumn({
614
+ column,
615
+ tasks,
616
+ totalCount,
617
+ unreadCount,
618
+ loadingMore,
619
+ onAddTask,
620
+ onTaskClick,
621
+ onTaskShare,
622
+ copiedTaskId,
623
+ onLoadMore
624
+ }) {
625
+ return /* @__PURE__ */ jsxs6("div", { className: "w-[280px] flex flex-col shrink-0 h-full", children: [
626
+ /* @__PURE__ */ jsxs6("div", { className: "mb-3 px-1 shrink-0", children: [
627
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
628
+ /* @__PURE__ */ jsx8("span", { className: `w-2 h-2 rounded-full ${column.color}` }),
629
+ /* @__PURE__ */ jsx8("h3", { className: "text-xs font-medium text-neutral-700 uppercase tracking-wide", children: column.label }),
630
+ unreadCount > 0 && /* @__PURE__ */ jsxs6("span", { className: "relative group/unread-col w-4 h-4 rounded-full bg-[#FF5E00] text-white text-[9px] font-semibold flex items-center justify-center cursor-default", children: [
631
+ unreadCount,
632
+ /* @__PURE__ */ jsxs6("span", { className: "absolute left-1/2 -translate-x-1/2 top-full mt-1.5 px-2 py-1 text-[10px] font-medium text-white bg-neutral-800 rounded whitespace-nowrap opacity-0 pointer-events-none group-hover/unread-col:opacity-100 transition-opacity duration-75 z-10", children: [
633
+ unreadCount,
634
+ " unread ",
635
+ unreadCount === 1 ? "task" : "tasks"
636
+ ] })
637
+ ] }),
638
+ /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-neutral-400 ml-auto", children: totalCount }),
639
+ /* @__PURE__ */ jsx8(
640
+ "button",
641
+ {
642
+ onClick: onAddTask,
643
+ className: "ml-1 p-1 rounded-md text-neutral-400 hover:text-[#FF5E00] hover:bg-[#FF5E00]/10 transition-colors",
644
+ "aria-label": `Add task to ${column.label}`,
645
+ children: /* @__PURE__ */ jsx8(PlusIcon, { size: 16 })
646
+ }
647
+ )
648
+ ] }),
649
+ /* @__PURE__ */ jsx8("p", { className: "text-[10px] text-neutral-400 mt-0.5 pl-4", children: column.description })
650
+ ] }),
651
+ /* @__PURE__ */ jsx8(Droppable, { droppableId: column.key, children: (provided, snapshot) => /* @__PURE__ */ jsxs6(
652
+ "div",
653
+ {
654
+ ref: provided.innerRef,
655
+ ...provided.droppableProps,
656
+ className: `flex-1 rounded-xl p-2 min-h-[120px] overflow-y-auto transition-colors ${snapshot.isDraggingOver ? "bg-[#FF5E00]/5 ring-1 ring-[#FF5E00]/20" : "bg-neutral-100/50"}`,
657
+ children: [
658
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
659
+ tasks.map((task, index) => /* @__PURE__ */ jsx8(
660
+ TaskCard,
661
+ {
662
+ task,
663
+ index,
664
+ onClick: () => onTaskClick(task),
665
+ onShare: (e) => {
666
+ e.stopPropagation();
667
+ onTaskShare(task.id, e);
668
+ },
669
+ copied: copiedTaskId === task.id
670
+ },
671
+ task.id
672
+ )),
673
+ provided.placeholder
674
+ ] }),
675
+ tasks.length < totalCount && /* @__PURE__ */ jsx8(
676
+ LoadMoreSentinel,
677
+ {
678
+ loading: loadingMore,
679
+ onLoadMore,
680
+ remaining: totalCount - tasks.length
681
+ }
682
+ ),
683
+ tasks.length === 0 && /* @__PURE__ */ jsx8("div", { className: "flex items-center justify-center h-20 text-xs text-neutral-400", children: "No tasks" })
684
+ ]
685
+ }
686
+ ) })
687
+ ] });
688
+ }
689
+
690
+ // src/components/FilterBar.tsx
691
+ import { useState as useState3, useRef as useRef4, useEffect as useEffect3 } from "react";
692
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
693
+ function FilterBar({
694
+ projects,
695
+ selectedProject,
696
+ onSelectProject,
697
+ filterTags,
698
+ onSetFilterTags
699
+ }) {
700
+ const { tags: predefinedTags, features } = useTaskBoardContext();
701
+ const [filterExpanded, setFilterExpanded] = useState3(false);
702
+ const filterDropdownRef = useRef4(null);
703
+ useEffect3(() => {
704
+ if (!filterExpanded) return;
705
+ function handleClick(e) {
706
+ if (filterDropdownRef.current && !filterDropdownRef.current.contains(e.target)) {
707
+ setFilterExpanded(false);
708
+ }
709
+ }
710
+ document.addEventListener("mousedown", handleClick);
711
+ return () => document.removeEventListener("mousedown", handleClick);
712
+ }, [filterExpanded]);
713
+ const toggleTag = (value) => {
714
+ onSetFilterTags(
715
+ filterTags.includes(value) ? filterTags.filter((t) => t !== value) : [...filterTags, value]
716
+ );
717
+ };
718
+ return /* @__PURE__ */ jsxs7("div", { className: "mb-4 shrink-0 flex items-start justify-between gap-3", children: [
719
+ projects.length > 1 && /* @__PURE__ */ jsx9("div", { className: "flex gap-2 flex-nowrap overflow-x-auto eb-tb-no-scrollbar sm:flex-wrap sm:overflow-visible", children: projects.map((project) => /* @__PURE__ */ jsx9(
720
+ "button",
721
+ {
722
+ onClick: () => onSelectProject(project.slug),
723
+ className: `shrink-0 px-4 py-2 text-xs font-medium rounded-lg border transition-colors whitespace-nowrap ${selectedProject === project.slug ? "bg-[#FF5E00] text-white border-[#FF5E00]" : "bg-white text-neutral-600 border-neutral-200 hover:border-neutral-300 hover:text-neutral-900"}`,
724
+ children: project.name
725
+ },
726
+ project.slug
727
+ )) }),
728
+ projects.length === 1 && /* @__PURE__ */ jsx9("span", { className: "text-sm font-medium text-neutral-600", children: projects[0].name }),
729
+ features.filters && /* @__PURE__ */ jsx9("div", { className: "self-stretch w-px bg-neutral-200 shrink-0" }),
730
+ features.filters && /* @__PURE__ */ jsxs7("div", { className: "relative shrink-0", ref: filterDropdownRef, children: [
731
+ /* @__PURE__ */ jsxs7(
732
+ "button",
733
+ {
734
+ onClick: () => setFilterExpanded(!filterExpanded),
735
+ className: `flex items-center gap-1.5 px-3 py-2 text-xs font-medium rounded-lg border transition-colors ${filterTags.length > 0 ? "bg-[#FF5E00]/5 border-[#FF5E00]/30 text-[#FF5E00]" : "bg-white text-neutral-500 border-neutral-200 hover:border-neutral-300 hover:text-neutral-700"}`,
736
+ children: [
737
+ /* @__PURE__ */ jsx9(FilterIcon, { size: 14 }),
738
+ "Filter",
739
+ filterTags.length > 0 && /* @__PURE__ */ jsx9("span", { className: "min-w-[16px] h-4 flex items-center justify-center rounded-full bg-[#FF5E00] text-white text-[9px] font-bold px-1", children: filterTags.length })
740
+ ]
741
+ }
742
+ ),
743
+ filterExpanded && /* @__PURE__ */ jsxs7("div", { className: "absolute right-0 top-full mt-1.5 w-64 bg-white border border-neutral-200 rounded-lg shadow-lg z-20 animate-in fade-in zoom-in-95 duration-75", children: [
744
+ /* @__PURE__ */ jsxs7("div", { className: "px-3 py-2.5 border-b border-neutral-100 flex items-center justify-between", children: [
745
+ /* @__PURE__ */ jsx9("span", { className: "text-xs font-medium text-neutral-700", children: "Filter by Tags" }),
746
+ filterTags.length > 0 && /* @__PURE__ */ jsx9(
747
+ "button",
748
+ {
749
+ onClick: () => onSetFilterTags([]),
750
+ className: "text-[10px] font-medium text-[#FF5E00] hover:text-[#E05200] transition-colors",
751
+ children: "Clear all"
752
+ }
753
+ )
754
+ ] }),
755
+ /* @__PURE__ */ jsxs7("div", { className: "py-1", children: [
756
+ predefinedTags.map((tag) => {
757
+ const isActive = filterTags.includes(tag.value);
758
+ return /* @__PURE__ */ jsxs7(
759
+ "button",
760
+ {
761
+ onClick: () => toggleTag(tag.value),
762
+ className: `w-full flex items-center justify-between px-3 py-2 text-xs hover:bg-neutral-50 transition-colors ${isActive ? "bg-neutral-50" : ""}`,
763
+ children: [
764
+ /* @__PURE__ */ jsx9("span", { className: `inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded border ${tag.className}`, children: tag.label }),
765
+ isActive && /* @__PURE__ */ jsx9(CheckIcon, { size: 13, strokeWidth: 2.5, className: "text-[#FF5E00]" })
766
+ ]
767
+ },
768
+ tag.value
769
+ );
770
+ }),
771
+ /* @__PURE__ */ jsxs7(
772
+ "button",
773
+ {
774
+ onClick: () => toggleTag("__other__"),
775
+ className: `w-full flex items-center justify-between px-3 py-2 text-xs hover:bg-neutral-50 transition-colors border-t border-neutral-100 ${filterTags.includes("__other__") ? "bg-neutral-50" : ""}`,
776
+ children: [
777
+ /* @__PURE__ */ jsx9("span", { className: "inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded border bg-neutral-100 text-neutral-500 border-neutral-200", children: "Other" }),
778
+ filterTags.includes("__other__") && /* @__PURE__ */ jsx9(CheckIcon, { size: 13, strokeWidth: 2.5, className: "text-[#FF5E00]" })
779
+ ]
780
+ }
781
+ )
782
+ ] })
783
+ ] })
784
+ ] })
785
+ ] });
786
+ }
787
+
788
+ // src/components/NotificationBell.tsx
789
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef5 } from "react";
790
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
791
+ function NotificationBell({ onOpenTask }) {
792
+ const { service, config } = useTaskBoardContext();
793
+ const [open, setOpen] = useState4(false);
794
+ const [notifications, setNotifications] = useState4([]);
795
+ const [unreadCount, setUnreadCount] = useState4(0);
796
+ const [loading, setLoading] = useState4(false);
797
+ const dropdownRef = useRef5(null);
798
+ const pollRef = useRef5(null);
799
+ const fetchCount = useCallback4(async () => {
800
+ try {
801
+ const count = await service.getNotificationCount();
802
+ setUnreadCount(count);
803
+ } catch (err) {
804
+ config.onError?.(err instanceof Error ? err : new Error(String(err)));
805
+ }
806
+ }, [service, config]);
807
+ useEffect4(() => {
808
+ fetchCount();
809
+ pollRef.current = setInterval(fetchCount, NOTIFICATION_POLL_INTERVAL);
810
+ return () => {
811
+ if (pollRef.current) clearInterval(pollRef.current);
812
+ };
813
+ }, [fetchCount]);
814
+ const fetchNotifications = async () => {
815
+ setLoading(true);
816
+ try {
817
+ const data = await service.listNotifications();
818
+ setNotifications(data);
819
+ } catch (err) {
820
+ config.onError?.(err instanceof Error ? err : new Error(String(err)));
821
+ } finally {
822
+ setLoading(false);
823
+ }
824
+ };
825
+ const toggleOpen = () => {
826
+ if (!open) fetchNotifications();
827
+ setOpen(!open);
828
+ };
829
+ useEffect4(() => {
830
+ if (!open) return;
831
+ function handleClick2(e) {
832
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) setOpen(false);
833
+ }
834
+ document.addEventListener("mousedown", handleClick2);
835
+ return () => document.removeEventListener("mousedown", handleClick2);
836
+ }, [open]);
837
+ const markAsRead = async (id) => {
838
+ try {
839
+ await service.markNotificationRead(id);
840
+ setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, read: true } : n));
841
+ setUnreadCount((c) => Math.max(0, c - 1));
842
+ } catch (err) {
843
+ config.onError?.(err instanceof Error ? err : new Error(String(err)));
844
+ }
845
+ };
846
+ const markAllRead = async () => {
847
+ try {
848
+ await service.markAllNotificationsRead();
849
+ setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
850
+ setUnreadCount(0);
851
+ } catch (err) {
852
+ config.onError?.(err instanceof Error ? err : new Error(String(err)));
853
+ }
854
+ };
855
+ const handleClick = (n) => {
856
+ if (!n.read) markAsRead(n.id);
857
+ setOpen(false);
858
+ onOpenTask(n.task_id, n.project_slug);
859
+ };
860
+ return /* @__PURE__ */ jsxs8("div", { ref: dropdownRef, className: "relative", children: [
861
+ /* @__PURE__ */ jsxs8(
862
+ "button",
863
+ {
864
+ onClick: toggleOpen,
865
+ className: "relative flex items-center justify-center w-9 h-9 rounded-lg border border-neutral-200 hover:border-neutral-300 hover:bg-neutral-50 transition-colors",
866
+ title: "Notifications",
867
+ children: [
868
+ /* @__PURE__ */ jsx10(BellIcon, { size: 16, className: "text-neutral-600" }),
869
+ unreadCount > 0 && /* @__PURE__ */ jsx10("span", { className: "absolute -top-1 -right-1 min-w-[16px] h-4 px-1 flex items-center justify-center rounded-full bg-[#FF5E00] text-white text-[9px] font-bold", children: unreadCount > 99 ? "99+" : unreadCount })
870
+ ]
871
+ }
872
+ ),
873
+ open && /* @__PURE__ */ jsxs8("div", { className: "absolute right-0 mt-2 w-[360px] bg-white rounded-xl shadow-xl border border-neutral-200 z-50 overflow-hidden", children: [
874
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-4 py-3 border-b border-neutral-100", children: [
875
+ /* @__PURE__ */ jsx10("h3", { className: "text-sm font-medium text-neutral-900", children: "Notifications" }),
876
+ unreadCount > 0 && /* @__PURE__ */ jsx10("button", { onClick: markAllRead, className: "text-[11px] font-medium text-[#FF5E00] hover:text-[#E05200] transition-colors", children: "Mark all as read" })
877
+ ] }),
878
+ /* @__PURE__ */ jsx10("div", { className: "max-h-[400px] overflow-y-auto", children: loading ? /* @__PURE__ */ jsx10("div", { className: "px-4 py-6 space-y-3", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxs8("div", { className: "flex gap-3 animate-pulse", children: [
879
+ /* @__PURE__ */ jsx10("div", { className: "w-8 h-8 rounded-full bg-neutral-200 shrink-0" }),
880
+ /* @__PURE__ */ jsxs8("div", { className: "flex-1 space-y-1.5", children: [
881
+ /* @__PURE__ */ jsx10(SkeletonPulse, { className: "h-3 w-3/4" }),
882
+ /* @__PURE__ */ jsx10(SkeletonPulse, { className: "h-2.5 w-full" }),
883
+ /* @__PURE__ */ jsx10(SkeletonPulse, { className: "h-2 w-16" })
884
+ ] })
885
+ ] }, i)) }) : notifications.length === 0 ? /* @__PURE__ */ jsxs8("div", { className: "px-4 py-10 text-center", children: [
886
+ /* @__PURE__ */ jsx10(BellIcon, { size: 32, className: "text-neutral-300 mx-auto mb-2" }),
887
+ /* @__PURE__ */ jsx10("p", { className: "text-xs text-neutral-400", children: "No notifications yet" })
888
+ ] }) : notifications.map((n) => /* @__PURE__ */ jsxs8(
889
+ "button",
890
+ {
891
+ onClick: () => handleClick(n),
892
+ className: `w-full flex items-start gap-3 px-4 py-3 text-left hover:bg-neutral-50 transition-colors border-b border-neutral-50 last:border-b-0 ${!n.read ? "bg-[#FF5E00]/[0.03]" : ""}`,
893
+ children: [
894
+ /* @__PURE__ */ jsx10("div", { className: "w-8 h-8 rounded-full bg-[#FF5E00] text-white text-[10px] font-semibold flex items-center justify-center shrink-0 mt-0.5", children: getInitials(n.actor_name) }),
895
+ /* @__PURE__ */ jsxs8("div", { className: "flex-1 min-w-0", children: [
896
+ /* @__PURE__ */ jsxs8("p", { className: "text-xs text-neutral-700 leading-relaxed", children: [
897
+ /* @__PURE__ */ jsx10("span", { className: "font-semibold text-neutral-900", children: n.actor_name }),
898
+ " mentioned you in ",
899
+ n.context === "description" ? "the description of " : "a comment on ",
900
+ /* @__PURE__ */ jsx10("span", { className: "font-medium text-neutral-800", children: n.task_title })
901
+ ] }),
902
+ n.snippet && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-neutral-400 mt-0.5 truncate", children: n.snippet }),
903
+ /* @__PURE__ */ jsx10("p", { className: "text-[10px] text-neutral-400 mt-1", children: formatDateTime(n.created_at) })
904
+ ] }),
905
+ !n.read && /* @__PURE__ */ jsx10("span", { className: "w-2 h-2 rounded-full bg-[#FF5E00] shrink-0 mt-2" })
906
+ ]
907
+ },
908
+ n.id
909
+ )) })
910
+ ] })
911
+ ] });
912
+ }
913
+
914
+ // src/components/CreateTaskModal.tsx
915
+ import { useState as useState5 } from "react";
916
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
917
+ function CreateTaskModal({
918
+ projectSlug,
919
+ defaultStatus = "backlog",
920
+ onClose,
921
+ onCreate
922
+ }) {
923
+ const { columns, priorities, tags: predefinedTags, service, config } = useTaskBoardContext();
924
+ const [title, setTitle] = useState5("");
925
+ const [description, setDescription] = useState5({ ...EMPTY_DESCRIPTION });
926
+ const [priority, setPriority] = useState5("medium");
927
+ const [taskStatus, setTaskStatus] = useState5(defaultStatus);
928
+ const [selectedTags, setSelectedTags] = useState5([]);
929
+ const [customTag, setCustomTag] = useState5("");
930
+ const [loading, setLoading] = useState5(false);
931
+ const [error, setError] = useState5("");
932
+ const [showColumnDropdown, setShowColumnDropdown] = useState5(false);
933
+ const [showPriorityDropdown, setShowPriorityDropdown] = useState5(false);
934
+ const toggleTag = (tag) => {
935
+ setSelectedTags(
936
+ (prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
937
+ );
938
+ };
939
+ const addCustomTag = () => {
940
+ const tag = customTag.trim().toLowerCase().replace(/\s+/g, "-");
941
+ if (tag && !selectedTags.includes(tag)) {
942
+ setSelectedTags((prev) => [...prev, tag]);
943
+ }
944
+ setCustomTag("");
945
+ };
946
+ const handleSubmit = async (e) => {
947
+ e.preventDefault();
948
+ if (!title.trim()) return;
949
+ setLoading(true);
950
+ setError("");
951
+ try {
952
+ const task = await service.createTask({
953
+ project_slug: projectSlug,
954
+ title: title.trim(),
955
+ description,
956
+ priority,
957
+ status: taskStatus,
958
+ tags: selectedTags
959
+ });
960
+ config.onTaskCreate?.(task);
961
+ onCreate();
962
+ onClose();
963
+ } catch (err) {
964
+ const apiErr = err;
965
+ setError(apiErr?.response?.data?.detail || apiErr?.message || "Failed to create task");
966
+ } finally {
967
+ setLoading(false);
968
+ }
969
+ };
970
+ const priorityStyle = getPriorityStyle(priority);
971
+ const statusCol = columns.find((c) => c.key === taskStatus);
972
+ return /* @__PURE__ */ jsx11(
973
+ "div",
974
+ {
975
+ className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/30 backdrop-blur-sm eb-tb-animate-fade-in",
976
+ onClick: (e) => e.target === e.currentTarget && onClose(),
977
+ children: /* @__PURE__ */ jsxs9("div", { className: "bg-white rounded-2xl shadow-2xl border border-neutral-200 w-[80vw] max-w-[1100px] h-[85vh] flex flex-col eb-tb-animate-zoom-in", children: [
978
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between px-6 py-4 border-b border-neutral-100 shrink-0", children: [
979
+ /* @__PURE__ */ jsx11("h2", { className: "text-lg font-semibold text-neutral-900", children: "New Task" }),
980
+ /* @__PURE__ */ jsx11(
981
+ "button",
982
+ {
983
+ onClick: onClose,
984
+ className: "p-1 rounded-md text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100 transition-colors",
985
+ children: /* @__PURE__ */ jsx11(XIcon, { size: 20 })
986
+ }
987
+ )
988
+ ] }),
989
+ /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, className: "flex-1 flex flex-col min-h-0", children: [
990
+ error && /* @__PURE__ */ jsx11("div", { className: "mx-6 mt-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm", children: error }),
991
+ /* @__PURE__ */ jsxs9("div", { className: "flex-1 flex min-h-0 overflow-hidden", children: [
992
+ /* @__PURE__ */ jsx11("div", { className: "flex-1 overflow-y-auto px-6 py-5 border-r border-neutral-100", children: /* @__PURE__ */ jsxs9("div", { className: "max-w-2xl", children: [
993
+ /* @__PURE__ */ jsxs9("div", { className: "pb-4", children: [
994
+ /* @__PURE__ */ jsx11("label", { className: "text-xs font-medium text-neutral-700 mb-1.5 block", children: "Title" }),
995
+ /* @__PURE__ */ jsx11(
996
+ "input",
997
+ {
998
+ type: "text",
999
+ required: true,
1000
+ value: title,
1001
+ onChange: (e) => setTitle(e.target.value),
1002
+ className: "w-full px-3 py-2.5 border border-neutral-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#FF5E00]/10 focus:border-[#FF5E00]/50",
1003
+ placeholder: "What needs to be done?",
1004
+ autoFocus: true
1005
+ }
1006
+ )
1007
+ ] }),
1008
+ /* @__PURE__ */ jsx11("div", { className: "border-t border-neutral-100 pt-4 pb-1", children: /* @__PURE__ */ jsx11("span", { className: "text-[10px] font-semibold text-neutral-400 uppercase tracking-wider", children: "Description" }) }),
1009
+ DESCRIPTION_SECTIONS.map((section, idx) => /* @__PURE__ */ jsxs9("div", { className: `py-3 ${idx < DESCRIPTION_SECTIONS.length - 1 ? "border-b border-dashed border-neutral-100" : ""}`, children: [
1010
+ /* @__PURE__ */ jsxs9("label", { className: "text-xs font-medium text-neutral-700 mb-1.5 block", children: [
1011
+ section.label,
1012
+ " ",
1013
+ /* @__PURE__ */ jsx11("span", { className: "text-neutral-400 font-normal", children: "(optional)" })
1014
+ ] }),
1015
+ /* @__PURE__ */ jsx11(
1016
+ "textarea",
1017
+ {
1018
+ value: description[section.key],
1019
+ onChange: (e) => setDescription((prev) => ({ ...prev, [section.key]: e.target.value })),
1020
+ rows: 3,
1021
+ className: "w-full px-3 py-2.5 border border-neutral-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#FF5E00]/10 focus:border-[#FF5E00]/50 resize-y",
1022
+ placeholder: `Add ${section.label.toLowerCase()}...`
1023
+ }
1024
+ )
1025
+ ] }, section.key))
1026
+ ] }) }),
1027
+ /* @__PURE__ */ jsxs9("div", { className: "w-[340px] shrink-0 overflow-y-auto px-5 py-5 bg-neutral-50/30", children: [
1028
+ /* @__PURE__ */ jsxs9("div", { className: "grid grid-cols-2 gap-4 mb-5", children: [
1029
+ /* @__PURE__ */ jsxs9("div", { children: [
1030
+ /* @__PURE__ */ jsx11("label", { className: "text-[11px] font-semibold text-neutral-500 uppercase tracking-wide mb-2 block", children: "Column" }),
1031
+ /* @__PURE__ */ jsxs9("div", { className: "relative", children: [
1032
+ /* @__PURE__ */ jsxs9(
1033
+ "button",
1034
+ {
1035
+ type: "button",
1036
+ onClick: () => setShowColumnDropdown(!showColumnDropdown),
1037
+ className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-xs font-medium rounded-lg border border-neutral-200 bg-white hover:border-neutral-300 transition-colors",
1038
+ children: [
1039
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
1040
+ /* @__PURE__ */ jsx11("span", { className: `w-2 h-2 rounded-full ${statusCol?.color ?? "bg-neutral-400"}` }),
1041
+ statusCol?.label ?? taskStatus
1042
+ ] }),
1043
+ /* @__PURE__ */ jsx11(ChevronDownIcon, { size: 12, className: "text-neutral-400" })
1044
+ ]
1045
+ }
1046
+ ),
1047
+ showColumnDropdown && /* @__PURE__ */ jsx11("div", { className: "absolute top-full mt-1 left-0 right-0 bg-white border border-neutral-200 rounded-lg shadow-lg z-10 py-1 eb-tb-animate-zoom-in", children: columns.map((col) => /* @__PURE__ */ jsxs9(
1048
+ "button",
1049
+ {
1050
+ type: "button",
1051
+ onClick: () => {
1052
+ setTaskStatus(col.key);
1053
+ setShowColumnDropdown(false);
1054
+ },
1055
+ className: `w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-neutral-50 text-left ${taskStatus === col.key ? "font-medium bg-neutral-50" : ""}`,
1056
+ children: [
1057
+ /* @__PURE__ */ jsx11("span", { className: `w-2 h-2 rounded-full ${col.color}` }),
1058
+ col.label
1059
+ ]
1060
+ },
1061
+ col.key
1062
+ )) })
1063
+ ] })
1064
+ ] }),
1065
+ /* @__PURE__ */ jsxs9("div", { children: [
1066
+ /* @__PURE__ */ jsx11("label", { className: "text-[11px] font-semibold text-neutral-500 uppercase tracking-wide mb-2 block", children: "Priority" }),
1067
+ /* @__PURE__ */ jsxs9("div", { className: "relative", children: [
1068
+ /* @__PURE__ */ jsxs9(
1069
+ "button",
1070
+ {
1071
+ type: "button",
1072
+ onClick: () => setShowPriorityDropdown(!showPriorityDropdown),
1073
+ className: `w-full flex items-center justify-between gap-2 px-3 py-2 text-xs font-medium rounded-lg border transition-colors ${priorityStyle.className}`,
1074
+ children: [
1075
+ priorityStyle.label,
1076
+ /* @__PURE__ */ jsx11(ChevronDownIcon, { size: 12, className: "opacity-50" })
1077
+ ]
1078
+ }
1079
+ ),
1080
+ showPriorityDropdown && /* @__PURE__ */ jsx11("div", { className: "absolute top-full mt-1 left-0 right-0 bg-white border border-neutral-200 rounded-lg shadow-lg z-10 py-1 eb-tb-animate-zoom-in", children: priorities.map((p) => /* @__PURE__ */ jsx11(
1081
+ "button",
1082
+ {
1083
+ type: "button",
1084
+ onClick: () => {
1085
+ setPriority(p.value);
1086
+ setShowPriorityDropdown(false);
1087
+ },
1088
+ className: `w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-neutral-50 text-left ${priority === p.value ? "bg-neutral-50" : ""}`,
1089
+ children: /* @__PURE__ */ jsx11("span", { className: `inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded border ${p.className}`, children: p.label })
1090
+ },
1091
+ p.value
1092
+ )) })
1093
+ ] })
1094
+ ] })
1095
+ ] }),
1096
+ /* @__PURE__ */ jsxs9("div", { children: [
1097
+ /* @__PURE__ */ jsx11("label", { className: "text-[11px] font-semibold text-neutral-500 uppercase tracking-wide mb-2 block", children: "Tags" }),
1098
+ /* @__PURE__ */ jsx11("div", { className: "space-y-1", children: predefinedTags.map((tag) => {
1099
+ const isSelected = selectedTags.includes(tag.value);
1100
+ return /* @__PURE__ */ jsxs9(
1101
+ "button",
1102
+ {
1103
+ type: "button",
1104
+ onClick: () => toggleTag(tag.value),
1105
+ className: `w-full flex items-center justify-between px-3 py-2 text-xs rounded-lg border transition-colors text-left ${isSelected ? "bg-[#FF5E00]/5 border-[#FF5E00]/30 text-neutral-800" : "bg-white text-neutral-600 border-neutral-200 hover:border-neutral-300"}`,
1106
+ children: [
1107
+ /* @__PURE__ */ jsx11("span", { className: `inline-flex items-center px-1.5 py-px text-[10px] font-medium rounded border ${tag.className}`, children: tag.label }),
1108
+ isSelected && /* @__PURE__ */ jsx11(CheckIcon, { size: 13, strokeWidth: 2.5, className: "text-[#FF5E00]" })
1109
+ ]
1110
+ },
1111
+ tag.value
1112
+ );
1113
+ }) }),
1114
+ /* @__PURE__ */ jsxs9("div", { className: "mt-2.5 flex gap-1.5", children: [
1115
+ /* @__PURE__ */ jsx11(
1116
+ "input",
1117
+ {
1118
+ type: "text",
1119
+ value: customTag,
1120
+ onChange: (e) => setCustomTag(e.target.value),
1121
+ onKeyDown: (e) => {
1122
+ if (e.key === "Enter") {
1123
+ e.preventDefault();
1124
+ addCustomTag();
1125
+ }
1126
+ },
1127
+ className: "flex-1 px-2.5 py-1.5 border border-neutral-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-[#FF5E00]/10 focus:border-[#FF5E00]/50 bg-white",
1128
+ placeholder: "Custom tag..."
1129
+ }
1130
+ ),
1131
+ /* @__PURE__ */ jsx11(
1132
+ "button",
1133
+ {
1134
+ type: "button",
1135
+ onClick: addCustomTag,
1136
+ disabled: !customTag.trim(),
1137
+ className: "px-2.5 py-1.5 text-xs font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded-lg transition-colors disabled:opacity-40",
1138
+ children: "Add"
1139
+ }
1140
+ )
1141
+ ] }),
1142
+ selectedTags.filter((t) => !predefinedTags.some((p) => p.value === t)).length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex gap-1.5 flex-wrap mt-2", children: selectedTags.filter((t) => !predefinedTags.some((p) => p.value === t)).map((tag) => /* @__PURE__ */ jsxs9(
1143
+ "span",
1144
+ {
1145
+ className: "inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-neutral-100 text-neutral-500 border border-neutral-200 rounded",
1146
+ children: [
1147
+ tag,
1148
+ /* @__PURE__ */ jsx11(
1149
+ "button",
1150
+ {
1151
+ type: "button",
1152
+ onClick: () => setSelectedTags((prev) => prev.filter((t) => t !== tag)),
1153
+ className: "text-neutral-400 hover:text-neutral-600",
1154
+ children: /* @__PURE__ */ jsx11(XIcon, { size: 12 })
1155
+ }
1156
+ )
1157
+ ]
1158
+ },
1159
+ tag
1160
+ )) })
1161
+ ] })
1162
+ ] })
1163
+ ] }),
1164
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-end gap-3 px-6 py-4 border-t border-neutral-100 shrink-0 bg-white", children: [
1165
+ /* @__PURE__ */ jsx11(
1166
+ "button",
1167
+ {
1168
+ type: "button",
1169
+ onClick: onClose,
1170
+ className: "px-4 py-2 text-sm text-neutral-600 hover:text-neutral-900 transition-colors",
1171
+ children: "Cancel"
1172
+ }
1173
+ ),
1174
+ /* @__PURE__ */ jsx11(
1175
+ "button",
1176
+ {
1177
+ type: "submit",
1178
+ disabled: loading,
1179
+ className: "px-6 py-2 text-sm font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded-lg transition-colors disabled:opacity-60",
1180
+ children: loading ? "Creating..." : "Create Task"
1181
+ }
1182
+ )
1183
+ ] })
1184
+ ] })
1185
+ ] })
1186
+ }
1187
+ );
1188
+ }
1189
+
1190
+ // src/components/TaskDetailPanel.tsx
1191
+ import { useState as useState8, useEffect as useEffect7, useRef as useRef7 } from "react";
1192
+
1193
+ // src/hooks/useTaskDetail.ts
1194
+ import { useState as useState6, useEffect as useEffect5, useCallback as useCallback5 } from "react";
1195
+ function getErrorMessage(err) {
1196
+ const apiErr = err;
1197
+ return apiErr?.response?.data?.detail || apiErr?.message || "An error occurred";
1198
+ }
1199
+ function useTaskDetail(taskId) {
1200
+ const { service, config } = useTaskBoardContext();
1201
+ const [comments, setComments] = useState6([]);
1202
+ const [activity, setActivity] = useState6([]);
1203
+ const [commentsLoaded, setCommentsLoaded] = useState6(false);
1204
+ const [commentLoading, setCommentLoading] = useState6(false);
1205
+ const fetchDetail = useCallback5(async () => {
1206
+ try {
1207
+ const data = await service.getTask(taskId);
1208
+ setComments(data.comments || []);
1209
+ setActivity(data.activity || []);
1210
+ } catch {
1211
+ try {
1212
+ const comments2 = await service.listComments(taskId);
1213
+ setComments(comments2);
1214
+ } catch {
1215
+ }
1216
+ } finally {
1217
+ setCommentsLoaded(true);
1218
+ }
1219
+ }, [taskId, service]);
1220
+ useEffect5(() => {
1221
+ fetchDetail();
1222
+ }, [fetchDetail]);
1223
+ const addComment = async (content, isInternal = false) => {
1224
+ setCommentLoading(true);
1225
+ try {
1226
+ await service.addComment(taskId, { content, is_internal: isInternal });
1227
+ await fetchDetail();
1228
+ } catch (err) {
1229
+ const message = getErrorMessage(err);
1230
+ config.onError?.(err instanceof Error ? err : new Error(message));
1231
+ } finally {
1232
+ setCommentLoading(false);
1233
+ }
1234
+ };
1235
+ const editComment = async (commentId, content) => {
1236
+ try {
1237
+ await service.editComment(taskId, commentId, { content });
1238
+ await fetchDetail();
1239
+ } catch (err) {
1240
+ const msg = getErrorMessage(err);
1241
+ throw new Error(msg);
1242
+ }
1243
+ };
1244
+ const deleteComment = async (commentId) => {
1245
+ try {
1246
+ await service.deleteComment(taskId, commentId);
1247
+ await fetchDetail();
1248
+ } catch (err) {
1249
+ const msg = getErrorMessage(err);
1250
+ throw new Error(msg);
1251
+ }
1252
+ };
1253
+ const saveField = async (field, value) => {
1254
+ await service.updateTask(taskId, { [field]: value });
1255
+ };
1256
+ return {
1257
+ comments,
1258
+ activity,
1259
+ commentsLoaded,
1260
+ commentLoading,
1261
+ addComment,
1262
+ editComment,
1263
+ deleteComment,
1264
+ saveField,
1265
+ refreshDetail: fetchDetail
1266
+ };
1267
+ }
1268
+
1269
+ // src/components/MentionText.tsx
1270
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1271
+ var MENTION_RE = /(@\[.*?\]\(.*?\))/g;
1272
+ var MENTION_PARSE = /^@\[(.*?)\]\((.*?)\)$/;
1273
+ function MentionText({ text, className = "" }) {
1274
+ if (!text) return null;
1275
+ const parts = text.split(MENTION_RE);
1276
+ return /* @__PURE__ */ jsx12("span", { className, children: parts.map((part, i) => {
1277
+ const match = part.match(MENTION_PARSE);
1278
+ if (match) {
1279
+ return /* @__PURE__ */ jsxs10(
1280
+ "span",
1281
+ {
1282
+ className: "inline-flex items-center px-1.5 py-0.5 mx-0.5 bg-[#FF5E00]/10 text-[#FF5E00] rounded text-[11px] font-semibold cursor-default",
1283
+ title: `@${match[2]}`,
1284
+ children: [
1285
+ "@",
1286
+ match[1]
1287
+ ]
1288
+ },
1289
+ i
1290
+ );
1291
+ }
1292
+ return /* @__PURE__ */ jsx12("span", { children: part }, i);
1293
+ }) });
1294
+ }
1295
+ function toDisplayText(stored) {
1296
+ return stored.replace(/@\[(.*?)\]\(.*?\)/g, "@$1");
1297
+ }
1298
+ function toStoredText(display, mentionMap) {
1299
+ let result = display;
1300
+ mentionMap.forEach((username, name) => {
1301
+ result = result.replace(new RegExp(`@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"), `@[${name}](${username})`);
1302
+ });
1303
+ return result;
1304
+ }
1305
+
1306
+ // src/components/MentionTextarea.tsx
1307
+ import { useState as useState7, useEffect as useEffect6, useRef as useRef6 } from "react";
1308
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1309
+ function MentionTextarea({
1310
+ value,
1311
+ onChange,
1312
+ onKeyDown,
1313
+ placeholder = "",
1314
+ rows = 2,
1315
+ className = "",
1316
+ disabled = false
1317
+ }) {
1318
+ const { service, features } = useTaskBoardContext();
1319
+ const textareaRef = useRef6(null);
1320
+ const [mentionQuery, setMentionQuery] = useState7(null);
1321
+ const [mentionStart, setMentionStart] = useState7(0);
1322
+ const [mentionUsers, setMentionUsers] = useState7([]);
1323
+ const [selectedIndex, setSelectedIndex] = useState7(0);
1324
+ const fetchTimeoutRef = useRef6(null);
1325
+ const mentionMapRef = useRef6(/* @__PURE__ */ new Map());
1326
+ useEffect6(() => {
1327
+ const re = /@\[(.*?)\]\((.*?)\)/g;
1328
+ let match;
1329
+ while ((match = re.exec(value)) !== null) {
1330
+ mentionMapRef.current.set(match[1], match[2]);
1331
+ }
1332
+ }, []);
1333
+ useEffect6(() => {
1334
+ if (!features.mentions || mentionQuery === null) {
1335
+ setMentionUsers([]);
1336
+ return;
1337
+ }
1338
+ if (fetchTimeoutRef.current) clearTimeout(fetchTimeoutRef.current);
1339
+ fetchTimeoutRef.current = setTimeout(async () => {
1340
+ try {
1341
+ const users = await service.searchMentionUsers(mentionQuery);
1342
+ setMentionUsers(users);
1343
+ setSelectedIndex(0);
1344
+ } catch {
1345
+ setMentionUsers([]);
1346
+ }
1347
+ }, 150);
1348
+ return () => {
1349
+ if (fetchTimeoutRef.current) clearTimeout(fetchTimeoutRef.current);
1350
+ };
1351
+ }, [mentionQuery, service, features.mentions]);
1352
+ const displayValue = toDisplayText(value);
1353
+ const handleChange = (e) => {
1354
+ const newDisplay = e.target.value;
1355
+ const stored = toStoredText(newDisplay, mentionMapRef.current);
1356
+ onChange(stored);
1357
+ const cursorPos = e.target.selectionStart;
1358
+ const textBefore = newDisplay.slice(0, cursorPos);
1359
+ const atIndex = textBefore.lastIndexOf("@");
1360
+ if (atIndex >= 0) {
1361
+ const charBefore = atIndex > 0 ? textBefore[atIndex - 1] : " ";
1362
+ if (charBefore === " " || charBefore === "\n" || atIndex === 0) {
1363
+ const query = textBefore.slice(atIndex + 1);
1364
+ if (!query.includes(" ") || query.length <= 20) {
1365
+ setMentionQuery(query);
1366
+ setMentionStart(atIndex);
1367
+ return;
1368
+ }
1369
+ }
1370
+ }
1371
+ setMentionQuery(null);
1372
+ };
1373
+ const insertMention = (user) => {
1374
+ const display = toDisplayText(value);
1375
+ const before = display.slice(0, mentionStart);
1376
+ const after = display.slice(mentionStart + 1 + (mentionQuery?.length || 0));
1377
+ mentionMapRef.current.set(user.name, user.username);
1378
+ const newDisplay = before + `@${user.name} ` + after;
1379
+ const stored = toStoredText(newDisplay, mentionMapRef.current);
1380
+ onChange(stored);
1381
+ setMentionQuery(null);
1382
+ setTimeout(() => {
1383
+ if (textareaRef.current) {
1384
+ textareaRef.current.focus();
1385
+ const pos = before.length + user.name.length + 2;
1386
+ textareaRef.current.setSelectionRange(pos, pos);
1387
+ }
1388
+ }, 0);
1389
+ };
1390
+ const handleKeyDown = (e) => {
1391
+ if (mentionQuery !== null && mentionUsers.length > 0) {
1392
+ if (e.key === "ArrowDown") {
1393
+ e.preventDefault();
1394
+ setSelectedIndex((i) => Math.min(i + 1, mentionUsers.length - 1));
1395
+ return;
1396
+ }
1397
+ if (e.key === "ArrowUp") {
1398
+ e.preventDefault();
1399
+ setSelectedIndex((i) => Math.max(i - 1, 0));
1400
+ return;
1401
+ }
1402
+ if (e.key === "Enter" || e.key === "Tab") {
1403
+ e.preventDefault();
1404
+ insertMention(mentionUsers[selectedIndex]);
1405
+ return;
1406
+ }
1407
+ if (e.key === "Escape") {
1408
+ e.preventDefault();
1409
+ setMentionQuery(null);
1410
+ return;
1411
+ }
1412
+ }
1413
+ onKeyDown?.(e);
1414
+ };
1415
+ return /* @__PURE__ */ jsxs11("div", { className: "relative", children: [
1416
+ /* @__PURE__ */ jsx13(
1417
+ "textarea",
1418
+ {
1419
+ ref: textareaRef,
1420
+ value: displayValue,
1421
+ onChange: handleChange,
1422
+ onKeyDown: handleKeyDown,
1423
+ rows,
1424
+ className,
1425
+ placeholder,
1426
+ disabled
1427
+ }
1428
+ ),
1429
+ mentionQuery !== null && mentionUsers.length > 0 && /* @__PURE__ */ jsxs11("div", { className: "absolute bottom-full left-0 mb-1 w-64 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1 max-h-48 overflow-y-auto", children: [
1430
+ /* @__PURE__ */ jsx13("div", { className: "px-2.5 py-1.5 text-[10px] font-medium text-neutral-400 uppercase tracking-wide", children: "People" }),
1431
+ mentionUsers.map((user, i) => /* @__PURE__ */ jsxs11(
1432
+ "button",
1433
+ {
1434
+ onClick: () => insertMention(user),
1435
+ className: `w-full flex items-center gap-2.5 px-3 py-2 text-xs text-left hover:bg-neutral-50 ${i === selectedIndex ? "bg-neutral-50" : ""}`,
1436
+ children: [
1437
+ /* @__PURE__ */ jsx13("div", { className: "w-6 h-6 rounded-full bg-[#FF5E00] flex items-center justify-center shrink-0", children: /* @__PURE__ */ jsx13("span", { className: "text-[9px] font-medium text-white", children: getInitials(user.name) }) }),
1438
+ /* @__PURE__ */ jsxs11("div", { className: "min-w-0", children: [
1439
+ /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-neutral-800 truncate", children: user.name }),
1440
+ /* @__PURE__ */ jsx13("div", { className: "text-[10px] text-neutral-400 truncate", children: user.email })
1441
+ ] })
1442
+ ]
1443
+ },
1444
+ user.username
1445
+ ))
1446
+ ] })
1447
+ ] });
1448
+ }
1449
+
1450
+ // src/components/TaskDetailPanel.tsx
1451
+ import { Fragment, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1452
+ function TaskDetailPanel({ task, projectSlug, onClose, onUpdate }) {
1453
+ const { columns, priorities, tags: predefinedTags, service, config, user, features } = useTaskBoardContext();
1454
+ const detail = useTaskDetail(task.id);
1455
+ const { copiedTaskId, copyShareLink } = useShareLink();
1456
+ const [title, setTitle] = useState8(task.title);
1457
+ const [description, setDescription] = useState8(task.description || EMPTY_DESCRIPTION);
1458
+ const [priority, setPriority] = useState8(task.priority);
1459
+ const [taskStatus, setTaskStatus] = useState8(task.status);
1460
+ const [localTags, setLocalTags] = useState8(task.tags || []);
1461
+ const [pendingTags, setPendingTags] = useState8([]);
1462
+ const [saving, setSaving] = useState8(false);
1463
+ const [editing, setEditing] = useState8(false);
1464
+ const [newComment, setNewComment] = useState8("");
1465
+ const [isInternalComment, setIsInternalComment] = useState8(false);
1466
+ const [showPriorityDropdown, setShowPriorityDropdown] = useState8(false);
1467
+ const [showStatusDropdown, setShowStatusDropdown] = useState8(false);
1468
+ const [showTagDropdown, setShowTagDropdown] = useState8(false);
1469
+ const [showOtherTagInput, setShowOtherTagInput] = useState8(false);
1470
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState8(false);
1471
+ const [showDiscardConfirm, setShowDiscardConfirm] = useState8(false);
1472
+ const [commentToDelete, setCommentToDelete] = useState8(null);
1473
+ const [editingCommentId, setEditingCommentId] = useState8(null);
1474
+ const [editingCommentContent, setEditingCommentContent] = useState8("");
1475
+ const commentsEndRef = useRef7(null);
1476
+ const hasUnsavedChanges = editing && (title !== task.title || JSON.stringify(description) !== JSON.stringify(task.description));
1477
+ useEffect7(() => {
1478
+ if (commentsEndRef.current) {
1479
+ commentsEndRef.current.scrollIntoView({ behavior: "smooth" });
1480
+ }
1481
+ }, [detail.comments]);
1482
+ const saveField = async (field, value) => {
1483
+ setSaving(true);
1484
+ try {
1485
+ await detail.saveField(field, value);
1486
+ onUpdate();
1487
+ } finally {
1488
+ setSaving(false);
1489
+ }
1490
+ };
1491
+ const handleSaveEdit = async () => {
1492
+ setSaving(true);
1493
+ try {
1494
+ const updates = {};
1495
+ if (title.trim() && title !== task.title) updates.title = title.trim();
1496
+ if (JSON.stringify(description) !== JSON.stringify(task.description)) updates.description = description;
1497
+ if (Object.keys(updates).length > 0) {
1498
+ await service.updateTask(task.id, updates);
1499
+ onUpdate();
1500
+ }
1501
+ } finally {
1502
+ setSaving(false);
1503
+ setEditing(false);
1504
+ }
1505
+ };
1506
+ const handleCancelEdit = () => {
1507
+ if (hasUnsavedChanges) {
1508
+ setShowDiscardConfirm(true);
1509
+ } else {
1510
+ setEditing(false);
1511
+ }
1512
+ };
1513
+ const handleDiscardChanges = () => {
1514
+ setTitle(task.title);
1515
+ setDescription(task.description || EMPTY_DESCRIPTION);
1516
+ setEditing(false);
1517
+ setShowDiscardConfirm(false);
1518
+ };
1519
+ const handleClosePanel = () => {
1520
+ if (hasUnsavedChanges) {
1521
+ setShowDiscardConfirm(true);
1522
+ } else {
1523
+ onClose();
1524
+ }
1525
+ };
1526
+ const handleDelete = async () => {
1527
+ setSaving(true);
1528
+ try {
1529
+ await service.deleteTask(task.id);
1530
+ config.onTaskDelete?.(task.id);
1531
+ onUpdate();
1532
+ onClose();
1533
+ } finally {
1534
+ setSaving(false);
1535
+ }
1536
+ };
1537
+ const handlePriorityChange = (p) => {
1538
+ setPriority(p);
1539
+ setShowPriorityDropdown(false);
1540
+ saveField("priority", p);
1541
+ };
1542
+ const handleStatusChange = (s) => {
1543
+ setTaskStatus(s);
1544
+ setShowStatusDropdown(false);
1545
+ saveField("status", s);
1546
+ };
1547
+ const handleAddComment = async () => {
1548
+ if (!newComment.trim()) return;
1549
+ await detail.addComment(newComment.trim(), isInternalComment);
1550
+ setNewComment("");
1551
+ setIsInternalComment(false);
1552
+ onUpdate();
1553
+ };
1554
+ const handleCommentKeyDown = (e) => {
1555
+ if (e.key === "Enter" && !e.shiftKey) {
1556
+ e.preventDefault();
1557
+ handleAddComment();
1558
+ }
1559
+ };
1560
+ const handleDeleteComment = async () => {
1561
+ if (!commentToDelete) return;
1562
+ try {
1563
+ await detail.deleteComment(commentToDelete);
1564
+ setCommentToDelete(null);
1565
+ onUpdate();
1566
+ } catch (err) {
1567
+ setCommentToDelete(null);
1568
+ }
1569
+ };
1570
+ const startEditingComment = (c) => {
1571
+ setEditingCommentId(c.id);
1572
+ setEditingCommentContent(c.content);
1573
+ };
1574
+ const cancelEditingComment = () => {
1575
+ setEditingCommentId(null);
1576
+ setEditingCommentContent("");
1577
+ };
1578
+ const saveEditedComment = async () => {
1579
+ if (!editingCommentId || !editingCommentContent.trim()) return;
1580
+ try {
1581
+ await detail.editComment(editingCommentId, editingCommentContent.trim());
1582
+ setEditingCommentId(null);
1583
+ setEditingCommentContent("");
1584
+ } catch {
1585
+ }
1586
+ };
1587
+ const statusCol = columns.find((c) => c.key === taskStatus);
1588
+ const priorityStyle = getPriorityStyle(priority);
1589
+ const initials = getInitials(task.created_by_name || "?");
1590
+ const linkCopied = copiedTaskId === task.id;
1591
+ const handleShareFromDetail = () => {
1592
+ copyShareLink(task.id, projectSlug);
1593
+ };
1594
+ const timeline = [
1595
+ ...detail.comments.map((c) => ({ kind: "comment", date: c.created_at, data: c })),
1596
+ ...detail.activity.map((a) => ({ kind: "activity", date: a.created_at, data: a }))
1597
+ ].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
1598
+ return /* @__PURE__ */ jsxs12(
1599
+ "div",
1600
+ {
1601
+ className: "fixed inset-0 z-[100] flex items-end justify-end bg-black/30 backdrop-blur-md eb-tb-animate-fade-in",
1602
+ onClick: (e) => e.target === e.currentTarget && handleClosePanel(),
1603
+ children: [
1604
+ /* @__PURE__ */ jsxs12(
1605
+ "div",
1606
+ {
1607
+ className: "bg-white shadow-2xl border-l border-neutral-200 w-full lg:w-[75vw] h-full flex flex-col",
1608
+ style: { animation: "eb-tb-slide-in-right 0.15s ease-out" },
1609
+ children: [
1610
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2 sm:gap-4 px-4 sm:px-8 py-3 sm:py-4 border-b border-neutral-100 shrink-0", children: [
1611
+ /* @__PURE__ */ jsx14("h2", { className: "flex-1 text-base sm:text-lg font-semibold text-neutral-900 truncate min-w-0", children: title }),
1612
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2 shrink-0", children: [
1613
+ saving && /* @__PURE__ */ jsx14("span", { className: "text-[11px] text-neutral-400 mr-1", children: "Saving..." }),
1614
+ editing ? /* @__PURE__ */ jsxs12(Fragment, { children: [
1615
+ /* @__PURE__ */ jsxs12("button", { onClick: handleCancelEdit, className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-neutral-600 hover:text-neutral-900 rounded-lg hover:bg-neutral-100 transition-colors", children: [
1616
+ /* @__PURE__ */ jsx14(XIcon, { size: 13 }),
1617
+ " Cancel"
1618
+ ] }),
1619
+ /* @__PURE__ */ jsxs12("button", { onClick: handleSaveEdit, disabled: saving, className: "flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded-lg transition-colors disabled:opacity-60", children: [
1620
+ /* @__PURE__ */ jsx14(CheckIcon, { size: 13 }),
1621
+ " ",
1622
+ saving ? "Saving..." : "Save"
1623
+ ] })
1624
+ ] }) : /* @__PURE__ */ jsxs12(Fragment, { children: [
1625
+ features.sharing && /* @__PURE__ */ jsx14("button", { onClick: handleShareFromDetail, className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-neutral-600 hover:text-neutral-900 rounded-lg border border-neutral-200 hover:border-neutral-300 transition-colors", children: linkCopied ? /* @__PURE__ */ jsxs12(Fragment, { children: [
1626
+ /* @__PURE__ */ jsx14(CheckIcon, { size: 13 }),
1627
+ " Copied!"
1628
+ ] }) : /* @__PURE__ */ jsxs12(Fragment, { children: [
1629
+ /* @__PURE__ */ jsx14(LinkIcon, { size: 13 }),
1630
+ " Share"
1631
+ ] }) }),
1632
+ /* @__PURE__ */ jsxs12("button", { onClick: () => setEditing(true), className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-neutral-600 hover:text-neutral-900 rounded-lg border border-neutral-200 hover:border-neutral-300 transition-colors", children: [
1633
+ /* @__PURE__ */ jsx14(PencilIcon, { size: 13 }),
1634
+ " Edit"
1635
+ ] }),
1636
+ /* @__PURE__ */ jsxs12("button", { onClick: () => setShowDeleteConfirm(true), className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-neutral-600 hover:text-neutral-900 rounded-lg border border-neutral-200 hover:border-neutral-300 transition-colors", children: [
1637
+ /* @__PURE__ */ jsx14(TrashIcon, { size: 13 }),
1638
+ " Delete"
1639
+ ] })
1640
+ ] }),
1641
+ /* @__PURE__ */ jsx14("button", { onClick: handleClosePanel, className: "p-1.5 rounded-md text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100 transition-colors ml-1", children: /* @__PURE__ */ jsx14(XIcon, { size: 16 }) })
1642
+ ] })
1643
+ ] }),
1644
+ /* @__PURE__ */ jsxs12("div", { className: "flex-1 flex flex-col md:flex-row min-h-0", children: [
1645
+ /* @__PURE__ */ jsx14("div", { className: "flex-1 overflow-y-auto min-w-0", children: /* @__PURE__ */ jsxs12("div", { className: "max-w-2xl px-4 sm:px-8 lg:px-10 py-6 sm:py-8", children: [
1646
+ editing && /* @__PURE__ */ jsxs12("div", { className: "mb-6", children: [
1647
+ /* @__PURE__ */ jsx14("label", { className: "text-[11px] font-medium text-neutral-400 uppercase tracking-wide mb-2 block", children: "Title" }),
1648
+ /* @__PURE__ */ jsx14(
1649
+ "input",
1650
+ {
1651
+ value: title,
1652
+ onChange: (e) => setTitle(e.target.value),
1653
+ className: "w-full text-lg font-semibold text-neutral-900 bg-white border border-neutral-200 rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF5E00]/10 focus:border-[#FF5E00]/50 leading-tight",
1654
+ placeholder: "Task title",
1655
+ autoFocus: true
1656
+ }
1657
+ )
1658
+ ] }),
1659
+ /* @__PURE__ */ jsxs12("div", { className: `${editing ? "mt-0" : "mt-2"} rounded-lg border border-neutral-100 grid grid-cols-1 sm:grid-cols-2`, children: [
1660
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center h-10 px-4 border-b sm:border-r border-neutral-100", children: [
1661
+ /* @__PURE__ */ jsx14("span", { className: "w-14 text-[11px] text-neutral-400 font-medium shrink-0", children: "Task ID" }),
1662
+ /* @__PURE__ */ jsxs12("span", { className: "text-[11px] font-mono text-neutral-400 bg-neutral-100 px-1.5 py-0.5 rounded", children: [
1663
+ "T-",
1664
+ task.id.slice(-6).toUpperCase()
1665
+ ] })
1666
+ ] }),
1667
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center h-10 px-4 border-b border-neutral-100", children: [
1668
+ /* @__PURE__ */ jsx14("span", { className: "w-14 text-[11px] text-neutral-400 font-medium shrink-0", children: "Creator" }),
1669
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2", children: [
1670
+ /* @__PURE__ */ jsx14("div", { className: "w-5 h-5 rounded-full bg-[#FF5E00] flex items-center justify-center", children: /* @__PURE__ */ jsx14("span", { className: "text-[8px] font-medium text-white leading-none", children: initials }) }),
1671
+ /* @__PURE__ */ jsx14("span", { className: "text-xs text-neutral-700", children: task.created_by_name || task.created_by })
1672
+ ] })
1673
+ ] }),
1674
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center h-10 px-4 border-b sm:border-r border-neutral-100", children: [
1675
+ /* @__PURE__ */ jsx14("span", { className: "w-14 text-[11px] text-neutral-400 font-medium shrink-0", children: "Created" }),
1676
+ /* @__PURE__ */ jsx14("span", { className: "text-xs text-neutral-600", children: formatDateTime(task.created_at) })
1677
+ ] }),
1678
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center h-10 px-4 border-b border-neutral-100", children: [
1679
+ /* @__PURE__ */ jsx14("span", { className: "w-14 text-[11px] text-neutral-400 font-medium shrink-0", children: "Updated" }),
1680
+ /* @__PURE__ */ jsx14("span", { className: "text-xs text-neutral-600", children: task.updated_at ? formatDateTime(task.updated_at) : "\u2014" })
1681
+ ] }),
1682
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center h-10 px-4 border-b sm:border-r border-neutral-100 hover:bg-neutral-50/50 transition-colors", children: [
1683
+ /* @__PURE__ */ jsx14("span", { className: "w-14 text-[11px] text-neutral-400 font-medium shrink-0", children: "Status" }),
1684
+ /* @__PURE__ */ jsxs12("div", { className: "relative", children: [
1685
+ /* @__PURE__ */ jsxs12(
1686
+ "button",
1687
+ {
1688
+ onClick: () => {
1689
+ setShowStatusDropdown(!showStatusDropdown);
1690
+ setShowPriorityDropdown(false);
1691
+ setShowTagDropdown(false);
1692
+ },
1693
+ className: "flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded-md hover:bg-neutral-100 transition-colors",
1694
+ children: [
1695
+ /* @__PURE__ */ jsx14("span", { className: `w-2 h-2 rounded-full ${statusCol?.color ?? "bg-neutral-400"}` }),
1696
+ statusCol?.label ?? taskStatus,
1697
+ /* @__PURE__ */ jsx14(ChevronDownIcon, { size: 12, className: "text-neutral-400" })
1698
+ ]
1699
+ }
1700
+ ),
1701
+ showStatusDropdown && /* @__PURE__ */ jsx14("div", { className: "absolute top-full mt-1 left-0 bg-white border border-neutral-200 rounded-lg shadow-lg z-10 py-1 w-52 eb-tb-animate-zoom-in", children: columns.map((col) => /* @__PURE__ */ jsxs12("button", { onClick: () => handleStatusChange(col.key), className: `w-full text-left px-3 py-1.5 text-xs hover:bg-neutral-50 flex items-center gap-2 ${taskStatus === col.key ? "font-medium bg-neutral-50" : ""}`, children: [
1702
+ /* @__PURE__ */ jsx14("span", { className: `w-2 h-2 rounded-full ${col.color}` }),
1703
+ " ",
1704
+ col.label
1705
+ ] }, col.key)) })
1706
+ ] })
1707
+ ] }),
1708
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center h-10 px-4 border-b border-neutral-100 hover:bg-neutral-50/50 transition-colors", children: [
1709
+ /* @__PURE__ */ jsx14("span", { className: "w-14 text-[11px] text-neutral-400 font-medium shrink-0", children: "Priority" }),
1710
+ /* @__PURE__ */ jsxs12("div", { className: "relative", children: [
1711
+ /* @__PURE__ */ jsxs12(
1712
+ "button",
1713
+ {
1714
+ onClick: () => {
1715
+ setShowPriorityDropdown(!showPriorityDropdown);
1716
+ setShowStatusDropdown(false);
1717
+ setShowTagDropdown(false);
1718
+ },
1719
+ className: `flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded-md transition-colors border ${priorityStyle.className}`,
1720
+ children: [
1721
+ priorityStyle.label,
1722
+ /* @__PURE__ */ jsx14(ChevronDownIcon, { size: 12, className: "opacity-50" })
1723
+ ]
1724
+ }
1725
+ ),
1726
+ showPriorityDropdown && /* @__PURE__ */ jsx14("div", { className: "absolute top-full mt-1 left-0 bg-white border border-neutral-200 rounded-lg shadow-lg z-10 py-1 w-40 eb-tb-animate-zoom-in", children: priorities.map((p) => /* @__PURE__ */ jsx14("button", { onClick: () => handlePriorityChange(p.value), className: `w-full text-left px-3 py-2 text-xs hover:bg-neutral-50 flex items-center gap-2 ${priority === p.value ? "bg-neutral-50" : ""}`, children: /* @__PURE__ */ jsx14("span", { className: `inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded border ${p.className}`, children: p.label }) }, p.value)) })
1727
+ ] })
1728
+ ] }),
1729
+ features.tags && /* @__PURE__ */ jsxs12("div", { className: "flex items-center min-h-[40px] px-4 border-b border-neutral-100 sm:col-span-2 hover:bg-neutral-50/50 transition-colors", children: [
1730
+ /* @__PURE__ */ jsx14("span", { className: "w-14 text-[11px] text-neutral-400 font-medium shrink-0", children: "Tags" }),
1731
+ /* @__PURE__ */ jsxs12("div", { className: "relative flex-1 flex items-center gap-1.5 flex-wrap py-1.5", children: [
1732
+ localTags.map((tag) => /* @__PURE__ */ jsx14(TagBadge, { tag, size: "sm", onRemove: () => {
1733
+ const newTags = localTags.filter((t) => t !== tag);
1734
+ setLocalTags(newTags);
1735
+ saveField("tags", newTags);
1736
+ } }, tag)),
1737
+ /* @__PURE__ */ jsxs12(
1738
+ "button",
1739
+ {
1740
+ onClick: () => {
1741
+ if (!showTagDropdown) setPendingTags([...localTags]);
1742
+ setShowTagDropdown(!showTagDropdown);
1743
+ setShowStatusDropdown(false);
1744
+ setShowPriorityDropdown(false);
1745
+ },
1746
+ className: "inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] text-neutral-400 hover:text-neutral-600 rounded border border-dashed border-neutral-300 hover:border-neutral-400 transition-colors",
1747
+ children: [
1748
+ /* @__PURE__ */ jsx14(PlusIcon, { size: 12 }),
1749
+ " ",
1750
+ showTagDropdown ? "Close" : "Edit"
1751
+ ]
1752
+ }
1753
+ ),
1754
+ showTagDropdown && /* @__PURE__ */ jsxs12("div", { className: "absolute top-full left-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-10 w-56 eb-tb-animate-zoom-in", children: [
1755
+ /* @__PURE__ */ jsxs12("div", { className: "py-1", children: [
1756
+ predefinedTags.map((tag) => {
1757
+ const isSelected = pendingTags.includes(tag.value);
1758
+ return /* @__PURE__ */ jsxs12(
1759
+ "button",
1760
+ {
1761
+ onClick: () => setPendingTags((prev) => isSelected ? prev.filter((t) => t !== tag.value) : [...prev, tag.value]),
1762
+ className: `w-full text-left px-3 py-1.5 text-xs hover:bg-neutral-50 flex items-center justify-between ${isSelected ? "bg-neutral-50" : ""}`,
1763
+ children: [
1764
+ /* @__PURE__ */ jsx14("span", { className: `inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded border ${tag.className}`, children: tag.label }),
1765
+ isSelected && /* @__PURE__ */ jsx14(CheckIcon, { size: 12, strokeWidth: 2.5, className: "text-[#FF5E00]" })
1766
+ ]
1767
+ },
1768
+ tag.value
1769
+ );
1770
+ }),
1771
+ !showOtherTagInput ? /* @__PURE__ */ jsx14("button", { onClick: () => setShowOtherTagInput(true), className: "w-full text-left px-3 py-1.5 text-xs hover:bg-neutral-50 flex items-center justify-between", children: /* @__PURE__ */ jsx14("span", { className: "inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded border bg-neutral-100 text-neutral-500 border-neutral-200", children: "Other" }) }) : /* @__PURE__ */ jsx14("div", { className: "px-3 py-2 bg-neutral-50/50", children: /* @__PURE__ */ jsxs12("div", { className: "flex gap-1.5", children: [
1772
+ /* @__PURE__ */ jsx14("input", { type: "text", autoFocus: true, onKeyDown: (e) => {
1773
+ if (e.key === "Enter") {
1774
+ e.preventDefault();
1775
+ const val = e.currentTarget.value.trim().toLowerCase().replace(/\s+/g, "-");
1776
+ if (val && !pendingTags.includes(val)) setPendingTags((prev) => [...prev, val]);
1777
+ e.currentTarget.value = "";
1778
+ setShowOtherTagInput(false);
1779
+ }
1780
+ if (e.key === "Escape") setShowOtherTagInput(false);
1781
+ }, className: "flex-1 px-2 py-1.5 text-xs border border-neutral-200 rounded-md focus:outline-none focus:ring-1 focus:ring-[#FF5E00]/20 focus:border-[#FF5E00]/50", placeholder: "Type a custom tag...", onClick: (e) => e.stopPropagation() }),
1782
+ /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => setShowOtherTagInput(false), className: "px-2.5 py-1.5 text-[10px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded-md transition-colors", children: "Add" })
1783
+ ] }) })
1784
+ ] }),
1785
+ /* @__PURE__ */ jsxs12("div", { className: "border-t border-neutral-100 px-2 py-2 flex items-center justify-end gap-2", children: [
1786
+ /* @__PURE__ */ jsx14("button", { onClick: () => {
1787
+ setPendingTags([...localTags]);
1788
+ setShowTagDropdown(false);
1789
+ setShowOtherTagInput(false);
1790
+ }, className: "px-2.5 py-1 text-[11px] font-medium text-neutral-500 hover:text-neutral-700 transition-colors", children: "Cancel" }),
1791
+ /* @__PURE__ */ jsx14("button", { onClick: () => {
1792
+ setLocalTags([...pendingTags]);
1793
+ saveField("tags", pendingTags);
1794
+ setShowTagDropdown(false);
1795
+ setShowOtherTagInput(false);
1796
+ }, className: "px-3 py-1 text-[11px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded transition-colors", children: "Save" })
1797
+ ] })
1798
+ ] })
1799
+ ] })
1800
+ ] })
1801
+ ] }),
1802
+ /* @__PURE__ */ jsxs12("div", { className: "mt-5 space-y-4", children: [
1803
+ DESCRIPTION_SECTIONS.map((section) => {
1804
+ const val = description[section.key] || "";
1805
+ const hasContent = val.trim().length > 0;
1806
+ if (!editing && !hasContent) return null;
1807
+ return /* @__PURE__ */ jsxs12("div", { children: [
1808
+ /* @__PURE__ */ jsx14("label", { className: "text-[11px] font-medium text-neutral-400 uppercase tracking-wide mb-1.5 block", children: section.label }),
1809
+ editing ? /* @__PURE__ */ jsx14(
1810
+ MentionTextarea,
1811
+ {
1812
+ value: val,
1813
+ onChange: (v) => setDescription((prev) => ({ ...prev, [section.key]: v })),
1814
+ rows: 3,
1815
+ className: "w-full px-4 py-2.5 bg-white border border-neutral-200 rounded-lg text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-[#FF5E00]/10 focus:border-[#FF5E00]/50 resize-y transition-all placeholder:text-neutral-400",
1816
+ placeholder: `Add ${section.label.toLowerCase()}...`
1817
+ }
1818
+ ) : /* @__PURE__ */ jsx14("div", { className: "w-full px-3.5 py-2.5 bg-neutral-50/60 border border-neutral-100 rounded-lg text-sm leading-relaxed text-neutral-700 whitespace-pre-wrap", children: /* @__PURE__ */ jsx14(MentionText, { text: val }) })
1819
+ ] }, section.key);
1820
+ }),
1821
+ !editing && !hasDescription(description) && /* @__PURE__ */ jsx14("div", { className: "px-3.5 py-4 bg-neutral-50/60 border border-neutral-100 rounded-lg text-sm text-neutral-400 text-center", children: "No description provided." })
1822
+ ] })
1823
+ ] }) }),
1824
+ features.comments && /* @__PURE__ */ jsxs12("div", { className: "w-full md:w-[380px] shrink-0 border-t md:border-t-0 md:border-l border-neutral-100 flex flex-col bg-[#FAFAFA]", children: [
1825
+ /* @__PURE__ */ jsx14("div", { className: "px-5 h-12 flex items-center border-b border-neutral-100", children: /* @__PURE__ */ jsxs12("h3", { className: "text-[11px] font-semibold text-neutral-500 uppercase tracking-wide", children: [
1826
+ "Activity",
1827
+ detail.commentsLoaded ? ` \xB7 ${detail.comments.length + detail.activity.length}` : ""
1828
+ ] }) }),
1829
+ /* @__PURE__ */ jsxs12("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: [
1830
+ !detail.commentsLoaded && /* @__PURE__ */ jsx14("div", { className: "space-y-4", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxs12("div", { className: "flex gap-2.5", children: [
1831
+ /* @__PURE__ */ jsx14(SkeletonPulse, { className: "w-7 h-7 rounded-full shrink-0" }),
1832
+ /* @__PURE__ */ jsxs12("div", { className: "flex-1 space-y-1.5", children: [
1833
+ /* @__PURE__ */ jsx14(SkeletonPulse, { className: "h-3 w-20" }),
1834
+ /* @__PURE__ */ jsx14(SkeletonPulse, { className: "h-3 w-full" })
1835
+ ] })
1836
+ ] }, i)) }),
1837
+ detail.commentsLoaded && detail.comments.length === 0 && detail.activity.length === 0 && /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [
1838
+ /* @__PURE__ */ jsx14("div", { className: "w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center mb-3", children: /* @__PURE__ */ jsx14(MessageSquareIcon, { size: 20, className: "text-neutral-400" }) }),
1839
+ /* @__PURE__ */ jsx14("p", { className: "text-xs text-neutral-400", children: "No activity yet" }),
1840
+ /* @__PURE__ */ jsx14("p", { className: "text-[10px] text-neutral-400 mt-0.5", children: "Comments and status changes will appear here" })
1841
+ ] }),
1842
+ /* @__PURE__ */ jsxs12("div", { className: "space-y-4", children: [
1843
+ timeline.map((item, i) => {
1844
+ if (item.kind === "comment") {
1845
+ const c = item.data;
1846
+ const isOwner = user.username === c.author_id;
1847
+ const isEditing = editingCommentId === c.id;
1848
+ return /* @__PURE__ */ jsxs12("div", { className: `group/comment flex gap-2.5 ${c.is_internal ? "pl-2 border-l-2 border-amber-300" : ""}`, children: [
1849
+ /* @__PURE__ */ jsx14("div", { className: `w-7 h-7 rounded-full flex items-center justify-center shrink-0 mt-0.5 ${c.is_internal ? "bg-amber-100" : "bg-neutral-200"}`, children: /* @__PURE__ */ jsx14("span", { className: `text-[10px] font-medium ${c.is_internal ? "text-amber-600" : "text-neutral-600"}`, children: getInitials(c.author_name || "?") }) }),
1850
+ /* @__PURE__ */ jsxs12("div", { className: "flex-1 min-w-0", children: [
1851
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2 mb-0.5", children: [
1852
+ /* @__PURE__ */ jsx14("span", { className: "text-[11px] font-semibold text-neutral-700", children: c.author_name }),
1853
+ c.is_internal && /* @__PURE__ */ jsxs12("span", { className: "inline-flex items-center gap-0.5 px-1.5 py-px text-[9px] font-medium text-amber-600 bg-amber-50 border border-amber-200 rounded", children: [
1854
+ /* @__PURE__ */ jsx14(LockIcon, { size: 8, strokeWidth: 2.5 }),
1855
+ " Internal"
1856
+ ] }),
1857
+ /* @__PURE__ */ jsx14("span", { className: "text-[10px] text-neutral-400", children: formatDateTime(c.created_at) }),
1858
+ c.edited && /* @__PURE__ */ jsx14("span", { className: "text-[9px] text-neutral-400 italic", title: c.edited_at ? `Edited ${formatDateTime(c.edited_at)}` : "Edited", children: "(edited)" }),
1859
+ isOwner && /* @__PURE__ */ jsx14("div", { className: `ml-auto flex items-center gap-0.5 transition-opacity ${isEditing ? "opacity-100" : "opacity-0 group-hover/comment:opacity-100"}`, children: isEditing ? /* @__PURE__ */ jsxs12(Fragment, { children: [
1860
+ /* @__PURE__ */ jsx14("button", { onClick: saveEditedComment, disabled: !editingCommentContent.trim() || editingCommentContent.trim() === c.content, className: "p-1 rounded hover:bg-green-50 text-neutral-400 hover:text-green-600 disabled:opacity-30", title: "Save edit", children: /* @__PURE__ */ jsx14(CheckIcon, { size: 12, strokeWidth: 2.5 }) }),
1861
+ /* @__PURE__ */ jsx14("button", { onClick: cancelEditingComment, className: "p-1 rounded hover:bg-red-50 text-neutral-400 hover:text-red-500", title: "Cancel edit", children: /* @__PURE__ */ jsx14(XIcon, { size: 12, strokeWidth: 2.5 }) })
1862
+ ] }) : /* @__PURE__ */ jsxs12(Fragment, { children: [
1863
+ /* @__PURE__ */ jsx14("button", { onClick: () => startEditingComment(c), className: "p-1 rounded hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600", title: "Edit comment", children: /* @__PURE__ */ jsx14(PencilIcon, { size: 11 }) }),
1864
+ /* @__PURE__ */ jsx14("button", { onClick: () => setCommentToDelete(c.id), className: "p-1 rounded hover:bg-red-50 text-neutral-400 hover:text-red-500", title: "Delete comment", children: /* @__PURE__ */ jsx14(TrashIcon, { size: 11 }) })
1865
+ ] }) })
1866
+ ] }),
1867
+ isEditing ? /* @__PURE__ */ jsx14(
1868
+ MentionTextarea,
1869
+ {
1870
+ value: editingCommentContent,
1871
+ onChange: setEditingCommentContent,
1872
+ onKeyDown: (e) => {
1873
+ if (e.key === "Escape") cancelEditingComment();
1874
+ if (e.key === "Enter" && !e.shiftKey) {
1875
+ e.preventDefault();
1876
+ saveEditedComment();
1877
+ }
1878
+ },
1879
+ rows: 2,
1880
+ className: "w-full px-2.5 py-1.5 bg-white border border-neutral-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-[#FF5E00]/20 focus:border-[#FF5E00]/50 resize-none"
1881
+ }
1882
+ ) : /* @__PURE__ */ jsx14("div", { className: "text-xs text-neutral-600 whitespace-pre-wrap leading-relaxed", children: /* @__PURE__ */ jsx14(MentionText, { text: c.content }) })
1883
+ ] })
1884
+ ] }, `c-${c.id}`);
1885
+ } else {
1886
+ const a = item.data;
1887
+ const fromCol = columns.find((col) => col.key === a.from_status);
1888
+ const toCol = columns.find((col) => col.key === a.to_status);
1889
+ const isCreated = a.type === "created";
1890
+ return /* @__PURE__ */ jsxs12("div", { className: "flex gap-2.5", children: [
1891
+ /* @__PURE__ */ jsx14("div", { className: "w-7 h-7 rounded-full bg-neutral-100 flex items-center justify-center shrink-0", children: isCreated ? /* @__PURE__ */ jsx14(PlusIcon, { size: 12, className: "text-neutral-400" }) : /* @__PURE__ */ jsxs12("svg", { xmlns: "http://www.w3.org/2000/svg", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-neutral-400", children: [
1892
+ /* @__PURE__ */ jsx14("path", { d: "M5 12h14" }),
1893
+ /* @__PURE__ */ jsx14("path", { d: "m12 5 7 7-7 7" })
1894
+ ] }) }),
1895
+ /* @__PURE__ */ jsxs12("div", { className: "flex-1 min-w-0", children: [
1896
+ isCreated ? /* @__PURE__ */ jsxs12("p", { className: "text-[11px] text-neutral-500", children: [
1897
+ /* @__PURE__ */ jsx14("span", { className: "font-medium text-neutral-700", children: a.user_name }),
1898
+ " created this task"
1899
+ ] }) : /* @__PURE__ */ jsxs12("p", { className: "text-[11px] text-neutral-500", children: [
1900
+ /* @__PURE__ */ jsx14("span", { className: "font-medium text-neutral-700", children: a.user_name }),
1901
+ " moved from ",
1902
+ /* @__PURE__ */ jsx14("span", { className: "font-medium text-neutral-700", children: fromCol?.label ?? a.from_status }),
1903
+ " to ",
1904
+ /* @__PURE__ */ jsx14("span", { className: "font-medium text-neutral-700", children: toCol?.label ?? a.to_status })
1905
+ ] }),
1906
+ /* @__PURE__ */ jsx14("span", { className: "text-[10px] text-neutral-400", children: formatDateTime(a.created_at) })
1907
+ ] })
1908
+ ] }, `a-${a.id}`);
1909
+ }
1910
+ }),
1911
+ /* @__PURE__ */ jsx14("div", { ref: commentsEndRef })
1912
+ ] })
1913
+ ] }),
1914
+ /* @__PURE__ */ jsxs12("div", { className: `px-5 py-4 pb-5 border-t bg-white transition-colors ${isInternalComment ? "border-amber-300 bg-amber-50/30" : "border-neutral-200"}`, children: [
1915
+ /* @__PURE__ */ jsx14(
1916
+ MentionTextarea,
1917
+ {
1918
+ value: newComment,
1919
+ onChange: setNewComment,
1920
+ onKeyDown: handleCommentKeyDown,
1921
+ rows: 2,
1922
+ className: `w-full px-3 py-2 border rounded-lg text-xs focus:outline-none focus:ring-2 resize-none transition-colors placeholder:text-neutral-400 ${isInternalComment ? "bg-amber-50/50 border-amber-200 focus:ring-amber-200/30 focus:border-amber-300" : "bg-neutral-50 border-neutral-200 focus:bg-white focus:ring-[#FF5E00]/10 focus:border-[#FF5E00]/50"}`,
1923
+ placeholder: isInternalComment ? "Write an internal note... (only visible to team)" : "Write a comment... (type @ to mention someone)"
1924
+ }
1925
+ ),
1926
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between mt-2.5", children: [
1927
+ features.internalComments && user.is_internal ? /* @__PURE__ */ jsxs12(
1928
+ "button",
1929
+ {
1930
+ type: "button",
1931
+ onClick: () => setIsInternalComment(!isInternalComment),
1932
+ className: `flex items-center gap-1.5 text-[11px] font-medium rounded-md px-2 py-1 transition-colors ${isInternalComment ? "bg-amber-100 text-amber-700 border border-amber-200" : "text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100"}`,
1933
+ children: [
1934
+ /* @__PURE__ */ jsx14(LockIcon, { size: 12 }),
1935
+ " Internal"
1936
+ ]
1937
+ }
1938
+ ) : /* @__PURE__ */ jsx14("div", {}),
1939
+ /* @__PURE__ */ jsx14(
1940
+ "button",
1941
+ {
1942
+ onClick: handleAddComment,
1943
+ disabled: detail.commentLoading || !newComment.trim(),
1944
+ className: `px-4 py-1.5 text-xs font-medium text-white rounded-lg transition-colors disabled:opacity-40 ${isInternalComment ? "bg-amber-500 hover:bg-amber-600" : "bg-[#FF5E00] hover:bg-[#E05200]"}`,
1945
+ children: detail.commentLoading ? "Sending..." : isInternalComment ? "Add Note" : "Comment"
1946
+ }
1947
+ )
1948
+ ] })
1949
+ ] })
1950
+ ] })
1951
+ ] })
1952
+ ]
1953
+ }
1954
+ ),
1955
+ showDeleteConfirm && /* @__PURE__ */ jsx14("div", { className: "fixed inset-0 z-[110] flex items-center justify-center bg-black/40 eb-tb-animate-fade-in", children: /* @__PURE__ */ jsx14("div", { className: "bg-white rounded-2xl shadow-2xl border border-neutral-200 w-full max-w-sm mx-4 eb-tb-animate-zoom-in", children: /* @__PURE__ */ jsxs12("div", { className: "p-5 space-y-4", children: [
1956
+ /* @__PURE__ */ jsx14("h2", { className: "text-lg font-medium text-neutral-900", children: "Delete Task" }),
1957
+ /* @__PURE__ */ jsxs12("p", { className: "text-sm text-neutral-500", children: [
1958
+ "Are you sure you want to delete ",
1959
+ /* @__PURE__ */ jsx14("span", { className: "font-medium text-neutral-900", children: task.title }),
1960
+ "? This cannot be undone."
1961
+ ] }),
1962
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-end gap-3", children: [
1963
+ /* @__PURE__ */ jsx14("button", { onClick: () => setShowDeleteConfirm(false), className: "px-4 py-2 text-sm text-neutral-600 hover:text-neutral-900 transition-colors", children: "Cancel" }),
1964
+ /* @__PURE__ */ jsx14("button", { onClick: handleDelete, disabled: saving, className: "px-5 py-2 text-sm font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded-lg transition-colors disabled:opacity-60", children: saving ? "Deleting..." : "Delete" })
1965
+ ] })
1966
+ ] }) }) }),
1967
+ commentToDelete && /* @__PURE__ */ jsx14("div", { className: "fixed inset-0 z-[110] flex items-center justify-center bg-black/40 eb-tb-animate-fade-in", children: /* @__PURE__ */ jsx14("div", { className: "bg-white rounded-2xl shadow-2xl border border-neutral-200 w-full max-w-sm mx-4 eb-tb-animate-zoom-in", children: /* @__PURE__ */ jsxs12("div", { className: "p-5 space-y-4", children: [
1968
+ /* @__PURE__ */ jsx14("h2", { className: "text-lg font-medium text-neutral-900", children: "Delete Comment" }),
1969
+ /* @__PURE__ */ jsx14("p", { className: "text-sm text-neutral-500", children: "Are you sure you want to delete this comment? This cannot be undone." }),
1970
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-end gap-3", children: [
1971
+ /* @__PURE__ */ jsx14("button", { onClick: () => setCommentToDelete(null), className: "px-4 py-2 text-sm text-neutral-600 hover:text-neutral-900 transition-colors", children: "Cancel" }),
1972
+ /* @__PURE__ */ jsx14("button", { onClick: handleDeleteComment, className: "px-5 py-2 text-sm font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded-lg transition-colors", children: "Delete" })
1973
+ ] })
1974
+ ] }) }) }),
1975
+ showDiscardConfirm && /* @__PURE__ */ jsx14("div", { className: "fixed inset-0 z-[110] flex items-center justify-center bg-black/40 eb-tb-animate-fade-in", children: /* @__PURE__ */ jsx14("div", { className: "bg-white rounded-2xl shadow-2xl border border-neutral-200 w-full max-w-sm mx-4 eb-tb-animate-zoom-in", children: /* @__PURE__ */ jsxs12("div", { className: "p-5 space-y-4", children: [
1976
+ /* @__PURE__ */ jsx14("h2", { className: "text-lg font-medium text-neutral-900", children: "Unsaved Changes" }),
1977
+ /* @__PURE__ */ jsx14("p", { className: "text-sm text-neutral-500", children: "You have unsaved changes that will be lost. Are you sure you want to discard them?" }),
1978
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-end gap-3", children: [
1979
+ /* @__PURE__ */ jsx14("button", { onClick: () => setShowDiscardConfirm(false), className: "px-4 py-2 text-sm text-neutral-600 hover:text-neutral-900 transition-colors", children: "Keep Editing" }),
1980
+ /* @__PURE__ */ jsx14("button", { onClick: () => {
1981
+ handleDiscardChanges();
1982
+ onClose();
1983
+ }, className: "px-5 py-2 text-sm font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded-lg transition-colors", children: "Discard" })
1984
+ ] })
1985
+ ] }) }) })
1986
+ ]
1987
+ }
1988
+ );
1989
+ }
1990
+
1991
+ // src/components/TaskBoard.tsx
1992
+ import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1993
+ function TaskBoard({
1994
+ className = "",
1995
+ headerActions,
1996
+ onTaskOpen,
1997
+ onShareFeedback,
1998
+ renderTaskDetail,
1999
+ renderCreateTask
2000
+ }) {
2001
+ const { columns, features, service } = useTaskBoardContext();
2002
+ const board = useTaskBoard();
2003
+ const actions = useTaskActions(board.tasks, board.setTasks, board.fetchTasks);
2004
+ const { copiedTaskId, copyShareLink } = useShareLink();
2005
+ const [selectedTask, setSelectedTask] = useState9(null);
2006
+ const [createForStatus, setCreateForStatus] = useState9("");
2007
+ const [filterTags, setFilterTags] = useState9([]);
2008
+ const [sharedTaskHandled, setSharedTaskHandled] = useState9(false);
2009
+ useEffect8(() => {
2010
+ if (sharedTaskHandled || !board.selectedProject || board.boardLoading) return;
2011
+ if (typeof window === "undefined") return;
2012
+ const params = new URLSearchParams(window.location.search);
2013
+ const taskId = params.get("task");
2014
+ if (!taskId) return;
2015
+ setSharedTaskHandled(true);
2016
+ let cancelled = false;
2017
+ (async () => {
2018
+ try {
2019
+ const task = await service.getTask(taskId);
2020
+ if (cancelled) return;
2021
+ setSelectedTask(task);
2022
+ service.markTaskRead(taskId).catch(() => {
2023
+ });
2024
+ const url = new URL(window.location.href);
2025
+ url.searchParams.delete("task");
2026
+ window.history.replaceState({}, "", url.toString());
2027
+ } catch {
2028
+ if (!cancelled) board.setError("Could not open shared task.");
2029
+ }
2030
+ })();
2031
+ return () => {
2032
+ cancelled = true;
2033
+ };
2034
+ }, [board.selectedProject, board.boardLoading, sharedTaskHandled, service]);
2035
+ useEffect8(() => {
2036
+ if (typeof window === "undefined") return;
2037
+ if (board.selectedProject && board.projects.length > 1) {
2038
+ const url = new URL(window.location.href);
2039
+ url.searchParams.set("project", board.selectedProject);
2040
+ window.history.replaceState({}, "", url.toString());
2041
+ }
2042
+ }, [board.selectedProject, board.projects]);
2043
+ const handleDragEnd = useCallback7((result) => {
2044
+ const { draggableId, source, destination } = result;
2045
+ if (!destination) return;
2046
+ if (source.droppableId === destination.droppableId && source.index === destination.index) return;
2047
+ actions.moveTask(
2048
+ draggableId,
2049
+ source.droppableId,
2050
+ destination.droppableId,
2051
+ source.index,
2052
+ destination.index
2053
+ );
2054
+ }, [actions]);
2055
+ const handleTaskClick = (task) => {
2056
+ setSelectedTask(task);
2057
+ onTaskOpen?.(task);
2058
+ actions.markTaskRead(task.id);
2059
+ if (task.has_unread) {
2060
+ board.setTasks((prev) => {
2061
+ const updated = { ...prev };
2062
+ const col = updated[task.status];
2063
+ if (col) {
2064
+ updated[task.status] = col.map(
2065
+ (t) => t.id === task.id ? { ...t, has_unread: false } : t
2066
+ );
2067
+ }
2068
+ return updated;
2069
+ });
2070
+ board.setColumnUnreads((prev) => ({
2071
+ ...prev,
2072
+ [task.status]: Math.max(0, (prev[task.status] || 0) - 1)
2073
+ }));
2074
+ }
2075
+ };
2076
+ const handleOpenTaskFromNotification = async (taskId, projectSlug) => {
2077
+ if (board.selectedProject !== projectSlug) {
2078
+ board.setSelectedProject(projectSlug);
2079
+ }
2080
+ try {
2081
+ const task = await service.getTask(taskId);
2082
+ setSelectedTask(task);
2083
+ service.markTaskRead(taskId).catch(() => {
2084
+ });
2085
+ } catch {
2086
+ board.setError("Could not open task.");
2087
+ }
2088
+ };
2089
+ const predefinedValues = PREDEFINED_TAGS.map((p) => p.value);
2090
+ const handleCreateClose = () => setCreateForStatus("");
2091
+ const handleCreateDone = () => {
2092
+ board.fetchTasks();
2093
+ board.showSuccess("Task created");
2094
+ };
2095
+ const handleDetailClose = () => setSelectedTask(null);
2096
+ return /* @__PURE__ */ jsxs13("div", { className: `flex flex-col h-full ${className}`, children: [
2097
+ /* @__PURE__ */ jsxs13("div", { className: "mb-4 sm:mb-6 shrink-0", children: [
2098
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between mb-1 sm:mb-2", children: [
2099
+ /* @__PURE__ */ jsx15("h1", { className: "text-2xl sm:text-3xl font-medium text-neutral-900 tracking-tight", children: "Task Board" }),
2100
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2", children: [
2101
+ onShareFeedback && /* @__PURE__ */ jsxs13(
2102
+ "button",
2103
+ {
2104
+ onClick: onShareFeedback,
2105
+ className: "flex items-center gap-1.5 text-xs font-medium text-neutral-600 hover:text-neutral-900 px-3 py-2 sm:py-2.5 rounded-lg border border-neutral-200 hover:border-neutral-300 transition-colors",
2106
+ children: [
2107
+ /* @__PURE__ */ jsx15(FeedbackIcon, { size: 16 }),
2108
+ /* @__PURE__ */ jsx15("span", { className: "hidden sm:inline", children: "Share Feedback" })
2109
+ ]
2110
+ }
2111
+ ),
2112
+ headerActions,
2113
+ features.notifications && /* @__PURE__ */ jsx15(NotificationBell, { onOpenTask: handleOpenTaskFromNotification }),
2114
+ board.projects.length > 0 && /* @__PURE__ */ jsxs13(
2115
+ "button",
2116
+ {
2117
+ onClick: () => setCreateForStatus("backlog"),
2118
+ className: "flex items-center gap-1.5 sm:gap-2 text-xs font-semibold text-white bg-[#FF5E00] hover:bg-[#E05200] px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg transition-colors shadow-sm",
2119
+ children: [
2120
+ /* @__PURE__ */ jsx15(PlusIcon, { size: 16 }),
2121
+ /* @__PURE__ */ jsx15("span", { className: "hidden sm:inline", children: "New Task" }),
2122
+ /* @__PURE__ */ jsx15("span", { className: "sm:hidden", children: "New" })
2123
+ ]
2124
+ }
2125
+ )
2126
+ ] })
2127
+ ] }),
2128
+ /* @__PURE__ */ jsx15("p", { className: "text-neutral-500 font-light text-sm sm:text-lg", children: "Track and manage work across projects." })
2129
+ ] }),
2130
+ board.successMessage && /* @__PURE__ */ jsx15("div", { className: "mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm", children: board.successMessage }),
2131
+ board.error && /* @__PURE__ */ jsxs13("div", { className: "mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm flex items-center justify-between", children: [
2132
+ board.error,
2133
+ /* @__PURE__ */ jsx15("button", { onClick: () => board.setError(""), className: "text-red-400 hover:text-red-600", children: /* @__PURE__ */ jsx15(XIcon, { size: 16 }) })
2134
+ ] }),
2135
+ board.projects.length === 0 ? /* @__PURE__ */ jsx15("div", { className: "flex-1 flex items-center justify-center", children: /* @__PURE__ */ jsxs13("div", { className: "text-center", children: [
2136
+ /* @__PURE__ */ jsx15("h2", { className: "text-xl font-medium text-neutral-900 mb-2", children: "No Projects Available" }),
2137
+ /* @__PURE__ */ jsx15("p", { className: "text-neutral-500", children: "You don't have access to any projects yet." })
2138
+ ] }) }) : /* @__PURE__ */ jsxs13(Fragment2, { children: [
2139
+ /* @__PURE__ */ jsx15(
2140
+ FilterBar,
2141
+ {
2142
+ projects: board.projects,
2143
+ selectedProject: board.selectedProject,
2144
+ onSelectProject: board.setSelectedProject,
2145
+ filterTags,
2146
+ onSetFilterTags: setFilterTags
2147
+ }
2148
+ ),
2149
+ board.boardLoading ? /* @__PURE__ */ jsx15(BoardSkeleton, {}) : /* @__PURE__ */ jsx15("div", { className: "flex-1 min-h-0 eb-tb-board-scroll overflow-x-auto overflow-y-hidden pb-4", children: /* @__PURE__ */ jsx15(DragDropContext, { onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx15("div", { className: "flex gap-4 min-w-max h-full", children: columns.map((col) => {
2150
+ const allColumnTasks = board.tasks[col.key] || [];
2151
+ const columnTasks = filterTags.length > 0 ? allColumnTasks.filter((t) => {
2152
+ const taskTags = t.tags || [];
2153
+ return filterTags.some((f) => {
2154
+ if (f === "__other__") return taskTags.some((tag) => !predefinedValues.includes(tag));
2155
+ return taskTags.includes(f);
2156
+ });
2157
+ }) : allColumnTasks;
2158
+ return /* @__PURE__ */ jsx15(
2159
+ KanbanColumn,
2160
+ {
2161
+ column: col,
2162
+ tasks: columnTasks,
2163
+ totalCount: board.columnTotals[col.key] || 0,
2164
+ unreadCount: board.columnUnreads[col.key] || 0,
2165
+ loadingMore: board.loadingMore[col.key] || false,
2166
+ onAddTask: () => setCreateForStatus(col.key),
2167
+ onTaskClick: handleTaskClick,
2168
+ onTaskShare: (taskId, e) => copyShareLink(taskId, board.selectedProject),
2169
+ copiedTaskId,
2170
+ onLoadMore: () => board.loadMoreTasks(col.key)
2171
+ },
2172
+ col.key
2173
+ );
2174
+ }) }) }) })
2175
+ ] }),
2176
+ createForStatus && (renderCreateTask ? renderCreateTask({
2177
+ projectSlug: board.selectedProject,
2178
+ defaultStatus: createForStatus,
2179
+ onClose: handleCreateClose,
2180
+ onCreate: handleCreateDone
2181
+ }) : /* @__PURE__ */ jsx15(
2182
+ CreateTaskModal,
2183
+ {
2184
+ projectSlug: board.selectedProject,
2185
+ defaultStatus: createForStatus,
2186
+ onClose: handleCreateClose,
2187
+ onCreate: handleCreateDone
2188
+ }
2189
+ )),
2190
+ selectedTask && (renderTaskDetail ? renderTaskDetail({
2191
+ task: selectedTask,
2192
+ onClose: handleDetailClose,
2193
+ onUpdate: board.fetchTasks
2194
+ }) : /* @__PURE__ */ jsx15(
2195
+ TaskDetailPanel,
2196
+ {
2197
+ task: selectedTask,
2198
+ projectSlug: board.selectedProject,
2199
+ onClose: handleDetailClose,
2200
+ onUpdate: board.fetchTasks
2201
+ }
2202
+ ))
2203
+ ] });
2204
+ }
2205
+ export {
2206
+ BellIcon,
2207
+ BoardSkeleton,
2208
+ CheckIcon,
2209
+ ChevronDownIcon,
2210
+ CreateTaskModal,
2211
+ DEFAULT_COLUMNS,
2212
+ DEFAULT_PAGE_SIZE,
2213
+ DEFAULT_PRIORITIES,
2214
+ DESCRIPTION_SECTIONS,
2215
+ EMPTY_DESCRIPTION,
2216
+ FeedbackIcon,
2217
+ FilterBar,
2218
+ FilterIcon,
2219
+ KanbanColumn,
2220
+ KanbanIcon,
2221
+ LinkIcon,
2222
+ LockIcon,
2223
+ MentionText,
2224
+ MentionTextarea,
2225
+ MessageSquareIcon,
2226
+ NotificationBell,
2227
+ POSITION_GAP,
2228
+ PREDEFINED_TAGS,
2229
+ PencilIcon,
2230
+ PlusIcon,
2231
+ PriorityBadge,
2232
+ SkeletonCard,
2233
+ SkeletonPulse,
2234
+ TagBadge,
2235
+ TaskBoard,
2236
+ TaskBoardProvider,
2237
+ TaskCard,
2238
+ TaskDetailPanel,
2239
+ TrashIcon,
2240
+ UserAvatar,
2241
+ XIcon,
2242
+ createTaskBoardService,
2243
+ formatDate,
2244
+ formatDateTime,
2245
+ getDescriptionPreview,
2246
+ getInitials,
2247
+ getPriorityStyle,
2248
+ getTagStyle,
2249
+ getUserProjects,
2250
+ hasDescription,
2251
+ toDisplayText,
2252
+ toStoredText,
2253
+ useShareLink,
2254
+ useTaskActions,
2255
+ useTaskBoard,
2256
+ useTaskBoardContext,
2257
+ useTaskDetail
2258
+ };
2259
+ //# sourceMappingURL=index.mjs.map