@emberai-engg/task-board 0.3.5 → 0.4.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 CHANGED
@@ -39,7 +39,7 @@ function createTaskBoardService(apiClient, basePath = "/api/v1/taskboard") {
39
39
  async markTaskRead(taskId) {
40
40
  await apiClient.post(`${basePath}/tasks/${taskId}/read`);
41
41
  },
42
- // ─── Comments ───
42
+ // ─── Comments / Threads ───
43
43
  async listComments(taskId) {
44
44
  const { data } = await apiClient.get(`${basePath}/tasks/${taskId}/comments`);
45
45
  return data;
@@ -58,6 +58,70 @@ function createTaskBoardService(apiClient, basePath = "/api/v1/taskboard") {
58
58
  async deleteComment(taskId, commentId) {
59
59
  await apiClient.delete(`${basePath}/tasks/${taskId}/comments/${commentId}`);
60
60
  },
61
+ async updateThread(taskId, threadId, payload) {
62
+ const { data } = await apiClient.patch(
63
+ `${basePath}/tasks/${taskId}/threads/${threadId}`,
64
+ payload
65
+ );
66
+ return data;
67
+ },
68
+ // ─── Outstanding Questions ───
69
+ async listQuestions(taskId) {
70
+ const { data } = await apiClient.get(`${basePath}/tasks/${taskId}/questions`);
71
+ return data;
72
+ },
73
+ async createQuestion(taskId, payload) {
74
+ const { data } = await apiClient.post(`${basePath}/tasks/${taskId}/questions`, payload);
75
+ return data;
76
+ },
77
+ async updateQuestion(taskId, questionId, payload) {
78
+ const { data } = await apiClient.patch(
79
+ `${basePath}/tasks/${taskId}/questions/${questionId}`,
80
+ payload
81
+ );
82
+ return data;
83
+ },
84
+ async deleteQuestion(taskId, questionId) {
85
+ await apiClient.delete(`${basePath}/tasks/${taskId}/questions/${questionId}`);
86
+ },
87
+ async addQuestionReply(taskId, questionId, payload) {
88
+ const { data } = await apiClient.post(
89
+ `${basePath}/tasks/${taskId}/questions/${questionId}/replies`,
90
+ payload
91
+ );
92
+ return data;
93
+ },
94
+ async deleteQuestionReply(taskId, questionId, replyId) {
95
+ await apiClient.delete(
96
+ `${basePath}/tasks/${taskId}/questions/${questionId}/replies/${replyId}`
97
+ );
98
+ },
99
+ // ─── Attachments ───
100
+ async listAttachments(taskId) {
101
+ const { data } = await apiClient.get(
102
+ `${basePath}/tasks/${taskId}/attachments`
103
+ );
104
+ return data;
105
+ },
106
+ async uploadAttachment(taskId, file) {
107
+ const form = new FormData();
108
+ form.append("file", file);
109
+ const { data } = await apiClient.post(
110
+ `${basePath}/tasks/${taskId}/attachments/upload`,
111
+ form
112
+ );
113
+ return data;
114
+ },
115
+ async addLinkAttachment(taskId, payload) {
116
+ const { data } = await apiClient.post(
117
+ `${basePath}/tasks/${taskId}/attachments/link`,
118
+ payload
119
+ );
120
+ return data;
121
+ },
122
+ async deleteAttachment(taskId, attachmentId) {
123
+ await apiClient.delete(`${basePath}/tasks/${taskId}/attachments/${attachmentId}`);
124
+ },
61
125
  // ─── Projects ───
62
126
  async listProjects() {
63
127
  const { data } = await apiClient.get(`${basePath}/projects`);
@@ -94,20 +158,20 @@ function createTaskBoardService(apiClient, basePath = "/api/v1/taskboard") {
94
158
 
95
159
  // src/utils/constants.ts
96
160
  var DEFAULT_COLUMNS = [
97
- { key: "backlog", label: "Backlog", color: "bg-neutral-400", description: "Tasks not yet scheduled" },
98
- { key: "blocked", label: "Blocked", color: "bg-red-500", description: "Waiting on a dependency" },
99
- { key: "queued", label: "Queued", color: "bg-blue-500", description: "Scheduled for this sprint" },
100
- { key: "in_progress", label: "In Progress", color: "bg-amber-500", description: "Actively being worked on" },
101
- { key: "in_testing", label: "In Testing", color: "bg-teal-500", description: "Under QA and validation" },
102
- { key: "client_review", label: "Client Review", color: "bg-purple-500", description: "Live & ready for client review" },
103
- { key: "changes_requested", label: "Changes Requested", color: "bg-orange-500", description: "Revisions needed from review" },
104
- { key: "approved", label: "Approved", color: "bg-green-500", description: "Signed off and complete" }
161
+ { key: "backlog", label: "Backlog", color: "bg-neutral-400", chip: "bg-neutral-100 text-neutral-700", description: "Tasks not yet scheduled" },
162
+ { key: "blocked", label: "Blocked", color: "bg-red-500", chip: "bg-red-50 text-red-700", description: "Waiting on a dependency" },
163
+ { key: "queued", label: "Queued", color: "bg-[#FF5E00]", chip: "bg-[#FF5E00]/10 text-[#FF5E00]", description: "Scheduled for this sprint" },
164
+ { key: "in_progress", label: "In Progress", color: "bg-amber-500", chip: "bg-amber-50 text-amber-700", description: "Actively being worked on" },
165
+ { key: "in_testing", label: "In Testing", color: "bg-teal-500", chip: "bg-teal-50 text-teal-700", description: "Under QA and validation" },
166
+ { key: "client_review", label: "Client Review", color: "bg-purple-500", chip: "bg-purple-50 text-purple-700", description: "Live & ready for client review" },
167
+ { key: "changes_requested", label: "Changes Requested", color: "bg-orange-500", chip: "bg-orange-50 text-orange-700", description: "Revisions needed from review" },
168
+ { key: "approved", label: "Approved", color: "bg-green-500", chip: "bg-green-50 text-green-700", description: "Signed off and complete" }
105
169
  ];
106
170
  var DEFAULT_PRIORITIES = [
107
- { value: "urgent", label: "Critical", className: "bg-red-50 text-red-600 border-red-200" },
108
- { value: "high", label: "High", className: "bg-orange-50 text-orange-600 border-orange-200" },
109
- { value: "medium", label: "Medium", className: "bg-amber-50 text-amber-600 border-amber-200" },
110
- { value: "low", label: "Low", className: "bg-neutral-100 text-neutral-500 border-neutral-200" }
171
+ { value: "urgent", label: "Critical", className: "bg-red-50 text-red-600 border-red-200", dot: "bg-red-500" },
172
+ { value: "high", label: "High", className: "bg-orange-50 text-orange-600 border-orange-200", dot: "bg-orange-500" },
173
+ { value: "medium", label: "Medium", className: "bg-amber-50 text-amber-600 border-amber-200", dot: "bg-amber-500" },
174
+ { value: "low", label: "Low", className: "bg-neutral-100 text-neutral-500 border-neutral-200", dot: "bg-neutral-300" }
111
175
  ];
112
176
  var PREDEFINED_TAGS = [
113
177
  { value: "traceability", label: "Traceability", className: "bg-blue-50 text-blue-600 border-blue-200" },
@@ -118,11 +182,10 @@ var PREDEFINED_TAGS = [
118
182
  { value: "bug-fix", label: "Bug Fix", className: "bg-red-50 text-red-600 border-red-200" }
119
183
  ];
120
184
  var DESCRIPTION_SECTIONS = [
121
- { key: "problem", label: "Problem" },
122
- { key: "user_story", label: "User Story" },
123
- { key: "proposed_behavior", label: "Proposed Behavior" },
124
- { key: "acceptance_criteria", label: "Acceptance Criteria" },
125
- { key: "open_questions", label: "Open Questions" }
185
+ { key: "problem", label: "Problem", placeholder: "Describe the problem..." },
186
+ { key: "user_story", label: "User Story", placeholder: "As a ___, I want ___, so that ___..." },
187
+ { key: "proposed_behavior", label: "Proposed Behavior", placeholder: "Describe the proposed behavior..." },
188
+ { key: "acceptance_criteria", label: "Acceptance Criteria", placeholder: "What needs to be true for this to be considered done?" }
126
189
  ];
127
190
  var EMPTY_DESCRIPTION = {
128
191
  problem: "",
@@ -134,6 +197,7 @@ var EMPTY_DESCRIPTION = {
134
197
  var POSITION_GAP = 1e3;
135
198
  var DEFAULT_PAGE_SIZE = 10;
136
199
  var NOTIFICATION_POLL_INTERVAL = 3e4;
200
+ var DEFAULT_INTERNAL_LABEL = "Internal";
137
201
 
138
202
  // src/context/TaskBoardProvider.tsx
139
203
  import { jsx } from "react/jsx-runtime";
@@ -175,6 +239,7 @@ function TaskBoardProvider({
175
239
  columns: config.columns ?? DEFAULT_COLUMNS,
176
240
  priorities: config.priorities ?? DEFAULT_PRIORITIES,
177
241
  tags: config.tags ?? PREDEFINED_TAGS,
242
+ internalLabel: config.internalLabel ?? "Internal",
178
243
  config,
179
244
  features
180
245
  }),
@@ -297,6 +362,7 @@ function useTaskBoard(isDragging) {
297
362
  tasks,
298
363
  setTasks,
299
364
  columnTotals,
365
+ setColumnTotals,
300
366
  columnUnreads,
301
367
  setColumnUnreads,
302
368
  boardLoading,
@@ -312,7 +378,7 @@ function useTaskBoard(isDragging) {
312
378
 
313
379
  // src/hooks/useTaskActions.ts
314
380
  import { useCallback as useCallback2, useRef as useRef2 } from "react";
315
- function useTaskActions(tasks, setTasks, fetchTasks, isDragging) {
381
+ function useTaskActions(tasks, setTasks, fetchTasks, isDragging, setColumnTotals) {
316
382
  const { service, config } = useTaskBoardContext();
317
383
  const internalDragging = useRef2(false);
318
384
  const draggingRef = isDragging ?? internalDragging;
@@ -366,6 +432,13 @@ function useTaskActions(tasks, setTasks, fetchTasks, isDragging) {
366
432
  }
367
433
  return newTasks;
368
434
  });
435
+ if (sourceStatus !== destStatus && setColumnTotals) {
436
+ setColumnTotals((prev) => ({
437
+ ...prev,
438
+ [sourceStatus]: Math.max(0, (prev[sourceStatus] || 0) - 1),
439
+ [destStatus]: (prev[destStatus] || 0) + 1
440
+ }));
441
+ }
369
442
  try {
370
443
  await service.updateTask(taskId, { status: destStatus, position: newPosition });
371
444
  } catch {
@@ -373,7 +446,7 @@ function useTaskActions(tasks, setTasks, fetchTasks, isDragging) {
373
446
  } finally {
374
447
  draggingRef.current = false;
375
448
  }
376
- }, [setTasks, service, fetchTasks]);
449
+ }, [setTasks, setColumnTotals, service, fetchTasks]);
377
450
  return { createTask, updateTask, deleteTask, markTaskRead, moveTask };
378
451
  }
379
452
 
@@ -445,27 +518,27 @@ function getInitials(name) {
445
518
  }
446
519
  function parseDate(dateStr) {
447
520
  if (!dateStr) return /* @__PURE__ */ new Date();
448
- const d = new Date(dateStr);
449
- if (isNaN(d.getTime()) && !dateStr.endsWith("Z") && !dateStr.includes("+")) {
521
+ if (!dateStr.endsWith("Z") && !dateStr.includes("+") && !/\d{2}:\d{2}$/.test(dateStr.slice(-6))) {
450
522
  return /* @__PURE__ */ new Date(dateStr + "Z");
451
523
  }
452
- return d;
524
+ return new Date(dateStr);
453
525
  }
454
526
  function formatDate(dateStr) {
455
527
  if (!dateStr) return "";
456
- const d = parseDate(dateStr);
457
- return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
528
+ return parseDate(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric" });
458
529
  }
459
530
  function formatDateTime(dateStr) {
460
531
  if (!dateStr) return "";
461
- const d = parseDate(dateStr);
462
- return d.toLocaleDateString("en-US", {
532
+ return parseDate(dateStr).toLocaleString("en-US", {
463
533
  month: "short",
464
534
  day: "numeric",
465
535
  hour: "numeric",
466
536
  minute: "2-digit"
467
537
  });
468
538
  }
539
+ function formatTaskId(id) {
540
+ return `T-${id.slice(-6).toUpperCase()}`;
541
+ }
469
542
  function stripMentionMarkup(text) {
470
543
  return text.replace(/@\[(.*?)\]\(.*?\)/g, "@$1");
471
544
  }
@@ -476,12 +549,14 @@ function getDescriptionPreview(desc) {
476
549
  const val = desc[section.key]?.trim();
477
550
  if (val) return stripMentionMarkup(val);
478
551
  }
552
+ if (desc.open_questions?.trim()) return stripMentionMarkup(desc.open_questions.trim());
479
553
  return "";
480
554
  }
481
555
  function hasDescription(desc) {
482
556
  if (!desc) return false;
483
557
  if (typeof desc === "string") return desc.trim().length > 0;
484
- return DESCRIPTION_SECTIONS.some((s) => desc[s.key]?.trim());
558
+ if (DESCRIPTION_SECTIONS.some((s) => desc[s.key]?.trim())) return true;
559
+ return !!desc.open_questions?.trim();
485
560
  }
486
561
  function getUserProjects(apps, allProjects) {
487
562
  if (apps.includes("all")) return allProjects;
@@ -507,6 +582,24 @@ var XIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */
507
582
  /* @__PURE__ */ jsx4("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
508
583
  ] });
509
584
  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" }) });
585
+ var ChevronLeftIcon = ({ 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: "15 18 9 12 15 6" }) });
586
+ var ChevronRightIcon = ({ 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: "9 18 15 12 9 6" }) });
587
+ var ArrowLeftIcon = ({ 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: [
588
+ /* @__PURE__ */ jsx4("line", { x1: "19", y1: "12", x2: "5", y2: "12" }),
589
+ /* @__PURE__ */ jsx4("polyline", { points: "12 19 5 12 12 5" })
590
+ ] });
591
+ var MoreVerticalIcon = ({ 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: [
592
+ /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "1" }),
593
+ /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "5", r: "1" }),
594
+ /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "19", r: "1" })
595
+ ] });
596
+ var Share2Icon = ({ 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: [
597
+ /* @__PURE__ */ jsx4("circle", { cx: "18", cy: "5", r: "3" }),
598
+ /* @__PURE__ */ jsx4("circle", { cx: "6", cy: "12", r: "3" }),
599
+ /* @__PURE__ */ jsx4("circle", { cx: "18", cy: "19", r: "3" }),
600
+ /* @__PURE__ */ jsx4("line", { x1: "8.59", y1: "13.51", x2: "15.42", y2: "17.49" }),
601
+ /* @__PURE__ */ jsx4("line", { x1: "15.41", y1: "6.51", x2: "8.59", y2: "10.49" })
602
+ ] });
510
603
  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" }) });
511
604
  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: [
512
605
  /* @__PURE__ */ jsx4("rect", { width: "8", height: "4", x: "8", y: "2", rx: "1", ry: "1" }),
@@ -517,7 +610,33 @@ var LinkIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__
517
610
  /* @__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" }),
518
611
  /* @__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" })
519
612
  ] });
613
+ var Link2Icon = ({ 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: [
614
+ /* @__PURE__ */ jsx4("path", { d: "M9 17H7A5 5 0 0 1 7 7h2" }),
615
+ /* @__PURE__ */ jsx4("path", { d: "M15 7h2a5 5 0 1 1 0 10h-2" }),
616
+ /* @__PURE__ */ jsx4("line", { x1: "8", y1: "12", x2: "16", y2: "12" })
617
+ ] });
618
+ var ExternalLinkIcon = ({ 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: [
619
+ /* @__PURE__ */ jsx4("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
620
+ /* @__PURE__ */ jsx4("polyline", { points: "15 3 21 3 21 9" }),
621
+ /* @__PURE__ */ jsx4("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
622
+ ] });
623
+ var ImageIcon = ({ 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: [
624
+ /* @__PURE__ */ jsx4("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }),
625
+ /* @__PURE__ */ jsx4("circle", { cx: "9", cy: "9", r: "2" }),
626
+ /* @__PURE__ */ jsx4("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
627
+ ] });
628
+ var FileTextIcon = ({ 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: [
629
+ /* @__PURE__ */ jsx4("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
630
+ /* @__PURE__ */ jsx4("polyline", { points: "14 2 14 8 20 8" }),
631
+ /* @__PURE__ */ jsx4("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
632
+ /* @__PURE__ */ jsx4("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
633
+ /* @__PURE__ */ jsx4("line", { x1: "10", y1: "9", x2: "8", y2: "9" })
634
+ ] });
520
635
  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" }) });
636
+ var CheckCircle2Icon = ({ 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: [
637
+ /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "10" }),
638
+ /* @__PURE__ */ jsx4("polyline", { points: "9 11 12 14 15 11" })
639
+ ] });
521
640
  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" }) });
522
641
  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" }) });
523
642
  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" }) });
@@ -536,6 +655,73 @@ var FeedbackIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PUR
536
655
  /* @__PURE__ */ jsx4("path", { d: "M12 12h.01" }),
537
656
  /* @__PURE__ */ jsx4("path", { d: "M16 12h.01" })
538
657
  ] });
658
+ var HelpCircleIcon = ({ 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: [
659
+ /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "10" }),
660
+ /* @__PURE__ */ jsx4("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
661
+ /* @__PURE__ */ jsx4("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
662
+ ] });
663
+ var CornerUpLeftIcon = ({ 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: [
664
+ /* @__PURE__ */ jsx4("polyline", { points: "9 14 4 9 9 4" }),
665
+ /* @__PURE__ */ jsx4("path", { d: "M20 20v-7a4 4 0 0 0-4-4H4" })
666
+ ] });
667
+ var RotateCcwIcon = ({ 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: [
668
+ /* @__PURE__ */ jsx4("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
669
+ /* @__PURE__ */ jsx4("path", { d: "M3 3v5h5" })
670
+ ] });
671
+ var Bold = ({ 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: [
672
+ /* @__PURE__ */ jsx4("path", { d: "M14 12a4 4 0 0 0 0-8H6v8" }),
673
+ /* @__PURE__ */ jsx4("path", { d: "M15 20a4 4 0 0 0 0-8H6v8Z" })
674
+ ] });
675
+ var Italic = ({ 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: [
676
+ /* @__PURE__ */ jsx4("line", { x1: "19", y1: "4", x2: "10", y2: "4" }),
677
+ /* @__PURE__ */ jsx4("line", { x1: "14", y1: "20", x2: "5", y2: "20" }),
678
+ /* @__PURE__ */ jsx4("line", { x1: "15", y1: "4", x2: "9", y2: "20" })
679
+ ] });
680
+ var List = ({ 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: [
681
+ /* @__PURE__ */ jsx4("line", { x1: "8", y1: "6", x2: "21", y2: "6" }),
682
+ /* @__PURE__ */ jsx4("line", { x1: "8", y1: "12", x2: "21", y2: "12" }),
683
+ /* @__PURE__ */ jsx4("line", { x1: "8", y1: "18", x2: "21", y2: "18" }),
684
+ /* @__PURE__ */ jsx4("line", { x1: "3", y1: "6", x2: "3.01", y2: "6" }),
685
+ /* @__PURE__ */ jsx4("line", { x1: "3", y1: "12", x2: "3.01", y2: "12" }),
686
+ /* @__PURE__ */ jsx4("line", { x1: "3", y1: "18", x2: "3.01", y2: "18" })
687
+ ] });
688
+ var ListOrdered = ({ 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: [
689
+ /* @__PURE__ */ jsx4("line", { x1: "10", y1: "6", x2: "21", y2: "6" }),
690
+ /* @__PURE__ */ jsx4("line", { x1: "10", y1: "12", x2: "21", y2: "12" }),
691
+ /* @__PURE__ */ jsx4("line", { x1: "10", y1: "18", x2: "21", y2: "18" }),
692
+ /* @__PURE__ */ jsx4("path", { d: "M4 6h1v4" }),
693
+ /* @__PURE__ */ jsx4("path", { d: "M4 10h2" }),
694
+ /* @__PURE__ */ jsx4("path", { d: "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" })
695
+ ] });
696
+ var Heading2 = ({ 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: [
697
+ /* @__PURE__ */ jsx4("path", { d: "M4 12h8" }),
698
+ /* @__PURE__ */ jsx4("path", { d: "M4 18V6" }),
699
+ /* @__PURE__ */ jsx4("path", { d: "M12 18V6" }),
700
+ /* @__PURE__ */ jsx4("path", { d: "M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1" })
701
+ ] });
702
+ var Quote = ({ 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: [
703
+ /* @__PURE__ */ jsx4("path", { d: "M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z" }),
704
+ /* @__PURE__ */ jsx4("path", { d: "M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z" })
705
+ ] });
706
+ var Code = ({ 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: [
707
+ /* @__PURE__ */ jsx4("polyline", { points: "16 18 22 12 16 6" }),
708
+ /* @__PURE__ */ jsx4("polyline", { points: "8 6 2 12 8 18" })
709
+ ] });
710
+ var ChatDotsIcon = ({ 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: [
711
+ /* @__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" }),
712
+ /* @__PURE__ */ jsx4("line", { x1: "8", y1: "10", x2: "8.01", y2: "10" }),
713
+ /* @__PURE__ */ jsx4("line", { x1: "12", y1: "10", x2: "12.01", y2: "10" }),
714
+ /* @__PURE__ */ jsx4("line", { x1: "16", y1: "10", x2: "16.01", y2: "10" })
715
+ ] });
716
+ var HistoryIcon = ({ 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: [
717
+ /* @__PURE__ */ jsx4("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
718
+ /* @__PURE__ */ jsx4("polyline", { points: "3 3 3 8 8 8" }),
719
+ /* @__PURE__ */ jsx4("polyline", { points: "12 7 12 12 15 14" })
720
+ ] });
721
+ var SidebarToggleIcon = ({ 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: [
722
+ /* @__PURE__ */ jsx4("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
723
+ /* @__PURE__ */ jsx4("line", { x1: "9", y1: "3", x2: "9", y2: "21" })
724
+ ] });
539
725
 
540
726
  // src/components/TagBadge.tsx
541
727
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -619,18 +805,20 @@ var TaskCard = memo(function TaskCard2({ task, index, onClick, onShare, copied }
619
805
  import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
620
806
  function LoadMoreSentinel({ loading, onLoadMore, remaining }) {
621
807
  const sentinelRef = useRef3(null);
808
+ const onLoadMoreRef = useRef3(onLoadMore);
809
+ onLoadMoreRef.current = onLoadMore;
622
810
  useEffect2(() => {
623
811
  const el = sentinelRef.current;
624
812
  if (!el) return;
625
813
  const observer = new IntersectionObserver(
626
814
  ([entry]) => {
627
- if (entry.isIntersecting && !loading) onLoadMore();
815
+ if (entry.isIntersecting && !loading) onLoadMoreRef.current();
628
816
  },
629
817
  { threshold: 0.1 }
630
818
  );
631
819
  observer.observe(el);
632
820
  return () => observer.disconnect();
633
- }, [loading, onLoadMore]);
821
+ }, [loading]);
634
822
  const skeletonCount = loading ? Math.min(remaining, 10) : 0;
635
823
  return /* @__PURE__ */ jsx8("div", { ref: sentinelRef, className: "space-y-2 pt-2", children: Array.from({ length: skeletonCount }).map((_, i) => /* @__PURE__ */ jsx8(SkeletonCard, {}, i)) });
636
824
  }
@@ -2025,7 +2213,7 @@ function TaskBoard({
2025
2213
  const { columns, features, service } = useTaskBoardContext();
2026
2214
  const isDraggingRef = useRef8(false);
2027
2215
  const board = useTaskBoard(isDraggingRef);
2028
- const actions = useTaskActions(board.tasks, board.setTasks, board.fetchTasks, isDraggingRef);
2216
+ const actions = useTaskActions(board.tasks, board.setTasks, board.fetchTasks, isDraggingRef, board.setColumnTotals);
2029
2217
  const { copiedTaskId, copyShareLink } = useShareLink();
2030
2218
  const [selectedTask, setSelectedTask] = useState9(null);
2031
2219
  const [createForStatus, setCreateForStatus] = useState9("");
@@ -2043,9 +2231,13 @@ function TaskBoard({
2043
2231
  try {
2044
2232
  const task = await service.getTask(taskId);
2045
2233
  if (cancelled) return;
2046
- setSelectedTask(task);
2047
- service.markTaskRead(taskId).catch(() => {
2048
- });
2234
+ if (onTaskOpen) {
2235
+ onTaskOpen(task);
2236
+ } else {
2237
+ setSelectedTask(task);
2238
+ service.markTaskRead(taskId).catch(() => {
2239
+ });
2240
+ }
2049
2241
  const url = new URL(window.location.href);
2050
2242
  url.searchParams.delete("task");
2051
2243
  window.history.replaceState({}, "", url.toString());
@@ -2056,7 +2248,7 @@ function TaskBoard({
2056
2248
  return () => {
2057
2249
  cancelled = true;
2058
2250
  };
2059
- }, [board.selectedProject, board.boardLoading, sharedTaskHandled, service]);
2251
+ }, [board.selectedProject, board.boardLoading, sharedTaskHandled, service, onTaskOpen]);
2060
2252
  useEffect8(() => {
2061
2253
  if (typeof window === "undefined") return;
2062
2254
  if (board.selectedProject && board.projects.length > 1) {
@@ -2078,7 +2270,9 @@ function TaskBoard({
2078
2270
  );
2079
2271
  }, [actions]);
2080
2272
  const handleTaskClick = (task) => {
2081
- setSelectedTask(task);
2273
+ if (!onTaskOpen) {
2274
+ setSelectedTask(task);
2275
+ }
2082
2276
  onTaskOpen?.(task);
2083
2277
  actions.markTaskRead(task.id);
2084
2278
  if (task.has_unread) {
@@ -2104,9 +2298,13 @@ function TaskBoard({
2104
2298
  }
2105
2299
  try {
2106
2300
  const task = await service.getTask(taskId);
2107
- setSelectedTask(task);
2108
- service.markTaskRead(taskId).catch(() => {
2109
- });
2301
+ if (onTaskOpen) {
2302
+ onTaskOpen(task);
2303
+ } else {
2304
+ setSelectedTask(task);
2305
+ service.markTaskRead(taskId).catch(() => {
2306
+ });
2307
+ }
2110
2308
  } catch {
2111
2309
  board.setError("Could not open task.");
2112
2310
  }
@@ -2227,33 +2425,3195 @@ function TaskBoard({
2227
2425
  ))
2228
2426
  ] });
2229
2427
  }
2428
+
2429
+ // src/components/TaskDetailView.tsx
2430
+ import { useCallback as useCallback11, useEffect as useEffect17, useMemo as useMemo3, useRef as useRef15, useState as useState19 } from "react";
2431
+
2432
+ // src/utils/threads.ts
2433
+ function deriveThreads(comments, attachments) {
2434
+ const attachById = new Map(attachments.map((a) => [a.id, a]));
2435
+ const topLevel = comments.filter((c) => !c.parent_id);
2436
+ const repliesByParent = /* @__PURE__ */ new Map();
2437
+ for (const c of comments) {
2438
+ if (c.parent_id) {
2439
+ const arr = repliesByParent.get(c.parent_id) || [];
2440
+ arr.push(c);
2441
+ repliesByParent.set(c.parent_id, arr);
2442
+ }
2443
+ }
2444
+ return topLevel.map((c) => {
2445
+ const stripped = c.content.replace(/@\[(.*?)\]\(.*?\)/g, "@$1");
2446
+ const lines = stripped.split(/\r?\n/).filter((l) => l.trim());
2447
+ let title = (c.title || "").trim();
2448
+ let titleDerived = false;
2449
+ if (!title) {
2450
+ const firstLine = lines[0] || "";
2451
+ title = firstLine.length > 80 ? firstLine.slice(0, 80) + "\u2026" : firstLine;
2452
+ if (!title) title = "Comment";
2453
+ titleDerived = true;
2454
+ }
2455
+ const preview = lines.join(" ").slice(0, 200);
2456
+ const replies = (repliesByParent.get(c.id) || []).sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()).map((r) => ({
2457
+ id: r.id,
2458
+ author_id: r.author_id,
2459
+ author_name: r.author_name,
2460
+ content: r.content,
2461
+ created_at: r.created_at,
2462
+ is_internal: !!r.is_internal
2463
+ }));
2464
+ const threadAttachments = (c.attachment_ids || []).map((aid) => attachById.get(aid)).filter((a) => Boolean(a));
2465
+ return {
2466
+ id: c.id,
2467
+ title,
2468
+ titleDerived,
2469
+ preview,
2470
+ author_id: c.author_id,
2471
+ author_name: c.author_name,
2472
+ created_at: c.created_at,
2473
+ is_internal: !!c.is_internal,
2474
+ status: c.thread_status === "complete" ? "complete" : "active",
2475
+ rawContent: c.content,
2476
+ anchor: c.anchor || null,
2477
+ attachments: threadAttachments,
2478
+ replies
2479
+ };
2480
+ });
2481
+ }
2482
+ function sectionLabel(key) {
2483
+ return DESCRIPTION_SECTIONS.find((s) => s.key === key)?.label || key;
2484
+ }
2485
+ function timeAgo(dateStr) {
2486
+ const d = parseDate(dateStr);
2487
+ const diff = Date.now() - d.getTime();
2488
+ const minutes = Math.floor(diff / 6e4);
2489
+ if (minutes < 1) return "just now";
2490
+ if (minutes < 60) return `${minutes}m ago`;
2491
+ const hours = Math.floor(minutes / 60);
2492
+ if (hours < 24) return `${hours}h ago`;
2493
+ const days = Math.floor(hours / 24);
2494
+ if (days < 30) return `${days}d ago`;
2495
+ return formatDate(dateStr);
2496
+ }
2497
+
2498
+ // src/components/DescriptionSection.tsx
2499
+ import { useState as useState10 } from "react";
2500
+
2501
+ // src/components/MarkdownView.tsx
2502
+ import React10 from "react";
2503
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2504
+ function MarkdownView({ content, className = "" }) {
2505
+ if (!content?.trim()) return null;
2506
+ const renderInline = (text, keyBase) => {
2507
+ const out = [];
2508
+ const parts = text.split(/(@\[.*?\]\(.*?\))/g);
2509
+ let k = 0;
2510
+ for (const part of parts) {
2511
+ const m = part.match(/^@\[(.*?)\]\((.*?)\)$/);
2512
+ if (m) {
2513
+ out.push(
2514
+ /* @__PURE__ */ jsxs14(
2515
+ "span",
2516
+ {
2517
+ 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",
2518
+ title: `@${m[2]}`,
2519
+ children: [
2520
+ "@",
2521
+ m[1]
2522
+ ]
2523
+ },
2524
+ `${keyBase}-m${k++}`
2525
+ )
2526
+ );
2527
+ continue;
2528
+ }
2529
+ const segs = part.split(/(\*\*.*?\*\*|`[^`]+`)/g);
2530
+ for (const seg of segs) {
2531
+ if (!seg) continue;
2532
+ const bold = seg.match(/^\*\*(.+?)\*\*$/);
2533
+ const code = seg.match(/^`(.+?)`$/);
2534
+ if (bold) {
2535
+ out.push(
2536
+ /* @__PURE__ */ jsx16("strong", { className: "font-semibold text-neutral-900", children: bold[1] }, `${keyBase}-b${k++}`)
2537
+ );
2538
+ } else if (code) {
2539
+ out.push(
2540
+ /* @__PURE__ */ jsx16(
2541
+ "code",
2542
+ {
2543
+ className: "px-1 py-px rounded bg-neutral-100 text-neutral-800 text-[12px] font-mono",
2544
+ children: code[1]
2545
+ },
2546
+ `${keyBase}-c${k++}`
2547
+ )
2548
+ );
2549
+ } else {
2550
+ out.push(/* @__PURE__ */ jsx16(React10.Fragment, { children: seg }, `${keyBase}-t${k++}`));
2551
+ }
2552
+ }
2553
+ }
2554
+ return out;
2555
+ };
2556
+ const lines = content.split(/\r?\n/);
2557
+ const blocks = [];
2558
+ let i = 0;
2559
+ let blockKey = 0;
2560
+ while (i < lines.length) {
2561
+ const raw = lines[i];
2562
+ const line = raw.trimEnd();
2563
+ const trimmed = line.trim();
2564
+ if (!trimmed) {
2565
+ i++;
2566
+ continue;
2567
+ }
2568
+ if (/^### /.test(trimmed)) {
2569
+ blocks.push(
2570
+ /* @__PURE__ */ jsx16("h3", { className: "text-[15px] font-semibold text-neutral-900 mt-3 mb-1.5", children: renderInline(trimmed.replace(/^### /, ""), `b${blockKey}`) }, blockKey++)
2571
+ );
2572
+ i++;
2573
+ continue;
2574
+ }
2575
+ if (/^## /.test(trimmed)) {
2576
+ blocks.push(
2577
+ /* @__PURE__ */ jsx16("h3", { className: "text-base font-semibold text-neutral-900 mt-4 mb-1.5", children: renderInline(trimmed.replace(/^## /, ""), `b${blockKey}`) }, blockKey++)
2578
+ );
2579
+ i++;
2580
+ continue;
2581
+ }
2582
+ if (/^# /.test(trimmed)) {
2583
+ blocks.push(
2584
+ /* @__PURE__ */ jsx16("h2", { className: "text-lg font-semibold text-neutral-900 mt-4 mb-2", children: renderInline(trimmed.replace(/^# /, ""), `b${blockKey}`) }, blockKey++)
2585
+ );
2586
+ i++;
2587
+ continue;
2588
+ }
2589
+ if (/^> /.test(trimmed)) {
2590
+ const quoteLines = [];
2591
+ while (i < lines.length && /^> /.test(lines[i].trim())) {
2592
+ quoteLines.push(lines[i].trim().replace(/^> /, ""));
2593
+ i++;
2594
+ }
2595
+ blocks.push(
2596
+ /* @__PURE__ */ jsx16(
2597
+ "blockquote",
2598
+ {
2599
+ className: "border-l-2 border-neutral-200 pl-3 my-2 text-neutral-600 italic",
2600
+ children: quoteLines.map((q, j) => /* @__PURE__ */ jsx16("p", { children: renderInline(q, `bq${blockKey}-${j}`) }, j))
2601
+ },
2602
+ blockKey++
2603
+ )
2604
+ );
2605
+ continue;
2606
+ }
2607
+ if (/^[-*] \[[ xX]\] /.test(trimmed)) {
2608
+ const taskItems = [];
2609
+ while (i < lines.length && /^[-*] \[[ xX]\] /.test(lines[i].trim())) {
2610
+ const m = lines[i].trim().match(/^[-*] \[([ xX])\] (.*)$/);
2611
+ if (!m) break;
2612
+ taskItems.push({ checked: m[1].toLowerCase() === "x", text: m[2] });
2613
+ i++;
2614
+ }
2615
+ blocks.push(
2616
+ /* @__PURE__ */ jsx16("ul", { className: "my-1.5 space-y-1", children: taskItems.map((t, j) => /* @__PURE__ */ jsxs14("li", { className: "flex items-start gap-2", children: [
2617
+ /* @__PURE__ */ jsx16(
2618
+ "input",
2619
+ {
2620
+ type: "checkbox",
2621
+ checked: t.checked,
2622
+ readOnly: true,
2623
+ className: "mt-1 accent-[#FF5E00]"
2624
+ }
2625
+ ),
2626
+ /* @__PURE__ */ jsx16("span", { className: t.checked ? "line-through text-neutral-400" : "text-neutral-700", children: renderInline(t.text, `t${blockKey}-${j}`) })
2627
+ ] }, j)) }, blockKey++)
2628
+ );
2629
+ continue;
2630
+ }
2631
+ if (/^[-*] /.test(trimmed)) {
2632
+ const items = [];
2633
+ while (i < lines.length && /^[-*] /.test(lines[i].trim())) {
2634
+ items.push(lines[i].trim().replace(/^[-*] /, ""));
2635
+ i++;
2636
+ }
2637
+ blocks.push(
2638
+ /* @__PURE__ */ jsx16("ul", { className: "list-disc pl-5 my-1.5 space-y-0.5", children: items.map((it, j) => /* @__PURE__ */ jsx16("li", { className: "text-neutral-700 leading-relaxed", children: renderInline(it, `u${blockKey}-${j}`) }, j)) }, blockKey++)
2639
+ );
2640
+ continue;
2641
+ }
2642
+ if (/^\d+\. /.test(trimmed)) {
2643
+ const items = [];
2644
+ while (i < lines.length && /^\d+\. /.test(lines[i].trim())) {
2645
+ items.push(lines[i].trim().replace(/^\d+\. /, ""));
2646
+ i++;
2647
+ }
2648
+ blocks.push(
2649
+ /* @__PURE__ */ jsx16("ol", { className: "list-decimal pl-5 my-1.5 space-y-0.5", children: items.map((it, j) => /* @__PURE__ */ jsx16("li", { className: "text-neutral-700 leading-relaxed", children: renderInline(it, `o${blockKey}-${j}`) }, j)) }, blockKey++)
2650
+ );
2651
+ continue;
2652
+ }
2653
+ const paraLines = [trimmed];
2654
+ i++;
2655
+ while (i < lines.length && lines[i].trim() && !/^(#{1,3} |> |[-*] (\[[ xX]\] )?|\d+\. )/.test(lines[i].trim())) {
2656
+ paraLines.push(lines[i].trim());
2657
+ i++;
2658
+ }
2659
+ blocks.push(
2660
+ /* @__PURE__ */ jsx16("p", { className: "text-neutral-700 leading-relaxed my-1.5", children: renderInline(paraLines.join(" "), `p${blockKey}`) }, blockKey++)
2661
+ );
2662
+ }
2663
+ return /* @__PURE__ */ jsx16("div", { className, children: blocks });
2664
+ }
2665
+
2666
+ // src/components/MarkdownEditor.tsx
2667
+ import { useEffect as useEffect9, useRef as useRef9 } from "react";
2668
+
2669
+ // src/utils/markdown.ts
2670
+ function escapeHtml(s) {
2671
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2672
+ }
2673
+ function inlineMdToHtml(text) {
2674
+ const tokens = text.split(/(@\[.*?\]\([^)]*?\))/g);
2675
+ return tokens.map((tok) => {
2676
+ const m = tok.match(/^@\[(.*?)\]\(([^)]*?)\)$/);
2677
+ if (m) {
2678
+ return `<span class="mention-pill" data-mention-username="${escapeHtml(m[2])}">@${escapeHtml(m[1])}</span>`;
2679
+ }
2680
+ let s = escapeHtml(tok);
2681
+ s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
2682
+ s = s.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
2683
+ s = s.replace(/(^|[^*])\*([^*\n]+?)\*(?!\*)/g, "$1<em>$2</em>");
2684
+ return s;
2685
+ }).join("");
2686
+ }
2687
+ function mdToHtml(md) {
2688
+ if (!md.trim()) return "";
2689
+ const lines = md.split(/\r?\n/);
2690
+ const out = [];
2691
+ let i = 0;
2692
+ while (i < lines.length) {
2693
+ const trimmed = lines[i].trim();
2694
+ if (!trimmed) {
2695
+ i++;
2696
+ continue;
2697
+ }
2698
+ if (/^### /.test(trimmed)) {
2699
+ out.push(`<h3>${inlineMdToHtml(trimmed.slice(4))}</h3>`);
2700
+ i++;
2701
+ continue;
2702
+ }
2703
+ if (/^## /.test(trimmed)) {
2704
+ out.push(`<h2>${inlineMdToHtml(trimmed.slice(3))}</h2>`);
2705
+ i++;
2706
+ continue;
2707
+ }
2708
+ if (/^# /.test(trimmed)) {
2709
+ out.push(`<h1>${inlineMdToHtml(trimmed.slice(2))}</h1>`);
2710
+ i++;
2711
+ continue;
2712
+ }
2713
+ if (/^[-*] /.test(trimmed)) {
2714
+ out.push("<ul>");
2715
+ while (i < lines.length && /^[-*] /.test(lines[i].trim())) {
2716
+ const text = lines[i].trim().replace(/^[-*] /, "");
2717
+ out.push(`<li>${inlineMdToHtml(text)}</li>`);
2718
+ i++;
2719
+ }
2720
+ out.push("</ul>");
2721
+ continue;
2722
+ }
2723
+ if (/^\d+\. /.test(trimmed)) {
2724
+ out.push("<ol>");
2725
+ while (i < lines.length && /^\d+\. /.test(lines[i].trim())) {
2726
+ const text = lines[i].trim().replace(/^\d+\. /, "");
2727
+ out.push(`<li>${inlineMdToHtml(text)}</li>`);
2728
+ i++;
2729
+ }
2730
+ out.push("</ol>");
2731
+ continue;
2732
+ }
2733
+ if (/^> /.test(trimmed)) {
2734
+ out.push("<blockquote>");
2735
+ while (i < lines.length && /^> /.test(lines[i].trim())) {
2736
+ out.push(`<p>${inlineMdToHtml(lines[i].trim().slice(2))}</p>`);
2737
+ i++;
2738
+ }
2739
+ out.push("</blockquote>");
2740
+ continue;
2741
+ }
2742
+ const paraLines = [trimmed];
2743
+ i++;
2744
+ while (i < lines.length && lines[i].trim() && !/^(#{1,3} |> |[-*] |\d+\. )/.test(lines[i].trim())) {
2745
+ paraLines.push(lines[i].trim());
2746
+ i++;
2747
+ }
2748
+ out.push(`<p>${inlineMdToHtml(paraLines.join(" "))}</p>`);
2749
+ }
2750
+ return out.join("");
2751
+ }
2752
+ function inlineHtmlToMd(node) {
2753
+ let out = "";
2754
+ for (const child of Array.from(node.childNodes)) {
2755
+ if (child.nodeType === Node.TEXT_NODE) {
2756
+ out += child.textContent || "";
2757
+ continue;
2758
+ }
2759
+ if (child.nodeType !== Node.ELEMENT_NODE) continue;
2760
+ const el = child;
2761
+ const tag = el.tagName.toLowerCase();
2762
+ if (tag === "br") {
2763
+ out += "\n";
2764
+ } else if (tag === "strong" || tag === "b") {
2765
+ out += "**" + inlineHtmlToMd(el) + "**";
2766
+ } else if (tag === "em" || tag === "i") {
2767
+ out += "*" + inlineHtmlToMd(el) + "*";
2768
+ } else if (tag === "code") {
2769
+ out += "`" + (el.textContent || "") + "`";
2770
+ } else if (tag === "span" && el.classList.contains("mention-pill")) {
2771
+ const username = el.dataset.mentionUsername || "";
2772
+ const name = (el.textContent || "").replace(/^@/, "");
2773
+ out += `@[${name}](${username})`;
2774
+ } else {
2775
+ out += inlineHtmlToMd(el);
2776
+ }
2777
+ }
2778
+ return out;
2779
+ }
2780
+ function htmlToMd(html) {
2781
+ if (!html || !html.trim()) return "";
2782
+ const div = document.createElement("div");
2783
+ div.innerHTML = html;
2784
+ const blocks = [];
2785
+ const handle = (el) => {
2786
+ const tag = el.tagName.toLowerCase();
2787
+ if (tag === "h1") return "# " + inlineHtmlToMd(el);
2788
+ if (tag === "h2") return "## " + inlineHtmlToMd(el);
2789
+ if (tag === "h3" || tag === "h4" || tag === "h5" || tag === "h6")
2790
+ return "### " + inlineHtmlToMd(el);
2791
+ if (tag === "p" || tag === "div") return inlineHtmlToMd(el);
2792
+ if (tag === "ul") {
2793
+ const items = Array.from(el.children).filter((c) => c.tagName.toLowerCase() === "li").map((li) => "- " + inlineHtmlToMd(li));
2794
+ return items.join("\n");
2795
+ }
2796
+ if (tag === "ol") {
2797
+ const items = Array.from(el.children).filter((c) => c.tagName.toLowerCase() === "li").map((li, idx) => `${idx + 1}. ` + inlineHtmlToMd(li));
2798
+ return items.join("\n");
2799
+ }
2800
+ if (tag === "blockquote") {
2801
+ const inner = Array.from(el.children).map((c) => handle(c) || inlineHtmlToMd(c)).filter(Boolean).join("\n");
2802
+ return inner.split("\n").map((line) => "> " + line).join("\n");
2803
+ }
2804
+ if (tag === "br") return "";
2805
+ return inlineHtmlToMd(el);
2806
+ };
2807
+ for (const child of Array.from(div.childNodes)) {
2808
+ if (child.nodeType === Node.TEXT_NODE) {
2809
+ const t = (child.textContent || "").trim();
2810
+ if (t) blocks.push(t);
2811
+ continue;
2812
+ }
2813
+ if (child.nodeType !== Node.ELEMENT_NODE) continue;
2814
+ const text = handle(child);
2815
+ if (text && text.trim()) blocks.push(text);
2816
+ }
2817
+ return blocks.join("\n\n").trim();
2818
+ }
2819
+
2820
+ // src/components/MarkdownEditor.tsx
2821
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2822
+ function ToolbarBtn({ icon: IconC, title, onClick }) {
2823
+ return /* @__PURE__ */ jsx17(
2824
+ "button",
2825
+ {
2826
+ type: "button",
2827
+ onMouseDown: (e) => e.preventDefault(),
2828
+ onClick,
2829
+ title,
2830
+ className: "w-7 h-7 inline-flex items-center justify-center rounded hover:bg-neutral-100 text-neutral-500 hover:text-neutral-900 transition-colors",
2831
+ children: /* @__PURE__ */ jsx17(IconC, { className: "w-4 h-4", strokeWidth: 1.5 })
2832
+ }
2833
+ );
2834
+ }
2835
+ function MarkdownEditor({ value, onCommit, onCancel, placeholder, saving }) {
2836
+ const editorRef = useRef9(null);
2837
+ useEffect9(() => {
2838
+ if (editorRef.current) {
2839
+ editorRef.current.innerHTML = mdToHtml(value);
2840
+ editorRef.current.focus();
2841
+ const range = document.createRange();
2842
+ range.selectNodeContents(editorRef.current);
2843
+ range.collapse(false);
2844
+ const sel = window.getSelection();
2845
+ sel?.removeAllRanges();
2846
+ sel?.addRange(range);
2847
+ }
2848
+ }, []);
2849
+ const focusEditor = () => editorRef.current?.focus();
2850
+ const exec = (command, arg) => {
2851
+ focusEditor();
2852
+ document.execCommand(command, false, arg);
2853
+ };
2854
+ const wrapSelectionInCode = () => {
2855
+ focusEditor();
2856
+ const sel = window.getSelection();
2857
+ if (!sel || sel.rangeCount === 0) return;
2858
+ const range = sel.getRangeAt(0);
2859
+ if (range.collapsed) return;
2860
+ const codeEl = document.createElement("code");
2861
+ try {
2862
+ codeEl.textContent = range.toString();
2863
+ range.deleteContents();
2864
+ range.insertNode(codeEl);
2865
+ range.setStartAfter(codeEl);
2866
+ range.collapse(true);
2867
+ sel.removeAllRanges();
2868
+ sel.addRange(range);
2869
+ } catch {
2870
+ }
2871
+ };
2872
+ const commit = () => {
2873
+ if (!editorRef.current) return;
2874
+ onCommit(htmlToMd(editorRef.current.innerHTML));
2875
+ };
2876
+ return /* @__PURE__ */ jsxs15("div", { children: [
2877
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-0.5 bg-neutral-50/60 border border-neutral-200 rounded-md p-0.5 mb-2 w-fit", children: [
2878
+ /* @__PURE__ */ jsx17(ToolbarBtn, { icon: Heading2, title: "Heading", onClick: () => exec("formatBlock", "h2") }),
2879
+ /* @__PURE__ */ jsx17(ToolbarBtn, { icon: Bold, title: "Bold", onClick: () => exec("bold") }),
2880
+ /* @__PURE__ */ jsx17(ToolbarBtn, { icon: Italic, title: "Italic", onClick: () => exec("italic") }),
2881
+ /* @__PURE__ */ jsx17("span", { className: "w-px h-5 bg-neutral-200 mx-0.5" }),
2882
+ /* @__PURE__ */ jsx17(ToolbarBtn, { icon: List, title: "Bullet list", onClick: () => exec("insertUnorderedList") }),
2883
+ /* @__PURE__ */ jsx17(ToolbarBtn, { icon: ListOrdered, title: "Numbered list", onClick: () => exec("insertOrderedList") }),
2884
+ /* @__PURE__ */ jsx17("span", { className: "w-px h-5 bg-neutral-200 mx-0.5" }),
2885
+ /* @__PURE__ */ jsx17(ToolbarBtn, { icon: Quote, title: "Quote", onClick: () => exec("formatBlock", "blockquote") }),
2886
+ /* @__PURE__ */ jsx17(ToolbarBtn, { icon: Code, title: "Inline code", onClick: wrapSelectionInCode })
2887
+ ] }),
2888
+ /* @__PURE__ */ jsx17(
2889
+ "div",
2890
+ {
2891
+ ref: editorRef,
2892
+ contentEditable: true,
2893
+ suppressContentEditableWarning: true,
2894
+ "data-placeholder": placeholder,
2895
+ onBlur: commit,
2896
+ onKeyDown: (e) => {
2897
+ if (e.key === "Escape") {
2898
+ e.preventDefault();
2899
+ onCancel();
2900
+ }
2901
+ },
2902
+ className: "eb-tb-markdown-editor w-full px-3.5 py-2.5 bg-white border border-neutral-200 rounded-lg text-[13px] leading-relaxed text-neutral-800 focus:outline-none focus:ring-2 focus:ring-[#FF5E00]/10 focus:border-[#FF5E00]/50 transition-all min-h-[80px]"
2903
+ }
2904
+ ),
2905
+ /* @__PURE__ */ jsxs15("p", { className: "mt-1.5 text-[11px] text-neutral-400", children: [
2906
+ /* @__PURE__ */ jsx17("kbd", { className: "px-1 py-0.5 rounded bg-neutral-100 border border-neutral-200 text-[10px]", children: "Esc" }),
2907
+ " ",
2908
+ "to cancel \xB7 click outside to save",
2909
+ saving && /* @__PURE__ */ jsx17("span", { className: "ml-2", children: "\xB7 Saving..." })
2910
+ ] })
2911
+ ] });
2912
+ }
2913
+
2914
+ // src/components/DescriptionSection.tsx
2915
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
2916
+ function DescriptionSection({
2917
+ sectionKey,
2918
+ label,
2919
+ placeholder,
2920
+ value,
2921
+ onChange,
2922
+ status,
2923
+ onStatusChange,
2924
+ saving
2925
+ }) {
2926
+ const [editing, setEditing] = useState10(false);
2927
+ const [openHelp, setOpenHelp] = useState10(null);
2928
+ const startEdit = () => setEditing(true);
2929
+ const cancel = () => setEditing(false);
2930
+ const handleCommit = (md) => {
2931
+ if (md !== value) onChange(md);
2932
+ setEditing(false);
2933
+ };
2934
+ const hasContent = value.trim().length > 0;
2935
+ return /* @__PURE__ */ jsxs16("section", { className: "group", "data-section": sectionKey, children: [
2936
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center justify-between gap-3 mb-3", children: [
2937
+ /* @__PURE__ */ jsx18("h2", { className: "text-[15px] font-semibold text-neutral-900 tracking-tight", children: label }),
2938
+ /* @__PURE__ */ jsx18(
2939
+ "div",
2940
+ {
2941
+ className: "flex rounded-2xl border border-neutral-200/60 bg-neutral-100/80 p-0.5 items-center h-7 shrink-0",
2942
+ role: "group",
2943
+ "aria-label": `${label} status`,
2944
+ children: ["draft", "approved"].map((s) => {
2945
+ const active = status === s;
2946
+ const labelText = s === "draft" ? "Draft" : "Approved";
2947
+ const help = s === "draft" ? "Draft \u2014 content is being written and is not yet ready for review." : "Approved \u2014 content has been reviewed and signed off (e.g. by the client).";
2948
+ return /* @__PURE__ */ jsxs16(
2949
+ "button",
2950
+ {
2951
+ type: "button",
2952
+ onClick: () => onStatusChange(s),
2953
+ "aria-pressed": active,
2954
+ className: `inline-flex items-center justify-center gap-1.5 rounded-xl px-3 h-6 text-[12px] font-medium transition-all ${active ? "bg-[#FF5E00] text-white shadow-sm" : "text-neutral-500 hover:text-neutral-900"}`,
2955
+ children: [
2956
+ labelText,
2957
+ /* @__PURE__ */ jsxs16(
2958
+ "span",
2959
+ {
2960
+ className: "relative inline-flex items-center",
2961
+ onMouseEnter: () => setOpenHelp(s),
2962
+ onMouseLeave: () => setOpenHelp(null),
2963
+ onClick: (e) => e.stopPropagation(),
2964
+ children: [
2965
+ /* @__PURE__ */ jsx18(HelpCircleIcon, { className: "w-3 h-3 opacity-70", strokeWidth: 1.5 }),
2966
+ openHelp === s && /* @__PURE__ */ jsx18("span", { className: "absolute right-0 top-full mt-2 z-50 pointer-events-none", children: /* @__PURE__ */ jsx18("span", { className: "block bg-white border border-neutral-200 shadow-lg rounded-xl px-3.5 py-2.5 text-[12px] font-medium text-neutral-700 whitespace-normal w-60 text-left", children: help }) })
2967
+ ]
2968
+ }
2969
+ )
2970
+ ]
2971
+ },
2972
+ s
2973
+ );
2974
+ })
2975
+ }
2976
+ )
2977
+ ] }),
2978
+ editing ? /* @__PURE__ */ jsx18(
2979
+ MarkdownEditor,
2980
+ {
2981
+ value,
2982
+ onCommit: handleCommit,
2983
+ onCancel: cancel,
2984
+ placeholder,
2985
+ saving
2986
+ }
2987
+ ) : /* @__PURE__ */ jsx18(
2988
+ "button",
2989
+ {
2990
+ type: "button",
2991
+ onClick: startEdit,
2992
+ className: "w-full text-left rounded-lg border border-transparent hover:border-neutral-200 hover:bg-neutral-50/60 transition-colors px-3.5 py-2.5 -mx-3.5 -my-2.5 cursor-text",
2993
+ children: hasContent ? /* @__PURE__ */ jsx18(MarkdownView, { content: value, className: "text-[13px]" }) : /* @__PURE__ */ jsx18("p", { className: "text-[13px] text-neutral-400 italic", children: placeholder })
2994
+ }
2995
+ )
2996
+ ] });
2997
+ }
2998
+
2999
+ // src/components/OutstandingQuestionsSection.tsx
3000
+ import { useEffect as useEffect10, useRef as useRef10, useState as useState11 } from "react";
3001
+ import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3002
+ function timeAgo2(dateStr) {
3003
+ const d = parseDate(dateStr);
3004
+ const diff = Date.now() - d.getTime();
3005
+ const minutes = Math.floor(diff / 6e4);
3006
+ if (minutes < 1) return "just now";
3007
+ if (minutes < 60) return `${minutes}m ago`;
3008
+ const hours = Math.floor(minutes / 60);
3009
+ if (hours < 24) return `${hours}h ago`;
3010
+ const days = Math.floor(hours / 24);
3011
+ if (days < 30) return `${days}d ago`;
3012
+ return formatDate(dateStr);
3013
+ }
3014
+ function OutstandingQuestionsSection({
3015
+ questions,
3016
+ currentUsername,
3017
+ onCreate,
3018
+ onSetStatus,
3019
+ onDelete,
3020
+ onAddReply
3021
+ }) {
3022
+ const [filter, setFilter] = useState11("awaiting");
3023
+ const [filterMenuOpen, setFilterMenuOpen] = useState11(false);
3024
+ const [adding, setAdding] = useState11(false);
3025
+ const [newText, setNewText] = useState11("");
3026
+ const [posting, setPosting] = useState11(false);
3027
+ const filterRef = useRef10(null);
3028
+ const newRef = useRef10(null);
3029
+ useEffect10(() => {
3030
+ if (!filterMenuOpen) return;
3031
+ const onClick = (e) => {
3032
+ if (filterRef.current && !filterRef.current.contains(e.target)) {
3033
+ setFilterMenuOpen(false);
3034
+ }
3035
+ };
3036
+ document.addEventListener("mousedown", onClick);
3037
+ return () => document.removeEventListener("mousedown", onClick);
3038
+ }, [filterMenuOpen]);
3039
+ const counts = {
3040
+ awaiting: questions.filter((q) => q.status === "awaiting").length,
3041
+ answered: questions.filter((q) => q.status === "answered").length
3042
+ };
3043
+ const filtered = questions.filter((q) => q.status === filter);
3044
+ const submitNew = async () => {
3045
+ if (!newText.trim()) return;
3046
+ setPosting(true);
3047
+ try {
3048
+ await onCreate(newText.trim());
3049
+ setNewText("");
3050
+ setAdding(false);
3051
+ } catch {
3052
+ } finally {
3053
+ setPosting(false);
3054
+ }
3055
+ };
3056
+ return /* @__PURE__ */ jsxs17("section", { children: [
3057
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-3 mb-3", children: [
3058
+ /* @__PURE__ */ jsx19("h2", { className: "text-[15px] font-semibold text-neutral-900 tracking-tight", children: "Outstanding Questions" }),
3059
+ /* @__PURE__ */ jsx19(
3060
+ "span",
3061
+ {
3062
+ className: `inline-flex items-center justify-center min-w-[18px] h-4 px-1 rounded text-[10px] font-semibold ${filter === "awaiting" ? "bg-amber-50 text-amber-700" : "bg-emerald-50 text-emerald-700"}`,
3063
+ children: filtered.length
3064
+ }
3065
+ ),
3066
+ /* @__PURE__ */ jsxs17("div", { className: "relative ml-2", ref: filterRef, children: [
3067
+ /* @__PURE__ */ jsxs17(
3068
+ "button",
3069
+ {
3070
+ type: "button",
3071
+ onClick: () => setFilterMenuOpen(!filterMenuOpen),
3072
+ className: "inline-flex items-center gap-1.5 text-[12px] font-medium text-neutral-700 hover:text-neutral-900",
3073
+ children: [
3074
+ /* @__PURE__ */ jsx19("span", { children: filter === "awaiting" ? "Awaiting answer" : "Answered" }),
3075
+ /* @__PURE__ */ jsx19(ChevronDownIcon, { className: "w-3 h-3 text-neutral-400", strokeWidth: 1.5 })
3076
+ ]
3077
+ }
3078
+ ),
3079
+ filterMenuOpen && /* @__PURE__ */ jsx19("div", { className: "absolute left-0 top-full mt-1 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-30 min-w-[200px]", children: ["awaiting", "answered"].map((f) => /* @__PURE__ */ jsxs17(
3080
+ "button",
3081
+ {
3082
+ onClick: () => {
3083
+ setFilter(f);
3084
+ setFilterMenuOpen(false);
3085
+ },
3086
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center justify-between gap-3",
3087
+ children: [
3088
+ /* @__PURE__ */ jsxs17("span", { className: "flex items-center gap-2", children: [
3089
+ /* @__PURE__ */ jsx19(
3090
+ CheckIcon,
3091
+ {
3092
+ size: 12,
3093
+ strokeWidth: 2.5,
3094
+ className: `text-[#FF5E00] ${filter === f ? "" : "invisible"}`
3095
+ }
3096
+ ),
3097
+ /* @__PURE__ */ jsx19("span", { children: f === "awaiting" ? "Awaiting answer" : "Answered" })
3098
+ ] }),
3099
+ /* @__PURE__ */ jsx19("span", { className: "text-[10px] text-neutral-400", children: counts[f] })
3100
+ ]
3101
+ },
3102
+ f
3103
+ )) })
3104
+ ] }),
3105
+ /* @__PURE__ */ jsxs17(
3106
+ "button",
3107
+ {
3108
+ type: "button",
3109
+ onClick: () => {
3110
+ setAdding(true);
3111
+ setTimeout(() => newRef.current?.focus(), 0);
3112
+ },
3113
+ className: "ml-auto inline-flex items-center gap-1 text-[12px] font-medium text-[#FF5E00] hover:text-[#E05200]",
3114
+ children: [
3115
+ /* @__PURE__ */ jsx19(PlusIcon, { size: 12, strokeWidth: 2 }),
3116
+ "Add question"
3117
+ ]
3118
+ }
3119
+ )
3120
+ ] }),
3121
+ adding && /* @__PURE__ */ jsxs17("div", { className: "mb-4 rounded-lg border border-neutral-200 bg-white p-3", children: [
3122
+ /* @__PURE__ */ jsx19(
3123
+ "textarea",
3124
+ {
3125
+ ref: newRef,
3126
+ value: newText,
3127
+ onChange: (e) => setNewText(e.target.value),
3128
+ onKeyDown: (e) => {
3129
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
3130
+ e.preventDefault();
3131
+ submitNew();
3132
+ }
3133
+ if (e.key === "Escape") {
3134
+ setAdding(false);
3135
+ setNewText("");
3136
+ }
3137
+ },
3138
+ placeholder: "What's the question?",
3139
+ rows: 2,
3140
+ className: "w-full px-2 py-1.5 text-[13px] text-neutral-800 placeholder:text-neutral-400 focus:outline-none resize-y"
3141
+ }
3142
+ ),
3143
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center justify-end gap-2 mt-2", children: [
3144
+ /* @__PURE__ */ jsx19(
3145
+ "button",
3146
+ {
3147
+ onClick: () => {
3148
+ setAdding(false);
3149
+ setNewText("");
3150
+ },
3151
+ className: "text-[12px] font-medium text-neutral-500 hover:text-neutral-900 px-2 h-7 rounded hover:bg-neutral-50",
3152
+ children: "Cancel"
3153
+ }
3154
+ ),
3155
+ /* @__PURE__ */ jsx19(
3156
+ "button",
3157
+ {
3158
+ onClick: submitNew,
3159
+ disabled: !newText.trim() || posting,
3160
+ className: "inline-flex items-center justify-center px-3 h-7 rounded-lg text-[12px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] disabled:opacity-50",
3161
+ children: posting ? "Adding..." : "Add"
3162
+ }
3163
+ )
3164
+ ] })
3165
+ ] }),
3166
+ filtered.length === 0 ? /* @__PURE__ */ jsx19("div", { className: "rounded-lg border border-dashed border-neutral-200 px-4 py-6 text-center", children: /* @__PURE__ */ jsx19("p", { className: "text-[12px] text-neutral-400", children: filter === "awaiting" ? "No outstanding questions." : "No answered questions yet." }) }) : /* @__PURE__ */ jsx19("ul", { className: "flex flex-col gap-1", children: filtered.map((q) => /* @__PURE__ */ jsx19(
3167
+ QuestionItem,
3168
+ {
3169
+ question: q,
3170
+ currentUsername,
3171
+ onSetStatus,
3172
+ onDelete,
3173
+ onAddReply
3174
+ },
3175
+ q.id
3176
+ )) })
3177
+ ] });
3178
+ }
3179
+ function QuestionItem({
3180
+ question,
3181
+ currentUsername,
3182
+ onSetStatus,
3183
+ onDelete,
3184
+ onAddReply
3185
+ }) {
3186
+ const [expanded, setExpanded] = useState11(false);
3187
+ const [reply, setReply] = useState11("");
3188
+ const [posting, setPosting] = useState11(false);
3189
+ const isAnswered = question.status === "answered";
3190
+ const dotColor = isAnswered ? "bg-emerald-500" : "bg-amber-500";
3191
+ const pillTextColor = isAnswered ? "text-emerald-700" : "text-amber-700";
3192
+ const borderColor = isAnswered ? "border-emerald-200" : "border-neutral-200";
3193
+ const submitReply = async () => {
3194
+ if (!reply.trim()) return;
3195
+ setPosting(true);
3196
+ try {
3197
+ await onAddReply(question.id, reply.trim());
3198
+ setReply("");
3199
+ } finally {
3200
+ setPosting(false);
3201
+ }
3202
+ };
3203
+ const toggleStatus = async () => {
3204
+ await onSetStatus(question.id, isAnswered ? "awaiting" : "answered");
3205
+ };
3206
+ const handleDelete = async () => {
3207
+ if (!confirm("Delete this question and its replies?")) return;
3208
+ await onDelete(question.id);
3209
+ };
3210
+ const canDelete = question.asked_by === currentUsername;
3211
+ return /* @__PURE__ */ jsxs17("li", { className: "group rounded-lg hover:bg-neutral-50/60 transition-colors", children: [
3212
+ /* @__PURE__ */ jsxs17(
3213
+ "button",
3214
+ {
3215
+ type: "button",
3216
+ onClick: () => setExpanded(!expanded),
3217
+ className: "w-full flex items-start gap-3 p-3 text-left",
3218
+ children: [
3219
+ /* @__PURE__ */ jsx19("span", { className: `mt-1.5 w-1.5 h-1.5 rounded-full shrink-0 ${dotColor}` }),
3220
+ /* @__PURE__ */ jsxs17("div", { className: "flex-1 min-w-0", children: [
3221
+ /* @__PURE__ */ jsx19("p", { className: "text-[13px] text-neutral-800 leading-snug", children: question.text }),
3222
+ /* @__PURE__ */ jsxs17("div", { className: "mt-1 flex items-center gap-2 text-[12px] text-neutral-500 flex-wrap", children: [
3223
+ /* @__PURE__ */ jsxs17("span", { children: [
3224
+ "Asked by",
3225
+ " ",
3226
+ /* @__PURE__ */ jsx19("span", { className: "font-medium text-neutral-700", children: question.asked_by_name || question.asked_by })
3227
+ ] }),
3228
+ /* @__PURE__ */ jsx19("span", { className: "text-neutral-300", children: "\xB7" }),
3229
+ /* @__PURE__ */ jsxs17("span", { children: [
3230
+ question.replies.length,
3231
+ " repl",
3232
+ question.replies.length === 1 ? "y" : "ies"
3233
+ ] }),
3234
+ /* @__PURE__ */ jsx19("span", { className: "text-neutral-300", children: "\xB7" }),
3235
+ /* @__PURE__ */ jsxs17("span", { className: `inline-flex items-center gap-1 ${pillTextColor}`, children: [
3236
+ /* @__PURE__ */ jsx19("span", { className: `w-1.5 h-1.5 rounded-full ${dotColor}` }),
3237
+ isAnswered ? "Answered" : "Awaiting answer"
3238
+ ] })
3239
+ ] })
3240
+ ] }),
3241
+ /* @__PURE__ */ jsx19(
3242
+ ChevronDownIcon,
3243
+ {
3244
+ className: `w-4 h-4 text-neutral-400 mt-1 shrink-0 transition-transform ${expanded ? "rotate-180" : ""}`,
3245
+ strokeWidth: 1.5
3246
+ }
3247
+ )
3248
+ ]
3249
+ }
3250
+ ),
3251
+ expanded && /* @__PURE__ */ jsx19("div", { className: "pl-7 pr-3 pb-3", children: /* @__PURE__ */ jsxs17("div", { className: `border-l-2 ${borderColor} pl-4 flex flex-col gap-3`, children: [
3252
+ question.replies.map((r) => /* @__PURE__ */ jsxs17("div", { className: "flex gap-2.5", children: [
3253
+ /* @__PURE__ */ jsx19("div", { className: "w-6 h-6 rounded-full bg-[#FF5E00] text-white flex items-center justify-center text-[10px] font-semibold shrink-0", children: getInitials(r.author_name || "?") }),
3254
+ /* @__PURE__ */ jsxs17("div", { className: "flex-1 min-w-0", children: [
3255
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2 mb-0.5", children: [
3256
+ /* @__PURE__ */ jsx19("span", { className: "text-[12px] font-medium text-neutral-900", children: r.author_name }),
3257
+ /* @__PURE__ */ jsx19("span", { className: "text-[10px] text-neutral-400", children: timeAgo2(r.created_at) })
3258
+ ] }),
3259
+ /* @__PURE__ */ jsx19("p", { className: "text-[12px] text-neutral-700 leading-relaxed whitespace-pre-wrap", children: /* @__PURE__ */ jsx19(MentionText, { text: r.content }) })
3260
+ ] })
3261
+ ] }, r.id)),
3262
+ /* @__PURE__ */ jsxs17("div", { className: "flex gap-2.5 pt-1", children: [
3263
+ /* @__PURE__ */ jsx19("div", { className: "w-6 h-6 rounded-full bg-neutral-100 text-neutral-700 flex items-center justify-center text-[10px] font-semibold shrink-0", children: getInitials(currentUsername || "?") }),
3264
+ /* @__PURE__ */ jsxs17("div", { className: "flex-1 min-w-0 flex flex-col gap-2", children: [
3265
+ /* @__PURE__ */ jsx19(
3266
+ MentionTextarea,
3267
+ {
3268
+ value: reply,
3269
+ onChange: setReply,
3270
+ rows: 2,
3271
+ placeholder: "Reply\u2026 (type @ to mention)",
3272
+ className: "w-full rounded-md border border-neutral-200 px-3 py-1.5 text-[12px] min-h-[60px] focus:outline-none focus:ring-1 focus:ring-neutral-300 resize-none"
3273
+ }
3274
+ ),
3275
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center justify-end gap-2", children: [
3276
+ canDelete && /* @__PURE__ */ jsxs17(
3277
+ "button",
3278
+ {
3279
+ type: "button",
3280
+ onClick: handleDelete,
3281
+ className: "inline-flex items-center gap-1.5 text-[12px] font-medium text-neutral-500 hover:text-red-600 px-2 h-7 rounded hover:bg-red-50",
3282
+ children: [
3283
+ /* @__PURE__ */ jsx19(TrashIcon, { size: 12, strokeWidth: 1.5 }),
3284
+ "Delete question"
3285
+ ]
3286
+ }
3287
+ ),
3288
+ /* @__PURE__ */ jsx19(
3289
+ "button",
3290
+ {
3291
+ type: "button",
3292
+ onClick: toggleStatus,
3293
+ className: "inline-flex items-center gap-1.5 text-[12px] font-medium text-neutral-600 hover:text-neutral-900 px-2 h-7 rounded hover:bg-neutral-100",
3294
+ children: isAnswered ? /* @__PURE__ */ jsxs17(Fragment3, { children: [
3295
+ /* @__PURE__ */ jsx19(RotateCcwIcon, { size: 12, strokeWidth: 1.5 }),
3296
+ "Reopen"
3297
+ ] }) : /* @__PURE__ */ jsxs17(Fragment3, { children: [
3298
+ /* @__PURE__ */ jsx19(CheckCircle2Icon, { size: 12, strokeWidth: 1.5 }),
3299
+ "Mark answered"
3300
+ ] })
3301
+ }
3302
+ ),
3303
+ /* @__PURE__ */ jsx19(
3304
+ "button",
3305
+ {
3306
+ type: "button",
3307
+ onClick: submitReply,
3308
+ disabled: !reply.trim() || posting,
3309
+ className: "inline-flex items-center justify-center px-3 h-7 rounded-lg text-[12px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] disabled:opacity-50",
3310
+ children: posting ? "Posting..." : "Reply"
3311
+ }
3312
+ )
3313
+ ] })
3314
+ ] })
3315
+ ] })
3316
+ ] }) })
3317
+ ] });
3318
+ }
3319
+
3320
+ // src/components/AttachmentsSection.tsx
3321
+ import { useEffect as useEffect11, useRef as useRef11, useState as useState12 } from "react";
3322
+ import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
3323
+ function formatBytes(bytes) {
3324
+ if (!bytes && bytes !== 0) return "";
3325
+ if (bytes < 1024) return `${bytes} B`;
3326
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
3327
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3328
+ }
3329
+ function attachmentMeta(a) {
3330
+ if (a.kind === "link") {
3331
+ try {
3332
+ return new URL(a.url.startsWith("http") ? a.url : `https://${a.url}`).hostname;
3333
+ } catch {
3334
+ return "";
3335
+ }
3336
+ }
3337
+ return formatBytes(a.size);
3338
+ }
3339
+ function AttachmentsSection({
3340
+ attachments,
3341
+ onUpload,
3342
+ onAddLink,
3343
+ onDelete
3344
+ }) {
3345
+ const [menuOpen, setMenuOpen] = useState12(false);
3346
+ const [linkModalOpen, setLinkModalOpen] = useState12(false);
3347
+ const [linkUrl, setLinkUrl] = useState12("");
3348
+ const [linkTitle, setLinkTitle] = useState12("");
3349
+ const [busy, setBusy] = useState12(false);
3350
+ const [error, setError] = useState12("");
3351
+ const fileInputRef = useRef11(null);
3352
+ const imageInputRef = useRef11(null);
3353
+ const menuRef = useRef11(null);
3354
+ useEffect11(() => {
3355
+ if (!menuOpen) return;
3356
+ const onClick = (e) => {
3357
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
3358
+ setMenuOpen(false);
3359
+ }
3360
+ };
3361
+ document.addEventListener("mousedown", onClick);
3362
+ return () => document.removeEventListener("mousedown", onClick);
3363
+ }, [menuOpen]);
3364
+ const showError = (msg) => {
3365
+ setError(msg);
3366
+ setTimeout(() => setError(""), 4e3);
3367
+ };
3368
+ const upload = async (file) => {
3369
+ setBusy(true);
3370
+ setError("");
3371
+ try {
3372
+ await onUpload(file);
3373
+ } catch (err) {
3374
+ const e = err;
3375
+ showError(e.response?.data?.detail || "Upload failed");
3376
+ } finally {
3377
+ setBusy(false);
3378
+ }
3379
+ };
3380
+ const submitLink = async () => {
3381
+ if (!linkUrl.trim()) return;
3382
+ setBusy(true);
3383
+ setError("");
3384
+ try {
3385
+ await onAddLink(linkUrl.trim(), linkTitle.trim() || void 0);
3386
+ setLinkUrl("");
3387
+ setLinkTitle("");
3388
+ setLinkModalOpen(false);
3389
+ } catch (err) {
3390
+ const e = err;
3391
+ showError(e.response?.data?.detail || "Failed to add link");
3392
+ } finally {
3393
+ setBusy(false);
3394
+ }
3395
+ };
3396
+ const remove = async (id) => {
3397
+ try {
3398
+ await onDelete(id);
3399
+ } catch (err) {
3400
+ const e = err;
3401
+ showError(e.response?.data?.detail || "Failed to delete");
3402
+ }
3403
+ };
3404
+ const images = attachments.filter((a) => a.kind === "image");
3405
+ const files = attachments.filter((a) => a.kind === "file");
3406
+ const links = attachments.filter((a) => a.kind === "link");
3407
+ return /* @__PURE__ */ jsxs18("section", { children: [
3408
+ /* @__PURE__ */ jsxs18("div", { className: "flex items-center gap-3 mb-3", children: [
3409
+ /* @__PURE__ */ jsx20("h2", { className: "text-[15px] font-semibold text-neutral-900 tracking-tight", children: "Attachments" }),
3410
+ busy && /* @__PURE__ */ jsx20("span", { className: "text-[11px] text-neutral-400", children: "Uploading..." }),
3411
+ /* @__PURE__ */ jsxs18("div", { className: "ml-auto relative", ref: menuRef, children: [
3412
+ /* @__PURE__ */ jsxs18(
3413
+ "button",
3414
+ {
3415
+ type: "button",
3416
+ onClick: () => setMenuOpen(!menuOpen),
3417
+ disabled: busy,
3418
+ className: "inline-flex items-center gap-1 text-[12px] font-medium text-[#FF5E00] hover:text-[#E05200] disabled:opacity-50",
3419
+ children: [
3420
+ /* @__PURE__ */ jsx20(PlusIcon, { size: 12, strokeWidth: 2 }),
3421
+ "Add attachment"
3422
+ ]
3423
+ }
3424
+ ),
3425
+ menuOpen && /* @__PURE__ */ jsxs18("div", { className: "absolute right-0 top-full mt-1 w-44 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-30", children: [
3426
+ /* @__PURE__ */ jsxs18(
3427
+ "button",
3428
+ {
3429
+ onClick: () => {
3430
+ setMenuOpen(false);
3431
+ imageInputRef.current?.click();
3432
+ },
3433
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2.5",
3434
+ children: [
3435
+ /* @__PURE__ */ jsx20(ImageIcon, { size: 14, strokeWidth: 1.5, className: "text-neutral-400" }),
3436
+ "Image"
3437
+ ]
3438
+ }
3439
+ ),
3440
+ /* @__PURE__ */ jsxs18(
3441
+ "button",
3442
+ {
3443
+ onClick: () => {
3444
+ setMenuOpen(false);
3445
+ fileInputRef.current?.click();
3446
+ },
3447
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2.5",
3448
+ children: [
3449
+ /* @__PURE__ */ jsx20(FileTextIcon, { size: 14, strokeWidth: 1.5, className: "text-neutral-400" }),
3450
+ "File"
3451
+ ]
3452
+ }
3453
+ ),
3454
+ /* @__PURE__ */ jsxs18(
3455
+ "button",
3456
+ {
3457
+ onClick: () => {
3458
+ setMenuOpen(false);
3459
+ setLinkModalOpen(true);
3460
+ },
3461
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2.5",
3462
+ children: [
3463
+ /* @__PURE__ */ jsx20(Link2Icon, { size: 14, strokeWidth: 1.5, className: "text-neutral-400" }),
3464
+ "Link / Recording"
3465
+ ]
3466
+ }
3467
+ )
3468
+ ] })
3469
+ ] }),
3470
+ /* @__PURE__ */ jsx20(
3471
+ "input",
3472
+ {
3473
+ ref: imageInputRef,
3474
+ type: "file",
3475
+ accept: "image/*",
3476
+ className: "hidden",
3477
+ onChange: (e) => {
3478
+ const f = e.target.files?.[0];
3479
+ if (f) upload(f);
3480
+ e.target.value = "";
3481
+ }
3482
+ }
3483
+ ),
3484
+ /* @__PURE__ */ jsx20(
3485
+ "input",
3486
+ {
3487
+ ref: fileInputRef,
3488
+ type: "file",
3489
+ className: "hidden",
3490
+ onChange: (e) => {
3491
+ const f = e.target.files?.[0];
3492
+ if (f) upload(f);
3493
+ e.target.value = "";
3494
+ }
3495
+ }
3496
+ )
3497
+ ] }),
3498
+ error && /* @__PURE__ */ jsx20("div", { className: "mb-3 text-[12px] text-red-600 bg-red-50 border border-red-100 rounded-lg px-3 py-2", children: error }),
3499
+ attachments.length === 0 ? /* @__PURE__ */ jsx20("div", { className: "rounded-lg border border-dashed border-neutral-200 px-4 py-6 text-center", children: /* @__PURE__ */ jsx20("p", { className: "text-[12px] text-neutral-400", children: "No attachments yet." }) }) : /* @__PURE__ */ jsxs18("div", { className: "space-y-5", children: [
3500
+ images.length > 0 && /* @__PURE__ */ jsxs18("div", { children: [
3501
+ /* @__PURE__ */ jsx20("p", { className: "text-[10px] font-semibold tracking-wider text-neutral-400 uppercase mb-2", children: "Images" }),
3502
+ /* @__PURE__ */ jsx20("div", { className: "grid grid-cols-3 gap-2", children: images.map((img) => /* @__PURE__ */ jsxs18(
3503
+ "a",
3504
+ {
3505
+ href: img.url || "#",
3506
+ target: "_blank",
3507
+ rel: "noopener noreferrer",
3508
+ className: "group relative aspect-video overflow-hidden rounded-lg border border-neutral-200 hover:border-neutral-300 bg-neutral-50",
3509
+ children: [
3510
+ img.url ? (
3511
+ // eslint-disable-next-line @next/next/no-img-element
3512
+ /* @__PURE__ */ jsx20("img", { src: img.url, alt: img.name, className: "w-full h-full object-cover" })
3513
+ ) : /* @__PURE__ */ jsx20("div", { className: "w-full h-full flex items-center justify-center text-neutral-400", children: /* @__PURE__ */ jsx20(ImageIcon, { size: 24, strokeWidth: 1.5 }) }),
3514
+ /* @__PURE__ */ jsx20("div", { className: "absolute bottom-1 left-1 right-8 text-[10px] text-white px-1.5 py-0.5 rounded bg-black/60 truncate opacity-0 group-hover:opacity-100", children: img.name }),
3515
+ /* @__PURE__ */ jsx20(
3516
+ "button",
3517
+ {
3518
+ onClick: (e) => {
3519
+ e.preventDefault();
3520
+ e.stopPropagation();
3521
+ remove(img.id);
3522
+ },
3523
+ className: "absolute top-1 right-1 w-6 h-6 rounded-full bg-black/60 text-white opacity-0 group-hover:opacity-100 flex items-center justify-center",
3524
+ title: "Remove",
3525
+ children: /* @__PURE__ */ jsx20(XIcon, { size: 12, strokeWidth: 2 })
3526
+ }
3527
+ )
3528
+ ]
3529
+ },
3530
+ img.id
3531
+ )) })
3532
+ ] }),
3533
+ files.length > 0 && /* @__PURE__ */ jsxs18("div", { children: [
3534
+ /* @__PURE__ */ jsx20("p", { className: "text-[10px] font-semibold tracking-wider text-neutral-400 uppercase mb-2", children: "Files" }),
3535
+ /* @__PURE__ */ jsx20("div", { className: "flex flex-col gap-1.5", children: files.map((f) => {
3536
+ const meta = attachmentMeta(f);
3537
+ return /* @__PURE__ */ jsxs18(
3538
+ "div",
3539
+ {
3540
+ className: "group flex items-center gap-3 p-2 rounded-lg border border-neutral-200 hover:bg-neutral-50",
3541
+ children: [
3542
+ /* @__PURE__ */ jsx20("div", { className: "w-8 h-8 rounded bg-neutral-100 flex items-center justify-center text-neutral-500", children: /* @__PURE__ */ jsx20(FileTextIcon, { size: 16, strokeWidth: 1.5 }) }),
3543
+ /* @__PURE__ */ jsxs18("div", { className: "flex-1 min-w-0", children: [
3544
+ /* @__PURE__ */ jsx20("div", { className: "text-[13px] font-medium text-neutral-900 truncate", children: f.name }),
3545
+ meta && /* @__PURE__ */ jsx20("div", { className: "text-[10px] text-neutral-500", children: meta })
3546
+ ] }),
3547
+ f.url && /* @__PURE__ */ jsx20(
3548
+ "a",
3549
+ {
3550
+ href: f.url,
3551
+ target: "_blank",
3552
+ rel: "noopener noreferrer",
3553
+ className: "w-7 h-7 rounded hover:bg-neutral-100 text-neutral-500 hover:text-neutral-900 flex items-center justify-center",
3554
+ title: "Open",
3555
+ children: /* @__PURE__ */ jsx20(ExternalLinkIcon, { size: 14, strokeWidth: 1.5 })
3556
+ }
3557
+ ),
3558
+ /* @__PURE__ */ jsx20(
3559
+ "button",
3560
+ {
3561
+ onClick: () => remove(f.id),
3562
+ className: "opacity-0 group-hover:opacity-100 w-7 h-7 rounded hover:bg-red-50 text-neutral-400 hover:text-red-500 flex items-center justify-center",
3563
+ title: "Remove",
3564
+ children: /* @__PURE__ */ jsx20(XIcon, { size: 14, strokeWidth: 2 })
3565
+ }
3566
+ )
3567
+ ]
3568
+ },
3569
+ f.id
3570
+ );
3571
+ }) })
3572
+ ] }),
3573
+ links.length > 0 && /* @__PURE__ */ jsxs18("div", { children: [
3574
+ /* @__PURE__ */ jsx20("p", { className: "text-[10px] font-semibold tracking-wider text-neutral-400 uppercase mb-2", children: "Links & recordings" }),
3575
+ /* @__PURE__ */ jsx20("div", { className: "flex flex-col gap-1.5", children: links.map((l) => {
3576
+ const meta = attachmentMeta(l);
3577
+ return /* @__PURE__ */ jsxs18(
3578
+ "a",
3579
+ {
3580
+ href: l.url.startsWith("http") ? l.url : `https://${l.url}`,
3581
+ target: "_blank",
3582
+ rel: "noopener noreferrer",
3583
+ className: "group flex items-center gap-3 p-2 rounded-lg border border-neutral-200 hover:bg-neutral-50",
3584
+ children: [
3585
+ /* @__PURE__ */ jsx20("div", { className: "w-8 h-8 rounded bg-neutral-100 flex items-center justify-center text-neutral-500", children: /* @__PURE__ */ jsx20(Link2Icon, { size: 16, strokeWidth: 1.5 }) }),
3586
+ /* @__PURE__ */ jsxs18("div", { className: "flex-1 min-w-0", children: [
3587
+ /* @__PURE__ */ jsx20("div", { className: "text-[13px] font-medium text-neutral-900 truncate", children: l.name }),
3588
+ meta && /* @__PURE__ */ jsx20("div", { className: "text-[10px] text-neutral-500", children: meta })
3589
+ ] }),
3590
+ /* @__PURE__ */ jsx20(ExternalLinkIcon, { size: 14, strokeWidth: 1.5, className: "text-neutral-400 shrink-0" }),
3591
+ /* @__PURE__ */ jsx20(
3592
+ "button",
3593
+ {
3594
+ onClick: (e) => {
3595
+ e.preventDefault();
3596
+ e.stopPropagation();
3597
+ remove(l.id);
3598
+ },
3599
+ className: "opacity-0 group-hover:opacity-100 w-7 h-7 rounded hover:bg-red-50 text-neutral-400 hover:text-red-500 flex items-center justify-center",
3600
+ title: "Remove",
3601
+ children: /* @__PURE__ */ jsx20(XIcon, { size: 14, strokeWidth: 2 })
3602
+ }
3603
+ )
3604
+ ]
3605
+ },
3606
+ l.id
3607
+ );
3608
+ }) })
3609
+ ] })
3610
+ ] }),
3611
+ linkModalOpen && /* @__PURE__ */ jsx20(
3612
+ "div",
3613
+ {
3614
+ className: "fixed inset-0 z-[80] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4",
3615
+ onClick: () => setLinkModalOpen(false),
3616
+ children: /* @__PURE__ */ jsxs18(
3617
+ "div",
3618
+ {
3619
+ className: "bg-white rounded-2xl shadow-2xl w-full max-w-[460px] overflow-hidden",
3620
+ onClick: (e) => e.stopPropagation(),
3621
+ children: [
3622
+ /* @__PURE__ */ jsxs18("div", { className: "flex items-center justify-between px-5 py-4 border-b border-neutral-100", children: [
3623
+ /* @__PURE__ */ jsx20("h3", { className: "text-[15px] font-semibold text-neutral-900", children: "Add link or recording" }),
3624
+ /* @__PURE__ */ jsx20(
3625
+ "button",
3626
+ {
3627
+ onClick: () => setLinkModalOpen(false),
3628
+ className: "w-8 h-8 flex items-center justify-center rounded-lg hover:bg-neutral-100 text-neutral-500",
3629
+ children: /* @__PURE__ */ jsx20(XIcon, { size: 16, strokeWidth: 1.5 })
3630
+ }
3631
+ )
3632
+ ] }),
3633
+ /* @__PURE__ */ jsxs18("div", { className: "p-5 flex flex-col gap-3", children: [
3634
+ /* @__PURE__ */ jsxs18("div", { className: "flex flex-col gap-1.5", children: [
3635
+ /* @__PURE__ */ jsx20("label", { className: "text-[12px] font-medium text-neutral-600", children: "URL" }),
3636
+ /* @__PURE__ */ jsx20(
3637
+ "input",
3638
+ {
3639
+ type: "url",
3640
+ value: linkUrl,
3641
+ onChange: (e) => setLinkUrl(e.target.value),
3642
+ placeholder: "https://loom.com/share/\u2026",
3643
+ autoFocus: true,
3644
+ className: "h-10 rounded-xl border border-neutral-200 px-3 text-[13px] text-neutral-900 placeholder:text-neutral-400 focus:outline-none focus:ring-1 focus:ring-neutral-300"
3645
+ }
3646
+ )
3647
+ ] }),
3648
+ /* @__PURE__ */ jsxs18("div", { className: "flex flex-col gap-1.5", children: [
3649
+ /* @__PURE__ */ jsxs18("label", { className: "text-[12px] font-medium text-neutral-600", children: [
3650
+ "Title ",
3651
+ /* @__PURE__ */ jsx20("span", { className: "text-neutral-400 font-normal", children: "(optional)" })
3652
+ ] }),
3653
+ /* @__PURE__ */ jsx20(
3654
+ "input",
3655
+ {
3656
+ type: "text",
3657
+ value: linkTitle,
3658
+ onChange: (e) => setLinkTitle(e.target.value),
3659
+ placeholder: "e.g. Walkthrough \u2014 parsing edge cases",
3660
+ className: "h-10 rounded-xl border border-neutral-200 px-3 text-[13px] text-neutral-900 placeholder:text-neutral-400 focus:outline-none focus:ring-1 focus:ring-neutral-300"
3661
+ }
3662
+ )
3663
+ ] })
3664
+ ] }),
3665
+ /* @__PURE__ */ jsxs18("div", { className: "px-5 py-4 border-t border-neutral-100 flex items-center justify-end gap-2", children: [
3666
+ /* @__PURE__ */ jsx20(
3667
+ "button",
3668
+ {
3669
+ onClick: () => setLinkModalOpen(false),
3670
+ className: "inline-flex items-center justify-center h-10 px-4 rounded-xl border border-neutral-200 bg-white text-neutral-700 text-[13px] font-medium hover:bg-neutral-50 transition-colors",
3671
+ children: "Cancel"
3672
+ }
3673
+ ),
3674
+ /* @__PURE__ */ jsx20(
3675
+ "button",
3676
+ {
3677
+ onClick: submitLink,
3678
+ disabled: !linkUrl.trim() || busy,
3679
+ className: "inline-flex items-center justify-center h-10 px-4 rounded-xl bg-[#FF5E00] text-white text-[13px] font-medium shadow-sm hover:bg-[#E05200] disabled:opacity-50 transition-colors",
3680
+ children: busy ? "Saving..." : "Save"
3681
+ }
3682
+ )
3683
+ ] })
3684
+ ]
3685
+ }
3686
+ )
3687
+ }
3688
+ )
3689
+ ] });
3690
+ }
3691
+
3692
+ // src/components/ThreadsPanel.tsx
3693
+ import { useEffect as useEffect13, useRef as useRef14, useState as useState15 } from "react";
3694
+
3695
+ // src/components/ActivityList.tsx
3696
+ import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
3697
+ function ActivityList({ activity }) {
3698
+ const { columns } = useTaskBoardContext();
3699
+ if (activity.length === 0) {
3700
+ return /* @__PURE__ */ jsxs19("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center text-center", children: [
3701
+ /* @__PURE__ */ jsx21("div", { className: "w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center mb-3", children: /* @__PURE__ */ jsx21(HistoryIcon, { size: 18, strokeWidth: 1.5, className: "text-neutral-400" }) }),
3702
+ /* @__PURE__ */ jsx21("p", { className: "text-[12px] text-neutral-400", children: "No activity yet" }),
3703
+ /* @__PURE__ */ jsx21("p", { className: "text-[10px] text-neutral-400 mt-0.5", children: "Status changes will appear here" })
3704
+ ] });
3705
+ }
3706
+ return /* @__PURE__ */ jsx21("div", { className: "flex-1 overflow-y-auto p-4", children: /* @__PURE__ */ jsx21("ul", { className: "flex flex-col gap-4", children: activity.map((a) => {
3707
+ const fromCol = columns.find((c) => c.key === a.from_status);
3708
+ const toCol = columns.find((c) => c.key === a.to_status);
3709
+ const isCreated = a.type === "created";
3710
+ return /* @__PURE__ */ jsxs19("li", { className: "flex gap-3", children: [
3711
+ /* @__PURE__ */ jsx21("div", { className: "w-7 h-7 rounded-full bg-neutral-100 text-neutral-500 flex items-center justify-center shrink-0", children: isCreated ? /* @__PURE__ */ jsx21(PlusIcon, { size: 14, strokeWidth: 1.5 }) : /* @__PURE__ */ jsx21(ChevronRightIcon, { size: 14, strokeWidth: 1.5 }) }),
3712
+ /* @__PURE__ */ jsxs19("div", { className: "flex-1 min-w-0", children: [
3713
+ /* @__PURE__ */ jsxs19("p", { className: "text-[12px] text-neutral-700", children: [
3714
+ /* @__PURE__ */ jsx21("span", { className: "font-medium text-neutral-900", children: a.user_name }),
3715
+ " ",
3716
+ isCreated ? "created this task" : /* @__PURE__ */ jsxs19(Fragment4, { children: [
3717
+ "moved from",
3718
+ " ",
3719
+ /* @__PURE__ */ jsxs19(
3720
+ "span",
3721
+ {
3722
+ className: `inline-flex items-center gap-1 px-1.5 py-0.5 rounded ${fromCol?.chip ?? "bg-neutral-100 text-neutral-700"}`,
3723
+ children: [
3724
+ /* @__PURE__ */ jsx21(
3725
+ "span",
3726
+ {
3727
+ className: `w-1.5 h-1.5 rounded-full ${fromCol?.color ?? "bg-neutral-300"}`
3728
+ }
3729
+ ),
3730
+ fromCol?.label ?? a.from_status
3731
+ ]
3732
+ }
3733
+ ),
3734
+ " ",
3735
+ "to",
3736
+ " ",
3737
+ /* @__PURE__ */ jsxs19(
3738
+ "span",
3739
+ {
3740
+ className: `inline-flex items-center gap-1 px-1.5 py-0.5 rounded ${toCol?.chip ?? "bg-neutral-100 text-neutral-700"}`,
3741
+ children: [
3742
+ /* @__PURE__ */ jsx21(
3743
+ "span",
3744
+ {
3745
+ className: `w-1.5 h-1.5 rounded-full ${toCol?.color ?? "bg-neutral-300"}`
3746
+ }
3747
+ ),
3748
+ toCol?.label ?? a.to_status
3749
+ ]
3750
+ }
3751
+ )
3752
+ ] })
3753
+ ] }),
3754
+ /* @__PURE__ */ jsx21("p", { className: "text-[10px] text-neutral-400 mt-0.5", children: formatDateTime(a.created_at) })
3755
+ ] })
3756
+ ] }, a.id);
3757
+ }) }) });
3758
+ }
3759
+
3760
+ // src/components/ContextPill.tsx
3761
+ import { jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
3762
+ var VARIANTS = {
3763
+ amber: "bg-amber-50 text-amber-700 border-amber-200 hover:bg-amber-100",
3764
+ purple: "bg-purple-50 text-purple-700 border-purple-200 hover:bg-purple-100",
3765
+ blue: "bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100",
3766
+ emerald: "bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100",
3767
+ neutral: "bg-neutral-50 text-neutral-700 border-neutral-200 hover:bg-neutral-100"
3768
+ };
3769
+ function ContextPill({ variant, icon: IconC, label, onClick }) {
3770
+ return /* @__PURE__ */ jsxs20(
3771
+ "span",
3772
+ {
3773
+ role: onClick ? "button" : void 0,
3774
+ onClick,
3775
+ className: `inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full border text-[11px] font-medium max-w-full self-start shrink-0 transition-colors ${VARIANTS[variant]} ${onClick ? "cursor-pointer" : ""}`,
3776
+ children: [
3777
+ /* @__PURE__ */ jsx22(IconC, { className: "w-3 h-3 shrink-0", strokeWidth: 1.5 }),
3778
+ /* @__PURE__ */ jsx22("span", { className: "truncate", children: label })
3779
+ ]
3780
+ }
3781
+ );
3782
+ }
3783
+
3784
+ // src/components/ThreadCard.tsx
3785
+ import { Fragment as Fragment5, jsx as jsx23, jsxs as jsxs21 } from "react/jsx-runtime";
3786
+ function ThreadCard({ thread, onOpen, onAnchorClick, shimmer }) {
3787
+ const { internalLabel } = useTaskBoardContext();
3788
+ const initials = getInitials(thread.author_name || "?");
3789
+ const hasContext = !!thread.anchor || thread.attachments.length > 0;
3790
+ const isComplete = thread.status === "complete";
3791
+ return /* @__PURE__ */ jsxs21(
3792
+ "div",
3793
+ {
3794
+ role: "button",
3795
+ tabIndex: 0,
3796
+ onClick: onOpen,
3797
+ onKeyDown: (e) => {
3798
+ if (e.key === "Enter" || e.key === " ") {
3799
+ e.preventDefault();
3800
+ onOpen();
3801
+ }
3802
+ },
3803
+ className: `text-left w-full px-3 py-4 flex flex-col gap-1.5 hover:bg-neutral-50/60 transition-colors cursor-pointer focus:outline-none focus-visible:bg-neutral-50 ${isComplete ? "opacity-60" : ""}`,
3804
+ children: [
3805
+ /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2", children: [
3806
+ shimmer ? /* @__PURE__ */ jsx23("span", { className: "eb-tb-thread-title-skeleton h-4 w-48 rounded shrink-0", "aria-hidden": true }) : /* @__PURE__ */ jsx23(
3807
+ "h4",
3808
+ {
3809
+ className: `text-[13px] font-semibold leading-snug flex-1 min-w-0 truncate ${isComplete ? "text-neutral-400" : "text-neutral-900"}`,
3810
+ children: thread.title
3811
+ }
3812
+ ),
3813
+ isComplete && /* @__PURE__ */ jsxs21("span", { className: "shrink-0 inline-flex items-center gap-1 text-[10px] font-medium text-emerald-700 bg-emerald-50 px-1.5 py-0.5 rounded", children: [
3814
+ /* @__PURE__ */ jsx23(CheckIcon, { size: 10, strokeWidth: 2.5 }),
3815
+ " Complete"
3816
+ ] })
3817
+ ] }),
3818
+ hasContext && /* @__PURE__ */ jsxs21("div", { className: "flex flex-wrap gap-1.5", children: [
3819
+ thread.anchor && /* @__PURE__ */ jsx23(
3820
+ ContextPill,
3821
+ {
3822
+ variant: "amber",
3823
+ icon: MessageSquareIcon,
3824
+ label: `Highlight \xB7 ${sectionLabel(thread.anchor.section)}`,
3825
+ onClick: (e) => {
3826
+ e.stopPropagation();
3827
+ if (thread.anchor) onAnchorClick(thread.anchor);
3828
+ }
3829
+ }
3830
+ ),
3831
+ thread.attachments.slice(0, 3).map((a) => {
3832
+ const variant = a.kind === "image" ? "emerald" : a.kind === "link" ? "blue" : "neutral";
3833
+ const IconC = a.kind === "image" ? ImageIcon : a.kind === "link" ? Link2Icon : FileTextIcon;
3834
+ return /* @__PURE__ */ jsx23(ContextPill, { variant, icon: IconC, label: a.name }, a.id);
3835
+ }),
3836
+ thread.attachments.length > 3 && /* @__PURE__ */ jsxs21("span", { className: "text-[10px] text-neutral-400 self-center", children: [
3837
+ "+",
3838
+ thread.attachments.length - 3
3839
+ ] })
3840
+ ] }),
3841
+ /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2 text-[10px] text-neutral-500", children: [
3842
+ /* @__PURE__ */ jsx23(
3843
+ "div",
3844
+ {
3845
+ className: `w-5 h-5 rounded-full flex items-center justify-center text-[9px] font-semibold shrink-0 ${thread.is_internal ? "bg-neutral-200 text-neutral-700" : "bg-[#FF5E00] text-white"}`,
3846
+ children: initials
3847
+ }
3848
+ ),
3849
+ /* @__PURE__ */ jsx23("span", { className: "font-medium text-neutral-700", children: thread.author_name }),
3850
+ /* @__PURE__ */ jsx23("span", { className: "text-neutral-300", children: "\xB7" }),
3851
+ /* @__PURE__ */ jsx23("span", { children: timeAgo(thread.created_at) }),
3852
+ thread.is_internal && /* @__PURE__ */ jsxs21(Fragment5, { children: [
3853
+ /* @__PURE__ */ jsx23("span", { className: "text-neutral-300", children: "\xB7" }),
3854
+ /* @__PURE__ */ jsxs21("span", { className: "text-[10px] text-neutral-500 bg-neutral-100 px-1.5 py-0.5 rounded inline-flex items-center gap-1", children: [
3855
+ /* @__PURE__ */ jsx23(LockIcon, { size: 10, strokeWidth: 1.5 }),
3856
+ internalLabel
3857
+ ] })
3858
+ ] })
3859
+ ] }),
3860
+ /* @__PURE__ */ jsx23(
3861
+ "div",
3862
+ {
3863
+ className: `text-[12px] leading-relaxed line-clamp-2 ${isComplete ? "text-neutral-400" : "text-neutral-700"}`,
3864
+ children: /* @__PURE__ */ jsx23(MentionText, { text: thread.preview })
3865
+ }
3866
+ ),
3867
+ thread.replies.length > 0 ? /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2 mt-0.5", children: [
3868
+ /* @__PURE__ */ jsxs21("span", { className: "text-[12px] font-medium text-[#FF5E00]", children: [
3869
+ thread.replies.length,
3870
+ " repl",
3871
+ thread.replies.length === 1 ? "y" : "ies"
3872
+ ] }),
3873
+ /* @__PURE__ */ jsxs21("span", { className: "text-[10px] text-neutral-400", children: [
3874
+ "\xB7 last reply ",
3875
+ timeAgo(thread.replies[thread.replies.length - 1].created_at)
3876
+ ] })
3877
+ ] }) : /* @__PURE__ */ jsxs21("span", { className: "self-start inline-flex items-center gap-1.5 mt-0.5 text-[12px] font-medium text-[#FF5E00]", children: [
3878
+ /* @__PURE__ */ jsx23(CornerUpLeftIcon, { size: 12, strokeWidth: 1.5 }),
3879
+ "Reply"
3880
+ ] })
3881
+ ]
3882
+ }
3883
+ );
3884
+ }
3885
+
3886
+ // src/components/ThreadComposer.tsx
3887
+ import { useRef as useRef12, useState as useState13 } from "react";
3888
+ import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
3889
+ function ThreadComposer({
3890
+ attachments,
3891
+ pendingAnchor,
3892
+ onClearAnchor,
3893
+ onSubmit,
3894
+ onUpload,
3895
+ onAddLink,
3896
+ isInternalUser,
3897
+ open,
3898
+ onOpen,
3899
+ onClose
3900
+ }) {
3901
+ const { internalLabel } = useTaskBoardContext();
3902
+ const [title, setTitle] = useState13("");
3903
+ const [body, setBody] = useState13("");
3904
+ const [internal, setInternal] = useState13(false);
3905
+ const [attachmentIds, setAttachmentIds] = useState13([]);
3906
+ const [posting, setPosting] = useState13(false);
3907
+ const closeAndReset = () => {
3908
+ setTitle("");
3909
+ setBody("");
3910
+ setInternal(false);
3911
+ setAttachmentIds([]);
3912
+ onClose();
3913
+ };
3914
+ const handleSubmit = async () => {
3915
+ if (!body.trim()) return;
3916
+ setPosting(true);
3917
+ try {
3918
+ await onSubmit({
3919
+ title: title.trim(),
3920
+ content: body.trim(),
3921
+ isInternal: internal,
3922
+ anchor: pendingAnchor,
3923
+ attachmentIds
3924
+ });
3925
+ closeAndReset();
3926
+ } finally {
3927
+ setPosting(false);
3928
+ }
3929
+ };
3930
+ if (!open) {
3931
+ return /* @__PURE__ */ jsx24("div", { className: "shrink-0 p-3 border-t border-neutral-100 bg-white", children: /* @__PURE__ */ jsxs22(
3932
+ "button",
3933
+ {
3934
+ onClick: onOpen,
3935
+ className: "w-full inline-flex items-center justify-center gap-2 font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded-xl h-10 px-4 shadow-sm text-[12px]",
3936
+ children: [
3937
+ /* @__PURE__ */ jsx24("span", { className: "inline-flex items-center justify-center w-4 h-4 rounded-full bg-white/20", children: "+" }),
3938
+ "Start a thread"
3939
+ ]
3940
+ }
3941
+ ) });
3942
+ }
3943
+ return /* @__PURE__ */ jsx24("div", { className: "shrink-0 p-3 border-t border-neutral-100 bg-white", children: /* @__PURE__ */ jsxs22("div", { className: "flex flex-col gap-2", children: [
3944
+ pendingAnchor && /* @__PURE__ */ jsxs22("div", { className: "flex items-start gap-2 px-2 py-1.5 rounded-md bg-amber-50 border border-amber-200", children: [
3945
+ /* @__PURE__ */ jsx24(
3946
+ MessageSquareIcon,
3947
+ {
3948
+ size: 14,
3949
+ strokeWidth: 1.5,
3950
+ className: "text-amber-600 shrink-0 mt-0.5"
3951
+ }
3952
+ ),
3953
+ /* @__PURE__ */ jsxs22("div", { className: "flex-1 min-w-0 text-[11px] text-amber-700", children: [
3954
+ /* @__PURE__ */ jsxs22("div", { className: "font-medium uppercase tracking-wider text-[9px]", children: [
3955
+ sectionLabel(pendingAnchor.section),
3956
+ " highlight"
3957
+ ] }),
3958
+ /* @__PURE__ */ jsxs22("div", { className: "truncate italic", children: [
3959
+ "\u201C",
3960
+ pendingAnchor.snippet,
3961
+ "\u201D"
3962
+ ] })
3963
+ ] }),
3964
+ /* @__PURE__ */ jsx24(
3965
+ "button",
3966
+ {
3967
+ type: "button",
3968
+ onClick: onClearAnchor,
3969
+ className: "text-amber-600 hover:text-amber-900",
3970
+ title: "Remove highlight",
3971
+ children: /* @__PURE__ */ jsx24(XIcon, { size: 12, strokeWidth: 2 })
3972
+ }
3973
+ )
3974
+ ] }),
3975
+ /* @__PURE__ */ jsx24(
3976
+ "input",
3977
+ {
3978
+ type: "text",
3979
+ value: title,
3980
+ onChange: (e) => setTitle(e.target.value),
3981
+ placeholder: "Add a title (optional)",
3982
+ className: "w-full rounded-lg border border-neutral-200 px-3 py-2 text-[13px] font-medium text-neutral-900 placeholder:font-normal placeholder:text-neutral-400 focus:outline-none focus:ring-1 focus:ring-neutral-300 focus:border-neutral-300"
3983
+ }
3984
+ ),
3985
+ /* @__PURE__ */ jsx24(
3986
+ MentionTextarea,
3987
+ {
3988
+ value: body,
3989
+ onChange: setBody,
3990
+ rows: 4,
3991
+ placeholder: "Write something\u2026 (type @ to mention)",
3992
+ className: "w-full rounded-lg border border-neutral-200 px-3 py-2 text-[12px] text-neutral-800 min-h-[100px] focus:outline-none focus:ring-1 focus:ring-neutral-300 focus:border-neutral-300 resize-none"
3993
+ }
3994
+ ),
3995
+ /* @__PURE__ */ jsx24(
3996
+ ComposerAttachments,
3997
+ {
3998
+ attachmentIds,
3999
+ attachments,
4000
+ onChange: setAttachmentIds,
4001
+ onUpload,
4002
+ onAddLink
4003
+ }
4004
+ ),
4005
+ /* @__PURE__ */ jsxs22("div", { className: "flex items-center justify-between gap-2", children: [
4006
+ /* @__PURE__ */ jsx24(
4007
+ "button",
4008
+ {
4009
+ onClick: closeAndReset,
4010
+ className: "text-[12px] font-medium text-neutral-500 hover:text-neutral-900 px-2 h-7 rounded hover:bg-neutral-50",
4011
+ children: "Cancel"
4012
+ }
4013
+ ),
4014
+ /* @__PURE__ */ jsxs22("div", { className: "flex items-center gap-2", children: [
4015
+ isInternalUser && /* @__PURE__ */ jsxs22(
4016
+ "button",
4017
+ {
4018
+ type: "button",
4019
+ onClick: () => setInternal(!internal),
4020
+ "aria-pressed": internal,
4021
+ className: `inline-flex items-center gap-1.5 px-2 h-7 rounded text-[12px] font-medium transition-colors ${internal ? "bg-neutral-100 text-neutral-700" : "bg-white text-neutral-500 border border-neutral-200 hover:bg-neutral-50"}`,
4022
+ children: [
4023
+ /* @__PURE__ */ jsx24(LockIcon, { size: 14, strokeWidth: 1.5 }),
4024
+ internal ? internalLabel : "Public"
4025
+ ]
4026
+ }
4027
+ ),
4028
+ /* @__PURE__ */ jsx24(
4029
+ "button",
4030
+ {
4031
+ onClick: handleSubmit,
4032
+ disabled: !body.trim() || posting,
4033
+ className: "inline-flex items-center justify-center px-3 h-7 rounded-lg text-[12px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] disabled:opacity-50",
4034
+ children: posting ? "Posting..." : "Post"
4035
+ }
4036
+ )
4037
+ ] })
4038
+ ] })
4039
+ ] }) });
4040
+ }
4041
+ function ComposerAttachments({
4042
+ attachmentIds,
4043
+ attachments,
4044
+ onChange,
4045
+ onUpload,
4046
+ onAddLink
4047
+ }) {
4048
+ const [busy, setBusy] = useState13(false);
4049
+ const [linkOpen, setLinkOpen] = useState13(false);
4050
+ const [linkUrl, setLinkUrl] = useState13("");
4051
+ const [linkName, setLinkName] = useState13("");
4052
+ const imageRef = useRef12(null);
4053
+ const fileRef = useRef12(null);
4054
+ const selected = attachmentIds.map((id) => attachments.find((a) => a.id === id)).filter((a) => Boolean(a));
4055
+ const upload = async (file) => {
4056
+ setBusy(true);
4057
+ try {
4058
+ const created = await onUpload(file);
4059
+ onChange([...attachmentIds, created.id]);
4060
+ } finally {
4061
+ setBusy(false);
4062
+ }
4063
+ };
4064
+ const submitLink = async () => {
4065
+ if (!linkUrl.trim()) return;
4066
+ setBusy(true);
4067
+ try {
4068
+ const created = await onAddLink(linkUrl.trim(), linkName.trim() || void 0);
4069
+ onChange([...attachmentIds, created.id]);
4070
+ setLinkUrl("");
4071
+ setLinkName("");
4072
+ setLinkOpen(false);
4073
+ } finally {
4074
+ setBusy(false);
4075
+ }
4076
+ };
4077
+ return /* @__PURE__ */ jsxs22(Fragment6, { children: [
4078
+ selected.length > 0 && /* @__PURE__ */ jsx24("div", { className: "flex flex-wrap gap-1.5", children: selected.map((a) => {
4079
+ const IconC = a.kind === "image" ? ImageIcon : a.kind === "link" ? Link2Icon : FileTextIcon;
4080
+ return /* @__PURE__ */ jsxs22(
4081
+ "span",
4082
+ {
4083
+ className: "inline-flex items-center gap-1.5 text-[11px] text-neutral-700 bg-neutral-100 border border-neutral-200 rounded-md px-2 py-1 max-w-full",
4084
+ children: [
4085
+ /* @__PURE__ */ jsx24(IconC, { size: 12, strokeWidth: 1.5 }),
4086
+ /* @__PURE__ */ jsx24("span", { className: "truncate max-w-[180px]", children: a.name }),
4087
+ /* @__PURE__ */ jsx24(
4088
+ "button",
4089
+ {
4090
+ type: "button",
4091
+ onClick: () => onChange(attachmentIds.filter((id) => id !== a.id)),
4092
+ className: "text-neutral-400 hover:text-neutral-700",
4093
+ children: /* @__PURE__ */ jsx24(XIcon, { size: 12, strokeWidth: 2 })
4094
+ }
4095
+ )
4096
+ ]
4097
+ },
4098
+ a.id
4099
+ );
4100
+ }) }),
4101
+ /* @__PURE__ */ jsxs22("div", { className: "flex items-center gap-1", children: [
4102
+ /* @__PURE__ */ jsx24(
4103
+ "button",
4104
+ {
4105
+ type: "button",
4106
+ disabled: busy,
4107
+ onClick: () => imageRef.current?.click(),
4108
+ className: "w-7 h-7 rounded hover:bg-neutral-100 text-neutral-500 hover:text-neutral-900 flex items-center justify-center disabled:opacity-50",
4109
+ "aria-label": "Attach image",
4110
+ title: "Attach image",
4111
+ children: /* @__PURE__ */ jsx24(ImageIcon, { size: 14, strokeWidth: 1.5 })
4112
+ }
4113
+ ),
4114
+ /* @__PURE__ */ jsx24(
4115
+ "button",
4116
+ {
4117
+ type: "button",
4118
+ disabled: busy,
4119
+ onClick: () => fileRef.current?.click(),
4120
+ className: "w-7 h-7 rounded hover:bg-neutral-100 text-neutral-500 hover:text-neutral-900 flex items-center justify-center disabled:opacity-50",
4121
+ "aria-label": "Attach file",
4122
+ title: "Attach file",
4123
+ children: /* @__PURE__ */ jsx24(FileTextIcon, { size: 14, strokeWidth: 1.5 })
4124
+ }
4125
+ ),
4126
+ /* @__PURE__ */ jsx24(
4127
+ "button",
4128
+ {
4129
+ type: "button",
4130
+ disabled: busy,
4131
+ onClick: () => setLinkOpen((v) => !v),
4132
+ className: "w-7 h-7 rounded hover:bg-neutral-100 text-neutral-500 hover:text-neutral-900 flex items-center justify-center disabled:opacity-50",
4133
+ "aria-label": "Attach link",
4134
+ title: "Attach link",
4135
+ children: /* @__PURE__ */ jsx24(Link2Icon, { size: 14, strokeWidth: 1.5 })
4136
+ }
4137
+ ),
4138
+ busy && /* @__PURE__ */ jsx24("span", { className: "text-[10px] text-neutral-400 ml-1", children: "Uploading\u2026" })
4139
+ ] }),
4140
+ linkOpen && /* @__PURE__ */ jsxs22("div", { className: "rounded-md border border-neutral-200 p-2 flex flex-col gap-1.5", children: [
4141
+ /* @__PURE__ */ jsx24(
4142
+ "input",
4143
+ {
4144
+ type: "url",
4145
+ value: linkUrl,
4146
+ onChange: (e) => setLinkUrl(e.target.value),
4147
+ placeholder: "https://\u2026",
4148
+ className: "text-[12px] px-2 py-1 border border-neutral-200 rounded focus:outline-none focus:ring-1 focus:ring-neutral-300"
4149
+ }
4150
+ ),
4151
+ /* @__PURE__ */ jsx24(
4152
+ "input",
4153
+ {
4154
+ type: "text",
4155
+ value: linkName,
4156
+ onChange: (e) => setLinkName(e.target.value),
4157
+ placeholder: "Title (optional)",
4158
+ className: "text-[12px] px-2 py-1 border border-neutral-200 rounded focus:outline-none focus:ring-1 focus:ring-neutral-300"
4159
+ }
4160
+ ),
4161
+ /* @__PURE__ */ jsxs22("div", { className: "flex justify-end gap-1.5", children: [
4162
+ /* @__PURE__ */ jsx24(
4163
+ "button",
4164
+ {
4165
+ type: "button",
4166
+ onClick: () => {
4167
+ setLinkOpen(false);
4168
+ setLinkUrl("");
4169
+ setLinkName("");
4170
+ },
4171
+ className: "text-[11px] text-neutral-500 hover:text-neutral-900 px-2 h-6 rounded hover:bg-neutral-50",
4172
+ children: "Cancel"
4173
+ }
4174
+ ),
4175
+ /* @__PURE__ */ jsx24(
4176
+ "button",
4177
+ {
4178
+ type: "button",
4179
+ onClick: submitLink,
4180
+ disabled: !linkUrl.trim() || busy,
4181
+ className: "text-[11px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] disabled:opacity-50 px-2 h-6 rounded",
4182
+ children: "Add"
4183
+ }
4184
+ )
4185
+ ] })
4186
+ ] }),
4187
+ /* @__PURE__ */ jsx24(
4188
+ "input",
4189
+ {
4190
+ ref: imageRef,
4191
+ type: "file",
4192
+ accept: "image/*",
4193
+ className: "hidden",
4194
+ onChange: (e) => {
4195
+ const f = e.target.files?.[0];
4196
+ if (f) upload(f);
4197
+ e.target.value = "";
4198
+ }
4199
+ }
4200
+ ),
4201
+ /* @__PURE__ */ jsx24(
4202
+ "input",
4203
+ {
4204
+ ref: fileRef,
4205
+ type: "file",
4206
+ className: "hidden",
4207
+ onChange: (e) => {
4208
+ const f = e.target.files?.[0];
4209
+ if (f) upload(f);
4210
+ e.target.value = "";
4211
+ }
4212
+ }
4213
+ )
4214
+ ] });
4215
+ }
4216
+
4217
+ // src/components/ThreadDetailView.tsx
4218
+ import { useEffect as useEffect12, useRef as useRef13, useState as useState14 } from "react";
4219
+ import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
4220
+ function ThreadDetailView({
4221
+ thread,
4222
+ onBack,
4223
+ onReply,
4224
+ onUpdateThread,
4225
+ onAnchorClick,
4226
+ isInternalUser
4227
+ }) {
4228
+ const { internalLabel } = useTaskBoardContext();
4229
+ const [body, setBody] = useState14("");
4230
+ const [internal, setInternal] = useState14(false);
4231
+ const [posting, setPosting] = useState14(false);
4232
+ const [editingTitle, setEditingTitle] = useState14(false);
4233
+ const [titleDraft, setTitleDraft] = useState14(thread.title);
4234
+ const messagesEndRef = useRef13(null);
4235
+ useEffect12(() => {
4236
+ setTitleDraft(thread.title);
4237
+ }, [thread.title]);
4238
+ const submit = async () => {
4239
+ if (!body.trim()) return;
4240
+ setPosting(true);
4241
+ try {
4242
+ await onReply(body.trim(), internal);
4243
+ setBody("");
4244
+ setInternal(false);
4245
+ setTimeout(() => {
4246
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
4247
+ }, 100);
4248
+ } finally {
4249
+ setPosting(false);
4250
+ }
4251
+ };
4252
+ const commitTitle = async () => {
4253
+ setEditingTitle(false);
4254
+ const next = titleDraft.trim();
4255
+ if (next && next !== thread.title) {
4256
+ await onUpdateThread({ title: next });
4257
+ } else {
4258
+ setTitleDraft(thread.title);
4259
+ }
4260
+ };
4261
+ const toggleStatus = async () => {
4262
+ const next = thread.status === "complete" ? "active" : "complete";
4263
+ await onUpdateThread({ thread_status: next });
4264
+ };
4265
+ const isComplete = thread.status === "complete";
4266
+ return /* @__PURE__ */ jsxs23(Fragment7, { children: [
4267
+ /* @__PURE__ */ jsxs23("div", { className: "shrink-0 bg-white px-4 py-3 border-b border-neutral-100", children: [
4268
+ /* @__PURE__ */ jsxs23("div", { className: "flex items-start justify-between gap-4", children: [
4269
+ /* @__PURE__ */ jsxs23(
4270
+ "button",
4271
+ {
4272
+ onClick: onBack,
4273
+ className: "inline-flex items-center gap-2 px-2.5 py-1.5 -ml-2 rounded-lg text-[12px] font-medium text-neutral-500 hover:text-neutral-900 hover:bg-neutral-50",
4274
+ children: [
4275
+ /* @__PURE__ */ jsx25(ArrowLeftIcon, { size: 14, strokeWidth: 1.5 }),
4276
+ /* @__PURE__ */ jsx25("span", { children: "All threads" })
4277
+ ]
4278
+ }
4279
+ ),
4280
+ /* @__PURE__ */ jsx25(
4281
+ "button",
4282
+ {
4283
+ type: "button",
4284
+ onClick: toggleStatus,
4285
+ className: "inline-flex items-center justify-center gap-1.5 text-[12px] font-medium text-neutral-600 hover:text-neutral-900 px-3 h-8 rounded-xl border border-neutral-200 bg-white hover:bg-neutral-50",
4286
+ children: isComplete ? /* @__PURE__ */ jsxs23(Fragment7, { children: [
4287
+ /* @__PURE__ */ jsx25(RotateCcwIcon, { size: 12, strokeWidth: 1.5 }),
4288
+ "Reopen"
4289
+ ] }) : /* @__PURE__ */ jsxs23(Fragment7, { children: [
4290
+ /* @__PURE__ */ jsx25(CheckCircle2Icon, { size: 12, strokeWidth: 1.5 }),
4291
+ "Mark complete"
4292
+ ] })
4293
+ }
4294
+ )
4295
+ ] }),
4296
+ editingTitle ? /* @__PURE__ */ jsx25(
4297
+ "input",
4298
+ {
4299
+ value: titleDraft,
4300
+ onChange: (e) => setTitleDraft(e.target.value),
4301
+ onBlur: commitTitle,
4302
+ onKeyDown: (e) => {
4303
+ if (e.key === "Enter") {
4304
+ e.preventDefault();
4305
+ e.target.blur();
4306
+ }
4307
+ if (e.key === "Escape") {
4308
+ e.preventDefault();
4309
+ setTitleDraft(thread.title);
4310
+ setEditingTitle(false);
4311
+ }
4312
+ },
4313
+ autoFocus: true,
4314
+ className: "mt-2 w-full text-[15px] font-semibold text-neutral-900 leading-snug bg-transparent border-b border-[#FF5E00] focus:outline-none -mx-1 px-1"
4315
+ }
4316
+ ) : /* @__PURE__ */ jsx25(
4317
+ "h3",
4318
+ {
4319
+ onClick: () => setEditingTitle(true),
4320
+ className: "mt-2 text-[15px] font-semibold text-neutral-900 leading-snug cursor-text rounded -mx-1 px-1 hover:bg-neutral-50/60",
4321
+ title: "Click to edit",
4322
+ children: thread.title
4323
+ }
4324
+ ),
4325
+ thread.anchor && /* @__PURE__ */ jsx25("div", { className: "mt-2", children: /* @__PURE__ */ jsx25(
4326
+ ContextPill,
4327
+ {
4328
+ variant: "amber",
4329
+ icon: MessageSquareIcon,
4330
+ label: `Highlight \xB7 ${sectionLabel(thread.anchor.section)}`,
4331
+ onClick: () => thread.anchor && onAnchorClick(thread.anchor)
4332
+ }
4333
+ ) })
4334
+ ] }),
4335
+ /* @__PURE__ */ jsxs23("div", { className: "flex-1 overflow-y-auto p-3 flex flex-col gap-4", children: [
4336
+ /* @__PURE__ */ jsx25(
4337
+ ThreadMessage,
4338
+ {
4339
+ authorName: thread.author_name,
4340
+ createdAt: thread.created_at,
4341
+ content: thread.rawContent,
4342
+ isInternal: thread.is_internal,
4343
+ internalLabel
4344
+ }
4345
+ ),
4346
+ thread.replies.map((r) => /* @__PURE__ */ jsx25(
4347
+ ThreadMessage,
4348
+ {
4349
+ authorName: r.author_name,
4350
+ createdAt: r.created_at,
4351
+ content: r.content,
4352
+ isInternal: r.is_internal,
4353
+ internalLabel
4354
+ },
4355
+ r.id
4356
+ )),
4357
+ /* @__PURE__ */ jsx25("div", { ref: messagesEndRef })
4358
+ ] }),
4359
+ /* @__PURE__ */ jsxs23("div", { className: "shrink-0 p-3 border-t border-neutral-100 bg-white", children: [
4360
+ /* @__PURE__ */ jsx25(
4361
+ MentionTextarea,
4362
+ {
4363
+ value: body,
4364
+ onChange: setBody,
4365
+ rows: 3,
4366
+ placeholder: "Reply\u2026 (type @ to mention)",
4367
+ className: "w-full rounded-lg border border-neutral-200 px-3 py-2 text-[12px] text-neutral-800 min-h-[80px] focus:outline-none focus:ring-1 focus:ring-neutral-300 focus:border-neutral-300 resize-none"
4368
+ }
4369
+ ),
4370
+ /* @__PURE__ */ jsxs23("div", { className: "flex items-center justify-end gap-2 mt-2", children: [
4371
+ isInternalUser && /* @__PURE__ */ jsxs23(
4372
+ "button",
4373
+ {
4374
+ type: "button",
4375
+ onClick: () => setInternal(!internal),
4376
+ "aria-pressed": internal,
4377
+ className: `inline-flex items-center gap-1.5 px-2 h-7 rounded text-[12px] font-medium transition-colors ${internal ? "bg-neutral-100 text-neutral-700" : "bg-white text-neutral-500 border border-neutral-200 hover:bg-neutral-50"}`,
4378
+ children: [
4379
+ /* @__PURE__ */ jsx25(LockIcon, { size: 14, strokeWidth: 1.5 }),
4380
+ internal ? internalLabel : "Public"
4381
+ ]
4382
+ }
4383
+ ),
4384
+ /* @__PURE__ */ jsx25(
4385
+ "button",
4386
+ {
4387
+ onClick: submit,
4388
+ disabled: !body.trim() || posting,
4389
+ className: "inline-flex items-center justify-center px-3 h-7 rounded-lg text-[12px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] disabled:opacity-50",
4390
+ children: posting ? "Posting..." : "Reply"
4391
+ }
4392
+ )
4393
+ ] })
4394
+ ] })
4395
+ ] });
4396
+ }
4397
+ function ThreadMessage({
4398
+ authorName,
4399
+ createdAt,
4400
+ content,
4401
+ isInternal,
4402
+ internalLabel
4403
+ }) {
4404
+ const initials = getInitials(authorName || "?");
4405
+ return /* @__PURE__ */ jsxs23("div", { className: "flex gap-2.5", children: [
4406
+ /* @__PURE__ */ jsx25(
4407
+ "div",
4408
+ {
4409
+ className: `w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0 ${isInternal ? "bg-neutral-200 text-neutral-700" : "bg-[#FF5E00] text-white"}`,
4410
+ children: initials
4411
+ }
4412
+ ),
4413
+ /* @__PURE__ */ jsxs23("div", { className: "flex-1 min-w-0", children: [
4414
+ /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2 mb-0.5 flex-wrap", children: [
4415
+ /* @__PURE__ */ jsx25("span", { className: "text-[13px] font-medium text-neutral-900", children: authorName }),
4416
+ /* @__PURE__ */ jsx25("span", { className: "text-[10px] text-neutral-400", children: timeAgo(createdAt) }),
4417
+ isInternal && /* @__PURE__ */ jsxs23("span", { className: "text-[10px] text-neutral-500 bg-neutral-100 px-1.5 py-0.5 rounded inline-flex items-center gap-1", children: [
4418
+ /* @__PURE__ */ jsx25(LockIcon, { size: 10, strokeWidth: 1.5 }),
4419
+ internalLabel
4420
+ ] })
4421
+ ] }),
4422
+ /* @__PURE__ */ jsx25("div", { className: "text-[12px] text-neutral-700 leading-relaxed whitespace-pre-wrap", children: /* @__PURE__ */ jsx25(MentionText, { text: content }) })
4423
+ ] })
4424
+ ] });
4425
+ }
4426
+
4427
+ // src/components/ThreadsPanel.tsx
4428
+ import { Fragment as Fragment8, jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
4429
+ function ThreadsPanel({
4430
+ threads,
4431
+ activity,
4432
+ attachments,
4433
+ open,
4434
+ onToggle,
4435
+ openThreadId,
4436
+ onOpenThread,
4437
+ pendingAnchor,
4438
+ onClearAnchor,
4439
+ onAnchorClick,
4440
+ shimmeringThreadIds,
4441
+ isInternalUser,
4442
+ onCreateThread,
4443
+ onCreateReply,
4444
+ onUpdateThread,
4445
+ onUploadAttachment,
4446
+ onAddLinkAttachment
4447
+ }) {
4448
+ const [tab, setTab] = useState15("threads");
4449
+ const [composerOpen, setComposerOpen] = useState15(false);
4450
+ const [filter, setFilter] = useState15("active");
4451
+ const [filterMenuOpen, setFilterMenuOpen] = useState15(false);
4452
+ const filterRef = useRef14(null);
4453
+ useEffect13(() => {
4454
+ if (pendingAnchor) {
4455
+ setComposerOpen(true);
4456
+ setTab("threads");
4457
+ onOpenThread(null);
4458
+ }
4459
+ }, [pendingAnchor, onOpenThread]);
4460
+ useEffect13(() => {
4461
+ if (!filterMenuOpen) return;
4462
+ const onClick = (e) => {
4463
+ if (filterRef.current && !filterRef.current.contains(e.target)) {
4464
+ setFilterMenuOpen(false);
4465
+ }
4466
+ };
4467
+ document.addEventListener("mousedown", onClick);
4468
+ return () => document.removeEventListener("mousedown", onClick);
4469
+ }, [filterMenuOpen]);
4470
+ const filteredThreads = threads.filter((t) => t.status === filter);
4471
+ const openThread = openThreadId ? threads.find((t) => t.id === openThreadId) : null;
4472
+ useEffect13(() => {
4473
+ if (openThreadId && !threads.some((t) => t.id === openThreadId)) {
4474
+ onOpenThread(null);
4475
+ }
4476
+ }, [threads, openThreadId, onOpenThread]);
4477
+ if (!open) {
4478
+ return /* @__PURE__ */ jsx26("aside", { className: "hidden xl:flex flex-col fixed top-[56px] bottom-0 right-0 w-14 border-l border-neutral-200 bg-white z-30", children: /* @__PURE__ */ jsx26("div", { className: "shrink-0 flex bg-white h-12 border-b border-neutral-200 items-center justify-center", children: /* @__PURE__ */ jsx26(
4479
+ "button",
4480
+ {
4481
+ type: "button",
4482
+ onClick: onToggle,
4483
+ "aria-label": "Open thread panel",
4484
+ title: "Open thread panel",
4485
+ className: "w-7 h-7 flex items-center justify-center rounded hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600",
4486
+ children: /* @__PURE__ */ jsx26(SidebarToggleIcon, { size: 18, strokeWidth: 1.5 })
4487
+ }
4488
+ ) }) });
4489
+ }
4490
+ return /* @__PURE__ */ jsxs24("aside", { className: "hidden xl:flex flex-col fixed top-[56px] bottom-0 right-0 w-[420px] border-l border-neutral-200 bg-white z-30", children: [
4491
+ /* @__PURE__ */ jsxs24("div", { className: "shrink-0 flex bg-white h-12 border-b border-neutral-200 px-5 items-center justify-between", children: [
4492
+ /* @__PURE__ */ jsxs24("span", { className: "text-[13px] font-medium text-neutral-500", children: [
4493
+ tab === "threads" ? "Threads" : "Activity",
4494
+ " ",
4495
+ /* @__PURE__ */ jsx26("span", { className: "text-neutral-400 ml-0.5", children: tab === "threads" ? threads.length : activity.length })
4496
+ ] }),
4497
+ /* @__PURE__ */ jsxs24("div", { className: "flex items-center gap-1", role: "tablist", children: [
4498
+ /* @__PURE__ */ jsx26(
4499
+ "button",
4500
+ {
4501
+ type: "button",
4502
+ onClick: () => {
4503
+ setTab("threads");
4504
+ onOpenThread(null);
4505
+ },
4506
+ "aria-pressed": tab === "threads",
4507
+ className: `w-7 h-7 flex items-center justify-center rounded ${tab === "threads" ? "bg-neutral-100 text-neutral-700" : "text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600"}`,
4508
+ "aria-label": "Threads",
4509
+ children: /* @__PURE__ */ jsx26(ChatDotsIcon, { size: 16, strokeWidth: 1.5 })
4510
+ }
4511
+ ),
4512
+ /* @__PURE__ */ jsx26(
4513
+ "button",
4514
+ {
4515
+ type: "button",
4516
+ onClick: () => {
4517
+ setTab("activity");
4518
+ onOpenThread(null);
4519
+ },
4520
+ "aria-pressed": tab === "activity",
4521
+ className: `w-7 h-7 flex items-center justify-center rounded ${tab === "activity" ? "bg-neutral-100 text-neutral-700" : "text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600"}`,
4522
+ "aria-label": "Activity",
4523
+ children: /* @__PURE__ */ jsx26(HistoryIcon, { size: 16, strokeWidth: 1.5 })
4524
+ }
4525
+ ),
4526
+ /* @__PURE__ */ jsx26("span", { className: "w-px h-4 bg-neutral-200 mx-1" }),
4527
+ /* @__PURE__ */ jsx26(
4528
+ "button",
4529
+ {
4530
+ type: "button",
4531
+ onClick: onToggle,
4532
+ "aria-label": "Close thread panel",
4533
+ title: "Close thread panel",
4534
+ className: "w-7 h-7 flex items-center justify-center rounded text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600",
4535
+ children: /* @__PURE__ */ jsx26(SidebarToggleIcon, { size: 18, strokeWidth: 1.5 })
4536
+ }
4537
+ )
4538
+ ] })
4539
+ ] }),
4540
+ tab === "threads" && !openThread && /* @__PURE__ */ jsxs24(Fragment8, { children: [
4541
+ /* @__PURE__ */ jsxs24("div", { className: "shrink-0 px-4 pt-3 pb-2 bg-white relative", ref: filterRef, children: [
4542
+ /* @__PURE__ */ jsxs24(
4543
+ "button",
4544
+ {
4545
+ onClick: () => setFilterMenuOpen(!filterMenuOpen),
4546
+ className: "inline-flex items-center gap-1.5 text-[12px] font-medium text-neutral-700 hover:text-neutral-900",
4547
+ children: [
4548
+ /* @__PURE__ */ jsx26("span", { children: filter === "active" ? "Active threads" : "Completed threads" }),
4549
+ /* @__PURE__ */ jsx26("span", { className: "text-[10px] text-neutral-400", children: filteredThreads.length }),
4550
+ /* @__PURE__ */ jsx26(ChevronDownIcon, { size: 12, strokeWidth: 1.5, className: "text-neutral-400" })
4551
+ ]
4552
+ }
4553
+ ),
4554
+ filterMenuOpen && /* @__PURE__ */ jsx26("div", { className: "absolute left-4 top-full mt-0.5 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-20 min-w-[200px]", children: ["active", "complete"].map((f) => /* @__PURE__ */ jsxs24(
4555
+ "button",
4556
+ {
4557
+ onClick: () => {
4558
+ setFilter(f);
4559
+ setFilterMenuOpen(false);
4560
+ },
4561
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center justify-between gap-3",
4562
+ children: [
4563
+ /* @__PURE__ */ jsxs24("span", { className: "flex items-center gap-2", children: [
4564
+ /* @__PURE__ */ jsx26(
4565
+ CheckIcon,
4566
+ {
4567
+ size: 12,
4568
+ strokeWidth: 2.5,
4569
+ className: `text-[#FF5E00] ${filter === f ? "" : "invisible"}`
4570
+ }
4571
+ ),
4572
+ /* @__PURE__ */ jsx26("span", { children: f === "active" ? "Active threads" : "Completed threads" })
4573
+ ] }),
4574
+ /* @__PURE__ */ jsx26("span", { className: "text-[10px] text-neutral-400", children: threads.filter((t) => t.status === f).length })
4575
+ ]
4576
+ },
4577
+ f
4578
+ )) })
4579
+ ] }),
4580
+ /* @__PURE__ */ jsxs24("div", { className: "flex-1 overflow-y-auto px-2 flex flex-col divide-y divide-neutral-100", children: [
4581
+ filteredThreads.length === 0 && /* @__PURE__ */ jsxs24("div", { className: "flex flex-col items-center justify-center py-16 text-center", children: [
4582
+ /* @__PURE__ */ jsx26("div", { className: "w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center mb-3", children: /* @__PURE__ */ jsx26(ChatDotsIcon, { size: 18, strokeWidth: 1.5, className: "text-neutral-400" }) }),
4583
+ /* @__PURE__ */ jsx26("p", { className: "text-[12px] text-neutral-400", children: "No threads yet" }),
4584
+ /* @__PURE__ */ jsx26("p", { className: "text-[10px] text-neutral-400 mt-0.5", children: "Start a thread to discuss this task." })
4585
+ ] }),
4586
+ filteredThreads.map((t) => /* @__PURE__ */ jsx26(
4587
+ ThreadCard,
4588
+ {
4589
+ thread: t,
4590
+ onOpen: () => onOpenThread(t.id),
4591
+ onAnchorClick,
4592
+ shimmer: shimmeringThreadIds.has(t.id)
4593
+ },
4594
+ t.id
4595
+ ))
4596
+ ] }),
4597
+ /* @__PURE__ */ jsx26(
4598
+ ThreadComposer,
4599
+ {
4600
+ attachments,
4601
+ pendingAnchor,
4602
+ onClearAnchor,
4603
+ onSubmit: onCreateThread,
4604
+ onUpload: onUploadAttachment,
4605
+ onAddLink: onAddLinkAttachment,
4606
+ isInternalUser,
4607
+ open: composerOpen,
4608
+ onOpen: () => setComposerOpen(true),
4609
+ onClose: () => {
4610
+ setComposerOpen(false);
4611
+ onClearAnchor();
4612
+ }
4613
+ }
4614
+ )
4615
+ ] }),
4616
+ tab === "threads" && openThread && /* @__PURE__ */ jsx26(
4617
+ ThreadDetailView,
4618
+ {
4619
+ thread: openThread,
4620
+ onBack: () => onOpenThread(null),
4621
+ onReply: (content, isInternal) => onCreateReply(openThread.id, content, isInternal),
4622
+ onUpdateThread: (body) => onUpdateThread(openThread.id, body),
4623
+ onAnchorClick,
4624
+ isInternalUser
4625
+ }
4626
+ ),
4627
+ tab === "activity" && /* @__PURE__ */ jsx26(ActivityList, { activity })
4628
+ ] });
4629
+ }
4630
+
4631
+ // src/components/HighlightBubble.tsx
4632
+ import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
4633
+ function HighlightBubble({ bubble, onComment }) {
4634
+ if (!bubble) return null;
4635
+ return /* @__PURE__ */ jsx27(
4636
+ "div",
4637
+ {
4638
+ "data-annot-bubble": true,
4639
+ style: {
4640
+ position: "absolute",
4641
+ left: bubble.x,
4642
+ top: bubble.y,
4643
+ transform: "translate(-50%, -100%)"
4644
+ },
4645
+ className: "z-[90] pointer-events-auto",
4646
+ children: /* @__PURE__ */ jsxs25(
4647
+ "button",
4648
+ {
4649
+ type: "button",
4650
+ onMouseDown: (e) => e.preventDefault(),
4651
+ onClick: onComment,
4652
+ className: "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-neutral-900 text-white text-[12px] font-medium shadow-lg hover:bg-neutral-800",
4653
+ children: [
4654
+ /* @__PURE__ */ jsx27(MessageSquareIcon, { size: 14, strokeWidth: 1.5 }),
4655
+ "Comment"
4656
+ ]
4657
+ }
4658
+ )
4659
+ }
4660
+ );
4661
+ }
4662
+
4663
+ // src/hooks/useTaskQuestions.ts
4664
+ import { useCallback as useCallback8, useEffect as useEffect14, useState as useState16 } from "react";
4665
+ function useTaskQuestions(taskId, initial) {
4666
+ const { service } = useTaskBoardContext();
4667
+ const [questions, setQuestions] = useState16(initial ?? []);
4668
+ const [loading, setLoading] = useState16(false);
4669
+ const refresh = useCallback8(async () => {
4670
+ if (!taskId) return;
4671
+ setLoading(true);
4672
+ try {
4673
+ const list = await service.listQuestions(taskId);
4674
+ setQuestions(list);
4675
+ } finally {
4676
+ setLoading(false);
4677
+ }
4678
+ }, [service, taskId]);
4679
+ useEffect14(() => {
4680
+ if (initial !== void 0) return;
4681
+ if (!taskId) return;
4682
+ refresh();
4683
+ }, [taskId]);
4684
+ const createQuestion = useCallback8(
4685
+ async (text) => {
4686
+ if (!taskId) return;
4687
+ await service.createQuestion(taskId, { text });
4688
+ await refresh();
4689
+ },
4690
+ [service, taskId, refresh]
4691
+ );
4692
+ const updateQuestion = useCallback8(
4693
+ async (questionId, text) => {
4694
+ if (!taskId) return;
4695
+ await service.updateQuestion(taskId, questionId, { text });
4696
+ await refresh();
4697
+ },
4698
+ [service, taskId, refresh]
4699
+ );
4700
+ const setStatus = useCallback8(
4701
+ async (questionId, status) => {
4702
+ if (!taskId) return;
4703
+ await service.updateQuestion(taskId, questionId, { status });
4704
+ await refresh();
4705
+ },
4706
+ [service, taskId, refresh]
4707
+ );
4708
+ const deleteQuestion = useCallback8(
4709
+ async (questionId) => {
4710
+ if (!taskId) return;
4711
+ await service.deleteQuestion(taskId, questionId);
4712
+ await refresh();
4713
+ },
4714
+ [service, taskId, refresh]
4715
+ );
4716
+ const addReply = useCallback8(
4717
+ async (questionId, content) => {
4718
+ if (!taskId) return;
4719
+ await service.addQuestionReply(taskId, questionId, { content });
4720
+ await refresh();
4721
+ },
4722
+ [service, taskId, refresh]
4723
+ );
4724
+ const deleteReply = useCallback8(
4725
+ async (questionId, replyId) => {
4726
+ if (!taskId) return;
4727
+ await service.deleteQuestionReply(taskId, questionId, replyId);
4728
+ await refresh();
4729
+ },
4730
+ [service, taskId, refresh]
4731
+ );
4732
+ return {
4733
+ questions,
4734
+ loading,
4735
+ refresh,
4736
+ setQuestions,
4737
+ createQuestion,
4738
+ updateQuestion,
4739
+ setStatus,
4740
+ deleteQuestion,
4741
+ addReply,
4742
+ deleteReply
4743
+ };
4744
+ }
4745
+
4746
+ // src/hooks/useTaskAttachments.ts
4747
+ import { useCallback as useCallback9, useEffect as useEffect15, useState as useState17 } from "react";
4748
+ function useTaskAttachments(taskId, initial) {
4749
+ const { service } = useTaskBoardContext();
4750
+ const [attachments, setAttachments] = useState17(initial ?? []);
4751
+ const [loading, setLoading] = useState17(false);
4752
+ const refresh = useCallback9(async () => {
4753
+ if (!taskId) return;
4754
+ setLoading(true);
4755
+ try {
4756
+ const list = await service.listAttachments(taskId);
4757
+ setAttachments(list);
4758
+ } finally {
4759
+ setLoading(false);
4760
+ }
4761
+ }, [service, taskId]);
4762
+ useEffect15(() => {
4763
+ if (initial !== void 0) return;
4764
+ if (!taskId) return;
4765
+ refresh();
4766
+ }, [taskId]);
4767
+ const uploadFile = useCallback9(
4768
+ async (file) => {
4769
+ if (!taskId) throw new Error("No taskId");
4770
+ const created = await service.uploadAttachment(taskId, file);
4771
+ await refresh();
4772
+ return created;
4773
+ },
4774
+ [service, taskId, refresh]
4775
+ );
4776
+ const addLink = useCallback9(
4777
+ async (payload) => {
4778
+ if (!taskId) throw new Error("No taskId");
4779
+ const created = await service.addLinkAttachment(taskId, payload);
4780
+ await refresh();
4781
+ return created;
4782
+ },
4783
+ [service, taskId, refresh]
4784
+ );
4785
+ const remove = useCallback9(
4786
+ async (attachmentId) => {
4787
+ if (!taskId) return;
4788
+ await service.deleteAttachment(taskId, attachmentId);
4789
+ await refresh();
4790
+ },
4791
+ [service, taskId, refresh]
4792
+ );
4793
+ return { attachments, loading, refresh, setAttachments, uploadFile, addLink, remove };
4794
+ }
4795
+
4796
+ // src/hooks/useHighlightAnchor.ts
4797
+ import { useCallback as useCallback10, useEffect as useEffect16, useState as useState18 } from "react";
4798
+ function useHighlightAnchor() {
4799
+ const [bubble, setBubble] = useState18(null);
4800
+ const [pendingAnchor, setPendingAnchor] = useState18(null);
4801
+ useEffect16(() => {
4802
+ const onMouseUp = () => {
4803
+ const sel = window.getSelection();
4804
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) {
4805
+ setBubble(null);
4806
+ return;
4807
+ }
4808
+ const text = sel.toString().trim();
4809
+ if (!text) {
4810
+ setBubble(null);
4811
+ return;
4812
+ }
4813
+ const range = sel.getRangeAt(0);
4814
+ const container = range.commonAncestorContainer;
4815
+ const el = container.nodeType === 1 ? container : container.parentElement;
4816
+ const sectionEl = el?.closest("[data-section]");
4817
+ if (!sectionEl) {
4818
+ setBubble(null);
4819
+ return;
4820
+ }
4821
+ const sectionKey = sectionEl.dataset.section;
4822
+ if (!sectionKey) {
4823
+ setBubble(null);
4824
+ return;
4825
+ }
4826
+ const rect = range.getBoundingClientRect();
4827
+ setBubble({
4828
+ x: rect.left + rect.width / 2,
4829
+ y: rect.top - 8 + window.scrollY,
4830
+ section: sectionKey,
4831
+ snippet: text.slice(0, 200)
4832
+ });
4833
+ };
4834
+ const onClickAway = (e) => {
4835
+ const target = e.target;
4836
+ if (target.closest("[data-annot-bubble]")) return;
4837
+ setTimeout(() => {
4838
+ const sel = window.getSelection();
4839
+ if (!sel || sel.isCollapsed) setBubble(null);
4840
+ }, 0);
4841
+ };
4842
+ document.addEventListener("mouseup", onMouseUp);
4843
+ document.addEventListener("mousedown", onClickAway);
4844
+ return () => {
4845
+ document.removeEventListener("mouseup", onMouseUp);
4846
+ document.removeEventListener("mousedown", onClickAway);
4847
+ };
4848
+ }, []);
4849
+ const clearBubble = useCallback10(() => setBubble(null), []);
4850
+ const clearPendingAnchor = useCallback10(() => setPendingAnchor(null), []);
4851
+ const beginAnchoredThread = useCallback10(() => {
4852
+ if (!bubble) return null;
4853
+ const anchor = { section: bubble.section, snippet: bubble.snippet };
4854
+ setPendingAnchor(anchor);
4855
+ setBubble(null);
4856
+ return anchor;
4857
+ }, [bubble]);
4858
+ const focusAnchor = useCallback10((anchor) => {
4859
+ const el = document.querySelector(`[data-section="${anchor.section}"]`);
4860
+ if (!el) return;
4861
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
4862
+ el.classList.add("ring-2", "ring-amber-300", "rounded-lg", "transition-all");
4863
+ setTimeout(() => {
4864
+ el.classList.remove("ring-2", "ring-amber-300", "rounded-lg");
4865
+ }, 1500);
4866
+ }, []);
4867
+ return {
4868
+ bubble,
4869
+ clearBubble,
4870
+ pendingAnchor,
4871
+ beginAnchoredThread,
4872
+ clearPendingAnchor,
4873
+ focusAnchor
4874
+ };
4875
+ }
4876
+
4877
+ // src/components/TaskDetailView.tsx
4878
+ import { Fragment as Fragment9, jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
4879
+ var PANEL_OPEN_KEY = "taskboard:panelOpen";
4880
+ function TaskDetailView({
4881
+ taskId,
4882
+ backHref,
4883
+ onBack,
4884
+ breadcrumb,
4885
+ onDeleted,
4886
+ onNavigateToTask,
4887
+ buildShareUrl
4888
+ }) {
4889
+ const { service, projects, columns, priorities, tags: predefinedTags, user, internalLabel } = useTaskBoardContext();
4890
+ const [task, setTask] = useState19(null);
4891
+ const [comments, setComments] = useState19([]);
4892
+ const [activity, setActivity] = useState19([]);
4893
+ const [initialQuestions, setInitialQuestions] = useState19(void 0);
4894
+ const [initialAttachments, setInitialAttachments] = useState19(void 0);
4895
+ const [loading, setLoading] = useState19(true);
4896
+ const [error, setError] = useState19("");
4897
+ const [saving, setSaving] = useState19(false);
4898
+ const [title, setTitle] = useState19("");
4899
+ const [titleEditing, setTitleEditing] = useState19(false);
4900
+ const [description, setDescription] = useState19(EMPTY_DESCRIPTION);
4901
+ const [priority, setPriority] = useState19("medium");
4902
+ const [taskStatus, setTaskStatus] = useState19("backlog");
4903
+ const [tags, setTags] = useState19([]);
4904
+ const [statusOpen, setStatusOpen] = useState19(false);
4905
+ const [priorityOpen, setPriorityOpen] = useState19(false);
4906
+ const [moreOpen, setMoreOpen] = useState19(false);
4907
+ const [tagsOpen, setTagsOpen] = useState19(false);
4908
+ const [pendingTags, setPendingTags] = useState19([]);
4909
+ const [showOtherTagInput, setShowOtherTagInput] = useState19(false);
4910
+ const [linkCopied, setLinkCopied] = useState19(false);
4911
+ const statusRef = useRef15(null);
4912
+ const priorityRef = useRef15(null);
4913
+ const moreRef = useRef15(null);
4914
+ const tagsRef = useRef15(null);
4915
+ const [projectTaskIds, setProjectTaskIds] = useState19([]);
4916
+ const [openThreadId, setOpenThreadId] = useState19(null);
4917
+ const [shimmeringThreadIds, setShimmeringThreadIds] = useState19(/* @__PURE__ */ new Set());
4918
+ const [panelOpen, setPanelOpen] = useState19(() => {
4919
+ if (typeof window === "undefined") return true;
4920
+ try {
4921
+ const stored = window.localStorage.getItem(PANEL_OPEN_KEY);
4922
+ return stored === null ? true : stored === "true";
4923
+ } catch {
4924
+ return true;
4925
+ }
4926
+ });
4927
+ const togglePanel = () => {
4928
+ setPanelOpen((prev) => {
4929
+ const next = !prev;
4930
+ try {
4931
+ window.localStorage.setItem(PANEL_OPEN_KEY, String(next));
4932
+ } catch {
4933
+ }
4934
+ return next;
4935
+ });
4936
+ };
4937
+ const fetchTask = useCallback11(async () => {
4938
+ try {
4939
+ const data = await service.getTask(taskId);
4940
+ setTask(data);
4941
+ setTitle(data.title || "");
4942
+ setDescription(data.description || EMPTY_DESCRIPTION);
4943
+ setPriority(data.priority || "medium");
4944
+ setTaskStatus(data.status || "backlog");
4945
+ setTags(data.tags || []);
4946
+ setComments(data.comments || []);
4947
+ setActivity(data.activity || []);
4948
+ setInitialQuestions(data.questions || []);
4949
+ setInitialAttachments(data.attachments || []);
4950
+ service.markTaskRead(taskId).catch(() => {
4951
+ });
4952
+ } catch {
4953
+ setError("Could not load task.");
4954
+ } finally {
4955
+ setLoading(false);
4956
+ }
4957
+ }, [service, taskId]);
4958
+ useEffect17(() => {
4959
+ fetchTask();
4960
+ }, [fetchTask]);
4961
+ useEffect17(() => {
4962
+ if (!task || !onNavigateToTask) return;
4963
+ let cancelled = false;
4964
+ (async () => {
4965
+ try {
4966
+ const data = await service.listTasks(task.project_slug, 200);
4967
+ if (cancelled) return;
4968
+ const ids = [];
4969
+ for (const col of columns) {
4970
+ const colData = data[col.key];
4971
+ if (colData && Array.isArray(colData.tasks)) {
4972
+ ids.push(...colData.tasks.map((t) => t.id));
4973
+ }
4974
+ }
4975
+ setProjectTaskIds(ids);
4976
+ } catch {
4977
+ }
4978
+ })();
4979
+ return () => {
4980
+ cancelled = true;
4981
+ };
4982
+ }, [task, columns, service, onNavigateToTask]);
4983
+ useEffect17(() => {
4984
+ const onClick = (e) => {
4985
+ const target = e.target;
4986
+ if (statusOpen && statusRef.current && !statusRef.current.contains(target)) setStatusOpen(false);
4987
+ if (priorityOpen && priorityRef.current && !priorityRef.current.contains(target)) setPriorityOpen(false);
4988
+ if (moreOpen && moreRef.current && !moreRef.current.contains(target)) setMoreOpen(false);
4989
+ if (tagsOpen && tagsRef.current && !tagsRef.current.contains(target)) setTagsOpen(false);
4990
+ };
4991
+ document.addEventListener("mousedown", onClick);
4992
+ return () => document.removeEventListener("mousedown", onClick);
4993
+ }, [statusOpen, priorityOpen, moreOpen, tagsOpen]);
4994
+ const questions = useTaskQuestions(taskId, initialQuestions);
4995
+ const attachments = useTaskAttachments(taskId, initialAttachments);
4996
+ const highlight = useHighlightAnchor();
4997
+ useEffect17(() => {
4998
+ if (highlight.pendingAnchor && !panelOpen) {
4999
+ setPanelOpen(true);
5000
+ try {
5001
+ window.localStorage.setItem(PANEL_OPEN_KEY, "true");
5002
+ } catch {
5003
+ }
5004
+ }
5005
+ }, [highlight.pendingAnchor, panelOpen]);
5006
+ const persist = useCallback11(
5007
+ async (updates) => {
5008
+ setSaving(true);
5009
+ try {
5010
+ const updated = await service.updateTask(taskId, updates);
5011
+ setTask((prev) => prev ? { ...prev, ...updated } : prev);
5012
+ } finally {
5013
+ setSaving(false);
5014
+ }
5015
+ },
5016
+ [service, taskId]
5017
+ );
5018
+ const handleTitleCommit = () => {
5019
+ setTitleEditing(false);
5020
+ if (task && title.trim() && title.trim() !== task.title) {
5021
+ persist({ title: title.trim() });
5022
+ } else if (task) {
5023
+ setTitle(task.title);
5024
+ }
5025
+ };
5026
+ const handleSectionChange = (section, val) => {
5027
+ const next = { ...description, [section]: val };
5028
+ setDescription(next);
5029
+ persist({ description: next });
5030
+ };
5031
+ const handleSectionStatusChange = (section, status) => {
5032
+ const sectionStatus = { ...description.section_status || {}, [section]: status };
5033
+ const next = { ...description, section_status: sectionStatus };
5034
+ setDescription(next);
5035
+ persist({ description: next });
5036
+ };
5037
+ const handleStatusChange = (s) => {
5038
+ setTaskStatus(s);
5039
+ setStatusOpen(false);
5040
+ persist({ status: s });
5041
+ setTimeout(() => fetchTask(), 500);
5042
+ };
5043
+ const handlePriorityChange = (p) => {
5044
+ setPriority(p);
5045
+ setPriorityOpen(false);
5046
+ persist({ priority: p });
5047
+ };
5048
+ const handleTagsCommit = () => {
5049
+ setTags([...pendingTags]);
5050
+ setTagsOpen(false);
5051
+ setShowOtherTagInput(false);
5052
+ persist({ tags: pendingTags });
5053
+ };
5054
+ const handleDelete = async () => {
5055
+ if (!task) return;
5056
+ if (!confirm(`Delete "${task.title}"? This cannot be undone.`)) return;
5057
+ try {
5058
+ await service.deleteTask(taskId);
5059
+ onDeleted?.(task);
5060
+ } catch {
5061
+ setError("Failed to delete task.");
5062
+ }
5063
+ };
5064
+ const handleShare = () => {
5065
+ if (!task) return;
5066
+ const defaultUrl = `${window.location.origin}/task-board/${taskId}${task.project_slug ? `?project=${task.project_slug}` : ""}`;
5067
+ const url = buildShareUrl ? buildShareUrl(task) : defaultUrl;
5068
+ navigator.clipboard.writeText(url).then(() => {
5069
+ setLinkCopied(true);
5070
+ setTimeout(() => setLinkCopied(false), 2e3);
5071
+ });
5072
+ };
5073
+ const handleCreateThread = async ({
5074
+ title: titleLine,
5075
+ content,
5076
+ isInternal,
5077
+ anchor,
5078
+ attachmentIds
5079
+ }) => {
5080
+ try {
5081
+ const created = await service.addComment(taskId, {
5082
+ content,
5083
+ is_internal: isInternal,
5084
+ title: titleLine || null,
5085
+ anchor,
5086
+ attachment_ids: attachmentIds
5087
+ });
5088
+ if (created.id && !titleLine) {
5089
+ setShimmeringThreadIds((prev) => new Set(prev).add(created.id));
5090
+ setTimeout(() => {
5091
+ setShimmeringThreadIds((prev) => {
5092
+ const next = new Set(prev);
5093
+ next.delete(created.id);
5094
+ return next;
5095
+ });
5096
+ }, 800);
5097
+ }
5098
+ highlight.clearPendingAnchor();
5099
+ await fetchTask();
5100
+ } catch {
5101
+ }
5102
+ };
5103
+ const handleCreateReply = async (parentId, content, isInternal) => {
5104
+ try {
5105
+ await service.addComment(taskId, {
5106
+ content,
5107
+ is_internal: isInternal,
5108
+ parent_id: parentId
5109
+ });
5110
+ await fetchTask();
5111
+ } catch {
5112
+ }
5113
+ };
5114
+ const handleUpdateThread = async (threadId, body) => {
5115
+ try {
5116
+ await service.updateThread(taskId, threadId, body);
5117
+ await fetchTask();
5118
+ } catch {
5119
+ }
5120
+ };
5121
+ const project = useMemo3(() => {
5122
+ if (!task) return null;
5123
+ return projects.find((p) => p.slug === task.project_slug) ?? null;
5124
+ }, [task, projects]);
5125
+ const threads = useMemo3(
5126
+ () => deriveThreads(comments, attachments.attachments),
5127
+ [comments, attachments.attachments]
5128
+ );
5129
+ const statusCol = columns.find((c) => c.key === taskStatus);
5130
+ const priorityStyle = getPriorityStyle(priority);
5131
+ const currentIdx = task ? projectTaskIds.indexOf(task.id) : -1;
5132
+ const prevId = currentIdx > 0 ? projectTaskIds[currentIdx - 1] : null;
5133
+ const nextId = currentIdx >= 0 && currentIdx < projectTaskIds.length - 1 ? projectTaskIds[currentIdx + 1] : null;
5134
+ if (loading) {
5135
+ return /* @__PURE__ */ jsx28("div", { className: "min-h-screen flex items-center justify-center bg-[#FAFAFA]", children: /* @__PURE__ */ jsx28("div", { className: "text-sm text-neutral-400", children: "Loading task\u2026" }) });
5136
+ }
5137
+ if (!task) {
5138
+ return /* @__PURE__ */ jsx28("div", { className: "min-h-screen flex items-center justify-center bg-[#FAFAFA]", children: /* @__PURE__ */ jsxs26("div", { className: "text-center", children: [
5139
+ /* @__PURE__ */ jsx28("h1", { className: "text-2xl font-medium text-neutral-900 mb-2", children: "Task not found" }),
5140
+ /* @__PURE__ */ jsx28("p", { className: "text-neutral-500", children: error || "This task may have been deleted." })
5141
+ ] }) });
5142
+ }
5143
+ const initials = getInitials(task.created_by_name || task.created_by || "?");
5144
+ return /* @__PURE__ */ jsxs26("div", { className: "bg-white text-neutral-900", children: [
5145
+ breadcrumb,
5146
+ /* @__PURE__ */ jsxs26(
5147
+ "div",
5148
+ {
5149
+ className: `flex-1 px-6 lg:px-10 pt-6 pb-24 transition-[padding] duration-200 ${panelOpen ? "xl:pr-[460px]" : "xl:pr-[88px]"}`,
5150
+ children: [
5151
+ /* @__PURE__ */ jsxs26("div", { className: "flex items-center justify-between mb-6", children: [
5152
+ onBack ? /* @__PURE__ */ jsxs26(
5153
+ "button",
5154
+ {
5155
+ onClick: onBack,
5156
+ className: "flex items-center gap-2 text-neutral-500 hover:text-neutral-900 w-fit",
5157
+ children: [
5158
+ /* @__PURE__ */ jsx28(ArrowLeftIcon, { size: 16, strokeWidth: 1.5 }),
5159
+ /* @__PURE__ */ jsx28("span", { className: "text-[13px] font-medium", children: "Back to Task Board" })
5160
+ ]
5161
+ }
5162
+ ) : backHref ? /* @__PURE__ */ jsxs26(
5163
+ "a",
5164
+ {
5165
+ href: backHref,
5166
+ className: "flex items-center gap-2 text-neutral-500 hover:text-neutral-900 w-fit",
5167
+ children: [
5168
+ /* @__PURE__ */ jsx28(ArrowLeftIcon, { size: 16, strokeWidth: 1.5 }),
5169
+ /* @__PURE__ */ jsx28("span", { className: "text-[13px] font-medium", children: "Back to Task Board" })
5170
+ ]
5171
+ }
5172
+ ) : /* @__PURE__ */ jsx28("span", {}),
5173
+ onNavigateToTask && /* @__PURE__ */ jsxs26("div", { className: "flex items-center gap-1 shrink-0", children: [
5174
+ /* @__PURE__ */ jsx28(
5175
+ "button",
5176
+ {
5177
+ onClick: () => prevId && onNavigateToTask(prevId, task.project_slug),
5178
+ disabled: !prevId,
5179
+ className: "inline-flex items-center justify-center w-9 h-9 rounded-xl border border-neutral-200 text-neutral-500 hover:text-neutral-900 hover:bg-neutral-50 hover:border-neutral-300 disabled:opacity-40 disabled:hover:bg-white disabled:hover:text-neutral-500",
5180
+ title: "Previous task",
5181
+ children: /* @__PURE__ */ jsx28(ChevronLeftIcon, { size: 16, strokeWidth: 1.5 })
5182
+ }
5183
+ ),
5184
+ /* @__PURE__ */ jsx28(
5185
+ "button",
5186
+ {
5187
+ onClick: () => nextId && onNavigateToTask(nextId, task.project_slug),
5188
+ disabled: !nextId,
5189
+ className: "inline-flex items-center justify-center w-9 h-9 rounded-xl border border-neutral-200 text-neutral-500 hover:text-neutral-900 hover:bg-neutral-50 hover:border-neutral-300 disabled:opacity-40 disabled:hover:bg-white disabled:hover:text-neutral-500",
5190
+ title: "Next task",
5191
+ children: /* @__PURE__ */ jsx28(ChevronRightIcon, { size: 16, strokeWidth: 1.5 })
5192
+ }
5193
+ )
5194
+ ] })
5195
+ ] }),
5196
+ /* @__PURE__ */ jsxs26("div", { className: "mb-3 flex items-center gap-2 flex-wrap", children: [
5197
+ /* @__PURE__ */ jsx28("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[12px] font-medium bg-neutral-50 text-neutral-700 border border-neutral-200", children: /* @__PURE__ */ jsx28("span", { className: "font-mono", children: formatTaskId(task.id) }) }),
5198
+ project && /* @__PURE__ */ jsxs26(Fragment9, { children: [
5199
+ /* @__PURE__ */ jsx28("span", { className: "text-[12px] text-neutral-400", children: "in" }),
5200
+ /* @__PURE__ */ jsx28("span", { className: "text-[12px] text-[#FF5E00] hover:text-[#E05200] font-medium", children: project.name })
5201
+ ] })
5202
+ ] }),
5203
+ /* @__PURE__ */ jsxs26("div", { className: "flex gap-6 items-start mb-4", children: [
5204
+ /* @__PURE__ */ jsxs26("div", { className: "flex-1 min-w-0", children: [
5205
+ titleEditing ? /* @__PURE__ */ jsx28(
5206
+ "input",
5207
+ {
5208
+ value: title,
5209
+ onChange: (e) => setTitle(e.target.value),
5210
+ onBlur: handleTitleCommit,
5211
+ onKeyDown: (e) => {
5212
+ if (e.key === "Enter") {
5213
+ e.preventDefault();
5214
+ handleTitleCommit();
5215
+ } else if (e.key === "Escape") {
5216
+ e.preventDefault();
5217
+ setTitle(task.title);
5218
+ setTitleEditing(false);
5219
+ }
5220
+ },
5221
+ autoFocus: true,
5222
+ className: "w-full text-[20px] font-semibold text-neutral-900 tracking-tight leading-snug bg-transparent border-b-2 border-[#FF5E00] focus:outline-none px-1 -mx-1"
5223
+ }
5224
+ ) : /* @__PURE__ */ jsx28(
5225
+ "h1",
5226
+ {
5227
+ onClick: () => setTitleEditing(true),
5228
+ className: "text-[20px] font-semibold text-neutral-900 tracking-tight leading-snug cursor-text rounded px-1 -mx-1 hover:bg-neutral-50/60",
5229
+ title: "Click to edit",
5230
+ children: title || "Untitled task"
5231
+ }
5232
+ ),
5233
+ /* @__PURE__ */ jsxs26("div", { className: "mt-3 flex items-center gap-3 flex-wrap text-[12px] text-neutral-500", children: [
5234
+ /* @__PURE__ */ jsxs26("span", { children: [
5235
+ "Created ",
5236
+ /* @__PURE__ */ jsx28("span", { className: "font-medium text-neutral-700", children: formatDate(task.created_at) })
5237
+ ] }),
5238
+ /* @__PURE__ */ jsx28("span", { className: "text-neutral-300", children: "\xB7" }),
5239
+ /* @__PURE__ */ jsxs26("span", { children: [
5240
+ "Updated",
5241
+ " ",
5242
+ /* @__PURE__ */ jsx28("span", { className: "font-medium text-neutral-700", children: task.updated_at ? formatDate(task.updated_at) : "\u2014" })
5243
+ ] }),
5244
+ /* @__PURE__ */ jsx28("span", { className: "text-neutral-300", children: "\xB7" }),
5245
+ /* @__PURE__ */ jsxs26("span", { className: "inline-flex items-center gap-1.5", children: [
5246
+ "Creator",
5247
+ /* @__PURE__ */ jsx28("span", { className: "w-4 h-4 rounded-full bg-[#FF5E00] text-white inline-flex items-center justify-center text-[9px] font-semibold", children: initials }),
5248
+ /* @__PURE__ */ jsx28("span", { className: "font-medium text-neutral-700", children: task.created_by_name || task.created_by })
5249
+ ] }),
5250
+ /* @__PURE__ */ jsx28("span", { className: "text-neutral-300", children: "\xB7" }),
5251
+ /* @__PURE__ */ jsxs26("div", { className: "inline-flex items-center gap-1.5 relative", ref: tagsRef, children: [
5252
+ /* @__PURE__ */ jsx28("span", { children: "Tags" }),
5253
+ tags.map((tag) => {
5254
+ const style = getTagStyle(tag);
5255
+ return /* @__PURE__ */ jsx28(
5256
+ "span",
5257
+ {
5258
+ className: `inline-flex items-center px-1.5 py-px text-[10px] font-medium rounded border ${style.className}`,
5259
+ children: style.label
5260
+ },
5261
+ tag
5262
+ );
5263
+ }),
5264
+ /* @__PURE__ */ jsxs26(
5265
+ "button",
5266
+ {
5267
+ onClick: () => {
5268
+ if (!tagsOpen) setPendingTags([...tags]);
5269
+ setTagsOpen(!tagsOpen);
5270
+ },
5271
+ className: "inline-flex items-center gap-1 px-1.5 py-0.5 rounded border border-dashed border-neutral-300 text-neutral-400 hover:text-neutral-700 hover:border-neutral-400 text-[10px]",
5272
+ children: [
5273
+ /* @__PURE__ */ jsx28(PlusIcon, { size: 10, strokeWidth: 2 }),
5274
+ "Edit"
5275
+ ]
5276
+ }
5277
+ ),
5278
+ tagsOpen && /* @__PURE__ */ jsxs26("div", { className: "absolute top-full left-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-30 w-56", children: [
5279
+ /* @__PURE__ */ jsxs26("div", { className: "py-1", children: [
5280
+ predefinedTags.map((tag) => {
5281
+ const isSelected = pendingTags.includes(tag.value);
5282
+ return /* @__PURE__ */ jsxs26(
5283
+ "button",
5284
+ {
5285
+ onClick: () => {
5286
+ setPendingTags(
5287
+ (prev) => isSelected ? prev.filter((t) => t !== tag.value) : [...prev, tag.value]
5288
+ );
5289
+ },
5290
+ className: `w-full text-left px-3 py-1.5 text-[12px] hover:bg-neutral-50 flex items-center justify-between ${isSelected ? "bg-neutral-50" : ""}`,
5291
+ children: [
5292
+ /* @__PURE__ */ jsx28(
5293
+ "span",
5294
+ {
5295
+ className: `inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded border ${tag.className}`,
5296
+ children: tag.label
5297
+ }
5298
+ ),
5299
+ isSelected && /* @__PURE__ */ jsx28(CheckIcon, { size: 12, strokeWidth: 2.5, className: "text-[#FF5E00]" })
5300
+ ]
5301
+ },
5302
+ tag.value
5303
+ );
5304
+ }),
5305
+ pendingTags.filter((t) => !predefinedTags.some((p) => p.value === t)).map((tag) => /* @__PURE__ */ jsxs26(
5306
+ "div",
5307
+ {
5308
+ className: "w-full text-left px-3 py-1.5 text-[12px] bg-neutral-50 flex items-center justify-between",
5309
+ children: [
5310
+ /* @__PURE__ */ jsx28("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: tag }),
5311
+ /* @__PURE__ */ jsx28(
5312
+ "button",
5313
+ {
5314
+ onClick: () => setPendingTags((prev) => prev.filter((t) => t !== tag)),
5315
+ className: "text-neutral-400 hover:text-neutral-600",
5316
+ children: /* @__PURE__ */ jsx28(XIcon, { size: 12, strokeWidth: 2 })
5317
+ }
5318
+ )
5319
+ ]
5320
+ },
5321
+ tag
5322
+ )),
5323
+ !showOtherTagInput ? /* @__PURE__ */ jsx28(
5324
+ "button",
5325
+ {
5326
+ onClick: () => setShowOtherTagInput(true),
5327
+ className: "w-full text-left px-3 py-1.5 text-[12px] hover:bg-neutral-50 flex items-center justify-between",
5328
+ children: /* @__PURE__ */ jsx28("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" })
5329
+ }
5330
+ ) : /* @__PURE__ */ jsx28("div", { className: "px-3 py-2 bg-neutral-50/50", children: /* @__PURE__ */ jsx28(
5331
+ "input",
5332
+ {
5333
+ type: "text",
5334
+ autoFocus: true,
5335
+ onKeyDown: (e) => {
5336
+ if (e.key === "Enter") {
5337
+ e.preventDefault();
5338
+ const val = e.currentTarget.value.trim().toLowerCase().replace(/\s+/g, "-");
5339
+ if (val && !pendingTags.includes(val)) {
5340
+ setPendingTags((prev) => [...prev, val]);
5341
+ }
5342
+ e.currentTarget.value = "";
5343
+ setShowOtherTagInput(false);
5344
+ }
5345
+ if (e.key === "Escape") {
5346
+ setShowOtherTagInput(false);
5347
+ }
5348
+ },
5349
+ className: "w-full px-2 py-1.5 text-[12px] border border-neutral-200 rounded-md focus:outline-none focus:ring-1 focus:ring-[#FF5E00]/20 focus:border-[#FF5E00]/50",
5350
+ placeholder: "Type a custom tag..."
5351
+ }
5352
+ ) })
5353
+ ] }),
5354
+ /* @__PURE__ */ jsxs26("div", { className: "border-t border-neutral-100 px-2 py-2 flex items-center justify-end gap-2", children: [
5355
+ /* @__PURE__ */ jsx28(
5356
+ "button",
5357
+ {
5358
+ onClick: () => {
5359
+ setPendingTags([...tags]);
5360
+ setTagsOpen(false);
5361
+ setShowOtherTagInput(false);
5362
+ },
5363
+ className: "px-2.5 py-1 text-[11px] font-medium text-neutral-500 hover:text-neutral-700",
5364
+ children: "Cancel"
5365
+ }
5366
+ ),
5367
+ /* @__PURE__ */ jsx28(
5368
+ "button",
5369
+ {
5370
+ onClick: handleTagsCommit,
5371
+ className: "px-3 py-1 text-[11px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] rounded",
5372
+ children: "Save"
5373
+ }
5374
+ )
5375
+ ] })
5376
+ ] })
5377
+ ] })
5378
+ ] })
5379
+ ] }),
5380
+ /* @__PURE__ */ jsxs26("div", { className: "flex items-center gap-2 shrink-0", children: [
5381
+ saving && /* @__PURE__ */ jsx28("span", { className: "text-[11px] text-neutral-400 mr-1", children: "Saving..." }),
5382
+ /* @__PURE__ */ jsxs26("div", { className: "relative", ref: statusRef, children: [
5383
+ /* @__PURE__ */ jsxs26(
5384
+ "button",
5385
+ {
5386
+ onClick: () => {
5387
+ setStatusOpen(!statusOpen);
5388
+ setPriorityOpen(false);
5389
+ setMoreOpen(false);
5390
+ },
5391
+ className: "inline-flex items-center gap-2 h-10 px-3 rounded-xl border border-neutral-200 bg-white text-[12px] font-medium text-neutral-700 hover:text-neutral-900 hover:border-neutral-300",
5392
+ children: [
5393
+ /* @__PURE__ */ jsx28("span", { className: `w-2 h-2 rounded-full ${statusCol?.color ?? "bg-neutral-400"}` }),
5394
+ /* @__PURE__ */ jsx28("span", { children: statusCol?.label ?? taskStatus }),
5395
+ /* @__PURE__ */ jsx28(ChevronDownIcon, { size: 12, strokeWidth: 1.5, className: "text-neutral-400" })
5396
+ ]
5397
+ }
5398
+ ),
5399
+ statusOpen && /* @__PURE__ */ jsx28("div", { className: "absolute right-0 top-full mt-1 w-52 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-40", children: columns.map((col) => /* @__PURE__ */ jsxs26(
5400
+ "button",
5401
+ {
5402
+ onClick: () => handleStatusChange(col.key),
5403
+ className: `w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2 ${taskStatus === col.key ? "font-medium bg-neutral-50" : ""}`,
5404
+ children: [
5405
+ /* @__PURE__ */ jsx28("span", { className: `w-2 h-2 rounded-full ${col.color}` }),
5406
+ col.label
5407
+ ]
5408
+ },
5409
+ col.key
5410
+ )) })
5411
+ ] }),
5412
+ /* @__PURE__ */ jsxs26("div", { className: "relative", ref: priorityRef, children: [
5413
+ /* @__PURE__ */ jsxs26(
5414
+ "button",
5415
+ {
5416
+ onClick: () => {
5417
+ setPriorityOpen(!priorityOpen);
5418
+ setStatusOpen(false);
5419
+ setMoreOpen(false);
5420
+ },
5421
+ className: `inline-flex items-center gap-2 h-10 px-3 rounded-xl border text-[12px] font-medium transition-colors ${priorityStyle.className}`,
5422
+ children: [
5423
+ /* @__PURE__ */ jsx28("span", { children: priorityStyle.label }),
5424
+ /* @__PURE__ */ jsx28(ChevronDownIcon, { size: 12, strokeWidth: 1.5, className: "opacity-50" })
5425
+ ]
5426
+ }
5427
+ ),
5428
+ priorityOpen && /* @__PURE__ */ jsx28("div", { className: "absolute right-0 top-full mt-1 w-44 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-40", children: priorities.map((p) => /* @__PURE__ */ jsx28(
5429
+ "button",
5430
+ {
5431
+ onClick: () => handlePriorityChange(p.value),
5432
+ className: `w-full text-left px-3 py-2 text-[12px] hover:bg-neutral-50 flex items-center gap-2 ${priority === p.value ? "bg-neutral-50" : ""}`,
5433
+ children: /* @__PURE__ */ jsx28(
5434
+ "span",
5435
+ {
5436
+ className: `inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded border ${p.className}`,
5437
+ children: p.label
5438
+ }
5439
+ )
5440
+ },
5441
+ p.value
5442
+ )) })
5443
+ ] }),
5444
+ /* @__PURE__ */ jsxs26("div", { className: "relative", ref: moreRef, children: [
5445
+ /* @__PURE__ */ jsx28(
5446
+ "button",
5447
+ {
5448
+ onClick: () => {
5449
+ setMoreOpen(!moreOpen);
5450
+ setStatusOpen(false);
5451
+ setPriorityOpen(false);
5452
+ },
5453
+ className: "w-10 h-10 flex items-center justify-center rounded-xl border border-neutral-200 bg-white text-neutral-500 hover:text-neutral-900 hover:border-neutral-300",
5454
+ "aria-label": "More actions",
5455
+ children: /* @__PURE__ */ jsx28(MoreVerticalIcon, { size: 16, strokeWidth: 1.5 })
5456
+ }
5457
+ ),
5458
+ moreOpen && /* @__PURE__ */ jsxs26("div", { className: "absolute right-0 top-full mt-1 w-40 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-40", children: [
5459
+ /* @__PURE__ */ jsxs26(
5460
+ "button",
5461
+ {
5462
+ onClick: () => {
5463
+ setMoreOpen(false);
5464
+ handleShare();
5465
+ },
5466
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2.5",
5467
+ children: [
5468
+ /* @__PURE__ */ jsx28(Share2Icon, { size: 14, strokeWidth: 1.5, className: "text-neutral-400" }),
5469
+ linkCopied ? "Copied!" : "Share"
5470
+ ]
5471
+ }
5472
+ ),
5473
+ /* @__PURE__ */ jsxs26(
5474
+ "button",
5475
+ {
5476
+ onClick: () => {
5477
+ setMoreOpen(false);
5478
+ handleDelete();
5479
+ },
5480
+ className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2.5",
5481
+ children: [
5482
+ /* @__PURE__ */ jsx28(TrashIcon, { size: 14, strokeWidth: 1.5 }),
5483
+ "Delete"
5484
+ ]
5485
+ }
5486
+ )
5487
+ ] })
5488
+ ] })
5489
+ ] })
5490
+ ] }),
5491
+ /* @__PURE__ */ jsxs26("div", { className: "flex flex-col gap-10 mt-10", children: [
5492
+ DESCRIPTION_SECTIONS.map((section) => {
5493
+ const status = description.section_status?.[section.key] === "approved" ? "approved" : "draft";
5494
+ return /* @__PURE__ */ jsx28(
5495
+ DescriptionSection,
5496
+ {
5497
+ sectionKey: section.key,
5498
+ label: section.label,
5499
+ placeholder: section.placeholder ?? "",
5500
+ value: description[section.key] || "",
5501
+ onChange: (v) => handleSectionChange(section.key, v),
5502
+ status,
5503
+ onStatusChange: (s) => handleSectionStatusChange(section.key, s),
5504
+ saving
5505
+ },
5506
+ section.key
5507
+ );
5508
+ }),
5509
+ /* @__PURE__ */ jsx28(
5510
+ OutstandingQuestionsSection,
5511
+ {
5512
+ questions: questions.questions,
5513
+ currentUsername: user.username,
5514
+ onCreate: questions.createQuestion,
5515
+ onSetStatus: questions.setStatus,
5516
+ onDelete: questions.deleteQuestion,
5517
+ onAddReply: questions.addReply
5518
+ }
5519
+ ),
5520
+ /* @__PURE__ */ jsx28(
5521
+ AttachmentsSection,
5522
+ {
5523
+ attachments: attachments.attachments,
5524
+ onUpload: attachments.uploadFile,
5525
+ onAddLink: (url, name) => attachments.addLink({ url, name }),
5526
+ onDelete: attachments.remove
5527
+ }
5528
+ )
5529
+ ] })
5530
+ ]
5531
+ }
5532
+ ),
5533
+ /* @__PURE__ */ jsx28(
5534
+ ThreadsPanel,
5535
+ {
5536
+ threads,
5537
+ activity,
5538
+ attachments: attachments.attachments,
5539
+ open: panelOpen,
5540
+ onToggle: togglePanel,
5541
+ openThreadId,
5542
+ onOpenThread: setOpenThreadId,
5543
+ pendingAnchor: highlight.pendingAnchor,
5544
+ onClearAnchor: highlight.clearPendingAnchor,
5545
+ onAnchorClick: highlight.focusAnchor,
5546
+ shimmeringThreadIds,
5547
+ isInternalUser: !!user.is_internal,
5548
+ onCreateThread: handleCreateThread,
5549
+ onCreateReply: handleCreateReply,
5550
+ onUpdateThread: handleUpdateThread,
5551
+ onUploadAttachment: attachments.uploadFile,
5552
+ onAddLinkAttachment: (url, name) => attachments.addLink({ url, name })
5553
+ }
5554
+ ),
5555
+ /* @__PURE__ */ jsx28(HighlightBubble, { bubble: highlight.bubble, onComment: highlight.beginAnchoredThread })
5556
+ ] });
5557
+ }
2230
5558
  export {
5559
+ ActivityList,
5560
+ ArrowLeftIcon,
5561
+ AttachmentsSection,
2231
5562
  BellIcon,
2232
5563
  BoardSkeleton,
5564
+ Bold,
5565
+ ChatDotsIcon,
5566
+ CheckCircle2Icon,
2233
5567
  CheckIcon,
2234
5568
  ChevronDownIcon,
5569
+ ChevronLeftIcon,
5570
+ ChevronRightIcon,
5571
+ Code,
5572
+ ContextPill,
5573
+ CornerUpLeftIcon,
2235
5574
  CreateTaskModal,
2236
5575
  DEFAULT_COLUMNS,
5576
+ DEFAULT_INTERNAL_LABEL,
2237
5577
  DEFAULT_PAGE_SIZE,
2238
5578
  DEFAULT_PRIORITIES,
2239
5579
  DESCRIPTION_SECTIONS,
5580
+ DescriptionSection,
2240
5581
  EMPTY_DESCRIPTION,
5582
+ ExternalLinkIcon,
2241
5583
  FeedbackIcon,
5584
+ FileTextIcon,
2242
5585
  FilterBar,
2243
5586
  FilterIcon,
5587
+ Heading2,
5588
+ HelpCircleIcon,
5589
+ HighlightBubble,
5590
+ HistoryIcon,
5591
+ ImageIcon,
5592
+ Italic,
2244
5593
  KanbanColumn,
2245
5594
  KanbanIcon,
5595
+ Link2Icon,
2246
5596
  LinkIcon,
5597
+ List,
5598
+ ListOrdered,
2247
5599
  LockIcon,
5600
+ MarkdownEditor,
5601
+ MarkdownView,
2248
5602
  MentionText,
2249
5603
  MentionTextarea,
2250
5604
  MessageSquareIcon,
5605
+ MoreVerticalIcon,
2251
5606
  NotificationBell,
5607
+ OutstandingQuestionsSection,
2252
5608
  POSITION_GAP,
2253
5609
  PREDEFINED_TAGS,
2254
5610
  PencilIcon,
2255
5611
  PlusIcon,
2256
5612
  PriorityBadge,
5613
+ Quote,
5614
+ RotateCcwIcon,
5615
+ Share2Icon,
5616
+ SidebarToggleIcon,
2257
5617
  SkeletonCard,
2258
5618
  SkeletonPulse,
2259
5619
  TagBadge,
@@ -2261,24 +5621,39 @@ export {
2261
5621
  TaskBoardProvider,
2262
5622
  TaskCard,
2263
5623
  TaskDetailPanel,
5624
+ TaskDetailView,
5625
+ ThreadCard,
5626
+ ThreadComposer,
5627
+ ThreadDetailView,
5628
+ ThreadsPanel,
2264
5629
  TrashIcon,
2265
5630
  UserAvatar,
2266
5631
  XIcon,
2267
5632
  createTaskBoardService,
5633
+ deriveThreads,
2268
5634
  formatDate,
2269
5635
  formatDateTime,
5636
+ formatTaskId,
2270
5637
  getDescriptionPreview,
2271
5638
  getInitials,
2272
5639
  getPriorityStyle,
2273
5640
  getTagStyle,
2274
5641
  getUserProjects,
2275
5642
  hasDescription,
5643
+ htmlToMd,
5644
+ mdToHtml,
5645
+ parseDate,
5646
+ sectionLabel,
5647
+ timeAgo,
2276
5648
  toDisplayText,
2277
5649
  toStoredText,
5650
+ useHighlightAnchor,
2278
5651
  useShareLink,
2279
5652
  useTaskActions,
5653
+ useTaskAttachments,
2280
5654
  useTaskBoard,
2281
5655
  useTaskBoardContext,
2282
- useTaskDetail
5656
+ useTaskDetail,
5657
+ useTaskQuestions
2283
5658
  };
2284
5659
  //# sourceMappingURL=index.mjs.map