@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.mjs CHANGED
@@ -1525,7 +1525,8 @@ function MentionTextarea({
1525
1525
  placeholder = "",
1526
1526
  rows = 2,
1527
1527
  className = "",
1528
- disabled = false
1528
+ disabled = false,
1529
+ autoFocus = false
1529
1530
  }) {
1530
1531
  const { service, features } = useTaskBoardContext();
1531
1532
  const textareaRef = useRef6(null);
@@ -1635,7 +1636,8 @@ function MentionTextarea({
1635
1636
  rows,
1636
1637
  className,
1637
1638
  placeholder,
1638
- disabled
1639
+ disabled,
1640
+ autoFocus
1639
1641
  }
1640
1642
  ),
1641
1643
  mentionQuery !== null && mentionUsers.length > 0 && /* @__PURE__ */ jsxs11("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: [
@@ -2427,7 +2429,7 @@ function TaskBoard({
2427
2429
  }
2428
2430
 
2429
2431
  // src/components/TaskDetailView.tsx
2430
- import { useCallback as useCallback11, useEffect as useEffect17, useMemo as useMemo3, useRef as useRef15, useState as useState19 } from "react";
2432
+ import { useCallback as useCallback11, useEffect as useEffect18, useMemo as useMemo3, useRef as useRef16, useState as useState19 } from "react";
2431
2433
 
2432
2434
  // src/utils/threads.ts
2433
2435
  function deriveThreads(comments, attachments) {
@@ -2459,7 +2461,8 @@ function deriveThreads(comments, attachments) {
2459
2461
  author_name: r.author_name,
2460
2462
  content: r.content,
2461
2463
  created_at: r.created_at,
2462
- is_internal: !!r.is_internal
2464
+ is_internal: !!r.is_internal,
2465
+ edited: !!r.edited
2463
2466
  }));
2464
2467
  const threadAttachments = (c.attachment_ids || []).map((aid) => attachById.get(aid)).filter((a) => Boolean(a));
2465
2468
  return {
@@ -2473,6 +2476,7 @@ function deriveThreads(comments, attachments) {
2473
2476
  is_internal: !!c.is_internal,
2474
2477
  status: c.thread_status === "complete" ? "complete" : "active",
2475
2478
  rawContent: c.content,
2479
+ edited: !!c.edited,
2476
2480
  anchor: c.anchor || null,
2477
2481
  attachments: threadAttachments,
2478
2482
  replies
@@ -2496,7 +2500,7 @@ function timeAgo(dateStr) {
2496
2500
  }
2497
2501
 
2498
2502
  // src/components/DescriptionSection.tsx
2499
- import { useState as useState10 } from "react";
2503
+ import { useEffect as useEffect10, useRef as useRef10, useState as useState10 } from "react";
2500
2504
 
2501
2505
  // src/components/MarkdownView.tsx
2502
2506
  import React10 from "react";
@@ -2921,10 +2925,13 @@ function DescriptionSection({
2921
2925
  onChange,
2922
2926
  status,
2923
2927
  onStatusChange,
2924
- saving
2928
+ saving,
2929
+ anchors,
2930
+ onAnchorClick
2925
2931
  }) {
2926
2932
  const [editing, setEditing] = useState10(false);
2927
2933
  const [openHelp, setOpenHelp] = useState10(null);
2934
+ const readRef = useRef10(null);
2928
2935
  const startEdit = () => setEditing(true);
2929
2936
  const cancel = () => setEditing(false);
2930
2937
  const handleCommit = (md) => {
@@ -2932,6 +2939,52 @@ function DescriptionSection({
2932
2939
  setEditing(false);
2933
2940
  };
2934
2941
  const hasContent = value.trim().length > 0;
2942
+ useEffect10(() => {
2943
+ if (editing) return;
2944
+ const root = readRef.current;
2945
+ if (!root) return;
2946
+ root.querySelectorAll("mark.eb-tb-annot").forEach((m) => {
2947
+ const text = m.textContent || "";
2948
+ m.replaceWith(document.createTextNode(text));
2949
+ });
2950
+ root.normalize();
2951
+ if (!anchors || anchors.length === 0) return;
2952
+ for (const anchor of anchors) {
2953
+ const snippet = anchor.snippet?.trim();
2954
+ if (!snippet) continue;
2955
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2956
+ let node = walker.nextNode();
2957
+ while (node) {
2958
+ const txt = node.textContent || "";
2959
+ const idx = txt.indexOf(snippet);
2960
+ if (idx !== -1) {
2961
+ const range = document.createRange();
2962
+ try {
2963
+ range.setStart(node, idx);
2964
+ range.setEnd(node, idx + snippet.length);
2965
+ const mark = document.createElement("mark");
2966
+ mark.className = "eb-tb-annot";
2967
+ mark.dataset.threadId = anchor.threadId;
2968
+ range.surroundContents(mark);
2969
+ } catch {
2970
+ }
2971
+ break;
2972
+ }
2973
+ node = walker.nextNode();
2974
+ }
2975
+ }
2976
+ }, [anchors, value, editing]);
2977
+ const handleReadClick = (e) => {
2978
+ const target = e.target;
2979
+ const mark = target?.closest("mark.eb-tb-annot");
2980
+ if (mark && mark.dataset.threadId && onAnchorClick) {
2981
+ e.preventDefault();
2982
+ e.stopPropagation();
2983
+ onAnchorClick(mark.dataset.threadId);
2984
+ return;
2985
+ }
2986
+ startEdit();
2987
+ };
2935
2988
  return /* @__PURE__ */ jsxs16("section", { className: "group", "data-section": sectionKey, children: [
2936
2989
  /* @__PURE__ */ jsxs16("div", { className: "flex items-center justify-between gap-3 mb-3", children: [
2937
2990
  /* @__PURE__ */ jsx18("h2", { className: "text-[15px] font-semibold text-neutral-900 tracking-tight", children: label }),
@@ -2985,11 +3038,19 @@ function DescriptionSection({
2985
3038
  saving
2986
3039
  }
2987
3040
  ) : /* @__PURE__ */ jsx18(
2988
- "button",
3041
+ "div",
2989
3042
  {
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",
3043
+ ref: readRef,
3044
+ role: "button",
3045
+ tabIndex: 0,
3046
+ onClick: handleReadClick,
3047
+ onKeyDown: (e) => {
3048
+ if (e.key === "Enter" || e.key === " ") {
3049
+ e.preventDefault();
3050
+ startEdit();
3051
+ }
3052
+ },
3053
+ 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",
2993
3054
  children: hasContent ? /* @__PURE__ */ jsx18(MarkdownView, { content: value, className: "text-[13px]" }) : /* @__PURE__ */ jsx18("p", { className: "text-[13px] text-neutral-400 italic", children: placeholder })
2994
3055
  }
2995
3056
  )
@@ -2997,7 +3058,7 @@ function DescriptionSection({
2997
3058
  }
2998
3059
 
2999
3060
  // src/components/OutstandingQuestionsSection.tsx
3000
- import { useEffect as useEffect10, useRef as useRef10, useState as useState11 } from "react";
3061
+ import { useEffect as useEffect11, useRef as useRef11, useState as useState11 } from "react";
3001
3062
  import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3002
3063
  function timeAgo2(dateStr) {
3003
3064
  const d = parseDate(dateStr);
@@ -3024,9 +3085,9 @@ function OutstandingQuestionsSection({
3024
3085
  const [adding, setAdding] = useState11(false);
3025
3086
  const [newText, setNewText] = useState11("");
3026
3087
  const [posting, setPosting] = useState11(false);
3027
- const filterRef = useRef10(null);
3028
- const newRef = useRef10(null);
3029
- useEffect10(() => {
3088
+ const filterRef = useRef11(null);
3089
+ const newRef = useRef11(null);
3090
+ useEffect11(() => {
3030
3091
  if (!filterMenuOpen) return;
3031
3092
  const onClick = (e) => {
3032
3093
  if (filterRef.current && !filterRef.current.contains(e.target)) {
@@ -3318,7 +3379,7 @@ function QuestionItem({
3318
3379
  }
3319
3380
 
3320
3381
  // src/components/AttachmentsSection.tsx
3321
- import { useEffect as useEffect11, useRef as useRef11, useState as useState12 } from "react";
3382
+ import { useEffect as useEffect12, useRef as useRef12, useState as useState12 } from "react";
3322
3383
  import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
3323
3384
  function formatBytes(bytes) {
3324
3385
  if (!bytes && bytes !== 0) return "";
@@ -3348,10 +3409,10 @@ function AttachmentsSection({
3348
3409
  const [linkTitle, setLinkTitle] = useState12("");
3349
3410
  const [busy, setBusy] = useState12(false);
3350
3411
  const [error, setError] = useState12("");
3351
- const fileInputRef = useRef11(null);
3352
- const imageInputRef = useRef11(null);
3353
- const menuRef = useRef11(null);
3354
- useEffect11(() => {
3412
+ const fileInputRef = useRef12(null);
3413
+ const imageInputRef = useRef12(null);
3414
+ const menuRef = useRef12(null);
3415
+ useEffect12(() => {
3355
3416
  if (!menuOpen) return;
3356
3417
  const onClick = (e) => {
3357
3418
  if (menuRef.current && !menuRef.current.contains(e.target)) {
@@ -3690,7 +3751,7 @@ function AttachmentsSection({
3690
3751
  }
3691
3752
 
3692
3753
  // src/components/ThreadsPanel.tsx
3693
- import { useEffect as useEffect13, useRef as useRef14, useState as useState15 } from "react";
3754
+ import { useEffect as useEffect14, useRef as useRef15, useState as useState15 } from "react";
3694
3755
 
3695
3756
  // src/components/ActivityList.tsx
3696
3757
  import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
@@ -3884,7 +3945,7 @@ function ThreadCard({ thread, onOpen, onAnchorClick, shimmer }) {
3884
3945
  }
3885
3946
 
3886
3947
  // src/components/ThreadComposer.tsx
3887
- import { useRef as useRef12, useState as useState13 } from "react";
3948
+ import { useRef as useRef13, useState as useState13 } from "react";
3888
3949
  import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
3889
3950
  function ThreadComposer({
3890
3951
  attachments,
@@ -3989,7 +4050,8 @@ function ThreadComposer({
3989
4050
  onChange: setBody,
3990
4051
  rows: 4,
3991
4052
  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"
4053
+ 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",
4054
+ autoFocus: true
3993
4055
  }
3994
4056
  ),
3995
4057
  /* @__PURE__ */ jsx24(
@@ -4049,8 +4111,8 @@ function ComposerAttachments({
4049
4111
  const [linkOpen, setLinkOpen] = useState13(false);
4050
4112
  const [linkUrl, setLinkUrl] = useState13("");
4051
4113
  const [linkName, setLinkName] = useState13("");
4052
- const imageRef = useRef12(null);
4053
- const fileRef = useRef12(null);
4114
+ const imageRef = useRef13(null);
4115
+ const fileRef = useRef13(null);
4054
4116
  const selected = attachmentIds.map((id) => attachments.find((a) => a.id === id)).filter((a) => Boolean(a));
4055
4117
  const upload = async (file) => {
4056
4118
  setBusy(true);
@@ -4215,13 +4277,17 @@ function ComposerAttachments({
4215
4277
  }
4216
4278
 
4217
4279
  // src/components/ThreadDetailView.tsx
4218
- import { useEffect as useEffect12, useRef as useRef13, useState as useState14 } from "react";
4280
+ import { useEffect as useEffect13, useRef as useRef14, useState as useState14 } from "react";
4219
4281
  import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
4220
4282
  function ThreadDetailView({
4221
4283
  thread,
4284
+ currentUsername,
4222
4285
  onBack,
4223
4286
  onReply,
4224
4287
  onUpdateThread,
4288
+ onEditMessage,
4289
+ onDeleteMessage,
4290
+ onDeleteThread,
4225
4291
  onAnchorClick,
4226
4292
  isInternalUser
4227
4293
  }) {
@@ -4231,10 +4297,29 @@ function ThreadDetailView({
4231
4297
  const [posting, setPosting] = useState14(false);
4232
4298
  const [editingTitle, setEditingTitle] = useState14(false);
4233
4299
  const [titleDraft, setTitleDraft] = useState14(thread.title);
4234
- const messagesEndRef = useRef13(null);
4235
- useEffect12(() => {
4300
+ const [editingMessageId, setEditingMessageId] = useState14(null);
4301
+ const [editDraft, setEditDraft] = useState14("");
4302
+ const [savingEdit, setSavingEdit] = useState14(false);
4303
+ const [headerMenuOpen, setHeaderMenuOpen] = useState14(false);
4304
+ const headerMenuRef = useRef14(null);
4305
+ const messagesEndRef = useRef14(null);
4306
+ useEffect13(() => {
4236
4307
  setTitleDraft(thread.title);
4237
4308
  }, [thread.title]);
4309
+ useEffect13(() => {
4310
+ setEditingMessageId(null);
4311
+ setEditDraft("");
4312
+ }, [thread.id]);
4313
+ useEffect13(() => {
4314
+ if (!headerMenuOpen) return;
4315
+ const onClick = (e) => {
4316
+ if (headerMenuRef.current && !headerMenuRef.current.contains(e.target)) {
4317
+ setHeaderMenuOpen(false);
4318
+ }
4319
+ };
4320
+ document.addEventListener("mousedown", onClick);
4321
+ return () => document.removeEventListener("mousedown", onClick);
4322
+ }, [headerMenuOpen]);
4238
4323
  const submit = async () => {
4239
4324
  if (!body.trim()) return;
4240
4325
  setPosting(true);
@@ -4262,7 +4347,40 @@ function ThreadDetailView({
4262
4347
  const next = thread.status === "complete" ? "active" : "complete";
4263
4348
  await onUpdateThread({ thread_status: next });
4264
4349
  };
4350
+ const startEditMessage = (id, content) => {
4351
+ setEditingMessageId(id);
4352
+ setEditDraft(content);
4353
+ };
4354
+ const cancelEditMessage = () => {
4355
+ setEditingMessageId(null);
4356
+ setEditDraft("");
4357
+ };
4358
+ const commitEditMessage = async () => {
4359
+ const id = editingMessageId;
4360
+ const next = editDraft.trim();
4361
+ if (!id || !next) {
4362
+ cancelEditMessage();
4363
+ return;
4364
+ }
4365
+ setSavingEdit(true);
4366
+ try {
4367
+ await onEditMessage(id, next);
4368
+ cancelEditMessage();
4369
+ } finally {
4370
+ setSavingEdit(false);
4371
+ }
4372
+ };
4373
+ const handleDeleteMessage = async (id, isThreadRoot) => {
4374
+ const msg = isThreadRoot ? "Delete this thread and all of its replies? This cannot be undone." : "Delete this message? This cannot be undone.";
4375
+ if (!confirm(msg)) return;
4376
+ if (isThreadRoot) {
4377
+ await onDeleteThread(id);
4378
+ } else {
4379
+ await onDeleteMessage(id);
4380
+ }
4381
+ };
4265
4382
  const isComplete = thread.status === "complete";
4383
+ const isThreadAuthor = thread.author_id === currentUsername;
4266
4384
  return /* @__PURE__ */ jsxs23(Fragment7, { children: [
4267
4385
  /* @__PURE__ */ jsxs23("div", { className: "shrink-0 bg-white px-4 py-3 border-b border-neutral-100", children: [
4268
4386
  /* @__PURE__ */ jsxs23("div", { className: "flex items-start justify-between gap-4", children: [
@@ -4277,21 +4395,71 @@ function ThreadDetailView({
4277
4395
  ]
4278
4396
  }
4279
4397
  ),
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"
4398
+ /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-1.5 shrink-0", children: [
4399
+ /* @__PURE__ */ jsx25(
4400
+ "button",
4401
+ {
4402
+ type: "button",
4403
+ onClick: toggleStatus,
4404
+ 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",
4405
+ children: isComplete ? /* @__PURE__ */ jsxs23(Fragment7, { children: [
4406
+ /* @__PURE__ */ jsx25(RotateCcwIcon, { size: 12, strokeWidth: 1.5 }),
4407
+ "Reopen"
4408
+ ] }) : /* @__PURE__ */ jsxs23(Fragment7, { children: [
4409
+ /* @__PURE__ */ jsx25(CheckCircle2Icon, { size: 12, strokeWidth: 1.5 }),
4410
+ "Mark complete"
4411
+ ] })
4412
+ }
4413
+ ),
4414
+ /* @__PURE__ */ jsxs23("div", { ref: headerMenuRef, className: "relative", children: [
4415
+ /* @__PURE__ */ jsx25(
4416
+ "button",
4417
+ {
4418
+ type: "button",
4419
+ onClick: () => setHeaderMenuOpen((v) => !v),
4420
+ "aria-label": "Thread actions",
4421
+ 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",
4422
+ children: /* @__PURE__ */ jsx25(MoreVerticalIcon, { size: 14, strokeWidth: 1.5 })
4423
+ }
4424
+ ),
4425
+ headerMenuOpen && /* @__PURE__ */ jsxs23("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: [
4426
+ isThreadAuthor && /* @__PURE__ */ jsxs23(
4427
+ "button",
4428
+ {
4429
+ type: "button",
4430
+ onClick: () => {
4431
+ setHeaderMenuOpen(false);
4432
+ setEditingTitle(true);
4433
+ },
4434
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2.5",
4435
+ children: [
4436
+ /* @__PURE__ */ jsx25(PencilIcon, { size: 14, strokeWidth: 1.5 }),
4437
+ "Edit title"
4438
+ ]
4439
+ }
4440
+ ),
4441
+ /* @__PURE__ */ jsxs23(
4442
+ "button",
4443
+ {
4444
+ type: "button",
4445
+ onClick: async () => {
4446
+ setHeaderMenuOpen(false);
4447
+ if (!confirm(
4448
+ "Delete this thread and all of its replies? This cannot be undone."
4449
+ ))
4450
+ return;
4451
+ await onDeleteThread(thread.id);
4452
+ },
4453
+ className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2.5",
4454
+ children: [
4455
+ /* @__PURE__ */ jsx25(TrashIcon, { size: 14, strokeWidth: 1.5 }),
4456
+ "Delete thread"
4457
+ ]
4458
+ }
4459
+ )
4292
4460
  ] })
4293
- }
4294
- )
4461
+ ] })
4462
+ ] })
4295
4463
  ] }),
4296
4464
  editingTitle ? /* @__PURE__ */ jsx25(
4297
4465
  "input",
@@ -4333,27 +4501,60 @@ function ThreadDetailView({
4333
4501
  ) })
4334
4502
  ] }),
4335
4503
  /* @__PURE__ */ jsxs23("div", { className: "flex-1 overflow-y-auto p-3 flex flex-col gap-4", children: [
4336
- /* @__PURE__ */ jsx25(
4504
+ editingMessageId === thread.id ? /* @__PURE__ */ jsx25(
4505
+ ThreadMessageEditor,
4506
+ {
4507
+ authorName: thread.author_name,
4508
+ isInternal: thread.is_internal,
4509
+ value: editDraft,
4510
+ onChange: setEditDraft,
4511
+ onCommit: commitEditMessage,
4512
+ onCancel: cancelEditMessage,
4513
+ saving: savingEdit
4514
+ }
4515
+ ) : /* @__PURE__ */ jsx25(
4337
4516
  ThreadMessage,
4338
4517
  {
4339
4518
  authorName: thread.author_name,
4340
4519
  createdAt: thread.created_at,
4341
4520
  content: thread.rawContent,
4342
4521
  isInternal: thread.is_internal,
4343
- internalLabel
4522
+ edited: thread.edited,
4523
+ isOwn: isThreadAuthor,
4524
+ internalLabel,
4525
+ onEdit: () => startEditMessage(thread.id, thread.rawContent),
4526
+ onDelete: () => handleDeleteMessage(thread.id, true)
4344
4527
  }
4345
4528
  ),
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
- )),
4529
+ thread.replies.map(
4530
+ (r) => editingMessageId === r.id ? /* @__PURE__ */ jsx25(
4531
+ ThreadMessageEditor,
4532
+ {
4533
+ authorName: r.author_name,
4534
+ isInternal: r.is_internal,
4535
+ value: editDraft,
4536
+ onChange: setEditDraft,
4537
+ onCommit: commitEditMessage,
4538
+ onCancel: cancelEditMessage,
4539
+ saving: savingEdit
4540
+ },
4541
+ r.id
4542
+ ) : /* @__PURE__ */ jsx25(
4543
+ ThreadMessage,
4544
+ {
4545
+ authorName: r.author_name,
4546
+ createdAt: r.created_at,
4547
+ content: r.content,
4548
+ isInternal: r.is_internal,
4549
+ edited: r.edited,
4550
+ isOwn: r.author_id === currentUsername,
4551
+ internalLabel,
4552
+ onEdit: () => startEditMessage(r.id, r.content),
4553
+ onDelete: () => handleDeleteMessage(r.id, false)
4554
+ },
4555
+ r.id
4556
+ )
4557
+ ),
4357
4558
  /* @__PURE__ */ jsx25("div", { ref: messagesEndRef })
4358
4559
  ] }),
4359
4560
  /* @__PURE__ */ jsxs23("div", { className: "shrink-0 p-3 border-t border-neutral-100 bg-white", children: [
@@ -4399,10 +4600,27 @@ function ThreadMessage({
4399
4600
  createdAt,
4400
4601
  content,
4401
4602
  isInternal,
4402
- internalLabel
4603
+ edited = false,
4604
+ isOwn = false,
4605
+ internalLabel,
4606
+ onEdit,
4607
+ onDelete
4403
4608
  }) {
4404
4609
  const initials = getInitials(authorName || "?");
4405
- return /* @__PURE__ */ jsxs23("div", { className: "flex gap-2.5", children: [
4610
+ const [menuOpen, setMenuOpen] = useState14(false);
4611
+ const menuRef = useRef14(null);
4612
+ useEffect13(() => {
4613
+ if (!menuOpen) return;
4614
+ const onClick = (e) => {
4615
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
4616
+ setMenuOpen(false);
4617
+ }
4618
+ };
4619
+ document.addEventListener("mousedown", onClick);
4620
+ return () => document.removeEventListener("mousedown", onClick);
4621
+ }, [menuOpen]);
4622
+ const showActions = isOwn && (onEdit || onDelete);
4623
+ return /* @__PURE__ */ jsxs23("div", { className: "group flex gap-2.5", children: [
4406
4624
  /* @__PURE__ */ jsx25(
4407
4625
  "div",
4408
4626
  {
@@ -4414,15 +4632,130 @@ function ThreadMessage({
4414
4632
  /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2 mb-0.5 flex-wrap", children: [
4415
4633
  /* @__PURE__ */ jsx25("span", { className: "text-[13px] font-medium text-neutral-900", children: authorName }),
4416
4634
  /* @__PURE__ */ jsx25("span", { className: "text-[10px] text-neutral-400", children: timeAgo(createdAt) }),
4635
+ edited && /* @__PURE__ */ jsx25("span", { className: "text-[10px] text-neutral-400 italic", children: "(edited)" }),
4417
4636
  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
4637
  /* @__PURE__ */ jsx25(LockIcon, { size: 10, strokeWidth: 1.5 }),
4419
4638
  internalLabel
4420
- ] })
4639
+ ] }),
4640
+ showActions && /* @__PURE__ */ jsxs23(
4641
+ "div",
4642
+ {
4643
+ ref: menuRef,
4644
+ className: "ml-auto relative opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity",
4645
+ children: [
4646
+ /* @__PURE__ */ jsx25(
4647
+ "button",
4648
+ {
4649
+ type: "button",
4650
+ onClick: () => setMenuOpen((v) => !v),
4651
+ "aria-label": "Message actions",
4652
+ className: "w-6 h-6 flex items-center justify-center rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100",
4653
+ children: /* @__PURE__ */ jsx25(MoreVerticalIcon, { size: 12, strokeWidth: 1.5 })
4654
+ }
4655
+ ),
4656
+ menuOpen && /* @__PURE__ */ jsxs23("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: [
4657
+ onEdit && /* @__PURE__ */ jsxs23(
4658
+ "button",
4659
+ {
4660
+ type: "button",
4661
+ onClick: () => {
4662
+ setMenuOpen(false);
4663
+ onEdit();
4664
+ },
4665
+ className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2",
4666
+ children: [
4667
+ /* @__PURE__ */ jsx25(PencilIcon, { size: 12, strokeWidth: 1.5 }),
4668
+ "Edit"
4669
+ ]
4670
+ }
4671
+ ),
4672
+ onDelete && /* @__PURE__ */ jsxs23(
4673
+ "button",
4674
+ {
4675
+ type: "button",
4676
+ onClick: () => {
4677
+ setMenuOpen(false);
4678
+ onDelete();
4679
+ },
4680
+ className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2",
4681
+ children: [
4682
+ /* @__PURE__ */ jsx25(TrashIcon, { size: 12, strokeWidth: 1.5 }),
4683
+ "Delete"
4684
+ ]
4685
+ }
4686
+ )
4687
+ ] })
4688
+ ]
4689
+ }
4690
+ )
4421
4691
  ] }),
4422
4692
  /* @__PURE__ */ jsx25("div", { className: "text-[12px] text-neutral-700 leading-relaxed whitespace-pre-wrap", children: /* @__PURE__ */ jsx25(MentionText, { text: content }) })
4423
4693
  ] })
4424
4694
  ] });
4425
4695
  }
4696
+ function ThreadMessageEditor({
4697
+ authorName,
4698
+ isInternal,
4699
+ value,
4700
+ onChange,
4701
+ onCommit,
4702
+ onCancel,
4703
+ saving
4704
+ }) {
4705
+ const initials = getInitials(authorName || "?");
4706
+ return /* @__PURE__ */ jsxs23("div", { className: "flex gap-2.5", children: [
4707
+ /* @__PURE__ */ jsx25(
4708
+ "div",
4709
+ {
4710
+ 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"}`,
4711
+ children: initials
4712
+ }
4713
+ ),
4714
+ /* @__PURE__ */ jsxs23("div", { className: "flex-1 min-w-0 flex flex-col gap-2", children: [
4715
+ /* @__PURE__ */ jsx25(
4716
+ MentionTextarea,
4717
+ {
4718
+ value,
4719
+ onChange,
4720
+ onKeyDown: (e) => {
4721
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
4722
+ e.preventDefault();
4723
+ onCommit();
4724
+ } else if (e.key === "Escape") {
4725
+ e.preventDefault();
4726
+ onCancel();
4727
+ }
4728
+ },
4729
+ rows: 3,
4730
+ placeholder: "Edit message\u2026",
4731
+ 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",
4732
+ autoFocus: true
4733
+ }
4734
+ ),
4735
+ /* @__PURE__ */ jsxs23("div", { className: "flex items-center justify-end gap-2", children: [
4736
+ /* @__PURE__ */ jsx25(
4737
+ "button",
4738
+ {
4739
+ type: "button",
4740
+ onClick: onCancel,
4741
+ className: "text-[12px] font-medium text-neutral-500 hover:text-neutral-900 px-2 h-7 rounded hover:bg-neutral-50",
4742
+ children: "Cancel"
4743
+ }
4744
+ ),
4745
+ /* @__PURE__ */ jsx25(
4746
+ "button",
4747
+ {
4748
+ type: "button",
4749
+ onClick: onCommit,
4750
+ disabled: saving || !value.trim(),
4751
+ 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",
4752
+ children: saving ? "Saving..." : "Save"
4753
+ }
4754
+ )
4755
+ ] })
4756
+ ] })
4757
+ ] });
4758
+ }
4426
4759
 
4427
4760
  // src/components/ThreadsPanel.tsx
4428
4761
  import { Fragment as Fragment8, jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
@@ -4430,6 +4763,7 @@ function ThreadsPanel({
4430
4763
  threads,
4431
4764
  activity,
4432
4765
  attachments,
4766
+ currentUsername,
4433
4767
  open,
4434
4768
  onToggle,
4435
4769
  openThreadId,
@@ -4442,6 +4776,9 @@ function ThreadsPanel({
4442
4776
  onCreateThread,
4443
4777
  onCreateReply,
4444
4778
  onUpdateThread,
4779
+ onEditMessage,
4780
+ onDeleteMessage,
4781
+ onDeleteThread,
4445
4782
  onUploadAttachment,
4446
4783
  onAddLinkAttachment
4447
4784
  }) {
@@ -4449,15 +4786,15 @@ function ThreadsPanel({
4449
4786
  const [composerOpen, setComposerOpen] = useState15(false);
4450
4787
  const [filter, setFilter] = useState15("active");
4451
4788
  const [filterMenuOpen, setFilterMenuOpen] = useState15(false);
4452
- const filterRef = useRef14(null);
4453
- useEffect13(() => {
4789
+ const filterRef = useRef15(null);
4790
+ useEffect14(() => {
4454
4791
  if (pendingAnchor) {
4455
4792
  setComposerOpen(true);
4456
4793
  setTab("threads");
4457
4794
  onOpenThread(null);
4458
4795
  }
4459
4796
  }, [pendingAnchor, onOpenThread]);
4460
- useEffect13(() => {
4797
+ useEffect14(() => {
4461
4798
  if (!filterMenuOpen) return;
4462
4799
  const onClick = (e) => {
4463
4800
  if (filterRef.current && !filterRef.current.contains(e.target)) {
@@ -4469,7 +4806,7 @@ function ThreadsPanel({
4469
4806
  }, [filterMenuOpen]);
4470
4807
  const filteredThreads = threads.filter((t) => t.status === filter);
4471
4808
  const openThread = openThreadId ? threads.find((t) => t.id === openThreadId) : null;
4472
- useEffect13(() => {
4809
+ useEffect14(() => {
4473
4810
  if (openThreadId && !threads.some((t) => t.id === openThreadId)) {
4474
4811
  onOpenThread(null);
4475
4812
  }
@@ -4617,9 +4954,13 @@ function ThreadsPanel({
4617
4954
  ThreadDetailView,
4618
4955
  {
4619
4956
  thread: openThread,
4957
+ currentUsername,
4620
4958
  onBack: () => onOpenThread(null),
4621
4959
  onReply: (content, isInternal) => onCreateReply(openThread.id, content, isInternal),
4622
4960
  onUpdateThread: (body) => onUpdateThread(openThread.id, body),
4961
+ onEditMessage,
4962
+ onDeleteMessage,
4963
+ onDeleteThread,
4623
4964
  onAnchorClick,
4624
4965
  isInternalUser
4625
4966
  }
@@ -4661,7 +5002,7 @@ function HighlightBubble({ bubble, onComment }) {
4661
5002
  }
4662
5003
 
4663
5004
  // src/hooks/useTaskQuestions.ts
4664
- import { useCallback as useCallback8, useEffect as useEffect14, useState as useState16 } from "react";
5005
+ import { useCallback as useCallback8, useEffect as useEffect15, useState as useState16 } from "react";
4665
5006
  function useTaskQuestions(taskId, initial) {
4666
5007
  const { service } = useTaskBoardContext();
4667
5008
  const [questions, setQuestions] = useState16(initial ?? []);
@@ -4676,7 +5017,7 @@ function useTaskQuestions(taskId, initial) {
4676
5017
  setLoading(false);
4677
5018
  }
4678
5019
  }, [service, taskId]);
4679
- useEffect14(() => {
5020
+ useEffect15(() => {
4680
5021
  if (initial !== void 0) return;
4681
5022
  if (!taskId) return;
4682
5023
  refresh();
@@ -4744,7 +5085,7 @@ function useTaskQuestions(taskId, initial) {
4744
5085
  }
4745
5086
 
4746
5087
  // src/hooks/useTaskAttachments.ts
4747
- import { useCallback as useCallback9, useEffect as useEffect15, useState as useState17 } from "react";
5088
+ import { useCallback as useCallback9, useEffect as useEffect16, useState as useState17 } from "react";
4748
5089
  function useTaskAttachments(taskId, initial) {
4749
5090
  const { service } = useTaskBoardContext();
4750
5091
  const [attachments, setAttachments] = useState17(initial ?? []);
@@ -4759,7 +5100,7 @@ function useTaskAttachments(taskId, initial) {
4759
5100
  setLoading(false);
4760
5101
  }
4761
5102
  }, [service, taskId]);
4762
- useEffect15(() => {
5103
+ useEffect16(() => {
4763
5104
  if (initial !== void 0) return;
4764
5105
  if (!taskId) return;
4765
5106
  refresh();
@@ -4794,11 +5135,11 @@ function useTaskAttachments(taskId, initial) {
4794
5135
  }
4795
5136
 
4796
5137
  // src/hooks/useHighlightAnchor.ts
4797
- import { useCallback as useCallback10, useEffect as useEffect16, useState as useState18 } from "react";
5138
+ import { useCallback as useCallback10, useEffect as useEffect17, useState as useState18 } from "react";
4798
5139
  function useHighlightAnchor() {
4799
5140
  const [bubble, setBubble] = useState18(null);
4800
5141
  const [pendingAnchor, setPendingAnchor] = useState18(null);
4801
- useEffect16(() => {
5142
+ useEffect17(() => {
4802
5143
  const onMouseUp = () => {
4803
5144
  const sel = window.getSelection();
4804
5145
  if (!sel || sel.rangeCount === 0 || sel.isCollapsed) {
@@ -4856,13 +5197,21 @@ function useHighlightAnchor() {
4856
5197
  return anchor;
4857
5198
  }, [bubble]);
4858
5199
  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);
5200
+ const sectionEl = document.querySelector(`[data-section="${anchor.section}"]`);
5201
+ if (!sectionEl) return;
5202
+ sectionEl.scrollIntoView({ behavior: "smooth", block: "center" });
5203
+ const markEl = Array.from(sectionEl.querySelectorAll("mark.eb-tb-annot")).find(
5204
+ (m) => (m.textContent || "").includes(anchor.snippet)
5205
+ ) || sectionEl.querySelector("mark.eb-tb-annot");
5206
+ if (markEl) {
5207
+ markEl.classList.add("eb-tb-annot-pulse");
5208
+ setTimeout(() => markEl.classList.remove("eb-tb-annot-pulse"), 1500);
5209
+ } else {
5210
+ sectionEl.classList.add("ring-2", "ring-amber-300", "rounded-lg", "transition-all");
5211
+ setTimeout(() => {
5212
+ sectionEl.classList.remove("ring-2", "ring-amber-300", "rounded-lg");
5213
+ }, 1500);
5214
+ }
4866
5215
  }, []);
4867
5216
  return {
4868
5217
  bubble,
@@ -4908,10 +5257,10 @@ function TaskDetailView({
4908
5257
  const [pendingTags, setPendingTags] = useState19([]);
4909
5258
  const [showOtherTagInput, setShowOtherTagInput] = useState19(false);
4910
5259
  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);
5260
+ const statusRef = useRef16(null);
5261
+ const priorityRef = useRef16(null);
5262
+ const moreRef = useRef16(null);
5263
+ const tagsRef = useRef16(null);
4915
5264
  const [projectTaskIds, setProjectTaskIds] = useState19([]);
4916
5265
  const [openThreadId, setOpenThreadId] = useState19(null);
4917
5266
  const [shimmeringThreadIds, setShimmeringThreadIds] = useState19(/* @__PURE__ */ new Set());
@@ -4955,10 +5304,10 @@ function TaskDetailView({
4955
5304
  setLoading(false);
4956
5305
  }
4957
5306
  }, [service, taskId]);
4958
- useEffect17(() => {
5307
+ useEffect18(() => {
4959
5308
  fetchTask();
4960
5309
  }, [fetchTask]);
4961
- useEffect17(() => {
5310
+ useEffect18(() => {
4962
5311
  if (!task || !onNavigateToTask) return;
4963
5312
  let cancelled = false;
4964
5313
  (async () => {
@@ -4980,7 +5329,7 @@ function TaskDetailView({
4980
5329
  cancelled = true;
4981
5330
  };
4982
5331
  }, [task, columns, service, onNavigateToTask]);
4983
- useEffect17(() => {
5332
+ useEffect18(() => {
4984
5333
  const onClick = (e) => {
4985
5334
  const target = e.target;
4986
5335
  if (statusOpen && statusRef.current && !statusRef.current.contains(target)) setStatusOpen(false);
@@ -4994,7 +5343,7 @@ function TaskDetailView({
4994
5343
  const questions = useTaskQuestions(taskId, initialQuestions);
4995
5344
  const attachments = useTaskAttachments(taskId, initialAttachments);
4996
5345
  const highlight = useHighlightAnchor();
4997
- useEffect17(() => {
5346
+ useEffect18(() => {
4998
5347
  if (highlight.pendingAnchor && !panelOpen) {
4999
5348
  setPanelOpen(true);
5000
5349
  try {
@@ -5118,6 +5467,28 @@ function TaskDetailView({
5118
5467
  } catch {
5119
5468
  }
5120
5469
  };
5470
+ const handleEditMessage = async (messageId, content) => {
5471
+ try {
5472
+ await service.editComment(taskId, messageId, { content });
5473
+ await fetchTask();
5474
+ } catch {
5475
+ }
5476
+ };
5477
+ const handleDeleteMessage = async (messageId) => {
5478
+ try {
5479
+ await service.deleteComment(taskId, messageId);
5480
+ await fetchTask();
5481
+ } catch {
5482
+ }
5483
+ };
5484
+ const handleDeleteThread = async (threadId) => {
5485
+ try {
5486
+ await service.deleteComment(taskId, threadId);
5487
+ setOpenThreadId(null);
5488
+ await fetchTask();
5489
+ } catch {
5490
+ }
5491
+ };
5121
5492
  const project = useMemo3(() => {
5122
5493
  if (!task) return null;
5123
5494
  return projects.find((p) => p.slug === task.project_slug) ?? null;
@@ -5126,6 +5497,29 @@ function TaskDetailView({
5126
5497
  () => deriveThreads(comments, attachments.attachments),
5127
5498
  [comments, attachments.attachments]
5128
5499
  );
5500
+ const anchorsBySection = useMemo3(() => {
5501
+ const map = {};
5502
+ for (const t of threads) {
5503
+ if (!t.anchor) continue;
5504
+ const key = t.anchor.section;
5505
+ if (!map[key]) map[key] = [];
5506
+ map[key].push({ snippet: t.anchor.snippet, threadId: t.id });
5507
+ }
5508
+ return map;
5509
+ }, [threads]);
5510
+ const openThreadFromAnchor = useCallback11(
5511
+ (threadId) => {
5512
+ setOpenThreadId(threadId);
5513
+ if (!panelOpen) {
5514
+ setPanelOpen(true);
5515
+ try {
5516
+ window.localStorage.setItem(PANEL_OPEN_KEY, "true");
5517
+ } catch {
5518
+ }
5519
+ }
5520
+ },
5521
+ [panelOpen]
5522
+ );
5129
5523
  const statusCol = columns.find((c) => c.key === taskStatus);
5130
5524
  const priorityStyle = getPriorityStyle(priority);
5131
5525
  const currentIdx = task ? projectTaskIds.indexOf(task.id) : -1;
@@ -5501,7 +5895,9 @@ function TaskDetailView({
5501
5895
  onChange: (v) => handleSectionChange(section.key, v),
5502
5896
  status,
5503
5897
  onStatusChange: (s) => handleSectionStatusChange(section.key, s),
5504
- saving
5898
+ saving,
5899
+ anchors: anchorsBySection[section.key],
5900
+ onAnchorClick: openThreadFromAnchor
5505
5901
  },
5506
5902
  section.key
5507
5903
  );
@@ -5536,6 +5932,7 @@ function TaskDetailView({
5536
5932
  threads,
5537
5933
  activity,
5538
5934
  attachments: attachments.attachments,
5935
+ currentUsername: user.username,
5539
5936
  open: panelOpen,
5540
5937
  onToggle: togglePanel,
5541
5938
  openThreadId,
@@ -5548,6 +5945,9 @@ function TaskDetailView({
5548
5945
  onCreateThread: handleCreateThread,
5549
5946
  onCreateReply: handleCreateReply,
5550
5947
  onUpdateThread: handleUpdateThread,
5948
+ onEditMessage: handleEditMessage,
5949
+ onDeleteMessage: handleDeleteMessage,
5950
+ onDeleteThread: handleDeleteThread,
5551
5951
  onUploadAttachment: attachments.uploadFile,
5552
5952
  onAddLinkAttachment: (url, name) => attachments.addLink({ url, name })
5553
5953
  }