rails-markup 1.4.6 → 1.5.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.
- checksums.yaml +4 -4
- data/README.md +37 -10
- data/app/assets/javascripts/rails_markup/toolbar/10-styles.js +104 -62
- data/app/assets/javascripts/rails_markup/toolbar/20-dom.js +31 -17
- data/app/assets/javascripts/rails_markup/toolbar/40-events.js +71 -26
- data/app/assets/javascripts/rails_markup/toolbar/50-sync.js +2 -2
- data/app/assets/javascripts/rails_markup/toolbar/60-render.js +128 -69
- data/app/assets/javascripts/rails_markup/toolbar/90-init.js +13 -0
- data/app/views/layouts/rails_markup/application.html.erb +17 -123
- data/app/views/rails_markup/dashboard/_annotation_card.html.erb +4 -8
- data/app/views/rails_markup/dashboard/_annotation_list.html.erb +4 -0
- data/app/views/rails_markup/dashboard/_feedback_modal.html.erb +4 -0
- data/app/views/rails_markup/dashboard/_filters.html.erb +31 -22
- data/app/views/rails_markup/dashboard/_modal_navigation.html.erb +81 -0
- data/app/views/rails_markup/dashboard/_styles.html.erb +118 -36
- data/app/views/rails_markup/dashboard/_toolbar_preferences.html.erb +32 -0
- data/app/views/rails_markup/dashboard/board.html.erb +42 -55
- data/app/views/rails_markup/dashboard/index.html.erb +5 -23
- data/app/views/rails_markup/dashboard/show.html.erb +10 -8
- data/config/routes.rb +1 -1
- data/lib/generators/rails_markup/install_generator.rb +39 -5
- data/lib/rails_markup/cli.rb +7 -7
- data/lib/rails_markup/configuration.rb +3 -4
- data/lib/rails_markup/mcp_server.rb +2 -2
- data/lib/rails_markup/version.rb +1 -1
- metadata +5 -2
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
this.drawingMode = null;
|
|
13
13
|
this._screenshotImg = null;
|
|
14
14
|
|
|
15
|
+
document.getElementById("rm-image-options").open = Boolean(this._currentScreenshot);
|
|
16
|
+
this._resetScreenshotStatus();
|
|
15
17
|
popup.style.display = "block";
|
|
16
18
|
popup.style.opacity = "0";
|
|
17
19
|
popup.style.left = "-9999px";
|
|
@@ -70,7 +72,7 @@
|
|
|
70
72
|
const len = document.getElementById("rm-popup-input").value.length;
|
|
71
73
|
const el = document.getElementById("rm-char-count");
|
|
72
74
|
el.textContent = len > 0 ? len : "";
|
|
73
|
-
el.style.color = len > 500 ? "#
|
|
75
|
+
el.style.color = len > 500 ? "#b91c1c" : "#596579";
|
|
74
76
|
},
|
|
75
77
|
submitAnnotation(event) {
|
|
76
78
|
if (event) event.preventDefault();
|
|
@@ -155,7 +157,7 @@
|
|
|
155
157
|
} else {
|
|
156
158
|
chip.className = "rm-chip rm-chip-inactive";
|
|
157
159
|
chip.style.background = "#f9fafb";
|
|
158
|
-
chip.style.color = "#
|
|
160
|
+
chip.style.color = "#596579";
|
|
159
161
|
}
|
|
160
162
|
});
|
|
161
163
|
},
|
|
@@ -199,10 +201,10 @@
|
|
|
199
201
|
options: this._statusOptions(),
|
|
200
202
|
compact: true
|
|
201
203
|
})}
|
|
202
|
-
<button data-edit-id="${annotation.id}" title="Edit" style="padding:2px 4px;background:none;border:none;cursor:pointer;color:#
|
|
204
|
+
<button data-edit-id="${annotation.id}" title="Edit" style="padding:2px 4px;background:none;border:none;cursor:pointer;color:#64748b;border-radius:4px;display:flex;align-items:center;" onmouseover="this.style.color='#6b7280'" onmouseout="this.style.color='#64748b'">
|
|
203
205
|
<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
206
|
</button>
|
|
205
|
-
<button data-delete-id="${annotation.id}" title="Delete" style="padding:2px 4px;background:none;border:none;cursor:pointer;color:#
|
|
207
|
+
<button data-delete-id="${annotation.id}" title="Delete" style="padding:2px 4px;background:none;border:none;cursor:pointer;color:#64748b;border-radius:4px;display:flex;align-items:center;" onmouseover="this.style.color='#ef4444'" onmouseout="this.style.color='#64748b'">
|
|
206
208
|
<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
209
|
</button>
|
|
208
210
|
</span>
|
|
@@ -346,14 +348,22 @@
|
|
|
346
348
|
const pin = document.createElement("div");
|
|
347
349
|
pin.className = "rm-pin" + (isResolved ? "" : " rm-pin-active");
|
|
348
350
|
pin.dataset.pinId = annotation.id;
|
|
349
|
-
pin
|
|
350
|
-
pin.style.left = (left + width - 10) + "px";
|
|
351
|
+
this._positionPin(pin, { top, left, width });
|
|
351
352
|
pin.style.background = isResolved ? "#d1d5db" : this._accentBg();
|
|
352
353
|
if (isResolved) pin.style.opacity = "0.6";
|
|
353
354
|
pin.textContent = annotation.id;
|
|
354
355
|
pin.title = "#" + annotation.id + ": " + annotation.comment.slice(0, 50);
|
|
355
356
|
container.appendChild(pin);
|
|
356
357
|
},
|
|
358
|
+
_positionPin(pin, { top, left, width }) {
|
|
359
|
+
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
360
|
+
const scrollLeft = window.scrollX || 0;
|
|
361
|
+
// The 20px pin must fit inside the viewport, including its focus/hover space.
|
|
362
|
+
const minLeft = scrollLeft + 4;
|
|
363
|
+
const maxLeft = Math.max(minLeft, scrollLeft + viewportWidth - 24);
|
|
364
|
+
pin.style.left = Math.max(minLeft, Math.min(left + width - 10, maxLeft)) + "px";
|
|
365
|
+
pin.style.top = Math.max(4, top - 10) + "px";
|
|
366
|
+
},
|
|
357
367
|
_renderPins() {
|
|
358
368
|
const container = document.getElementById("rm-pins-container");
|
|
359
369
|
if (container) container.innerHTML = "";
|
|
@@ -377,7 +387,7 @@
|
|
|
377
387
|
const rect = el.getBoundingClientRect();
|
|
378
388
|
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
389
|
const pin = document.querySelector('[data-pin-id="' + annotation.id + '"]');
|
|
380
|
-
if (pin)
|
|
390
|
+
if (pin) this._positionPin(pin, annotation.element.boundingBox);
|
|
381
391
|
});
|
|
382
392
|
},
|
|
383
393
|
_debouncedRepositionPins(delay = 250) {
|
|
@@ -466,8 +476,8 @@
|
|
|
466
476
|
case "accent": return "Accent";
|
|
467
477
|
case "position": return "Position";
|
|
468
478
|
case "size": return "Size";
|
|
469
|
-
case "fabVisible": return "Show
|
|
470
|
-
case "enableScreenshots": return "
|
|
479
|
+
case "fabVisible": return "Show floating toolbar";
|
|
480
|
+
case "enableScreenshots": return "Image attachments";
|
|
471
481
|
default: return setting;
|
|
472
482
|
}
|
|
473
483
|
},
|
|
@@ -496,17 +506,19 @@
|
|
|
496
506
|
_toolbarSettingsRowMarkup(setting) {
|
|
497
507
|
const current = this._toolbarSettingCurrentValue(setting);
|
|
498
508
|
const options = this._toolbarSettingOptions(setting);
|
|
499
|
-
const
|
|
509
|
+
const label = this._toolbarSettingLabel(setting);
|
|
510
|
+
if (typeof current === "boolean") {
|
|
511
|
+
return `<div class="rm-settings-group rm-settings-switch-row"><span class="rm-settings-label">${this._esc(label)}</span><button type="button" class="rm-setting-switch" role="switch" aria-label="${this._esc(label)}" aria-checked="${current}" data-setting="${setting}" data-value="${!current}"><span></span></button></div>`;
|
|
512
|
+
}
|
|
513
|
+
const colors = { indigo: "#4f46e5", amber: "#f59e0b", blue: "#2563eb", emerald: "#059669", rose: "#e11d48" };
|
|
514
|
+
const corners = { bl: "◱", br: "◲", tl: "◰", tr: "◳" };
|
|
515
|
+
const buttons = options.map(([value, optionLabel]) => {
|
|
500
516
|
const active = String(value) === String(current);
|
|
501
|
-
const
|
|
502
|
-
|
|
517
|
+
const swatch = setting === "accent";
|
|
518
|
+
const content = swatch ? (active ? "✓" : "") : setting === "position" ? corners[value] : this._esc(optionLabel);
|
|
519
|
+
return `<button type="button" class="rm-setting-choice${swatch ? " rm-color-swatch" : ""}${setting === "position" ? " rm-corner-choice" : ""}" data-setting="${setting}" data-value="${this._esc(this._toolbarSettingDisplayValue(value))}" aria-label="${this._esc(optionLabel)}" title="${this._esc(optionLabel)}" aria-pressed="${active}"${swatch ? ` style="background:${colors[value]}"` : ""}>${content}</button>`;
|
|
503
520
|
}).join("");
|
|
504
|
-
return
|
|
505
|
-
<div class="rm-settings-group">
|
|
506
|
-
<div class="rm-settings-label">${this._esc(this._toolbarSettingLabel(setting))}</div>
|
|
507
|
-
<div class="rm-settings-options">${buttons}</div>
|
|
508
|
-
</div>
|
|
509
|
-
`;
|
|
521
|
+
return `<div class="rm-settings-group"><div class="rm-settings-label">${this._esc(label)}</div><div class="rm-settings-options" role="group" aria-label="${this._esc(label)}">${buttons}</div></div>`;
|
|
510
522
|
},
|
|
511
523
|
_toolbarSettingValue(setting, rawValue) {
|
|
512
524
|
if (setting === "fabVisible" || setting === "enableScreenshots") {
|
|
@@ -520,66 +532,107 @@
|
|
|
520
532
|
const normalized = this._normalizeToolbarSettings(next);
|
|
521
533
|
if (JSON.stringify(normalized) === JSON.stringify(this.toolbarSettings)) return;
|
|
522
534
|
if (!this._saveToolbarSettings(normalized)) return;
|
|
535
|
+
const settingsOpen = this._settingsPanelOpen;
|
|
523
536
|
this.toolbarSettings = normalized;
|
|
524
537
|
this.destroy();
|
|
525
538
|
this.init(this._bootstrapOptions || {});
|
|
539
|
+
if (settingsOpen && this.fabVisible) {
|
|
540
|
+
this._toggleSettings();
|
|
541
|
+
this.root.querySelector(`[data-setting="${setting}"][aria-pressed="true"], [data-setting="${setting}"][role="switch"]`)?.focus();
|
|
542
|
+
}
|
|
526
543
|
},
|
|
527
544
|
_toggleSettings() {
|
|
528
545
|
const panel = document.getElementById("rm-settings-panel");
|
|
529
546
|
const toggle = document.getElementById("rm-settings-toggle");
|
|
530
547
|
if (!panel) return;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
548
|
+
this._expandDock();
|
|
549
|
+
const feedback = document.getElementById("rm-panel");
|
|
550
|
+
const opening = panel.hidden || feedback.style.display !== "flex";
|
|
551
|
+
feedback.style.display = opening ? "flex" : "none";
|
|
552
|
+
feedback.dataset.view = opening ? "settings" : "feedback";
|
|
553
|
+
feedback.querySelector("h3").textContent = opening ? "Toolbar settings" : "Feedback";
|
|
554
|
+
panel.hidden = !opening;
|
|
555
|
+
this._settingsPanelOpen = opening;
|
|
556
|
+
document.getElementById("rm-panel-toggle").setAttribute("aria-expanded", "false");
|
|
557
|
+
if (toggle) toggle.setAttribute("aria-expanded", String(opening));
|
|
558
|
+
},
|
|
559
|
+
_resetScreenshotStatus(message = "Optional · capture, upload, or paste an image") {
|
|
560
|
+
document.getElementById("rm-screenshot-status").textContent = message;
|
|
561
|
+
document.getElementById("rm-remove-screenshot").hidden = !this._currentScreenshot;
|
|
562
|
+
},
|
|
563
|
+
_removeScreenshot() {
|
|
564
|
+
this._currentScreenshot = null;
|
|
565
|
+
this.root.querySelector(".rm-drawing-container")?.remove();
|
|
566
|
+
const tool = this.root.querySelector("[data-draw]");
|
|
567
|
+
tool?.parentElement.remove();
|
|
568
|
+
this.drawingCanvas = null;
|
|
569
|
+
this.drawingCtx = null;
|
|
570
|
+
this.drawingHistory = [];
|
|
571
|
+
this._screenshotImg = null;
|
|
572
|
+
document.getElementById("rm-screenshot-file").value = "";
|
|
573
|
+
this._resetScreenshotStatus();
|
|
574
|
+
},
|
|
575
|
+
_useScreenshot(dataUrl) {
|
|
576
|
+
if (document.getElementById("rm-popup").style.display !== "block") return;
|
|
577
|
+
this._removeScreenshot();
|
|
578
|
+
this._currentScreenshot = dataUrl;
|
|
579
|
+
document.getElementById("rm-image-options").open = true;
|
|
580
|
+
this._initDrawing(dataUrl);
|
|
581
|
+
this._resetScreenshotStatus("Image attached · review before submitting");
|
|
582
|
+
},
|
|
583
|
+
async _attachScreenshotFile(file) {
|
|
584
|
+
if (!file) return;
|
|
585
|
+
if (!["image/png", "image/jpeg", "image/webp"].includes(file.type) || file.size > 5 * 1024 * 1024) {
|
|
586
|
+
this._resetScreenshotStatus("Choose a PNG, JPEG, or WebP under 5 MB.");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const popup = document.getElementById("rm-popup");
|
|
590
|
+
const reader = new FileReader();
|
|
591
|
+
reader.onload = () => { if (popup === document.getElementById("rm-popup")) this._useScreenshot(reader.result); };
|
|
592
|
+
reader.onerror = () => this._resetScreenshotStatus("Could not read this image. Try another file.");
|
|
593
|
+
reader.readAsDataURL(file);
|
|
594
|
+
},
|
|
595
|
+
async _attachScreen() {
|
|
596
|
+
if (!navigator.mediaDevices?.getDisplayMedia) {
|
|
597
|
+
this._resetScreenshotStatus("Tab capture is unavailable here. Upload or paste an image instead.");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
let stream;
|
|
601
|
+
let video;
|
|
602
|
+
let frameTimeout;
|
|
603
|
+
const popup = document.getElementById("rm-popup");
|
|
604
|
+
const button = document.getElementById("rm-attach-screen");
|
|
605
|
+
button.disabled = true;
|
|
606
|
+
this._resetScreenshotStatus("Choose a tab or window in the browser prompt. You can also upload an image.");
|
|
536
607
|
try {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const svgNS = "http://www.w3.org/2000/svg";
|
|
552
|
-
const svg = `<svg xmlns="${svgNS}" width="${width}" height="${height}">
|
|
553
|
-
<foreignObject width="100%" height="100%">
|
|
554
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:14px;">${clone.outerHTML}</div>
|
|
555
|
-
</foreignObject>
|
|
556
|
-
</svg>`;
|
|
557
|
-
|
|
608
|
+
// Must run directly from the user's click to retain browser activation.
|
|
609
|
+
stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false, preferCurrentTab: true });
|
|
610
|
+
video = document.createElement("video");
|
|
611
|
+
video.srcObject = stream;
|
|
612
|
+
video.muted = true;
|
|
613
|
+
await Promise.race([
|
|
614
|
+
video.play(),
|
|
615
|
+
new Promise((_, reject) => { frameTimeout = setTimeout(() => reject(new Error("Capture timed out")), 8000); })
|
|
616
|
+
]);
|
|
617
|
+
if (popup !== document.getElementById("rm-popup") || popup.style.display !== "block") return;
|
|
618
|
+
this._removeHighlight();
|
|
619
|
+
this.root.style.visibility = "hidden";
|
|
620
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
621
|
+
const scale = Math.min(1, 1600 / Math.max(video.videoWidth, video.videoHeight));
|
|
558
622
|
const canvas = document.createElement("canvas");
|
|
559
|
-
|
|
560
|
-
canvas.
|
|
561
|
-
canvas.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
URL.revokeObjectURL(url);
|
|
573
|
-
try { resolve(canvas.toDataURL("image/png", 0.7)); } catch { resolve(null); }
|
|
574
|
-
};
|
|
575
|
-
img.onerror = () => {
|
|
576
|
-
URL.revokeObjectURL(url);
|
|
577
|
-
resolve(null);
|
|
578
|
-
};
|
|
579
|
-
img.src = url;
|
|
580
|
-
});
|
|
581
|
-
} catch {
|
|
582
|
-
return null;
|
|
623
|
+
canvas.width = Math.round(video.videoWidth * scale);
|
|
624
|
+
canvas.height = Math.round(video.videoHeight * scale);
|
|
625
|
+
if (!canvas.width || !canvas.height) throw new Error("No video frame");
|
|
626
|
+
canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
627
|
+
this._useScreenshot(canvas.toDataURL("image/png"));
|
|
628
|
+
} catch (error) {
|
|
629
|
+
this._resetScreenshotStatus(error.name === "NotAllowedError" ? "Capture cancelled. You can upload or paste an image." : "Could not capture this tab. Upload or paste an image instead.");
|
|
630
|
+
} finally {
|
|
631
|
+
clearTimeout(frameTimeout);
|
|
632
|
+
stream?.getTracks().forEach(track => track.stop());
|
|
633
|
+
if (video) video.srcObject = null;
|
|
634
|
+
this.root.style.visibility = "";
|
|
635
|
+
button.disabled = false;
|
|
583
636
|
}
|
|
584
637
|
},
|
|
585
638
|
_initDrawing(screenshotDataUrl) {
|
|
@@ -593,7 +646,7 @@
|
|
|
593
646
|
|
|
594
647
|
const img = new Image();
|
|
595
648
|
img.src = screenshotDataUrl;
|
|
596
|
-
img.style.cssText = "display:block;
|
|
649
|
+
img.style.cssText = "display:block;width:100%;height:auto;border-radius:8px;";
|
|
597
650
|
container.appendChild(img);
|
|
598
651
|
|
|
599
652
|
const canvas = document.createElement("canvas");
|
|
@@ -602,6 +655,7 @@
|
|
|
602
655
|
|
|
603
656
|
// Tool buttons
|
|
604
657
|
const tools = document.createElement("div");
|
|
658
|
+
tools.className = "rm-drawing-tools";
|
|
605
659
|
tools.style.cssText = "display:flex;gap:4px;padding:6px 0;";
|
|
606
660
|
tools.innerHTML = `
|
|
607
661
|
<button type="button" data-draw="arrow" class="rm-pill" style="font-size:11px;padding:4px 10px;">Arrow</button>
|
|
@@ -614,6 +668,11 @@
|
|
|
614
668
|
popup.insertBefore(tools, textarea);
|
|
615
669
|
popup.insertBefore(container, tools);
|
|
616
670
|
|
|
671
|
+
this._resetScreenshotStatus("Image attached · review before submitting");
|
|
672
|
+
img.onerror = () => {
|
|
673
|
+
this._removeScreenshot();
|
|
674
|
+
this._resetScreenshotStatus("Could not open this image. Try another file.");
|
|
675
|
+
};
|
|
617
676
|
img.onload = () => {
|
|
618
677
|
canvas.width = img.naturalWidth || img.offsetWidth;
|
|
619
678
|
canvas.height = img.naturalHeight || img.offsetHeight;
|
|
@@ -44,11 +44,24 @@
|
|
|
44
44
|
this._boundOnline = () => this._onOnline();
|
|
45
45
|
window.addEventListener("online", this._boundOnline);
|
|
46
46
|
}
|
|
47
|
+
if (!this._boundSettingsStorage) {
|
|
48
|
+
this._boundSettingsStorage = event => {
|
|
49
|
+
if (event.key !== this._toolbarSettingsKey()) return;
|
|
50
|
+
const options = this._bootstrapOptions;
|
|
51
|
+
this.destroy();
|
|
52
|
+
this.init(options);
|
|
53
|
+
};
|
|
54
|
+
window.addEventListener("storage", this._boundSettingsStorage);
|
|
55
|
+
}
|
|
47
56
|
this._renderPins();
|
|
48
57
|
this._updateCount();
|
|
49
58
|
if (previousPageUrl && previousPageUrl !== this._currentPageUrl && this.serverOnline) this._initSession();
|
|
50
59
|
},
|
|
51
60
|
destroy() {
|
|
61
|
+
if (this._boundSettingsStorage) {
|
|
62
|
+
window.removeEventListener("storage", this._boundSettingsStorage);
|
|
63
|
+
this._boundSettingsStorage = null;
|
|
64
|
+
}
|
|
52
65
|
this._closeAllMenus();
|
|
53
66
|
this._deactivateMode();
|
|
54
67
|
if (this.sseSource) { this.sseSource.close(); this.sseSource = null; }
|
|
@@ -6,132 +6,23 @@
|
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
7
|
<title><%= content_for?(:title) ? "#{yield(:title)} — Rails Markup" : "Rails Markup" %></title>
|
|
8
8
|
<style>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
a { color: #4361ee; text-decoration: none; }
|
|
9
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
10
|
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #fafbfc; color: #25272d; line-height: 1.55; }
|
|
11
|
+
a { color: #4f46e5; text-decoration: none; }
|
|
13
12
|
a:hover { text-decoration: underline; }
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.rm-header {
|
|
17
|
-
.rm-header
|
|
18
|
-
.rm-header
|
|
19
|
-
.rm-header nav
|
|
20
|
-
.rm-
|
|
21
|
-
.rm-
|
|
22
|
-
.rm-container { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
|
|
23
|
-
|
|
24
|
-
/* Flash */
|
|
25
|
-
.rm-flash { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; }
|
|
13
|
+
.rm-header { background: #fff; color: #25272d; min-height: 52px; border-bottom:1px solid #e8ebef; padding: 8px max(24px, calc((100vw - 1080px) / 2)); display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
14
|
+
.rm-header h1 { margin: 0; font-size: 14px; font-weight: 650; letter-spacing: -.025em; }
|
|
15
|
+
.rm-header h1 span { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; background: #4f46e5; color: white; margin-right: 8px; }
|
|
16
|
+
.rm-header nav { display: flex; gap: 20px; flex-wrap: wrap; font-size: 13px; }
|
|
17
|
+
.rm-header nav a { color: #64748b; padding: 6px 0; }
|
|
18
|
+
.rm-header nav a:hover { color: #25272d; }
|
|
19
|
+
.rm-container { max-width: 1128px; margin: 0 auto; padding: 28px 24px; }
|
|
20
|
+
.rm-flash { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; }
|
|
26
21
|
.rm-flash-notice { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
|
|
27
22
|
.rm-flash-alert { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.rm-
|
|
31
|
-
.rm-stat { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px 20px; flex: 1; min-width: 120px; }
|
|
32
|
-
.rm-stat-value { font-size: 28px; font-weight: 700; line-height: 1; }
|
|
33
|
-
.rm-stat-label { font-size: 12px; color: #6b7280; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
34
|
-
|
|
35
|
-
/* Filters */
|
|
36
|
-
.rm-filters { display: flex; gap: 8px; margin-bottom: 20px; align-items: center; flex-wrap: wrap; }
|
|
37
|
-
.rm-pill { display: inline-flex; align-items: center; padding: 6px 14px; border-radius: 20px; font-size: 12px; font-weight: 500; cursor: pointer; border: 1px solid #e5e7eb; background: #fff; color: #6b7280; transition: all 0.15s; }
|
|
38
|
-
.rm-pill:hover { border-color: #818cf8; color: #4361ee; text-decoration: none; }
|
|
39
|
-
.rm-pill-active { background: #4361ee; color: #fff; border-color: #4361ee; }
|
|
40
|
-
.rm-pill-active:hover { background: #3730a3; color: #fff; }
|
|
41
|
-
.rm-select { padding: 6px 12px; border-radius: 8px; border: 1px solid #e5e7eb; font-size: 12px; background: #fff; color: #374151; cursor: pointer; max-width: 240px; }
|
|
42
|
-
|
|
43
|
-
/* Cards */
|
|
44
|
-
.rm-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px 20px; margin-bottom: 10px; transition: border-color 0.15s; position: relative; }
|
|
45
|
-
.rm-card:hover { border-color: #818cf8; }
|
|
46
|
-
.rm-card-link { display: block; color: inherit; }
|
|
47
|
-
.rm-card-link:hover { text-decoration: none; }
|
|
48
|
-
.rm-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
49
|
-
.rm-card-content { font-size: 14px; color: #374151; line-height: 1.6; }
|
|
50
|
-
.rm-card-meta { display: flex; align-items: center; gap: 12px; margin-top: 10px; font-size: 11px; color: #9ca3af; }
|
|
51
|
-
.rm-card-meta code { font-size: 11px; background: #f3f4f6; padding: 2px 6px; border-radius: 4px; color: #6b7280; }
|
|
52
|
-
|
|
53
|
-
/* Badges */
|
|
54
|
-
.rm-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; text-transform: capitalize; }
|
|
55
|
-
.rm-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
|
56
|
-
.rm-status-pending { background: #fef3c7; color: #92400e; }
|
|
57
|
-
.rm-status-acknowledged { background: #dbeafe; color: #1e40af; }
|
|
58
|
-
.rm-status-resolved { background: #d1fae5; color: #065f46; }
|
|
59
|
-
.rm-status-dismissed { background: #f3f4f6; color: #6b7280; }
|
|
60
|
-
.rm-dot-pending { background: #f59e0b; }
|
|
61
|
-
.rm-dot-acknowledged { background: #3b82f6; }
|
|
62
|
-
.rm-dot-resolved { background: #10b981; }
|
|
63
|
-
.rm-dot-dismissed { background: #9ca3af; }
|
|
64
|
-
.rm-intent-fix { background: #fef2f2; color: #991b1b; }
|
|
65
|
-
.rm-intent-change { background: #eff6ff; color: #1e40af; }
|
|
66
|
-
.rm-intent-question { background: #f5f3ff; color: #5b21b6; }
|
|
67
|
-
.rm-intent-approve { background: #ecfdf5; color: #065f46; }
|
|
68
|
-
.rm-severity-blocking { background: #fef2f2; color: #991b1b; font-weight: 700; }
|
|
69
|
-
.rm-severity-important { background: #fff7ed; color: #9a3412; }
|
|
70
|
-
.rm-severity-suggestion { background: #f3f4f6; color: #6b7280; }
|
|
71
|
-
|
|
72
|
-
/* Detail page */
|
|
73
|
-
.rm-detail { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 24px; }
|
|
74
|
-
.rm-detail-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
75
|
-
.rm-detail-content { font-size: 15px; line-height: 1.7; margin-bottom: 24px; white-space: pre-wrap; }
|
|
76
|
-
.rm-detail-meta { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; font-size: 13px; margin-bottom: 24px; padding: 16px; background: #f9fafb; border-radius: 8px; }
|
|
77
|
-
.rm-detail-meta dt { color: #6b7280; font-weight: 500; }
|
|
78
|
-
.rm-detail-meta dd { color: #374151; }
|
|
79
|
-
.rm-detail-meta dd code { font-size: 12px; background: #e5e7eb; padding: 1px 5px; border-radius: 3px; }
|
|
80
|
-
|
|
81
|
-
/* Thread */
|
|
82
|
-
.rm-thread { margin-top: 24px; border-top: 1px solid #e5e7eb; padding-top: 20px; }
|
|
83
|
-
.rm-thread h3 { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: #374151; }
|
|
84
|
-
.rm-thread-entry { padding: 12px 16px; border-radius: 8px; margin-bottom: 8px; font-size: 13px; line-height: 1.5; }
|
|
85
|
-
.rm-thread-agent { background: #eff6ff; border-left: 3px solid #3b82f6; }
|
|
86
|
-
.rm-thread-user { background: #f9fafb; border-left: 3px solid #e5e7eb; }
|
|
87
|
-
.rm-thread-role { font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; }
|
|
88
|
-
.rm-thread-time { font-size: 11px; color: #9ca3af; margin-top: 4px; }
|
|
89
|
-
|
|
90
|
-
/* Actions */
|
|
91
|
-
.rm-actions { display: flex; gap: 8px; margin-top: 24px; flex-wrap: wrap; }
|
|
92
|
-
.rm-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; border: 1px solid #e5e7eb; background: #fff; color: #374151; cursor: pointer; transition: all 0.15s; }
|
|
93
|
-
.rm-btn:hover { border-color: #818cf8; color: #4361ee; text-decoration: none; }
|
|
94
|
-
.rm-btn-primary { background: #4361ee; color: #fff; border-color: #4361ee; }
|
|
95
|
-
.rm-btn-primary:hover { background: #3730a3; color: #fff; }
|
|
96
|
-
.rm-btn-success { background: #10b981; color: #fff; border-color: #10b981; }
|
|
97
|
-
.rm-btn-success:hover { background: #059669; color: #fff; }
|
|
98
|
-
.rm-btn-danger { background: #ef4444; color: #fff; border-color: #ef4444; }
|
|
99
|
-
.rm-btn-danger:hover { background: #dc2626; color: #fff; }
|
|
100
|
-
|
|
101
|
-
/* Forms */
|
|
102
|
-
.rm-form-group { margin-bottom: 12px; }
|
|
103
|
-
.rm-form-group label { display: block; font-size: 12px; font-weight: 500; color: #6b7280; margin-bottom: 4px; }
|
|
104
|
-
.rm-input { width: 100%; padding: 8px 12px; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 13px; font-family: inherit; resize: vertical; }
|
|
105
|
-
.rm-input:focus { outline: none; border-color: #818cf8; box-shadow: 0 0 0 3px rgba(99,102,241,0.1); }
|
|
106
|
-
|
|
107
|
-
/* Back link */
|
|
108
|
-
.rm-back { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: #6b7280; margin-bottom: 16px; }
|
|
109
|
-
.rm-back:hover { color: #4361ee; text-decoration: none; }
|
|
110
|
-
|
|
111
|
-
/* Empty state */
|
|
112
|
-
.rm-empty { text-align: center; padding: 48px 24px; color: #9ca3af; }
|
|
113
|
-
.rm-empty-icon { font-size: 36px; margin-bottom: 12px; }
|
|
114
|
-
.rm-empty-text { font-size: 14px; }
|
|
115
|
-
|
|
116
|
-
/* Two-column layout */
|
|
117
|
-
.rm-layout { display: flex; gap: 24px; }
|
|
118
|
-
.rm-main { flex: 1; min-width: 0; }
|
|
119
|
-
.rm-panel { width: 420px; flex-shrink: 0; position: sticky; top: 80px; max-height: calc(100vh - 100px); overflow-y: auto; background: #f8f9fa; }
|
|
120
|
-
.rm-panel .rm-detail { border-left: 3px solid #818cf8; }
|
|
121
|
-
.rm-card-active { border-color: #818cf8 !important; background: #f5f3ff; }
|
|
122
|
-
.rm-panel-empty { display: none; }
|
|
123
|
-
|
|
124
|
-
/* Responsive */
|
|
125
|
-
@media (max-width: 900px) {
|
|
126
|
-
.rm-layout { flex-direction: column; }
|
|
127
|
-
.rm-panel { width: 100%; position: static; max-height: none; background: #f8f9fa; }
|
|
128
|
-
}
|
|
129
|
-
@media (max-width: 640px) {
|
|
130
|
-
.rm-stats { flex-direction: column; }
|
|
131
|
-
.rm-stat { min-width: unset; }
|
|
132
|
-
.rm-filters { flex-direction: column; align-items: stretch; }
|
|
133
|
-
.rm-detail-header { flex-direction: column; align-items: flex-start; }
|
|
134
|
-
}
|
|
23
|
+
.rm-footer { max-width: 1128px; margin: 0 auto; padding: 0 24px 28px; color: #596579; font-size: 11px; text-align: left; }
|
|
24
|
+
:focus-visible { outline: 3px solid #6366f1; outline-offset: 3px; }
|
|
25
|
+
@media (max-width: 640px) { .rm-header { padding: 12px 16px; } .rm-container { padding: 24px 16px; } .rm-header nav { gap: 12px; } }
|
|
135
26
|
</style>
|
|
136
27
|
</head>
|
|
137
28
|
<body>
|
|
@@ -155,5 +46,8 @@
|
|
|
155
46
|
|
|
156
47
|
<%= yield %>
|
|
157
48
|
</main>
|
|
49
|
+
<footer class="rm-footer">
|
|
50
|
+
Built with love in Sydney by <a href="https://nauman.one">Nauman Tariq</a> and Pavelabs.
|
|
51
|
+
</footer>
|
|
158
52
|
</body>
|
|
159
53
|
</html>
|
|
@@ -2,16 +2,11 @@
|
|
|
2
2
|
<a href="<%= rails_markup.annotation_path(annotation) %>"
|
|
3
3
|
data-turbo-frame="detail-panel"
|
|
4
4
|
class="rm-card-link">
|
|
5
|
-
<div class="rm-card-header">
|
|
6
|
-
<span class="rm-dot rm-dot-<%= annotation.status %>"></span>
|
|
7
|
-
<span class="rm-badge rm-status-<%= annotation.status %>"><%= annotation.status %></span>
|
|
8
|
-
<span class="rm-badge rm-intent-<%= annotation.intent %>"><%= annotation.intent %></span>
|
|
9
|
-
<% if annotation.severity != "suggestion" %>
|
|
10
|
-
<span class="rm-badge rm-severity-<%= annotation.severity %>"><%= annotation.severity %></span>
|
|
11
|
-
<% end %>
|
|
12
|
-
</div>
|
|
13
5
|
<div class="rm-card-content"><%= truncate(annotation.content, length: 200) %></div>
|
|
14
6
|
<div class="rm-card-meta">
|
|
7
|
+
<% unless local_assigns[:board] %><span class="rm-row-status"><span class="rm-dot rm-dot-<%= annotation.status %>"></span><%= annotation.status.capitalize %></span><% end %>
|
|
8
|
+
<% if annotation.intent != "change" %><span class="rm-badge rm-intent-<%= annotation.intent %>"><%= annotation.intent %></span><% end %>
|
|
9
|
+
<% if annotation.severity != "suggestion" %><span class="rm-badge rm-severity-<%= annotation.severity %>"><%= annotation.severity %></span><% end %>
|
|
15
10
|
<code><%= truncate(annotation.page_url, length: 50) %></code>
|
|
16
11
|
<% if annotation.author_name.present? %>
|
|
17
12
|
<span class="rm-author"><%= annotation.author_name %></span>
|
|
@@ -20,6 +15,7 @@
|
|
|
20
15
|
<% if annotation.thread.any? %>
|
|
21
16
|
<span><%= annotation.thread.size %> replies</span>
|
|
22
17
|
<% end %>
|
|
18
|
+
<span class="rm-card-number">#<%= annotation.id %></span>
|
|
23
19
|
</div>
|
|
24
20
|
</a>
|
|
25
21
|
</div>
|
|
@@ -47,8 +47,12 @@
|
|
|
47
47
|
<% elsif @current_author.present? %>
|
|
48
48
|
No annotations by <%= @current_author %>.
|
|
49
49
|
<a href="<%= rails_markup.root_path(status: @current_status, page_url: @current_page_url) %>">Clear filter</a>
|
|
50
|
+
<% elsif @total_count == 0 %>
|
|
51
|
+
<h3 class="rm-empty-title">Your next improvement starts on the page.</h3>
|
|
52
|
+
<p class="rm-empty-help">Open a page in your app, expand the markup toolbar, and select an element to leave feedback. Your annotations will appear here.</p>
|
|
50
53
|
<% elsif @current_status != "all" %>
|
|
51
54
|
No <%= @current_status %> annotations.
|
|
55
|
+
<p class="rm-empty-help"><a href="<%= rails_markup.root_path(status: 'all', page_url: @current_page_url) %>">View all feedback</a> or use the toolbar in your app to add a new annotation.</p>
|
|
52
56
|
<% else %>
|
|
53
57
|
No annotations yet.<br>Use the toolbar to start annotating.
|
|
54
58
|
<% end %>
|
|
@@ -1,37 +1,31 @@
|
|
|
1
|
-
<div class="rm-filters">
|
|
1
|
+
<div class="rm-filters rm-status-tabs" role="group" aria-label="Filter by status">
|
|
2
2
|
<%
|
|
3
3
|
statuses = [
|
|
4
|
-
["all", "All"],
|
|
5
|
-
["pending", "Pending"],
|
|
6
|
-
["acknowledged", "Acknowledged"],
|
|
7
|
-
["resolved", "Resolved"],
|
|
8
|
-
["dismissed", "Dismissed"]
|
|
4
|
+
["all", "All", @total_count],
|
|
5
|
+
["pending", "Pending", @pending_count],
|
|
6
|
+
["acknowledged", "Acknowledged", @acknowledged_count],
|
|
7
|
+
["resolved", "Resolved", @resolved_count],
|
|
8
|
+
["dismissed", "Dismissed", @dismissed_count]
|
|
9
9
|
]
|
|
10
10
|
%>
|
|
11
|
-
<% statuses.each do |value, label| %>
|
|
11
|
+
<% statuses.each do |value, label, count| %>
|
|
12
12
|
<a href="<%= rails_markup.root_path(status: value, page_url: @current_page_url, q: @current_query, author: @current_author) %>"
|
|
13
|
-
class="rm-pill <%= 'rm-pill-active' if @current_status == value %>"
|
|
14
|
-
<%= label %>
|
|
13
|
+
class="rm-pill <%= 'rm-pill-active' if @current_status == value %>" <%= 'aria-current=page' if @current_status == value %>>
|
|
14
|
+
<%= label %> <span class="rm-tab-count"><%= count %></span>
|
|
15
15
|
</a>
|
|
16
16
|
<% end %>
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
<%= form_tag(rails_markup.dismiss_all_path(status: @current_status), method: :post, style: "display:inline", data: { turbo_confirm: "Dismiss all #{@current_status} annotations?" }) do %>
|
|
20
|
-
<button type="submit" class="rm-pill" style="color: #ef4444; border-color: #fecaca;">Dismiss all <%= @current_status %></button>
|
|
21
|
-
<% end %>
|
|
22
|
-
<% end %>
|
|
23
|
-
|
|
24
|
-
<div style="margin-left: auto; display: flex; gap: 4px; align-items: center;">
|
|
18
|
+
<div class="rm-filter-tools" role="group" aria-label="View">
|
|
25
19
|
<a href="<%= rails_markup.root_path(status: @current_status, page_url: @current_page_url, q: @current_query, author: @current_author) %>"
|
|
26
20
|
class="rm-pill <%= 'rm-pill-active' unless action_name == 'board' %>">List</a>
|
|
27
21
|
<a href="<%= rails_markup.board_path %>" class="rm-pill <%= 'rm-pill-active' if action_name == 'board' %>">Board</a>
|
|
28
22
|
</div>
|
|
29
23
|
</div>
|
|
30
24
|
|
|
31
|
-
<div class="rm-filters">
|
|
25
|
+
<div class="rm-filters rm-filter-row">
|
|
32
26
|
<%# Search %>
|
|
33
|
-
<form action="<%= rails_markup.root_path %>" method="get"
|
|
34
|
-
<input type="
|
|
27
|
+
<form action="<%= rails_markup.root_path %>" method="get" class="rm-search-form" role="search">
|
|
28
|
+
<input type="search" name="q" value="<%= @current_query %>" placeholder="Search annotations..." aria-label="Search annotations" class="rm-select">
|
|
35
29
|
<input type="hidden" name="status" value="<%= @current_status %>">
|
|
36
30
|
<input type="hidden" name="page_url" value="<%= @current_page_url %>">
|
|
37
31
|
<input type="hidden" name="author" value="<%= @current_author %>">
|
|
@@ -41,9 +35,13 @@
|
|
|
41
35
|
<% end %>
|
|
42
36
|
</form>
|
|
43
37
|
|
|
38
|
+
<details class="rm-options" <%= 'open' if @current_page_url.present? || @current_author.present? %>>
|
|
39
|
+
<summary>Options<%= ' · filtered' if @current_page_url.present? || @current_author.present? %></summary>
|
|
40
|
+
<div class="rm-options-content">
|
|
41
|
+
<div class="rm-options-heading">Filter feedback</div>
|
|
44
42
|
<%# Page URL filter %>
|
|
45
43
|
<% if @page_urls.any? %>
|
|
46
|
-
<select class="rm-select" onchange="
|
|
44
|
+
<select id="rm-page-filter" class="rm-select" aria-label="Filter by page" onchange="window.location=this.value">
|
|
47
45
|
<option value="<%= rails_markup.root_path(status: @current_status == 'all' ? nil : @current_status, q: @current_query, author: @current_author) %>">All pages</option>
|
|
48
46
|
<% @page_urls.each do |url| %>
|
|
49
47
|
<option value="<%= rails_markup.root_path(status: @current_status == 'all' ? nil : @current_status, page_url: url, q: @current_query, author: @current_author) %>"
|
|
@@ -56,7 +54,7 @@
|
|
|
56
54
|
|
|
57
55
|
<%# Author filter %>
|
|
58
56
|
<% if @authors.any? %>
|
|
59
|
-
<select class="rm-select" onchange="window.location=this.value">
|
|
57
|
+
<select class="rm-select" aria-label="Filter by author" onchange="window.location=this.value">
|
|
60
58
|
<option value="<%= rails_markup.root_path(status: @current_status, page_url: @current_page_url, q: @current_query) %>">All authors</option>
|
|
61
59
|
<% @authors.each do |author| %>
|
|
62
60
|
<option value="<%= rails_markup.root_path(status: @current_status, page_url: @current_page_url, q: @current_query, author: author) %>"
|
|
@@ -67,9 +65,20 @@
|
|
|
67
65
|
</select>
|
|
68
66
|
<% end %>
|
|
69
67
|
|
|
68
|
+
<div class="rm-options-heading">Export</div>
|
|
70
69
|
<%# Export %>
|
|
71
|
-
<div
|
|
70
|
+
<div class="rm-filter-tools" role="group" aria-label="Export annotations">
|
|
72
71
|
<a href="<%= rails_markup.export_csv_path(status: @current_status, page_url: @current_page_url, q: @current_query, author: @current_author) %>" class="rm-pill">CSV</a>
|
|
73
72
|
<a href="<%= rails_markup.export_json_path(status: @current_status, page_url: @current_page_url, q: @current_query, author: @current_author) %>" class="rm-pill">JSON</a>
|
|
74
73
|
</div>
|
|
74
|
+
<div class="rm-options-heading">Toolbar</div>
|
|
75
|
+
<%= render "toolbar_preferences" %>
|
|
76
|
+
<% if @current_status.in?(%w[pending acknowledged]) && @annotations.any? %>
|
|
77
|
+
<%= form_tag(rails_markup.dismiss_all_path(status: @current_status), method: :post, style: "display:inline", data: { turbo_confirm: "Dismiss all #{@current_status} annotations?" }) do %>
|
|
78
|
+
<button type="submit" class="rm-pill" style="color: #ef4444; border-color: #fecaca;">Dismiss all <%= @current_status %></button>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% end %>
|
|
81
|
+
|
|
82
|
+
</div>
|
|
83
|
+
</details>
|
|
75
84
|
</div>
|