@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/README.md +15 -4
- package/dist/bin/cowrite.js +162 -19
- package/dist/bin/cowrite.js.map +1 -1
- package/package.json +1 -1
- package/ui/client.js +365 -99
- package/ui/index.html +16 -7
- package/ui/styles.css +278 -5
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)
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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">✓ 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">✗ 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
|
-
|
|
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
|
-
|
|
474
|
-
<
|
|
475
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
|
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
|
-
|
|
1573
|
+
const inner = domToMarkdown(element).trim();
|
|
1574
|
+
return prefix + " " + inner;
|
|
1276
1575
|
}
|
|
1277
1576
|
|
|
1278
1577
|
if (blockType === "blockquote") {
|
|
1279
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
1415
|
-
|
|
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></></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';
|