rails-markup 1.4.3 → 1.4.4

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.
@@ -0,0 +1,675 @@
1
+ Object.assign(RailsMarkupToolbar, {
2
+ _showPopup(x, y) {
3
+ const popup = document.getElementById("rm-popup");
4
+ // Clean up previous drawing elements
5
+ const oldContainer = popup.querySelector(".rm-drawing-container");
6
+ if (oldContainer) oldContainer.remove();
7
+ const oldTools = popup.querySelector("[data-draw]");
8
+ if (oldTools) { const p = oldTools.parentElement; if (p && !p.classList.contains("rm-popup-actions")) p.remove(); }
9
+ this.drawingCanvas = null;
10
+ this.drawingCtx = null;
11
+ this.drawingHistory = [];
12
+ this.drawingMode = null;
13
+ this._screenshotImg = null;
14
+
15
+ popup.style.display = "block";
16
+ popup.style.opacity = "0";
17
+ popup.style.left = "-9999px";
18
+ popup.style.top = "-9999px";
19
+ popup.style.width = this._currentScreenshot ? "480px" : "360px";
20
+ const pw = popup.offsetWidth || (this._currentScreenshot ? 480 : 360);
21
+ const ph = popup.offsetHeight || 300;
22
+ let left = Math.min(x + 10, window.innerWidth - pw - 20);
23
+ let top = Math.min(y + 10, window.innerHeight - ph - 20);
24
+ left = Math.max(10, left);
25
+ top = Math.max(10, top);
26
+ popup.style.left = left + "px";
27
+ popup.style.top = top + "px";
28
+ requestAnimationFrame(() => {
29
+ popup.style.transition = "opacity 0.2s ease";
30
+ popup.style.opacity = "1";
31
+ });
32
+ document.getElementById("rm-popup-el").textContent = this._currentElement.selector;
33
+ document.getElementById("rm-popup-text").textContent = this.selectedText
34
+ ? '"' + this.selectedText.slice(0, 60) + '"'
35
+ : this._currentElement.nearbyText.slice(0, 60);
36
+ const input = document.getElementById("rm-popup-input");
37
+ input.value = "";
38
+ this._setMenuValue(document.getElementById("rm-intent-select"), "change", false);
39
+ this._setMenuValue(document.getElementById("rm-severity-select"), "suggestion", false);
40
+ document.getElementById("rm-char-count").textContent = "";
41
+ document.getElementById("rm-submit-label").textContent = "Add";
42
+ this._closeAllMenus();
43
+
44
+ // Show screenshot preview with drawing tools
45
+ if (this._currentScreenshot) {
46
+ this._initDrawing(this._currentScreenshot);
47
+ }
48
+
49
+ setTimeout(() => input.focus(), 50);
50
+ },
51
+ _closePopup() {
52
+ this._closeAllMenus();
53
+ const popup = document.getElementById("rm-popup");
54
+ popup.style.transition = "opacity 0.15s ease";
55
+ popup.style.opacity = "0";
56
+ setTimeout(() => { popup.style.display = "none"; popup.style.transition = ""; popup.style.opacity = ""; popup.style.width = ""; }, 150);
57
+ document.getElementById("rm-popup-input").value = "";
58
+ this.selectedText = null;
59
+ this._currentElement = null;
60
+ this._currentScreenshot = null;
61
+ this.drawingCanvas = null;
62
+ this.drawingCtx = null;
63
+ this.drawingHistory = [];
64
+ this.drawingMode = null;
65
+ this._screenshotImg = null;
66
+ this.editingId = null;
67
+ document.getElementById("rm-submit-label").textContent = "Add";
68
+ },
69
+ _updateCharCount() {
70
+ const len = document.getElementById("rm-popup-input").value.length;
71
+ const el = document.getElementById("rm-char-count");
72
+ el.textContent = len > 0 ? len : "";
73
+ el.style.color = len > 500 ? "#f87171" : "#d1d5db";
74
+ },
75
+ submitAnnotation(event) {
76
+ if (event) event.preventDefault();
77
+ const comment = document.getElementById("rm-popup-input").value.trim();
78
+ if (!comment) return;
79
+ const intent = document.getElementById("rm-intent-select").value;
80
+ const severity = document.getElementById("rm-severity-select").value;
81
+
82
+ // If drawing canvas exists, merge drawings onto screenshot
83
+ const screenshot = this._mergeDrawing() || this._currentScreenshot;
84
+
85
+ // Edit existing annotation
86
+ if (this.editingId) {
87
+ const existing = this.annotations.find(a => a.id === this.editingId);
88
+ if (existing) {
89
+ const dirtyFields = [];
90
+ if (existing.comment !== comment) dirtyFields.push("content");
91
+ if (existing.intent !== intent) dirtyFields.push("intent");
92
+ if (existing.severity !== severity) dirtyFields.push("severity");
93
+ if (screenshot && existing.screenshot !== screenshot) dirtyFields.push("metadata");
94
+ const committed = dirtyFields.length === 0 || this._persistLocalMutation("upsert", dirtyFields, () => {
95
+ existing.comment = comment;
96
+ existing.intent = intent;
97
+ existing.severity = severity;
98
+ if (screenshot) existing.screenshot = screenshot;
99
+ return existing;
100
+ });
101
+ if (!committed) return;
102
+ this._rebuildList();
103
+ this._closePopup();
104
+ return;
105
+ }
106
+ }
107
+
108
+ // New annotation
109
+ const annotation = {
110
+ id: this.nextId,
111
+ clientId: this._newClientId(),
112
+ serverId: null,
113
+ serverRevision: 0,
114
+ syncState: "pending",
115
+ serverUpdatedAt: null,
116
+ dirtyFields: [],
117
+ revision: 0,
118
+ comment, intent, severity,
119
+ element: this._currentElement,
120
+ selectedText: this.selectedText || null,
121
+ screenshot: screenshot || null,
122
+ url: window.location.href,
123
+ pathname: this._pageUrl(),
124
+ pageUrl: this._pageUrl(),
125
+ timestamp: new Date().toISOString(),
126
+ status: "pending",
127
+ thread: []
128
+ };
129
+
130
+ const committed = this._persistLocalMutation("upsert", this._browserCreateFields(), () => {
131
+ this.nextId += 1;
132
+ this.annotations.push(annotation);
133
+ return annotation;
134
+ });
135
+ if (!committed) return;
136
+ this._renderPin(annotation);
137
+ // Rebuild (not append) so a new card honors the active panel filter —
138
+ // e.g. a pending annotation must not show while "Resolved" is selected.
139
+ this._rebuildList();
140
+ this._updateCount();
141
+ this._closePopup();
142
+ },
143
+ _filterAnnotations(filter) {
144
+ this.activeFilter = filter;
145
+ this._updateFilterChips();
146
+ this._rebuildList();
147
+ },
148
+ _updateFilterChips() {
149
+ const chips = document.querySelectorAll("#rm-filter-chips [data-filter]");
150
+ chips.forEach(chip => {
151
+ if (chip.dataset.filter === this.activeFilter) {
152
+ chip.className = "rm-chip rm-chip-active";
153
+ chip.style.background = this._accentBg();
154
+ chip.style.color = "#fff";
155
+ } else {
156
+ chip.className = "rm-chip rm-chip-inactive";
157
+ chip.style.background = "#f9fafb";
158
+ chip.style.color = "#9ca3af";
159
+ }
160
+ });
161
+ },
162
+ _filteredAnnotations() {
163
+ if (this.activeFilter === "all") return this.annotations;
164
+ if (this.activeFilter === "pending") return this.annotations.filter(a => a.status === "pending" || a.status === "acknowledged");
165
+ if (this.activeFilter === "resolved") return this.annotations.filter(a => a.status === "resolved" || a.status === "dismissed");
166
+ return this.annotations;
167
+ },
168
+ _renderCard(annotation) {
169
+ const list = document.getElementById("rm-panel-list");
170
+ const card = document.createElement("div");
171
+ card.className = "rm-card";
172
+ card.dataset.cardId = annotation.id;
173
+
174
+ const borderColor = annotation.status === "resolved" ? "#10b981" : annotation.status === "dismissed" ? "#d1d5db" : this._accentBg();
175
+ card.style.borderLeftColor = borderColor;
176
+
177
+ const dotColor = { pending: "#3b82f6", acknowledged: "#f59e0b", resolved: "#10b981", dismissed: "#d1d5db" }[annotation.status] || "#3b82f6";
178
+ const intentColors = { fix: { bg: "#fef2f2", text: "#dc2626" }, change: { bg: "#eff6ff", text: "#2563eb" }, question: { bg: "#f5f3ff", text: "#7c3aed" }, approve: { bg: "#ecfdf5", text: "#059669" } };
179
+ const ic = intentColors[annotation.intent] || intentColors.change;
180
+
181
+ let threadHtml = "";
182
+ const thread = annotation.thread || [];
183
+ if (thread.length > 0) {
184
+ const last = thread[thread.length - 1];
185
+ threadHtml = `<div class="rm-card-thread" style="border-left-color:${this._accentBg()}"><span class="rm-card-thread-role">${this._esc(last.role || "agent")}</span><div style="margin-top:2px">${this._esc(last.message)}</div></div>`;
186
+ }
187
+
188
+ card.innerHTML = `
189
+ <div class="rm-card-top">
190
+ <span class="rm-card-dot" style="background:${dotColor}"></span>
191
+ <span class="rm-card-id">#${annotation.id}</span>
192
+ <span class="rm-card-badge" style="background:${ic.bg};color:${ic.text}">${annotation.intent}</span>
193
+ ${annotation.severity !== "suggestion" ? '<span class="rm-card-badge" style="background:#fff7ed;color:#9a3412">' + annotation.severity + '</span>' : ''}
194
+ <span style="margin-left:auto;display:flex;gap:2px;align-items:center;">
195
+ ${this._menuMarkup({
196
+ statusId: annotation.id,
197
+ label: "Change status",
198
+ value: annotation.status,
199
+ options: this._statusOptions(),
200
+ compact: true
201
+ })}
202
+ <button data-edit-id="${annotation.id}" title="Edit" style="padding:2px 4px;background:none;border:none;cursor:pointer;color:#d1d5db;border-radius:4px;display:flex;align-items:center;" onmouseover="this.style.color='#6b7280'" onmouseout="this.style.color='#d1d5db'">
203
+ <svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;"><path d="M17 3a2.85 2.85 0 114 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
204
+ </button>
205
+ <button data-delete-id="${annotation.id}" title="Delete" style="padding:2px 4px;background:none;border:none;cursor:pointer;color:#d1d5db;border-radius:4px;display:flex;align-items:center;" onmouseover="this.style.color='#ef4444'" onmouseout="this.style.color='#d1d5db'">
206
+ <svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
207
+ </button>
208
+ </span>
209
+ </div>
210
+ <div class="rm-card-body">${this._esc(annotation.comment)}</div>
211
+ <div class="rm-card-path">${this._esc(annotation.pathname || "")} &rsaquo; ${this._esc(annotation.element?.selector || "")}</div>
212
+ ${annotation.selectedText ? '<div class="rm-card-path" style="font-style:italic">"' + this._esc(annotation.selectedText.slice(0, 60)) + '"</div>' : ''}
213
+ ${threadHtml}
214
+ `;
215
+
216
+ list.appendChild(card);
217
+ },
218
+ _rebuildList() {
219
+ const list = document.getElementById("rm-panel-list");
220
+ if (!list) return;
221
+ this._closeAllMenus();
222
+ list.innerHTML = "";
223
+ this._renderStorageError(list);
224
+ this._renderFailedSync(list);
225
+ const filtered = this._filteredAnnotations();
226
+ if (filtered.length === 0) {
227
+ const empty = document.createElement("div");
228
+ empty.className = "rm-empty";
229
+ empty.innerHTML = '<div class="rm-empty-icon">&#9670;</div><div class="rm-empty-text">No annotations yet</div>';
230
+ list.appendChild(empty);
231
+ return;
232
+ }
233
+ filtered.forEach(a => this._renderCard(a));
234
+ },
235
+ _renderStorageError(list) {
236
+ if (!this._storageError) return;
237
+ const error = document.createElement("div");
238
+ error.className = "rm-storage-error";
239
+ error.setAttribute("role", "alert");
240
+ error.style.cssText = "padding:8px;margin-bottom:8px;border:1px solid #fecaca;border-radius:8px;background:#fef2f2;color:#991b1b;font-size:11px;";
241
+ error.textContent = this._storageError;
242
+ list.appendChild(error);
243
+ },
244
+ _renderFailedSync(list) {
245
+ const failed = Object.values(this.outbox || {}).filter(entry => entry?.syncState === "failed");
246
+ if (failed.length === 0) return;
247
+ const section = document.createElement("div");
248
+ section.className = "rm-failed-sync";
249
+ section.setAttribute("role", "status");
250
+ failed.forEach(entry => {
251
+ const item = document.createElement("div");
252
+ item.style.cssText = "display:flex;align-items:center;gap:8px;padding:8px;margin-bottom:8px;border:1px solid #fecaca;border-radius:8px;background:#fef2f2;color:#991b1b;font-size:11px;";
253
+ const label = entry.type === "delete" ? "Delete failed" : "Sync failed";
254
+ item.innerHTML = `<span style="flex:1">${label}</span><button type="button" data-retry-client-id="${this._esc(entry.clientId)}" style="border:1px solid #fca5a5;border-radius:6px;background:#fff;padding:3px 7px;color:#991b1b;cursor:pointer">Retry</button>`;
255
+ section.appendChild(item);
256
+ });
257
+ list.appendChild(section);
258
+ },
259
+ _editAnnotation(id) {
260
+ const annotation = this.annotations.find(a => a.id === id);
261
+ if (!annotation) return;
262
+ this.editingId = id;
263
+ this._currentElement = annotation.element;
264
+ this.selectedText = annotation.selectedText;
265
+ this._currentScreenshot = annotation.screenshot || null;
266
+
267
+ // Pre-fill popup fields
268
+ document.getElementById("rm-popup-el").textContent = annotation.element?.selector || "";
269
+ document.getElementById("rm-popup-text").textContent = annotation.selectedText
270
+ ? '"' + annotation.selectedText.slice(0, 60) + '"'
271
+ : (annotation.element?.nearbyText || "").slice(0, 60);
272
+ document.getElementById("rm-popup-input").value = annotation.comment;
273
+ this._setMenuValue(document.getElementById("rm-intent-select"), annotation.intent, false);
274
+ this._setMenuValue(document.getElementById("rm-severity-select"), annotation.severity, false);
275
+ document.getElementById("rm-submit-label").textContent = "Save";
276
+ this._updateCharCount();
277
+ this._closeAllMenus();
278
+
279
+ // Clean up previous drawing elements
280
+ const popup = document.getElementById("rm-popup");
281
+ const oldContainer = popup.querySelector(".rm-drawing-container");
282
+ if (oldContainer) oldContainer.remove();
283
+ const oldTools = popup.querySelector("[data-draw]");
284
+ if (oldTools) { const p = oldTools.parentElement; if (p && !p.classList.contains("rm-popup-actions")) p.remove(); }
285
+ this.drawingCanvas = null;
286
+ this.drawingCtx = null;
287
+ this.drawingHistory = [];
288
+ this.drawingMode = null;
289
+ this._screenshotImg = null;
290
+
291
+ // Position popup centered on screen
292
+ popup.style.display = "block";
293
+ popup.style.width = this._currentScreenshot ? "480px" : "360px";
294
+ const pw = popup.offsetWidth || 360;
295
+ const ph = popup.offsetHeight || 300;
296
+ popup.style.left = Math.max(10, Math.round((window.innerWidth - pw) / 2)) + "px";
297
+ popup.style.top = Math.max(10, Math.round((window.innerHeight - ph) / 2)) + "px";
298
+ popup.style.opacity = "1";
299
+
300
+ if (this._currentScreenshot) {
301
+ this._initDrawing(this._currentScreenshot);
302
+ }
303
+
304
+ setTimeout(() => document.getElementById("rm-popup-input").focus(), 50);
305
+ },
306
+ _changeStatus(id, newStatus) {
307
+ const annotation = this.annotations.find(a => a.id === id);
308
+ if (!annotation) return;
309
+ const committed = this._persistLocalMutation("upsert", ["status"], () => {
310
+ annotation.status = newStatus;
311
+ return annotation;
312
+ });
313
+ if (!committed) return;
314
+ this._renderPins();
315
+ this._rebuildList();
316
+ const label = newStatus.charAt(0).toUpperCase() + newStatus.slice(1);
317
+ this._showToast(`#${id} marked as ${label}`, newStatus === "resolved" ? "resolved" : "dismissed");
318
+ },
319
+ _deleteAnnotation(id) {
320
+ const idx = this.annotations.findIndex(a => a.id === id);
321
+ if (idx === -1) return;
322
+ const committed = this._persistLocalMutation("delete", [], () => this.annotations.splice(idx, 1)[0]);
323
+ if (!committed) return;
324
+ this._renderPins();
325
+ this._rebuildList();
326
+ this._updateCount();
327
+ },
328
+ _updateCount() {
329
+ const count = this.annotations.length;
330
+ const countEl = document.getElementById("rm-panel-count");
331
+ if (countEl) countEl.textContent = count;
332
+ const badge = document.getElementById("rm-fab-badge");
333
+ if (badge) {
334
+ if (count > 0) { badge.textContent = count; badge.style.display = "flex"; }
335
+ else { badge.style.display = "none"; }
336
+ }
337
+ const toggle = document.getElementById("rm-panel-toggle");
338
+ if (toggle) toggle.style.display = "flex";
339
+ },
340
+ _renderPin(annotation) {
341
+ if (!annotation.element?.boundingBox) return;
342
+ const { top, left, width } = annotation.element.boundingBox;
343
+ const container = document.getElementById("rm-pins-container");
344
+ if (!container) return;
345
+ const isResolved = annotation.status === "resolved" || annotation.status === "dismissed";
346
+ const pin = document.createElement("div");
347
+ pin.className = "rm-pin" + (isResolved ? "" : " rm-pin-active");
348
+ pin.dataset.pinId = annotation.id;
349
+ pin.style.top = (top - 10) + "px";
350
+ pin.style.left = (left + width - 10) + "px";
351
+ pin.style.background = isResolved ? "#d1d5db" : this._accentBg();
352
+ if (isResolved) pin.style.opacity = "0.6";
353
+ pin.textContent = annotation.id;
354
+ pin.title = "#" + annotation.id + ": " + annotation.comment.slice(0, 50);
355
+ container.appendChild(pin);
356
+ },
357
+ _renderPins() {
358
+ const container = document.getElementById("rm-pins-container");
359
+ if (container) container.innerHTML = "";
360
+ // Only render pins for annotations on the current page
361
+ const currentPath = this._pageUrl();
362
+ this.annotations
363
+ .filter(a => (a.pageUrl || a.pathname) === currentPath)
364
+ .forEach(a => this._renderPin(a));
365
+ },
366
+ _findElement(annotation) {
367
+ if (!annotation.element) return null;
368
+ const { cssPath, selector } = annotation.element;
369
+ if (cssPath) { try { const el = document.querySelector(cssPath); if (el) return el; } catch {} }
370
+ if (selector) { try { const el = document.querySelector(selector); if (el) return el; } catch {} }
371
+ return null;
372
+ },
373
+ _repositionPins() {
374
+ this.annotations.forEach(annotation => {
375
+ const el = this._findElement(annotation);
376
+ if (!el) return;
377
+ const rect = el.getBoundingClientRect();
378
+ annotation.element.boundingBox = { top: Math.round(rect.top + window.scrollY), left: Math.round(rect.left + window.scrollX), width: Math.round(rect.width), height: Math.round(rect.height) };
379
+ const pin = document.querySelector('[data-pin-id="' + annotation.id + '"]');
380
+ if (pin) { pin.style.top = (annotation.element.boundingBox.top - 10) + "px"; pin.style.left = (annotation.element.boundingBox.left + annotation.element.boundingBox.width - 10) + "px"; }
381
+ });
382
+ },
383
+ _debouncedRepositionPins(delay = 250) {
384
+ let timer = null;
385
+ return () => { if (timer) clearTimeout(timer); timer = setTimeout(() => this._repositionPins(), delay); };
386
+ },
387
+ _removeHighlight() {
388
+ if (this.hoveredElement) {
389
+ this.hoveredElement.style.outline = this.hoveredElement.dataset.rmOrigOutline || "";
390
+ this.hoveredElement.style.outlineOffset = "";
391
+ delete this.hoveredElement.dataset.rmOrigOutline;
392
+ this.hoveredElement = null;
393
+ }
394
+ },
395
+ _isToolbar(el) {
396
+ const root = document.getElementById("rm-toolbar-root");
397
+ return root && root.contains(el);
398
+ },
399
+ _showToast(message, type) {
400
+ const container = document.getElementById("rm-toast-container");
401
+ if (!container) return;
402
+ const toast = document.createElement("div");
403
+ toast.className = "rm-toast";
404
+ const colors = { resolved: { bg: "#ecfdf5", border: "#a7f3d0", text: "#065f46" }, dismissed: { bg: "#f3f4f6", border: "#e5e7eb", text: "#6b7280" } };
405
+ const c = colors[type] || { bg: this._accentLight(), border: this._accentBg(), text: this._accentText() };
406
+ toast.style.background = c.bg;
407
+ toast.style.borderColor = c.border;
408
+ toast.style.color = c.text;
409
+ toast.textContent = message;
410
+ container.appendChild(toast);
411
+ setTimeout(() => { toast.style.animation = "rm-toast-out 0.3s ease forwards"; setTimeout(() => toast.remove(), 300); }, 4000);
412
+ },
413
+ _updateStatus() {
414
+ const dot = document.getElementById("rm-status-dot");
415
+ const text = document.getElementById("rm-status-text");
416
+ if (dot) {
417
+ dot.style.background = this.serverOnline ? "#4ade80" : "#d1d5db";
418
+ dot.style.boxShadow = this.serverOnline ? "0 0 0 2px rgba(74,222,128,0.2)" : "";
419
+ }
420
+ if (text) text.textContent = this.serverOnline ? "Connected" : (this._syncUnavailable || "Offline");
421
+ },
422
+ async _captureElement(element) {
423
+ try {
424
+ const rect = element.getBoundingClientRect();
425
+ const width = Math.min(Math.round(rect.width), 800);
426
+ const height = Math.min(Math.round(rect.height), 600);
427
+ if (width < 10 || height < 10) return null;
428
+
429
+ const clone = element.cloneNode(true);
430
+ // Strip scripts and event handlers
431
+ clone.querySelectorAll("script").forEach(s => s.remove());
432
+ // Remove cross-origin images to avoid tainting the canvas
433
+ const origin = location.origin;
434
+ clone.querySelectorAll("img").forEach(img => {
435
+ try { if (img.src && !img.src.startsWith(origin) && !img.src.startsWith("data:")) img.removeAttribute("src"); } catch {}
436
+ });
437
+
438
+ const svgNS = "http://www.w3.org/2000/svg";
439
+ const svg = `<svg xmlns="${svgNS}" width="${width}" height="${height}">
440
+ <foreignObject width="100%" height="100%">
441
+ <div xmlns="http://www.w3.org/1999/xhtml" style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:14px;">${clone.outerHTML}</div>
442
+ </foreignObject>
443
+ </svg>`;
444
+
445
+ const canvas = document.createElement("canvas");
446
+ const scale = Math.min(window.devicePixelRatio || 1, 2);
447
+ canvas.width = width * scale;
448
+ canvas.height = height * scale;
449
+ const ctx = canvas.getContext("2d");
450
+ ctx.scale(scale, scale);
451
+
452
+ const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
453
+ const url = URL.createObjectURL(blob);
454
+
455
+ return new Promise((resolve) => {
456
+ const img = new Image();
457
+ img.onload = () => {
458
+ ctx.drawImage(img, 0, 0, width, height);
459
+ URL.revokeObjectURL(url);
460
+ try { resolve(canvas.toDataURL("image/png", 0.7)); } catch { resolve(null); }
461
+ };
462
+ img.onerror = () => {
463
+ URL.revokeObjectURL(url);
464
+ resolve(null);
465
+ };
466
+ img.src = url;
467
+ });
468
+ } catch {
469
+ return null;
470
+ }
471
+ },
472
+ _initDrawing(screenshotDataUrl) {
473
+ const popup = document.getElementById("rm-popup");
474
+ let container = popup.querySelector(".rm-drawing-container");
475
+ if (container) container.remove();
476
+
477
+ container = document.createElement("div");
478
+ container.className = "rm-drawing-container";
479
+ container.style.cssText = "position:relative;margin-bottom:12px;border-radius:8px;overflow:hidden;border:1px solid #e5e7eb;";
480
+
481
+ const img = new Image();
482
+ img.src = screenshotDataUrl;
483
+ img.style.cssText = "display:block;max-width:100%;border-radius:8px;";
484
+ container.appendChild(img);
485
+
486
+ const canvas = document.createElement("canvas");
487
+ canvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair;";
488
+ container.appendChild(canvas);
489
+
490
+ // Tool buttons
491
+ const tools = document.createElement("div");
492
+ tools.style.cssText = "display:flex;gap:4px;padding:6px 0;";
493
+ tools.innerHTML = `
494
+ <button type="button" data-draw="arrow" class="rm-pill" style="font-size:11px;padding:4px 10px;">Arrow</button>
495
+ <button type="button" data-draw="rect" class="rm-pill" style="font-size:11px;padding:4px 10px;">Rect</button>
496
+ <button type="button" data-draw="highlight" class="rm-pill" style="font-size:11px;padding:4px 10px;">Highlight</button>
497
+ <button type="button" data-draw="undo" class="rm-pill" style="font-size:11px;padding:4px 10px;margin-left:auto;">Undo</button>
498
+ `;
499
+
500
+ const textarea = popup.querySelector("textarea");
501
+ popup.insertBefore(tools, textarea);
502
+ popup.insertBefore(container, tools);
503
+
504
+ img.onload = () => {
505
+ canvas.width = img.naturalWidth || img.offsetWidth;
506
+ canvas.height = img.naturalHeight || img.offsetHeight;
507
+ this.drawingCanvas = canvas;
508
+ this.drawingCtx = canvas.getContext("2d");
509
+ this._screenshotImg = img;
510
+ this.drawingHistory = [];
511
+ this.drawingMode = null;
512
+ this._bindDrawingEvents(canvas, tools);
513
+ };
514
+ },
515
+ _bindDrawingEvents(canvas, tools) {
516
+ const self = this;
517
+ let isDrawing = false;
518
+ let points = [];
519
+
520
+ tools.addEventListener("click", (e) => {
521
+ const btn = e.target.closest("[data-draw]");
522
+ if (!btn) return;
523
+ const mode = btn.dataset.draw;
524
+ if (mode === "undo") {
525
+ self._undoDrawing();
526
+ return;
527
+ }
528
+ self.drawingMode = mode;
529
+ tools.querySelectorAll("[data-draw]").forEach(b => {
530
+ b.style.background = b.dataset.draw === mode ? self._accentBg() : "#fff";
531
+ b.style.color = b.dataset.draw === mode ? "#fff" : "#374151";
532
+ });
533
+ });
534
+
535
+ const getPos = (e) => {
536
+ const rect = canvas.getBoundingClientRect();
537
+ return {
538
+ x: (e.clientX - rect.left) * (canvas.width / rect.width),
539
+ y: (e.clientY - rect.top) * (canvas.height / rect.height)
540
+ };
541
+ };
542
+
543
+ canvas.addEventListener("mousedown", (e) => {
544
+ if (!self.drawingMode) return;
545
+ e.stopPropagation();
546
+ isDrawing = true;
547
+ self.drawingStart = getPos(e);
548
+ points = [self.drawingStart];
549
+ });
550
+
551
+ canvas.addEventListener("mousemove", (e) => {
552
+ if (!isDrawing || !self.drawingMode) return;
553
+ e.stopPropagation();
554
+ const pos = getPos(e);
555
+ if (self.drawingMode === "highlight") {
556
+ points.push(pos);
557
+ self._redrawCanvas();
558
+ self._drawHighlightPath(points);
559
+ } else {
560
+ self._redrawCanvas();
561
+ self._drawShape(self.drawingMode, self.drawingStart, pos);
562
+ }
563
+ });
564
+
565
+ canvas.addEventListener("mouseup", (e) => {
566
+ if (!isDrawing || !self.drawingMode) return;
567
+ e.stopPropagation();
568
+ isDrawing = false;
569
+ const end = getPos(e);
570
+ if (self.drawingMode === "highlight") {
571
+ self.drawingHistory.push({ type: "highlight", points: [...points] });
572
+ } else {
573
+ self.drawingHistory.push({ type: self.drawingMode, start: self.drawingStart, end: end });
574
+ }
575
+ self._redrawCanvas();
576
+ points = [];
577
+ });
578
+ },
579
+ _drawShape(type, start, end) {
580
+ const ctx = this.drawingCtx;
581
+ if (!ctx) return;
582
+ ctx.lineWidth = 3;
583
+
584
+ if (type === "arrow") {
585
+ ctx.strokeStyle = "#ef4444";
586
+ ctx.beginPath();
587
+ ctx.moveTo(start.x, start.y);
588
+ ctx.lineTo(end.x, end.y);
589
+ ctx.stroke();
590
+ // Arrowhead
591
+ const angle = Math.atan2(end.y - start.y, end.x - start.x);
592
+ const headLen = 12;
593
+ ctx.beginPath();
594
+ ctx.moveTo(end.x, end.y);
595
+ ctx.lineTo(end.x - headLen * Math.cos(angle - Math.PI / 6), end.y - headLen * Math.sin(angle - Math.PI / 6));
596
+ ctx.moveTo(end.x, end.y);
597
+ ctx.lineTo(end.x - headLen * Math.cos(angle + Math.PI / 6), end.y - headLen * Math.sin(angle + Math.PI / 6));
598
+ ctx.stroke();
599
+ } else if (type === "rect") {
600
+ ctx.strokeStyle = "#ef4444";
601
+ ctx.strokeRect(start.x, start.y, end.x - start.x, end.y - start.y);
602
+ }
603
+ },
604
+ _drawHighlightPath(points) {
605
+ const ctx = this.drawingCtx;
606
+ if (!ctx || points.length < 2) return;
607
+ ctx.strokeStyle = "rgba(250,204,21,0.5)";
608
+ ctx.lineWidth = 16;
609
+ ctx.lineCap = "round";
610
+ ctx.lineJoin = "round";
611
+ ctx.beginPath();
612
+ ctx.moveTo(points[0].x, points[0].y);
613
+ for (let i = 1; i < points.length; i++) {
614
+ ctx.lineTo(points[i].x, points[i].y);
615
+ }
616
+ ctx.stroke();
617
+ ctx.lineWidth = 3;
618
+ },
619
+ _redrawCanvas() {
620
+ const ctx = this.drawingCtx;
621
+ if (!ctx) return;
622
+ ctx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
623
+ this.drawingHistory.forEach(shape => {
624
+ if (shape.type === "highlight") {
625
+ this._drawHighlightPath(shape.points);
626
+ } else {
627
+ this._drawShape(shape.type, shape.start, shape.end);
628
+ }
629
+ });
630
+ },
631
+ _undoDrawing() {
632
+ if (this.drawingHistory.length === 0) return;
633
+ this.drawingHistory.pop();
634
+ this._redrawCanvas();
635
+ },
636
+ _mergeDrawing() {
637
+ if (!this.drawingCanvas || !this._screenshotImg || this.drawingHistory.length === 0) return null;
638
+ try {
639
+ const merged = document.createElement("canvas");
640
+ merged.width = this.drawingCanvas.width;
641
+ merged.height = this.drawingCanvas.height;
642
+ const ctx = merged.getContext("2d");
643
+ ctx.drawImage(this._screenshotImg, 0, 0, merged.width, merged.height);
644
+ ctx.drawImage(this.drawingCanvas, 0, 0);
645
+ return merged.toDataURL("image/png", 0.7);
646
+ } catch {
647
+ return null;
648
+ }
649
+ },
650
+ _fabIconSize() {
651
+ const map = { "default": 20, compact: 18, slim: 16 };
652
+ return map[this.size] || 20;
653
+ },
654
+ _accentBg() {
655
+ const map = { indigo: "#4f46e5", amber: "#f59e0b", blue: "#2563eb", emerald: "#059669", rose: "#e11d48" };
656
+ return map[this.accent] || map.indigo;
657
+ },
658
+ _accentBgHover() {
659
+ const map = { indigo: "#4338ca", amber: "#d97706", blue: "#1d4ed8", emerald: "#047857", rose: "#be123c" };
660
+ return map[this.accent] || map.indigo;
661
+ },
662
+ _accentLight() {
663
+ const map = { indigo: "#e0e7ff", amber: "#fef3c7", blue: "#dbeafe", emerald: "#d1fae5", rose: "#ffe4e6" };
664
+ return map[this.accent] || map.indigo;
665
+ },
666
+ _accentText() {
667
+ const map = { indigo: "#3730a3", amber: "#92400e", blue: "#1e40af", emerald: "#065f46", rose: "#9f1239" };
668
+ return map[this.accent] || map.indigo;
669
+ },
670
+ _esc(str) {
671
+ const div = document.createElement("div");
672
+ div.textContent = str || "";
673
+ return div.innerHTML;
674
+ }
675
+ });