rails-markup 1.4.5 → 1.4.7

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: f116e4650e408eb4c0157a1ecdfe528025dcc1fe56e48d56d5d878e6aeb76625
4
- data.tar.gz: 989ea2f307e753b10c68e257b8206b7b3fe8fef47b1d3eeace916e1fc50f8306
3
+ metadata.gz: 66260d5eb5dfb34aee5daa7f51a9a0649a1f241d6dfc3d0bf55fba7435727491
4
+ data.tar.gz: c4461eba4189f4351502bbf2fbcdef57387d6183570ef19c950538edb97c91ee
5
5
  SHA512:
6
- metadata.gz: dbcf8ce0f1c775122d1b58440dd648c7e02f22c63f25a0817d514e4951078ee283d859d3c7ac6ee5da37695feaaa756411d368b450a9f14be09050f6a99bf14f
7
- data.tar.gz: 4b2d4640b67a39c5e8ce41a99481afe6cf153112657919d9e2b3160df732c319fb34ecbbd458db32974bdb545b68d17a4c3ab04dcc62095b04bcb58f9a17a480
6
+ metadata.gz: a74b3f974b9124593cd889a78702ebeb466a83f9ca9763102499a4898246b0b0cecca3b53a5af77754d848188cd161f5e90c50a10d6d5b4962b309e343309850
7
+ data.tar.gz: d1f9f8910a108fe314da756afedaf9bd0b46d8d780603adb034d960e17e34cbf4823a2ede9832afb4199ebdf1042d071ef89586051fd5e3731ba878acb1a4457
data/README.md CHANGED
@@ -81,14 +81,14 @@ The generator creates:
81
81
  | `config/initializers/rails_markup.rb` | Configuration |
82
82
  | `app/controllers/rails_markup_auth_controller.rb` | Auth controller |
83
83
  | `bin/markup` | CLI wrapper |
84
- | Route mount | Engine at `/admin/annotations` |
84
+ | Route mount | Engine at `/admin/rails-markup` |
85
85
  | Toolbar injection | Admin-gated `render "rails_markup/shared/toolbar"` in your layout |
86
86
 
87
87
  ### Generator options
88
88
 
89
89
  ```bash
90
90
  rails generate rails_markup:install \
91
- --mount-path=/admin/annotations \
91
+ --mount-path=/admin/rails-markup \
92
92
  --base-controller=Admin::BaseController \
93
93
  --layout=application
94
94
  ```
@@ -169,7 +169,7 @@ end
169
169
 
170
170
  ## Dashboard
171
171
 
172
- Visit `/admin/annotations` for the full dashboard:
172
+ Visit `/admin/rails-markup` for the full dashboard:
173
173
 
174
174
  - **List view** — status filters, search, author filter, load-more pagination
175
175
  - **Board view** — kanban with drag-and-drop status transitions
@@ -27,6 +27,7 @@
27
27
  activeFilter: "all",
28
28
  editingId: null,
29
29
  _currentScreenshot: null,
30
+ toolbarSettings: {},
30
31
  _storageError: null,
31
32
  _outboxFlushScheduled: false,
32
33
  _outboxFlushTimer: null,
@@ -44,6 +45,8 @@
44
45
  _syncMalformedLimit: 3,
45
46
  _syncUnavailable: null,
46
47
  legacyStorageEndpoint: null,
48
+ _bootstrapOptions: null,
49
+ _settingsPanelOpen: false,
47
50
 
48
51
  // Drawing state
49
52
  drawingMode: null, // null | "arrow" | "rect" | "highlight"
@@ -9,13 +9,17 @@
9
9
  @keyframes rm-toast-out { from{opacity:1;transform:translateY(0) scale(1)} to{opacity:0;transform:translateY(16px) scale(0.95)} }
10
10
  #rm-toolbar-root { position:fixed; inset:0; pointer-events:none; z-index:9979; }
11
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; }
12
+ .rm-fab, .rm-panel-toggle, .rm-toolbar-settings, .rm-panel, .rm-popup { pointer-events:auto; }
13
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
14
  .rm-fab svg { width:20px; height:20px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
15
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
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
17
  .rm-panel-toggle:hover { color:#4361ee; }
18
18
  .rm-panel-toggle svg { width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
19
+ .rm-panel-toggle-badge { position:absolute; top:-4px; right:-4px; min-width:18px; height:18px; padding:0 4px; border-radius:9px; background:#ef4444; color:#fff; font-size:10px; font-weight:700; display:none; align-items:center; justify-content:center; }
20
+ .rm-toolbar-settings { position:fixed; z-index:9980; width:32px; height:32px; border-radius:50%; border:1px solid #e5e7eb; background:#fff; cursor:pointer; display:none; align-items:center; justify-content:center; color:#6b7280; transition:all 0.2s; backdrop-filter:blur(8px); }
21
+ .rm-toolbar-settings:hover { color:#374151; background:#f9fafb; }
22
+ .rm-toolbar-settings svg { width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
19
23
  .rm-toast-container { position:fixed; z-index:9983; display:flex; flex-direction:column; gap:8px; pointer-events:none; }
20
24
  .rm-pins-container { position:absolute; top:0; left:0; width:100%; z-index:9979; pointer-events:none; }
21
25
  .rm-pin { pointer-events:auto; }
@@ -47,7 +51,13 @@
47
51
  .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
52
  .rm-panel-header { display:flex; align-items:center; justify-content:space-between; padding:12px 16px; border-bottom:1px solid #f3f4f6; }
49
53
  .rm-panel-header h3 { font-size:14px; font-weight:600; color:#1f2937; }
54
+ .rm-panel-header-actions { display:flex; align-items:center; gap:6px; }
50
55
  .rm-panel-count { min-width:20px; text-align:center; padding:2px 6px; font-size:10px; font-weight:600; border-radius:10px; }
56
+ .rm-settings { padding:12px 16px 4px; border-bottom:1px solid #f3f4f6; }
57
+ .rm-settings[hidden] { display:none; }
58
+ .rm-settings-group { display:flex; flex-direction:column; gap:6px; margin-bottom:10px; }
59
+ .rm-settings-label { font-size:11px; font-weight:600; color:#6b7280; text-transform:uppercase; letter-spacing:0.05em; }
60
+ .rm-settings-options { display:flex; flex-wrap:wrap; gap:6px; }
51
61
  .rm-panel-close { padding:6px; color:#d1d5db; background:none; border:none; cursor:pointer; border-radius:8px; }
52
62
  .rm-panel-close:hover { color:#6b7280; }
53
63
  .rm-panel-close svg { width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
@@ -12,14 +12,17 @@
12
12
  const posMap = { bl: "bottom:24px;left:24px;", br: "bottom:24px;right:24px;", tl: "top:24px;left:24px;", tr: "top:24px;right:24px;" };
13
13
  const fabPos = posMap[this.position] || posMap.bl;
14
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 } };
15
+ // Size: default (40px), compact (36px), slim (32px)
16
+ const sizeMap = { "default": { dim: 40, icon: 18 }, compact: { dim: 36, icon: 16 }, slim: { dim: 32, icon: 16 } };
17
17
  const fabSize = sizeMap[this.size] || sizeMap["default"];
18
18
 
19
- // Panel offset matches FAB position
19
+ const controlSize = 32;
20
+ const controlGap = 8;
21
+ const controlStart = this.fabVisible ? fabSize.dim + controlGap + 24 : 24;
20
22
  const isRight = this.position === "br" || this.position === "tr";
21
23
  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;`;
24
+ const settingsToggleStyle = isRight ? `${isTop ? 'top' : 'bottom'}:24px;right:${controlStart}px;` : `${isTop ? 'top' : 'bottom'}:24px;left:${controlStart}px;`;
25
+ const panelToggleStyle = isRight ? `${isTop ? 'top' : 'bottom'}:24px;right:${controlStart + controlSize + controlGap}px;` : `${isTop ? 'top' : 'bottom'}:24px;left:${controlStart + controlSize + controlGap}px;`;
23
26
  const panelStyle = isRight
24
27
  ? `${isTop ? 'top' : 'bottom'}:${fabSize.dim + 32}px;right:24px;`
25
28
  : `${isTop ? 'top' : 'bottom'}:${fabSize.dim + 32}px;left:24px;`;
@@ -30,10 +33,13 @@
30
33
  root.innerHTML = `
31
34
  <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
35
  <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>
36
+ </button>
37
+ <button class="rm-toolbar-settings" id="rm-settings-toggle" style="${settingsToggleStyle}" title="Toolbar settings" aria-label="Toolbar settings" aria-expanded="false" aria-controls="rm-settings-panel">
38
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8a4 4 0 100 8 4 4 0 000-8zm8.94 3.06l-1.3-.75.08-1.36a1 1 0 00-.3-.79l-1.72-1.72a1 1 0 00-.79-.3l-1.36.08-.75-1.3a1 1 0 00-.86-.5h-2.44a1 1 0 00-.86.5l-.75 1.3-1.36-.08a1 1 0 00-.79.3L4.58 8.86a1 1 0 00-.3.79l.08 1.36-1.3.75a1 1 0 00-.5.86v2.44a1 1 0 00.5.86l1.3.75-.08 1.36a1 1 0 00.3.79l1.72 1.72a1 1 0 00.79.3l1.36-.08.75 1.3a1 1 0 00.86.5h2.44a1 1 0 00.86-.5l.75-1.3 1.36.08a1 1 0 00.79-.3l1.72-1.72a1 1 0 00.3-.79l-.08-1.36 1.3-.75a1 1 0 00.5-.86v-2.44a1 1 0 00-.5-.86zm-2.24 1.44l-1.14.66a1 1 0 00-.5.86l.08 1.2-.98.98-1.2-.08a1 1 0 00-.86.5l-.66 1.14h-1.96l-.66-1.14a1 1 0 00-.86-.5l-1.2.08-.98-.98.08-1.2a1 1 0 00-.5-.86l-1.14-.66v-1.96l1.14-.66a1 1 0 00.5-.86l-.08-1.2.98-.98 1.2.08a1 1 0 00.86-.5l.66-1.14h1.96l.66 1.14a1 1 0 00.86.5l1.2-.08.98.98-.08 1.2a1 1 0 00.5.86l1.14.66v1.96z"/></svg>
34
39
  </button>
35
40
  <button class="rm-panel-toggle" id="rm-panel-toggle" style="${panelToggleStyle}" title="View annotations" aria-label="View annotations" aria-controls="rm-panel">
36
41
  <svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h7"/></svg>
42
+ <span class="rm-panel-toggle-badge" id="rm-panel-toggle-badge"></span>
37
43
  </button>
38
44
  <div class="rm-toast-container" id="rm-toast-container" style="${toastStyle}"></div>
39
45
  <div class="rm-popup" id="rm-popup" role="dialog" aria-label="Add annotation" aria-modal="false">
@@ -61,9 +67,14 @@
61
67
  <h3>Feedback</h3>
62
68
  <span class="rm-panel-count" id="rm-panel-count" style="background:${accentLight};color:${accentText}">0</span>
63
69
  </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>
70
+ <div class="rm-panel-header-actions">
71
+ <button class="rm-panel-close" id="rm-panel-close" aria-label="Close annotations panel">
72
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18L18 6M6 6l12 12"/></svg>
73
+ </button>
74
+ </div>
75
+ </div>
76
+ <div class="rm-settings" id="rm-settings-panel" hidden>
77
+ ${this._toolbarSettingsMarkup()}
67
78
  </div>
68
79
  <div class="rm-filter-chips" id="rm-filter-chips">
69
80
  <button class="rm-chip rm-chip-active" data-filter="all" style="background:${accentBg}">All</button>
@@ -4,6 +4,7 @@
4
4
  document.getElementById("rm-fab").addEventListener("click", () => self.toggleMode());
5
5
  document.getElementById("rm-panel-toggle").addEventListener("click", () => self.togglePanel());
6
6
  document.getElementById("rm-panel-close").addEventListener("click", () => self.togglePanel());
7
+ document.getElementById("rm-settings-toggle").addEventListener("click", () => self._toggleSettings());
7
8
  document.getElementById("rm-btn-cancel").addEventListener("click", () => self._closePopup());
8
9
  document.getElementById("rm-btn-submit").addEventListener("click", (e) => self.submitAnnotation(e));
9
10
  document.getElementById("rm-popup-input").addEventListener("input", () => self._updateCharCount());
@@ -15,6 +16,13 @@
15
16
  // Custom menus (intent/severity/status) — button+menu, never native selects
16
17
  // so host FormSelect/Select2 enhancers cannot rewrite our DOM (#4).
17
18
  this.root.addEventListener("click", (e) => {
19
+ const setting = e.target.closest("[data-setting]");
20
+ if (setting) {
21
+ e.preventDefault();
22
+ e.stopPropagation();
23
+ this._setToolbarSetting(setting.dataset.setting, setting.dataset.value);
24
+ return;
25
+ }
18
26
  const option = e.target.closest(".rm-menu-option");
19
27
  if (option) {
20
28
  e.preventDefault();
@@ -147,10 +155,6 @@
147
155
  this.active = !this.active;
148
156
  if (this.active) {
149
157
  this._activateMode();
150
- // Always open panel when activating annotation mode
151
- if (document.getElementById("rm-panel").style.display !== "flex") {
152
- this.togglePanel();
153
- }
154
158
  } else {
155
159
  this._deactivateMode();
156
160
  }
@@ -161,7 +165,7 @@
161
165
  const iconSize = this._fabIconSize();
162
166
  fab.style.transform = "scale(0.9)";
163
167
  fab.style.boxShadow = `0 0 0 3px ${this._accentBg()}, 0 0 0 6px rgba(99,102,241,0.2)`;
164
- fab.innerHTML = `<svg viewBox="0 0 24 24" style="width:${iconSize}px;height:${iconSize}px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M6 18L18 6M6 6l12 12"/></svg><span class="rm-fab-badge" id="rm-fab-badge">${document.getElementById("rm-fab-badge")?.textContent || ""}</span>`;
168
+ fab.innerHTML = `<svg viewBox="0 0 24 24" style="width:${iconSize}px;height:${iconSize}px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M6 18L18 6M6 6l12 12"/></svg>`;
165
169
  document.addEventListener("mousemove", this._boundMouseMove, true);
166
170
  document.addEventListener("mousedown", this._boundMouseDown, true);
167
171
  document.addEventListener("mouseup", this._boundMouseUp, true);
@@ -177,7 +181,7 @@
177
181
  const iconSize = this._fabIconSize();
178
182
  fab.style.transform = "";
179
183
  fab.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
180
- fab.innerHTML = `<svg viewBox="0 0 24 24" style="width:${iconSize}px;height:${iconSize}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><span class="rm-fab-badge" id="rm-fab-badge">${this.annotations.length || ""}</span>`;
184
+ fab.innerHTML = `<svg viewBox="0 0 24 24" style="width:${iconSize}px;height:${iconSize}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>`;
181
185
  this._updateCount();
182
186
  }
183
187
  document.removeEventListener("mousemove", this._boundMouseMove, true);
@@ -3,8 +3,41 @@
3
3
  const endpoint = (this.endpoint || "/feedback/api").replace(/\/+$/, "") || "/";
4
4
  return `rm-annotations:${encodeURIComponent(endpoint)}`;
5
5
  },
6
+ _toolbarSettingsKey() {
7
+ const endpoint = (this.endpoint || "/feedback/api").replace(/\/+$/, "") || "/";
8
+ return `rm-toolbar-settings:${encodeURIComponent(endpoint)}`;
9
+ },
6
10
  _pageUrl() { return window.location.pathname + window.location.search; },
7
11
  _pageStorageKey() { return this._storageKey() + ":" + this._pageUrl(); },
12
+ _normalizeToolbarSettings(candidate = {}) {
13
+ const source = candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : {};
14
+ const normalized = {};
15
+ if (this._validToolbarAccent(source.accent)) normalized.accent = source.accent;
16
+ if (this._validToolbarPosition(source.position)) normalized.position = source.position;
17
+ if (this._validToolbarSize(source.size)) normalized.size = source.size;
18
+ if (typeof source.fabVisible === "boolean") normalized.fabVisible = source.fabVisible;
19
+ if (typeof source.enableScreenshots === "boolean") normalized.enableScreenshots = source.enableScreenshots;
20
+ return normalized;
21
+ },
22
+ _loadToolbarSettings() {
23
+ try {
24
+ const raw = localStorage.getItem(this._toolbarSettingsKey());
25
+ if (!raw) return {};
26
+ return this._normalizeToolbarSettings(JSON.parse(raw));
27
+ } catch (e) {
28
+ console.warn("[rails-markup] settings load failed:", e);
29
+ return {};
30
+ }
31
+ },
32
+ _saveToolbarSettings(settings = this.toolbarSettings || {}) {
33
+ try {
34
+ localStorage.setItem(this._toolbarSettingsKey(), JSON.stringify(this._normalizeToolbarSettings(settings)));
35
+ return true;
36
+ } catch (e) {
37
+ console.warn("[rails-markup] settings save failed:", e);
38
+ return false;
39
+ }
40
+ },
8
41
  _saveToStorage() {
9
42
  try {
10
43
  // Cross-tab storage-event merging is deferred: safely reconciling ordered
@@ -72,6 +105,7 @@
72
105
  this.outbox = snapshot.outbox;
73
106
  this.nextId = snapshot.nextId;
74
107
  this.legacyMigrations = snapshot.legacyMigrations;
108
+ this.toolbarSettings = snapshot.toolbarSettings || {};
75
109
  this._storageError = "Changes could not be saved in this browser. Free storage space and try again.";
76
110
  this._renderPins();
77
111
  this._rebuildList();
@@ -87,7 +121,8 @@
87
121
  annotations: this.annotations,
88
122
  outbox: this.outbox,
89
123
  nextId: this.nextId,
90
- legacyMigrations: this.legacyMigrations
124
+ legacyMigrations: this.legacyMigrations,
125
+ toolbarSettings: this.toolbarSettings
91
126
  }));
92
127
  },
93
128
  _mergeDirtyFields(...collections) {
@@ -329,7 +329,7 @@
329
329
  const count = this.annotations.length;
330
330
  const countEl = document.getElementById("rm-panel-count");
331
331
  if (countEl) countEl.textContent = count;
332
- const badge = document.getElementById("rm-fab-badge");
332
+ const badge = document.getElementById("rm-panel-toggle-badge");
333
333
  if (badge) {
334
334
  if (count > 0) { badge.textContent = count; badge.style.display = "flex"; }
335
335
  else { badge.style.display = "none"; }
@@ -419,6 +419,119 @@
419
419
  }
420
420
  if (text) text.textContent = this.serverOnline ? "Connected" : (this._syncUnavailable || "Offline");
421
421
  },
422
+ _validToolbarAccent(value) {
423
+ return ["indigo", "amber", "blue", "emerald", "rose"].includes(value);
424
+ },
425
+ _validToolbarPosition(value) {
426
+ return ["bl", "br", "tl", "tr"].includes(value);
427
+ },
428
+ _validToolbarSize(value) {
429
+ return ["slim", "compact", "default"].includes(value);
430
+ },
431
+ _toolbarSettingOptions(setting) {
432
+ switch (setting) {
433
+ case "accent":
434
+ return [
435
+ ["indigo", "Indigo"],
436
+ ["amber", "Amber"],
437
+ ["blue", "Blue"],
438
+ ["emerald", "Emerald"],
439
+ ["rose", "Rose"]
440
+ ];
441
+ case "position":
442
+ return [
443
+ ["bl", "Bottom-left"],
444
+ ["br", "Bottom-right"],
445
+ ["tl", "Top-left"],
446
+ ["tr", "Top-right"]
447
+ ];
448
+ case "size":
449
+ return [
450
+ ["default", "Default"],
451
+ ["compact", "Compact"],
452
+ ["slim", "Slim"]
453
+ ];
454
+ case "fabVisible":
455
+ case "enableScreenshots":
456
+ return [
457
+ [true, "On"],
458
+ [false, "Off"]
459
+ ];
460
+ default:
461
+ return [];
462
+ }
463
+ },
464
+ _toolbarSettingLabel(setting) {
465
+ switch (setting) {
466
+ case "accent": return "Accent";
467
+ case "position": return "Position";
468
+ case "size": return "Size";
469
+ case "fabVisible": return "Show FAB";
470
+ case "enableScreenshots": return "Screenshots";
471
+ default: return setting;
472
+ }
473
+ },
474
+ _toolbarSettingCurrentValue(setting) {
475
+ switch (setting) {
476
+ case "accent": return this.accent;
477
+ case "position": return this.position;
478
+ case "size": return this.size;
479
+ case "fabVisible": return this.fabVisible;
480
+ case "enableScreenshots": return this.enableScreenshots;
481
+ default: return null;
482
+ }
483
+ },
484
+ _toolbarSettingDisplayValue(value) {
485
+ return value === true ? "true" : value === false ? "false" : String(value);
486
+ },
487
+ _toolbarSettingsMarkup() {
488
+ return [
489
+ "accent",
490
+ "position",
491
+ "size",
492
+ "fabVisible",
493
+ "enableScreenshots"
494
+ ].map(setting => this._toolbarSettingsRowMarkup(setting)).join("");
495
+ },
496
+ _toolbarSettingsRowMarkup(setting) {
497
+ const current = this._toolbarSettingCurrentValue(setting);
498
+ const options = this._toolbarSettingOptions(setting);
499
+ const buttons = options.map(([value, label]) => {
500
+ const active = String(value) === String(current);
501
+ const valueAttr = this._toolbarSettingDisplayValue(value);
502
+ return `<button type="button" class="rm-chip${active ? " rm-chip-active" : " rm-chip-inactive"} rm-setting-chip" data-setting="${setting}" data-value="${this._esc(valueAttr)}"${active ? ` style="background:${this._accentBg()}"` : ""}>${this._esc(label)}</button>`;
503
+ }).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
+ `;
510
+ },
511
+ _toolbarSettingValue(setting, rawValue) {
512
+ if (setting === "fabVisible" || setting === "enableScreenshots") {
513
+ return rawValue === true || rawValue === "true";
514
+ }
515
+ return String(rawValue || "");
516
+ },
517
+ _setToolbarSetting(setting, rawValue) {
518
+ const value = this._toolbarSettingValue(setting, rawValue);
519
+ const next = Object.assign({}, this.toolbarSettings, { [setting]: value });
520
+ const normalized = this._normalizeToolbarSettings(next);
521
+ if (JSON.stringify(normalized) === JSON.stringify(this.toolbarSettings)) return;
522
+ if (!this._saveToolbarSettings(normalized)) return;
523
+ this.toolbarSettings = normalized;
524
+ this.destroy();
525
+ this.init(this._bootstrapOptions || {});
526
+ },
527
+ _toggleSettings() {
528
+ const panel = document.getElementById("rm-settings-panel");
529
+ const toggle = document.getElementById("rm-settings-toggle");
530
+ if (!panel) return;
531
+ panel.hidden = !panel.hidden;
532
+ this._settingsPanelOpen = !panel.hidden;
533
+ if (toggle) toggle.setAttribute("aria-expanded", String(!panel.hidden));
534
+ },
422
535
  async _captureElement(element) {
423
536
  try {
424
537
  const rect = element.getBoundingClientRect();
@@ -3,12 +3,21 @@
3
3
  const previousPathname = this._currentPathname;
4
4
  const previousPageUrl = this._currentPageUrl;
5
5
  const currentPageUrl = this._pageUrl();
6
+ this._bootstrapOptions = Object.assign({}, opts);
6
7
  this.endpoint = opts.endpoint || "/feedback/api";
7
- this.accent = opts.accent || "indigo";
8
- this.position = opts.position || "bl";
9
- this.size = opts.size || "default";
10
- this.fabVisible = opts.fabVisible !== false;
11
- this.enableScreenshots = opts.enableScreenshots !== false;
8
+ const savedSettings = this._loadToolbarSettings();
9
+ this.toolbarSettings = this._normalizeToolbarSettings({
10
+ accent: savedSettings.accent || opts.accent || "indigo",
11
+ position: savedSettings.position || opts.position || "bl",
12
+ size: savedSettings.size || opts.size || "default",
13
+ fabVisible: savedSettings.fabVisible != null ? savedSettings.fabVisible : (opts.fabVisible !== false),
14
+ enableScreenshots: savedSettings.enableScreenshots != null ? savedSettings.enableScreenshots : (opts.enableScreenshots !== false)
15
+ });
16
+ this.accent = this.toolbarSettings.accent || "indigo";
17
+ this.position = this.toolbarSettings.position || "bl";
18
+ this.size = this.toolbarSettings.size || "default";
19
+ this.fabVisible = this.toolbarSettings.fabVisible !== false;
20
+ this.enableScreenshots = this.toolbarSettings.enableScreenshots !== false;
12
21
  this.legacyStorageEndpoint = opts.legacyStorageEndpoint || this.legacyStorageEndpoint;
13
22
  this.healthIntervalMs = (opts.healthInterval || 60) * 1000;
14
23
 
@@ -84,6 +93,7 @@
84
93
  if (pins) pins.remove();
85
94
  const styles = document.getElementById("rm-toolbar-styles");
86
95
  if (styles) styles.remove();
96
+ this._settingsPanelOpen = false;
87
97
  },
88
98
  });
89
99
 
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <%= csrf_meta_tags %>
6
7
  <title><%= content_for?(:title) ? "#{yield(:title)} — Rails Markup" : "Rails Markup" %></title>
7
8
  <style>
8
9
  /* Reset */
@@ -5,7 +5,7 @@
5
5
  <turbo-frame id="detail-panel">
6
6
  <div class="rm-detail">
7
7
  <%# Close button %>
8
- <button type="button" class="rm-back" onclick="var f=this.closest('turbo-frame'); f.innerHTML=''; f.classList.add('rm-panel-empty'); document.querySelectorAll('.rm-card-active').forEach(c => c.classList.remove('rm-card-active'));">&#10005; Close</button>
8
+ <button type="button" class="rm-back" onclick="var f=this.closest('turbo-frame'); if (this.closest('.rm-layout')) { f.innerHTML=''; f.classList.add('rm-panel-empty'); document.querySelectorAll('.rm-card-active').forEach(c => c.classList.remove('rm-card-active')); } else { window.location.href='<%= j(rails_markup.root_path) %>'; }">&#10005; Close</button>
9
9
 
10
10
  <%# Header badges %>
11
11
  <div class="rm-detail-header">
data/config/routes.rb CHANGED
@@ -30,7 +30,7 @@ RailsMarkup::Engine.routes.draw do
30
30
 
31
31
  # External API (token-authenticated, used by MCP/CLI tools)
32
32
  # Paths: /external/pending, /external/:id, /external/:id/{action}
33
- # Full URL (with mount): /admin/annotations/external/pending
33
+ # Full URL (with mount): /admin/rails-markup/external/pending
34
34
  namespace :external, defaults: { format: :json } do
35
35
  get "pending", to: "annotations#pending"
36
36
  get ":id", to: "annotations#show", constraints: { id: /\d+/ }
@@ -31,7 +31,7 @@ RailsMarkup.configure do |config|
31
31
  # "tl" (top-left), "tr" (top-right). Default: "bl"
32
32
  # config.toolbar_position = "bl"
33
33
 
34
- # FAB button size: "default" (48px), "compact" (40px), "slim" (32px)
34
+ # FAB button size: "default" (40px), "compact" (36px), "slim" (32px)
35
35
  # config.toolbar_size = "default"
36
36
 
37
37
  # "Back to app" link in the dashboard header.
@@ -12,8 +12,8 @@ module RailsMarkup
12
12
 
13
13
  desc "Install Rails Markup: create migration, initializer, auth controller, toolbar, and mount engine routes."
14
14
 
15
- class_option :mount_path, type: :string, default: "/admin/annotations",
16
- desc: "Path to mount the Rails Markup engine"
15
+ class_option :mount_path, type: :string, default: nil,
16
+ desc: "Path to mount the Rails Markup engine (leave blank to be prompted)"
17
17
  class_option :base_controller, type: :string, default: "ApplicationController",
18
18
  desc: "Base controller class for authentication"
19
19
  class_option :layout, type: :string, default: "application",
@@ -59,7 +59,7 @@ module RailsMarkup
59
59
  end
60
60
 
61
61
  def mount_engine
62
- mount_path = options[:mount_path]
62
+ mount_path = resolved_mount_path
63
63
  route_line = %{mount RailsMarkup::Engine, at: "#{mount_path}" if defined?(RailsMarkup::Engine)}
64
64
  route route_line
65
65
  end
@@ -140,7 +140,7 @@ module RailsMarkup
140
140
  dev_url = detect_dev_url
141
141
  env_updates = {
142
142
  "RAILS_MARKUP_DEV_URL" => dev_url,
143
- "RAILS_MARKUP_MOUNT_PATH" => options[:mount_path]
143
+ "RAILS_MARKUP_MOUNT_PATH" => resolved_mount_path
144
144
  }
145
145
  config.update_env(env_updates)
146
146
  say_status :create, ".mcp.json (dev URL: #{dev_url})", :green
@@ -159,7 +159,7 @@ module RailsMarkup
159
159
  say ""
160
160
  say "Next steps:"
161
161
  say " 1. Run migrations: rails db:migrate"
162
- say " 2. Visit dashboard: #{options[:mount_path]}"
162
+ say " 2. Visit dashboard: #{resolved_mount_path}"
163
163
  say " 3. Configure auth in config/initializers/rails_markup.rb"
164
164
  say ""
165
165
  dev_url = detect_dev_url
@@ -180,6 +180,40 @@ module RailsMarkup
180
180
 
181
181
  private
182
182
 
183
+ def resolved_mount_path
184
+ @resolved_mount_path ||= begin
185
+ explicit = options[:mount_path]
186
+ return normalize_mount_path(explicit) if explicit && !explicit.strip.empty?
187
+
188
+ prompt_mount_path
189
+ end
190
+ end
191
+
192
+ def prompt_mount_path
193
+ return "/rails-markup" unless interactive_prompt_available?
194
+
195
+ if yes?("Choose a nested prefix before /rails-markup? (for example /admin/rails-markup or /dashboard/rails-markup)")
196
+ prefix = ask("Prefix segment (leave blank for admin)")
197
+ prefix = "admin" if prefix.to_s.strip.empty?
198
+ normalize_mount_path("#{prefix}/rails-markup")
199
+ else
200
+ "/rails-markup"
201
+ end
202
+ end
203
+
204
+ def interactive_prompt_available?
205
+ $stdin.tty? && $stdout.tty?
206
+ end
207
+
208
+ def normalize_mount_path(path)
209
+ value = path.to_s.strip
210
+ value = value.sub(%r{\A/+}, "")
211
+ value = value.sub(%r{/+\z}, "")
212
+ value = "/#{value}"
213
+ value = value.gsub(%r{/+}, "/")
214
+ value == "/" ? "/rails-markup" : value
215
+ end
216
+
183
217
  def migration_version
184
218
  "[#{ActiveRecord::Migration.current_version}]"
185
219
  end
@@ -31,7 +31,7 @@ module RailsMarkup
31
31
  title: "Toolbar Size",
32
32
  type: :select,
33
33
  options: %w[slim compact default],
34
- labels: { "slim" => "Slim (32px)", "compact" => "Compact (40px)", "default" => "Default (48px)" }
34
+ labels: { "slim" => "Slim (32px)", "compact" => "Compact (36px)", "default" => "Default (40px)" }
35
35
  },
36
36
  fab: {
37
37
  title: "Show Floating Button",
@@ -110,7 +110,7 @@ module RailsMarkup
110
110
  method_option :prod_url, type: :string, desc: "Production URL (RAILS_MARKUP_PROD_URL)"
111
111
  method_option :prod_token, type: :string, desc: "Production API token (RAILS_MARKUP_PROD_TOKEN)"
112
112
  method_option :dev_url, type: :string, desc: "Dev URL (RAILS_MARKUP_DEV_URL)"
113
- method_option :mount_path, type: :string, desc: "Engine mount path (RAILS_MARKUP_MOUNT_PATH)"
113
+ method_option :mount_path, type: :string, desc: "Engine mount path (RAILS_MARKUP_MOUNT_PATH; e.g. /admin/rails-markup)"
114
114
  method_option :global, type: :boolean, default: false, desc: "Write to ~/.claude/settings.json"
115
115
  method_option :codex, type: :boolean, default: false, desc: "Write to ~/.codex/config.toml"
116
116
  def configure
@@ -209,7 +209,7 @@ module RailsMarkup
209
209
  method_option :production, type: :boolean, default: false, desc: "Watch production"
210
210
  method_option :url, type: :string, desc: "Override base URL"
211
211
  method_option :token, type: :string, desc: "Override API token"
212
- method_option :mount_path, type: :string, desc: "Engine mount path"
212
+ method_option :mount_path, type: :string, desc: "Engine mount path (e.g. /admin/rails-markup)"
213
213
  method_option :interval, type: :numeric, default: 5, desc: "Poll interval in seconds"
214
214
  def watch
215
215
  env = resolve_env(options[:production])
@@ -259,7 +259,7 @@ module RailsMarkup
259
259
  method_option :production, type: :boolean, default: false, desc: "Fetch from production"
260
260
  method_option :url, type: :string, desc: "Override base URL"
261
261
  method_option :token, type: :string, desc: "Override API token"
262
- method_option :mount_path, type: :string, desc: "Engine mount path"
262
+ method_option :mount_path, type: :string, desc: "Engine mount path (e.g. /admin/rails-markup)"
263
263
  def pending
264
264
  env = resolve_env(options[:production])
265
265
  return unless env
@@ -433,7 +433,7 @@ module RailsMarkup
433
433
  desc: "Environment to fetch from"
434
434
  method_option :url, type: :string, desc: "Override base URL"
435
435
  method_option :token, type: :string, desc: "Override API token"
436
- method_option :mount_path, type: :string, desc: "Engine mount path (default: /admin/annotations)"
436
+ method_option :mount_path, type: :string, desc: "Engine mount path (default: /admin/rails-markup; e.g. /admin/rails-markup)"
437
437
  def fetch(env_arg = nil)
438
438
  production = env_arg == "production" || options[:production] || options[:environment] == "production"
439
439
  env = resolve_env(production)
@@ -571,7 +571,7 @@ module RailsMarkup
571
571
  sections << " --prod-url URL Production URL"
572
572
  sections << " --prod-token TOKEN Production API token"
573
573
  sections << " --dev-url URL Development URL"
574
- sections << " --mount-path PATH Engine mount path"
574
+ sections << " --mount-path PATH Engine mount path (e.g. /admin/rails-markup)"
575
575
  sections << " --global Write to ~/.claude/settings.json"
576
576
  sections << " --codex Write to ~/.codex/config.toml"
577
577
  sections << " setup-production --url URL Generate token + configure production"
@@ -671,7 +671,7 @@ module RailsMarkup
671
671
  if production
672
672
  base_url = options[:url] || mcp_env["RAILS_MARKUP_PROD_URL"]
673
673
  token = options[:token] || mcp_env["RAILS_MARKUP_PROD_TOKEN"]
674
- mount = options[:mount_path] || mcp_env["RAILS_MARKUP_MOUNT_PATH"] || "/admin/annotations"
674
+ mount = options[:mount_path] || mcp_env["RAILS_MARKUP_MOUNT_PATH"] || "/admin/rails-markup"
675
675
 
676
676
  unless base_url
677
677
  say "No production URL. Set it via:", :red
@@ -695,7 +695,7 @@ module RailsMarkup
695
695
  else
696
696
  base_url = options[:url] || mcp_env["RAILS_MARKUP_DEV_URL"]
697
697
  token = options[:token] || mcp_env["RAILS_MARKUP_DEV_TOKEN"]
698
- mount = options[:mount_path] || mcp_env["RAILS_MARKUP_MOUNT_PATH"] || "/admin/annotations"
698
+ mount = options[:mount_path] || mcp_env["RAILS_MARKUP_MOUNT_PATH"] || "/admin/rails-markup"
699
699
 
700
700
  unless base_url
701
701
  say "No dev URL. Set it via:", :red
@@ -73,7 +73,7 @@ module RailsMarkup
73
73
  # "tl" (top-left), "tr" (top-right). Default: "bl"
74
74
  attr_reader :toolbar_position
75
75
 
76
- # FAB button size: "default" (48px), "compact" (40px), "slim" (32px).
76
+ # FAB button size: "default" (40px), "compact" (36px), "slim" (32px).
77
77
  # Default: "default"
78
78
  attr_reader :toolbar_size
79
79
 
@@ -17,7 +17,7 @@ module RailsMarkup
17
17
  # RAILS_MARKUP_DEV_URL — local Rails server URL (auto-detected on install)
18
18
  # RAILS_MARKUP_PROD_URL — production URL
19
19
  # RAILS_MARKUP_PROD_TOKEN — production API token
20
- # RAILS_MARKUP_MOUNT_PATH — engine mount path (default: /admin/annotations)
20
+ # RAILS_MARKUP_MOUNT_PATH — engine mount path (default: /admin/rails-markup)
21
21
  class McpServer
22
22
  class ToolError < StandardError; end
23
23
  class TargetError < ToolError; end
@@ -184,7 +184,7 @@ module RailsMarkup
184
184
  end
185
185
 
186
186
  def mount_path
187
- config_value("RAILS_MARKUP_MOUNT_PATH") || "/admin/annotations"
187
+ config_value("RAILS_MARKUP_MOUNT_PATH") || "/admin/rails-markup"
188
188
  end
189
189
 
190
190
  def prod_url
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsMarkup
4
- VERSION = "1.4.5"
4
+ VERSION = "1.4.7"
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.5
4
+ version: 1.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - InventList
@@ -184,7 +184,7 @@ post_install_message: |
184
184
  rails db:migrate
185
185
 
186
186
  Options:
187
- --mount-path=/admin/annotations (engine route)
187
+ --mount-path=/admin/rails-markup (engine route)
188
188
  --base-controller=AdminController (auth parent class)
189
189
  --layout=application (toolbar injection)
190
190
  rdoc_options: []