@emberai-engg/task-board 0.3.6 → 0.4.1

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