@emberai-engg/task-board 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -71
- package/dist/index.d.mts +563 -3
- package/dist/index.d.ts +563 -3
- package/dist/index.js +3473 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3415 -40
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +64 -0
- package/package.json +2 -2
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-
|
|
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
|
}),
|
|
@@ -374,6 +496,7 @@ function useTaskBoard(isDragging) {
|
|
|
374
496
|
tasks,
|
|
375
497
|
setTasks,
|
|
376
498
|
columnTotals,
|
|
499
|
+
setColumnTotals,
|
|
377
500
|
columnUnreads,
|
|
378
501
|
setColumnUnreads,
|
|
379
502
|
boardLoading,
|
|
@@ -389,7 +512,7 @@ function useTaskBoard(isDragging) {
|
|
|
389
512
|
|
|
390
513
|
// src/hooks/useTaskActions.ts
|
|
391
514
|
var import_react3 = require("react");
|
|
392
|
-
function useTaskActions(tasks, setTasks, fetchTasks, isDragging) {
|
|
515
|
+
function useTaskActions(tasks, setTasks, fetchTasks, isDragging, setColumnTotals) {
|
|
393
516
|
const { service, config } = useTaskBoardContext();
|
|
394
517
|
const internalDragging = (0, import_react3.useRef)(false);
|
|
395
518
|
const draggingRef = isDragging ?? internalDragging;
|
|
@@ -443,6 +566,13 @@ function useTaskActions(tasks, setTasks, fetchTasks, isDragging) {
|
|
|
443
566
|
}
|
|
444
567
|
return newTasks;
|
|
445
568
|
});
|
|
569
|
+
if (sourceStatus !== destStatus && setColumnTotals) {
|
|
570
|
+
setColumnTotals((prev) => ({
|
|
571
|
+
...prev,
|
|
572
|
+
[sourceStatus]: Math.max(0, (prev[sourceStatus] || 0) - 1),
|
|
573
|
+
[destStatus]: (prev[destStatus] || 0) + 1
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
446
576
|
try {
|
|
447
577
|
await service.updateTask(taskId, { status: destStatus, position: newPosition });
|
|
448
578
|
} catch {
|
|
@@ -450,7 +580,7 @@ function useTaskActions(tasks, setTasks, fetchTasks, isDragging) {
|
|
|
450
580
|
} finally {
|
|
451
581
|
draggingRef.current = false;
|
|
452
582
|
}
|
|
453
|
-
}, [setTasks, service, fetchTasks]);
|
|
583
|
+
}, [setTasks, setColumnTotals, service, fetchTasks]);
|
|
454
584
|
return { createTask, updateTask, deleteTask, markTaskRead, moveTask };
|
|
455
585
|
}
|
|
456
586
|
|
|
@@ -522,27 +652,27 @@ function getInitials(name) {
|
|
|
522
652
|
}
|
|
523
653
|
function parseDate(dateStr) {
|
|
524
654
|
if (!dateStr) return /* @__PURE__ */ new Date();
|
|
525
|
-
|
|
526
|
-
if (isNaN(d.getTime()) && !dateStr.endsWith("Z") && !dateStr.includes("+")) {
|
|
655
|
+
if (!dateStr.endsWith("Z") && !dateStr.includes("+") && !/\d{2}:\d{2}$/.test(dateStr.slice(-6))) {
|
|
527
656
|
return /* @__PURE__ */ new Date(dateStr + "Z");
|
|
528
657
|
}
|
|
529
|
-
return
|
|
658
|
+
return new Date(dateStr);
|
|
530
659
|
}
|
|
531
660
|
function formatDate(dateStr) {
|
|
532
661
|
if (!dateStr) return "";
|
|
533
|
-
|
|
534
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
662
|
+
return parseDate(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
535
663
|
}
|
|
536
664
|
function formatDateTime(dateStr) {
|
|
537
665
|
if (!dateStr) return "";
|
|
538
|
-
|
|
539
|
-
return d.toLocaleDateString("en-US", {
|
|
666
|
+
return parseDate(dateStr).toLocaleString("en-US", {
|
|
540
667
|
month: "short",
|
|
541
668
|
day: "numeric",
|
|
542
669
|
hour: "numeric",
|
|
543
670
|
minute: "2-digit"
|
|
544
671
|
});
|
|
545
672
|
}
|
|
673
|
+
function formatTaskId(id) {
|
|
674
|
+
return `T-${id.slice(-6).toUpperCase()}`;
|
|
675
|
+
}
|
|
546
676
|
function stripMentionMarkup(text) {
|
|
547
677
|
return text.replace(/@\[(.*?)\]\(.*?\)/g, "@$1");
|
|
548
678
|
}
|
|
@@ -553,12 +683,14 @@ function getDescriptionPreview(desc) {
|
|
|
553
683
|
const val = desc[section.key]?.trim();
|
|
554
684
|
if (val) return stripMentionMarkup(val);
|
|
555
685
|
}
|
|
686
|
+
if (desc.open_questions?.trim()) return stripMentionMarkup(desc.open_questions.trim());
|
|
556
687
|
return "";
|
|
557
688
|
}
|
|
558
689
|
function hasDescription(desc) {
|
|
559
690
|
if (!desc) return false;
|
|
560
691
|
if (typeof desc === "string") return desc.trim().length > 0;
|
|
561
|
-
|
|
692
|
+
if (DESCRIPTION_SECTIONS.some((s) => desc[s.key]?.trim())) return true;
|
|
693
|
+
return !!desc.open_questions?.trim();
|
|
562
694
|
}
|
|
563
695
|
function getUserProjects(apps, allProjects) {
|
|
564
696
|
if (apps.includes("all")) return allProjects;
|
|
@@ -584,6 +716,24 @@ var XIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__ */
|
|
|
584
716
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
585
717
|
] });
|
|
586
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
|
+
] });
|
|
587
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" }) });
|
|
588
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: [
|
|
589
739
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { width: "8", height: "4", x: "8", y: "2", rx: "1", ry: "1" }),
|
|
@@ -594,7 +744,33 @@ var LinkIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PURE__
|
|
|
594
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" }),
|
|
595
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" })
|
|
596
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
|
+
] });
|
|
597
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
|
+
] });
|
|
598
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" }) });
|
|
599
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" }) });
|
|
600
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" }) });
|
|
@@ -613,6 +789,73 @@ var FeedbackIcon = ({ className = "", size = 24, strokeWidth = 2 }) => /* @__PUR
|
|
|
613
789
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M12 12h.01" }),
|
|
614
790
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M16 12h.01" })
|
|
615
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
|
+
] });
|
|
616
859
|
|
|
617
860
|
// src/components/TagBadge.tsx
|
|
618
861
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
@@ -696,18 +939,20 @@ var TaskCard = (0, import_react5.memo)(function TaskCard2({ task, index, onClick
|
|
|
696
939
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
697
940
|
function LoadMoreSentinel({ loading, onLoadMore, remaining }) {
|
|
698
941
|
const sentinelRef = (0, import_react6.useRef)(null);
|
|
942
|
+
const onLoadMoreRef = (0, import_react6.useRef)(onLoadMore);
|
|
943
|
+
onLoadMoreRef.current = onLoadMore;
|
|
699
944
|
(0, import_react6.useEffect)(() => {
|
|
700
945
|
const el = sentinelRef.current;
|
|
701
946
|
if (!el) return;
|
|
702
947
|
const observer = new IntersectionObserver(
|
|
703
948
|
([entry]) => {
|
|
704
|
-
if (entry.isIntersecting && !loading)
|
|
949
|
+
if (entry.isIntersecting && !loading) onLoadMoreRef.current();
|
|
705
950
|
},
|
|
706
951
|
{ threshold: 0.1 }
|
|
707
952
|
);
|
|
708
953
|
observer.observe(el);
|
|
709
954
|
return () => observer.disconnect();
|
|
710
|
-
}, [loading
|
|
955
|
+
}, [loading]);
|
|
711
956
|
const skeletonCount = loading ? Math.min(remaining, 10) : 0;
|
|
712
957
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { ref: sentinelRef, className: "space-y-2 pt-2", children: Array.from({ length: skeletonCount }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SkeletonCard, {}, i)) });
|
|
713
958
|
}
|
|
@@ -2102,7 +2347,7 @@ function TaskBoard({
|
|
|
2102
2347
|
const { columns, features, service } = useTaskBoardContext();
|
|
2103
2348
|
const isDraggingRef = (0, import_react13.useRef)(false);
|
|
2104
2349
|
const board = useTaskBoard(isDraggingRef);
|
|
2105
|
-
const actions = useTaskActions(board.tasks, board.setTasks, board.fetchTasks, isDraggingRef);
|
|
2350
|
+
const actions = useTaskActions(board.tasks, board.setTasks, board.fetchTasks, isDraggingRef, board.setColumnTotals);
|
|
2106
2351
|
const { copiedTaskId, copyShareLink } = useShareLink();
|
|
2107
2352
|
const [selectedTask, setSelectedTask] = (0, import_react13.useState)(null);
|
|
2108
2353
|
const [createForStatus, setCreateForStatus] = (0, import_react13.useState)("");
|
|
@@ -2120,9 +2365,13 @@ function TaskBoard({
|
|
|
2120
2365
|
try {
|
|
2121
2366
|
const task = await service.getTask(taskId);
|
|
2122
2367
|
if (cancelled) return;
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
}
|
|
2368
|
+
if (onTaskOpen) {
|
|
2369
|
+
onTaskOpen(task);
|
|
2370
|
+
} else {
|
|
2371
|
+
setSelectedTask(task);
|
|
2372
|
+
service.markTaskRead(taskId).catch(() => {
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2126
2375
|
const url = new URL(window.location.href);
|
|
2127
2376
|
url.searchParams.delete("task");
|
|
2128
2377
|
window.history.replaceState({}, "", url.toString());
|
|
@@ -2133,7 +2382,7 @@ function TaskBoard({
|
|
|
2133
2382
|
return () => {
|
|
2134
2383
|
cancelled = true;
|
|
2135
2384
|
};
|
|
2136
|
-
}, [board.selectedProject, board.boardLoading, sharedTaskHandled, service]);
|
|
2385
|
+
}, [board.selectedProject, board.boardLoading, sharedTaskHandled, service, onTaskOpen]);
|
|
2137
2386
|
(0, import_react13.useEffect)(() => {
|
|
2138
2387
|
if (typeof window === "undefined") return;
|
|
2139
2388
|
if (board.selectedProject && board.projects.length > 1) {
|
|
@@ -2155,7 +2404,9 @@ function TaskBoard({
|
|
|
2155
2404
|
);
|
|
2156
2405
|
}, [actions]);
|
|
2157
2406
|
const handleTaskClick = (task) => {
|
|
2158
|
-
|
|
2407
|
+
if (!onTaskOpen) {
|
|
2408
|
+
setSelectedTask(task);
|
|
2409
|
+
}
|
|
2159
2410
|
onTaskOpen?.(task);
|
|
2160
2411
|
actions.markTaskRead(task.id);
|
|
2161
2412
|
if (task.has_unread) {
|
|
@@ -2181,9 +2432,13 @@ function TaskBoard({
|
|
|
2181
2432
|
}
|
|
2182
2433
|
try {
|
|
2183
2434
|
const task = await service.getTask(taskId);
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
}
|
|
2435
|
+
if (onTaskOpen) {
|
|
2436
|
+
onTaskOpen(task);
|
|
2437
|
+
} else {
|
|
2438
|
+
setSelectedTask(task);
|
|
2439
|
+
service.markTaskRead(taskId).catch(() => {
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2187
2442
|
} catch {
|
|
2188
2443
|
board.setError("Could not open task.");
|
|
2189
2444
|
}
|
|
@@ -2304,34 +2559,3196 @@ function TaskBoard({
|
|
|
2304
2559
|
))
|
|
2305
2560
|
] });
|
|
2306
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
+
}
|
|
2307
5692
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2308
5693
|
0 && (module.exports = {
|
|
5694
|
+
ActivityList,
|
|
5695
|
+
ArrowLeftIcon,
|
|
5696
|
+
AttachmentsSection,
|
|
2309
5697
|
BellIcon,
|
|
2310
5698
|
BoardSkeleton,
|
|
5699
|
+
Bold,
|
|
5700
|
+
ChatDotsIcon,
|
|
5701
|
+
CheckCircle2Icon,
|
|
2311
5702
|
CheckIcon,
|
|
2312
5703
|
ChevronDownIcon,
|
|
5704
|
+
ChevronLeftIcon,
|
|
5705
|
+
ChevronRightIcon,
|
|
5706
|
+
Code,
|
|
5707
|
+
ContextPill,
|
|
5708
|
+
CornerUpLeftIcon,
|
|
2313
5709
|
CreateTaskModal,
|
|
2314
5710
|
DEFAULT_COLUMNS,
|
|
5711
|
+
DEFAULT_INTERNAL_LABEL,
|
|
2315
5712
|
DEFAULT_PAGE_SIZE,
|
|
2316
5713
|
DEFAULT_PRIORITIES,
|
|
2317
5714
|
DESCRIPTION_SECTIONS,
|
|
5715
|
+
DescriptionSection,
|
|
2318
5716
|
EMPTY_DESCRIPTION,
|
|
5717
|
+
ExternalLinkIcon,
|
|
2319
5718
|
FeedbackIcon,
|
|
5719
|
+
FileTextIcon,
|
|
2320
5720
|
FilterBar,
|
|
2321
5721
|
FilterIcon,
|
|
5722
|
+
Heading2,
|
|
5723
|
+
HelpCircleIcon,
|
|
5724
|
+
HighlightBubble,
|
|
5725
|
+
HistoryIcon,
|
|
5726
|
+
ImageIcon,
|
|
5727
|
+
Italic,
|
|
2322
5728
|
KanbanColumn,
|
|
2323
5729
|
KanbanIcon,
|
|
5730
|
+
Link2Icon,
|
|
2324
5731
|
LinkIcon,
|
|
5732
|
+
List,
|
|
5733
|
+
ListOrdered,
|
|
2325
5734
|
LockIcon,
|
|
5735
|
+
MarkdownEditor,
|
|
5736
|
+
MarkdownView,
|
|
2326
5737
|
MentionText,
|
|
2327
5738
|
MentionTextarea,
|
|
2328
5739
|
MessageSquareIcon,
|
|
5740
|
+
MoreVerticalIcon,
|
|
2329
5741
|
NotificationBell,
|
|
5742
|
+
OutstandingQuestionsSection,
|
|
2330
5743
|
POSITION_GAP,
|
|
2331
5744
|
PREDEFINED_TAGS,
|
|
2332
5745
|
PencilIcon,
|
|
2333
5746
|
PlusIcon,
|
|
2334
5747
|
PriorityBadge,
|
|
5748
|
+
Quote,
|
|
5749
|
+
RotateCcwIcon,
|
|
5750
|
+
Share2Icon,
|
|
5751
|
+
SidebarToggleIcon,
|
|
2335
5752
|
SkeletonCard,
|
|
2336
5753
|
SkeletonPulse,
|
|
2337
5754
|
TagBadge,
|
|
@@ -2339,24 +5756,39 @@ function TaskBoard({
|
|
|
2339
5756
|
TaskBoardProvider,
|
|
2340
5757
|
TaskCard,
|
|
2341
5758
|
TaskDetailPanel,
|
|
5759
|
+
TaskDetailView,
|
|
5760
|
+
ThreadCard,
|
|
5761
|
+
ThreadComposer,
|
|
5762
|
+
ThreadDetailView,
|
|
5763
|
+
ThreadsPanel,
|
|
2342
5764
|
TrashIcon,
|
|
2343
5765
|
UserAvatar,
|
|
2344
5766
|
XIcon,
|
|
2345
5767
|
createTaskBoardService,
|
|
5768
|
+
deriveThreads,
|
|
2346
5769
|
formatDate,
|
|
2347
5770
|
formatDateTime,
|
|
5771
|
+
formatTaskId,
|
|
2348
5772
|
getDescriptionPreview,
|
|
2349
5773
|
getInitials,
|
|
2350
5774
|
getPriorityStyle,
|
|
2351
5775
|
getTagStyle,
|
|
2352
5776
|
getUserProjects,
|
|
2353
5777
|
hasDescription,
|
|
5778
|
+
htmlToMd,
|
|
5779
|
+
mdToHtml,
|
|
5780
|
+
parseDate,
|
|
5781
|
+
sectionLabel,
|
|
5782
|
+
timeAgo,
|
|
2354
5783
|
toDisplayText,
|
|
2355
5784
|
toStoredText,
|
|
5785
|
+
useHighlightAnchor,
|
|
2356
5786
|
useShareLink,
|
|
2357
5787
|
useTaskActions,
|
|
5788
|
+
useTaskAttachments,
|
|
2358
5789
|
useTaskBoard,
|
|
2359
5790
|
useTaskBoardContext,
|
|
2360
|
-
useTaskDetail
|
|
5791
|
+
useTaskDetail,
|
|
5792
|
+
useTaskQuestions
|
|
2361
5793
|
});
|
|
2362
5794
|
//# sourceMappingURL=index.js.map
|