rails-markup 1.4.2 → 1.4.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34e44f1b30e4c51432e862ee09efd9d1f72e906a41e0e38f9c7553267f737d6f
4
- data.tar.gz: '0909684467d87ed6a1248657ea2fc0e120956b66203c55b47750e74b57971bcb'
3
+ metadata.gz: 385e2a5ce158bd1832873c6edb4ec22f689209b7e5c7ceb33ac500c3a5e4e4fc
4
+ data.tar.gz: be53ae296ff60727333058950f374ebf80a57e2e85a953db216614aeb2837c1b
5
5
  SHA512:
6
- metadata.gz: ea571e4c90037645f9f81ddc6e95710ffd6333addee88f6b43b3352ee8be8bcf9afb9b4d0d2868965f30825c370bf668a6e3a61e7fe345c6ae2469e8e205216c
7
- data.tar.gz: ec0f2ff1ae946b95e7e6c6a7c0fb90429faa8bab7e8cbe30763a09440056760ab9049aed14b296a36c52095f9c1eae5d45941c6a1ccae8bcc346e5f12d03a5d3
6
+ metadata.gz: 33bd8549180fb872d8a5cb07d68ce154e9df3fd82a2e6f36e16f36c434d2f2c82f6583977ca0e623b4c705b0054ed9e60d523db2fcfbab2f601fa5cb4548a690
7
+ data.tar.gz: 78cf0596ec74242a08857c23b1cc27deb2594616a497157b60cb2f561726f628f5fd5c4ed5be40d9e0aa16074300ad024f09caa4a09c86f734a7df83975ee34a
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
@@ -106,6 +106,7 @@
106
106
  },
107
107
 
108
108
  destroy() {
109
+ this._closeAllMenus();
109
110
  this._deactivateMode();
110
111
  if (this.sseSource) { this.sseSource.close(); this.sseSource = null; }
111
112
  if (this.healthInterval) { clearInterval(this.healthInterval); this.healthInterval = null; }
@@ -120,12 +121,27 @@
120
121
  window.removeEventListener("online", this._boundOnline);
121
122
  this._boundOnline = null;
122
123
  }
124
+ if (this._boundKeyDown) {
125
+ document.removeEventListener("keydown", this._boundKeyDown, true);
126
+ this._boundKeyDown = null;
127
+ }
123
128
  if (this._onResize) window.removeEventListener("resize", this._onResize);
124
129
  if (this._onScroll) window.removeEventListener("scroll", this._onScroll);
125
130
  if (this._boundTurboFrame) {
126
131
  document.removeEventListener("turbo:frame-render", this._boundTurboFrame);
127
132
  this._boundTurboFrame = null;
128
133
  }
134
+ if (this._boundMenuDocClick) {
135
+ document.removeEventListener("click", this._boundMenuDocClick);
136
+ this._boundMenuDocClick = null;
137
+ }
138
+ if (this._boundMenuViewportChange) {
139
+ document.getElementById("rm-panel-list")
140
+ ?.removeEventListener("scroll", this._boundMenuViewportChange);
141
+ window.removeEventListener("resize", this._boundMenuViewportChange);
142
+ window.removeEventListener("scroll", this._boundMenuViewportChange);
143
+ this._boundMenuViewportChange = null;
144
+ }
129
145
  this._onResize = null;
130
146
  this._onScroll = null;
131
147
  const root = document.getElementById("rm-toolbar-root");
@@ -159,19 +175,30 @@
159
175
  .rm-pins-container { position:absolute; top:0; left:0; width:100%; z-index:9979; pointer-events:none; }
160
176
  .rm-pin { pointer-events:auto; }
161
177
  .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; }
162
- .rm-popup textarea { width:100%; font-size:13px; border:1px solid #e5e7eb; border-radius:12px; padding:12px; resize:none; outline:none; font-family:inherit; transition:border-color 0.15s,box-shadow 0.15s; }
163
- .rm-popup textarea:focus { border-color:#818cf8; box-shadow:0 0 0 3px rgba(99,102,241,0.1); }
164
- #rm-toolbar-root .rm-popup select { display:inline-block; width:auto; visibility:visible; opacity:1; font-size:11px; font-weight:500; line-height:1.4; color:#374151; height:auto; margin:0; border:1px solid #e5e7eb; border-top:1px solid #e5e7eb; border-right:1px solid #e5e7eb; border-bottom:1px solid #e5e7eb; border-left:1px solid #e5e7eb; border-radius:8px; padding:6px 24px 6px 8px; background:#fff; background-image:none; box-shadow:none; outline:none; text-transform:none; appearance:none; -webkit-appearance:none; -moz-appearance:none; cursor:pointer; }
165
- #rm-toolbar-root .rm-popup select:focus { outline:none; border:1px solid #818cf8; box-shadow:0 0 0 3px rgba(99,102,241,0.1); }
166
- #rm-toolbar-root .rm-popup select:hover { border:1px solid #d1d5db; background:#fff; }
178
+ #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; }
179
+ #rm-toolbar-root .rm-popup textarea:focus { border:1px solid #818cf8; box-shadow:0 0 0 3px rgba(99,102,241,0.1); }
180
+ #rm-toolbar-root .rm-menu { position:relative; display:inline-block; vertical-align:middle; }
181
+ #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; }
182
+ #rm-toolbar-root .rm-menu-btn:hover { border-color:#d1d5db; background:#fff; }
183
+ #rm-toolbar-root .rm-menu-btn:focus { outline:none; border-color:#818cf8; box-shadow:0 0 0 3px rgba(99,102,241,0.1); }
184
+ #rm-toolbar-root .rm-menu-btn[aria-expanded="true"] { border-color:#818cf8; }
185
+ #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; }
186
+ #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); }
187
+ #rm-toolbar-root .rm-menu-list.rm-menu-open { display:block; }
188
+ #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; }
189
+ #rm-toolbar-root .rm-menu-option:hover, #rm-toolbar-root .rm-menu-option:focus { background:#f3f4f6; outline:none; }
190
+ #rm-toolbar-root .rm-menu-option-active { background:#eef2ff; color:#4338ca; }
191
+ #rm-toolbar-root .rm-menu-compact .rm-menu-btn { font-size:10px; padding:2px 6px; border-radius:4px; color:#6b7280; }
192
+ #rm-toolbar-root .rm-menu-compact .rm-menu-list, #rm-toolbar-root .rm-menu-list-compact { min-width:120px; right:0; left:auto; }
193
+ #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; }
167
194
  .rm-popup-el { font-size:11px; color:#9ca3af; font-family:monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; line-height:1.4; }
168
195
  .rm-popup-text { font-size:12px; color:#6b7280; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:2px; line-height:1.4; }
169
196
  .rm-popup-actions { display:flex; align-items:center; gap:8px; margin-top:8px; }
170
197
  .rm-popup-actions .rm-count { font-size:10px; color:#d1d5db; margin-left:auto; font-variant-numeric:tabular-nums; }
171
- .rm-btn-cancel { padding:6px 12px; font-size:12px; color:#9ca3af; background:none; border:none; cursor:pointer; border-radius:8px; }
172
- .rm-btn-cancel:hover { color:#6b7280; }
173
- .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; }
174
- .rm-btn-submit kbd { font-size:9px; opacity:0.6; font-family:sans-serif; }
198
+ #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; }
199
+ #rm-toolbar-root .rm-btn-cancel:hover { color:#6b7280; }
200
+ #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; }
201
+ #rm-toolbar-root .rm-btn-submit kbd { font-size:9px; opacity:0.6; font-family:sans-serif; }
175
202
  .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; }
176
203
  .rm-panel-header { display:flex; align-items:center; justify-content:space-between; padding:12px 16px; border-bottom:1px solid #f3f4f6; }
177
204
  .rm-panel-header h3 { font-size:14px; font-weight:600; color:#1f2937; }
@@ -252,17 +279,8 @@
252
279
  </div>
253
280
  <textarea id="rm-popup-input" rows="3" placeholder="What should change?"></textarea>
254
281
  <div style="display:flex;align-items:center;gap:8px;margin-top:12px">
255
- <select id="rm-intent-select">
256
- <option value="fix">Fix</option>
257
- <option value="change" selected>Change</option>
258
- <option value="question">Question</option>
259
- <option value="approve">Approve</option>
260
- </select>
261
- <select id="rm-severity-select">
262
- <option value="suggestion" selected>Suggestion</option>
263
- <option value="important">Important</option>
264
- <option value="blocking">Blocking</option>
265
- </select>
282
+ ${this._menuMarkup({ inputId: "rm-intent-select", label: "Intent", value: "change", options: this._intentOptions() })}
283
+ ${this._menuMarkup({ inputId: "rm-severity-select", label: "Severity", value: "suggestion", options: this._severityOptions() })}
266
284
  <span class="rm-count" id="rm-char-count"></span>
267
285
  </div>
268
286
  <div class="rm-popup-actions">
@@ -324,15 +342,43 @@
324
342
  if (chip) self._filterAnnotations(chip.dataset.filter);
325
343
  });
326
344
 
345
+ // Custom menus (intent/severity/status) — button+menu, never native selects
346
+ // so host FormSelect/Select2 enhancers cannot rewrite our DOM (#4).
347
+ this.root.addEventListener("click", (e) => {
348
+ const option = e.target.closest(".rm-menu-option");
349
+ if (option) {
350
+ e.preventDefault();
351
+ e.stopPropagation();
352
+ this._selectMenuOption(option);
353
+ return;
354
+ }
355
+ const btn = e.target.closest(".rm-menu-btn");
356
+ if (btn) {
357
+ e.preventDefault();
358
+ e.stopPropagation();
359
+ const menu = this._menuForElement(btn);
360
+ if (!menu || !this.root.contains(menu)) return;
361
+ this._toggleMenu(menu);
362
+ return;
363
+ }
364
+ if (!this._menuForElement(e.target)) this._closeAllMenus();
365
+ });
366
+ if (!this._boundMenuDocClick) {
367
+ this._boundMenuDocClick = (e) => {
368
+ if (!this.root || this._menuForElement(e.target)) return;
369
+ this._closeAllMenus();
370
+ };
371
+ document.addEventListener("click", this._boundMenuDocClick);
372
+ }
373
+
327
374
  // Event delegation for cards (status change, edit, delete, or click scrolls to element)
328
375
  const panelList = document.getElementById("rm-panel-list");
329
- panelList.addEventListener("change", (e) => {
330
- const select = e.target.closest("[data-status-id]");
331
- if (!select) return;
332
- e.stopPropagation();
333
- const id = parseInt(select.dataset.statusId, 10);
334
- self._changeStatus(id, select.value);
335
- });
376
+ if (!this._boundMenuViewportChange) {
377
+ this._boundMenuViewportChange = () => self._closeAllMenus();
378
+ window.addEventListener("resize", this._boundMenuViewportChange);
379
+ window.addEventListener("scroll", this._boundMenuViewportChange, { passive: true });
380
+ }
381
+ panelList.addEventListener("scroll", this._boundMenuViewportChange, { passive: true });
336
382
  panelList.addEventListener("click", (e) => {
337
383
  const retryBtn = e.target.closest("[data-retry-client-id]");
338
384
  if (retryBtn) {
@@ -340,8 +386,8 @@
340
386
  self._retrySync(retryBtn.dataset.retryClientId);
341
387
  return;
342
388
  }
343
- // Status dropdown click — don't scroll
344
- if (e.target.closest("[data-status-id]")) { e.stopPropagation(); return; }
389
+ // Status menuleave bubbling for the root menu handler; don't scroll.
390
+ if (e.target.closest(".rm-menu")) return;
345
391
  // Edit button
346
392
  const editBtn = e.target.closest("[data-edit-id]");
347
393
  if (editBtn) {
@@ -382,7 +428,10 @@
382
428
  this._boundMouseDown = (e) => self._handleMouseDown(e);
383
429
  this._boundMouseUp = (e) => self._handleMouseUp(e);
384
430
  this._boundClick = (e) => self._handleClick(e);
385
- this._boundKeyDown = (e) => self._handleKeyDown(e);
431
+ if (!this._boundKeyDown) {
432
+ this._boundKeyDown = (e) => self._handleKeyDown(e);
433
+ document.addEventListener("keydown", this._boundKeyDown, true);
434
+ }
386
435
  this._boundTouchStart = (e) => { if (self.active && e.touches[0]) { const t = e.touches[0]; const el = document.elementFromPoint(t.clientX, t.clientY); if (el && !self._isToolbar(el) && e.cancelable) e.preventDefault(); self._handleMouseDown({ clientX: t.clientX, clientY: t.clientY }); } };
387
436
  this._boundTouchEnd = (e) => { if (self.active && e.changedTouches[0]) { const t = e.changedTouches[0]; const el = document.elementFromPoint(t.clientX, t.clientY); if (el && !self._isToolbar(el)) { e.preventDefault(); self._handleMouseUp({ clientX: t.clientX, clientY: t.clientY, preventDefault(){}, stopPropagation(){} }); } } };
388
437
 
@@ -458,7 +507,6 @@
458
507
  document.addEventListener("mousedown", this._boundMouseDown, true);
459
508
  document.addEventListener("mouseup", this._boundMouseUp, true);
460
509
  document.addEventListener("click", this._boundClick, true);
461
- document.addEventListener("keydown", this._boundKeyDown, true);
462
510
  document.addEventListener("touchstart", this._boundTouchStart, true);
463
511
  document.addEventListener("touchend", this._boundTouchEnd, true);
464
512
  },
@@ -478,7 +526,6 @@
478
526
  document.removeEventListener("mousedown", this._boundMouseDown, true);
479
527
  document.removeEventListener("mouseup", this._boundMouseUp, true);
480
528
  document.removeEventListener("click", this._boundClick, true);
481
- document.removeEventListener("keydown", this._boundKeyDown, true);
482
529
  document.removeEventListener("touchstart", this._boundTouchStart, true);
483
530
  document.removeEventListener("touchend", this._boundTouchEnd, true);
484
531
  this._removeHighlight();
@@ -551,12 +598,69 @@
551
598
  },
552
599
 
553
600
  _handleKeyDown(event) {
601
+ const openList = this.root?.querySelector(".rm-menu-list.rm-menu-open");
602
+ const openMenu = this._menuForElement(openList);
603
+ if (openMenu) {
604
+ const options = Array.from(openList.querySelectorAll(".rm-menu-option"));
605
+ const focusedIndex = options.indexOf(document.activeElement);
606
+
607
+ if (event.key === "Escape") {
608
+ this._closeMenu(openMenu, true);
609
+ event.preventDefault();
610
+ event.stopPropagation();
611
+ return;
612
+ }
613
+ if (event.key === "Tab") {
614
+ this._closeMenu(openMenu);
615
+ return;
616
+ }
617
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
618
+ const direction = event.key === "ArrowDown" ? 1 : -1;
619
+ const start = focusedIndex === -1 ? 0 : focusedIndex;
620
+ const next = (start + direction + options.length) % options.length;
621
+ options[next]?.focus();
622
+ event.preventDefault();
623
+ event.stopPropagation();
624
+ return;
625
+ }
626
+ if (event.key === "Home" || event.key === "End") {
627
+ const option = event.key === "Home" ? options[0] : options[options.length - 1];
628
+ option?.focus();
629
+ event.preventDefault();
630
+ event.stopPropagation();
631
+ return;
632
+ }
633
+ if ((event.key === "Enter" || event.key === " ") && focusedIndex !== -1) {
634
+ this._selectMenuOption(options[focusedIndex]);
635
+ event.preventDefault();
636
+ event.stopPropagation();
637
+ return;
638
+ }
639
+ }
640
+
641
+ const trigger = event.target.closest?.(".rm-menu-btn");
642
+ if (trigger && this.root?.contains(trigger) &&
643
+ ["Enter", " ", "ArrowDown", "ArrowUp"].includes(event.key)) {
644
+ const menu = this._menuForElement(trigger);
645
+ if (menu) this._openMenu(menu);
646
+ event.preventDefault();
647
+ event.stopPropagation();
648
+ return;
649
+ }
650
+
554
651
  if (event.key === "Escape") {
555
652
  const popup = document.getElementById("rm-popup");
556
653
  if (popup && popup.style.display === "block") {
557
654
  this._closePopup();
558
- } else {
655
+ event.preventDefault();
656
+ event.stopPropagation();
657
+ return;
658
+ }
659
+ if (this.active) {
559
660
  this._deactivateMode();
661
+ event.preventDefault();
662
+ event.stopPropagation();
663
+ return;
560
664
  }
561
665
  }
562
666
  if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
@@ -602,6 +706,192 @@
602
706
 
603
707
  // ---- Popup ----
604
708
 
709
+ _intentOptions() {
710
+ return [
711
+ ["fix", "Fix"],
712
+ ["change", "Change"],
713
+ ["question", "Question"],
714
+ ["approve", "Approve"]
715
+ ];
716
+ },
717
+
718
+ _severityOptions() {
719
+ return [
720
+ ["suggestion", "Suggestion"],
721
+ ["important", "Important"],
722
+ ["blocking", "Blocking"]
723
+ ];
724
+ },
725
+
726
+ _statusOptions() {
727
+ return [
728
+ ["pending", "Pending"],
729
+ ["acknowledged", "Acknowledged"],
730
+ ["resolved", "Resolved"],
731
+ ["dismissed", "Dismissed"]
732
+ ];
733
+ },
734
+
735
+ _menuMarkup({ inputId, statusId, label, value, options, compact }) {
736
+ const current = options.find(([v]) => v === value) || options[0];
737
+ const currentValue = current[0];
738
+ const currentLabel = current[1];
739
+ const menuId = inputId ? `rm-menu-${inputId}` : `rm-menu-status-${statusId}`;
740
+ const listId = `${menuId}-list`;
741
+ const inputAttrs = inputId
742
+ ? `id="${inputId}"`
743
+ : `data-status-id="${statusId}"`;
744
+ const optionsHtml = options.map(([v, text]) => {
745
+ const active = v === currentValue;
746
+ 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>`;
747
+ }).join("");
748
+ return `<div class="rm-menu${compact ? " rm-menu-compact" : ""}" id="${menuId}">` +
749
+ `<input type="hidden" ${inputAttrs} value="${currentValue}">` +
750
+ `<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)}">` +
751
+ `<span class="rm-menu-label">${currentLabel}</span>` +
752
+ `<svg class="rm-menu-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>` +
753
+ `</button>` +
754
+ `<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>` +
755
+ `</div>`;
756
+ },
757
+
758
+ _menuForElement(element) {
759
+ if (!element) return null;
760
+ const nestedMenu = element.closest?.(".rm-menu");
761
+ if (nestedMenu) return nestedMenu;
762
+ const ownerId = element.closest?.("[data-menu-owner]")?.dataset.menuOwner;
763
+ return ownerId ? document.getElementById(ownerId) : null;
764
+ },
765
+
766
+ _menuList(menu) {
767
+ if (!menu) return null;
768
+ return menu.querySelector(".rm-menu-list") ||
769
+ document.getElementById(`${menu.id}-list`);
770
+ },
771
+
772
+ _selectMenuOption(option) {
773
+ const menu = this._menuForElement(option);
774
+ if (!menu || !this.root?.contains(menu)) return;
775
+ const value = option.dataset.value;
776
+ const statusInput = menu.querySelector("[data-status-id]");
777
+ const statusId = statusInput?.dataset.statusId;
778
+ this._setMenuValue(menu, value);
779
+ if (!statusId) return;
780
+ const id = parseInt(statusId, 10);
781
+ if (Number.isNaN(id)) return;
782
+ this._changeStatus(id, value);
783
+ this.root.querySelector(`[data-status-id="${statusId}"]`)
784
+ ?.closest(".rm-menu")
785
+ ?.querySelector(".rm-menu-btn")
786
+ ?.focus();
787
+ },
788
+
789
+ _setMenuValue(inputOrMenu, value, restoreFocus = true) {
790
+ if (!inputOrMenu) return;
791
+ const menu = inputOrMenu.classList?.contains("rm-menu")
792
+ ? inputOrMenu
793
+ : inputOrMenu.closest?.(".rm-menu");
794
+ const input = menu
795
+ ? menu.querySelector('input[type="hidden"]')
796
+ : inputOrMenu;
797
+ if (!input) return;
798
+ const resolvedMenu = menu || input.closest(".rm-menu");
799
+ input.value = value;
800
+ if (!resolvedMenu) return;
801
+ const list = this._menuList(resolvedMenu);
802
+ const option = list?.querySelector(`.rm-menu-option[data-value="${value}"]`);
803
+ const label = resolvedMenu.querySelector(".rm-menu-label");
804
+ if (label && option) label.textContent = option.textContent;
805
+ list?.querySelectorAll(".rm-menu-option").forEach(opt => {
806
+ const active = opt.dataset.value === value;
807
+ opt.classList.toggle("rm-menu-option-active", active);
808
+ opt.setAttribute("aria-selected", active ? "true" : "false");
809
+ });
810
+ this._closeMenu(resolvedMenu, restoreFocus);
811
+ },
812
+
813
+ _toggleMenu(menu) {
814
+ const open = this._menuList(menu)?.classList.contains("rm-menu-open");
815
+ this._closeAllMenus();
816
+ if (!open) this._openMenu(menu);
817
+ },
818
+
819
+ _openMenu(menu) {
820
+ const list = this._menuList(menu);
821
+ const btn = menu.querySelector(".rm-menu-btn");
822
+ if (!list || !btn) return;
823
+ this._closeAllMenus();
824
+ const rect = btn.getBoundingClientRect();
825
+ const gap = 4;
826
+ const viewportPadding = 8;
827
+ const compactWidth = menu.classList.contains("rm-menu-compact") ? 120 : 0;
828
+ const minimumWidth = Math.max(rect.width || 0, compactWidth);
829
+
830
+ list._rmMenuPortalOrigin = {
831
+ parent: list.parentNode,
832
+ nextSibling: list.nextSibling
833
+ };
834
+ this.root.appendChild(list);
835
+ list.style.pointerEvents = "auto";
836
+ list.style.position = "fixed";
837
+ list.style.top = "0px";
838
+ list.style.bottom = "";
839
+ list.style.left = "0px";
840
+ list.style.right = "auto";
841
+ list.style.minWidth = `${minimumWidth}px`;
842
+ list.classList.add("rm-menu-open");
843
+ btn.setAttribute("aria-expanded", "true");
844
+
845
+ const menuWidth = Math.max(list.offsetWidth || 0, rect.width || 0, compactWidth);
846
+ const menuHeight = list.offsetHeight || 0;
847
+ const spaceBelow = window.innerHeight - rect.bottom - viewportPadding;
848
+ const spaceAbove = rect.top - viewportPadding;
849
+ const openUp = menuHeight > spaceBelow && spaceAbove > spaceBelow;
850
+ const preferredLeft = compactWidth ? rect.right - menuWidth : rect.left;
851
+ const maxLeft = Math.max(viewportPadding, window.innerWidth - menuWidth - viewportPadding);
852
+ const left = Math.min(Math.max(viewportPadding, preferredLeft), maxLeft);
853
+
854
+ list.style.position = "fixed";
855
+ list.style.top = openUp ? "" : `${rect.bottom + gap}px`;
856
+ list.style.bottom = openUp ? `${window.innerHeight - rect.top + gap}px` : "";
857
+ list.style.left = `${left}px`;
858
+ list.style.right = "auto";
859
+ list.style.minWidth = `${minimumWidth}px`;
860
+ const availableHeight = Math.max(0, (openUp ? spaceAbove : spaceBelow) - gap);
861
+ list.style.maxHeight = availableHeight ? `${availableHeight}px` : "";
862
+ list.style.overflowY = menuHeight > availableHeight && availableHeight > 0 ? "auto" : "";
863
+
864
+ const selected = list.querySelector('.rm-menu-option[aria-selected="true"]') ||
865
+ list.querySelector(".rm-menu-option");
866
+ selected?.focus();
867
+ },
868
+
869
+ _closeMenu(menu, restoreFocus = false) {
870
+ const list = this._menuList(menu);
871
+ const btn = menu.querySelector(".rm-menu-btn");
872
+ if (list) {
873
+ list.classList.remove("rm-menu-open");
874
+ ["position", "top", "bottom", "left", "right", "minWidth", "maxHeight", "overflowY", "pointerEvents"]
875
+ .forEach(property => { list.style[property] = ""; });
876
+ const origin = list._rmMenuPortalOrigin;
877
+ if (origin?.parent) {
878
+ if (origin.nextSibling?.parentNode === origin.parent) {
879
+ origin.parent.insertBefore(list, origin.nextSibling);
880
+ } else {
881
+ origin.parent.appendChild(list);
882
+ }
883
+ }
884
+ delete list._rmMenuPortalOrigin;
885
+ }
886
+ if (btn) btn.setAttribute("aria-expanded", "false");
887
+ if (restoreFocus) btn?.focus();
888
+ },
889
+
890
+ _closeAllMenus() {
891
+ if (!this.root) return;
892
+ this.root.querySelectorAll(".rm-menu").forEach(menu => this._closeMenu(menu));
893
+ },
894
+
605
895
  _showPopup(x, y) {
606
896
  const popup = document.getElementById("rm-popup");
607
897
  // Clean up previous drawing elements
@@ -638,10 +928,11 @@
638
928
  : this._currentElement.nearbyText.slice(0, 60);
639
929
  const input = document.getElementById("rm-popup-input");
640
930
  input.value = "";
641
- document.getElementById("rm-intent-select").value = "change";
642
- document.getElementById("rm-severity-select").value = "suggestion";
931
+ this._setMenuValue(document.getElementById("rm-intent-select"), "change", false);
932
+ this._setMenuValue(document.getElementById("rm-severity-select"), "suggestion", false);
643
933
  document.getElementById("rm-char-count").textContent = "";
644
934
  document.getElementById("rm-submit-label").textContent = "Add";
935
+ this._closeAllMenus();
645
936
 
646
937
  // Show screenshot preview with drawing tools
647
938
  if (this._currentScreenshot) {
@@ -652,6 +943,7 @@
652
943
  },
653
944
 
654
945
  _closePopup() {
946
+ this._closeAllMenus();
655
947
  const popup = document.getElementById("rm-popup");
656
948
  popup.style.transition = "opacity 0.15s ease";
657
949
  popup.style.opacity = "0";
@@ -806,12 +1098,13 @@
806
1098
  <span class="rm-card-badge" style="background:${ic.bg};color:${ic.text}">${annotation.intent}</span>
807
1099
  ${annotation.severity !== "suggestion" ? '<span class="rm-card-badge" style="background:#fff7ed;color:#9a3412">' + annotation.severity + '</span>' : ''}
808
1100
  <span style="margin-left:auto;display:flex;gap:2px;align-items:center;">
809
- <select data-status-id="${annotation.id}" title="Change status" style="font-size:10px;padding:2px 4px;border:1px solid #e5e7eb;border-radius:4px;background:#fff;color:#6b7280;cursor:pointer;appearance:auto;">
810
- <option value="pending"${annotation.status === "pending" ? " selected" : ""}>Pending</option>
811
- <option value="acknowledged"${annotation.status === "acknowledged" ? " selected" : ""}>Acknowledged</option>
812
- <option value="resolved"${annotation.status === "resolved" ? " selected" : ""}>Resolved</option>
813
- <option value="dismissed"${annotation.status === "dismissed" ? " selected" : ""}>Dismissed</option>
814
- </select>
1101
+ ${this._menuMarkup({
1102
+ statusId: annotation.id,
1103
+ label: "Change status",
1104
+ value: annotation.status,
1105
+ options: this._statusOptions(),
1106
+ compact: true
1107
+ })}
815
1108
  <button data-edit-id="${annotation.id}" title="Edit" style="padding:2px 4px;background:none;border:none;cursor:pointer;color:#d1d5db;border-radius:4px;display:flex;align-items:center;" onmouseover="this.style.color='#6b7280'" onmouseout="this.style.color='#d1d5db'">
816
1109
  <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>
817
1110
  </button>
@@ -832,6 +1125,7 @@
832
1125
  _rebuildList() {
833
1126
  const list = document.getElementById("rm-panel-list");
834
1127
  if (!list) return;
1128
+ this._closeAllMenus();
835
1129
  list.innerHTML = "";
836
1130
  this._renderStorageError(list);
837
1131
  this._renderFailedSync(list);
@@ -886,10 +1180,11 @@
886
1180
  ? '"' + annotation.selectedText.slice(0, 60) + '"'
887
1181
  : (annotation.element?.nearbyText || "").slice(0, 60);
888
1182
  document.getElementById("rm-popup-input").value = annotation.comment;
889
- document.getElementById("rm-intent-select").value = annotation.intent;
890
- document.getElementById("rm-severity-select").value = annotation.severity;
1183
+ this._setMenuValue(document.getElementById("rm-intent-select"), annotation.intent, false);
1184
+ this._setMenuValue(document.getElementById("rm-severity-select"), annotation.severity, false);
891
1185
  document.getElementById("rm-submit-label").textContent = "Save";
892
1186
  this._updateCharCount();
1187
+ this._closeAllMenus();
893
1188
 
894
1189
  // Clean up previous drawing elements
895
1190
  const popup = document.getElementById("rm-popup");
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsMarkup
4
- VERSION = "1.4.2"
4
+ VERSION = "1.4.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-markup
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.2
4
+ version: 1.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - InventList