@mrsf/markdown-it-mrsf 0.3.0 → 0.4.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.
@@ -1,287 +1,537 @@
1
- // src/controller.ts
2
- var floatingAddButton = null;
3
- var lastSelectionText = null;
4
- var overlayEl = null;
5
- var styleInjected = false;
6
- function injectStyles() {
7
- if (styleInjected) return;
8
- const css = `
9
- .mrsf-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 2000; display: flex; align-items: center; justify-content: center; }
10
- .mrsf-dialog { background: var(--mrsf-dialog-bg, var(--vp-c-bg, #1e1e1e)); color: var(--mrsf-dialog-fg, var(--vp-c-text-1, #f3f3f3)); border: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #444)); border-radius: 8px; width: min(520px, 90vw); box-shadow: 0 10px 40px rgba(0,0,0,0.35); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
11
- .mrsf-dialog header { padding: 12px 16px; font-weight: 600; border-bottom: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #333)); }
12
- .mrsf-dialog form { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
13
- .mrsf-field { display: flex; flex-direction: column; gap: 6px; }
14
- .mrsf-field label { font-size: 13px; color: var(--mrsf-dialog-muted, var(--vp-c-text-2, #ccc)); }
15
- .mrsf-field input, .mrsf-field select, .mrsf-field textarea { background: var(--mrsf-field-bg, var(--vp-c-bg-soft, #252526)); color: var(--mrsf-dialog-fg, var(--vp-c-text-1, #f3f3f3)); border: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #3c3c3c)); border-radius: 4px; padding: 6px 8px; font-size: 13px; }
16
- .mrsf-field textarea { min-height: 100px; resize: vertical; }
17
- .mrsf-actions-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 0; padding: 12px 16px 16px; border-top: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #333)); }
18
- .mrsf-btn { padding: 6px 12px; border-radius: 4px; border: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #3c3c3c)); background: var(--mrsf-button-bg, var(--vp-c-bg-soft, #2d2d30)); color: var(--mrsf-dialog-fg, var(--vp-c-text-1, #f3f3f3)); cursor: pointer; }
19
- .mrsf-btn-primary { background: var(--mrsf-button-primary-bg, #0e639c); border-color: var(--mrsf-button-primary-bg, #0e639c); color: #fff; }
20
- .mrsf-helper { font-size: 12px; color: var(--mrsf-dialog-muted, var(--vp-c-text-2, #aaa)); }
21
- `;
22
- const style = document.createElement("style");
23
- style.textContent = css;
24
- document.head.appendChild(style);
25
- styleInjected = true;
26
- }
27
- function ensureFloatingAddButton() {
28
- if (floatingAddButton) return floatingAddButton;
29
- const btn = document.createElement("button");
30
- btn.textContent = "Add comment";
31
- btn.className = "mrsf-add-inline-button";
32
- btn.dataset.mrsfAction = "add";
33
- btn.style.display = "none";
34
- btn.style.position = "absolute";
35
- btn.style.zIndex = "1200";
36
- document.body.appendChild(btn);
37
- floatingAddButton = btn;
38
- return btn;
1
+ // src/html.ts
2
+ function escapeHtml(str) {
3
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
39
4
  }
40
- function hideFloatingAddButton() {
41
- if (!floatingAddButton) return;
42
- floatingAddButton.style.display = "none";
43
- floatingAddButton.dataset.mrsfLine = "";
44
- floatingAddButton.dataset.mrsfStartLine = "";
45
- floatingAddButton.dataset.mrsfEndLine = "";
46
- lastSelectionText = null;
5
+ function formatTime(iso) {
6
+ if (!iso) return "";
7
+ try {
8
+ const d = new Date(iso);
9
+ return d.toLocaleDateString(void 0, {
10
+ year: "numeric",
11
+ month: "short",
12
+ day: "numeric"
13
+ });
14
+ } catch {
15
+ return "";
16
+ }
47
17
  }
48
- function showFloatingAddButton(startLine, endLine, startColumn, endColumn, rect, selectionText) {
49
- const btn = ensureFloatingAddButton();
50
- if (startLine != null) {
51
- btn.dataset.mrsfLine = String(startLine);
52
- btn.dataset.mrsfStartLine = String(startLine);
53
- btn.dataset.mrsfEndLine = String(endLine ?? startLine);
54
- } else {
55
- delete btn.dataset.mrsfLine;
56
- delete btn.dataset.mrsfStartLine;
57
- delete btn.dataset.mrsfEndLine;
18
+ function renderCommentHtml(comment, isReply, interactive) {
19
+ const resolvedClass = comment.resolved ? " mrsf-resolved" : "";
20
+ const replyClass = isReply ? " mrsf-reply" : "";
21
+ let html = `<span class="mrsf-comment${resolvedClass}${replyClass}" data-mrsf-comment-id="${escapeHtml(comment.id)}">`;
22
+ html += `<span class="mrsf-comment-header">`;
23
+ html += `<span class="mrsf-author">${escapeHtml(comment.author)}</span>`;
24
+ if (comment.timestamp) {
25
+ html += `<span class="mrsf-date">${escapeHtml(formatTime(comment.timestamp))}</span>`;
58
26
  }
59
- if (startColumn != null) {
60
- btn.dataset.mrsfStartColumn = String(startColumn);
61
- } else {
62
- delete btn.dataset.mrsfStartColumn;
27
+ if (comment.severity) {
28
+ html += `<span class="mrsf-severity mrsf-severity-${escapeHtml(comment.severity)}">${escapeHtml(comment.severity)}</span>`;
63
29
  }
64
- if (endColumn != null) {
65
- btn.dataset.mrsfEndColumn = String(endColumn);
66
- } else {
67
- delete btn.dataset.mrsfEndColumn;
68
- }
69
- lastSelectionText = selectionText;
70
- const margin = 6;
71
- btn.style.visibility = "hidden";
72
- btn.style.display = "block";
73
- const height = btn.offsetHeight || 0;
74
- const top = window.scrollY + rect.top - height - margin;
75
- const left = window.scrollX + rect.left;
76
- btn.style.top = `${Math.max(0, top)}px`;
77
- btn.style.left = `${Math.max(0, left)}px`;
78
- btn.style.visibility = "visible";
79
- }
80
- function handleSelectionChange() {
81
- const sel = document.getSelection();
82
- if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
83
- hideFloatingAddButton();
84
- return;
85
- }
86
- const text = sel.toString().trim();
87
- if (!text) {
88
- hideFloatingAddButton();
89
- return;
90
- }
91
- const range = sel.getRangeAt(0);
92
- const rect = range.getBoundingClientRect();
93
- if (!rect || rect.width === 0 && rect.height === 0) {
94
- hideFloatingAddButton();
95
- return;
96
- }
97
- const startAnchor = range.startContainer.parentElement?.closest(
98
- "[data-mrsf-line]"
99
- );
100
- const endAnchor = range.endContainer.parentElement?.closest(
101
- "[data-mrsf-line]"
102
- );
103
- const startLineStr = startAnchor?.dataset.mrsfLine;
104
- const endLineStr = endAnchor?.dataset.mrsfLine ?? startLineStr;
105
- const startLine = startLineStr ? parseInt(startLineStr, 10) : null;
106
- const endLine = endLineStr ? parseInt(endLineStr, 10) : startLine;
107
- const startColumn = startAnchor?.dataset.mrsfStartColumn ? parseInt(startAnchor.dataset.mrsfStartColumn, 10) : range.startContainer.nodeType === Node.TEXT_NODE ? range.startOffset : null;
108
- const endColumn = endAnchor?.dataset.mrsfEndColumn ? parseInt(endAnchor.dataset.mrsfEndColumn, 10) : range.endContainer.nodeType === Node.TEXT_NODE ? range.endOffset : null;
109
- showFloatingAddButton(startLine, endLine, startColumn, endColumn, rect, text);
110
- }
111
- function closeOverlay() {
112
- if (overlayEl && overlayEl.parentElement) {
113
- overlayEl.parentElement.removeChild(overlayEl);
30
+ if (comment.type) {
31
+ html += `<span class="mrsf-type">${escapeHtml(comment.type)}</span>`;
114
32
  }
115
- overlayEl = null;
33
+ if (comment.resolved) {
34
+ html += `<span class="mrsf-resolved-badge">\u2713 resolved</span>`;
35
+ }
36
+ html += `</span>`;
37
+ if (comment.selected_text) {
38
+ html += `<details class="mrsf-selected-text"><summary class="mrsf-selected-text-summary">${escapeHtml(comment.selected_text)}</summary><span class="mrsf-selected-text-full">${escapeHtml(comment.selected_text)}</span></details>`;
39
+ }
40
+ html += `<span class="mrsf-comment-body">${escapeHtml(comment.text)}</span>`;
41
+ if (interactive) {
42
+ const line = comment.line != null ? String(comment.line) : "";
43
+ html += `<span class="mrsf-actions">`;
44
+ if (comment.resolved) {
45
+ html += `<button class="mrsf-action-btn" data-mrsf-action="unresolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Unresolve</button>`;
46
+ } else {
47
+ html += `<button class="mrsf-action-btn" data-mrsf-action="resolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Resolve</button>`;
48
+ }
49
+ html += `<button class="mrsf-action-btn" data-mrsf-action="reply" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Reply</button>`;
50
+ html += `<button class="mrsf-action-btn" data-mrsf-action="edit" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Edit</button>`;
51
+ html += `<button class="mrsf-action-btn mrsf-action-danger" data-mrsf-action="delete" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Delete</button>`;
52
+ html += `</span>`;
53
+ }
54
+ html += `</span>`;
55
+ return html;
116
56
  }
117
- function openForm(action, detail) {
118
- if (window.mrsfDisableBuiltinUi) return;
119
- injectStyles();
120
- closeOverlay();
121
- const selText = detail.selectionText ?? "";
122
- const line = detail.line ?? detail.start_line ?? null;
123
- const endLine = detail.end_line ?? detail.line ?? null;
124
- const startCol = detail.start_column ?? null;
125
- const endCol = detail.end_column ?? null;
126
- const overlay = document.createElement("div");
127
- overlay.className = "mrsf-overlay";
128
- const dialog = document.createElement("div");
129
- dialog.className = "mrsf-dialog";
130
- const header = document.createElement("header");
131
- header.textContent = action === "add" ? "Add comment" : action === "edit" ? "Edit comment" : "Reply";
132
- dialog.appendChild(header);
133
- const form = document.createElement("form");
134
- const field = (labelText, inputEl, helper) => {
135
- const wrap = document.createElement("div");
136
- wrap.className = "mrsf-field";
137
- const label = document.createElement("label");
138
- label.textContent = labelText;
139
- wrap.appendChild(label);
140
- wrap.appendChild(inputEl);
141
- if (helper) {
142
- const h = document.createElement("div");
143
- h.className = "mrsf-helper";
144
- h.textContent = helper;
145
- wrap.appendChild(h);
146
- }
147
- form.appendChild(wrap);
148
- };
149
- const textArea = document.createElement("textarea");
150
- textArea.name = "text";
151
- textArea.required = true;
152
- field("Comment text", textArea);
153
- const typeSelect = document.createElement("select");
154
- typeSelect.name = "type";
155
- ["", "suggestion", "issue", "question", "accuracy", "style", "clarity"].forEach((t) => {
156
- const opt = document.createElement("option");
157
- opt.value = t;
158
- opt.textContent = t || "(none)";
159
- typeSelect.appendChild(opt);
160
- });
161
- field("Type", typeSelect, "Optional");
162
- const severitySelect = document.createElement("select");
163
- severitySelect.name = "severity";
164
- ["", "low", "medium", "high"].forEach((s) => {
165
- const opt = document.createElement("option");
166
- opt.value = s;
167
- opt.textContent = s || "(none)";
168
- severitySelect.appendChild(opt);
169
- });
170
- field("Severity", severitySelect, "Optional");
171
- if (selText) {
172
- const pre = document.createElement("pre");
173
- pre.textContent = selText;
174
- pre.style.margin = "0";
175
- pre.style.whiteSpace = "pre-wrap";
176
- field("Selected text", pre, "Captured automatically");
177
- }
178
- const actions = document.createElement("div");
179
- actions.className = "mrsf-actions-row";
180
- const cancelBtn = document.createElement("button");
181
- cancelBtn.type = "button";
182
- cancelBtn.className = "mrsf-btn";
183
- cancelBtn.textContent = "Cancel";
184
- cancelBtn.addEventListener("click", () => closeOverlay());
185
- actions.appendChild(cancelBtn);
186
- const submitBtn = document.createElement("button");
187
- submitBtn.type = "submit";
188
- submitBtn.className = "mrsf-btn mrsf-btn-primary";
189
- submitBtn.textContent = action === "add" ? "Add" : action === "reply" ? "Reply" : "Save";
190
- actions.appendChild(submitBtn);
191
- form.appendChild(actions);
192
- form.addEventListener("submit", (ev) => {
193
- ev.preventDefault();
194
- const detailOut = {
195
- action,
196
- commentId: detail.commentId,
197
- text: textArea.value.trim(),
198
- type: typeSelect.value || null,
199
- severity: severitySelect.value || null,
200
- line,
201
- end_line: endLine,
202
- start_column: startCol,
203
- end_column: endCol,
204
- selection_text: selText || null
205
- };
206
- document.dispatchEvent(new CustomEvent("mrsf:submit", { detail: detailOut, bubbles: true }));
207
- closeOverlay();
208
- });
209
- dialog.appendChild(form);
210
- overlay.appendChild(dialog);
211
- overlay.addEventListener("click", (e) => {
212
- if (e.target === overlay) closeOverlay();
213
- });
214
- document.body.appendChild(overlay);
215
- overlayEl = overlay;
57
+ function renderThreadHtml(thread, interactive) {
58
+ let html = `<span class="mrsf-thread">`;
59
+ html += renderCommentHtml(thread.comment, false, interactive);
60
+ if (thread.replies.length > 0) {
61
+ html += `<span class="mrsf-replies">`;
62
+ for (const reply of thread.replies) {
63
+ html += renderCommentHtml(reply, true, interactive);
64
+ }
65
+ html += `</span>`;
66
+ }
67
+ html += `</span>`;
68
+ return html;
216
69
  }
217
- function openConfirm(action, detail) {
218
- if (window.mrsfDisableBuiltinUi) return;
219
- injectStyles();
220
- closeOverlay();
221
- const overlay = document.createElement("div");
222
- overlay.className = "mrsf-overlay";
223
- const dialog = document.createElement("div");
224
- dialog.className = "mrsf-dialog";
225
- const header = document.createElement("header");
226
- header.textContent = action === "delete" ? "Delete comment" : "Change status";
227
- dialog.appendChild(header);
228
- const body = document.createElement("div");
229
- body.style.padding = "16px";
230
- body.textContent = action === "delete" ? "Delete this comment?" : action === "resolve" ? "Mark this comment as resolved?" : "Mark this comment as unresolved?";
231
- dialog.appendChild(body);
232
- const actions = document.createElement("div");
233
- actions.className = "mrsf-actions-row";
234
- const cancelBtn = document.createElement("button");
235
- cancelBtn.type = "button";
236
- cancelBtn.className = "mrsf-btn";
237
- cancelBtn.textContent = "Cancel";
238
- cancelBtn.addEventListener("click", () => closeOverlay());
239
- actions.appendChild(cancelBtn);
240
- const confirmBtn = document.createElement("button");
241
- confirmBtn.type = "button";
242
- confirmBtn.className = "mrsf-btn mrsf-btn-primary";
243
- confirmBtn.textContent = action === "delete" ? "Delete" : "Confirm";
244
- confirmBtn.addEventListener("click", () => {
245
- const detailOut = {
246
- action,
247
- commentId: detail.commentId,
248
- text: "",
249
- type: null,
250
- severity: null,
251
- line: detail.line,
252
- end_line: detail.end_line ?? detail.line ?? null,
253
- start_column: detail.start_column ?? null,
254
- end_column: detail.end_column ?? null,
255
- selection_text: detail.selectionText ?? null
70
+
71
+ // src/controller.ts
72
+ var MrsfController = class _MrsfController {
73
+ container;
74
+ opts;
75
+ threads = /* @__PURE__ */ new Map();
76
+ gutterLeft = null;
77
+ gutterRight = null;
78
+ activeTooltip = null;
79
+ floatingAddButton = null;
80
+ overlayEl = null;
81
+ lastSelectionText = null;
82
+ resizeObserver = null;
83
+ styleInjected = false;
84
+ inlineMarks = [];
85
+ inlineTooltipEl = null;
86
+ orphanedSection = null;
87
+ handleResizeBound = this.positionGutterItems.bind(this);
88
+ handleClickBound = this.handleClick.bind(this);
89
+ handleSelectionBound = this.handleSelectionChange.bind(this);
90
+ constructor(container, options = {}) {
91
+ this.container = container;
92
+ this.opts = {
93
+ gutterPosition: options.gutterPosition ?? "right",
94
+ interactive: options.interactive ?? false,
95
+ comments: options.comments ?? [],
96
+ inlineHighlights: options.inlineHighlights ?? true
256
97
  };
257
- document.dispatchEvent(new CustomEvent("mrsf:submit", { detail: detailOut, bubbles: true }));
258
- closeOverlay();
259
- });
260
- actions.appendChild(confirmBtn);
261
- dialog.appendChild(actions);
262
- overlay.appendChild(dialog);
263
- overlay.addEventListener("click", (e) => {
264
- if (e.target === overlay) closeOverlay();
265
- });
266
- document.body.appendChild(overlay);
267
- overlayEl = overlay;
268
- }
269
- function init() {
270
- const globalAny = window;
271
- if (globalAny.__mrsfControllerReady) return;
272
- globalAny.__mrsfControllerReady = true;
273
- document.addEventListener("selectionchange", handleSelectionChange);
274
- document.addEventListener("click", (e) => {
275
- const target = e.target.closest(
276
- "[data-mrsf-action]"
277
- );
98
+ this.loadCommentData();
99
+ this.setupOverlayStructure();
100
+ this.renderGutterItems();
101
+ this.positionGutterItems();
102
+ this.renderInlineHighlights();
103
+ this.renderOrphanedSection();
104
+ this.resizeObserver = new ResizeObserver(this.handleResizeBound);
105
+ this.resizeObserver.observe(this.container);
106
+ document.addEventListener("click", this.handleClickBound);
107
+ if (this.opts.interactive) {
108
+ document.addEventListener("selectionchange", this.handleSelectionBound);
109
+ }
110
+ }
111
+ /** Remove all controller DOM and listeners. */
112
+ destroy() {
113
+ this.resizeObserver?.disconnect();
114
+ document.removeEventListener("click", this.handleClickBound);
115
+ document.removeEventListener("selectionchange", this.handleSelectionBound);
116
+ this.removeInlineHighlights();
117
+ this.orphanedSection?.remove();
118
+ this.gutterLeft?.remove();
119
+ this.gutterRight?.remove();
120
+ this.floatingAddButton?.remove();
121
+ this.closeOverlay();
122
+ this.container.classList.remove("mrsf-overlay-root");
123
+ }
124
+ // ── Data loading ────────────────────────────────────────
125
+ loadCommentData() {
126
+ if (this.opts.comments.length > 0) {
127
+ this.buildThreadMap(this.opts.comments);
128
+ return;
129
+ }
130
+ const script = this.container.querySelector('script[type="application/mrsf+json"]');
131
+ if (!script?.textContent) return;
132
+ try {
133
+ const data = JSON.parse(script.textContent);
134
+ if (data.threads) {
135
+ this.buildThreadMap(data.threads);
136
+ }
137
+ } catch {
138
+ }
139
+ }
140
+ buildThreadMap(threads) {
141
+ this.threads.clear();
142
+ for (const t of threads) {
143
+ const line = t.comment.line;
144
+ if (line == null) continue;
145
+ const arr = this.threads.get(line) ?? [];
146
+ arr.push(t);
147
+ this.threads.set(line, arr);
148
+ }
149
+ }
150
+ // ── Overlay structure ───────────────────────────────────
151
+ setupOverlayStructure() {
152
+ this.container.classList.add("mrsf-overlay-root");
153
+ const pos = this.opts.gutterPosition;
154
+ if (pos === "left") {
155
+ this.gutterLeft = this.createGutter("mrsf-gutter-left");
156
+ } else {
157
+ this.gutterRight = this.createGutter("mrsf-gutter-right");
158
+ }
159
+ }
160
+ createGutter(cls) {
161
+ const gutter = document.createElement("div");
162
+ gutter.className = `mrsf-gutter ${cls}`;
163
+ this.container.appendChild(gutter);
164
+ return gutter;
165
+ }
166
+ // ── Gutter rendering ───────────────────────────────────
167
+ /** Build badge/add-button elements for each line in the gutter(s). */
168
+ renderGutterItems() {
169
+ const lines = this.collectLines();
170
+ const gutter = this.primaryGutter();
171
+ if (!gutter) return;
172
+ for (const line of lines) {
173
+ const threads = this.threads.get(line);
174
+ if (threads && threads.length > 0) {
175
+ const item = this.createBadgeItem(line, threads);
176
+ gutter.appendChild(item);
177
+ } else if (this.opts.interactive) {
178
+ const item = this.createAddItem(line);
179
+ gutter.appendChild(item);
180
+ }
181
+ }
182
+ }
183
+ primaryGutter() {
184
+ return this.gutterLeft ?? this.gutterRight;
185
+ }
186
+ /** Collect all unique line numbers from data-mrsf-line elements, expanding multi-line ranges. */
187
+ collectLines() {
188
+ const els = this.container.querySelectorAll("[data-mrsf-line]");
189
+ const seen = /* @__PURE__ */ new Set();
190
+ for (const el of els) {
191
+ if (el.tagName === "SCRIPT") continue;
192
+ const line = parseInt(el.dataset.mrsfLine, 10);
193
+ if (isNaN(line)) continue;
194
+ const startLine = parseInt(el.dataset.mrsfStartLine ?? "", 10);
195
+ const endLine = parseInt(el.dataset.mrsfEndLine ?? "", 10);
196
+ if (!isNaN(startLine) && !isNaN(endLine) && endLine > startLine) {
197
+ for (let l = startLine; l <= endLine; l++) seen.add(l);
198
+ } else {
199
+ seen.add(line);
200
+ }
201
+ }
202
+ return [...seen].sort((a, b) => a - b);
203
+ }
204
+ createBadgeItem(line, threads) {
205
+ const item = document.createElement("div");
206
+ item.className = "mrsf-gutter-item";
207
+ item.dataset.mrsfGutterLine = String(line);
208
+ const total = threads.reduce((n, t) => n + 1 + t.replies.length, 0);
209
+ const allResolved = threads.every((t) => t.comment.resolved);
210
+ const highestSeverity = threads.reduce((sev, t) => {
211
+ if (t.comment.severity === "high" || sev === "high") return "high";
212
+ if (t.comment.severity === "medium" || sev === "medium") return "medium";
213
+ if (t.comment.severity === "low" || sev === "low") return "low";
214
+ return sev;
215
+ }, null);
216
+ const classes = ["mrsf-badge"];
217
+ if (allResolved) classes.push("mrsf-badge-resolved");
218
+ if (highestSeverity === "high" || highestSeverity === "medium") {
219
+ classes.push(`mrsf-badge-severity-${highestSeverity}`);
220
+ }
221
+ const icon = allResolved ? "\u2713" : "\u{1F4AC}";
222
+ const badge = document.createElement("span");
223
+ badge.className = classes.join(" ");
224
+ badge.dataset.mrsfLine = String(line);
225
+ badge.dataset.mrsfAction = "navigate";
226
+ badge.dataset.mrsfCommentId = threads[0].comment.id;
227
+ badge.tabIndex = 0;
228
+ badge.textContent = `${icon} ${total}`;
229
+ badge.addEventListener("click", (e) => {
230
+ e.stopPropagation();
231
+ this.toggleTooltip(item, line, threads);
232
+ });
233
+ item.appendChild(badge);
234
+ if (this.opts.interactive) {
235
+ const addBtn = this.createAddButton(line);
236
+ item.appendChild(addBtn);
237
+ }
238
+ return item;
239
+ }
240
+ createAddItem(line) {
241
+ const item = document.createElement("div");
242
+ item.className = "mrsf-gutter-item";
243
+ item.dataset.mrsfGutterLine = String(line);
244
+ const addBtn = this.createAddButton(line);
245
+ item.appendChild(addBtn);
246
+ return item;
247
+ }
248
+ createAddButton(line) {
249
+ const btn = document.createElement("button");
250
+ btn.className = "mrsf-gutter-add";
251
+ btn.dataset.mrsfAction = "add";
252
+ btn.dataset.mrsfLine = String(line);
253
+ btn.dataset.mrsfStartLine = String(line);
254
+ btn.dataset.mrsfEndLine = String(line);
255
+ btn.setAttribute("aria-label", "Add comment");
256
+ btn.textContent = "+";
257
+ return btn;
258
+ }
259
+ // ── Positioning ─────────────────────────────────────────
260
+ /** Measure [data-mrsf-line] elements and set gutter item Y offsets. */
261
+ positionGutterItems() {
262
+ const containerRect = this.container.getBoundingClientRect();
263
+ const gutters = [this.gutterLeft, this.gutterRight].filter(Boolean);
264
+ for (const gutter of gutters) {
265
+ const items = gutter.querySelectorAll(".mrsf-gutter-item");
266
+ for (const item of items) {
267
+ const line = parseInt(item.dataset.mrsfGutterLine, 10);
268
+ let target = this.container.querySelector(
269
+ `[data-mrsf-line="${line}"]:not(script):not(.mrsf-gutter-item)`
270
+ );
271
+ if (!target) {
272
+ target = this.findElementForLine(line);
273
+ }
274
+ if (!target) {
275
+ item.style.display = "none";
276
+ continue;
277
+ }
278
+ const targetRect = target.getBoundingClientRect();
279
+ const top = targetRect.top - containerRect.top;
280
+ item.style.top = `${top}px`;
281
+ item.style.display = "";
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Find the element whose start-line/end-line range contains the given line.
287
+ * Used for positioning gutter items on expanded multi-line elements.
288
+ */
289
+ findElementForLine(line) {
290
+ const els = this.container.querySelectorAll("[data-mrsf-start-line][data-mrsf-end-line]");
291
+ for (const el of els) {
292
+ if (el.tagName === "SCRIPT") continue;
293
+ const start = parseInt(el.dataset.mrsfStartLine, 10);
294
+ const end = parseInt(el.dataset.mrsfEndLine, 10);
295
+ if (!isNaN(start) && !isNaN(end) && line >= start && line <= end) {
296
+ return el;
297
+ }
298
+ }
299
+ return null;
300
+ }
301
+ // ── Orphaned comments section ────────────────────────────
302
+ /**
303
+ * Render orphaned comments (whose line doesn't match any DOM element)
304
+ * in a dedicated section at the bottom of the container.
305
+ */
306
+ renderOrphanedSection() {
307
+ const lines = this.collectLines();
308
+ const lineSet = new Set(lines);
309
+ const orphanedThreads = [];
310
+ for (const [line, threads] of this.threads) {
311
+ if (!lineSet.has(line)) {
312
+ orphanedThreads.push(...threads);
313
+ }
314
+ }
315
+ if (orphanedThreads.length === 0) return;
316
+ const section = document.createElement("div");
317
+ section.className = "mrsf-orphaned-section";
318
+ const heading = document.createElement("div");
319
+ heading.className = "mrsf-orphaned-heading";
320
+ heading.textContent = `Orphaned Comments (${orphanedThreads.length})`;
321
+ section.appendChild(heading);
322
+ const interactive = this.opts.interactive;
323
+ for (const thread of orphanedThreads) {
324
+ const wrapper = document.createElement("div");
325
+ wrapper.className = interactive ? "mrsf-orphaned-thread mrsf-interactive" : "mrsf-orphaned-thread";
326
+ wrapper.innerHTML = renderThreadHtml(thread, interactive);
327
+ section.appendChild(wrapper);
328
+ }
329
+ this.container.appendChild(section);
330
+ this.orphanedSection = section;
331
+ }
332
+ // ── Inline text highlights ──────────────────────────────
333
+ /**
334
+ * For comments with `selected_text`, find the matching text in the DOM
335
+ * and wrap it in a `<mark class="mrsf-inline-highlight">` element with
336
+ * hover/click behaviour to show the comment tooltip.
337
+ */
338
+ renderInlineHighlights() {
339
+ if (!this.opts.inlineHighlights) return;
340
+ for (const [line, threads] of this.threads) {
341
+ for (const thread of threads) {
342
+ const comment = thread.comment;
343
+ if (!comment.selected_text) continue;
344
+ const el = this.container.querySelector(
345
+ `[data-mrsf-line="${line}"]:not(script):not(.mrsf-gutter):not(.mrsf-gutter-item)`
346
+ );
347
+ if (!el) continue;
348
+ this.wrapSelectedText(el, comment.selected_text, thread);
349
+ }
350
+ }
351
+ }
352
+ /**
353
+ * Strip common inline markdown syntax so `selected_text` from source
354
+ * can be matched against rendered text content.
355
+ */
356
+ static stripInlineMarkdown(text) {
357
+ let s = text;
358
+ s = s.replace(/`([^`]+)`/g, "$1");
359
+ s = s.replace(/\*\*(.+?)\*\*/g, "$1");
360
+ s = s.replace(/__(.+?)__/g, "$1");
361
+ s = s.replace(/\*(.+?)\*/g, "$1");
362
+ s = s.replace(/_(.+?)_/g, "$1");
363
+ s = s.replace(/~~(.+?)~~/g, "$1");
364
+ return s;
365
+ }
366
+ /**
367
+ * Walk text nodes inside `root` to find `text`, then wrap the matching
368
+ * range in a `<mark>` element. Falls back to markdown-stripped matching.
369
+ */
370
+ wrapSelectedText(root, selectedText, thread) {
371
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
372
+ let accumulated = "";
373
+ const textNodes = [];
374
+ let node;
375
+ while (node = walker.nextNode()) {
376
+ const start = accumulated.length;
377
+ accumulated += node.textContent || "";
378
+ textNodes.push({ node, start, end: accumulated.length });
379
+ }
380
+ let matchStart = accumulated.indexOf(selectedText);
381
+ let matchLen = selectedText.length;
382
+ if (matchStart === -1) {
383
+ const stripped = _MrsfController.stripInlineMarkdown(selectedText);
384
+ if (stripped !== selectedText) {
385
+ matchStart = accumulated.indexOf(stripped);
386
+ matchLen = stripped.length;
387
+ }
388
+ }
389
+ if (matchStart === -1) return;
390
+ const matchEnd = matchStart + matchLen;
391
+ const range = document.createRange();
392
+ let startSet = false;
393
+ for (const tn of textNodes) {
394
+ if (!startSet && tn.end > matchStart) {
395
+ range.setStart(tn.node, matchStart - tn.start);
396
+ startSet = true;
397
+ }
398
+ if (startSet && tn.end >= matchEnd) {
399
+ range.setEnd(tn.node, matchEnd - tn.start);
400
+ break;
401
+ }
402
+ }
403
+ if (!startSet) return;
404
+ const mark = document.createElement("mark");
405
+ mark.className = "mrsf-inline-highlight";
406
+ mark.dataset.mrsfCommentId = thread.comment.id;
407
+ mark.dataset.mrsfLine = String(thread.comment.line);
408
+ try {
409
+ range.surroundContents(mark);
410
+ } catch {
411
+ const fragment = range.extractContents();
412
+ mark.appendChild(fragment);
413
+ range.insertNode(mark);
414
+ }
415
+ this.inlineMarks.push(mark);
416
+ mark.addEventListener("mouseenter", () => {
417
+ this.showInlineTooltip(mark, thread);
418
+ });
419
+ mark.addEventListener("mouseleave", (e) => {
420
+ const related = e.relatedTarget;
421
+ if (related && this.inlineTooltipEl?.contains(related)) return;
422
+ this.scheduleHideInlineTooltip();
423
+ });
424
+ mark.addEventListener("click", (e) => {
425
+ e.stopPropagation();
426
+ if (this.inlineTooltipEl && this.inlineTooltipEl.dataset.mrsfForMark === thread.comment.id) {
427
+ this.hideInlineTooltip();
428
+ } else {
429
+ this.showInlineTooltip(mark, thread);
430
+ }
431
+ });
432
+ }
433
+ showInlineTooltip(mark, thread) {
434
+ this.hideInlineTooltip();
435
+ const tooltip = document.createElement("div");
436
+ tooltip.className = this.opts.interactive ? "mrsf-inline-tooltip mrsf-interactive mrsf-tooltip-visible" : "mrsf-inline-tooltip mrsf-tooltip-visible";
437
+ tooltip.dataset.mrsfForMark = thread.comment.id;
438
+ tooltip.innerHTML = renderThreadHtml(thread, this.opts.interactive);
439
+ tooltip.addEventListener("mouseenter", () => {
440
+ this.cancelHideInlineTooltip();
441
+ });
442
+ tooltip.addEventListener("mouseleave", () => {
443
+ this.hideInlineTooltip();
444
+ });
445
+ document.body.appendChild(tooltip);
446
+ this.inlineTooltipEl = tooltip;
447
+ const rect = mark.getBoundingClientRect();
448
+ const margin = 4;
449
+ const tooltipH = tooltip.offsetHeight;
450
+ if (rect.bottom + margin + tooltipH > window.innerHeight) {
451
+ tooltip.style.top = `${rect.top - tooltipH - margin}px`;
452
+ } else {
453
+ tooltip.style.top = `${rect.bottom + margin}px`;
454
+ }
455
+ tooltip.style.left = `${Math.max(0, rect.left)}px`;
456
+ }
457
+ hideInlineTimeout = null;
458
+ scheduleHideInlineTooltip() {
459
+ this.hideInlineTimeout = setTimeout(() => this.hideInlineTooltip(), 120);
460
+ }
461
+ cancelHideInlineTooltip() {
462
+ if (this.hideInlineTimeout) {
463
+ clearTimeout(this.hideInlineTimeout);
464
+ this.hideInlineTimeout = null;
465
+ }
466
+ }
467
+ hideInlineTooltip() {
468
+ this.cancelHideInlineTooltip();
469
+ if (this.inlineTooltipEl) {
470
+ this.inlineTooltipEl.remove();
471
+ this.inlineTooltipEl = null;
472
+ }
473
+ }
474
+ /** Remove all inline marks, unwrapping their contents back to text. */
475
+ removeInlineHighlights() {
476
+ this.hideInlineTooltip();
477
+ for (const mark of this.inlineMarks) {
478
+ const parent = mark.parentNode;
479
+ if (!parent) continue;
480
+ while (mark.firstChild) {
481
+ parent.insertBefore(mark.firstChild, mark);
482
+ }
483
+ parent.removeChild(mark);
484
+ }
485
+ this.inlineMarks = [];
486
+ }
487
+ // ── Tooltip ─────────────────────────────────────────────
488
+ toggleTooltip(anchor, line, threads) {
489
+ if (this.activeTooltip && this.activeTooltip.parentElement === anchor) {
490
+ this.hideTooltip();
491
+ return;
492
+ }
493
+ this.hideTooltip();
494
+ this.showTooltip(anchor, line, threads);
495
+ }
496
+ showTooltip(anchor, line, threads) {
497
+ const tooltip = document.createElement("div");
498
+ const interactive = this.opts.interactive;
499
+ tooltip.className = interactive ? "mrsf-tooltip mrsf-interactive mrsf-tooltip-visible" : "mrsf-tooltip mrsf-tooltip-visible";
500
+ tooltip.dataset.mrsfLine = String(line);
501
+ let html = "";
502
+ for (const thread of threads) {
503
+ html += renderThreadHtml(thread, interactive);
504
+ }
505
+ if (interactive) {
506
+ html += `<div class="mrsf-tooltip-actions"><button class="mrsf-action-btn" data-mrsf-action="add" data-mrsf-line="${line}" data-mrsf-start-line="${line}" data-mrsf-end-line="${line}">Add comment</button></div>`;
507
+ }
508
+ tooltip.innerHTML = html;
509
+ anchor.appendChild(tooltip);
510
+ this.activeTooltip = tooltip;
511
+ }
512
+ hideTooltip() {
513
+ if (this.activeTooltip) {
514
+ this.activeTooltip.remove();
515
+ this.activeTooltip = null;
516
+ }
517
+ }
518
+ // ── Click handling ──────────────────────────────────────
519
+ handleClick(e) {
520
+ const target = e.target.closest("[data-mrsf-action]");
521
+ if (!target && this.activeTooltip) {
522
+ const tooltipClick = e.target.closest(".mrsf-tooltip");
523
+ if (!tooltipClick) {
524
+ this.hideTooltip();
525
+ }
526
+ return;
527
+ }
278
528
  if (!target) return;
279
529
  const action = target.dataset.mrsfAction;
280
- const commentId = target.dataset.mrsfCommentId ?? null;
281
530
  if (!action) return;
531
+ const commentId = target.dataset.mrsfCommentId ?? null;
282
532
  const lineStr = target.dataset.mrsfLine;
283
533
  const line = lineStr ? parseInt(lineStr, 10) : null;
284
- const selectionText = target.dataset.mrsfSelection ?? lastSelectionText ?? null;
534
+ const selectionText = target.dataset.mrsfSelection ?? this.lastSelectionText ?? null;
285
535
  const startLineStr = target.dataset.mrsfStartLine;
286
536
  const endLineStr = target.dataset.mrsfEndLine;
287
537
  const startColStr = target.dataset.mrsfStartColumn;
@@ -304,19 +554,306 @@ function init() {
304
554
  };
305
555
  if (action === "add" || action === "edit" || action === "reply") {
306
556
  if (action === "add") {
307
- hideFloatingAddButton();
557
+ this.hideFloatingAddButton();
308
558
  }
309
- openForm(action, detail);
559
+ this.openForm(action, detail);
310
560
  return;
311
561
  }
312
562
  if (action === "resolve" || action === "unresolve" || action === "delete") {
313
- openConfirm(action, detail);
563
+ this.openConfirm(action, detail);
314
564
  return;
315
565
  }
316
566
  document.dispatchEvent(
317
567
  new CustomEvent(`mrsf:${action}`, { detail, bubbles: true })
318
568
  );
319
- });
569
+ }
570
+ // ── Selection handling ──────────────────────────────────
571
+ handleSelectionChange() {
572
+ const sel = document.getSelection();
573
+ if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
574
+ this.hideFloatingAddButton();
575
+ return;
576
+ }
577
+ const text = sel.toString().trim();
578
+ if (!text) {
579
+ this.hideFloatingAddButton();
580
+ return;
581
+ }
582
+ const range = sel.getRangeAt(0);
583
+ const rect = range.getBoundingClientRect();
584
+ if (!rect || rect.width === 0 && rect.height === 0) {
585
+ this.hideFloatingAddButton();
586
+ return;
587
+ }
588
+ const startAnchor = range.startContainer.parentElement?.closest(
589
+ "[data-mrsf-line]"
590
+ );
591
+ const endAnchor = range.endContainer.parentElement?.closest(
592
+ "[data-mrsf-line]"
593
+ );
594
+ const startLineStr = startAnchor?.dataset.mrsfStartLine ?? startAnchor?.dataset.mrsfLine;
595
+ const endLineStr = endAnchor?.dataset.mrsfEndLine ?? endAnchor?.dataset.mrsfLine ?? startLineStr;
596
+ const startLine = startLineStr ? parseInt(startLineStr, 10) : null;
597
+ const endLine = endLineStr ? parseInt(endLineStr, 10) : startLine;
598
+ const startColumn = range.startContainer.nodeType === Node.TEXT_NODE ? range.startOffset : null;
599
+ const endColumn = range.endContainer.nodeType === Node.TEXT_NODE ? range.endOffset : null;
600
+ this.showFloatingAddButton(startLine, endLine, startColumn, endColumn, rect, text);
601
+ }
602
+ ensureFloatingAddButton() {
603
+ if (this.floatingAddButton) return this.floatingAddButton;
604
+ const btn = document.createElement("button");
605
+ btn.textContent = "Add comment";
606
+ btn.className = "mrsf-add-inline-button";
607
+ btn.dataset.mrsfAction = "add";
608
+ btn.style.display = "none";
609
+ btn.style.position = "absolute";
610
+ btn.style.zIndex = "1200";
611
+ document.body.appendChild(btn);
612
+ this.floatingAddButton = btn;
613
+ return btn;
614
+ }
615
+ hideFloatingAddButton() {
616
+ if (!this.floatingAddButton) return;
617
+ this.floatingAddButton.style.display = "none";
618
+ this.floatingAddButton.dataset.mrsfLine = "";
619
+ this.floatingAddButton.dataset.mrsfStartLine = "";
620
+ this.floatingAddButton.dataset.mrsfEndLine = "";
621
+ this.lastSelectionText = null;
622
+ }
623
+ showFloatingAddButton(startLine, endLine, startColumn, endColumn, rect, selectionText) {
624
+ const btn = this.ensureFloatingAddButton();
625
+ if (startLine != null) {
626
+ btn.dataset.mrsfLine = String(startLine);
627
+ btn.dataset.mrsfStartLine = String(startLine);
628
+ btn.dataset.mrsfEndLine = String(endLine ?? startLine);
629
+ } else {
630
+ delete btn.dataset.mrsfLine;
631
+ delete btn.dataset.mrsfStartLine;
632
+ delete btn.dataset.mrsfEndLine;
633
+ }
634
+ if (startColumn != null) {
635
+ btn.dataset.mrsfStartColumn = String(startColumn);
636
+ } else {
637
+ delete btn.dataset.mrsfStartColumn;
638
+ }
639
+ if (endColumn != null) {
640
+ btn.dataset.mrsfEndColumn = String(endColumn);
641
+ } else {
642
+ delete btn.dataset.mrsfEndColumn;
643
+ }
644
+ this.lastSelectionText = selectionText;
645
+ const margin = 6;
646
+ btn.style.visibility = "hidden";
647
+ btn.style.display = "block";
648
+ const height = btn.offsetHeight || 0;
649
+ const top = window.scrollY + rect.top - height - margin;
650
+ const left = window.scrollX + rect.left;
651
+ btn.style.top = `${Math.max(0, top)}px`;
652
+ btn.style.left = `${Math.max(0, left)}px`;
653
+ btn.style.visibility = "visible";
654
+ }
655
+ // ── Dialog: form (add/edit/reply) ───────────────────────
656
+ injectStyles() {
657
+ if (this.styleInjected) return;
658
+ const css = `
659
+ .mrsf-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 2000; display: flex; align-items: center; justify-content: center; }
660
+ .mrsf-dialog { background: var(--mrsf-dialog-bg, var(--vp-c-bg, #1e1e1e)); color: var(--mrsf-dialog-fg, var(--vp-c-text-1, #f3f3f3)); border: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #444)); border-radius: 8px; width: min(520px, 90vw); box-shadow: 0 10px 40px rgba(0,0,0,0.35); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
661
+ .mrsf-dialog header { padding: 12px 16px; font-weight: 600; border-bottom: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #333)); }
662
+ .mrsf-dialog form { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
663
+ .mrsf-field { display: flex; flex-direction: column; gap: 6px; }
664
+ .mrsf-field label { font-size: 13px; color: var(--mrsf-dialog-muted, var(--vp-c-text-2, #ccc)); }
665
+ .mrsf-field input, .mrsf-field select, .mrsf-field textarea { background: var(--mrsf-field-bg, var(--vp-c-bg-soft, #252526)); color: var(--mrsf-dialog-fg, var(--vp-c-text-1, #f3f3f3)); border: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #3c3c3c)); border-radius: 4px; padding: 6px 8px; font-size: 13px; }
666
+ .mrsf-field textarea { min-height: 100px; resize: vertical; }
667
+ .mrsf-actions-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 0; padding: 12px 16px 16px; border-top: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #333)); }
668
+ .mrsf-btn { padding: 6px 12px; border-radius: 4px; border: 1px solid var(--mrsf-dialog-border, var(--vp-c-divider, #3c3c3c)); background: var(--mrsf-button-bg, var(--vp-c-bg-soft, #2d2d30)); color: var(--mrsf-dialog-fg, var(--vp-c-text-1, #f3f3f3)); cursor: pointer; }
669
+ .mrsf-btn-primary { background: var(--mrsf-button-primary-bg, #0e639c); border-color: var(--mrsf-button-primary-bg, #0e639c); color: #fff; }
670
+ .mrsf-helper { font-size: 12px; color: var(--mrsf-dialog-muted, var(--vp-c-text-2, #aaa)); }
671
+ `;
672
+ const style = document.createElement("style");
673
+ style.textContent = css;
674
+ document.head.appendChild(style);
675
+ this.styleInjected = true;
676
+ }
677
+ closeOverlay() {
678
+ if (this.overlayEl?.parentElement) {
679
+ this.overlayEl.parentElement.removeChild(this.overlayEl);
680
+ }
681
+ this.overlayEl = null;
682
+ }
683
+ openForm(action, detail) {
684
+ if (window.mrsfDisableBuiltinUi) return;
685
+ this.injectStyles();
686
+ this.closeOverlay();
687
+ const selText = detail.selectionText ?? "";
688
+ const line = detail.line ?? detail.start_line ?? null;
689
+ const endLine = detail.end_line ?? detail.line ?? null;
690
+ const startCol = detail.start_column ?? null;
691
+ const endCol = detail.end_column ?? null;
692
+ const overlay = document.createElement("div");
693
+ overlay.className = "mrsf-overlay";
694
+ const dialog = document.createElement("div");
695
+ dialog.className = "mrsf-dialog";
696
+ const header = document.createElement("header");
697
+ header.textContent = action === "add" ? "Add comment" : action === "edit" ? "Edit comment" : "Reply";
698
+ dialog.appendChild(header);
699
+ const form = document.createElement("form");
700
+ const field = (labelText, inputEl, helper) => {
701
+ const wrap = document.createElement("div");
702
+ wrap.className = "mrsf-field";
703
+ const label = document.createElement("label");
704
+ label.textContent = labelText;
705
+ wrap.appendChild(label);
706
+ wrap.appendChild(inputEl);
707
+ if (helper) {
708
+ const h = document.createElement("div");
709
+ h.className = "mrsf-helper";
710
+ h.textContent = helper;
711
+ wrap.appendChild(h);
712
+ }
713
+ form.appendChild(wrap);
714
+ };
715
+ const textArea = document.createElement("textarea");
716
+ textArea.name = "text";
717
+ textArea.required = true;
718
+ field("Comment text", textArea);
719
+ const typeSelect = document.createElement("select");
720
+ typeSelect.name = "type";
721
+ ["", "suggestion", "issue", "question", "accuracy", "style", "clarity"].forEach((t) => {
722
+ const opt = document.createElement("option");
723
+ opt.value = t;
724
+ opt.textContent = t || "(none)";
725
+ typeSelect.appendChild(opt);
726
+ });
727
+ field("Type", typeSelect, "Optional");
728
+ const severitySelect = document.createElement("select");
729
+ severitySelect.name = "severity";
730
+ ["", "low", "medium", "high"].forEach((s) => {
731
+ const opt = document.createElement("option");
732
+ opt.value = s;
733
+ opt.textContent = s || "(none)";
734
+ severitySelect.appendChild(opt);
735
+ });
736
+ field("Severity", severitySelect, "Optional");
737
+ if (selText) {
738
+ const pre = document.createElement("pre");
739
+ pre.textContent = selText;
740
+ pre.style.margin = "0";
741
+ pre.style.whiteSpace = "pre-wrap";
742
+ field("Selected text", pre, "Captured automatically");
743
+ }
744
+ const actions = document.createElement("div");
745
+ actions.className = "mrsf-actions-row";
746
+ const cancelBtn = document.createElement("button");
747
+ cancelBtn.type = "button";
748
+ cancelBtn.className = "mrsf-btn";
749
+ cancelBtn.textContent = "Cancel";
750
+ cancelBtn.addEventListener("click", () => this.closeOverlay());
751
+ actions.appendChild(cancelBtn);
752
+ const submitBtn = document.createElement("button");
753
+ submitBtn.type = "submit";
754
+ submitBtn.className = "mrsf-btn mrsf-btn-primary";
755
+ submitBtn.textContent = action === "add" ? "Add" : action === "reply" ? "Reply" : "Save";
756
+ actions.appendChild(submitBtn);
757
+ form.appendChild(actions);
758
+ form.addEventListener("submit", (ev) => {
759
+ ev.preventDefault();
760
+ const detailOut = {
761
+ action,
762
+ commentId: detail.commentId,
763
+ text: textArea.value.trim(),
764
+ type: typeSelect.value || null,
765
+ severity: severitySelect.value || null,
766
+ line,
767
+ end_line: endLine,
768
+ start_column: startCol,
769
+ end_column: endCol,
770
+ selection_text: selText || null
771
+ };
772
+ document.dispatchEvent(new CustomEvent("mrsf:submit", { detail: detailOut, bubbles: true }));
773
+ this.closeOverlay();
774
+ });
775
+ dialog.appendChild(form);
776
+ overlay.appendChild(dialog);
777
+ overlay.addEventListener("click", (e) => {
778
+ if (e.target === overlay) this.closeOverlay();
779
+ });
780
+ document.body.appendChild(overlay);
781
+ this.overlayEl = overlay;
782
+ }
783
+ // ── Dialog: confirm (resolve / unresolve / delete) ──────
784
+ openConfirm(action, detail) {
785
+ if (window.mrsfDisableBuiltinUi) return;
786
+ this.injectStyles();
787
+ this.closeOverlay();
788
+ const overlay = document.createElement("div");
789
+ overlay.className = "mrsf-overlay";
790
+ const dialog = document.createElement("div");
791
+ dialog.className = "mrsf-dialog";
792
+ const header = document.createElement("header");
793
+ header.textContent = action === "delete" ? "Delete comment" : "Change status";
794
+ dialog.appendChild(header);
795
+ const body = document.createElement("div");
796
+ body.style.padding = "16px";
797
+ body.textContent = action === "delete" ? "Delete this comment?" : action === "resolve" ? "Mark this comment as resolved?" : "Mark this comment as unresolved?";
798
+ dialog.appendChild(body);
799
+ const actions = document.createElement("div");
800
+ actions.className = "mrsf-actions-row";
801
+ const cancelBtn = document.createElement("button");
802
+ cancelBtn.type = "button";
803
+ cancelBtn.className = "mrsf-btn";
804
+ cancelBtn.textContent = "Cancel";
805
+ cancelBtn.addEventListener("click", () => this.closeOverlay());
806
+ actions.appendChild(cancelBtn);
807
+ const confirmBtn = document.createElement("button");
808
+ confirmBtn.type = "button";
809
+ confirmBtn.className = "mrsf-btn mrsf-btn-primary";
810
+ confirmBtn.textContent = action === "delete" ? "Delete" : "Confirm";
811
+ confirmBtn.addEventListener("click", () => {
812
+ const detailOut = {
813
+ action,
814
+ commentId: detail.commentId,
815
+ text: "",
816
+ type: null,
817
+ severity: null,
818
+ line: detail.line,
819
+ end_line: detail.end_line ?? detail.line ?? null,
820
+ start_column: detail.start_column ?? null,
821
+ end_column: detail.end_column ?? null,
822
+ selection_text: detail.selectionText ?? null
823
+ };
824
+ document.dispatchEvent(new CustomEvent("mrsf:submit", { detail: detailOut, bubbles: true }));
825
+ this.closeOverlay();
826
+ });
827
+ actions.appendChild(confirmBtn);
828
+ dialog.appendChild(actions);
829
+ overlay.appendChild(dialog);
830
+ overlay.addEventListener("click", (e) => {
831
+ if (e.target === overlay) this.closeOverlay();
832
+ });
833
+ document.body.appendChild(overlay);
834
+ this.overlayEl = overlay;
835
+ }
836
+ };
837
+ var autoInitDone = false;
838
+ function autoInit() {
839
+ if (autoInitDone) return;
840
+ autoInitDone = true;
841
+ const containers = document.querySelectorAll("[data-mrsf-controller]");
842
+ for (const container of containers) {
843
+ const pos = container.dataset.mrsfGutterPosition;
844
+ const interactive = container.dataset.mrsfInteractive === "true";
845
+ new MrsfController(container, { gutterPosition: pos ?? "right", interactive });
846
+ }
847
+ }
848
+ if (typeof document !== "undefined") {
849
+ if (document.readyState === "loading") {
850
+ document.addEventListener("DOMContentLoaded", autoInit);
851
+ } else {
852
+ autoInit();
853
+ }
320
854
  }
321
- init();
855
+ export {
856
+ MrsfController,
857
+ autoInit
858
+ };
322
859
  //# sourceMappingURL=controller.js.map