@emberai-engg/task-board 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1659,7 +1659,8 @@ function MentionTextarea({
1659
1659
  placeholder = "",
1660
1660
  rows = 2,
1661
1661
  className = "",
1662
- disabled = false
1662
+ disabled = false,
1663
+ autoFocus = false
1663
1664
  }) {
1664
1665
  const { service, features } = useTaskBoardContext();
1665
1666
  const textareaRef = (0, import_react11.useRef)(null);
@@ -1769,7 +1770,8 @@ function MentionTextarea({
1769
1770
  rows,
1770
1771
  className,
1771
1772
  placeholder,
1772
- disabled
1773
+ disabled,
1774
+ autoFocus
1773
1775
  }
1774
1776
  ),
1775
1777
  mentionQuery !== null && mentionUsers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "absolute bottom-full left-0 mb-1 w-64 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1 max-h-48 overflow-y-auto", children: [
@@ -2593,7 +2595,8 @@ function deriveThreads(comments, attachments) {
2593
2595
  author_name: r.author_name,
2594
2596
  content: r.content,
2595
2597
  created_at: r.created_at,
2596
- is_internal: !!r.is_internal
2598
+ is_internal: !!r.is_internal,
2599
+ edited: !!r.edited
2597
2600
  }));
2598
2601
  const threadAttachments = (c.attachment_ids || []).map((aid) => attachById.get(aid)).filter((a) => Boolean(a));
2599
2602
  return {
@@ -2607,6 +2610,7 @@ function deriveThreads(comments, attachments) {
2607
2610
  is_internal: !!c.is_internal,
2608
2611
  status: c.thread_status === "complete" ? "complete" : "active",
2609
2612
  rawContent: c.content,
2613
+ edited: !!c.edited,
2610
2614
  anchor: c.anchor || null,
2611
2615
  attachments: threadAttachments,
2612
2616
  replies
@@ -3055,10 +3059,13 @@ function DescriptionSection({
3055
3059
  onChange,
3056
3060
  status,
3057
3061
  onStatusChange,
3058
- saving
3062
+ saving,
3063
+ anchors,
3064
+ onAnchorClick
3059
3065
  }) {
3060
3066
  const [editing, setEditing] = (0, import_react16.useState)(false);
3061
3067
  const [openHelp, setOpenHelp] = (0, import_react16.useState)(null);
3068
+ const readRef = (0, import_react16.useRef)(null);
3062
3069
  const startEdit = () => setEditing(true);
3063
3070
  const cancel = () => setEditing(false);
3064
3071
  const handleCommit = (md) => {
@@ -3066,6 +3073,52 @@ function DescriptionSection({
3066
3073
  setEditing(false);
3067
3074
  };
3068
3075
  const hasContent = value.trim().length > 0;
3076
+ (0, import_react16.useEffect)(() => {
3077
+ if (editing) return;
3078
+ const root = readRef.current;
3079
+ if (!root) return;
3080
+ root.querySelectorAll("mark.eb-tb-annot").forEach((m) => {
3081
+ const text = m.textContent || "";
3082
+ m.replaceWith(document.createTextNode(text));
3083
+ });
3084
+ root.normalize();
3085
+ if (!anchors || anchors.length === 0) return;
3086
+ for (const anchor of anchors) {
3087
+ const snippet = anchor.snippet?.trim();
3088
+ if (!snippet) continue;
3089
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
3090
+ let node = walker.nextNode();
3091
+ while (node) {
3092
+ const txt = node.textContent || "";
3093
+ const idx = txt.indexOf(snippet);
3094
+ if (idx !== -1) {
3095
+ const range = document.createRange();
3096
+ try {
3097
+ range.setStart(node, idx);
3098
+ range.setEnd(node, idx + snippet.length);
3099
+ const mark = document.createElement("mark");
3100
+ mark.className = "eb-tb-annot";
3101
+ mark.dataset.threadId = anchor.threadId;
3102
+ range.surroundContents(mark);
3103
+ } catch {
3104
+ }
3105
+ break;
3106
+ }
3107
+ node = walker.nextNode();
3108
+ }
3109
+ }
3110
+ }, [anchors, value, editing]);
3111
+ const handleReadClick = (e) => {
3112
+ const target = e.target;
3113
+ const mark = target?.closest("mark.eb-tb-annot");
3114
+ if (mark && mark.dataset.threadId && onAnchorClick) {
3115
+ e.preventDefault();
3116
+ e.stopPropagation();
3117
+ onAnchorClick(mark.dataset.threadId);
3118
+ return;
3119
+ }
3120
+ startEdit();
3121
+ };
3069
3122
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { className: "group", "data-section": sectionKey, children: [
3070
3123
  /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center justify-between gap-3 mb-3", children: [
3071
3124
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h2", { className: "text-[15px] font-semibold text-neutral-900 tracking-tight", children: label }),
@@ -3119,11 +3172,19 @@ function DescriptionSection({
3119
3172
  saving
3120
3173
  }
3121
3174
  ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3122
- "button",
3175
+ "div",
3123
3176
  {
3124
- type: "button",
3125
- onClick: startEdit,
3126
- className: "w-full text-left rounded-lg border border-transparent hover:border-neutral-200 hover:bg-neutral-50/60 transition-colors px-3.5 py-2.5 -mx-3.5 -my-2.5 cursor-text",
3177
+ ref: readRef,
3178
+ role: "button",
3179
+ tabIndex: 0,
3180
+ onClick: handleReadClick,
3181
+ onKeyDown: (e) => {
3182
+ if (e.key === "Enter" || e.key === " ") {
3183
+ e.preventDefault();
3184
+ startEdit();
3185
+ }
3186
+ },
3187
+ 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 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#FF5E00]/20",
3127
3188
  children: hasContent ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MarkdownView, { content: value, className: "text-[13px]" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[13px] text-neutral-400 italic", children: placeholder })
3128
3189
  }
3129
3190
  )
@@ -4123,7 +4184,8 @@ function ThreadComposer({
4123
4184
  onChange: setBody,
4124
4185
  rows: 4,
4125
4186
  placeholder: "Write something\u2026 (type @ to mention)",
4126
- className: "w-full rounded-lg border border-neutral-200 px-3 py-2 text-[12px] text-neutral-800 min-h-[100px] focus:outline-none focus:ring-1 focus:ring-neutral-300 focus:border-neutral-300 resize-none"
4187
+ 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",
4188
+ autoFocus: true
4127
4189
  }
4128
4190
  ),
4129
4191
  /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
@@ -4353,9 +4415,13 @@ var import_react20 = require("react");
4353
4415
  var import_jsx_runtime25 = require("react/jsx-runtime");
4354
4416
  function ThreadDetailView({
4355
4417
  thread,
4418
+ currentUsername,
4356
4419
  onBack,
4357
4420
  onReply,
4358
4421
  onUpdateThread,
4422
+ onEditMessage,
4423
+ onDeleteMessage,
4424
+ onDeleteThread,
4359
4425
  onAnchorClick,
4360
4426
  isInternalUser
4361
4427
  }) {
@@ -4365,10 +4431,29 @@ function ThreadDetailView({
4365
4431
  const [posting, setPosting] = (0, import_react20.useState)(false);
4366
4432
  const [editingTitle, setEditingTitle] = (0, import_react20.useState)(false);
4367
4433
  const [titleDraft, setTitleDraft] = (0, import_react20.useState)(thread.title);
4434
+ const [editingMessageId, setEditingMessageId] = (0, import_react20.useState)(null);
4435
+ const [editDraft, setEditDraft] = (0, import_react20.useState)("");
4436
+ const [savingEdit, setSavingEdit] = (0, import_react20.useState)(false);
4437
+ const [headerMenuOpen, setHeaderMenuOpen] = (0, import_react20.useState)(false);
4438
+ const headerMenuRef = (0, import_react20.useRef)(null);
4368
4439
  const messagesEndRef = (0, import_react20.useRef)(null);
4369
4440
  (0, import_react20.useEffect)(() => {
4370
4441
  setTitleDraft(thread.title);
4371
4442
  }, [thread.title]);
4443
+ (0, import_react20.useEffect)(() => {
4444
+ setEditingMessageId(null);
4445
+ setEditDraft("");
4446
+ }, [thread.id]);
4447
+ (0, import_react20.useEffect)(() => {
4448
+ if (!headerMenuOpen) return;
4449
+ const onClick = (e) => {
4450
+ if (headerMenuRef.current && !headerMenuRef.current.contains(e.target)) {
4451
+ setHeaderMenuOpen(false);
4452
+ }
4453
+ };
4454
+ document.addEventListener("mousedown", onClick);
4455
+ return () => document.removeEventListener("mousedown", onClick);
4456
+ }, [headerMenuOpen]);
4372
4457
  const submit = async () => {
4373
4458
  if (!body.trim()) return;
4374
4459
  setPosting(true);
@@ -4396,7 +4481,40 @@ function ThreadDetailView({
4396
4481
  const next = thread.status === "complete" ? "active" : "complete";
4397
4482
  await onUpdateThread({ thread_status: next });
4398
4483
  };
4484
+ const startEditMessage = (id, content) => {
4485
+ setEditingMessageId(id);
4486
+ setEditDraft(content);
4487
+ };
4488
+ const cancelEditMessage = () => {
4489
+ setEditingMessageId(null);
4490
+ setEditDraft("");
4491
+ };
4492
+ const commitEditMessage = async () => {
4493
+ const id = editingMessageId;
4494
+ const next = editDraft.trim();
4495
+ if (!id || !next) {
4496
+ cancelEditMessage();
4497
+ return;
4498
+ }
4499
+ setSavingEdit(true);
4500
+ try {
4501
+ await onEditMessage(id, next);
4502
+ cancelEditMessage();
4503
+ } finally {
4504
+ setSavingEdit(false);
4505
+ }
4506
+ };
4507
+ const handleDeleteMessage = async (id, isThreadRoot) => {
4508
+ const msg = isThreadRoot ? "Delete this thread and all of its replies? This cannot be undone." : "Delete this message? This cannot be undone.";
4509
+ if (!confirm(msg)) return;
4510
+ if (isThreadRoot) {
4511
+ await onDeleteThread(id);
4512
+ } else {
4513
+ await onDeleteMessage(id);
4514
+ }
4515
+ };
4399
4516
  const isComplete = thread.status === "complete";
4517
+ const isThreadAuthor = thread.author_id === currentUsername;
4400
4518
  return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
4401
4519
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "shrink-0 bg-white px-4 py-3 border-b border-neutral-100", children: [
4402
4520
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex items-start justify-between gap-4", children: [
@@ -4411,21 +4529,71 @@ function ThreadDetailView({
4411
4529
  ]
4412
4530
  }
4413
4531
  ),
4414
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4415
- "button",
4416
- {
4417
- type: "button",
4418
- onClick: toggleStatus,
4419
- className: "inline-flex items-center justify-center gap-1.5 text-[12px] font-medium text-neutral-600 hover:text-neutral-900 px-3 h-8 rounded-xl border border-neutral-200 bg-white hover:bg-neutral-50",
4420
- children: isComplete ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
4421
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(RotateCcwIcon, { size: 12, strokeWidth: 1.5 }),
4422
- "Reopen"
4423
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
4424
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(CheckCircle2Icon, { size: 12, strokeWidth: 1.5 }),
4425
- "Mark complete"
4532
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex items-center gap-1.5 shrink-0", children: [
4533
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4534
+ "button",
4535
+ {
4536
+ type: "button",
4537
+ onClick: toggleStatus,
4538
+ 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",
4539
+ children: isComplete ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
4540
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(RotateCcwIcon, { size: 12, strokeWidth: 1.5 }),
4541
+ "Reopen"
4542
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
4543
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(CheckCircle2Icon, { size: 12, strokeWidth: 1.5 }),
4544
+ "Mark complete"
4545
+ ] })
4546
+ }
4547
+ ),
4548
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { ref: headerMenuRef, className: "relative", children: [
4549
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4550
+ "button",
4551
+ {
4552
+ type: "button",
4553
+ onClick: () => setHeaderMenuOpen((v) => !v),
4554
+ "aria-label": "Thread actions",
4555
+ className: "w-8 h-8 flex items-center justify-center rounded-xl border border-neutral-200 bg-white text-neutral-500 hover:text-neutral-900 hover:border-neutral-300 transition-colors",
4556
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MoreVerticalIcon, { size: 14, strokeWidth: 1.5 })
4557
+ }
4558
+ ),
4559
+ headerMenuOpen && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "absolute right-0 top-full mt-1 w-44 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-40", children: [
4560
+ isThreadAuthor && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
4561
+ "button",
4562
+ {
4563
+ type: "button",
4564
+ onClick: () => {
4565
+ setHeaderMenuOpen(false);
4566
+ setEditingTitle(true);
4567
+ },
4568
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2.5",
4569
+ children: [
4570
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(PencilIcon, { size: 14, strokeWidth: 1.5 }),
4571
+ "Edit title"
4572
+ ]
4573
+ }
4574
+ ),
4575
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
4576
+ "button",
4577
+ {
4578
+ type: "button",
4579
+ onClick: async () => {
4580
+ setHeaderMenuOpen(false);
4581
+ if (!confirm(
4582
+ "Delete this thread and all of its replies? This cannot be undone."
4583
+ ))
4584
+ return;
4585
+ await onDeleteThread(thread.id);
4586
+ },
4587
+ className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2.5",
4588
+ children: [
4589
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(TrashIcon, { size: 14, strokeWidth: 1.5 }),
4590
+ "Delete thread"
4591
+ ]
4592
+ }
4593
+ )
4426
4594
  ] })
4427
- }
4428
- )
4595
+ ] })
4596
+ ] })
4429
4597
  ] }),
4430
4598
  editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4431
4599
  "input",
@@ -4467,27 +4635,60 @@ function ThreadDetailView({
4467
4635
  ) })
4468
4636
  ] }),
4469
4637
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex-1 overflow-y-auto p-3 flex flex-col gap-4", children: [
4470
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4638
+ editingMessageId === thread.id ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4639
+ ThreadMessageEditor,
4640
+ {
4641
+ authorName: thread.author_name,
4642
+ isInternal: thread.is_internal,
4643
+ value: editDraft,
4644
+ onChange: setEditDraft,
4645
+ onCommit: commitEditMessage,
4646
+ onCancel: cancelEditMessage,
4647
+ saving: savingEdit
4648
+ }
4649
+ ) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4471
4650
  ThreadMessage,
4472
4651
  {
4473
4652
  authorName: thread.author_name,
4474
4653
  createdAt: thread.created_at,
4475
4654
  content: thread.rawContent,
4476
4655
  isInternal: thread.is_internal,
4477
- internalLabel
4656
+ edited: thread.edited,
4657
+ isOwn: isThreadAuthor,
4658
+ internalLabel,
4659
+ onEdit: () => startEditMessage(thread.id, thread.rawContent),
4660
+ onDelete: () => handleDeleteMessage(thread.id, true)
4478
4661
  }
4479
4662
  ),
4480
- thread.replies.map((r) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4481
- ThreadMessage,
4482
- {
4483
- authorName: r.author_name,
4484
- createdAt: r.created_at,
4485
- content: r.content,
4486
- isInternal: r.is_internal,
4487
- internalLabel
4488
- },
4489
- r.id
4490
- )),
4663
+ thread.replies.map(
4664
+ (r) => editingMessageId === r.id ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4665
+ ThreadMessageEditor,
4666
+ {
4667
+ authorName: r.author_name,
4668
+ isInternal: r.is_internal,
4669
+ value: editDraft,
4670
+ onChange: setEditDraft,
4671
+ onCommit: commitEditMessage,
4672
+ onCancel: cancelEditMessage,
4673
+ saving: savingEdit
4674
+ },
4675
+ r.id
4676
+ ) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4677
+ ThreadMessage,
4678
+ {
4679
+ authorName: r.author_name,
4680
+ createdAt: r.created_at,
4681
+ content: r.content,
4682
+ isInternal: r.is_internal,
4683
+ edited: r.edited,
4684
+ isOwn: r.author_id === currentUsername,
4685
+ internalLabel,
4686
+ onEdit: () => startEditMessage(r.id, r.content),
4687
+ onDelete: () => handleDeleteMessage(r.id, false)
4688
+ },
4689
+ r.id
4690
+ )
4691
+ ),
4491
4692
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { ref: messagesEndRef })
4492
4693
  ] }),
4493
4694
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "shrink-0 p-3 border-t border-neutral-100 bg-white", children: [
@@ -4533,10 +4734,27 @@ function ThreadMessage({
4533
4734
  createdAt,
4534
4735
  content,
4535
4736
  isInternal,
4536
- internalLabel
4737
+ edited = false,
4738
+ isOwn = false,
4739
+ internalLabel,
4740
+ onEdit,
4741
+ onDelete
4537
4742
  }) {
4538
4743
  const initials = getInitials(authorName || "?");
4539
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex gap-2.5", children: [
4744
+ const [menuOpen, setMenuOpen] = (0, import_react20.useState)(false);
4745
+ const menuRef = (0, import_react20.useRef)(null);
4746
+ (0, import_react20.useEffect)(() => {
4747
+ if (!menuOpen) return;
4748
+ const onClick = (e) => {
4749
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
4750
+ setMenuOpen(false);
4751
+ }
4752
+ };
4753
+ document.addEventListener("mousedown", onClick);
4754
+ return () => document.removeEventListener("mousedown", onClick);
4755
+ }, [menuOpen]);
4756
+ const showActions = isOwn && (onEdit || onDelete);
4757
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "group flex gap-2.5", children: [
4540
4758
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4541
4759
  "div",
4542
4760
  {
@@ -4548,15 +4766,130 @@ function ThreadMessage({
4548
4766
  /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex items-center gap-2 mb-0.5 flex-wrap", children: [
4549
4767
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-[13px] font-medium text-neutral-900", children: authorName }),
4550
4768
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-[10px] text-neutral-400", children: timeAgo(createdAt) }),
4769
+ edited && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-[10px] text-neutral-400 italic", children: "(edited)" }),
4551
4770
  isInternal && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "text-[10px] text-neutral-500 bg-neutral-100 px-1.5 py-0.5 rounded inline-flex items-center gap-1", children: [
4552
4771
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(LockIcon, { size: 10, strokeWidth: 1.5 }),
4553
4772
  internalLabel
4554
- ] })
4773
+ ] }),
4774
+ showActions && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
4775
+ "div",
4776
+ {
4777
+ ref: menuRef,
4778
+ className: "ml-auto relative opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity",
4779
+ children: [
4780
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4781
+ "button",
4782
+ {
4783
+ type: "button",
4784
+ onClick: () => setMenuOpen((v) => !v),
4785
+ "aria-label": "Message actions",
4786
+ className: "w-6 h-6 flex items-center justify-center rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100",
4787
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MoreVerticalIcon, { size: 12, strokeWidth: 1.5 })
4788
+ }
4789
+ ),
4790
+ menuOpen && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "absolute right-0 top-full mt-1 w-36 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-30", children: [
4791
+ onEdit && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
4792
+ "button",
4793
+ {
4794
+ type: "button",
4795
+ onClick: () => {
4796
+ setMenuOpen(false);
4797
+ onEdit();
4798
+ },
4799
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2",
4800
+ children: [
4801
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(PencilIcon, { size: 12, strokeWidth: 1.5 }),
4802
+ "Edit"
4803
+ ]
4804
+ }
4805
+ ),
4806
+ onDelete && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
4807
+ "button",
4808
+ {
4809
+ type: "button",
4810
+ onClick: () => {
4811
+ setMenuOpen(false);
4812
+ onDelete();
4813
+ },
4814
+ className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2",
4815
+ children: [
4816
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(TrashIcon, { size: 12, strokeWidth: 1.5 }),
4817
+ "Delete"
4818
+ ]
4819
+ }
4820
+ )
4821
+ ] })
4822
+ ]
4823
+ }
4824
+ )
4555
4825
  ] }),
4556
4826
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "text-[12px] text-neutral-700 leading-relaxed whitespace-pre-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MentionText, { text: content }) })
4557
4827
  ] })
4558
4828
  ] });
4559
4829
  }
4830
+ function ThreadMessageEditor({
4831
+ authorName,
4832
+ isInternal,
4833
+ value,
4834
+ onChange,
4835
+ onCommit,
4836
+ onCancel,
4837
+ saving
4838
+ }) {
4839
+ const initials = getInitials(authorName || "?");
4840
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex gap-2.5", children: [
4841
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4842
+ "div",
4843
+ {
4844
+ 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"}`,
4845
+ children: initials
4846
+ }
4847
+ ),
4848
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex-1 min-w-0 flex flex-col gap-2", children: [
4849
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4850
+ MentionTextarea,
4851
+ {
4852
+ value,
4853
+ onChange,
4854
+ onKeyDown: (e) => {
4855
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
4856
+ e.preventDefault();
4857
+ onCommit();
4858
+ } else if (e.key === "Escape") {
4859
+ e.preventDefault();
4860
+ onCancel();
4861
+ }
4862
+ },
4863
+ rows: 3,
4864
+ placeholder: "Edit message\u2026",
4865
+ 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",
4866
+ autoFocus: true
4867
+ }
4868
+ ),
4869
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex items-center justify-end gap-2", children: [
4870
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4871
+ "button",
4872
+ {
4873
+ type: "button",
4874
+ onClick: onCancel,
4875
+ className: "text-[12px] font-medium text-neutral-500 hover:text-neutral-900 px-2 h-7 rounded hover:bg-neutral-50",
4876
+ children: "Cancel"
4877
+ }
4878
+ ),
4879
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4880
+ "button",
4881
+ {
4882
+ type: "button",
4883
+ onClick: onCommit,
4884
+ disabled: saving || !value.trim(),
4885
+ 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",
4886
+ children: saving ? "Saving..." : "Save"
4887
+ }
4888
+ )
4889
+ ] })
4890
+ ] })
4891
+ ] });
4892
+ }
4560
4893
 
4561
4894
  // src/components/ThreadsPanel.tsx
4562
4895
  var import_jsx_runtime26 = require("react/jsx-runtime");
@@ -4564,6 +4897,7 @@ function ThreadsPanel({
4564
4897
  threads,
4565
4898
  activity,
4566
4899
  attachments,
4900
+ currentUsername,
4567
4901
  open,
4568
4902
  onToggle,
4569
4903
  openThreadId,
@@ -4576,6 +4910,9 @@ function ThreadsPanel({
4576
4910
  onCreateThread,
4577
4911
  onCreateReply,
4578
4912
  onUpdateThread,
4913
+ onEditMessage,
4914
+ onDeleteMessage,
4915
+ onDeleteThread,
4579
4916
  onUploadAttachment,
4580
4917
  onAddLinkAttachment
4581
4918
  }) {
@@ -4751,9 +5088,13 @@ function ThreadsPanel({
4751
5088
  ThreadDetailView,
4752
5089
  {
4753
5090
  thread: openThread,
5091
+ currentUsername,
4754
5092
  onBack: () => onOpenThread(null),
4755
5093
  onReply: (content, isInternal) => onCreateReply(openThread.id, content, isInternal),
4756
5094
  onUpdateThread: (body) => onUpdateThread(openThread.id, body),
5095
+ onEditMessage,
5096
+ onDeleteMessage,
5097
+ onDeleteThread,
4757
5098
  onAnchorClick,
4758
5099
  isInternalUser
4759
5100
  }
@@ -4990,13 +5331,21 @@ function useHighlightAnchor() {
4990
5331
  return anchor;
4991
5332
  }, [bubble]);
4992
5333
  const focusAnchor = (0, import_react24.useCallback)((anchor) => {
4993
- const el = document.querySelector(`[data-section="${anchor.section}"]`);
4994
- if (!el) return;
4995
- el.scrollIntoView({ behavior: "smooth", block: "center" });
4996
- el.classList.add("ring-2", "ring-amber-300", "rounded-lg", "transition-all");
4997
- setTimeout(() => {
4998
- el.classList.remove("ring-2", "ring-amber-300", "rounded-lg");
4999
- }, 1500);
5334
+ const sectionEl = document.querySelector(`[data-section="${anchor.section}"]`);
5335
+ if (!sectionEl) return;
5336
+ sectionEl.scrollIntoView({ behavior: "smooth", block: "center" });
5337
+ const markEl = Array.from(sectionEl.querySelectorAll("mark.eb-tb-annot")).find(
5338
+ (m) => (m.textContent || "").includes(anchor.snippet)
5339
+ ) || sectionEl.querySelector("mark.eb-tb-annot");
5340
+ if (markEl) {
5341
+ markEl.classList.add("eb-tb-annot-pulse");
5342
+ setTimeout(() => markEl.classList.remove("eb-tb-annot-pulse"), 1500);
5343
+ } else {
5344
+ sectionEl.classList.add("ring-2", "ring-amber-300", "rounded-lg", "transition-all");
5345
+ setTimeout(() => {
5346
+ sectionEl.classList.remove("ring-2", "ring-amber-300", "rounded-lg");
5347
+ }, 1500);
5348
+ }
5000
5349
  }, []);
5001
5350
  return {
5002
5351
  bubble,
@@ -5252,6 +5601,28 @@ function TaskDetailView({
5252
5601
  } catch {
5253
5602
  }
5254
5603
  };
5604
+ const handleEditMessage = async (messageId, content) => {
5605
+ try {
5606
+ await service.editComment(taskId, messageId, { content });
5607
+ await fetchTask();
5608
+ } catch {
5609
+ }
5610
+ };
5611
+ const handleDeleteMessage = async (messageId) => {
5612
+ try {
5613
+ await service.deleteComment(taskId, messageId);
5614
+ await fetchTask();
5615
+ } catch {
5616
+ }
5617
+ };
5618
+ const handleDeleteThread = async (threadId) => {
5619
+ try {
5620
+ await service.deleteComment(taskId, threadId);
5621
+ setOpenThreadId(null);
5622
+ await fetchTask();
5623
+ } catch {
5624
+ }
5625
+ };
5255
5626
  const project = (0, import_react25.useMemo)(() => {
5256
5627
  if (!task) return null;
5257
5628
  return projects.find((p) => p.slug === task.project_slug) ?? null;
@@ -5260,6 +5631,29 @@ function TaskDetailView({
5260
5631
  () => deriveThreads(comments, attachments.attachments),
5261
5632
  [comments, attachments.attachments]
5262
5633
  );
5634
+ const anchorsBySection = (0, import_react25.useMemo)(() => {
5635
+ const map = {};
5636
+ for (const t of threads) {
5637
+ if (!t.anchor) continue;
5638
+ const key = t.anchor.section;
5639
+ if (!map[key]) map[key] = [];
5640
+ map[key].push({ snippet: t.anchor.snippet, threadId: t.id });
5641
+ }
5642
+ return map;
5643
+ }, [threads]);
5644
+ const openThreadFromAnchor = (0, import_react25.useCallback)(
5645
+ (threadId) => {
5646
+ setOpenThreadId(threadId);
5647
+ if (!panelOpen) {
5648
+ setPanelOpen(true);
5649
+ try {
5650
+ window.localStorage.setItem(PANEL_OPEN_KEY, "true");
5651
+ } catch {
5652
+ }
5653
+ }
5654
+ },
5655
+ [panelOpen]
5656
+ );
5263
5657
  const statusCol = columns.find((c) => c.key === taskStatus);
5264
5658
  const priorityStyle = getPriorityStyle(priority);
5265
5659
  const currentIdx = task ? projectTaskIds.indexOf(task.id) : -1;
@@ -5635,7 +6029,9 @@ function TaskDetailView({
5635
6029
  onChange: (v) => handleSectionChange(section.key, v),
5636
6030
  status,
5637
6031
  onStatusChange: (s) => handleSectionStatusChange(section.key, s),
5638
- saving
6032
+ saving,
6033
+ anchors: anchorsBySection[section.key],
6034
+ onAnchorClick: openThreadFromAnchor
5639
6035
  },
5640
6036
  section.key
5641
6037
  );
@@ -5670,6 +6066,7 @@ function TaskDetailView({
5670
6066
  threads,
5671
6067
  activity,
5672
6068
  attachments: attachments.attachments,
6069
+ currentUsername: user.username,
5673
6070
  open: panelOpen,
5674
6071
  onToggle: togglePanel,
5675
6072
  openThreadId,
@@ -5682,6 +6079,9 @@ function TaskDetailView({
5682
6079
  onCreateThread: handleCreateThread,
5683
6080
  onCreateReply: handleCreateReply,
5684
6081
  onUpdateThread: handleUpdateThread,
6082
+ onEditMessage: handleEditMessage,
6083
+ onDeleteMessage: handleDeleteMessage,
6084
+ onDeleteThread: handleDeleteThread,
5685
6085
  onUploadAttachment: attachments.uploadFile,
5686
6086
  onAddLinkAttachment: (url, name) => attachments.addLink({ url, name })
5687
6087
  }