@herb-tools/dev-tools 0.7.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.
@@ -0,0 +1,1001 @@
1
+ import { ErrorOverlay } from './error-overlay';
2
+
3
+ export interface HerbDevToolsOptions {
4
+ projectPath?: string;
5
+ autoInit?: boolean;
6
+ }
7
+
8
+ export class HerbOverlay {
9
+ private showingERB = false;
10
+ private showingERBOutlines = false;
11
+ private showingERBHoverReveal = false;
12
+ private showingTooltips = true;
13
+ private showingViewOutlines = false;
14
+ private showingPartialOutlines = false;
15
+ private showingComponentOutlines = false;
16
+ private menuOpen = false;
17
+ private projectPath = '';
18
+ private currentlyHoveredERBElement: HTMLElement | null = null;
19
+ private errorOverlay: ErrorOverlay | null = null;
20
+
21
+ private static readonly SETTINGS_KEY = 'herb-dev-tools-settings';
22
+
23
+ constructor(private options: HerbDevToolsOptions = {}) {
24
+ if (options.autoInit !== false) {
25
+ this.init();
26
+ }
27
+ }
28
+
29
+ private init() {
30
+ this.loadProjectPath();
31
+ this.loadSettings();
32
+ this.injectMenu();
33
+ this.setupMenuToggle();
34
+ this.setupToggleSwitches();
35
+ this.initializeErrorOverlay();
36
+ this.setupTurboListeners();
37
+ this.applySettings();
38
+ }
39
+
40
+ private loadProjectPath() {
41
+ if (this.options.projectPath) {
42
+ this.projectPath = this.options.projectPath;
43
+ return;
44
+ }
45
+
46
+ const metaTag = document.querySelector('meta[name="herb-project-path"]') as HTMLMetaElement;
47
+
48
+ if (metaTag?.content) {
49
+ this.projectPath = metaTag.content;
50
+ }
51
+ }
52
+
53
+ private loadSettings() {
54
+ const savedSettings = localStorage.getItem(HerbOverlay.SETTINGS_KEY);
55
+ if (savedSettings) {
56
+ try {
57
+ const settings = JSON.parse(savedSettings);
58
+ this.showingERB = settings.showingERB || false;
59
+ this.showingERBOutlines = settings.showingERBOutlines || false;
60
+ this.showingERBHoverReveal = settings.showingERBHoverReveal || false;
61
+ this.showingTooltips = settings.showingTooltips !== undefined ? settings.showingTooltips : true;
62
+ this.showingViewOutlines = settings.showingViewOutlines || false;
63
+ this.showingPartialOutlines = settings.showingPartialOutlines || false;
64
+ this.showingComponentOutlines = settings.showingComponentOutlines || false;
65
+ this.menuOpen = settings.menuOpen || false;
66
+ } catch (e) {
67
+ console.warn('Failed to load Herb dev tools settings:', e);
68
+ }
69
+ }
70
+ }
71
+
72
+ private saveSettings() {
73
+ const settings = {
74
+ showingERB: this.showingERB,
75
+ showingERBOutlines: this.showingERBOutlines,
76
+ showingERBHoverReveal: this.showingERBHoverReveal,
77
+ showingTooltips: this.showingTooltips,
78
+ showingViewOutlines: this.showingViewOutlines,
79
+ showingPartialOutlines: this.showingPartialOutlines,
80
+ showingComponentOutlines: this.showingComponentOutlines,
81
+ menuOpen: this.menuOpen
82
+ };
83
+
84
+ localStorage.setItem(HerbOverlay.SETTINGS_KEY, JSON.stringify(settings));
85
+ this.updateMenuButtonState();
86
+ }
87
+
88
+ private updateMenuButtonState() {
89
+ const menuTrigger = document.getElementById('herbMenuTrigger');
90
+ if (menuTrigger) {
91
+ const hasActiveOptions = this.showingERB || this.showingERBOutlines || this.showingViewOutlines || this.showingPartialOutlines || this.showingComponentOutlines;
92
+ if (hasActiveOptions) {
93
+ menuTrigger.classList.add('has-active-options');
94
+ } else {
95
+ menuTrigger.classList.remove('has-active-options');
96
+ }
97
+ }
98
+ }
99
+
100
+ private injectMenu() {
101
+ const existingMenu = document.querySelector('.herb-floating-menu');
102
+
103
+ if (existingMenu) {
104
+ return;
105
+ }
106
+
107
+ const menuHTML = `
108
+ <div class="herb-floating-menu">
109
+ <button class="herb-menu-trigger" id="herbMenuTrigger">
110
+ <span class="herb-icon">🌿</span>
111
+ <span class="herb-text">Herb</span>
112
+ </button>
113
+
114
+ <div class="herb-menu-panel" id="herbMenuPanel">
115
+ <div class="herb-menu-header">Herb Debug Tools</div>
116
+
117
+ <div class="herb-toggle-item">
118
+ <label class="herb-toggle-label">
119
+ <input type="checkbox" id="herbToggleViewOutlines" class="herb-toggle-input">
120
+ <span class="herb-toggle-switch"></span>
121
+ <span class="herb-toggle-text">Show View Outlines</span>
122
+ </label>
123
+ </div>
124
+
125
+ <div class="herb-toggle-item">
126
+ <label class="herb-toggle-label">
127
+ <input type="checkbox" id="herbTogglePartialOutlines" class="herb-toggle-input">
128
+ <span class="herb-toggle-switch"></span>
129
+ <span class="herb-toggle-text">Show Partial Outlines</span>
130
+ </label>
131
+ </div>
132
+
133
+ <div class="herb-toggle-item">
134
+ <label class="herb-toggle-label">
135
+ <input type="checkbox" id="herbToggleComponentOutlines" class="herb-toggle-input">
136
+ <span class="herb-toggle-switch"></span>
137
+ <span class="herb-toggle-text">Show Component Outlines</span>
138
+ </label>
139
+ </div>
140
+
141
+ <div class="herb-toggle-item">
142
+ <label class="herb-toggle-label">
143
+ <input type="checkbox" id="herbToggleERBOutlines" class="herb-toggle-input">
144
+ <span class="herb-toggle-switch"></span>
145
+ <span class="herb-toggle-text">Show ERB Output Outlines</span>
146
+ </label>
147
+
148
+ <div class="herb-nested-toggle" id="herbERBHoverRevealNested" style="display: none;">
149
+ <label class="herb-toggle-label herb-nested-label">
150
+ <input type="checkbox" id="herbToggleERBHoverReveal" class="herb-toggle-input">
151
+ <span class="herb-toggle-switch herb-nested-switch"></span>
152
+ <span class="herb-toggle-text">Reveal ERB Output tag on hover</span>
153
+ </label>
154
+ </div>
155
+
156
+ <div class="herb-nested-toggle" id="herbTooltipsNested" style="display: none;">
157
+ <label class="herb-toggle-label herb-nested-label">
158
+ <input type="checkbox" id="herbToggleTooltips" class="herb-toggle-input">
159
+ <span class="herb-toggle-switch herb-nested-switch"></span>
160
+ <span class="herb-toggle-text">Show Tooltips</span>
161
+ </label>
162
+ </div>
163
+ </div>
164
+
165
+ <div class="herb-toggle-item">
166
+ <label class="herb-toggle-label">
167
+ <input type="checkbox" id="herbToggleERB" class="herb-toggle-input">
168
+ <span class="herb-toggle-switch"></span>
169
+ <span class="herb-toggle-text">Show ERB Output Tags</span>
170
+ </label>
171
+ </div>
172
+
173
+ <div class="herb-disable-all-section">
174
+ <button id="herbDisableAll" class="herb-disable-all-btn">Disable All</button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ `;
179
+
180
+ document.body.insertAdjacentHTML('beforeend', menuHTML);
181
+ }
182
+
183
+ private applySettings() {
184
+ this.toggleViewOutlines(this.showingViewOutlines);
185
+ this.togglePartialOutlines(this.showingPartialOutlines);
186
+ this.toggleComponentOutlines(this.showingComponentOutlines);
187
+ this.toggleERBTags(this.showingERB);
188
+ this.toggleERBOutlines(this.showingERBOutlines);
189
+
190
+ const menuTrigger = document.getElementById('herbMenuTrigger');
191
+ const menuPanel = document.getElementById('herbMenuPanel');
192
+
193
+ if (menuTrigger && menuPanel && this.menuOpen) {
194
+ menuTrigger.classList.add('active');
195
+ menuPanel.classList.add('open');
196
+ }
197
+ }
198
+
199
+ private setupMenuToggle() {
200
+ const menuTrigger = document.getElementById('herbMenuTrigger');
201
+ const menuPanel = document.getElementById('herbMenuPanel');
202
+
203
+ if (menuTrigger && menuPanel) {
204
+ menuTrigger.addEventListener('click', () => {
205
+ this.menuOpen = !this.menuOpen;
206
+
207
+ if (this.menuOpen) {
208
+ menuTrigger.classList.add('active');
209
+ menuPanel.classList.add('open');
210
+ } else {
211
+ menuTrigger.classList.remove('active');
212
+ menuPanel.classList.remove('open');
213
+ }
214
+
215
+ this.saveSettings();
216
+ });
217
+
218
+ document.addEventListener('click', (e) => {
219
+ const target = e.target as HTMLElement;
220
+ const floatingMenu = document.querySelector('.herb-floating-menu');
221
+
222
+ if (floatingMenu && !floatingMenu.contains(target) && this.menuOpen) {
223
+ this.menuOpen = false;
224
+ menuTrigger.classList.remove('active');
225
+ menuPanel.classList.remove('open');
226
+ this.saveSettings();
227
+ }
228
+ });
229
+ }
230
+ }
231
+
232
+ private setupTurboListeners() {
233
+ document.addEventListener('turbo:load', () => {
234
+ this.reinitializeAfterNavigation();
235
+ });
236
+
237
+ document.addEventListener('turbo:render', () => {
238
+ this.reinitializeAfterNavigation();
239
+ });
240
+
241
+ document.addEventListener('turbo:visit', () => {
242
+ this.reinitializeAfterNavigation();
243
+ });
244
+ }
245
+
246
+ private reinitializeAfterNavigation() {
247
+ this.injectMenu();
248
+ this.setupMenuToggle();
249
+ this.setupToggleSwitches();
250
+ this.applySettings();
251
+ this.updateMenuButtonState();
252
+ }
253
+
254
+ private setupToggleSwitches() {
255
+ const toggleViewOutlinesSwitch = document.getElementById('herbToggleViewOutlines') as HTMLInputElement;
256
+
257
+ if (toggleViewOutlinesSwitch) {
258
+ toggleViewOutlinesSwitch.checked = this.showingViewOutlines;
259
+ toggleViewOutlinesSwitch.addEventListener('change', () => {
260
+ this.toggleViewOutlines(toggleViewOutlinesSwitch.checked);
261
+ });
262
+ }
263
+
264
+ const togglePartialOutlinesSwitch = document.getElementById('herbTogglePartialOutlines') as HTMLInputElement;
265
+
266
+ if (togglePartialOutlinesSwitch) {
267
+ togglePartialOutlinesSwitch.checked = this.showingPartialOutlines;
268
+ togglePartialOutlinesSwitch.addEventListener('change', () => {
269
+ this.togglePartialOutlines(togglePartialOutlinesSwitch.checked);
270
+ });
271
+ }
272
+
273
+ const toggleComponentOutlinesSwitch = document.getElementById('herbToggleComponentOutlines') as HTMLInputElement;
274
+
275
+ if (toggleComponentOutlinesSwitch) {
276
+ toggleComponentOutlinesSwitch.checked = this.showingComponentOutlines;
277
+ toggleComponentOutlinesSwitch.addEventListener('change', () => {
278
+ this.toggleComponentOutlines(toggleComponentOutlinesSwitch.checked);
279
+ });
280
+ }
281
+
282
+ const toggleERBSwitch = document.getElementById('herbToggleERB') as HTMLInputElement;
283
+ const toggleERBOutlinesSwitch = document.getElementById('herbToggleERBOutlines') as HTMLInputElement;
284
+
285
+ if (toggleERBSwitch) {
286
+ toggleERBSwitch.checked = this.showingERB;
287
+ toggleERBSwitch.addEventListener('change', () => {
288
+ if (toggleERBSwitch.checked && toggleERBOutlinesSwitch) {
289
+ toggleERBOutlinesSwitch.checked = false;
290
+ this.toggleERBOutlines(false);
291
+ }
292
+ this.toggleERBTags(toggleERBSwitch.checked);
293
+ });
294
+ }
295
+
296
+ if (toggleERBOutlinesSwitch) {
297
+ toggleERBOutlinesSwitch.checked = this.showingERBOutlines;
298
+ toggleERBOutlinesSwitch.addEventListener('change', () => {
299
+ if (toggleERBOutlinesSwitch.checked && toggleERBSwitch) {
300
+ toggleERBSwitch.checked = false;
301
+ this.toggleERBTags(false);
302
+ }
303
+
304
+ this.toggleERBOutlines(toggleERBOutlinesSwitch.checked);
305
+ this.updateNestedToggleVisibility();
306
+ });
307
+ } else {
308
+ console.warn('ERB outlines toggle switch not found');
309
+ }
310
+
311
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal') as HTMLInputElement;
312
+
313
+ if (toggleERBHoverRevealSwitch) {
314
+ toggleERBHoverRevealSwitch.checked = this.showingERBHoverReveal;
315
+ toggleERBHoverRevealSwitch.addEventListener('change', () => {
316
+ this.toggleERBHoverReveal(toggleERBHoverRevealSwitch.checked);
317
+ });
318
+ }
319
+
320
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips') as HTMLInputElement;
321
+
322
+ if (toggleTooltipsSwitch) {
323
+ toggleTooltipsSwitch.checked = this.showingTooltips;
324
+ toggleTooltipsSwitch.addEventListener('change', () => {
325
+ this.toggleTooltips(toggleTooltipsSwitch.checked);
326
+ });
327
+ }
328
+
329
+ this.updateNestedToggleVisibility();
330
+
331
+ const disableAllBtn = document.getElementById('herbDisableAll') as HTMLButtonElement;
332
+ if (disableAllBtn) {
333
+ disableAllBtn.addEventListener('click', () => {
334
+ this.disableAll();
335
+ });
336
+ }
337
+ }
338
+
339
+ private toggleViewOutlines(show?: boolean) {
340
+ this.showingViewOutlines = show !== undefined ? show : !this.showingViewOutlines;
341
+ const viewOutlines = document.querySelectorAll('[data-herb-debug-outline-type="view"], [data-herb-debug-outline-type*="view"]');
342
+
343
+ viewOutlines.forEach((outline) => {
344
+ const element = outline as HTMLElement;
345
+
346
+ if (this.showingViewOutlines) {
347
+ element.style.outline = '2px dotted #3b82f6';
348
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
349
+ element.classList.add('show-outline');
350
+
351
+ this.createOverlayLabel(element, 'view');
352
+ } else {
353
+ element.style.outline = 'none';
354
+ element.style.outlineOffset = '0';
355
+ element.classList.remove('show-outline');
356
+
357
+ this.removeOverlayLabel(element);
358
+ }
359
+ });
360
+
361
+ this.saveSettings();
362
+ }
363
+
364
+ private togglePartialOutlines(show?: boolean) {
365
+ this.showingPartialOutlines = show !== undefined ? show : !this.showingPartialOutlines;
366
+ const partialOutlines = document.querySelectorAll('[data-herb-debug-outline-type="partial"], [data-herb-debug-outline-type*="partial"]');
367
+
368
+ partialOutlines.forEach((outline) => {
369
+ const element = outline as HTMLElement;
370
+
371
+ if (this.showingPartialOutlines) {
372
+ element.style.outline = '2px dotted #10b981';
373
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
374
+ element.classList.add('show-outline');
375
+
376
+ this.createOverlayLabel(element, 'partial');
377
+ } else {
378
+ element.style.outline = 'none';
379
+ element.style.outlineOffset = '0';
380
+ element.classList.remove('show-outline');
381
+
382
+ this.removeOverlayLabel(element);
383
+ }
384
+ });
385
+
386
+ this.saveSettings();
387
+ }
388
+
389
+ private toggleComponentOutlines(show?: boolean) {
390
+ this.showingComponentOutlines = show !== undefined ? show : !this.showingComponentOutlines;
391
+ const componentOutlines = document.querySelectorAll('[data-herb-debug-outline-type="component"], [data-herb-debug-outline-type*="component"]');
392
+
393
+ componentOutlines.forEach((outline) => {
394
+ const element = outline as HTMLElement;
395
+
396
+ if (this.showingComponentOutlines) {
397
+ element.style.outline = '2px dotted #f59e0b';
398
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
399
+ element.classList.add('show-outline');
400
+
401
+ this.createOverlayLabel(element, 'component');
402
+ } else {
403
+ element.style.outline = 'none';
404
+ element.style.outlineOffset = '0';
405
+ element.classList.remove('show-outline');
406
+
407
+ this.removeOverlayLabel(element);
408
+ }
409
+ });
410
+
411
+ this.saveSettings();
412
+ }
413
+
414
+ private createOverlayLabel(element: HTMLElement, type: 'view' | 'partial' | 'component') {
415
+ if (element.querySelector('.herb-overlay-label')) {
416
+ return;
417
+ }
418
+
419
+ const shortName = element.getAttribute('data-herb-debug-file-name') || '';
420
+ const relativePath = element.getAttribute('data-herb-debug-file-relative-path') || shortName;
421
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path') || relativePath;
422
+ const label = document.createElement('div');
423
+
424
+ label.className = 'herb-overlay-label';
425
+ label.textContent = shortName;
426
+ label.setAttribute('data-label-setup', 'true');
427
+
428
+ label.addEventListener('mouseenter', () => {
429
+ label.textContent = relativePath;
430
+
431
+ document.querySelectorAll('.herb-overlay-label').forEach(otherLabel => {
432
+ (otherLabel as HTMLElement).style.zIndex = '1000';
433
+ });
434
+
435
+ label.style.zIndex = '1002';
436
+ });
437
+
438
+ label.addEventListener('mouseleave', () => {
439
+ label.textContent = shortName;
440
+ label.style.zIndex = '1000';
441
+ });
442
+
443
+ label.addEventListener('click', (e) => {
444
+ e.stopPropagation();
445
+ this.openFileInEditor(fullPath, 1, 1);
446
+ });
447
+
448
+ const shouldAttachToParent = element.getAttribute('data-herb-debug-attach-to-parent') === 'true';
449
+
450
+ if (shouldAttachToParent && element.parentElement) {
451
+ const parent = element.parentElement;
452
+
453
+ element.style.outline = 'none';
454
+ element.classList.remove('show-outline');
455
+
456
+ const outlineColor = type === 'component' ? '#f59e0b' : type === 'partial' ? '#10b981' : '#3b82f6';
457
+ parent.style.outline = `2px dotted ${outlineColor}`;
458
+ parent.style.outlineOffset = parent.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
459
+ parent.classList.add('show-outline');
460
+
461
+ parent.setAttribute('data-herb-debug-attached-outline-type', type);
462
+
463
+ parent.style.position = 'relative';
464
+ label.style.position = 'absolute';
465
+ label.style.top = '0';
466
+ label.style.left = '0';
467
+
468
+ parent.appendChild(label);
469
+ return;
470
+ }
471
+
472
+ element.style.position = 'relative';
473
+ element.appendChild(label);
474
+ }
475
+
476
+ private removeOverlayLabel(element: HTMLElement) {
477
+ const shouldAttachToParent = element.getAttribute('data-herb-debug-attach-to-parent') === 'true';
478
+
479
+ if (shouldAttachToParent && element.parentElement) {
480
+ const parent = element.parentElement;
481
+ const label = parent.querySelector('.herb-overlay-label');
482
+
483
+ if (label) {
484
+ label.remove();
485
+ }
486
+
487
+ parent.style.outline = 'none';
488
+ parent.style.outlineOffset = '0';
489
+ parent.classList.remove('show-outline');
490
+ parent.removeAttribute('data-herb-debug-attached-outline-type');
491
+ } else {
492
+ const label = element.querySelector('.herb-overlay-label');
493
+
494
+ if (label) {
495
+ label.remove();
496
+ }
497
+ }
498
+ }
499
+
500
+ private resetShowingERB() {
501
+ const elements = document.querySelectorAll('[data-herb-debug-showing-erb')
502
+
503
+ elements.forEach(element => {
504
+ const originalContent = element.getAttribute('data-herb-debug-original') || "";
505
+
506
+ element.innerHTML = originalContent;
507
+ element.removeAttribute("data-herb-debug-showing-erb")
508
+ })
509
+ }
510
+
511
+ private toggleERBTags(show?: boolean) {
512
+ this.showingERB = show !== undefined ? show : !this.showingERB;
513
+ const erbOutputs = document.querySelectorAll<HTMLElement>('[data-herb-debug-outline-type*="erb-output"]');
514
+
515
+ erbOutputs.forEach((element) => {
516
+ const erbCode = element.getAttribute('data-herb-debug-erb');
517
+
518
+ if (this.showingERB && erbCode) {
519
+ // this.resetShowingERB()
520
+
521
+ if (!element.hasAttribute('data-herb-debug-original')) {
522
+ element.setAttribute('data-herb-debug-original', element.innerHTML);
523
+ }
524
+
525
+ element.textContent = erbCode;
526
+ element.setAttribute("data-herb-debug-showing-erb", "true")
527
+
528
+ element.style.background = '#f3e8ff';
529
+ element.style.color = '#7c3aed';
530
+
531
+ if (this.showingTooltips) {
532
+ this.addTooltipHoverHandler(element);
533
+ }
534
+ } else {
535
+ const originalContent = element.getAttribute('data-herb-debug-original') || "";
536
+
537
+ if (element && element.hasAttribute("data-herb-debug-showing-erb")) {
538
+ element.innerHTML = originalContent;
539
+ element.removeAttribute("data-herb-debug-showing-erb")
540
+ }
541
+
542
+ element.style.background = 'transparent';
543
+ element.style.color = 'inherit';
544
+
545
+ this.removeTooltipHoverHandler(element);
546
+ this.removeHoverTooltip(element);
547
+ }
548
+ });
549
+
550
+ this.saveSettings();
551
+ }
552
+
553
+ private toggleERBOutlines(show?: boolean) {
554
+ this.showingERBOutlines = show !== undefined ? show : !this.showingERBOutlines;
555
+
556
+ this.clearCurrentHoveredERB();
557
+
558
+ const erbOutputs = document.querySelectorAll<HTMLElement>('[data-herb-debug-outline-type*="erb-output"]');
559
+
560
+ erbOutputs.forEach(element => {
561
+ const inserted = element.hasAttribute("data-herb-debug-inserted")
562
+ const needsWrapperToggled = (inserted && !element.children[0])
563
+
564
+ const realElement = (element.children[0] as HTMLElement) || element
565
+
566
+ if (this.showingERBOutlines) {
567
+
568
+ realElement.style.outline = '2px dotted #a78bfa';
569
+ realElement.style.outlineOffset = '1px';
570
+
571
+ if (needsWrapperToggled) {
572
+ element.style.display = 'inline';
573
+ }
574
+
575
+ if (this.showingTooltips) {
576
+ this.addTooltipHoverHandler(element);
577
+ }
578
+
579
+ if (this.showingERBHoverReveal) {
580
+ this.addERBHoverReveal(element);
581
+ }
582
+ } else {
583
+ realElement.style.outline = 'none';
584
+ realElement.style.outlineOffset = '0';
585
+
586
+ if (needsWrapperToggled) {
587
+ element.style.display = 'contents';
588
+ }
589
+
590
+ this.removeTooltipHoverHandler(element);
591
+ this.removeHoverTooltip(element);
592
+ this.removeERBHoverReveal(element);
593
+ }
594
+ });
595
+
596
+ this.saveSettings();
597
+ }
598
+
599
+ private updateNestedToggleVisibility() {
600
+ const nestedToggle = document.getElementById('herbERBHoverRevealNested');
601
+ const tooltipsNestedToggle = document.getElementById('herbTooltipsNested');
602
+
603
+ if (nestedToggle) {
604
+ nestedToggle.style.display = this.showingERBOutlines ? 'block' : 'none';
605
+ }
606
+
607
+ if (tooltipsNestedToggle) {
608
+ tooltipsNestedToggle.style.display = this.showingERBOutlines ? 'block' : 'none';
609
+ }
610
+ }
611
+
612
+ private toggleERBHoverReveal(show?: boolean) {
613
+ this.showingERBHoverReveal = show !== undefined ? show : !this.showingERBHoverReveal;
614
+
615
+ if (this.showingERBHoverReveal && this.showingTooltips) {
616
+ this.toggleTooltips(false);
617
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips') as HTMLInputElement;
618
+
619
+ if (toggleTooltipsSwitch) {
620
+ toggleTooltipsSwitch.checked = false;
621
+ }
622
+ }
623
+
624
+ this.clearCurrentHoveredERB();
625
+
626
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
627
+
628
+ erbOutputs.forEach((el) => {
629
+ const element = el as HTMLElement;
630
+
631
+ this.removeERBHoverReveal(element);
632
+
633
+ if (this.showingERBHoverReveal && this.showingERBOutlines) {
634
+ this.addERBHoverReveal(element);
635
+ }
636
+ });
637
+
638
+ this.saveSettings();
639
+ }
640
+
641
+ private clearCurrentHoveredERB() {
642
+ if (this.currentlyHoveredERBElement) {
643
+ const handlers = (this.currentlyHoveredERBElement as any)._erbHoverHandlers;
644
+ if (handlers) {
645
+ handlers.hideERBCode();
646
+ }
647
+ this.currentlyHoveredERBElement = null;
648
+ }
649
+ }
650
+
651
+ private handleRevealedERBClick = (event: Event) => {
652
+ event.stopPropagation();
653
+ event.preventDefault();
654
+
655
+ const element = event.currentTarget as HTMLElement;
656
+ if (!element) return;
657
+
658
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path');
659
+ const line = element.getAttribute('data-herb-debug-line');
660
+ const column = element.getAttribute('data-herb-debug-column');
661
+
662
+ if (fullPath) {
663
+ this.openFileInEditor(
664
+ fullPath,
665
+ line ? parseInt(line) : 1,
666
+ column ? parseInt(column) : 1
667
+ );
668
+ }
669
+ }
670
+
671
+ private addERBHoverReveal(element: HTMLElement) {
672
+ const erbCode = element.getAttribute('data-herb-debug-erb');
673
+ if (!erbCode) return;
674
+
675
+ this.removeERBHoverReveal(element);
676
+
677
+ if (!element.hasAttribute('data-herb-debug-original')) {
678
+ element.setAttribute('data-herb-debug-original', element.innerHTML);
679
+ }
680
+
681
+ const showERBCode = () => {
682
+ if (!this.showingERBHoverReveal || !this.showingERBOutlines) {
683
+ return;
684
+ }
685
+
686
+ if (this.currentlyHoveredERBElement === element) {
687
+ return;
688
+ }
689
+
690
+ this.clearCurrentHoveredERB();
691
+
692
+ this.currentlyHoveredERBElement = element;
693
+
694
+ element.style.background = '#f3e8ff';
695
+ element.style.color = '#7c3aed';
696
+ element.style.fontFamily = 'inherit';
697
+ element.style.fontSize = 'inherit';
698
+ element.style.borderRadius = '3px';
699
+ element.style.cursor = 'pointer';
700
+ element.textContent = erbCode;
701
+
702
+ element.addEventListener('click', this.handleRevealedERBClick);
703
+ };
704
+
705
+ const hideERBCode = () => {
706
+ if (this.currentlyHoveredERBElement === element) {
707
+ this.currentlyHoveredERBElement = null;
708
+ }
709
+
710
+ const originalContent = element.getAttribute('data-herb-debug-original');
711
+
712
+ if (originalContent) {
713
+ element.innerHTML = originalContent;
714
+ }
715
+
716
+ element.style.background = 'transparent';
717
+ element.style.color = 'inherit';
718
+ element.style.fontFamily = 'inherit';
719
+ element.style.fontSize = 'inherit';
720
+ element.style.borderRadius = '0';
721
+ element.style.cursor = 'default';
722
+
723
+ element.removeEventListener('click', this.handleRevealedERBClick);
724
+ };
725
+
726
+ (element as any)._erbHoverHandlers = { showERBCode, hideERBCode };
727
+
728
+ element.addEventListener('mouseenter', showERBCode);
729
+ }
730
+
731
+ private removeERBHoverReveal(element: HTMLElement) {
732
+ const handlers = (element as any)._erbHoverHandlers;
733
+ if (handlers) {
734
+ element.removeEventListener('mouseenter', handlers.showERBCode);
735
+
736
+ delete (element as any)._erbHoverHandlers;
737
+
738
+ handlers.hideERBCode();
739
+ }
740
+ }
741
+
742
+ private createHoverTooltip(element: HTMLElement, elementForPosition: HTMLElement) {
743
+ this.removeHoverTooltip(element);
744
+
745
+ const relativePath = element.getAttribute('data-herb-debug-file-relative-path') || element.getAttribute('data-herb-debug-file-name') || '';
746
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path') || relativePath;
747
+ const line = element.getAttribute('data-herb-debug-line') || '';
748
+ const column = element.getAttribute('data-herb-debug-column') || '';
749
+ const erb = element.getAttribute('data-herb-debug-erb') || '';
750
+
751
+ if (!relativePath || !erb) return;
752
+
753
+ const tooltip = document.createElement('div');
754
+ tooltip.className = 'herb-tooltip';
755
+
756
+ tooltip.innerHTML = `
757
+ <div class="herb-location" data-tooltip="Open in Editor">
758
+ <span class="herb-file-path">${relativePath}:${line}:${column}</span>
759
+ <button class="herb-copy-path-btn" data-tooltip="Copy file path">📋</button>
760
+ </div>
761
+ <div class="herb-erb-code">${erb}</div>
762
+ `;
763
+
764
+ let hideTimeout: number | null = null;
765
+
766
+ const showTooltip = () => {
767
+ if (hideTimeout) {
768
+ clearTimeout(hideTimeout);
769
+ hideTimeout = null;
770
+ }
771
+ tooltip.classList.add('visible');
772
+ };
773
+
774
+ const hideTooltip = () => {
775
+ hideTimeout = window.setTimeout(() => {
776
+ tooltip.classList.remove('visible');
777
+ }, 100);
778
+ };
779
+
780
+ element.addEventListener('mouseenter', showTooltip);
781
+ element.addEventListener('mouseleave', hideTooltip);
782
+ tooltip.addEventListener('mouseenter', showTooltip);
783
+ tooltip.addEventListener('mouseleave', hideTooltip);
784
+
785
+ const locationElement = tooltip.querySelector('.herb-location');
786
+ const openInEditor = (e: Event) => {
787
+ if ((e.target as HTMLElement).closest('.herb-copy-path-btn')) {
788
+ return;
789
+ }
790
+ e.preventDefault();
791
+ e.stopPropagation();
792
+ this.openFileInEditor(fullPath, parseInt(line), parseInt(column));
793
+ };
794
+ locationElement?.addEventListener('click', openInEditor);
795
+
796
+ const copyButton = tooltip.querySelector('.herb-copy-path-btn');
797
+ const copyFilePath = (e: Event) => {
798
+ e.preventDefault();
799
+ e.stopPropagation();
800
+ const textToCopy = `${relativePath}:${line}:${column}`;
801
+ navigator.clipboard.writeText(textToCopy).then(() => {
802
+ copyButton!.textContent = '✅';
803
+ setTimeout(() => {
804
+ copyButton!.textContent = '📋';
805
+ }, 1000);
806
+ }).catch((err) => {
807
+ console.error('Failed to copy file path:', err);
808
+ });
809
+ };
810
+ copyButton?.addEventListener('click', copyFilePath);
811
+
812
+ const positionTooltip = () => {
813
+ const elementRect = elementForPosition.getBoundingClientRect();
814
+ const viewportHeight = window.innerHeight;
815
+ const viewportWidth = window.innerWidth;
816
+
817
+ tooltip.style.position = 'fixed';
818
+ tooltip.style.left = '0';
819
+ tooltip.style.top = '0';
820
+ tooltip.style.transform = 'none';
821
+ tooltip.style.bottom = 'auto';
822
+
823
+ const actualTooltipRect = tooltip.getBoundingClientRect();
824
+ const tooltipWidth = actualTooltipRect.width;
825
+ const tooltipHeight = actualTooltipRect.height;
826
+
827
+ let left = elementRect.left + (elementRect.width / 2) - (tooltipWidth / 2);
828
+ let top = elementRect.top - tooltipHeight - 8;
829
+
830
+ if (left < 8) {
831
+ left = 8;
832
+ } else if (left + tooltipWidth > viewportWidth - 8) {
833
+ left = viewportWidth - tooltipWidth - 8;
834
+ }
835
+
836
+ if (top < 8) {
837
+ top = elementRect.bottom + 8;
838
+
839
+ if (top + tooltipHeight > viewportHeight - 8) {
840
+ top = Math.max(8, (viewportHeight - tooltipHeight) / 2);
841
+ }
842
+ }
843
+
844
+ if (top + tooltipHeight > viewportHeight - 8) {
845
+ top = viewportHeight - tooltipHeight - 8;
846
+ }
847
+
848
+ tooltip.style.position = 'fixed';
849
+ tooltip.style.left = `${left}px`;
850
+ tooltip.style.top = `${top}px`;
851
+ tooltip.style.transform = 'none';
852
+ tooltip.style.bottom = 'auto';
853
+ };
854
+
855
+ (element as any)._tooltipHandlers = { showTooltip, hideTooltip, openInEditor, copyFilePath, positionTooltip };
856
+ (tooltip as any)._tooltipHandlers = { showTooltip, hideTooltip };
857
+
858
+ element.appendChild(tooltip);
859
+
860
+ setTimeout(positionTooltip, 0);
861
+ window.addEventListener('scroll', positionTooltip, { passive: true });
862
+ window.addEventListener('resize', positionTooltip, { passive: true });
863
+ }
864
+
865
+ private removeHoverTooltip(element: HTMLElement) {
866
+ const tooltip = element.querySelector('.herb-tooltip');
867
+
868
+ if (tooltip) {
869
+ const handlers = (element as any)._tooltipHandlers;
870
+ const tooltipHandlers = (tooltip as any)._tooltipHandlers;
871
+
872
+ if (handlers) {
873
+ element.removeEventListener('mouseenter', handlers.showTooltip);
874
+ element.removeEventListener('mouseleave', handlers.hideTooltip);
875
+
876
+ const locationElement = tooltip.querySelector('.herb-location');
877
+ locationElement?.removeEventListener('click', handlers.openInEditor);
878
+
879
+ const copyButton = tooltip.querySelector('.herb-copy-path-btn');
880
+ copyButton?.removeEventListener('click', handlers.copyFilePath);
881
+
882
+ if (handlers.positionTooltip) {
883
+ window.removeEventListener('scroll', handlers.positionTooltip);
884
+ window.removeEventListener('resize', handlers.positionTooltip);
885
+ }
886
+
887
+ delete (element as any)._tooltipHandlers;
888
+ }
889
+
890
+ if (tooltipHandlers) {
891
+ tooltip.removeEventListener('mouseenter', tooltipHandlers.showTooltip);
892
+ tooltip.removeEventListener('mouseleave', tooltipHandlers.hideTooltip);
893
+ delete (tooltip as any)._tooltipHandlers;
894
+ }
895
+
896
+ tooltip.remove();
897
+ }
898
+ }
899
+
900
+ private addTooltipHoverHandler(element: HTMLElement) {
901
+ this.removeTooltipHoverHandler(element);
902
+
903
+ const lazyTooltipHandler = () => {
904
+ if (!this.showingTooltips || !this.showingERBOutlines) {
905
+ return;
906
+ }
907
+
908
+ if (element.querySelector('.herb-tooltip')) {
909
+ return;
910
+ }
911
+
912
+ this.createHoverTooltip(element, element);
913
+ };
914
+
915
+ (element as any)._lazyTooltipHandler = lazyTooltipHandler;
916
+ element.addEventListener('mouseenter', lazyTooltipHandler);
917
+ }
918
+
919
+ private removeTooltipHoverHandler(element: HTMLElement) {
920
+ const handler = (element as any)._lazyTooltipHandler;
921
+ if (handler) {
922
+ element.removeEventListener('mouseenter', handler);
923
+ delete (element as any)._lazyTooltipHandler;
924
+ }
925
+ }
926
+
927
+ private openFileInEditor(file: string, line: number, column: number) {
928
+ const absolutePath = file.startsWith('/') ? file : (this.projectPath ? `${this.projectPath}/${file}` : file);
929
+
930
+ const editors = [
931
+ `vscode://file/${absolutePath}:${line}:${column}`,
932
+ `subl://open?url=file://${absolutePath}&line=${line}&column=${column}`,
933
+ `atom://core/open/file?filename=${absolutePath}&line=${line}&column=${column}`,
934
+ `txmt://open?url=file://${absolutePath}&line=${line}&column=${column}`,
935
+ ];
936
+
937
+ try {
938
+ window.open(editors[0], '_self');
939
+ } catch (error) {
940
+ console.log(`Open in editor: ${absolutePath}:${line}:${column}`);
941
+ }
942
+ }
943
+
944
+ private toggleTooltips(show?: boolean) {
945
+ this.showingTooltips = show !== undefined ? show : !this.showingTooltips;
946
+
947
+ if (this.showingTooltips && this.showingERBHoverReveal) {
948
+ this.toggleERBHoverReveal(false);
949
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal') as HTMLInputElement;
950
+
951
+ if (toggleERBHoverRevealSwitch) {
952
+ toggleERBHoverRevealSwitch.checked = false;
953
+ }
954
+ }
955
+
956
+ const erbOutputs = document.querySelectorAll<HTMLElement>('[data-herb-debug-outline-type*="erb-output"]');
957
+
958
+ erbOutputs.forEach((element) => {
959
+ if (this.showingERBOutlines && this.showingTooltips) {
960
+ this.addTooltipHoverHandler(element);
961
+ } else {
962
+ this.removeTooltipHoverHandler(element);
963
+ this.removeHoverTooltip(element);
964
+ }
965
+ });
966
+
967
+ this.saveSettings();
968
+ }
969
+
970
+ private disableAll() {
971
+ this.clearCurrentHoveredERB();
972
+
973
+ this.toggleViewOutlines(false);
974
+ this.togglePartialOutlines(false);
975
+ this.toggleComponentOutlines(false);
976
+ this.toggleERBTags(false);
977
+ this.toggleERBOutlines(false);
978
+ this.toggleERBHoverReveal(false);
979
+ this.toggleTooltips(false);
980
+
981
+ const toggleViewOutlinesSwitch = document.getElementById('herbToggleViewOutlines') as HTMLInputElement;
982
+ const togglePartialOutlinesSwitch = document.getElementById('herbTogglePartialOutlines') as HTMLInputElement;
983
+ const toggleComponentOutlinesSwitch = document.getElementById('herbToggleComponentOutlines') as HTMLInputElement;
984
+ const toggleERBSwitch = document.getElementById('herbToggleERB') as HTMLInputElement;
985
+ const toggleERBOutlinesSwitch = document.getElementById('herbToggleERBOutlines') as HTMLInputElement;
986
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal') as HTMLInputElement;
987
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips') as HTMLInputElement;
988
+
989
+ if (toggleViewOutlinesSwitch) toggleViewOutlinesSwitch.checked = false;
990
+ if (togglePartialOutlinesSwitch) togglePartialOutlinesSwitch.checked = false;
991
+ if (toggleComponentOutlinesSwitch) toggleComponentOutlinesSwitch.checked = false;
992
+ if (toggleERBSwitch) toggleERBSwitch.checked = false;
993
+ if (toggleERBOutlinesSwitch) toggleERBOutlinesSwitch.checked = false;
994
+ if (toggleERBHoverRevealSwitch) toggleERBHoverRevealSwitch.checked = false;
995
+ if (toggleTooltipsSwitch) toggleTooltipsSwitch.checked = false;
996
+ }
997
+
998
+ private initializeErrorOverlay() {
999
+ this.errorOverlay = new ErrorOverlay();
1000
+ }
1001
+ }