plumbo 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e37ee057af091dc00bb62836449ce4cd80c903ca6153507c68825efbf589a4c4
4
- data.tar.gz: 773e4b2b5def7b84992e3fa0a09517c619f39f1dd8548c3c1d587d8597289c50
3
+ metadata.gz: a9d835f22037aca3f59bd7784b5b53f0df96d6f357058f7fee0feb2960fdd2dc
4
+ data.tar.gz: 046c9e808514912b46a1db902f5b4eaee0cc24505161461bf2f8a70559ce15c0
5
5
  SHA512:
6
- metadata.gz: 86bd0dccd19d772f0c31e313d3858ed9a9b923f6fd66bf18908a9a6614d307c2adb3825d632bbb51168bd398cdcec275dce7abf00d6cd894d2c2f71503e082a2
7
- data.tar.gz: eee1962f2bff1cb3f1a6a02921c4e2d4d1f1285b43e952dcde62218c8018c8b380ad2321a1a78c9ec9b7342bc057342082748a65b79f89f816957fd5ad376ecf
6
+ metadata.gz: b7e38789548920e3de8e6a02bb461cba040a68440e71b133049ca6761df508a00d4aa6287f59dc73cb19e7064a1ac14b9ed1db7cb821055338559caccf4364a4
7
+ data.tar.gz: c4ec8aa1b0181cef27a1955dc220aa9051bdd59c71cfc8fb131d6c08bda3147392eec9bd3da1a7018aa60092636cd79f2caa7b475a2bdefeb0fe376b23af80f0
data/README.md CHANGED
@@ -31,6 +31,9 @@ showing how many files rendered the current page.
31
31
  and Stimulus controllers nested (and color-coded) under their parent.
32
32
  - **Collapse or expand** a parent by clicking its row; **copy** a single path with
33
33
  its copy icon, or **Copy All** for the whole (filtered) list.
34
+ - **Hover a row** to highlight the part of the page that file rendered — a partial
35
+ used in a loop lights up every instance. Rows with no on-page output (controllers,
36
+ helpers, JavaScript) simply don't highlight.
34
37
  - **Filter** by typing in the search box, or click a type chip — Controllers,
35
38
  Views, Partials, Stimulus, … — to show just one kind.
36
39
  - **Clear All** empties the list so you can watch fresh files appear as you click
@@ -49,6 +52,7 @@ Plumbo.configure do |c|
49
52
  c.max_files = 500 # safety cap on listed files
50
53
  c.include_stimulus = true # list Stimulus controllers
51
54
  c.javascript_root = "app/javascript" # source dir Stimulus paths map into
55
+ c.highlight = true # highlight a file's region on hover
52
56
  end
53
57
  ```
54
58
 
@@ -65,6 +69,13 @@ every response carries an `X-Plumbo-Files` header that the panel reads on each
65
69
  > detected — those emitted by helpers or ViewComponents, and other JavaScript,
66
70
  > aren't listed.
67
71
 
72
+ Hover highlighting reuses Rails' built-in
73
+ `annotate_rendered_view_with_filenames`, which Plumbo enables in development so each
74
+ rendered template and partial is wrapped in `<!-- BEGIN … -->`/`<!-- END … -->`
75
+ comments. The panel reads those markers to locate a file's output on the page. This
76
+ adds the comments to your development HTML; set `c.highlight = false` to leave the
77
+ markup untouched (the panel still lists files, just without hover highlighting).
78
+
68
79
  ## Notes
69
80
 
70
81
  - Production-safe: disabled outside development by default.
@@ -18,6 +18,13 @@ module Plumbo
18
18
  # in the rendered HTML. Defaults to true.
19
19
  attr_accessor :include_stimulus
20
20
 
21
+ # Whether hovering a view/partial/layout row highlights its rendered region
22
+ # on the page. Defaults to true. When on, Plumbo enables Rails' built-in
23
+ # annotate_rendered_view_with_filenames so the rendered HTML carries the
24
+ # BEGIN/END comment markers the highlight reads. Turn off to leave the host
25
+ # app's HTML untouched (highlighting then does nothing).
26
+ attr_accessor :highlight
27
+
21
28
  # Source directory Stimulus controllers are mapped into. Combined with the
22
29
  # "controllers/" subdirectory and the Stimulus identifier to form the path.
23
30
  attr_accessor :javascript_root
@@ -29,6 +36,7 @@ module Plumbo
29
36
  @max_files = 500
30
37
  @path_prefix = "@"
31
38
  @include_stimulus = true
39
+ @highlight = true
32
40
  @javascript_root = "app/javascript"
33
41
  @root = nil
34
42
  end
data/lib/plumbo/panel.rb CHANGED
@@ -94,8 +94,9 @@ module Plumbo
94
94
  #plumbo .plumbo-chip{flex:none;white-space:nowrap;color:#9ca3af;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:9999px;padding:2px 8px;font-size:10px}
95
95
  #plumbo .plumbo-chip:hover{background:rgba(255,255,255,.1);color:#fff}
96
96
  #plumbo .plumbo-chip[aria-pressed="true"]{background:#2563eb;border-color:#2563eb;color:#fff}
97
- #plumbo .plumbo-list{flex:1;min-height:0;list-style:none;margin:0;padding:0;overflow-y:auto;background:#0b1220}
98
- #plumbo .plumbo-row{--d:0;display:flex;align-items:center;gap:8px;width:100%;padding:7px 16px;padding-left:calc(16px + var(--d) * 14px);color:#d1d5db;text-align:left;cursor:default;background-image:repeating-linear-gradient(to right,rgba(255,255,255,.09) 0,rgba(255,255,255,.09) 1px,transparent 1px,transparent 14px);background-repeat:no-repeat;background-position:20px 0;background-size:calc(var(--d) * 14px) 100%}
97
+ #plumbo .plumbo-list{flex:1;min-height:0;list-style:none;margin:0;padding:0;overflow:auto;background:#0b1220;scrollbar-width:none}
98
+ #plumbo .plumbo-list::-webkit-scrollbar{display:none}
99
+ #plumbo .plumbo-row{--d:0;display:flex;align-items:center;gap:8px;width:max-content;min-width:100%;padding:7px 16px;padding-left:calc(16px + var(--d) * 14px);color:#d1d5db;text-align:left;cursor:default;background-image:repeating-linear-gradient(to right,rgba(255,255,255,.09) 0,rgba(255,255,255,.09) 1px,transparent 1px,transparent 14px);background-repeat:no-repeat;background-position:20px 0;background-size:calc(var(--d) * 14px) 100%}
99
100
  #plumbo .plumbo-row:hover{background-color:rgba(255,255,255,.05);color:#fff}
100
101
  #plumbo .plumbo-row.plumbo-parent{cursor:pointer}
101
102
  #plumbo .plumbo-caret{flex:none;width:12px;display:flex;color:#6b7280}
@@ -105,9 +106,9 @@ module Plumbo
105
106
  #plumbo .plumbo-row.plumbo-parent:hover .plumbo-caret{color:#fff}
106
107
  #plumbo .plumbo-type{flex:none;display:flex;color:#6b7280}
107
108
  #plumbo .plumbo-row:hover .plumbo-type{color:#9ca3af}
108
- #plumbo .plumbo-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
109
- #plumbo .plumbo-copy{margin-left:auto;flex:none;color:#4b5563;display:flex;cursor:pointer}
110
- #plumbo .plumbo-row:hover .plumbo-copy{color:#9ca3af}
109
+ #plumbo .plumbo-path{flex:none;white-space:nowrap}
110
+ #plumbo .plumbo-copy{position:sticky;right:16px;margin-left:auto;flex:none;color:#4b5563;display:flex;cursor:pointer;padding-left:20px;background:linear-gradient(to right,rgba(11,18,32,0),#0b1220 16px)}
111
+ #plumbo .plumbo-row:hover .plumbo-copy{color:#9ca3af;background:linear-gradient(to right,rgba(23,30,43,0),#171e2b 16px)}
111
112
  #plumbo .plumbo-row[data-depth="1"] .plumbo-path{color:#93c5fd}
112
113
  #plumbo .plumbo-row[data-depth="1"] .plumbo-type{color:#60a5fa}
113
114
  #plumbo .plumbo-row[data-depth="2"] .plumbo-path{color:#c4b5fd}
@@ -120,6 +121,7 @@ module Plumbo
120
121
  #plumbo .plumbo-row[data-depth="5"] .plumbo-type{color:#f472b6}
121
122
  #plumbo .plumbo-flash{color:#4ade80}
122
123
  #plumbo svg{display:block}
124
+ .plumbo-hl{position:fixed;z-index:2147482000;pointer-events:none;background:rgba(59,130,246,.18);outline:2px solid #3b82f6;outline-offset:-1px;border-radius:2px}
123
125
  CSS
124
126
 
125
127
  JS = <<~JS.freeze
@@ -163,7 +165,7 @@ module Plumbo
163
165
  }
164
166
  else if (hit.hasAttribute("data-plumbo-clear")) {
165
167
  var emptied = root.querySelector("#plumbo-list");
166
- if (emptied) { emptied.innerHTML = ""; refresh(); }
168
+ if (emptied) { emptied.innerHTML = ""; clearHighlight(); refresh(); }
167
169
  }
168
170
  else if (hit.hasAttribute("data-plumbo-chip")) {
169
171
  var cat = hit.getAttribute("data-category");
@@ -290,6 +292,75 @@ module Plumbo
290
292
  if (html) { list.insertAdjacentHTML("beforeend", html); }
291
293
  refresh();
292
294
  }
295
+ // Hover-to-highlight: map a panel row back to the region(s) it rendered.
296
+ // Relies on Rails' annotate_rendered_view_with_filenames, which wraps each
297
+ // template/partial in `<!-- BEGIN path -->`/`<!-- END path -->` comments
298
+ // (path relative to Rails.root). Overlays are throwaway, pointer-events:none.
299
+ var overlays = [];
300
+ function clearHighlight(){
301
+ for (var i = 0; i < overlays.length; i++){ if (overlays[i].parentNode) overlays[i].parentNode.removeChild(overlays[i]); }
302
+ overlays = [];
303
+ }
304
+ // Walk every comment node and pair BEGIN/END markers by path using a stack
305
+ // (markers nest cleanly), returning {path, begin, end} for each pair.
306
+ function commentPairs(){
307
+ var pairs = [], stack = [];
308
+ var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT, null, false);
309
+ var node;
310
+ while ((node = walker.nextNode())){
311
+ var m = /^\\s*(BEGIN|END)\\s+(.+?)\\s*$/.exec(node.nodeValue || "");
312
+ if (!m) continue;
313
+ if (m[1] === "BEGIN"){ stack.push({ path: m[2], begin: node }); }
314
+ else {
315
+ for (var i = stack.length - 1; i >= 0; i--){
316
+ if (stack[i].path === m[2]){ pairs.push({ path: m[2], begin: stack[i].begin, end: node }); stack.splice(i, 1); break; }
317
+ }
318
+ }
319
+ }
320
+ return pairs;
321
+ }
322
+ // Draw one fixed overlay around the whole region a row rendered — a single
323
+ // bounding box, not one per line/inline fragment, so nested links and labels
324
+ // don't each get their own border. A looped partial still yields one box per
325
+ // instance (one comment pair each). A row's data-path carries the configured
326
+ // prefix (e.g. "@app/..."); the comment path doesn't — endsWith matches
327
+ // regardless of prefix. Rows with no rendered region simply match nothing.
328
+ function highlight(path){
329
+ clearHighlight();
330
+ if (!path) return;
331
+ var pairs = commentPairs();
332
+ for (var i = 0; i < pairs.length; i++){
333
+ if (!path.endsWith(pairs[i].path)) continue;
334
+ var range = document.createRange();
335
+ try { range.setStartAfter(pairs[i].begin); range.setEndBefore(pairs[i].end); } catch (e) { continue; }
336
+ var rect = range.getBoundingClientRect();
337
+ if (rect.width <= 0 && rect.height <= 0) continue;
338
+ var box = document.createElement("div");
339
+ box.className = "plumbo-hl";
340
+ box.style.cssText = "top:" + rect.top + "px;left:" + rect.left + "px;width:" + rect.width + "px;height:" + rect.height + "px";
341
+ document.body.appendChild(box);
342
+ overlays.push(box);
343
+ }
344
+ }
345
+ function rowFor(target, root){
346
+ if (!root || !target || !target.closest) return null;
347
+ var row = target.closest(".plumbo-row");
348
+ return (row && root.contains(row)) ? row : null;
349
+ }
350
+ // Delegate hover like the click/input handlers. mouseover bubbles, so it
351
+ // fires for every move; track the row under the cursor and only re-highlight
352
+ // when it changes (re-paint on row→row, clear when leaving rows). A null
353
+ // relatedTarget on mouseout means the pointer left the window — clear then.
354
+ var hoveredRow = null;
355
+ document.addEventListener("mouseover", function(event){
356
+ var row = rowFor(event.target, document.getElementById("plumbo"));
357
+ if (row === hoveredRow) return;
358
+ hoveredRow = row;
359
+ highlight(row ? row.getAttribute("data-path") : null);
360
+ });
361
+ document.addEventListener("mouseout", function(event){
362
+ if (!event.relatedTarget && hoveredRow) { hoveredRow = null; clearHighlight(); }
363
+ });
293
364
  refresh();
294
365
  // Read the file list off every fetch response (Turbo Drive/Frame/Stream
295
366
  // and custom fetch all go through fetch) so the panel keeps up without a
@@ -305,7 +376,7 @@ module Plumbo
305
376
  }
306
377
  // A full Turbo Drive visit swaps in a fresh panel; reset the filter to
307
378
  // match the new page, then rebuild.
308
- document.addEventListener("turbo:load", function(){ query = ""; activeCategory = null; refresh(); });
379
+ document.addEventListener("turbo:load", function(){ query = ""; activeCategory = null; clearHighlight(); refresh(); });
309
380
  })();
310
381
  JS
311
382
 
@@ -10,5 +10,16 @@ module Plumbo
10
10
  initializer "plumbo.middleware" do |app|
11
11
  app.middleware.use Plumbo::Middleware
12
12
  end
13
+
14
+ # Enable Rails' filename annotations so each rendered template/partial is
15
+ # wrapped in BEGIN/END HTML comments — the markers the panel's hover-to-
16
+ # highlight reads to map a row back to its region on the page. Templates bake
17
+ # the comments in at compile time, so this must be set before Action View is
18
+ # configured rather than per-request. Only ever switched on (never off) so a
19
+ # host that enabled it independently keeps it.
20
+ initializer "plumbo.annotate_views", before: "action_view.setup" do |app|
21
+ config = Plumbo.config
22
+ app.config.action_view.annotate_rendered_view_with_filenames = true if config.enabled && config.highlight
23
+ end
13
24
  end
14
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbo
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plumbo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Sears
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-06-23 00:00:00.000000000 Z
10
+ date: 2026-06-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport