@emberai-engg/task-board 0.3.6 → 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
  }),
@@ -453,27 +518,27 @@ function getInitials(name) {
453
518
  }
454
519
  function parseDate(dateStr) {
455
520
  if (!dateStr) return /* @__PURE__ */ new Date();
456
- const d = new Date(dateStr);
457
- if (isNaN(d.getTime()) && !dateStr.endsWith("Z") && !dateStr.includes("+")) {
521
+ if (!dateStr.endsWith("Z") && !dateStr.includes("+") && !/\d{2}:\d{2}$/.test(dateStr.slice(-6))) {
458
522
  return /* @__PURE__ */ new Date(dateStr + "Z");
459
523
  }
460
- return d;
524
+ return new Date(dateStr);
461
525
  }
462
526
  function formatDate(dateStr) {
463
527
  if (!dateStr) return "";
464
- const d = parseDate(dateStr);
465
- return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
528
+ return parseDate(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric" });
466
529
  }
467
530
  function formatDateTime(dateStr) {
468
531
  if (!dateStr) return "";
469
- const d = parseDate(dateStr);
470
- return d.toLocaleDateString("en-US", {
532
+ return parseDate(dateStr).toLocaleString("en-US", {
471
533
  month: "short",
472
534
  day: "numeric",
473
535
  hour: "numeric",
474
536
  minute: "2-digit"
475
537
  });
476
538
  }
539
+ function formatTaskId(id) {
540
+ return `T-${id.slice(-6).toUpperCase()}`;
541
+ }
477
542
  function stripMentionMarkup(text) {
478
543
  return text.replace(/@\[(.*?)\]\(.*?\)/g, "@$1");
479
544
  }
@@ -484,12 +549,14 @@ function getDescriptionPreview(desc) {
484
549
  const val = desc[section.key]?.trim();
485
550
  if (val) return stripMentionMarkup(val);
486
551
  }
552
+ if (desc.open_questions?.trim()) return stripMentionMarkup(desc.open_questions.trim());
487
553
  return "";
488
554
  }
489
555
  function hasDescription(desc) {
490
556
  if (!desc) return false;
491
557
  if (typeof desc === "string") return desc.trim().length > 0;
492
- 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();
493
560
  }
494
561
  function getUserProjects(apps, allProjects) {
495
562
  if (apps.includes("all")) return allProjects;
@@ -515,6 +582,24 @@ var XIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */
515
582
  /* @__PURE__ */ jsx4("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
516
583
  ] });
517
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
+ ] });
518
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" }) });
519
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: [
520
605
  /* @__PURE__ */ jsx4("rect", { width: "8", height: "4", x: "8", y: "2", rx: "1", ry: "1" }),
@@ -525,7 +610,33 @@ var LinkIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__
525
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" }),
526
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" })
527
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
+ ] });
528
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
+ ] });
529
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" }) });
530
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" }) });
531
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" }) });
@@ -544,6 +655,73 @@ var FeedbackIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PUR
544
655
  /* @__PURE__ */ jsx4("path", { d: "M12 12h.01" }),
545
656
  /* @__PURE__ */ jsx4("path", { d: "M16 12h.01" })
546
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
+ ] });
547
725
 
548
726
  // src/components/TagBadge.tsx
549
727
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -2053,9 +2231,13 @@ function TaskBoard({
2053
2231
  try {
2054
2232
  const task = await service.getTask(taskId);
2055
2233
  if (cancelled) return;
2056
- setSelectedTask(task);
2057
- service.markTaskRead(taskId).catch(() => {
2058
- });
2234
+ if (onTaskOpen) {
2235
+ onTaskOpen(task);
2236
+ } else {
2237
+ setSelectedTask(task);
2238
+ service.markTaskRead(taskId).catch(() => {
2239
+ });
2240
+ }
2059
2241
  const url = new URL(window.location.href);
2060
2242
  url.searchParams.delete("task");
2061
2243
  window.history.replaceState({}, "", url.toString());
@@ -2066,7 +2248,7 @@ function TaskBoard({
2066
2248
  return () => {
2067
2249
  cancelled = true;
2068
2250
  };
2069
- }, [board.selectedProject, board.boardLoading, sharedTaskHandled, service]);
2251
+ }, [board.selectedProject, board.boardLoading, sharedTaskHandled, service, onTaskOpen]);
2070
2252
  useEffect8(() => {
2071
2253
  if (typeof window === "undefined") return;
2072
2254
  if (board.selectedProject && board.projects.length > 1) {
@@ -2088,7 +2270,9 @@ function TaskBoard({
2088
2270
  );
2089
2271
  }, [actions]);
2090
2272
  const handleTaskClick = (task) => {
2091
- setSelectedTask(task);
2273
+ if (!onTaskOpen) {
2274
+ setSelectedTask(task);
2275
+ }
2092
2276
  onTaskOpen?.(task);
2093
2277
  actions.markTaskRead(task.id);
2094
2278
  if (task.has_unread) {
@@ -2114,9 +2298,13 @@ function TaskBoard({
2114
2298
  }
2115
2299
  try {
2116
2300
  const task = await service.getTask(taskId);
2117
- setSelectedTask(task);
2118
- service.markTaskRead(taskId).catch(() => {
2119
- });
2301
+ if (onTaskOpen) {
2302
+ onTaskOpen(task);
2303
+ } else {
2304
+ setSelectedTask(task);
2305
+ service.markTaskRead(taskId).catch(() => {
2306
+ });
2307
+ }
2120
2308
  } catch {
2121
2309
  board.setError("Could not open task.");
2122
2310
  }
@@ -2237,33 +2425,3195 @@ function TaskBoard({
2237
2425
  ))
2238
2426
  ] });
2239
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
+ }
2240
5558
  export {
5559
+ ActivityList,
5560
+ ArrowLeftIcon,
5561
+ AttachmentsSection,
2241
5562
  BellIcon,
2242
5563
  BoardSkeleton,
5564
+ Bold,
5565
+ ChatDotsIcon,
5566
+ CheckCircle2Icon,
2243
5567
  CheckIcon,
2244
5568
  ChevronDownIcon,
5569
+ ChevronLeftIcon,
5570
+ ChevronRightIcon,
5571
+ Code,
5572
+ ContextPill,
5573
+ CornerUpLeftIcon,
2245
5574
  CreateTaskModal,
2246
5575
  DEFAULT_COLUMNS,
5576
+ DEFAULT_INTERNAL_LABEL,
2247
5577
  DEFAULT_PAGE_SIZE,
2248
5578
  DEFAULT_PRIORITIES,
2249
5579
  DESCRIPTION_SECTIONS,
5580
+ DescriptionSection,
2250
5581
  EMPTY_DESCRIPTION,
5582
+ ExternalLinkIcon,
2251
5583
  FeedbackIcon,
5584
+ FileTextIcon,
2252
5585
  FilterBar,
2253
5586
  FilterIcon,
5587
+ Heading2,
5588
+ HelpCircleIcon,
5589
+ HighlightBubble,
5590
+ HistoryIcon,
5591
+ ImageIcon,
5592
+ Italic,
2254
5593
  KanbanColumn,
2255
5594
  KanbanIcon,
5595
+ Link2Icon,
2256
5596
  LinkIcon,
5597
+ List,
5598
+ ListOrdered,
2257
5599
  LockIcon,
5600
+ MarkdownEditor,
5601
+ MarkdownView,
2258
5602
  MentionText,
2259
5603
  MentionTextarea,
2260
5604
  MessageSquareIcon,
5605
+ MoreVerticalIcon,
2261
5606
  NotificationBell,
5607
+ OutstandingQuestionsSection,
2262
5608
  POSITION_GAP,
2263
5609
  PREDEFINED_TAGS,
2264
5610
  PencilIcon,
2265
5611
  PlusIcon,
2266
5612
  PriorityBadge,
5613
+ Quote,
5614
+ RotateCcwIcon,
5615
+ Share2Icon,
5616
+ SidebarToggleIcon,
2267
5617
  SkeletonCard,
2268
5618
  SkeletonPulse,
2269
5619
  TagBadge,
@@ -2271,24 +5621,39 @@ export {
2271
5621
  TaskBoardProvider,
2272
5622
  TaskCard,
2273
5623
  TaskDetailPanel,
5624
+ TaskDetailView,
5625
+ ThreadCard,
5626
+ ThreadComposer,
5627
+ ThreadDetailView,
5628
+ ThreadsPanel,
2274
5629
  TrashIcon,
2275
5630
  UserAvatar,
2276
5631
  XIcon,
2277
5632
  createTaskBoardService,
5633
+ deriveThreads,
2278
5634
  formatDate,
2279
5635
  formatDateTime,
5636
+ formatTaskId,
2280
5637
  getDescriptionPreview,
2281
5638
  getInitials,
2282
5639
  getPriorityStyle,
2283
5640
  getTagStyle,
2284
5641
  getUserProjects,
2285
5642
  hasDescription,
5643
+ htmlToMd,
5644
+ mdToHtml,
5645
+ parseDate,
5646
+ sectionLabel,
5647
+ timeAgo,
2286
5648
  toDisplayText,
2287
5649
  toStoredText,
5650
+ useHighlightAnchor,
2288
5651
  useShareLink,
2289
5652
  useTaskActions,
5653
+ useTaskAttachments,
2290
5654
  useTaskBoard,
2291
5655
  useTaskBoardContext,
2292
- useTaskDetail
5656
+ useTaskDetail,
5657
+ useTaskQuestions
2293
5658
  };
2294
5659
  //# sourceMappingURL=index.mjs.map