@filipc77/cowrite 0.6.3 → 0.6.5

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/ui/client.js CHANGED
@@ -14,13 +14,11 @@ const popupSelection = $("#popupSelection");
14
14
  const commentInput = $("#commentInput");
15
15
  const selectionToolbar = $("#selectionToolbar");
16
16
  const commentTrigger = $("#commentTrigger");
17
- const highlightPopover = $("#highlightPopover");
18
- const highlightPopoverText = $("#highlightPopoverText");
19
- const highlightEditBtn = $("#highlightEditBtn");
20
17
  const undoBtn = $("#undoBtn");
21
18
  const filePicker = $("#filePicker");
22
19
  const fileList = $("#fileList");
23
20
  const fileCommentBtn = $("#fileCommentBtn");
21
+ const formatButtons = $("#formatButtons");
24
22
 
25
23
  /** @type {Comment[]} */
26
24
  let comments = [];
@@ -33,7 +31,6 @@ let currentBlocks = [];
33
31
  let insertBtn = null;
34
32
  let insertLine = null;
35
33
  let activeGapIndex = -1;
36
- let activeHighlightCommentId = null;
37
34
  let undoStack = [];
38
35
  const MAX_UNDO = 50;
39
36
 
@@ -46,6 +43,43 @@ let pendingFileUpdate = null;
46
43
  let pendingEditAfterInsert = -1;
47
44
  let contentEditableActive = false;
48
45
 
46
+ // --- Resizable Sidebar ---
47
+ (function initResizableSidebar() {
48
+ const handle = document.getElementById("sidebarDragHandle");
49
+ const sidebar = document.getElementById("sidebar");
50
+ if (!handle || !sidebar) return;
51
+
52
+ // Restore saved width
53
+ const saved = localStorage.getItem("cowrite-sidebar-width");
54
+ if (saved) document.documentElement.style.setProperty("--sidebar-width", saved + "px");
55
+
56
+ let startX = 0;
57
+ let startWidth = 0;
58
+
59
+ handle.addEventListener("mousedown", (e) => {
60
+ e.preventDefault();
61
+ startX = e.clientX;
62
+ startWidth = sidebar.offsetWidth;
63
+ document.body.classList.add("sidebar-resizing");
64
+ document.addEventListener("mousemove", onMouseMove);
65
+ document.addEventListener("mouseup", onMouseUp);
66
+ });
67
+
68
+ function onMouseMove(e) {
69
+ const delta = startX - e.clientX; // sidebar is on the right
70
+ const newWidth = Math.min(Math.max(startWidth + delta, 300), window.innerWidth * 0.5);
71
+ document.documentElement.style.setProperty("--sidebar-width", newWidth + "px");
72
+ }
73
+
74
+ function onMouseUp() {
75
+ document.body.classList.remove("sidebar-resizing");
76
+ document.removeEventListener("mousemove", onMouseMove);
77
+ document.removeEventListener("mouseup", onMouseUp);
78
+ const width = sidebar.offsetWidth;
79
+ localStorage.setItem("cowrite-sidebar-width", String(width));
80
+ }
81
+ })();
82
+
49
83
  const BLOCK_TYPES = [
50
84
  { id: "text", label: "Text", category: "Basic blocks", icon: "Aa", template: "\u200B" },
51
85
  { id: "h1", label: "Heading 1", category: "Basic blocks", icon: "H1", template: "# " },
@@ -90,15 +124,36 @@ function switchFile(file) {
90
124
  history.replaceState(null, "", url.toString());
91
125
  }
92
126
 
127
+ // Track meta key for Cmd+Click to open in new tab
128
+ let lastClickHadMeta = false;
129
+ document.addEventListener("mousedown", (e) => { lastClickHadMeta = e.metaKey || e.ctrlKey; });
130
+
131
+ function openFileInNewTab(file) {
132
+ const url = new URL(location.href);
133
+ url.searchParams.set("file", file);
134
+ window.open(url.toString(), "_blank");
135
+ }
136
+
93
137
  filePicker.addEventListener("change", () => {
94
138
  const file = filePicker.value.trim();
95
- if (file) switchFile(file);
139
+ if (!file) return;
140
+ if (lastClickHadMeta) {
141
+ openFileInNewTab(file);
142
+ filePicker.value = "";
143
+ } else {
144
+ switchFile(file);
145
+ }
96
146
  });
97
147
 
98
148
  filePicker.addEventListener("keydown", (e) => {
99
- if (e.key === "Enter") {
100
- const file = filePicker.value.trim();
101
- if (file) switchFile(file);
149
+ const file = filePicker.value.trim();
150
+ if (!file) return;
151
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
152
+ e.preventDefault();
153
+ openFileInNewTab(file);
154
+ filePicker.value = "";
155
+ } else if (e.key === "Enter") {
156
+ switchFile(file);
102
157
  }
103
158
  });
104
159
 
@@ -236,6 +291,9 @@ document.addEventListener("mouseup", (e) => {
236
291
 
237
292
  selectionInfo = { offset, length: text.length, selectedText: text };
238
293
 
294
+ const isMarkdown = /\.(md|mdx)$/i.test(currentFile);
295
+ formatButtons.hidden = !isMarkdown;
296
+
239
297
  const range = selection.getRangeAt(0);
240
298
  const rect = range.getBoundingClientRect();
241
299
  selectionToolbar.style.left = `${rect.left + rect.width / 2 - 40}px`;
@@ -370,6 +428,88 @@ function hideTrigger() {
370
428
  selectionInfo = null;
371
429
  }
372
430
 
431
+ // --- Rich text formatting (markdown files only) ---
432
+
433
+ const FORMAT_SYNTAX = {
434
+ bold: { prefix: "**", suffix: "**" },
435
+ italic: { prefix: "*", suffix: "*" },
436
+ strikethrough: { prefix: "~~", suffix: "~~" },
437
+ code: { prefix: "`", suffix: "`" },
438
+ };
439
+
440
+ function applyMarkdownFormat(format) {
441
+ if (!selectionInfo || selectionInfo.offset < 0) return;
442
+ const { offset, length, selectedText } = selectionInfo;
443
+
444
+ // Exit contenteditable mode first so file_update from our edit applies normally
445
+ if (contentEditableActive && editingBlockEl) {
446
+ cancelContentEditable(editingBlockEl);
447
+ }
448
+
449
+ pushUndo();
450
+
451
+ if (format === "link") {
452
+ const url = prompt("Link URL:", "https://");
453
+ if (!url) return;
454
+ send({ type: "edit_apply", offset, length, newText: `[${selectedText}](${url})` });
455
+ hideTrigger();
456
+ return;
457
+ }
458
+
459
+ if (format === "blockquote") {
460
+ const lines = selectedText.split("\n").map(l => `> ${l}`).join("\n");
461
+ send({ type: "edit_apply", offset, length, newText: lines });
462
+ hideTrigger();
463
+ return;
464
+ }
465
+
466
+ if (format === "bulletList") {
467
+ const lines = selectedText.split("\n").map(l => `- ${l}`).join("\n");
468
+ send({ type: "edit_apply", offset, length, newText: lines });
469
+ hideTrigger();
470
+ return;
471
+ }
472
+
473
+ const { prefix, suffix } = FORMAT_SYNTAX[format];
474
+
475
+ // Toggle detection: check if selection is already wrapped
476
+ const before = currentContent.slice(offset - prefix.length, offset);
477
+ const after = currentContent.slice(offset + length, offset + length + suffix.length);
478
+ if (before === prefix && after === suffix) {
479
+ // Unwrap: remove the surrounding markers
480
+ send({ type: "edit_apply", offset: offset - prefix.length, length: length + prefix.length + suffix.length, newText: selectedText });
481
+ } else {
482
+ // Wrap: add markers
483
+ send({ type: "edit_apply", offset, length, newText: `${prefix}${selectedText}${suffix}` });
484
+ }
485
+
486
+ hideTrigger();
487
+ }
488
+
489
+ selectionToolbar.addEventListener("click", (e) => {
490
+ const btn = e.target.closest("[data-format]");
491
+ if (!btn) return;
492
+ applyMarkdownFormat(btn.dataset.format);
493
+ });
494
+
495
+ document.addEventListener("keydown", (e) => {
496
+ if (!selectionInfo || selectionInfo.offset < 0) return;
497
+ if (!/\.(md|mdx)$/i.test(currentFile)) return;
498
+ if (e.target.closest("input, textarea, [contenteditable]")) return;
499
+
500
+ const mod = e.metaKey || e.ctrlKey;
501
+ if (!mod) return;
502
+
503
+ const map = { b: "bold", i: "italic", e: "code", k: "link" };
504
+ if (e.shiftKey && e.key.toLowerCase() === "s") {
505
+ e.preventDefault();
506
+ applyMarkdownFormat("strikethrough");
507
+ } else if (map[e.key]) {
508
+ e.preventDefault();
509
+ applyMarkdownFormat(map[e.key]);
510
+ }
511
+ });
512
+
373
513
  function hidePopup() {
374
514
  popup.hidden = true;
375
515
  selectionInfo = null;
@@ -440,48 +580,99 @@ function renderComments() {
440
580
  return;
441
581
  }
442
582
 
443
- commentListEl.innerHTML = comments.map((c) => `
444
- <div class="comment-card ${c.status}" data-id="${c.id}">
445
- <button class="comment-delete-btn" onclick="deleteComment('${c.id}')" title="Delete comment">
446
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
447
- </button>
448
- ${c.selectedText
449
- ? `<div class="comment-selected-text">${escapeHtml(c.selectedText)}</div>`
450
- : `<div class="comment-file-badge">Whole file</div>`
451
- }
452
- <div class="comment-text">${escapeHtml(c.comment)}</div>
453
- ${c.replies.length > 0 ? `
454
- <div class="comment-replies">
455
- ${c.replies.map((r) => `
456
- <div class="reply ${r.from}">
457
- <div class="reply-from ${r.from}">${r.from}</div>
458
- <div>${escapeHtml(r.text)}</div>
583
+ commentListEl.innerHTML = comments.map((c) => {
584
+ const repliesHtml = c.replies.length > 0 ? `
585
+ <div class="comment-replies">
586
+ ${c.replies.map((r) => r.proposal ? `
587
+ <div class="reply agent proposal-reply proposal-${r.proposal.status}">
588
+ <div class="reply-from agent">agent — proposal</div>
589
+ <div class="proposal-explanation">${escapeHtml(r.proposal.explanation)}</div>
590
+ ${r.proposal.status === "pending" ? `
591
+ <div class="proposal-diff">
592
+ <div class="proposal-old"><span class="proposal-label">Current</span><pre>${escapeHtml(r.proposal.oldText)}</pre></div>
593
+ <div class="proposal-new"><span class="proposal-label">Proposed</span><pre>${escapeHtml(r.proposal.newText)}</pre></div>
594
+ </div>
595
+ <div class="proposal-actions">
596
+ <button class="proposal-apply-btn" onclick="applyProposal('${c.id}', '${r.id}')">Apply</button>
597
+ <button class="proposal-reject-btn" onclick="rejectProposal('${c.id}', '${r.id}')">Reject</button>
598
+ </div>
599
+ ` : r.proposal.status === "applied" ? `
600
+ <div class="proposal-diff">
601
+ <div class="proposal-new"><span class="proposal-label">&#10003; Applied</span><pre>${escapeHtml(r.proposal.newText)}</pre></div>
602
+ </div>
603
+ ` : `
604
+ <div class="proposal-diff">
605
+ <div class="proposal-old"><span class="proposal-label">&#10007; Rejected</span><pre>${escapeHtml(r.proposal.oldText)}</pre></div>
606
+ </div>
607
+ `}
608
+ </div>
609
+ ` : `
610
+ <div class="reply ${r.from}">
611
+ <div class="reply-from ${r.from}">${r.from}</div>
612
+ <div>${escapeHtml(r.text)}</div>
613
+ </div>
614
+ `).join("")}
615
+ </div>
616
+ ` : "";
617
+
618
+ if (c.status === "resolved") {
619
+ const truncated = c.comment.length > 60 ? c.comment.slice(0, 60) + "..." : c.comment;
620
+ return `
621
+ <div class="comment-card resolved" data-id="${c.id}">
622
+ <button class="comment-delete-btn" onclick="deleteComment('${c.id}')" title="Delete comment">
623
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
624
+ </button>
625
+ <div class="resolved-summary" onclick="toggleResolvedExpand('${c.id}')">
626
+ <svg class="resolved-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
627
+ <span class="comment-status resolved">resolved</span>
628
+ <span class="resolved-summary-text">${escapeHtml(truncated)}</span>
629
+ </div>
630
+ <div class="resolved-details" hidden>
631
+ ${c.selectedText
632
+ ? `<div class="comment-selected-text">${escapeHtml(c.selectedText)}</div>`
633
+ : `<div class="comment-file-badge">Whole file</div>`
634
+ }
635
+ <div class="comment-text">${escapeHtml(c.comment)}</div>
636
+ ${repliesHtml}
637
+ <div class="comment-meta">
638
+ <span>${timeAgo(c.createdAt)}</span>
639
+ </div>
640
+ <div class="comment-actions">
641
+ <button onclick="reopenComment('${c.id}')">Reopen</button>
459
642
  </div>
460
- `).join("")}
643
+ </div>
644
+ </div>
645
+ `;
646
+ }
647
+
648
+ return `
649
+ <div class="comment-card ${c.status}" data-id="${c.id}">
650
+ <button class="comment-delete-btn" onclick="deleteComment('${c.id}')" title="Delete comment">
651
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
652
+ </button>
653
+ ${c.selectedText
654
+ ? `<div class="comment-selected-text">${escapeHtml(c.selectedText)}</div>`
655
+ : `<div class="comment-file-badge">Whole file</div>`
656
+ }
657
+ <div class="comment-text">${escapeHtml(c.comment)}</div>
658
+ ${repliesHtml}
659
+ <div class="comment-meta">
660
+ <span>${timeAgo(c.createdAt)}</span>
661
+ <span class="comment-status ${c.status}">${c.status}</span>
461
662
  </div>
462
- ` : ""}
463
- <div class="comment-meta">
464
- <span>${timeAgo(c.createdAt)}</span>
465
- <span class="comment-status ${c.status}">${c.status}</span>
466
- </div>
467
- ${c.status !== "resolved" ? `
468
663
  <div class="comment-actions">
469
664
  <button onclick="showReplyForm('${c.id}')">Reply</button>
470
665
  <button onclick="resolveComment('${c.id}')">Resolve</button>
471
666
  </div>
472
- ` : `
473
- <div class="comment-actions">
474
- <button onclick="reopenComment('${c.id}')">Reopen</button>
475
- </div>
476
- `}
477
- <div class="reply-form" id="reply-form-${c.id}" hidden>
478
- <textarea rows="2" placeholder="Reply..."></textarea>
479
- <div class="reply-form-actions">
480
- <button onclick="submitReply('${c.id}')">Send</button>
667
+ <div class="reply-form" id="reply-form-${c.id}" hidden>
668
+ <textarea rows="2" placeholder="Reply..."></textarea>
669
+ <div class="reply-form-actions">
670
+ <button onclick="submitReply('${c.id}')">Send</button>
671
+ </div>
481
672
  </div>
482
673
  </div>
483
- </div>
484
- `).join("");
674
+ `;
675
+ }).join("");
485
676
 
486
677
  // Click to scroll to highlight
487
678
  for (const card of commentListEl.querySelectorAll(".comment-card")) {
@@ -529,6 +720,25 @@ window.submitReply = function (id) {
529
720
  form.hidden = true;
530
721
  };
531
722
 
723
+ window.applyProposal = function (commentId, replyId) {
724
+ send({ type: "proposal_apply", commentId, replyId });
725
+ };
726
+
727
+ window.rejectProposal = function (commentId, replyId) {
728
+ send({ type: "proposal_reject", commentId, replyId });
729
+ };
730
+
731
+ window.toggleResolvedExpand = function (id) {
732
+ const card = commentListEl.querySelector(`.comment-card[data-id="${id}"]`);
733
+ if (!card) return;
734
+ const details = card.querySelector(".resolved-details");
735
+ const chevron = card.querySelector(".resolved-chevron");
736
+ if (!details) return;
737
+ const expanding = details.hidden;
738
+ details.hidden = !expanding;
739
+ card.classList.toggle("resolved-expanded", expanding);
740
+ };
741
+
532
742
  // --- Highlights ---
533
743
 
534
744
  function applyHighlights() {
@@ -657,6 +867,24 @@ themeToggle.addEventListener("change", () => {
657
867
  applyTheme(theme);
658
868
  });
659
869
 
870
+ // --- Font size toggle ---
871
+ const FONT_SIZE_KEY = "cowrite-font-size";
872
+ (function initFontSize() {
873
+ const saved = localStorage.getItem(FONT_SIZE_KEY) || "large";
874
+ if (saved === "large") document.body.classList.add("font-large");
875
+ for (const btn of document.querySelectorAll(".font-size-btn")) {
876
+ btn.setAttribute("aria-pressed", btn.dataset.size === saved ? "true" : "false");
877
+ btn.addEventListener("click", () => {
878
+ const size = btn.dataset.size;
879
+ document.body.classList.toggle("font-large", size === "large");
880
+ localStorage.setItem(FONT_SIZE_KEY, size);
881
+ for (const b of document.querySelectorAll(".font-size-btn")) {
882
+ b.setAttribute("aria-pressed", b.dataset.size === size ? "true" : "false");
883
+ }
884
+ });
885
+ }
886
+ })();
887
+
660
888
  // Hide trigger when selection is cleared
661
889
  document.addEventListener("selectionchange", () => {
662
890
  const selection = window.getSelection();
@@ -1228,6 +1456,17 @@ function enterContentEditable(blockIndex, blockEl) {
1228
1456
 
1229
1457
  const blockType = getBlockType(blockEl);
1230
1458
 
1459
+ // Markdown shortcut patterns: detected on Space keydown BEFORE browser default.
1460
+ // This prevents Chrome from auto-formatting "* " into a native list element.
1461
+ const MD_SHORTCUTS = [
1462
+ { pattern: /^(\*|-)\s*$/, prefix: "- " },
1463
+ { pattern: /^1\.\s*$/, prefix: "1. " },
1464
+ { pattern: /^###\s*$/, prefix: "### " },
1465
+ { pattern: /^##\s*$/, prefix: "## " },
1466
+ { pattern: /^#\s*$/, prefix: "# " },
1467
+ { pattern: /^>\s*$/, prefix: "> " },
1468
+ ];
1469
+
1231
1470
  const onKeydown = (e) => {
1232
1471
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
1233
1472
  e.preventDefault();
@@ -1238,6 +1477,38 @@ function enterContentEditable(blockIndex, blockEl) {
1238
1477
  } else if (e.key === "Enter" && blockType === "heading") {
1239
1478
  e.preventDefault();
1240
1479
  commitContentEditable(blockEl);
1480
+ } else if (e.key === "Enter" && blockType === "paragraph") {
1481
+ // Insert a line break within the same block
1482
+ e.preventDefault();
1483
+ document.execCommand("insertLineBreak");
1484
+ } else if (e.key === " " && blockType === "paragraph") {
1485
+ // Markdown shortcuts: intercept Space BEFORE browser auto-formats.
1486
+ // At keydown time, Space isn't in the DOM yet, so check text before cursor.
1487
+ if (editingBlockIndex === -1 || !contentEditableActive) return;
1488
+ const sel = window.getSelection();
1489
+ if (!sel || !sel.rangeCount) return;
1490
+ const range = sel.getRangeAt(0);
1491
+ // Get text from start of block to cursor position
1492
+ const preRange = range.cloneRange();
1493
+ preRange.selectNodeContents(blockEl);
1494
+ preRange.setEnd(range.startContainer, range.startOffset);
1495
+ const textBeforeCursor = preRange.toString().replace(/\u200B/g, "");
1496
+ // Get text after cursor
1497
+ const postRange = range.cloneRange();
1498
+ postRange.selectNodeContents(blockEl);
1499
+ postRange.setStart(range.endContainer, range.endOffset);
1500
+ const textAfterCursor = postRange.toString().replace(/\u200B/g, "");
1501
+
1502
+ for (const { pattern, prefix } of MD_SHORTCUTS) {
1503
+ if (!pattern.test(textBeforeCursor)) continue;
1504
+ // Match found — prevent browser default (stops Chrome list auto-format)
1505
+ e.preventDefault();
1506
+ const newSource = prefix + textAfterCursor;
1507
+ pendingEditAfterInsert = editingBlockIndex;
1508
+ cleanupContentEditable(blockEl);
1509
+ commitBlockEdit(newSource);
1510
+ return;
1511
+ }
1241
1512
  }
1242
1513
  };
1243
1514
 
@@ -1261,22 +1532,51 @@ function enterContentEditable(blockIndex, blockEl) {
1261
1532
  blockEl.addEventListener("paste", onPaste);
1262
1533
  }
1263
1534
 
1535
+ // Walk DOM nodes and reconstruct markdown, preserving inline formatting
1536
+ function domToMarkdown(node) {
1537
+ if (node.nodeType === Node.TEXT_NODE) {
1538
+ return node.textContent.replace(/\u200B/g, "");
1539
+ }
1540
+ if (node.nodeType !== Node.ELEMENT_NODE) return "";
1541
+
1542
+ const tag = node.tagName.toLowerCase();
1543
+
1544
+ // Line break
1545
+ if (tag === "br") return " \n";
1546
+
1547
+ // Recurse into children
1548
+ const inner = Array.from(node.childNodes).map(domToMarkdown).join("");
1549
+
1550
+ // Inline formatting
1551
+ if (tag === "strong" || tag === "b") return `**${inner}**`;
1552
+ if (tag === "em" || tag === "i") return `*${inner}*`;
1553
+ if (tag === "del" || tag === "s") return `~~${inner}~~`;
1554
+ if (tag === "code") return `\`${inner}\``;
1555
+ if (tag === "a") {
1556
+ const href = node.getAttribute("href") || "";
1557
+ return `[${inner}](${href})`;
1558
+ }
1559
+
1560
+ return inner;
1561
+ }
1562
+
1264
1563
  function extractMarkdownFromElement(element, originalSource) {
1265
1564
  const blockType = getBlockType(element);
1266
- const text = element.textContent.replace(/\u200B/g, "").trim();
1267
1565
 
1268
1566
  if (blockType === "paragraph") {
1269
- return text;
1567
+ return domToMarkdown(element).trim();
1270
1568
  }
1271
1569
 
1272
1570
  if (blockType === "heading") {
1273
1571
  const match = originalSource.match(/^(#{1,6})\s/);
1274
1572
  const prefix = match ? match[1] : "#";
1275
- return prefix + " " + text;
1573
+ const inner = domToMarkdown(element).trim();
1574
+ return prefix + " " + inner;
1276
1575
  }
1277
1576
 
1278
1577
  if (blockType === "blockquote") {
1279
- return text.split("\n").map(line => "> " + line.trim()).join("\n");
1578
+ const inner = domToMarkdown(element).trim();
1579
+ return inner.split("\n").map(line => "> " + line.trim()).join("\n");
1280
1580
  }
1281
1581
 
1282
1582
  if (blockType === "list") {
@@ -1284,10 +1584,11 @@ function extractMarkdownFromElement(element, originalSource) {
1284
1584
  const items = Array.from(element.querySelectorAll("li"));
1285
1585
  return items.map((li, i) => {
1286
1586
  const prefix = isOrdered ? `${i + 1}. ` : "- ";
1287
- return prefix + li.textContent.trim();
1587
+ return prefix + domToMarkdown(li).trim();
1288
1588
  }).join("\n");
1289
1589
  }
1290
1590
 
1591
+ const text = (element.innerText || element.textContent).replace(/\u200B/g, "").trim();
1291
1592
  return text;
1292
1593
  }
1293
1594
 
@@ -1331,15 +1632,23 @@ fileContentEl.addEventListener("click", (e) => {
1331
1632
  const target = e.target;
1332
1633
  if (target.closest("a, .mermaid-container, .block-insert-btn, .block-type-menu, .inline-editor, .block-edit-wrapper, .block-editing, .code-block-header")) return;
1333
1634
 
1334
- // Handle comment highlight clicks
1635
+ // Handle comment highlight clicks — scroll to sidebar card (skip resolved, let them edit)
1335
1636
  const highlightEl = target.closest(".comment-highlight");
1336
1637
  if (highlightEl) {
1337
1638
  const commentId = highlightEl.dataset.commentId;
1338
1639
  const comment = comments.find(c => c.id === commentId);
1339
- if (comment) {
1340
- showHighlightPopover(highlightEl, comment);
1640
+ if (comment && comment.status !== "resolved") {
1641
+ for (const card of commentListEl.querySelectorAll(".comment-card")) {
1642
+ card.classList.remove("active");
1643
+ }
1644
+ const card = commentListEl.querySelector(`.comment-card[data-id="${comment.id}"]`);
1645
+ if (card) {
1646
+ card.classList.add("active");
1647
+ card.scrollIntoView({ behavior: "smooth", block: "nearest" });
1648
+ }
1649
+ return;
1341
1650
  }
1342
- return;
1651
+ // Resolved highlights fall through to block editing
1343
1652
  }
1344
1653
 
1345
1654
  if (!currentBlocks.length) return;
@@ -1364,55 +1673,12 @@ fileContentEl.addEventListener("click", (e) => {
1364
1673
  });
1365
1674
  });
1366
1675
 
1367
- // --- Highlight Popover ---
1368
-
1369
- function showHighlightPopover(el, comment) {
1370
- activeHighlightCommentId = comment.id;
1371
- highlightPopoverText.textContent = comment.comment;
1372
-
1373
- const rect = el.getBoundingClientRect();
1374
- highlightPopover.style.left = `${Math.min(rect.left, window.innerWidth - 260)}px`;
1375
- highlightPopover.style.top = `${rect.bottom + 8}px`;
1376
- highlightPopover.hidden = false;
1377
-
1378
- // Highlight and scroll to the corresponding comment card in the sidebar
1379
- for (const card of commentListEl.querySelectorAll(".comment-card")) {
1380
- card.classList.remove("active");
1381
- }
1382
- const card = commentListEl.querySelector(`.comment-card[data-id="${comment.id}"]`);
1383
- if (card) {
1384
- card.classList.add("active");
1385
- card.scrollIntoView({ behavior: "smooth", block: "nearest" });
1386
- }
1387
- }
1388
-
1389
- function hideHighlightPopover() {
1390
- highlightPopover.hidden = true;
1391
- activeHighlightCommentId = null;
1392
- for (const card of commentListEl.querySelectorAll(".comment-card.active")) {
1393
- card.classList.remove("active");
1394
- }
1395
- }
1396
-
1397
- highlightEditBtn.addEventListener("click", () => {
1398
- if (!activeHighlightCommentId) return;
1399
- const comment = comments.find(c => c.id === activeHighlightCommentId);
1400
- if (!comment) { hideHighlightPopover(); return; }
1401
-
1402
- for (let i = 0; i < currentBlocks.length; i++) {
1403
- const b = currentBlocks[i];
1404
- if (comment.offset >= b.sourceStart && comment.offset < b.sourceEnd) {
1405
- hideHighlightPopover();
1406
- enterBlockEditDispatch(i);
1407
- return;
1408
- }
1409
- }
1410
- hideHighlightPopover();
1411
- });
1412
-
1676
+ // --- Highlight click: clear active on outside click ---
1413
1677
  document.addEventListener("mousedown", (e) => {
1414
- if (!highlightPopover.hidden && !highlightPopover.contains(e.target) && !e.target.closest(".comment-highlight")) {
1415
- hideHighlightPopover();
1678
+ if (!e.target.closest(".comment-highlight")) {
1679
+ for (const card of commentListEl.querySelectorAll(".comment-card.active")) {
1680
+ card.classList.remove("active");
1681
+ }
1416
1682
  }
1417
1683
  });
1418
1684
 
package/ui/index.html CHANGED
@@ -26,6 +26,11 @@
26
26
  <span class="file-path" id="filePath"></span>
27
27
  </div>
28
28
  <div class="header-right">
29
+ <div class="font-size-toggle" id="fontSizeToggle">
30
+ <button class="font-size-btn" data-size="regular" title="Regular size"><span>A</span></button>
31
+ <button class="font-size-btn" data-size="large" title="Large size" aria-pressed="true"><span>A</span></button>
32
+ </div>
33
+ <div class="header-divider"></div>
29
34
  <button class="undo-btn" id="undoBtn" disabled title="Undo (Cmd+Z)">
30
35
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
31
36
  </button>
@@ -49,6 +54,7 @@
49
54
  <div id="fileContent"></div>
50
55
  </div>
51
56
 
57
+ <div class="sidebar-drag-handle" id="sidebarDragHandle"></div>
52
58
  <div class="sidebar" id="sidebar">
53
59
  <div class="sidebar-header">
54
60
  <h2>Comments <span class="comment-count" id="commentCount">0</span></h2>
@@ -60,6 +66,16 @@
60
66
 
61
67
  <!-- Floating selection toolbar (appears on text selection) -->
62
68
  <div class="selection-toolbar" id="selectionToolbar" hidden>
69
+ <div class="format-buttons" id="formatButtons" hidden>
70
+ <button data-format="bold" title="Bold (Cmd+B)"><strong>B</strong></button>
71
+ <button data-format="italic" title="Italic (Cmd+I)"><em>I</em></button>
72
+ <button data-format="strikethrough" title="Strikethrough (Cmd+Shift+S)"><s>S</s></button>
73
+ <button data-format="code" title="Code (Cmd+E)"><code>&lt;/&gt;</code></button>
74
+ <button data-format="link" title="Link (Cmd+K)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
75
+ <button data-format="blockquote" title="Quote"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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.031V21"/><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 4v3"/></svg></button>
76
+ <button data-format="bulletList" title="Bullet list"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg></button>
77
+ <span class="toolbar-divider"></span>
78
+ </div>
63
79
  <button id="commentTrigger">Comment</button>
64
80
  </div>
65
81
 
@@ -74,13 +90,6 @@
74
90
  </div>
75
91
  </div>
76
92
 
77
- <!-- Highlight popover (appears when clicking a comment highlight) -->
78
- <div class="highlight-popover" id="highlightPopover" hidden>
79
- <div class="highlight-popover-text" id="highlightPopoverText"></div>
80
- <div class="highlight-popover-actions">
81
- <button id="highlightEditBtn">Edit</button>
82
- </div>
83
- </div>
84
93
 
85
94
  <script type="module">
86
95
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';