@filipc77/cowrite 0.6.4 → 0.6.6

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,9 +14,6 @@ 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");
@@ -34,7 +31,6 @@ let currentBlocks = [];
34
31
  let insertBtn = null;
35
32
  let insertLine = null;
36
33
  let activeGapIndex = -1;
37
- let activeHighlightCommentId = null;
38
34
  let undoStack = [];
39
35
  const MAX_UNDO = 50;
40
36
 
@@ -47,6 +43,43 @@ let pendingFileUpdate = null;
47
43
  let pendingEditAfterInsert = -1;
48
44
  let contentEditableActive = false;
49
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
+
50
83
  const BLOCK_TYPES = [
51
84
  { id: "text", label: "Text", category: "Basic blocks", icon: "Aa", template: "\u200B" },
52
85
  { id: "h1", label: "Heading 1", category: "Basic blocks", icon: "H1", template: "# " },
@@ -91,15 +124,36 @@ function switchFile(file) {
91
124
  history.replaceState(null, "", url.toString());
92
125
  }
93
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
+
94
137
  filePicker.addEventListener("change", () => {
95
138
  const file = filePicker.value.trim();
96
- if (file) switchFile(file);
139
+ if (!file) return;
140
+ if (lastClickHadMeta) {
141
+ openFileInNewTab(file);
142
+ filePicker.value = "";
143
+ } else {
144
+ switchFile(file);
145
+ }
97
146
  });
98
147
 
99
148
  filePicker.addEventListener("keydown", (e) => {
100
- if (e.key === "Enter") {
101
- const file = filePicker.value.trim();
102
- 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);
103
157
  }
104
158
  });
105
159
 
@@ -526,48 +580,99 @@ function renderComments() {
526
580
  return;
527
581
  }
528
582
 
529
- commentListEl.innerHTML = comments.map((c) => `
530
- <div class="comment-card ${c.status}" data-id="${c.id}">
531
- <button class="comment-delete-btn" onclick="deleteComment('${c.id}')" title="Delete comment">
532
- <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>
533
- </button>
534
- ${c.selectedText
535
- ? `<div class="comment-selected-text">${escapeHtml(c.selectedText)}</div>`
536
- : `<div class="comment-file-badge">Whole file</div>`
537
- }
538
- <div class="comment-text">${escapeHtml(c.comment)}</div>
539
- ${c.replies.length > 0 ? `
540
- <div class="comment-replies">
541
- ${c.replies.map((r) => `
542
- <div class="reply ${r.from}">
543
- <div class="reply-from ${r.from}">${r.from}</div>
544
- <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>
545
639
  </div>
546
- `).join("")}
640
+ <div class="comment-actions">
641
+ <button onclick="reopenComment('${c.id}')">Reopen</button>
642
+ </div>
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>
547
662
  </div>
548
- ` : ""}
549
- <div class="comment-meta">
550
- <span>${timeAgo(c.createdAt)}</span>
551
- <span class="comment-status ${c.status}">${c.status}</span>
552
- </div>
553
- ${c.status !== "resolved" ? `
554
663
  <div class="comment-actions">
555
664
  <button onclick="showReplyForm('${c.id}')">Reply</button>
556
665
  <button onclick="resolveComment('${c.id}')">Resolve</button>
557
666
  </div>
558
- ` : `
559
- <div class="comment-actions">
560
- <button onclick="reopenComment('${c.id}')">Reopen</button>
561
- </div>
562
- `}
563
- <div class="reply-form" id="reply-form-${c.id}" hidden>
564
- <textarea rows="2" placeholder="Reply..."></textarea>
565
- <div class="reply-form-actions">
566
- <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>
567
672
  </div>
568
673
  </div>
569
- </div>
570
- `).join("");
674
+ `;
675
+ }).join("");
571
676
 
572
677
  // Click to scroll to highlight
573
678
  for (const card of commentListEl.querySelectorAll(".comment-card")) {
@@ -615,6 +720,25 @@ window.submitReply = function (id) {
615
720
  form.hidden = true;
616
721
  };
617
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
+
618
742
  // --- Highlights ---
619
743
 
620
744
  function applyHighlights() {
@@ -743,6 +867,24 @@ themeToggle.addEventListener("change", () => {
743
867
  applyTheme(theme);
744
868
  });
745
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
+
746
888
  // Hide trigger when selection is cleared
747
889
  document.addEventListener("selectionchange", () => {
748
890
  const selection = window.getSelection();
@@ -1490,15 +1632,23 @@ fileContentEl.addEventListener("click", (e) => {
1490
1632
  const target = e.target;
1491
1633
  if (target.closest("a, .mermaid-container, .block-insert-btn, .block-type-menu, .inline-editor, .block-edit-wrapper, .block-editing, .code-block-header")) return;
1492
1634
 
1493
- // Handle comment highlight clicks
1635
+ // Handle comment highlight clicks — scroll to sidebar card (skip resolved, let them edit)
1494
1636
  const highlightEl = target.closest(".comment-highlight");
1495
1637
  if (highlightEl) {
1496
1638
  const commentId = highlightEl.dataset.commentId;
1497
1639
  const comment = comments.find(c => c.id === commentId);
1498
- if (comment) {
1499
- 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;
1500
1650
  }
1501
- return;
1651
+ // Resolved highlights fall through to block editing
1502
1652
  }
1503
1653
 
1504
1654
  if (!currentBlocks.length) return;
@@ -1523,55 +1673,12 @@ fileContentEl.addEventListener("click", (e) => {
1523
1673
  });
1524
1674
  });
1525
1675
 
1526
- // --- Highlight Popover ---
1527
-
1528
- function showHighlightPopover(el, comment) {
1529
- activeHighlightCommentId = comment.id;
1530
- highlightPopoverText.textContent = comment.comment;
1531
-
1532
- const rect = el.getBoundingClientRect();
1533
- highlightPopover.style.left = `${Math.min(rect.left, window.innerWidth - 260)}px`;
1534
- highlightPopover.style.top = `${rect.bottom + 8}px`;
1535
- highlightPopover.hidden = false;
1536
-
1537
- // Highlight and scroll to the corresponding comment card in the sidebar
1538
- for (const card of commentListEl.querySelectorAll(".comment-card")) {
1539
- card.classList.remove("active");
1540
- }
1541
- const card = commentListEl.querySelector(`.comment-card[data-id="${comment.id}"]`);
1542
- if (card) {
1543
- card.classList.add("active");
1544
- card.scrollIntoView({ behavior: "smooth", block: "nearest" });
1545
- }
1546
- }
1547
-
1548
- function hideHighlightPopover() {
1549
- highlightPopover.hidden = true;
1550
- activeHighlightCommentId = null;
1551
- for (const card of commentListEl.querySelectorAll(".comment-card.active")) {
1552
- card.classList.remove("active");
1553
- }
1554
- }
1555
-
1556
- highlightEditBtn.addEventListener("click", () => {
1557
- if (!activeHighlightCommentId) return;
1558
- const comment = comments.find(c => c.id === activeHighlightCommentId);
1559
- if (!comment) { hideHighlightPopover(); return; }
1560
-
1561
- for (let i = 0; i < currentBlocks.length; i++) {
1562
- const b = currentBlocks[i];
1563
- if (comment.offset >= b.sourceStart && comment.offset < b.sourceEnd) {
1564
- hideHighlightPopover();
1565
- enterBlockEditDispatch(i);
1566
- return;
1567
- }
1568
- }
1569
- hideHighlightPopover();
1570
- });
1571
-
1676
+ // --- Highlight click: clear active on outside click ---
1572
1677
  document.addEventListener("mousedown", (e) => {
1573
- if (!highlightPopover.hidden && !highlightPopover.contains(e.target) && !e.target.closest(".comment-highlight")) {
1574
- hideHighlightPopover();
1678
+ if (!e.target.closest(".comment-highlight")) {
1679
+ for (const card of commentListEl.querySelectorAll(".comment-card.active")) {
1680
+ card.classList.remove("active");
1681
+ }
1575
1682
  }
1576
1683
  });
1577
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>
@@ -84,13 +90,6 @@
84
90
  </div>
85
91
  </div>
86
92
 
87
- <!-- Highlight popover (appears when clicking a comment highlight) -->
88
- <div class="highlight-popover" id="highlightPopover" hidden>
89
- <div class="highlight-popover-text" id="highlightPopoverText"></div>
90
- <div class="highlight-popover-actions">
91
- <button id="highlightEditBtn">Edit</button>
92
- </div>
93
- </div>
94
93
 
95
94
  <script type="module">
96
95
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
package/ui/styles.css CHANGED
@@ -185,6 +185,53 @@ header h1 {
185
185
  letter-spacing: 0.2px;
186
186
  }
187
187
 
188
+ /* ---- Font size toggle ---- */
189
+ .font-size-toggle {
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 2px;
193
+ background: var(--bg);
194
+ border: 1px solid var(--border);
195
+ border-radius: var(--radius-sm);
196
+ padding: 2px;
197
+ }
198
+
199
+ .font-size-btn {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ border: none;
204
+ background: transparent;
205
+ color: var(--text-faint);
206
+ cursor: pointer;
207
+ border-radius: 6px;
208
+ padding: 3px 8px;
209
+ font-family: var(--font-sans);
210
+ font-weight: 600;
211
+ transition: all 0.15s ease;
212
+ }
213
+
214
+ .font-size-btn[data-size="regular"] span { font-size: 11px; }
215
+ .font-size-btn[data-size="large"] span { font-size: 15px; }
216
+
217
+ .font-size-btn:hover {
218
+ color: var(--text-dim);
219
+ }
220
+
221
+ .font-size-btn[aria-pressed="true"] {
222
+ background: var(--surface-hover);
223
+ color: var(--accent);
224
+ }
225
+
226
+ /* Large font size mode */
227
+ body.font-large #fileContent .markdown-body {
228
+ font-size: 19px !important;
229
+ }
230
+
231
+ body.font-large #fileContent .plain-text {
232
+ font-size: 15px !important;
233
+ }
234
+
188
235
  /* ---- Undo button ---- */
189
236
  .undo-btn {
190
237
  display: flex;
@@ -443,6 +490,7 @@ main {
443
490
  .comment-highlight.resolved {
444
491
  background: transparent;
445
492
  border-bottom: none;
493
+ cursor: text;
446
494
  }
447
495
 
448
496
  [data-theme="light"] .comment-highlight:hover {
@@ -453,10 +501,31 @@ main {
453
501
  background: rgba(58, 114, 160, 0.12);
454
502
  }
455
503
 
504
+ /* ---- Sidebar drag handle ---- */
505
+ .sidebar-drag-handle {
506
+ width: 5px;
507
+ cursor: col-resize;
508
+ background: transparent;
509
+ flex-shrink: 0;
510
+ transition: background 0.15s ease;
511
+ z-index: 10;
512
+ }
513
+
514
+ .sidebar-drag-handle:hover,
515
+ body.sidebar-resizing .sidebar-drag-handle {
516
+ background: var(--accent);
517
+ }
518
+
519
+ body.sidebar-resizing {
520
+ cursor: col-resize;
521
+ user-select: none;
522
+ }
523
+
456
524
  /* ---- Sidebar ---- */
457
525
  .sidebar {
458
- width: 360px;
459
- min-width: 360px;
526
+ width: var(--sidebar-width, 360px);
527
+ min-width: 300px;
528
+ max-width: 50vw;
460
529
  background: var(--surface);
461
530
  border-left: 1px solid var(--border);
462
531
  overflow-y: auto;
@@ -596,12 +665,58 @@ main {
596
665
  }
597
666
 
598
667
  .comment-card.resolved {
599
- opacity: 0.5;
600
668
  border-left: 3px solid var(--green);
669
+ padding: 10px 14px;
670
+ }
671
+
672
+ .comment-card.resolved .comment-text,
673
+ .comment-card.resolved .comment-selected-text,
674
+ .comment-card.resolved .comment-meta {
675
+ color: var(--text-faint);
676
+ }
677
+
678
+ .comment-card.resolved .comment-selected-text {
679
+ border-left-color: var(--green);
680
+ background: var(--green-bg);
681
+ }
682
+
683
+ /* Resolved collapsed summary */
684
+ .resolved-summary {
685
+ display: flex;
686
+ align-items: center;
687
+ gap: 8px;
688
+ cursor: pointer;
689
+ user-select: none;
690
+ }
691
+
692
+ .resolved-summary:hover .resolved-summary-text {
693
+ color: var(--text-dim);
601
694
  }
602
695
 
603
- .comment-card.resolved:hover {
604
- opacity: 0.7;
696
+ .resolved-chevron {
697
+ flex-shrink: 0;
698
+ color: var(--text-faint);
699
+ transition: transform 0.15s ease;
700
+ }
701
+
702
+ .comment-card.resolved-expanded .resolved-chevron {
703
+ transform: rotate(90deg);
704
+ }
705
+
706
+ .resolved-summary-text {
707
+ font-size: 12px;
708
+ color: var(--text-faint);
709
+ white-space: nowrap;
710
+ overflow: hidden;
711
+ text-overflow: ellipsis;
712
+ min-width: 0;
713
+ transition: color 0.15s ease;
714
+ }
715
+
716
+ .resolved-details {
717
+ margin-top: 12px;
718
+ padding-top: 12px;
719
+ border-top: 1px solid var(--border);
605
720
  }
606
721
 
607
722
  @keyframes card-enter {
@@ -1269,6 +1384,130 @@ main {
1269
1384
  padding-right: 2px;
1270
1385
  }
1271
1386
 
1387
+ /* ---- Proposal diff ---- */
1388
+ .proposal-explanation {
1389
+ font-size: 12px;
1390
+ line-height: 1.5;
1391
+ margin-bottom: 8px;
1392
+ }
1393
+
1394
+ .proposal-diff {
1395
+ border: 1px solid var(--border);
1396
+ border-radius: var(--radius-sm);
1397
+ overflow: hidden;
1398
+ margin-bottom: 8px;
1399
+ }
1400
+
1401
+ .proposal-diff pre {
1402
+ margin: 0;
1403
+ padding: 8px 10px;
1404
+ font-family: var(--font-mono);
1405
+ font-size: 11px;
1406
+ line-height: 1.5;
1407
+ white-space: pre-wrap;
1408
+ word-wrap: break-word;
1409
+ }
1410
+
1411
+ .proposal-label {
1412
+ display: block;
1413
+ font-family: var(--font-sans);
1414
+ font-size: 10px;
1415
+ font-weight: 600;
1416
+ text-transform: uppercase;
1417
+ letter-spacing: 0.5px;
1418
+ padding: 4px 10px 0;
1419
+ }
1420
+
1421
+ .proposal-old {
1422
+ background: rgba(212, 97, 110, 0.08);
1423
+ border-bottom: 1px solid var(--border);
1424
+ }
1425
+
1426
+ .proposal-old .proposal-label { color: var(--red); }
1427
+
1428
+ .proposal-new {
1429
+ background: rgba(111, 191, 138, 0.08);
1430
+ }
1431
+
1432
+ .proposal-new .proposal-label { color: var(--green); }
1433
+
1434
+ .proposal-actions {
1435
+ display: flex;
1436
+ gap: 6px;
1437
+ }
1438
+
1439
+ .proposal-apply-btn {
1440
+ font-family: var(--font-sans);
1441
+ font-size: 11px;
1442
+ font-weight: 600;
1443
+ padding: 5px 14px;
1444
+ border-radius: var(--radius-sm);
1445
+ border: none;
1446
+ background: var(--green);
1447
+ color: var(--bg);
1448
+ cursor: pointer;
1449
+ transition: opacity 0.15s ease;
1450
+ }
1451
+
1452
+ .proposal-apply-btn:hover { opacity: 0.85; }
1453
+
1454
+ .proposal-reject-btn {
1455
+ font-family: var(--font-sans);
1456
+ font-size: 11px;
1457
+ font-weight: 500;
1458
+ padding: 5px 14px;
1459
+ border-radius: var(--radius-sm);
1460
+ border: 1px solid var(--border);
1461
+ background: var(--surface);
1462
+ color: var(--text-dim);
1463
+ cursor: pointer;
1464
+ transition: all 0.15s ease;
1465
+ }
1466
+
1467
+ .proposal-reject-btn:hover {
1468
+ color: var(--red);
1469
+ border-color: var(--red);
1470
+ }
1471
+
1472
+ .proposal-status-badge {
1473
+ display: inline-block;
1474
+ font-size: 10px;
1475
+ font-weight: 600;
1476
+ padding: 3px 10px;
1477
+ border-radius: 100px;
1478
+ text-transform: uppercase;
1479
+ letter-spacing: 0.4px;
1480
+ }
1481
+
1482
+ .proposal-status-badge.applied {
1483
+ color: var(--green);
1484
+ background: var(--green-bg);
1485
+ }
1486
+
1487
+ .proposal-status-badge.rejected {
1488
+ color: var(--red);
1489
+ background: rgba(212, 97, 110, 0.08);
1490
+ }
1491
+
1492
+ .proposal-reply.proposal-applied .proposal-diff {
1493
+ border-left: 3px solid var(--green);
1494
+ }
1272
1495
 
1496
+ .proposal-reply.proposal-rejected .proposal-diff {
1497
+ border-left: 3px solid var(--text-faint);
1498
+ }
1499
+
1500
+ .proposal-reply.proposal-rejected .proposal-diff pre {
1501
+ text-decoration: line-through;
1502
+ color: var(--text-faint);
1503
+ }
1504
+
1505
+ .proposal-reply.proposal-rejected .proposal-label {
1506
+ color: var(--text-faint);
1507
+ }
1508
+
1509
+ .proposal-reply.proposal-rejected .proposal-old {
1510
+ background: var(--surface-hover);
1511
+ }
1273
1512
 
1274
1513