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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/rails_markup/toolbar/00-core.js +68 -0
- data/app/assets/javascripts/rails_markup/toolbar/10-styles.js +82 -0
- data/app/assets/javascripts/rails_markup/toolbar/20-dom.js +95 -0
- data/app/assets/javascripts/rails_markup/toolbar/30-menu.js +176 -0
- data/app/assets/javascripts/rails_markup/toolbar/40-events.js +350 -0
- data/app/assets/javascripts/rails_markup/toolbar/50-sync.js +1087 -0
- data/app/assets/javascripts/rails_markup/toolbar/60-render.js +675 -0
- data/app/assets/javascripts/rails_markup/toolbar/90-init.js +91 -0
- data/app/views/rails_markup/shared/_toolbar.html.erb +1 -1
- data/lib/rails_markup/engine.rb +0 -7
- data/lib/rails_markup/toolbar_source.rb +24 -0
- data/lib/rails_markup/version.rb +1 -1
- data/lib/rails_markup.rb +1 -0
- metadata +10 -2
- data/app/assets/javascripts/rails_markup/toolbar.js +0 -2779
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88eacfb8b1c5006e209a9152bda3f464c54675d0460388624aefdae0c106e838
|
|
4
|
+
data.tar.gz: 34abe3d1f0f3c3cb0304114543bc1649c6c87a47fa7faa67577f37c1cd041bee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1910ed15f034f2c343a29e5f2060ad35f868f6cc6a66619e43f21127304bbeb1eef3008a7490d584e3efa2c05964344d5eda6815b2ae0628e4d1a271227df094
|
|
7
|
+
data.tar.gz: 62cb48444c13a8c2c4812a3e065aff880b87646620166e92f7c21e0f6fb05bf3c0032fa82df423ce6e536b7c34cd1826b5dcc3e21cbc2cec4146b5ac1bd504a7
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rails Markup Toolbar — self-contained annotation UI
|
|
3
|
+
* No dependencies (no Stimulus, no importmap). Works in any Rails app.
|
|
4
|
+
*
|
|
5
|
+
* Usage: include via <script> tag, then call:
|
|
6
|
+
* RailsMarkupToolbar.init({ endpoint: "/feedback/api", accent: "indigo" })
|
|
7
|
+
*/
|
|
8
|
+
(function(global) {
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
if (global.RailsMarkupToolbar) return;
|
|
12
|
+
|
|
13
|
+
const RailsMarkupToolbar = {
|
|
14
|
+
// State
|
|
15
|
+
annotations: [],
|
|
16
|
+
outbox: {},
|
|
17
|
+
legacyMigrations: {},
|
|
18
|
+
nextId: 1,
|
|
19
|
+
active: false,
|
|
20
|
+
serverOnline: false,
|
|
21
|
+
sessionId: null,
|
|
22
|
+
sseSource: null,
|
|
23
|
+
healthInterval: null,
|
|
24
|
+
hoveredElement: null,
|
|
25
|
+
selectedText: null,
|
|
26
|
+
clickedElement: null,
|
|
27
|
+
activeFilter: "all",
|
|
28
|
+
editingId: null,
|
|
29
|
+
_currentScreenshot: null,
|
|
30
|
+
_storageError: null,
|
|
31
|
+
_outboxFlushScheduled: false,
|
|
32
|
+
_outboxFlushTimer: null,
|
|
33
|
+
_flushPromise: null,
|
|
34
|
+
_flushAfterPullPromise: null,
|
|
35
|
+
_healthPromise: null,
|
|
36
|
+
_pullPromise: null,
|
|
37
|
+
_pullPageUrl: null,
|
|
38
|
+
_pullNeeded: true,
|
|
39
|
+
_syncRetryTimer: null,
|
|
40
|
+
_syncRetryAttempt: 0,
|
|
41
|
+
_syncRetryDelay: 0,
|
|
42
|
+
_syncBaseRetryDelay: 1000,
|
|
43
|
+
_syncMaxRetryDelay: 30000,
|
|
44
|
+
_syncMalformedLimit: 3,
|
|
45
|
+
_syncUnavailable: null,
|
|
46
|
+
legacyStorageEndpoint: null,
|
|
47
|
+
|
|
48
|
+
// Drawing state
|
|
49
|
+
drawingMode: null, // null | "arrow" | "rect" | "highlight"
|
|
50
|
+
drawingCanvas: null,
|
|
51
|
+
drawingCtx: null,
|
|
52
|
+
drawingStart: null,
|
|
53
|
+
drawingHistory: [],
|
|
54
|
+
_screenshotImg: null,
|
|
55
|
+
|
|
56
|
+
// Config
|
|
57
|
+
endpoint: "/feedback/api",
|
|
58
|
+
accent: "indigo",
|
|
59
|
+
position: "bl",
|
|
60
|
+
size: "default",
|
|
61
|
+
fabVisible: true,
|
|
62
|
+
enableScreenshots: true,
|
|
63
|
+
|
|
64
|
+
// DOM refs (set in init)
|
|
65
|
+
root: null,
|
|
66
|
+
_currentPathname: null,
|
|
67
|
+
_currentPageUrl: null
|
|
68
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Object.assign(RailsMarkupToolbar, {
|
|
2
|
+
_injectStyles() {
|
|
3
|
+
if (document.getElementById("rm-toolbar-styles")) return;
|
|
4
|
+
const style = document.createElement("style");
|
|
5
|
+
style.id = "rm-toolbar-styles";
|
|
6
|
+
style.textContent = `
|
|
7
|
+
@keyframes rm-pulse { 0%,100%{box-shadow:0 2px 8px rgba(0,0,0,0.2)} 50%{box-shadow:0 2px 12px rgba(0,0,0,0.3),0 0 0 4px rgba(99,102,241,0.15)} }
|
|
8
|
+
@keyframes rm-toast-in { from{opacity:0;transform:translateY(16px) scale(0.95)} to{opacity:1;transform:translateY(0) scale(1)} }
|
|
9
|
+
@keyframes rm-toast-out { from{opacity:1;transform:translateY(0) scale(1)} to{opacity:0;transform:translateY(16px) scale(0.95)} }
|
|
10
|
+
#rm-toolbar-root { position:fixed; inset:0; pointer-events:none; z-index:9979; }
|
|
11
|
+
#rm-toolbar-root * { box-sizing:border-box; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif; }
|
|
12
|
+
.rm-fab, .rm-panel-toggle, .rm-panel, .rm-popup { pointer-events:auto; }
|
|
13
|
+
.rm-fab { position:fixed; z-index:9980; border-radius:50%; border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all 0.2s; box-shadow:0 4px 12px rgba(0,0,0,0.15); }
|
|
14
|
+
.rm-fab svg { width:20px; height:20px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
|
|
15
|
+
.rm-fab-badge { position:absolute; top:-4px; right:-4px; min-width:20px; height:20px; padding:0 4px; border-radius:10px; background:#ef4444; color:#fff; font-size:11px; font-weight:700; display:none; align-items:center; justify-content:center; }
|
|
16
|
+
.rm-panel-toggle { position:fixed; z-index:9980; width:32px; height:32px; border-radius:50%; border:1px solid #e5e7eb; background:rgba(255,255,255,0.9); cursor:pointer; display:none; align-items:center; justify-content:center; color:#6b7280; transition:all 0.2s; backdrop-filter:blur(8px); }
|
|
17
|
+
.rm-panel-toggle:hover { color:#4361ee; }
|
|
18
|
+
.rm-panel-toggle svg { width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
|
|
19
|
+
.rm-toast-container { position:fixed; z-index:9983; display:flex; flex-direction:column; gap:8px; pointer-events:none; }
|
|
20
|
+
.rm-pins-container { position:absolute; top:0; left:0; width:100%; z-index:9979; pointer-events:none; }
|
|
21
|
+
.rm-pin { pointer-events:auto; }
|
|
22
|
+
.rm-popup { display:none; position:fixed; z-index:9982; width:360px; max-width:calc(100vw - 24px); background:rgba(255,255,255,0.95); backdrop-filter:blur(12px); border-radius:16px; box-shadow:0 25px 50px rgba(0,0,0,0.1); border:1px solid rgba(229,231,235,0.8); padding:16px; }
|
|
23
|
+
#rm-toolbar-root .rm-popup textarea { display:block; width:100%; max-width:100%; font-size:13px; font-weight:400; line-height:1.4; color:#1f2937; height:auto; margin:0; border:1px solid #e5e7eb; border-radius:12px; padding:12px; resize:none; outline:none; font-family:inherit; background:#fff; background-image:none; box-shadow:none; appearance:none; -webkit-appearance:none; transition:border-color 0.15s,box-shadow 0.15s; }
|
|
24
|
+
#rm-toolbar-root .rm-popup textarea:focus { border:1px solid #818cf8; box-shadow:0 0 0 3px rgba(99,102,241,0.1); }
|
|
25
|
+
#rm-toolbar-root .rm-menu { position:relative; display:inline-block; vertical-align:middle; }
|
|
26
|
+
#rm-toolbar-root .rm-menu-btn { display:inline-flex; align-items:center; gap:4px; width:auto; height:auto; margin:0; font-size:11px; font-weight:500; line-height:1.4; color:#374151; border:1px solid #e5e7eb; border-radius:8px; padding:6px 8px; background:#fff; background-image:none; box-shadow:none; outline:none; text-transform:none; appearance:none; -webkit-appearance:none; cursor:pointer; }
|
|
27
|
+
#rm-toolbar-root .rm-menu-btn:hover { border-color:#d1d5db; background:#fff; }
|
|
28
|
+
#rm-toolbar-root .rm-menu-btn:focus { outline:none; border-color:#818cf8; box-shadow:0 0 0 3px rgba(99,102,241,0.1); }
|
|
29
|
+
#rm-toolbar-root .rm-menu-btn[aria-expanded="true"] { border-color:#818cf8; }
|
|
30
|
+
#rm-toolbar-root .rm-menu-chevron { width:10px; height:10px; flex-shrink:0; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; opacity:0.55; }
|
|
31
|
+
#rm-toolbar-root .rm-menu-list { display:none; position:absolute; top:calc(100% + 4px); left:0; z-index:9984; min-width:100%; padding:4px; margin:0; list-style:none; background:#fff; border:1px solid #e5e7eb; border-radius:10px; box-shadow:0 10px 24px rgba(0,0,0,0.12); }
|
|
32
|
+
#rm-toolbar-root .rm-menu-list.rm-menu-open { display:block; }
|
|
33
|
+
#rm-toolbar-root .rm-menu-option { display:block; width:100%; margin:0; padding:6px 10px; font-size:11px; font-weight:500; line-height:1.4; color:#374151; text-align:left; border:none; border-radius:6px; background:transparent; background-image:none; box-shadow:none; cursor:pointer; appearance:none; -webkit-appearance:none; }
|
|
34
|
+
#rm-toolbar-root .rm-menu-option:hover, #rm-toolbar-root .rm-menu-option:focus { background:#f3f4f6; outline:none; }
|
|
35
|
+
#rm-toolbar-root .rm-menu-option-active { background:#eef2ff; color:#4338ca; }
|
|
36
|
+
#rm-toolbar-root .rm-menu-compact .rm-menu-btn { font-size:10px; padding:2px 6px; border-radius:4px; color:#6b7280; }
|
|
37
|
+
#rm-toolbar-root .rm-menu-compact .rm-menu-list, #rm-toolbar-root .rm-menu-list-compact { min-width:120px; right:0; left:auto; }
|
|
38
|
+
#rm-toolbar-root .rm-menu-compact .rm-menu-option, #rm-toolbar-root .rm-menu-list-compact .rm-menu-option { font-size:10px; padding:5px 8px; }
|
|
39
|
+
.rm-popup-el { font-size:11px; color:#9ca3af; font-family:monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; line-height:1.4; }
|
|
40
|
+
.rm-popup-text { font-size:12px; color:#6b7280; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:2px; line-height:1.4; }
|
|
41
|
+
.rm-popup-actions { display:flex; align-items:center; gap:8px; margin-top:8px; }
|
|
42
|
+
.rm-popup-actions .rm-count { font-size:10px; color:#d1d5db; margin-left:auto; font-variant-numeric:tabular-nums; }
|
|
43
|
+
#rm-toolbar-root .rm-btn-cancel { padding:6px 12px; font-size:12px; color:#9ca3af; background:none; background-image:none; border:none; box-shadow:none; cursor:pointer; border-radius:8px; appearance:none; -webkit-appearance:none; }
|
|
44
|
+
#rm-toolbar-root .rm-btn-cancel:hover { color:#6b7280; }
|
|
45
|
+
#rm-toolbar-root .rm-btn-submit { padding:6px 16px; font-size:12px; font-weight:500; color:#fff; border:none; border-radius:8px; cursor:pointer; display:inline-flex; align-items:center; gap:6px; box-shadow:none; appearance:none; -webkit-appearance:none; }
|
|
46
|
+
#rm-toolbar-root .rm-btn-submit kbd { font-size:9px; opacity:0.6; font-family:sans-serif; }
|
|
47
|
+
.rm-panel { display:none; position:fixed; z-index:9981; width:380px; max-width:calc(100vw - 48px); max-height:60vh; background:rgba(255,255,255,0.95); backdrop-filter:blur(12px); border-radius:16px; box-shadow:0 25px 50px rgba(0,0,0,0.1); border:1px solid rgba(229,231,235,0.8); flex-direction:column; }
|
|
48
|
+
.rm-panel-header { display:flex; align-items:center; justify-content:space-between; padding:12px 16px; border-bottom:1px solid #f3f4f6; }
|
|
49
|
+
.rm-panel-header h3 { font-size:14px; font-weight:600; color:#1f2937; }
|
|
50
|
+
.rm-panel-count { min-width:20px; text-align:center; padding:2px 6px; font-size:10px; font-weight:600; border-radius:10px; }
|
|
51
|
+
.rm-panel-close { padding:6px; color:#d1d5db; background:none; border:none; cursor:pointer; border-radius:8px; }
|
|
52
|
+
.rm-panel-close:hover { color:#6b7280; }
|
|
53
|
+
.rm-panel-close svg { width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
|
|
54
|
+
.rm-filter-chips { display:flex; gap:6px; padding:8px 16px; border-bottom:1px solid #f9fafb; }
|
|
55
|
+
.rm-chip { padding:4px 8px; font-size:10px; font-weight:500; border-radius:10px; cursor:pointer; transition:all 0.15s; border:none; }
|
|
56
|
+
.rm-chip-active { color:#fff; }
|
|
57
|
+
.rm-chip-inactive { background:#f9fafb; color:#9ca3af; }
|
|
58
|
+
.rm-chip-inactive:hover { color:#6b7280; }
|
|
59
|
+
.rm-panel-list { flex:1; overflow-y:auto; padding:12px; display:flex; flex-direction:column; gap:8px; }
|
|
60
|
+
.rm-panel-footer { display:flex; align-items:center; gap:8px; padding:10px 16px; border-top:1px solid #f3f4f6; font-size:11px; color:#9ca3af; }
|
|
61
|
+
.rm-status-dot { width:8px; height:8px; border-radius:50%; background:#d1d5db; }
|
|
62
|
+
.rm-card { padding:12px; background:#fff; border-radius:8px; border:1px solid #f3f4f6; border-left:3px solid; cursor:pointer; transition:all 0.15s; }
|
|
63
|
+
.rm-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.05); }
|
|
64
|
+
.rm-card-top { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
|
65
|
+
.rm-card-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
|
66
|
+
.rm-card-id { font-size:10px; font-weight:600; color:#9ca3af; }
|
|
67
|
+
.rm-card-badge { padding:2px 6px; font-size:10px; font-weight:500; border-radius:10px; }
|
|
68
|
+
.rm-card-body { margin-top:6px; font-size:13px; line-height:1.5; color:#1f2937; }
|
|
69
|
+
.rm-card-path { margin-top:4px; font-size:10px; color:#d1d5db; font-family:monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
70
|
+
.rm-card-thread { margin-top:8px; padding:8px; background:rgba(249,250,251,0.8); border-radius:8px; font-size:12px; color:#6b7280; border-left:2px solid; }
|
|
71
|
+
.rm-card-thread-role { font-size:10px; font-weight:500; color:#9ca3af; text-transform:uppercase; letter-spacing:0.05em; }
|
|
72
|
+
.rm-empty { text-align:center; padding:32px 16px; color:#9ca3af; }
|
|
73
|
+
.rm-empty-icon { font-size:32px; margin-bottom:8px; }
|
|
74
|
+
.rm-empty-text { font-size:13px; }
|
|
75
|
+
.rm-pin { position:absolute; display:flex; align-items:center; justify-content:center; width:20px; height:20px; border-radius:50%; color:#fff; font-size:10px; font-weight:700; cursor:pointer; transition:transform 0.2s; z-index:9979; box-shadow:0 2px 8px rgba(0,0,0,0.2); }
|
|
76
|
+
.rm-pin:hover { transform:scale(1.25); }
|
|
77
|
+
.rm-pin-active { animation:rm-pulse 2s ease-in-out infinite; }
|
|
78
|
+
.rm-toast { padding:8px 12px; border-radius:8px; border:1px solid; font-size:12px; font-weight:500; box-shadow:0 2px 8px rgba(0,0,0,0.05); animation:rm-toast-in 0.3s ease; }
|
|
79
|
+
`;
|
|
80
|
+
document.head.appendChild(style);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Object.assign(RailsMarkupToolbar, {
|
|
2
|
+
_injectDOM() {
|
|
3
|
+
const root = document.createElement("div");
|
|
4
|
+
root.id = "rm-toolbar-root";
|
|
5
|
+
|
|
6
|
+
const accentBg = this._accentBg();
|
|
7
|
+
const accentBgHover = this._accentBgHover();
|
|
8
|
+
const accentLight = this._accentLight();
|
|
9
|
+
const accentText = this._accentText();
|
|
10
|
+
|
|
11
|
+
// Position: bl (bottom-left), br (bottom-right), tl (top-left), tr (top-right)
|
|
12
|
+
const posMap = { bl: "bottom:24px;left:24px;", br: "bottom:24px;right:24px;", tl: "top:24px;left:24px;", tr: "top:24px;right:24px;" };
|
|
13
|
+
const fabPos = posMap[this.position] || posMap.bl;
|
|
14
|
+
|
|
15
|
+
// Size: default (48px), compact (40px), slim (32px)
|
|
16
|
+
const sizeMap = { "default": { dim: 48, icon: 20 }, compact: { dim: 40, icon: 18 }, slim: { dim: 32, icon: 16 } };
|
|
17
|
+
const fabSize = sizeMap[this.size] || sizeMap["default"];
|
|
18
|
+
|
|
19
|
+
// Panel offset matches FAB position
|
|
20
|
+
const isRight = this.position === "br" || this.position === "tr";
|
|
21
|
+
const isTop = this.position === "tl" || this.position === "tr";
|
|
22
|
+
const panelToggleStyle = isRight ? `${isTop ? 'top' : 'bottom'}:24px;right:${fabSize.dim + 8 + 24}px;` : `${isTop ? 'top' : 'bottom'}:24px;left:${fabSize.dim + 8 + 24}px;`;
|
|
23
|
+
const panelStyle = isRight
|
|
24
|
+
? `${isTop ? 'top' : 'bottom'}:${fabSize.dim + 32}px;right:24px;`
|
|
25
|
+
: `${isTop ? 'top' : 'bottom'}:${fabSize.dim + 32}px;left:24px;`;
|
|
26
|
+
const toastStyle = isRight
|
|
27
|
+
? `${isTop ? 'top' : 'bottom'}:${fabSize.dim + 32}px;right:24px;`
|
|
28
|
+
: `${isTop ? 'top' : 'bottom'}:${fabSize.dim + 32}px;left:24px;`;
|
|
29
|
+
|
|
30
|
+
root.innerHTML = `
|
|
31
|
+
<button class="rm-fab" id="rm-fab" style="${fabPos}width:${fabSize.dim}px;height:${fabSize.dim}px;background:${accentBg};color:#fff;${this.fabVisible ? "" : "display:none;"}" title="Toggle annotation mode" aria-label="Toggle annotation mode" aria-expanded="false" aria-controls="rm-panel">
|
|
32
|
+
<svg viewBox="0 0 24 24" style="width:${fabSize.icon}px;height:${fabSize.icon}px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
|
|
33
|
+
<span class="rm-fab-badge" id="rm-fab-badge"></span>
|
|
34
|
+
</button>
|
|
35
|
+
<button class="rm-panel-toggle" id="rm-panel-toggle" style="${panelToggleStyle}" title="View annotations" aria-label="View annotations" aria-controls="rm-panel">
|
|
36
|
+
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h7"/></svg>
|
|
37
|
+
</button>
|
|
38
|
+
<div class="rm-toast-container" id="rm-toast-container" style="${toastStyle}"></div>
|
|
39
|
+
<div class="rm-popup" id="rm-popup" role="dialog" aria-label="Add annotation" aria-modal="false">
|
|
40
|
+
<div style="margin-bottom:12px">
|
|
41
|
+
<p class="rm-popup-el" id="rm-popup-el"></p>
|
|
42
|
+
<p class="rm-popup-text" id="rm-popup-text"></p>
|
|
43
|
+
</div>
|
|
44
|
+
<textarea id="rm-popup-input" rows="3" placeholder="What should change?"></textarea>
|
|
45
|
+
<div style="display:flex;align-items:center;gap:8px;margin-top:12px">
|
|
46
|
+
${this._menuMarkup({ inputId: "rm-intent-select", label: "Intent", value: "change", options: this._intentOptions() })}
|
|
47
|
+
${this._menuMarkup({ inputId: "rm-severity-select", label: "Severity", value: "suggestion", options: this._severityOptions() })}
|
|
48
|
+
<span class="rm-count" id="rm-char-count"></span>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="rm-popup-actions">
|
|
51
|
+
<button class="rm-btn-cancel" id="rm-btn-cancel">Cancel</button>
|
|
52
|
+
<button class="rm-btn-submit" id="rm-btn-submit" style="background:${accentBg}">
|
|
53
|
+
<span id="rm-submit-label">Add</span>
|
|
54
|
+
<kbd>⌘↩</kbd>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="rm-panel" id="rm-panel" style="${panelStyle}" role="dialog" aria-label="Annotations panel">
|
|
59
|
+
<div class="rm-panel-header">
|
|
60
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
61
|
+
<h3>Feedback</h3>
|
|
62
|
+
<span class="rm-panel-count" id="rm-panel-count" style="background:${accentLight};color:${accentText}">0</span>
|
|
63
|
+
</div>
|
|
64
|
+
<button class="rm-panel-close" id="rm-panel-close" aria-label="Close annotations panel">
|
|
65
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18L18 6M6 6l12 12"/></svg>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="rm-filter-chips" id="rm-filter-chips">
|
|
69
|
+
<button class="rm-chip rm-chip-active" data-filter="all" style="background:${accentBg}">All</button>
|
|
70
|
+
<button class="rm-chip rm-chip-inactive" data-filter="pending">Pending</button>
|
|
71
|
+
<button class="rm-chip rm-chip-inactive" data-filter="resolved">Resolved</button>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="rm-panel-list" id="rm-panel-list"></div>
|
|
74
|
+
<div class="rm-panel-footer">
|
|
75
|
+
<span class="rm-status-dot" id="rm-status-dot"></span>
|
|
76
|
+
<span id="rm-status-text">Offline</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
document.body.appendChild(root);
|
|
82
|
+
this.root = root;
|
|
83
|
+
// Pins container lives on body (not inside fixed root); scroll listener keeps them stuck to target elements
|
|
84
|
+
const pinsContainer = document.createElement("div");
|
|
85
|
+
pinsContainer.className = "rm-pins-container";
|
|
86
|
+
pinsContainer.id = "rm-pins-container";
|
|
87
|
+
document.body.appendChild(pinsContainer);
|
|
88
|
+
if (!this._onResize) {
|
|
89
|
+
this._onResize = this._debouncedRepositionPins(250);
|
|
90
|
+
this._onScroll = this._debouncedRepositionPins(50);
|
|
91
|
+
window.addEventListener("resize", this._onResize);
|
|
92
|
+
window.addEventListener("scroll", this._onScroll, { passive: true });
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Object.assign(RailsMarkupToolbar, {
|
|
2
|
+
_intentOptions() {
|
|
3
|
+
return [
|
|
4
|
+
["fix", "Fix"],
|
|
5
|
+
["change", "Change"],
|
|
6
|
+
["question", "Question"],
|
|
7
|
+
["approve", "Approve"]
|
|
8
|
+
];
|
|
9
|
+
},
|
|
10
|
+
_severityOptions() {
|
|
11
|
+
return [
|
|
12
|
+
["suggestion", "Suggestion"],
|
|
13
|
+
["important", "Important"],
|
|
14
|
+
["blocking", "Blocking"]
|
|
15
|
+
];
|
|
16
|
+
},
|
|
17
|
+
_statusOptions() {
|
|
18
|
+
return [
|
|
19
|
+
["pending", "Pending"],
|
|
20
|
+
["acknowledged", "Acknowledged"],
|
|
21
|
+
["resolved", "Resolved"],
|
|
22
|
+
["dismissed", "Dismissed"]
|
|
23
|
+
];
|
|
24
|
+
},
|
|
25
|
+
_menuMarkup({ inputId, statusId, label, value, options, compact }) {
|
|
26
|
+
const current = options.find(([v]) => v === value) || options[0];
|
|
27
|
+
const currentValue = current[0];
|
|
28
|
+
const currentLabel = current[1];
|
|
29
|
+
const menuId = inputId ? `rm-menu-${inputId}` : `rm-menu-status-${statusId}`;
|
|
30
|
+
const listId = `${menuId}-list`;
|
|
31
|
+
const inputAttrs = inputId
|
|
32
|
+
? `id="${inputId}"`
|
|
33
|
+
: `data-status-id="${statusId}"`;
|
|
34
|
+
const optionsHtml = options.map(([v, text]) => {
|
|
35
|
+
const active = v === currentValue;
|
|
36
|
+
return `<button type="button" class="rm-menu-option${active ? " rm-menu-option-active" : ""}" role="option" data-menu-owner="${menuId}" data-value="${v}" aria-selected="${active ? "true" : "false"}">${text}</button>`;
|
|
37
|
+
}).join("");
|
|
38
|
+
return `<div class="rm-menu${compact ? " rm-menu-compact" : ""}" id="${menuId}">` +
|
|
39
|
+
`<input type="hidden" ${inputAttrs} value="${currentValue}">` +
|
|
40
|
+
`<button type="button" class="rm-menu-btn" aria-haspopup="listbox" aria-controls="${listId}" aria-expanded="false" title="${this._esc(label)}" aria-label="${this._esc(label)}">` +
|
|
41
|
+
`<span class="rm-menu-label">${currentLabel}</span>` +
|
|
42
|
+
`<svg class="rm-menu-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>` +
|
|
43
|
+
`</button>` +
|
|
44
|
+
`<div class="rm-menu-list${compact ? " rm-menu-list-compact" : ""}" id="${listId}" role="listbox" data-menu-owner="${menuId}" aria-label="${this._esc(label)}">${optionsHtml}</div>` +
|
|
45
|
+
`</div>`;
|
|
46
|
+
},
|
|
47
|
+
_menuForElement(element) {
|
|
48
|
+
if (!element) return null;
|
|
49
|
+
const nestedMenu = element.closest?.(".rm-menu");
|
|
50
|
+
if (nestedMenu) return nestedMenu;
|
|
51
|
+
const ownerId = element.closest?.("[data-menu-owner]")?.dataset.menuOwner;
|
|
52
|
+
return ownerId ? document.getElementById(ownerId) : null;
|
|
53
|
+
},
|
|
54
|
+
_menuList(menu) {
|
|
55
|
+
if (!menu) return null;
|
|
56
|
+
return menu.querySelector(".rm-menu-list") ||
|
|
57
|
+
document.getElementById(`${menu.id}-list`);
|
|
58
|
+
},
|
|
59
|
+
_selectMenuOption(option) {
|
|
60
|
+
const menu = this._menuForElement(option);
|
|
61
|
+
if (!menu || !this.root?.contains(menu)) return;
|
|
62
|
+
const value = option.dataset.value;
|
|
63
|
+
const statusInput = menu.querySelector("[data-status-id]");
|
|
64
|
+
const statusId = statusInput?.dataset.statusId;
|
|
65
|
+
this._setMenuValue(menu, value);
|
|
66
|
+
if (!statusId) return;
|
|
67
|
+
const id = parseInt(statusId, 10);
|
|
68
|
+
if (Number.isNaN(id)) return;
|
|
69
|
+
this._changeStatus(id, value);
|
|
70
|
+
this.root.querySelector(`[data-status-id="${statusId}"]`)
|
|
71
|
+
?.closest(".rm-menu")
|
|
72
|
+
?.querySelector(".rm-menu-btn")
|
|
73
|
+
?.focus();
|
|
74
|
+
},
|
|
75
|
+
_setMenuValue(inputOrMenu, value, restoreFocus = true) {
|
|
76
|
+
if (!inputOrMenu) return;
|
|
77
|
+
const menu = inputOrMenu.classList?.contains("rm-menu")
|
|
78
|
+
? inputOrMenu
|
|
79
|
+
: inputOrMenu.closest?.(".rm-menu");
|
|
80
|
+
const input = menu
|
|
81
|
+
? menu.querySelector('input[type="hidden"]')
|
|
82
|
+
: inputOrMenu;
|
|
83
|
+
if (!input) return;
|
|
84
|
+
const resolvedMenu = menu || input.closest(".rm-menu");
|
|
85
|
+
input.value = value;
|
|
86
|
+
if (!resolvedMenu) return;
|
|
87
|
+
const list = this._menuList(resolvedMenu);
|
|
88
|
+
const option = list?.querySelector(`.rm-menu-option[data-value="${value}"]`);
|
|
89
|
+
const label = resolvedMenu.querySelector(".rm-menu-label");
|
|
90
|
+
if (label && option) label.textContent = option.textContent;
|
|
91
|
+
list?.querySelectorAll(".rm-menu-option").forEach(opt => {
|
|
92
|
+
const active = opt.dataset.value === value;
|
|
93
|
+
opt.classList.toggle("rm-menu-option-active", active);
|
|
94
|
+
opt.setAttribute("aria-selected", active ? "true" : "false");
|
|
95
|
+
});
|
|
96
|
+
this._closeMenu(resolvedMenu, restoreFocus);
|
|
97
|
+
},
|
|
98
|
+
_toggleMenu(menu) {
|
|
99
|
+
const open = this._menuList(menu)?.classList.contains("rm-menu-open");
|
|
100
|
+
this._closeAllMenus();
|
|
101
|
+
if (!open) this._openMenu(menu);
|
|
102
|
+
},
|
|
103
|
+
_openMenu(menu) {
|
|
104
|
+
const list = this._menuList(menu);
|
|
105
|
+
const btn = menu.querySelector(".rm-menu-btn");
|
|
106
|
+
if (!list || !btn) return;
|
|
107
|
+
this._closeAllMenus();
|
|
108
|
+
const rect = btn.getBoundingClientRect();
|
|
109
|
+
const gap = 4;
|
|
110
|
+
const viewportPadding = 8;
|
|
111
|
+
const compactWidth = menu.classList.contains("rm-menu-compact") ? 120 : 0;
|
|
112
|
+
const minimumWidth = Math.max(rect.width || 0, compactWidth);
|
|
113
|
+
|
|
114
|
+
list._rmMenuPortalOrigin = {
|
|
115
|
+
parent: list.parentNode,
|
|
116
|
+
nextSibling: list.nextSibling
|
|
117
|
+
};
|
|
118
|
+
this.root.appendChild(list);
|
|
119
|
+
list.style.pointerEvents = "auto";
|
|
120
|
+
list.style.position = "fixed";
|
|
121
|
+
list.style.top = "0px";
|
|
122
|
+
list.style.bottom = "";
|
|
123
|
+
list.style.left = "0px";
|
|
124
|
+
list.style.right = "auto";
|
|
125
|
+
list.style.minWidth = `${minimumWidth}px`;
|
|
126
|
+
list.classList.add("rm-menu-open");
|
|
127
|
+
btn.setAttribute("aria-expanded", "true");
|
|
128
|
+
|
|
129
|
+
const menuWidth = Math.max(list.offsetWidth || 0, rect.width || 0, compactWidth);
|
|
130
|
+
const menuHeight = list.offsetHeight || 0;
|
|
131
|
+
const spaceBelow = window.innerHeight - rect.bottom - viewportPadding;
|
|
132
|
+
const spaceAbove = rect.top - viewportPadding;
|
|
133
|
+
const openUp = menuHeight > spaceBelow && spaceAbove > spaceBelow;
|
|
134
|
+
const preferredLeft = compactWidth ? rect.right - menuWidth : rect.left;
|
|
135
|
+
const maxLeft = Math.max(viewportPadding, window.innerWidth - menuWidth - viewportPadding);
|
|
136
|
+
const left = Math.min(Math.max(viewportPadding, preferredLeft), maxLeft);
|
|
137
|
+
|
|
138
|
+
list.style.position = "fixed";
|
|
139
|
+
list.style.top = openUp ? "" : `${rect.bottom + gap}px`;
|
|
140
|
+
list.style.bottom = openUp ? `${window.innerHeight - rect.top + gap}px` : "";
|
|
141
|
+
list.style.left = `${left}px`;
|
|
142
|
+
list.style.right = "auto";
|
|
143
|
+
list.style.minWidth = `${minimumWidth}px`;
|
|
144
|
+
const availableHeight = Math.max(0, (openUp ? spaceAbove : spaceBelow) - gap);
|
|
145
|
+
list.style.maxHeight = availableHeight ? `${availableHeight}px` : "";
|
|
146
|
+
list.style.overflowY = menuHeight > availableHeight && availableHeight > 0 ? "auto" : "";
|
|
147
|
+
|
|
148
|
+
const selected = list.querySelector('.rm-menu-option[aria-selected="true"]') ||
|
|
149
|
+
list.querySelector(".rm-menu-option");
|
|
150
|
+
selected?.focus();
|
|
151
|
+
},
|
|
152
|
+
_closeMenu(menu, restoreFocus = false) {
|
|
153
|
+
const list = this._menuList(menu);
|
|
154
|
+
const btn = menu.querySelector(".rm-menu-btn");
|
|
155
|
+
if (list) {
|
|
156
|
+
list.classList.remove("rm-menu-open");
|
|
157
|
+
["position", "top", "bottom", "left", "right", "minWidth", "maxHeight", "overflowY", "pointerEvents"]
|
|
158
|
+
.forEach(property => { list.style[property] = ""; });
|
|
159
|
+
const origin = list._rmMenuPortalOrigin;
|
|
160
|
+
if (origin?.parent) {
|
|
161
|
+
if (origin.nextSibling?.parentNode === origin.parent) {
|
|
162
|
+
origin.parent.insertBefore(list, origin.nextSibling);
|
|
163
|
+
} else {
|
|
164
|
+
origin.parent.appendChild(list);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
delete list._rmMenuPortalOrigin;
|
|
168
|
+
}
|
|
169
|
+
if (btn) btn.setAttribute("aria-expanded", "false");
|
|
170
|
+
if (restoreFocus) btn?.focus();
|
|
171
|
+
},
|
|
172
|
+
_closeAllMenus() {
|
|
173
|
+
if (!this.root) return;
|
|
174
|
+
this.root.querySelectorAll(".rm-menu").forEach(menu => this._closeMenu(menu));
|
|
175
|
+
},
|
|
176
|
+
});
|