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