rails-markup 1.4.2 → 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/README.md +18 -0
- 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 -2484
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
|
data/README.md
CHANGED
|
@@ -33,6 +33,24 @@ bin/rails railties:install:migrations FROM=rails_markup
|
|
|
33
33
|
bin/rails db:migrate
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
### Compatibility: `connection_pool` 3.x
|
|
37
|
+
|
|
38
|
+
Rails Markup does **not** use Redis or `connection_pool`. However, Rails 7.2 and
|
|
39
|
+
Rails 8.0 apps configured with `:redis_cache_store` fail to boot against
|
|
40
|
+
`connection_pool` 3.x (released Dec 2025): Active Support passes pool options as
|
|
41
|
+
a positional hash, while `connection_pool` 3 requires keyword arguments, raising
|
|
42
|
+
`ArgumentError: wrong number of arguments (given 1, expected 0)` during
|
|
43
|
+
`Rails.application.initialize!`. This is an upstream Rails/`connection_pool`
|
|
44
|
+
incompatibility (fixed in Rails 8.1), unrelated to this gem — but because it
|
|
45
|
+
breaks boot, it will also break `bin/dev`, `db:migrate`, and the migration-copy
|
|
46
|
+
step above whenever this gem happens to be installed.
|
|
47
|
+
|
|
48
|
+
If you hit it, pin the dependency in your app's Gemfile until you're on Rails 8.1+:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
gem "connection_pool", "< 3.0"
|
|
52
|
+
```
|
|
53
|
+
|
|
36
54
|
### Rails Markup 1.2 rolling UUID upgrade
|
|
37
55
|
|
|
38
56
|
The 1.2 upgrade migration adds, repairs, and uniquely indexes `client_uuid`, but
|
|
@@ -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
|
+
});
|