reactionview 0.0.1 → 0.1.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,1796 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReActionViewDevTools = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ function styleInject(css, ref) {
8
+ if ( ref === void 0 ) ref = {};
9
+ var insertAt = ref.insertAt;
10
+
11
+ if (typeof document === 'undefined') { return; }
12
+
13
+ var head = document.head || document.getElementsByTagName('head')[0];
14
+ var style = document.createElement('style');
15
+ style.type = 'text/css';
16
+
17
+ if (insertAt === 'top') {
18
+ if (head.firstChild) {
19
+ head.insertBefore(style, head.firstChild);
20
+ } else {
21
+ head.appendChild(style);
22
+ }
23
+ } else {
24
+ head.appendChild(style);
25
+ }
26
+
27
+ if (style.styleSheet) {
28
+ style.styleSheet.cssText = css;
29
+ } else {
30
+ style.appendChild(document.createTextNode(css));
31
+ }
32
+ }
33
+
34
+ var css_248z = ".herb-overlay-label{background:rgba(0,0,0,.8);border-radius:3px;color:#fff;cursor:pointer;display:block;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,monospace;font-size:11px;font-weight:500;left:4px;line-height:1.2;padding:2px 6px;position:absolute;top:-18px;transition:all .2s ease;white-space:nowrap;z-index:1000}.herb-overlay-label:hover{background:rgba(0,0,0,.9);color:#374151;transform:scale(1.02);z-index:1001}[data-herb-debug-outline-type*=view]>.herb-overlay-label{background:#dbeafe;border-color:#93c5fd;color:#1e40af}[data-herb-debug-outline-type*=partial]>.herb-overlay-label{background:#d1fae5;border-color:#6ee7b7;color:#065f46}[data-herb-debug-outline-type*=component]>.herb-overlay-label{background:#fef3c7;border-color:#fcd34d;color:#92400e}[data-herb-debug-outline-type*=erb-output]{transition:all .3s ease}.herb-tooltip{background:#fff;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.12),0 2px 8px rgba(0,0,0,.08);display:flex;flex-direction:column;font-family:SF Mono,Monaco,Inconsolata,Fira Code,monospace;font-size:14px;gap:12px;max-width:calc(100vw - 16px);opacity:0;overflow:visible;padding:16px 20px;pointer-events:none;position:fixed;transition:opacity .2s ease,visibility .2s ease;visibility:hidden;white-space:nowrap;z-index:10001}.herb-tooltip.visible{opacity:1;pointer-events:auto;visibility:visible}.herb-tooltip .herb-location{align-items:center;background:#f8f9fa;border-radius:12px 12px 0 0;color:#6b7280;cursor:pointer;display:flex;font-size:13px;font-weight:500;gap:12px;justify-content:space-between;margin:-16px -20px 0;padding:12px 20px;transition:all .2s ease}.herb-tooltip .herb-location:hover{background:#f1f3f4;color:#374151}.herb-copy-path-btn{background:transparent;border:none;border-radius:4px;color:#6b7280;cursor:pointer;flex-shrink:0;font-size:14px;padding:4px;position:relative;transition:all .2s ease}.herb-copy-path-btn:hover{background:hsla(220,9%,46%,.1);color:#374151}.herb-copy-path-btn:active{transform:scale(.95)}.herb-location:after{background:#1f2937;border-radius:6px;bottom:calc(100% + 8px);color:#fff;content:attr(data-tooltip);font-size:12px;padding:6px 10px;pointer-events:none;white-space:nowrap}.herb-location:after,.herb-location:before{left:50%;opacity:0;position:absolute;transform:translateX(-50%);transition:all .2s ease;visibility:hidden;z-index:10002}.herb-location:before{border:4px solid transparent;border-top-color:#1f2937;bottom:calc(100% + 2px);content:\"\"}.herb-location:hover:after,.herb-location:hover:before{opacity:1;visibility:visible}.herb-location:has(.herb-copy-path-btn:hover):after,.herb-location:has(.herb-copy-path-btn:hover):before{opacity:0!important;visibility:hidden!important}.herb-copy-path-btn:after{background:#1f2937;border-radius:6px;color:#fff;content:attr(data-tooltip);font-size:12px;padding:6px 10px;pointer-events:none;top:-36px;white-space:nowrap}.herb-copy-path-btn:after,.herb-copy-path-btn:before{left:50%;opacity:0;position:absolute;transform:translateX(-50%);transition:all .2s ease;visibility:hidden;z-index:10003}.herb-copy-path-btn:before{border:4px solid transparent;border-bottom-color:#1f2937;content:\"\";top:-6px}.herb-copy-path-btn:hover:after,.herb-copy-path-btn:hover:before{opacity:1;visibility:visible}.herb-tooltip .herb-erb-code{color:#111827;cursor:text;font-size:16px;font-weight:600;letter-spacing:-.025em;user-select:text}.herb-tooltip:before{bottom:-8px;content:\"\";height:8px;left:0;pointer-events:auto;position:absolute;right:0}.herb-tooltip:after{border:6px solid transparent;border-top-color:#e5e7eb;bottom:-6px;content:\"\";left:50%;pointer-events:none;position:absolute;transform:translateX(-50%);z-index:10000}.herb-floating-menu{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;position:fixed;right:0;top:0;z-index:2147483643}.herb-menu-trigger{align-items:center;background:#fff;border:1px solid silver;border-radius:0 0 0 10px;border-right:none;border-top:none;box-shadow:0 1px 3px rgba(0,0,0,.1);cursor:pointer;display:flex;font-size:12px;gap:4px;justify-content:center;padding:4px 7px;position:relative;transition:all .2s ease;z-index:2147483640}.herb-menu-trigger:hover{background:#f9fafb;border-color:#9ca3af;box-shadow:0 4px 12px rgba(0,0,0,.15)}.herb-menu-trigger:active{transform:scale(.98)}.herb-menu-trigger.has-active-options{background:#dbeafe;border-color:#3b82f6}.herb-menu-trigger.has-active-options:hover{background:#bfdbfe;border-color:#2563eb}.herb-menu-trigger.has-active-options .herb-text{color:#1d4ed8}.herb-icon{display:block;font-size:14px;line-height:1}.herb-text{color:#555;font-size:11px;font-weight:600;letter-spacing:.2px}.herb-menu-panel{background:#fff;border:1px solid silver;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);min-width:280px;opacity:0;padding:0;position:absolute;right:10px;top:28px;transform:translateY(-10px) scale(.95);transform-origin:top right;transition:all .3s cubic-bezier(.4,0,.2,1);visibility:hidden}.herb-menu-panel.open{opacity:1;transform:translateY(0) scale(1);visibility:visible}.herb-menu-header{background:#f9fafb;border-bottom:1px solid #e5e7eb;border-radius:8px 8px 0 0;color:#374151;font-size:14px;font-weight:600;padding:16px 20px}.herb-toggle-item{border-bottom:1px solid #f3f4f6;padding:12px 20px}.herb-toggle-item:last-child{border-bottom:none;border-radius:0 0 8px 8px}.herb-nested-toggle{border-left:2px solid #f3f4f6;margin-top:8px;padding-left:24px;transition:all .3s ease}.herb-nested-label{opacity:.8}.herb-nested-label .herb-toggle-text{color:#6b7280;font-size:13px}.herb-nested-switch{background:#e5e7eb;height:20px;width:36px}.herb-nested-switch:after{height:14px;left:3px;top:3px;width:14px}.herb-toggle-input:checked+.herb-nested-switch:after{transform:translateX(16px)}.herb-toggle-label{align-items:center;cursor:pointer;display:flex;gap:12px;user-select:none}.herb-toggle-input{display:none}.herb-toggle-switch{background:#cbd5e1;border-radius:12px;flex-shrink:0;height:24px;position:relative;transition:background .3s ease;width:44px}.herb-toggle-switch:after{background:#fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2);content:\"\";height:18px;left:3px;position:absolute;top:3px;transition:transform .3s ease;width:18px}.herb-toggle-input:checked+.herb-toggle-switch{background:#8b5cf6}.herb-toggle-input:checked+.herb-toggle-switch:after{transform:translateX(20px)}.herb-toggle-text{color:#374151;flex:1;font-size:14px}.herb-toggle-label:hover .herb-toggle-switch{background:#94a3b8}.herb-toggle-label:hover .herb-toggle-input:checked+.herb-toggle-switch{background:#7c3aed}.herb-disable-all-section{background:#f9fafb;border-radius:0 0 8px 8px;border-top:1px solid #f3f4f6;padding:16px 20px}.herb-disable-all-btn{background:#ef4444;border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:13px;font-weight:500;padding:8px 16px;transition:background .2s ease;width:100%}.herb-disable-all-btn:hover{background:#dc2626}.herb-disable-all-btn:active{background:#b91c1c}.herb-validation-overlay{align-items:center;backdrop-filter:blur(4px);background:rgba(0,0,0,.8);bottom:0;color:#e5e5e5;display:flex;font-family:SF Mono,Monaco,Cascadia Code,Roboto Mono,Consolas,Courier New,monospace;justify-content:center;left:0;line-height:1.6;overflow-y:auto;padding:20px;position:fixed;right:0;top:0;z-index:2147483640}.herb-validation-panel{background:#000;border:1px solid #374151;border-radius:12px;box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04);display:flex;flex-direction:column;max-height:80vh;max-width:1200px;overflow:hidden;width:100%}.herb-validation-header{align-items:flex-start;background:linear-gradient(135deg,#dc2626,#b91c1c);border-bottom:1px solid #374151;border-radius:12px 12px 0 0;color:#fff;display:flex;flex-shrink:0;gap:16px;justify-content:space-between;padding:20px 24px}.herb-validation-title{font-size:18px;font-weight:600;margin:0}.herb-validation-close{align-items:center;background:hsla(0,0%,100%,.1);border:1px solid hsla(0,0%,100%,.2);border-radius:6px;color:#fff;cursor:pointer;display:flex;flex-shrink:0;font-size:16px;height:32px;justify-content:center;padding:0;transition:all .2s;width:32px}.herb-validation-close:hover{background:hsla(0,0%,100%,.2);border-color:hsla(0,0%,100%,.3)}.herb-file-tabs{background:#262626;border-bottom:1px solid #374151;display:flex;flex-shrink:0;overflow-x:auto}.herb-file-tab{background:none;border:none;border-bottom:3px solid transparent;color:#9ca3af;cursor:pointer;font-size:14px;font-weight:500;padding:12px 16px;transition:all .2s ease;white-space:nowrap}.herb-file-tab:hover{background:#2d2d2d;color:#e5e5e5}.herb-file-tab.active{background:#374151;border-bottom-color:#3b82f6;color:#fff}.herb-validation-content{flex:1;overflow-y:auto;padding:24px}.herb-validator-section{margin-bottom:32px}.herb-validator-section:last-child{margin-bottom:0}.herb-validator-section.hidden{display:none}.herb-validator-header{align-items:center;background:#262626;border-bottom:1px solid #374151;border-radius:8px 8px 0 0;color:#e5e5e5;display:flex;font-size:16px;font-weight:600;justify-content:space-between;padding:12px 16px}.herb-validator-count{background:hsla(0,0%,100%,.2);border-radius:12px;font-size:14px;font-weight:500;padding:2px 8px}.herb-validator-items{background:#111;border:1px solid #374151;border-radius:0 0 8px 8px;border-top:none}.herb-validation-item{background:#111;border-bottom:1px solid #374151;padding:20px}.herb-validation-item:last-child{border-bottom:none;border-radius:0 0 8px 8px}.herb-validation-item.hidden{display:none}.herb-validation-item .herb-validation-header{align-items:center;background:#1a1a1a;border:none;border-bottom:1px solid #374151;color:#9ca3af;display:flex;font-size:13px;gap:12px;margin:-20px -20px 16px;padding:12px 16px}.herb-validation-badge{border-radius:4px;color:#fff;font-size:12px;font-weight:600;letter-spacing:.025em;padding:4px 8px;text-transform:uppercase}.herb-validation-location{color:#9ca3af;font-family:SF Mono,Monaco,Inconsolata,Fira Code,monospace;font-size:13px}.herb-validation-message{background:#1a1a1a;border-bottom:1px solid #374151;color:#fbbf24;font-size:13px;font-weight:500;line-height:1.4;margin:-16px -16px 16px;padding:12px 16px}.herb-code-snippet{background:#1f2937;border-radius:6px;font-family:SF Mono,Monaco,Inconsolata,Fira Code,monospace;margin-bottom:16px;overflow:hidden}.herb-code-line{align-items:stretch;display:flex}.herb-code-line.herb-error-line{background:rgba(239,68,68,.1)}.herb-validation-overlay .herb-line-number{background:#374151;border-right:1px solid #4b5563;color:#9ca3af;flex-shrink:0;font-size:13px;padding:8px 12px;text-align:right;user-select:none;width:40px}.herb-validation-overlay .herb-error-line .herb-line-number{background:#dc2626;color:#fff}.herb-validation-overlay .herb-line-content{color:#e5e7eb;flex:1;font-size:13px;padding:8px 16px;white-space:pre-wrap}.herb-validation-overlay .herb-error-pointer{background:#1f2937;color:#dc2626;font-size:13px;font-weight:700;padding:4px 16px 8px 57px}.herb-validation-suggestion{align-items:flex-start;background:#111;border:1px solid #374151;border-radius:6px;color:#d1d5db;display:flex;font-size:14px;gap:8px;margin-top:16px;padding:12px 16px}.herb-suggestion-icon{color:#10b981;flex-shrink:0;font-size:16px;margin-top:1px}.herb-erb{color:#fbbf24;font-weight:600}.herb-erb-content{color:#34d399}.herb-tag{color:#60a5fa;font-weight:500}.herb-attr{color:#f472b6}.herb-value{color:#a78bfa}.herb-comment{color:#6b7280;font-style:italic}";
35
+ styleInject(css_248z);
36
+
37
+ class ErrorOverlay {
38
+ constructor() {
39
+ this.overlay = null;
40
+ this.allValidationData = [];
41
+ this.isVisible = false;
42
+ this.init();
43
+ }
44
+ init() {
45
+ this.detectValidationErrors();
46
+ const hasParserErrors = document.querySelector('.herb-parser-error-overlay') !== null;
47
+ if (this.getTotalErrorCount() > 0) {
48
+ this.createOverlay();
49
+ this.setupToggleHandler();
50
+ }
51
+ else if (hasParserErrors) {
52
+ console.log('[ErrorOverlay] Parser error overlay already displayed');
53
+ }
54
+ else {
55
+ console.log('[ErrorOverlay] No errors found, not creating overlay');
56
+ }
57
+ }
58
+ detectValidationErrors() {
59
+ const templatesToRemove = [];
60
+ const validationTemplates = document.querySelectorAll('template[data-herb-validation-error]');
61
+ if (validationTemplates.length > 0) {
62
+ this.processValidationTemplates(validationTemplates, templatesToRemove);
63
+ }
64
+ const jsonTemplates = document.querySelectorAll('template[data-herb-validation-errors]');
65
+ jsonTemplates.forEach((template, _index) => {
66
+ try {
67
+ let jsonData = template.textContent?.trim();
68
+ if (!jsonData) {
69
+ jsonData = template.innerHTML?.trim();
70
+ }
71
+ if (jsonData) {
72
+ const validationData = JSON.parse(jsonData);
73
+ this.allValidationData.push(validationData);
74
+ templatesToRemove.push(template);
75
+ }
76
+ }
77
+ catch (error) {
78
+ console.error('Failed to parse validation errors from template:', error, {
79
+ textContent: template.textContent,
80
+ innerHTML: template.innerHTML
81
+ });
82
+ templatesToRemove.push(template);
83
+ }
84
+ });
85
+ const htmlTemplates = document.querySelectorAll('template[data-herb-parser-error]');
86
+ htmlTemplates.forEach((template, _index) => {
87
+ try {
88
+ let htmlContent = template.innerHTML?.trim() || template.textContent?.trim();
89
+ if (htmlContent) {
90
+ this.displayParserErrorOverlay(htmlContent);
91
+ templatesToRemove.push(template);
92
+ }
93
+ }
94
+ catch (error) {
95
+ console.error('Failed to process parser error template:', error);
96
+ templatesToRemove.push(template);
97
+ }
98
+ });
99
+ templatesToRemove.forEach((template, _index) => template.remove());
100
+ }
101
+ processValidationTemplates(templates, templatesToRemove) {
102
+ const validationFragments = [];
103
+ const errorMap = new Map();
104
+ templates.forEach((template) => {
105
+ try {
106
+ const metadata = {
107
+ severity: template.getAttribute('data-severity') || 'error',
108
+ source: template.getAttribute('data-source') || 'unknown',
109
+ code: template.getAttribute('data-code') || '',
110
+ line: parseInt(template.getAttribute('data-line') || '0'),
111
+ column: parseInt(template.getAttribute('data-column') || '0'),
112
+ filename: template.getAttribute('data-filename') || 'unknown',
113
+ message: template.getAttribute('data-message') || '',
114
+ suggestion: template.getAttribute('data-suggestion') || undefined,
115
+ timestamp: template.getAttribute('data-timestamp') || new Date().toISOString()
116
+ };
117
+ const html = template.innerHTML?.trim() || '';
118
+ if (html) {
119
+ const errorKey = `${metadata.filename}:${metadata.line}:${metadata.column}:${metadata.code}:${metadata.message}`;
120
+ if (errorMap.has(errorKey)) {
121
+ const existing = errorMap.get(errorKey);
122
+ existing.count++;
123
+ }
124
+ else {
125
+ errorMap.set(errorKey, { metadata, html, count: 1 });
126
+ }
127
+ templatesToRemove.push(template);
128
+ }
129
+ }
130
+ catch (error) {
131
+ console.error('Failed to process validation template:', error);
132
+ templatesToRemove.push(template);
133
+ }
134
+ });
135
+ validationFragments.push(...errorMap.values());
136
+ if (validationFragments.length > 0) {
137
+ this.displayValidationOverlay(validationFragments);
138
+ }
139
+ }
140
+ createOverlay() {
141
+ if (this.allValidationData.length === 0)
142
+ return;
143
+ this.overlay = document.createElement('div');
144
+ this.overlay.id = 'herb-error-overlay';
145
+ this.overlay.innerHTML = `
146
+ <style>
147
+ #herb-error-overlay {
148
+ position: fixed;
149
+ top: 0;
150
+ left: 0;
151
+ right: 0;
152
+ bottom: 0;
153
+ background: rgba(0, 0, 0, 0.8);
154
+ z-index: 10000;
155
+ display: none;
156
+ overflow: auto;
157
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
158
+ }
159
+
160
+ .herb-error-content {
161
+ background: #1a1a1a;
162
+ margin: 20px auto;
163
+ padding: 20px;
164
+ border-radius: 8px;
165
+ max-width: 800px;
166
+ color: #fff;
167
+ }
168
+
169
+ .herb-error-header {
170
+ display: flex;
171
+ justify-content: space-between;
172
+ align-items: center;
173
+ margin-bottom: 20px;
174
+ border-bottom: 1px solid #333;
175
+ padding-bottom: 10px;
176
+ }
177
+
178
+ .herb-error-title {
179
+ font-size: 18px;
180
+ font-weight: 600;
181
+ color: #ff6b6b;
182
+ }
183
+
184
+ .herb-error-close {
185
+ background: none;
186
+ border: none;
187
+ color: #fff;
188
+ font-size: 20px;
189
+ cursor: pointer;
190
+ padding: 5px;
191
+ }
192
+
193
+ .herb-error-file-section {
194
+ margin-bottom: 20px;
195
+ }
196
+
197
+ .herb-error-file {
198
+ font-size: 14px;
199
+ color: #888;
200
+ margin-bottom: 10px;
201
+ font-weight: 600;
202
+ }
203
+
204
+ .herb-error-item {
205
+ background: #2a2a2a;
206
+ border-radius: 6px;
207
+ padding: 15px;
208
+ margin-bottom: 10px;
209
+ border-left: 4px solid #ff6b6b;
210
+ }
211
+
212
+ .herb-error-item.warning {
213
+ border-left-color: #ffd93d;
214
+ }
215
+
216
+ .herb-error-item.info {
217
+ border-left-color: #4ecdc4;
218
+ }
219
+
220
+ .herb-error-item.hint {
221
+ border-left-color: #95a5a6;
222
+ }
223
+
224
+ .herb-error-message {
225
+ font-size: 14px;
226
+ margin-bottom: 8px;
227
+ line-height: 1.4;
228
+ }
229
+
230
+ .herb-error-location {
231
+ font-size: 12px;
232
+ color: #888;
233
+ margin-bottom: 8px;
234
+ }
235
+
236
+ .herb-error-suggestion {
237
+ font-size: 12px;
238
+ color: #4ecdc4;
239
+ font-style: italic;
240
+ }
241
+
242
+ .herb-error-source {
243
+ font-size: 11px;
244
+ color: #666;
245
+ text-align: right;
246
+ }
247
+
248
+ .herb-file-separator {
249
+ border-top: 1px solid #444;
250
+ margin: 20px 0;
251
+ }
252
+ </style>
253
+
254
+ <div class="herb-error-content">
255
+ <div class="herb-error-header">
256
+ <div class="herb-error-title">
257
+ Errors (${this.getTotalErrorCount()})
258
+ </div>
259
+ <button class="herb-error-close">&times;</button>
260
+ </div>
261
+
262
+ <div class="herb-error-files">
263
+ ${this.allValidationData.map((validationData, index) => `
264
+ ${index > 0 ? '<div class="herb-file-separator"></div>' : ''}
265
+ <div class="herb-error-file-section">
266
+ <div class="herb-error-file">${validationData.filename} (${this.getErrorSummary(validationData.validationErrors)})</div>
267
+ <div class="herb-error-list">
268
+ ${validationData.validationErrors.map(error => `
269
+ <div class="herb-error-item ${error.severity}">
270
+ <div class="herb-error-message">${this.escapeHtml(error.message)}</div>
271
+ ${error.location ? `<div class="herb-error-location">Line ${error.location.line}, Column ${error.location.column}</div>` : ''}
272
+ ${error.suggestion ? `<div class="herb-error-suggestion">💡 ${this.escapeHtml(error.suggestion)}</div>` : ''}
273
+ <div class="herb-error-source">${error.source}${error.code ? ` (${error.code})` : ''}</div>
274
+ </div>
275
+ `).join('')}
276
+ </div>
277
+ </div>
278
+ `).join('')}
279
+ </div>
280
+ </div>
281
+ `;
282
+ document.body.appendChild(this.overlay);
283
+ const closeBtn = this.overlay.querySelector('.herb-error-close');
284
+ closeBtn?.addEventListener('click', () => this.hide());
285
+ this.overlay.addEventListener('click', (e) => {
286
+ if (e.target === this.overlay) {
287
+ this.hide();
288
+ }
289
+ });
290
+ document.addEventListener('keydown', (e) => {
291
+ if (e.key === 'Escape' && this.isVisible) {
292
+ this.hide();
293
+ }
294
+ });
295
+ }
296
+ setupToggleHandler() {
297
+ document.addEventListener('keydown', (e) => {
298
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'E') {
299
+ e.preventDefault();
300
+ this.toggle();
301
+ }
302
+ });
303
+ if (this.hasErrorSeverity()) {
304
+ setTimeout(() => this.show(), 100);
305
+ }
306
+ }
307
+ getTotalErrorCount() {
308
+ return this.allValidationData.reduce((total, data) => total + data.validationErrors.length, 0);
309
+ }
310
+ getErrorSummary(errors) {
311
+ if (errors.length === 1) {
312
+ return '1 error';
313
+ }
314
+ const errorsBySource = errors.reduce((acc, error) => {
315
+ const source = error.source || 'Unknown';
316
+ acc[source] = (acc[source] || 0) + 1;
317
+ return acc;
318
+ }, {});
319
+ const sourceKeys = Object.keys(errorsBySource);
320
+ if (sourceKeys.length === 1) {
321
+ const source = sourceKeys[0];
322
+ const count = errorsBySource[source];
323
+ const sourceLabel = this.getSourceLabel(source);
324
+ return `${count} ${sourceLabel} error${count === 1 ? '' : 's'}`;
325
+ }
326
+ else {
327
+ const parts = sourceKeys.map(source => {
328
+ const count = errorsBySource[source];
329
+ const sourceLabel = this.getSourceLabel(source);
330
+ return `${count} ${sourceLabel}`;
331
+ });
332
+ return `${errors.length} errors (${parts.join(', ')})`;
333
+ }
334
+ }
335
+ getSourceLabel(source) {
336
+ switch (source) {
337
+ case 'Parser': return 'parser';
338
+ case 'SecurityValidator': return 'security';
339
+ case 'NestingValidator': return 'nesting';
340
+ case 'AccessibilityValidator': return 'accessibility';
341
+ default: return 'validation';
342
+ }
343
+ }
344
+ hasErrorSeverity() {
345
+ return this.allValidationData.some(data => data.validationErrors.some(error => error.severity === 'error'));
346
+ }
347
+ escapeHtml(unsafe) {
348
+ return unsafe
349
+ .replace(/&/g, '&amp;')
350
+ .replace(/</g, '&lt;')
351
+ .replace(/>/g, '&gt;')
352
+ .replace(/"/g, '&quot;')
353
+ .replace(/'/g, '&#039;');
354
+ }
355
+ show() {
356
+ if (this.overlay) {
357
+ this.overlay.style.display = 'block';
358
+ this.isVisible = true;
359
+ }
360
+ }
361
+ hide() {
362
+ if (this.overlay) {
363
+ this.overlay.style.display = 'none';
364
+ this.isVisible = false;
365
+ }
366
+ }
367
+ toggle() {
368
+ if (this.isVisible) {
369
+ this.hide();
370
+ }
371
+ else {
372
+ this.show();
373
+ }
374
+ }
375
+ hasErrors() {
376
+ return this.getTotalErrorCount() > 0;
377
+ }
378
+ getErrorCount() {
379
+ return this.getTotalErrorCount();
380
+ }
381
+ displayParserErrorOverlay(htmlContent) {
382
+ const existingOverlay = document.querySelector('.herb-parser-error-overlay');
383
+ if (existingOverlay) {
384
+ existingOverlay.remove();
385
+ }
386
+ const container = document.createElement('div');
387
+ container.innerHTML = htmlContent;
388
+ const overlay = container.querySelector('.herb-parser-error-overlay');
389
+ if (overlay) {
390
+ document.body.appendChild(overlay);
391
+ overlay.style.display = 'flex';
392
+ }
393
+ else {
394
+ console.error('[ErrorOverlay] No parser error overlay found in HTML template');
395
+ }
396
+ }
397
+ displayValidationOverlay(fragments) {
398
+ const existingOverlay = document.querySelector('.herb-validation-overlay');
399
+ if (existingOverlay) {
400
+ existingOverlay.remove();
401
+ }
402
+ const errorsBySource = new Map();
403
+ const errorsByFile = new Map();
404
+ fragments.forEach(fragment => {
405
+ const source = fragment.metadata.source;
406
+ if (!errorsBySource.has(source)) {
407
+ errorsBySource.set(source, []);
408
+ }
409
+ errorsBySource.get(source).push(fragment);
410
+ const file = fragment.metadata.filename;
411
+ if (!errorsByFile.has(file)) {
412
+ errorsByFile.set(file, []);
413
+ }
414
+ errorsByFile.get(file).push(fragment);
415
+ });
416
+ const errorCount = fragments.filter(f => f.metadata.severity === 'error').reduce((sum, f) => sum + f.count, 0);
417
+ const warningCount = fragments.filter(f => f.metadata.severity === 'warning').reduce((sum, f) => sum + f.count, 0);
418
+ const totalCount = fragments.reduce((sum, f) => sum + f.count, 0);
419
+ const uniqueCount = fragments.length;
420
+ const overlayHTML = this.buildValidationOverlayHTML(fragments, errorsBySource, errorsByFile, { errorCount, warningCount, totalCount, uniqueCount });
421
+ const overlay = document.createElement('div');
422
+ overlay.className = 'herb-validation-overlay';
423
+ overlay.innerHTML = overlayHTML;
424
+ document.body.appendChild(overlay);
425
+ this.setupValidationOverlayHandlers(overlay);
426
+ }
427
+ buildValidationOverlayHTML(_fragments, errorsBySource, errorsByFile, counts) {
428
+ let title = counts.uniqueCount === 1 ? 'Validation Issue' : `Validation Issues`;
429
+ if (counts.totalCount !== counts.uniqueCount) {
430
+ title += ` (${counts.uniqueCount} unique, ${counts.totalCount} total)`;
431
+ }
432
+ else {
433
+ title += ` (${counts.totalCount})`;
434
+ }
435
+ const subtitle = [];
436
+ if (counts.errorCount > 0)
437
+ subtitle.push(`${counts.errorCount} error${counts.errorCount !== 1 ? 's' : ''}`);
438
+ if (counts.warningCount > 0)
439
+ subtitle.push(`${counts.warningCount} warning${counts.warningCount !== 1 ? 's' : ''}`);
440
+ let fileTabs = '';
441
+ if (errorsByFile.size > 1) {
442
+ const totalErrors = Array.from(errorsByFile.values()).reduce((sum, errors) => sum + errors.length, 0);
443
+ fileTabs = `
444
+ <div class="herb-file-tabs">
445
+ <button class="herb-file-tab active" data-file="*">
446
+ All (${totalErrors})
447
+ </button>
448
+ ${Array.from(errorsByFile.entries()).map(([file, errors]) => `
449
+ <button class="herb-file-tab" data-file="${this.escapeAttr(file)}">
450
+ ${this.escapeHtml(file)} (${errors.length})
451
+ </button>
452
+ `).join('')}
453
+ </div>
454
+ `;
455
+ }
456
+ const contentSections = Array.from(errorsBySource.entries()).map(([source, sourceFragments]) => `
457
+ <div class="herb-validator-section" data-source="${this.escapeAttr(source)}">
458
+ <div class="herb-validator-header">
459
+ <h3>${this.escapeHtml(source.replace('Validator', ''))} Issues (${sourceFragments.length})</h3>
460
+ </div>
461
+ <div class="herb-validator-content">
462
+ ${sourceFragments.map(f => {
463
+ const fileAttribute = `data-error-file="${this.escapeAttr(f.metadata.filename)}"`;
464
+ if (f.count > 1) {
465
+ return `
466
+ <div class="herb-validation-item-wrapper" ${fileAttribute}>
467
+ ${f.html}
468
+ <div class="herb-occurrence-badge" title="This error occurs ${f.count} times in the template">
469
+ <span class="herb-occurrence-icon">⚠</span>
470
+ <span class="herb-occurrence-count">×${f.count}</span>
471
+ </div>
472
+ </div>
473
+ `;
474
+ }
475
+ return `<div class="herb-validation-error-container" ${fileAttribute}>${f.html}</div>`;
476
+ }).join('')}
477
+ </div>
478
+ </div>
479
+ `).join('');
480
+ return `
481
+ <style>${this.getValidationOverlayStyles()}</style>
482
+ <div class="herb-validation-container">
483
+ <div class="herb-validation-header">
484
+ <div class="herb-validation-header-content">
485
+ <div class="herb-validation-title">
486
+ <span class="herb-validation-icon">⚠️</span>
487
+ ${title}
488
+ </div>
489
+ <div class="herb-validation-subtitle">${subtitle.join(', ')}</div>
490
+ </div>
491
+ <button class="herb-close-button" title="Close (Esc)">×</button>
492
+ </div>
493
+ ${fileTabs}
494
+ <div class="herb-validation-content">
495
+ ${contentSections}
496
+ </div>
497
+ <div class="herb-dismiss-hint" style="padding-left: 24px; padding-right: 24px; padding-bottom: 12px;">
498
+ Click outside, press <kbd style="display: inline-block; padding: 2px 6px; font-family: monospace; font-size: 0.9em; color: #333; background: #f7f7f7; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 2px 0 #ccc, 0 2px 3px rgba(0,0,0,0.2) inset;">Esc</kbd> key, or fix the code to dismiss.<br>
499
+
500
+ You can also disable this overlay by passing <code style="color: #ffeb3b; font-family: monospace; font-size: 12pt;">validation_mode: :none</code> to <code style="color: #ffeb3b; font-family: monospace; font-size: 12pt;">Herb::Engine</code>.
501
+ </div>
502
+ </div>
503
+ `;
504
+ }
505
+ getValidationOverlayStyles() {
506
+ return `
507
+ .herb-validation-overlay {
508
+ position: fixed;
509
+ top: 0;
510
+ left: 0;
511
+ right: 0;
512
+ bottom: 0;
513
+ background: rgba(0, 0, 0, 0.8);
514
+ backdrop-filter: blur(4px);
515
+ z-index: 9999;
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ padding: 20px;
520
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
521
+ color: #e5e5e5;
522
+ line-height: 1.6;
523
+ }
524
+
525
+ .herb-validation-container {
526
+ background: #000000;
527
+ border: 1px solid #374151;
528
+ border-radius: 12px;
529
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
530
+ max-width: 1200px;
531
+ max-height: 80vh;
532
+ width: 100%;
533
+ display: flex;
534
+ flex-direction: column;
535
+ overflow: hidden;
536
+ }
537
+
538
+ .herb-validation-header {
539
+ background: linear-gradient(135deg, #f59e0b, #d97706);
540
+ padding: 20px 24px;
541
+ border-bottom: 1px solid #374151;
542
+ display: flex;
543
+ justify-content: space-between;
544
+ align-items: flex-start;
545
+ }
546
+
547
+ .herb-validation-title {
548
+ font-size: 18px;
549
+ font-weight: 600;
550
+ color: white;
551
+ display: flex;
552
+ align-items: center;
553
+ gap: 12px;
554
+ }
555
+
556
+ .herb-validation-subtitle {
557
+ font-size: 14px;
558
+ color: rgba(255, 255, 255, 0.9);
559
+ margin-top: 4px;
560
+ }
561
+
562
+ .herb-file-tabs {
563
+ background: #1a1a1a;
564
+ border-bottom: 1px solid #374151;
565
+ padding: 0;
566
+ display: flex;
567
+ overflow-x: auto;
568
+ }
569
+
570
+ .herb-file-tab {
571
+ background: transparent;
572
+ border: none;
573
+ color: #9ca3af;
574
+ padding: 12px 20px;
575
+ cursor: pointer;
576
+ font-size: 13px;
577
+ white-space: nowrap;
578
+ transition: all 0.2s;
579
+ border-bottom: 2px solid transparent;
580
+ }
581
+
582
+ .herb-file-tab:hover {
583
+ background: #262626;
584
+ color: #e5e5e5;
585
+ }
586
+
587
+ .herb-file-tab.active {
588
+ color: #f59e0b;
589
+ border-bottom-color: #f59e0b;
590
+ background: #262626;
591
+ }
592
+
593
+ .herb-validation-content {
594
+ flex: 1;
595
+ overflow-y: auto;
596
+ padding: 24px;
597
+ display: flex;
598
+ flex-direction: column;
599
+ gap: 24px;
600
+ }
601
+
602
+ .herb-validator-section {
603
+ background: #0f0f0f;
604
+ border: 1px solid #2d2d2d;
605
+ border-radius: 8px;
606
+ }
607
+
608
+ .herb-validator-header {
609
+ background: #1a1a1a;
610
+ padding: 12px 16px;
611
+ border-bottom: 1px solid #2d2d2d;
612
+ }
613
+
614
+ .herb-validator-header h3 {
615
+ margin: 0;
616
+ font-size: 14px;
617
+ font-weight: 500;
618
+ color: #e5e5e5;
619
+ }
620
+
621
+ .herb-validator-content {
622
+ padding: 16px;
623
+ display: flex;
624
+ flex-direction: column;
625
+ gap: 16px;
626
+ }
627
+
628
+ .herb-validation-item {
629
+ border-left: 3px solid #4a4a4a;
630
+ padding-left: 16px;
631
+ }
632
+
633
+ .herb-validation-item[data-severity="error"] {
634
+ border-left-color: #7f1d1d;
635
+ }
636
+
637
+ .herb-validation-item[data-severity="warning"] {
638
+ border-left-color: #78350f;
639
+ }
640
+
641
+ .herb-validation-item[data-severity="info"] {
642
+ border-left-color: #1e3a8a;
643
+ }
644
+
645
+ .herb-validation-header {
646
+ display: flex;
647
+ align-items: center;
648
+ gap: 8px;
649
+ margin-bottom: 8px;
650
+ }
651
+
652
+ .herb-validation-badge {
653
+ padding: 2px 8px;
654
+ border-radius: 4px;
655
+ font-size: 11px;
656
+ font-weight: 600;
657
+ color: white;
658
+ text-transform: uppercase;
659
+ }
660
+
661
+ .herb-validation-location {
662
+ font-size: 12px;
663
+ color: #9ca3af;
664
+ }
665
+
666
+ .herb-validation-message {
667
+ font-size: 14px;
668
+ margin-bottom: 12px;
669
+ line-height: 1.5;
670
+ }
671
+
672
+ .herb-code-snippet {
673
+ background: #1a1a1a;
674
+ border: 1px solid #2d2d2d;
675
+ border-radius: 4px;
676
+ padding: 12px;
677
+ overflow-x: auto;
678
+ }
679
+
680
+ .herb-code-line {
681
+ display: flex;
682
+ align-items: flex-start;
683
+ min-height: 20px;
684
+ font-size: 13px;
685
+ line-height: 1.5;
686
+ }
687
+
688
+ .herb-line-number {
689
+ color: #6b7280;
690
+ width: 40px;
691
+ text-align: right;
692
+ padding-right: 16px;
693
+ user-select: none;
694
+ flex-shrink: 0;
695
+ }
696
+
697
+ .herb-line-content {
698
+ flex: 1;
699
+ white-space: pre;
700
+ font-family: inherit;
701
+ }
702
+
703
+ .herb-error-line {
704
+ background: rgba(220, 38, 38, 0.1);
705
+ }
706
+
707
+ .herb-error-line .herb-line-number {
708
+ color: #dc2626;
709
+ font-weight: 600;
710
+ }
711
+
712
+ .herb-error-pointer {
713
+ color: #dc2626;
714
+ font-weight: bold;
715
+ margin-left: 56px;
716
+ font-size: 12px;
717
+ }
718
+
719
+ .herb-validation-suggestion {
720
+ background: #1e3a1e;
721
+ border: 1px solid #10b981;
722
+ border-radius: 4px;
723
+ padding: 8px 12px;
724
+ margin-top: 8px;
725
+ font-size: 13px;
726
+ color: #10b981;
727
+ display: flex;
728
+ align-items: center;
729
+ gap: 8px;
730
+ }
731
+
732
+ .herb-validation-item-wrapper,
733
+ .herb-validation-error-container {
734
+ position: relative;
735
+ }
736
+
737
+ .herb-validation-error-container.hidden,
738
+ .herb-validation-item-wrapper.hidden,
739
+ .herb-validator-section.hidden {
740
+ display: none;
741
+ }
742
+
743
+ .herb-occurrence-badge {
744
+ position: absolute;
745
+ top: 8px;
746
+ right: 8px;
747
+ background: #dc2626;
748
+ color: white;
749
+ padding: 4px 8px;
750
+ border-radius: 12px;
751
+ font-size: 12px;
752
+ font-weight: 600;
753
+ display: flex;
754
+ align-items: center;
755
+ gap: 4px;
756
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
757
+ }
758
+
759
+ .herb-occurrence-icon {
760
+ font-size: 10px;
761
+ }
762
+
763
+ .herb-occurrence-count {
764
+ font-weight: bold;
765
+ }
766
+
767
+ .herb-close-button {
768
+ background: rgba(255, 255, 255, 0.1);
769
+ border: 1px solid rgba(255, 255, 255, 0.2);
770
+ color: white;
771
+ width: 32px;
772
+ height: 32px;
773
+ border-radius: 6px;
774
+ display: flex;
775
+ align-items: center;
776
+ justify-content: center;
777
+ cursor: pointer;
778
+ font-size: 16px;
779
+ transition: all 0.2s;
780
+ }
781
+
782
+ .herb-close-button:hover {
783
+ background: rgba(255, 255, 255, 0.2);
784
+ border-color: rgba(255, 255, 255, 0.3);
785
+ }
786
+
787
+ /* Syntax highlighting */
788
+ .herb-erb { color: #61dafb; }
789
+ .herb-erb-content { color: #c678dd; }
790
+ .herb-tag { color: #e06c75; }
791
+ .herb-attr { color: #d19a66; }
792
+ .herb-value { color: #98c379; }
793
+ .herb-comment { color: #5c6370; font-style: italic; }
794
+ `;
795
+ }
796
+ setupValidationOverlayHandlers(overlay) {
797
+ const closeBtn = overlay.querySelector('.herb-close-button');
798
+ if (closeBtn) {
799
+ closeBtn.addEventListener('click', () => overlay.remove());
800
+ }
801
+ overlay.addEventListener('click', (e) => {
802
+ if (e.target === overlay) {
803
+ overlay.remove();
804
+ }
805
+ });
806
+ const escHandler = (e) => {
807
+ if (e.key === 'Escape') {
808
+ overlay.remove();
809
+ document.removeEventListener('keydown', escHandler);
810
+ }
811
+ };
812
+ document.addEventListener('keydown', escHandler);
813
+ const fileTabs = overlay.querySelectorAll('.herb-file-tab');
814
+ fileTabs.forEach(tab => {
815
+ tab.addEventListener('click', () => {
816
+ const selectedFile = tab.getAttribute('data-file');
817
+ fileTabs.forEach(t => t.classList.remove('active'));
818
+ tab.classList.add('active');
819
+ const errorContainers = overlay.querySelectorAll('[data-error-file]');
820
+ const validatorSections = overlay.querySelectorAll('.herb-validator-section');
821
+ errorContainers.forEach(container => {
822
+ const containerFile = container.getAttribute('data-error-file');
823
+ if (selectedFile === '*' || containerFile === selectedFile) {
824
+ container.classList.remove('hidden');
825
+ }
826
+ else {
827
+ container.classList.add('hidden');
828
+ }
829
+ });
830
+ validatorSections.forEach(section => {
831
+ const sectionContent = section.querySelector('.herb-validator-content');
832
+ const visibleErrors = sectionContent?.querySelectorAll('[data-error-file]:not(.hidden)').length || 0;
833
+ const header = section.querySelector('h3');
834
+ const source = section.getAttribute('data-source')?.replace('Validator', '') || 'Unknown';
835
+ if (header) {
836
+ header.textContent = `${source} Issues (${visibleErrors})`;
837
+ }
838
+ if (visibleErrors === 0) {
839
+ section.classList.add('hidden');
840
+ }
841
+ else {
842
+ section.classList.remove('hidden');
843
+ }
844
+ });
845
+ });
846
+ });
847
+ }
848
+ escapeAttr(text) {
849
+ return this.escapeHtml(text).replace(/"/g, '&quot;');
850
+ }
851
+ }
852
+
853
+ class HerbOverlay {
854
+ constructor(options = {}) {
855
+ this.options = options;
856
+ this.showingERB = false;
857
+ this.showingERBOutlines = false;
858
+ this.showingERBHoverReveal = false;
859
+ this.showingTooltips = true;
860
+ this.showingViewOutlines = false;
861
+ this.showingPartialOutlines = false;
862
+ this.showingComponentOutlines = false;
863
+ this.menuOpen = false;
864
+ this.projectPath = '';
865
+ this.currentlyHoveredERBElement = null;
866
+ this.errorOverlay = null;
867
+ this.handleRevealedERBClick = (event) => {
868
+ event.stopPropagation();
869
+ event.preventDefault();
870
+ const element = event.currentTarget;
871
+ if (!element)
872
+ return;
873
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path');
874
+ const line = element.getAttribute('data-herb-debug-line');
875
+ const column = element.getAttribute('data-herb-debug-column');
876
+ if (fullPath) {
877
+ this.openFileInEditor(fullPath, line ? parseInt(line) : 1, column ? parseInt(column) : 1);
878
+ }
879
+ };
880
+ if (options.autoInit !== false) {
881
+ this.init();
882
+ }
883
+ }
884
+ init() {
885
+ this.loadProjectPath();
886
+ this.loadSettings();
887
+ this.injectMenu();
888
+ this.setupMenuToggle();
889
+ this.setupToggleSwitches();
890
+ this.initializeErrorOverlay();
891
+ this.setupTurboListeners();
892
+ this.applySettings();
893
+ }
894
+ loadProjectPath() {
895
+ if (this.options.projectPath) {
896
+ this.projectPath = this.options.projectPath;
897
+ return;
898
+ }
899
+ const metaTag = document.querySelector('meta[name="herb-project-path"]');
900
+ if (metaTag?.content) {
901
+ this.projectPath = metaTag.content;
902
+ }
903
+ }
904
+ loadSettings() {
905
+ const savedSettings = localStorage.getItem(HerbOverlay.SETTINGS_KEY);
906
+ if (savedSettings) {
907
+ try {
908
+ const settings = JSON.parse(savedSettings);
909
+ this.showingERB = settings.showingERB || false;
910
+ this.showingERBOutlines = settings.showingERBOutlines || false;
911
+ this.showingERBHoverReveal = settings.showingERBHoverReveal || false;
912
+ this.showingTooltips = settings.showingTooltips !== undefined ? settings.showingTooltips : true;
913
+ this.showingViewOutlines = settings.showingViewOutlines || false;
914
+ this.showingPartialOutlines = settings.showingPartialOutlines || false;
915
+ this.showingComponentOutlines = settings.showingComponentOutlines || false;
916
+ this.menuOpen = settings.menuOpen || false;
917
+ }
918
+ catch (e) {
919
+ console.warn('Failed to load Herb dev tools settings:', e);
920
+ }
921
+ }
922
+ }
923
+ saveSettings() {
924
+ const settings = {
925
+ showingERB: this.showingERB,
926
+ showingERBOutlines: this.showingERBOutlines,
927
+ showingERBHoverReveal: this.showingERBHoverReveal,
928
+ showingTooltips: this.showingTooltips,
929
+ showingViewOutlines: this.showingViewOutlines,
930
+ showingPartialOutlines: this.showingPartialOutlines,
931
+ showingComponentOutlines: this.showingComponentOutlines,
932
+ menuOpen: this.menuOpen
933
+ };
934
+ localStorage.setItem(HerbOverlay.SETTINGS_KEY, JSON.stringify(settings));
935
+ this.updateMenuButtonState();
936
+ }
937
+ updateMenuButtonState() {
938
+ const menuTrigger = document.getElementById('herbMenuTrigger');
939
+ if (menuTrigger) {
940
+ const hasActiveOptions = this.showingERB || this.showingERBOutlines || this.showingViewOutlines || this.showingPartialOutlines || this.showingComponentOutlines;
941
+ if (hasActiveOptions) {
942
+ menuTrigger.classList.add('has-active-options');
943
+ }
944
+ else {
945
+ menuTrigger.classList.remove('has-active-options');
946
+ }
947
+ }
948
+ }
949
+ injectMenu() {
950
+ const existingMenu = document.querySelector('.herb-floating-menu');
951
+ if (existingMenu) {
952
+ return;
953
+ }
954
+ const menuHTML = `
955
+ <div class="herb-floating-menu">
956
+ <button class="herb-menu-trigger" id="herbMenuTrigger">
957
+ <span class="herb-icon">🌿</span>
958
+ <span class="herb-text">Herb</span>
959
+ </button>
960
+
961
+ <div class="herb-menu-panel" id="herbMenuPanel">
962
+ <div class="herb-menu-header">Herb Debug Tools</div>
963
+
964
+ <div class="herb-toggle-item">
965
+ <label class="herb-toggle-label">
966
+ <input type="checkbox" id="herbToggleViewOutlines" class="herb-toggle-input">
967
+ <span class="herb-toggle-switch"></span>
968
+ <span class="herb-toggle-text">Show View Outlines</span>
969
+ </label>
970
+ </div>
971
+
972
+ <div class="herb-toggle-item">
973
+ <label class="herb-toggle-label">
974
+ <input type="checkbox" id="herbTogglePartialOutlines" class="herb-toggle-input">
975
+ <span class="herb-toggle-switch"></span>
976
+ <span class="herb-toggle-text">Show Partial Outlines</span>
977
+ </label>
978
+ </div>
979
+
980
+ <div class="herb-toggle-item">
981
+ <label class="herb-toggle-label">
982
+ <input type="checkbox" id="herbToggleComponentOutlines" class="herb-toggle-input">
983
+ <span class="herb-toggle-switch"></span>
984
+ <span class="herb-toggle-text">Show Component Outlines</span>
985
+ </label>
986
+ </div>
987
+
988
+ <div class="herb-toggle-item">
989
+ <label class="herb-toggle-label">
990
+ <input type="checkbox" id="herbToggleERBOutlines" class="herb-toggle-input">
991
+ <span class="herb-toggle-switch"></span>
992
+ <span class="herb-toggle-text">Show ERB Output Outlines</span>
993
+ </label>
994
+
995
+ <div class="herb-nested-toggle" id="herbERBHoverRevealNested" style="display: none;">
996
+ <label class="herb-toggle-label herb-nested-label">
997
+ <input type="checkbox" id="herbToggleERBHoverReveal" class="herb-toggle-input">
998
+ <span class="herb-toggle-switch herb-nested-switch"></span>
999
+ <span class="herb-toggle-text">Reveal ERB Output tag on hover</span>
1000
+ </label>
1001
+ </div>
1002
+
1003
+ <div class="herb-nested-toggle" id="herbTooltipsNested" style="display: none;">
1004
+ <label class="herb-toggle-label herb-nested-label">
1005
+ <input type="checkbox" id="herbToggleTooltips" class="herb-toggle-input">
1006
+ <span class="herb-toggle-switch herb-nested-switch"></span>
1007
+ <span class="herb-toggle-text">Show Tooltips</span>
1008
+ </label>
1009
+ </div>
1010
+ </div>
1011
+
1012
+ <div class="herb-toggle-item">
1013
+ <label class="herb-toggle-label">
1014
+ <input type="checkbox" id="herbToggleERB" class="herb-toggle-input">
1015
+ <span class="herb-toggle-switch"></span>
1016
+ <span class="herb-toggle-text">Show ERB Output Tags</span>
1017
+ </label>
1018
+ </div>
1019
+
1020
+ <div class="herb-disable-all-section">
1021
+ <button id="herbDisableAll" class="herb-disable-all-btn">Disable All</button>
1022
+ </div>
1023
+ </div>
1024
+ </div>
1025
+ `;
1026
+ document.body.insertAdjacentHTML('beforeend', menuHTML);
1027
+ }
1028
+ applySettings() {
1029
+ this.toggleViewOutlines(this.showingViewOutlines);
1030
+ this.togglePartialOutlines(this.showingPartialOutlines);
1031
+ this.toggleComponentOutlines(this.showingComponentOutlines);
1032
+ this.toggleERBTags(this.showingERB);
1033
+ this.toggleERBOutlines(this.showingERBOutlines);
1034
+ const menuTrigger = document.getElementById('herbMenuTrigger');
1035
+ const menuPanel = document.getElementById('herbMenuPanel');
1036
+ if (menuTrigger && menuPanel && this.menuOpen) {
1037
+ menuTrigger.classList.add('active');
1038
+ menuPanel.classList.add('open');
1039
+ }
1040
+ }
1041
+ setupMenuToggle() {
1042
+ const menuTrigger = document.getElementById('herbMenuTrigger');
1043
+ const menuPanel = document.getElementById('herbMenuPanel');
1044
+ if (menuTrigger && menuPanel) {
1045
+ menuTrigger.addEventListener('click', () => {
1046
+ this.menuOpen = !this.menuOpen;
1047
+ if (this.menuOpen) {
1048
+ menuTrigger.classList.add('active');
1049
+ menuPanel.classList.add('open');
1050
+ }
1051
+ else {
1052
+ menuTrigger.classList.remove('active');
1053
+ menuPanel.classList.remove('open');
1054
+ }
1055
+ this.saveSettings();
1056
+ });
1057
+ document.addEventListener('click', (e) => {
1058
+ const target = e.target;
1059
+ const floatingMenu = document.querySelector('.herb-floating-menu');
1060
+ if (floatingMenu && !floatingMenu.contains(target) && this.menuOpen) {
1061
+ this.menuOpen = false;
1062
+ menuTrigger.classList.remove('active');
1063
+ menuPanel.classList.remove('open');
1064
+ this.saveSettings();
1065
+ }
1066
+ });
1067
+ }
1068
+ }
1069
+ setupTurboListeners() {
1070
+ document.addEventListener('turbo:load', () => {
1071
+ this.reinitializeAfterNavigation();
1072
+ });
1073
+ document.addEventListener('turbo:render', () => {
1074
+ this.reinitializeAfterNavigation();
1075
+ });
1076
+ document.addEventListener('turbo:visit', () => {
1077
+ this.reinitializeAfterNavigation();
1078
+ });
1079
+ }
1080
+ reinitializeAfterNavigation() {
1081
+ this.injectMenu();
1082
+ this.setupMenuToggle();
1083
+ this.setupToggleSwitches();
1084
+ this.applySettings();
1085
+ this.updateMenuButtonState();
1086
+ }
1087
+ setupToggleSwitches() {
1088
+ const toggleViewOutlinesSwitch = document.getElementById('herbToggleViewOutlines');
1089
+ if (toggleViewOutlinesSwitch) {
1090
+ toggleViewOutlinesSwitch.checked = this.showingViewOutlines;
1091
+ toggleViewOutlinesSwitch.addEventListener('change', () => {
1092
+ this.toggleViewOutlines(toggleViewOutlinesSwitch.checked);
1093
+ });
1094
+ }
1095
+ const togglePartialOutlinesSwitch = document.getElementById('herbTogglePartialOutlines');
1096
+ if (togglePartialOutlinesSwitch) {
1097
+ togglePartialOutlinesSwitch.checked = this.showingPartialOutlines;
1098
+ togglePartialOutlinesSwitch.addEventListener('change', () => {
1099
+ this.togglePartialOutlines(togglePartialOutlinesSwitch.checked);
1100
+ });
1101
+ }
1102
+ const toggleComponentOutlinesSwitch = document.getElementById('herbToggleComponentOutlines');
1103
+ if (toggleComponentOutlinesSwitch) {
1104
+ toggleComponentOutlinesSwitch.checked = this.showingComponentOutlines;
1105
+ toggleComponentOutlinesSwitch.addEventListener('change', () => {
1106
+ this.toggleComponentOutlines(toggleComponentOutlinesSwitch.checked);
1107
+ });
1108
+ }
1109
+ const toggleERBSwitch = document.getElementById('herbToggleERB');
1110
+ const toggleERBOutlinesSwitch = document.getElementById('herbToggleERBOutlines');
1111
+ if (toggleERBSwitch) {
1112
+ toggleERBSwitch.checked = this.showingERB;
1113
+ toggleERBSwitch.addEventListener('change', () => {
1114
+ if (toggleERBSwitch.checked && toggleERBOutlinesSwitch) {
1115
+ toggleERBOutlinesSwitch.checked = false;
1116
+ this.toggleERBOutlines(false);
1117
+ }
1118
+ this.toggleERBTags(toggleERBSwitch.checked);
1119
+ });
1120
+ }
1121
+ if (toggleERBOutlinesSwitch) {
1122
+ toggleERBOutlinesSwitch.checked = this.showingERBOutlines;
1123
+ toggleERBOutlinesSwitch.addEventListener('change', () => {
1124
+ if (toggleERBOutlinesSwitch.checked && toggleERBSwitch) {
1125
+ toggleERBSwitch.checked = false;
1126
+ this.toggleERBTags(false);
1127
+ }
1128
+ this.toggleERBOutlines(toggleERBOutlinesSwitch.checked);
1129
+ this.updateNestedToggleVisibility();
1130
+ });
1131
+ }
1132
+ else {
1133
+ console.warn('ERB outlines toggle switch not found');
1134
+ }
1135
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
1136
+ if (toggleERBHoverRevealSwitch) {
1137
+ toggleERBHoverRevealSwitch.checked = this.showingERBHoverReveal;
1138
+ toggleERBHoverRevealSwitch.addEventListener('change', () => {
1139
+ this.toggleERBHoverReveal(toggleERBHoverRevealSwitch.checked);
1140
+ });
1141
+ }
1142
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
1143
+ if (toggleTooltipsSwitch) {
1144
+ toggleTooltipsSwitch.checked = this.showingTooltips;
1145
+ toggleTooltipsSwitch.addEventListener('change', () => {
1146
+ this.toggleTooltips(toggleTooltipsSwitch.checked);
1147
+ });
1148
+ }
1149
+ this.updateNestedToggleVisibility();
1150
+ const disableAllBtn = document.getElementById('herbDisableAll');
1151
+ if (disableAllBtn) {
1152
+ disableAllBtn.addEventListener('click', () => {
1153
+ this.disableAll();
1154
+ });
1155
+ }
1156
+ }
1157
+ toggleViewOutlines(show) {
1158
+ this.showingViewOutlines = show !== undefined ? show : !this.showingViewOutlines;
1159
+ const viewOutlines = document.querySelectorAll('[data-herb-debug-outline-type="view"], [data-herb-debug-outline-type*="view"]');
1160
+ viewOutlines.forEach((outline) => {
1161
+ const element = outline;
1162
+ if (this.showingViewOutlines) {
1163
+ element.style.outline = '2px dotted #3b82f6';
1164
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
1165
+ element.classList.add('show-outline');
1166
+ this.createOverlayLabel(element, 'view');
1167
+ }
1168
+ else {
1169
+ element.style.outline = 'none';
1170
+ element.style.outlineOffset = '0';
1171
+ element.classList.remove('show-outline');
1172
+ this.removeOverlayLabel(element);
1173
+ }
1174
+ });
1175
+ this.saveSettings();
1176
+ }
1177
+ togglePartialOutlines(show) {
1178
+ this.showingPartialOutlines = show !== undefined ? show : !this.showingPartialOutlines;
1179
+ const partialOutlines = document.querySelectorAll('[data-herb-debug-outline-type="partial"], [data-herb-debug-outline-type*="partial"]');
1180
+ partialOutlines.forEach((outline) => {
1181
+ const element = outline;
1182
+ if (this.showingPartialOutlines) {
1183
+ element.style.outline = '2px dotted #10b981';
1184
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
1185
+ element.classList.add('show-outline');
1186
+ this.createOverlayLabel(element, 'partial');
1187
+ }
1188
+ else {
1189
+ element.style.outline = 'none';
1190
+ element.style.outlineOffset = '0';
1191
+ element.classList.remove('show-outline');
1192
+ this.removeOverlayLabel(element);
1193
+ }
1194
+ });
1195
+ this.saveSettings();
1196
+ }
1197
+ toggleComponentOutlines(show) {
1198
+ this.showingComponentOutlines = show !== undefined ? show : !this.showingComponentOutlines;
1199
+ const componentOutlines = document.querySelectorAll('[data-herb-debug-outline-type="component"], [data-herb-debug-outline-type*="component"]');
1200
+ componentOutlines.forEach((outline) => {
1201
+ const element = outline;
1202
+ if (this.showingComponentOutlines) {
1203
+ element.style.outline = '2px dotted #f59e0b';
1204
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
1205
+ element.classList.add('show-outline');
1206
+ this.createOverlayLabel(element, 'component');
1207
+ }
1208
+ else {
1209
+ element.style.outline = 'none';
1210
+ element.style.outlineOffset = '0';
1211
+ element.classList.remove('show-outline');
1212
+ this.removeOverlayLabel(element);
1213
+ }
1214
+ });
1215
+ this.saveSettings();
1216
+ }
1217
+ createOverlayLabel(element, type) {
1218
+ if (element.querySelector('.herb-overlay-label')) {
1219
+ return;
1220
+ }
1221
+ const shortName = element.getAttribute('data-herb-debug-file-name') || '';
1222
+ const relativePath = element.getAttribute('data-herb-debug-file-relative-path') || shortName;
1223
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path') || relativePath;
1224
+ const label = document.createElement('div');
1225
+ label.className = 'herb-overlay-label';
1226
+ label.textContent = shortName;
1227
+ label.setAttribute('data-label-setup', 'true');
1228
+ label.addEventListener('mouseenter', () => {
1229
+ label.textContent = relativePath;
1230
+ document.querySelectorAll('.herb-overlay-label').forEach(otherLabel => {
1231
+ otherLabel.style.zIndex = '1000';
1232
+ });
1233
+ label.style.zIndex = '1002';
1234
+ });
1235
+ label.addEventListener('mouseleave', () => {
1236
+ label.textContent = shortName;
1237
+ label.style.zIndex = '1000';
1238
+ });
1239
+ label.addEventListener('click', (e) => {
1240
+ e.stopPropagation();
1241
+ this.openFileInEditor(fullPath, 1, 1);
1242
+ });
1243
+ const shouldAttachToParent = element.getAttribute('data-herb-debug-attach-to-parent') === 'true';
1244
+ if (shouldAttachToParent && element.parentElement) {
1245
+ const parent = element.parentElement;
1246
+ element.style.outline = 'none';
1247
+ element.classList.remove('show-outline');
1248
+ const outlineColor = type === 'component' ? '#f59e0b' : type === 'partial' ? '#10b981' : '#3b82f6';
1249
+ parent.style.outline = `2px dotted ${outlineColor}`;
1250
+ parent.style.outlineOffset = parent.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
1251
+ parent.classList.add('show-outline');
1252
+ parent.setAttribute('data-herb-debug-attached-outline-type', type);
1253
+ parent.style.position = 'relative';
1254
+ label.style.position = 'absolute';
1255
+ label.style.top = '0';
1256
+ label.style.left = '0';
1257
+ parent.appendChild(label);
1258
+ return;
1259
+ }
1260
+ element.style.position = 'relative';
1261
+ element.appendChild(label);
1262
+ }
1263
+ removeOverlayLabel(element) {
1264
+ const shouldAttachToParent = element.getAttribute('data-herb-debug-attach-to-parent') === 'true';
1265
+ if (shouldAttachToParent && element.parentElement) {
1266
+ const parent = element.parentElement;
1267
+ const label = parent.querySelector('.herb-overlay-label');
1268
+ if (label) {
1269
+ label.remove();
1270
+ }
1271
+ parent.style.outline = 'none';
1272
+ parent.style.outlineOffset = '0';
1273
+ parent.classList.remove('show-outline');
1274
+ parent.removeAttribute('data-herb-debug-attached-outline-type');
1275
+ }
1276
+ else {
1277
+ const label = element.querySelector('.herb-overlay-label');
1278
+ if (label) {
1279
+ label.remove();
1280
+ }
1281
+ }
1282
+ }
1283
+ resetShowingERB() {
1284
+ const elements = document.querySelectorAll('[data-herb-debug-showing-erb');
1285
+ elements.forEach(element => {
1286
+ const originalContent = element.getAttribute('data-herb-debug-original') || "";
1287
+ element.innerHTML = originalContent;
1288
+ element.removeAttribute("data-herb-debug-showing-erb");
1289
+ });
1290
+ }
1291
+ toggleERBTags(show) {
1292
+ this.showingERB = show !== undefined ? show : !this.showingERB;
1293
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
1294
+ erbOutputs.forEach((element) => {
1295
+ const erbCode = element.getAttribute('data-herb-debug-erb');
1296
+ if (this.showingERB && erbCode) {
1297
+ // this.resetShowingERB()
1298
+ if (!element.hasAttribute('data-herb-debug-original')) {
1299
+ element.setAttribute('data-herb-debug-original', element.innerHTML);
1300
+ }
1301
+ element.textContent = erbCode;
1302
+ element.setAttribute("data-herb-debug-showing-erb", "true");
1303
+ element.style.background = '#f3e8ff';
1304
+ element.style.color = '#7c3aed';
1305
+ if (this.showingTooltips) {
1306
+ this.addTooltipHoverHandler(element);
1307
+ }
1308
+ }
1309
+ else {
1310
+ const originalContent = element.getAttribute('data-herb-debug-original') || "";
1311
+ if (element && element.hasAttribute("data-herb-debug-showing-erb")) {
1312
+ element.innerHTML = originalContent;
1313
+ element.removeAttribute("data-herb-debug-showing-erb");
1314
+ }
1315
+ element.style.background = 'transparent';
1316
+ element.style.color = 'inherit';
1317
+ this.removeTooltipHoverHandler(element);
1318
+ this.removeHoverTooltip(element);
1319
+ }
1320
+ });
1321
+ this.saveSettings();
1322
+ }
1323
+ toggleERBOutlines(show) {
1324
+ this.showingERBOutlines = show !== undefined ? show : !this.showingERBOutlines;
1325
+ this.clearCurrentHoveredERB();
1326
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
1327
+ erbOutputs.forEach(element => {
1328
+ const inserted = element.hasAttribute("data-herb-debug-inserted");
1329
+ const needsWrapperToggled = (inserted && !element.children[0]);
1330
+ const realElement = element.children[0] || element;
1331
+ if (this.showingERBOutlines) {
1332
+ realElement.style.outline = '2px dotted #a78bfa';
1333
+ realElement.style.outlineOffset = '1px';
1334
+ if (needsWrapperToggled) {
1335
+ element.style.display = 'inline';
1336
+ }
1337
+ if (this.showingTooltips) {
1338
+ this.addTooltipHoverHandler(element);
1339
+ }
1340
+ if (this.showingERBHoverReveal) {
1341
+ this.addERBHoverReveal(element);
1342
+ }
1343
+ }
1344
+ else {
1345
+ realElement.style.outline = 'none';
1346
+ realElement.style.outlineOffset = '0';
1347
+ if (needsWrapperToggled) {
1348
+ element.style.display = 'contents';
1349
+ }
1350
+ this.removeTooltipHoverHandler(element);
1351
+ this.removeHoverTooltip(element);
1352
+ this.removeERBHoverReveal(element);
1353
+ }
1354
+ });
1355
+ this.saveSettings();
1356
+ }
1357
+ updateNestedToggleVisibility() {
1358
+ const nestedToggle = document.getElementById('herbERBHoverRevealNested');
1359
+ const tooltipsNestedToggle = document.getElementById('herbTooltipsNested');
1360
+ if (nestedToggle) {
1361
+ nestedToggle.style.display = this.showingERBOutlines ? 'block' : 'none';
1362
+ }
1363
+ if (tooltipsNestedToggle) {
1364
+ tooltipsNestedToggle.style.display = this.showingERBOutlines ? 'block' : 'none';
1365
+ }
1366
+ }
1367
+ toggleERBHoverReveal(show) {
1368
+ this.showingERBHoverReveal = show !== undefined ? show : !this.showingERBHoverReveal;
1369
+ if (this.showingERBHoverReveal && this.showingTooltips) {
1370
+ this.toggleTooltips(false);
1371
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
1372
+ if (toggleTooltipsSwitch) {
1373
+ toggleTooltipsSwitch.checked = false;
1374
+ }
1375
+ }
1376
+ this.clearCurrentHoveredERB();
1377
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
1378
+ erbOutputs.forEach((el) => {
1379
+ const element = el;
1380
+ this.removeERBHoverReveal(element);
1381
+ if (this.showingERBHoverReveal && this.showingERBOutlines) {
1382
+ this.addERBHoverReveal(element);
1383
+ }
1384
+ });
1385
+ this.saveSettings();
1386
+ }
1387
+ clearCurrentHoveredERB() {
1388
+ if (this.currentlyHoveredERBElement) {
1389
+ const handlers = this.currentlyHoveredERBElement._erbHoverHandlers;
1390
+ if (handlers) {
1391
+ handlers.hideERBCode();
1392
+ }
1393
+ this.currentlyHoveredERBElement = null;
1394
+ }
1395
+ }
1396
+ addERBHoverReveal(element) {
1397
+ const erbCode = element.getAttribute('data-herb-debug-erb');
1398
+ if (!erbCode)
1399
+ return;
1400
+ this.removeERBHoverReveal(element);
1401
+ if (!element.hasAttribute('data-herb-debug-original')) {
1402
+ element.setAttribute('data-herb-debug-original', element.innerHTML);
1403
+ }
1404
+ const showERBCode = () => {
1405
+ if (!this.showingERBHoverReveal || !this.showingERBOutlines) {
1406
+ return;
1407
+ }
1408
+ if (this.currentlyHoveredERBElement === element) {
1409
+ return;
1410
+ }
1411
+ this.clearCurrentHoveredERB();
1412
+ this.currentlyHoveredERBElement = element;
1413
+ element.style.background = '#f3e8ff';
1414
+ element.style.color = '#7c3aed';
1415
+ element.style.fontFamily = 'inherit';
1416
+ element.style.fontSize = 'inherit';
1417
+ element.style.borderRadius = '3px';
1418
+ element.style.cursor = 'pointer';
1419
+ element.textContent = erbCode;
1420
+ element.addEventListener('click', this.handleRevealedERBClick);
1421
+ };
1422
+ const hideERBCode = () => {
1423
+ if (this.currentlyHoveredERBElement === element) {
1424
+ this.currentlyHoveredERBElement = null;
1425
+ }
1426
+ const originalContent = element.getAttribute('data-herb-debug-original');
1427
+ if (originalContent) {
1428
+ element.innerHTML = originalContent;
1429
+ }
1430
+ element.style.background = 'transparent';
1431
+ element.style.color = 'inherit';
1432
+ element.style.fontFamily = 'inherit';
1433
+ element.style.fontSize = 'inherit';
1434
+ element.style.borderRadius = '0';
1435
+ element.style.cursor = 'default';
1436
+ element.removeEventListener('click', this.handleRevealedERBClick);
1437
+ };
1438
+ element._erbHoverHandlers = { showERBCode, hideERBCode };
1439
+ element.addEventListener('mouseenter', showERBCode);
1440
+ }
1441
+ removeERBHoverReveal(element) {
1442
+ const handlers = element._erbHoverHandlers;
1443
+ if (handlers) {
1444
+ element.removeEventListener('mouseenter', handlers.showERBCode);
1445
+ delete element._erbHoverHandlers;
1446
+ handlers.hideERBCode();
1447
+ }
1448
+ }
1449
+ createHoverTooltip(element, elementForPosition) {
1450
+ this.removeHoverTooltip(element);
1451
+ const relativePath = element.getAttribute('data-herb-debug-file-relative-path') || element.getAttribute('data-herb-debug-file-name') || '';
1452
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path') || relativePath;
1453
+ const line = element.getAttribute('data-herb-debug-line') || '';
1454
+ const column = element.getAttribute('data-herb-debug-column') || '';
1455
+ const erb = element.getAttribute('data-herb-debug-erb') || '';
1456
+ if (!relativePath || !erb)
1457
+ return;
1458
+ const tooltip = document.createElement('div');
1459
+ tooltip.className = 'herb-tooltip';
1460
+ tooltip.innerHTML = `
1461
+ <div class="herb-location" data-tooltip="Open in Editor">
1462
+ <span class="herb-file-path">${relativePath}:${line}:${column}</span>
1463
+ <button class="herb-copy-path-btn" data-tooltip="Copy file path">📋</button>
1464
+ </div>
1465
+ <div class="herb-erb-code">${erb}</div>
1466
+ `;
1467
+ let hideTimeout = null;
1468
+ const showTooltip = () => {
1469
+ if (hideTimeout) {
1470
+ clearTimeout(hideTimeout);
1471
+ hideTimeout = null;
1472
+ }
1473
+ tooltip.classList.add('visible');
1474
+ };
1475
+ const hideTooltip = () => {
1476
+ hideTimeout = window.setTimeout(() => {
1477
+ tooltip.classList.remove('visible');
1478
+ }, 100);
1479
+ };
1480
+ element.addEventListener('mouseenter', showTooltip);
1481
+ element.addEventListener('mouseleave', hideTooltip);
1482
+ tooltip.addEventListener('mouseenter', showTooltip);
1483
+ tooltip.addEventListener('mouseleave', hideTooltip);
1484
+ const locationElement = tooltip.querySelector('.herb-location');
1485
+ const openInEditor = (e) => {
1486
+ if (e.target.closest('.herb-copy-path-btn')) {
1487
+ return;
1488
+ }
1489
+ e.preventDefault();
1490
+ e.stopPropagation();
1491
+ this.openFileInEditor(fullPath, parseInt(line), parseInt(column));
1492
+ };
1493
+ locationElement?.addEventListener('click', openInEditor);
1494
+ const copyButton = tooltip.querySelector('.herb-copy-path-btn');
1495
+ const copyFilePath = (e) => {
1496
+ e.preventDefault();
1497
+ e.stopPropagation();
1498
+ const textToCopy = `${relativePath}:${line}:${column}`;
1499
+ navigator.clipboard.writeText(textToCopy).then(() => {
1500
+ copyButton.textContent = '✅';
1501
+ setTimeout(() => {
1502
+ copyButton.textContent = '📋';
1503
+ }, 1000);
1504
+ }).catch((err) => {
1505
+ console.error('Failed to copy file path:', err);
1506
+ });
1507
+ };
1508
+ copyButton?.addEventListener('click', copyFilePath);
1509
+ const positionTooltip = () => {
1510
+ const elementRect = elementForPosition.getBoundingClientRect();
1511
+ const viewportHeight = window.innerHeight;
1512
+ const viewportWidth = window.innerWidth;
1513
+ tooltip.style.position = 'fixed';
1514
+ tooltip.style.left = '0';
1515
+ tooltip.style.top = '0';
1516
+ tooltip.style.transform = 'none';
1517
+ tooltip.style.bottom = 'auto';
1518
+ const actualTooltipRect = tooltip.getBoundingClientRect();
1519
+ const tooltipWidth = actualTooltipRect.width;
1520
+ const tooltipHeight = actualTooltipRect.height;
1521
+ let left = elementRect.left + (elementRect.width / 2) - (tooltipWidth / 2);
1522
+ let top = elementRect.top - tooltipHeight - 8;
1523
+ if (left < 8) {
1524
+ left = 8;
1525
+ }
1526
+ else if (left + tooltipWidth > viewportWidth - 8) {
1527
+ left = viewportWidth - tooltipWidth - 8;
1528
+ }
1529
+ if (top < 8) {
1530
+ top = elementRect.bottom + 8;
1531
+ if (top + tooltipHeight > viewportHeight - 8) {
1532
+ top = Math.max(8, (viewportHeight - tooltipHeight) / 2);
1533
+ }
1534
+ }
1535
+ if (top + tooltipHeight > viewportHeight - 8) {
1536
+ top = viewportHeight - tooltipHeight - 8;
1537
+ }
1538
+ tooltip.style.position = 'fixed';
1539
+ tooltip.style.left = `${left}px`;
1540
+ tooltip.style.top = `${top}px`;
1541
+ tooltip.style.transform = 'none';
1542
+ tooltip.style.bottom = 'auto';
1543
+ };
1544
+ element._tooltipHandlers = { showTooltip, hideTooltip, openInEditor, copyFilePath, positionTooltip };
1545
+ tooltip._tooltipHandlers = { showTooltip, hideTooltip };
1546
+ element.appendChild(tooltip);
1547
+ setTimeout(positionTooltip, 0);
1548
+ window.addEventListener('scroll', positionTooltip, { passive: true });
1549
+ window.addEventListener('resize', positionTooltip, { passive: true });
1550
+ }
1551
+ removeHoverTooltip(element) {
1552
+ const tooltip = element.querySelector('.herb-tooltip');
1553
+ if (tooltip) {
1554
+ const handlers = element._tooltipHandlers;
1555
+ const tooltipHandlers = tooltip._tooltipHandlers;
1556
+ if (handlers) {
1557
+ element.removeEventListener('mouseenter', handlers.showTooltip);
1558
+ element.removeEventListener('mouseleave', handlers.hideTooltip);
1559
+ const locationElement = tooltip.querySelector('.herb-location');
1560
+ locationElement?.removeEventListener('click', handlers.openInEditor);
1561
+ const copyButton = tooltip.querySelector('.herb-copy-path-btn');
1562
+ copyButton?.removeEventListener('click', handlers.copyFilePath);
1563
+ if (handlers.positionTooltip) {
1564
+ window.removeEventListener('scroll', handlers.positionTooltip);
1565
+ window.removeEventListener('resize', handlers.positionTooltip);
1566
+ }
1567
+ delete element._tooltipHandlers;
1568
+ }
1569
+ if (tooltipHandlers) {
1570
+ tooltip.removeEventListener('mouseenter', tooltipHandlers.showTooltip);
1571
+ tooltip.removeEventListener('mouseleave', tooltipHandlers.hideTooltip);
1572
+ delete tooltip._tooltipHandlers;
1573
+ }
1574
+ tooltip.remove();
1575
+ }
1576
+ }
1577
+ addTooltipHoverHandler(element) {
1578
+ this.removeTooltipHoverHandler(element);
1579
+ const lazyTooltipHandler = () => {
1580
+ if (!this.showingTooltips || !this.showingERBOutlines) {
1581
+ return;
1582
+ }
1583
+ if (element.querySelector('.herb-tooltip')) {
1584
+ return;
1585
+ }
1586
+ this.createHoverTooltip(element, element);
1587
+ };
1588
+ element._lazyTooltipHandler = lazyTooltipHandler;
1589
+ element.addEventListener('mouseenter', lazyTooltipHandler);
1590
+ }
1591
+ removeTooltipHoverHandler(element) {
1592
+ const handler = element._lazyTooltipHandler;
1593
+ if (handler) {
1594
+ element.removeEventListener('mouseenter', handler);
1595
+ delete element._lazyTooltipHandler;
1596
+ }
1597
+ }
1598
+ openFileInEditor(file, line, column) {
1599
+ const absolutePath = file.startsWith('/') ? file : (this.projectPath ? `${this.projectPath}/${file}` : file);
1600
+ const editors = [
1601
+ `vscode://file/${absolutePath}:${line}:${column}`,
1602
+ `subl://open?url=file://${absolutePath}&line=${line}&column=${column}`,
1603
+ `atom://core/open/file?filename=${absolutePath}&line=${line}&column=${column}`,
1604
+ `txmt://open?url=file://${absolutePath}&line=${line}&column=${column}`,
1605
+ ];
1606
+ try {
1607
+ window.open(editors[0], '_self');
1608
+ }
1609
+ catch (error) {
1610
+ console.log(`Open in editor: ${absolutePath}:${line}:${column}`);
1611
+ }
1612
+ }
1613
+ toggleTooltips(show) {
1614
+ this.showingTooltips = show !== undefined ? show : !this.showingTooltips;
1615
+ if (this.showingTooltips && this.showingERBHoverReveal) {
1616
+ this.toggleERBHoverReveal(false);
1617
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
1618
+ if (toggleERBHoverRevealSwitch) {
1619
+ toggleERBHoverRevealSwitch.checked = false;
1620
+ }
1621
+ }
1622
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
1623
+ erbOutputs.forEach((element) => {
1624
+ if (this.showingERBOutlines && this.showingTooltips) {
1625
+ this.addTooltipHoverHandler(element);
1626
+ }
1627
+ else {
1628
+ this.removeTooltipHoverHandler(element);
1629
+ this.removeHoverTooltip(element);
1630
+ }
1631
+ });
1632
+ this.saveSettings();
1633
+ }
1634
+ disableAll() {
1635
+ this.clearCurrentHoveredERB();
1636
+ this.toggleViewOutlines(false);
1637
+ this.togglePartialOutlines(false);
1638
+ this.toggleComponentOutlines(false);
1639
+ this.toggleERBTags(false);
1640
+ this.toggleERBOutlines(false);
1641
+ this.toggleERBHoverReveal(false);
1642
+ this.toggleTooltips(false);
1643
+ const toggleViewOutlinesSwitch = document.getElementById('herbToggleViewOutlines');
1644
+ const togglePartialOutlinesSwitch = document.getElementById('herbTogglePartialOutlines');
1645
+ const toggleComponentOutlinesSwitch = document.getElementById('herbToggleComponentOutlines');
1646
+ const toggleERBSwitch = document.getElementById('herbToggleERB');
1647
+ const toggleERBOutlinesSwitch = document.getElementById('herbToggleERBOutlines');
1648
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
1649
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
1650
+ if (toggleViewOutlinesSwitch)
1651
+ toggleViewOutlinesSwitch.checked = false;
1652
+ if (togglePartialOutlinesSwitch)
1653
+ togglePartialOutlinesSwitch.checked = false;
1654
+ if (toggleComponentOutlinesSwitch)
1655
+ toggleComponentOutlinesSwitch.checked = false;
1656
+ if (toggleERBSwitch)
1657
+ toggleERBSwitch.checked = false;
1658
+ if (toggleERBOutlinesSwitch)
1659
+ toggleERBOutlinesSwitch.checked = false;
1660
+ if (toggleERBHoverRevealSwitch)
1661
+ toggleERBHoverRevealSwitch.checked = false;
1662
+ if (toggleTooltipsSwitch)
1663
+ toggleTooltipsSwitch.checked = false;
1664
+ }
1665
+ initializeErrorOverlay() {
1666
+ this.errorOverlay = new ErrorOverlay();
1667
+ }
1668
+ }
1669
+ HerbOverlay.SETTINGS_KEY = 'herb-dev-tools-settings';
1670
+
1671
+ function initHerbDevTools(options = {}) {
1672
+ return new HerbOverlay(options);
1673
+ }
1674
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1675
+ const hasDebugMode = document.querySelector('meta[name="herb-debug-mode"]')?.getAttribute('content') === 'true';
1676
+ const hasDebugErb = document.querySelector('[data-herb-debug-erb]') !== null;
1677
+ const hasValidationErrors = document.querySelector('template[data-herb-validation-errors]') !== null;
1678
+ const hasValidationError = document.querySelector('template[data-herb-validation-error]') !== null;
1679
+ const hasParserErrors = document.querySelector('template[data-herb-parser-error]') !== null;
1680
+ const shouldAutoInit = hasDebugMode || hasDebugErb || hasValidationErrors || hasValidationError || hasParserErrors;
1681
+ if (shouldAutoInit) {
1682
+ document.addEventListener('DOMContentLoaded', () => {
1683
+ initHerbDevTools();
1684
+ });
1685
+ }
1686
+ }
1687
+ if (typeof window !== 'undefined') {
1688
+ window.HerbDevTools = {
1689
+ init: initHerbDevTools,
1690
+ HerbOverlay
1691
+ };
1692
+ }
1693
+
1694
+ class ReActionViewDevTools {
1695
+ constructor(options = {}) {
1696
+ this.options = options;
1697
+ this.herbOverlay = null;
1698
+ if (options.autoInit !== false) {
1699
+ this.init();
1700
+ }
1701
+ }
1702
+ init() {
1703
+ if (this.herbOverlay) {
1704
+ this.destroy();
1705
+ }
1706
+ this.herbOverlay = initHerbDevTools({
1707
+ projectPath: this.options.projectPath,
1708
+ ...this.options
1709
+ });
1710
+ return this.herbOverlay;
1711
+ }
1712
+ destroy() {
1713
+ if (this.herbOverlay) {
1714
+ const existingMenu = document.querySelector(".herb-floating-menu");
1715
+ if (existingMenu) {
1716
+ existingMenu.remove();
1717
+ }
1718
+ }
1719
+ this.herbOverlay = null;
1720
+ }
1721
+ getHerbOverlay() {
1722
+ return this.herbOverlay;
1723
+ }
1724
+ static getInstance() {
1725
+ return ReActionViewDevTools.instance;
1726
+ }
1727
+ static setInstance(instance) {
1728
+ ReActionViewDevTools.instance = instance;
1729
+ }
1730
+ }
1731
+ ReActionViewDevTools.instance = null;
1732
+ function initReActionViewDevTools(options = {}) {
1733
+ const existingInstance = ReActionViewDevTools.getInstance();
1734
+ if (existingInstance) {
1735
+ existingInstance.destroy();
1736
+ }
1737
+ const instance = new ReActionViewDevTools(options);
1738
+ ReActionViewDevTools.setInstance(instance);
1739
+ return instance;
1740
+ }
1741
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
1742
+ let isInitializing = false;
1743
+ const initializeDevTools = () => {
1744
+ var _a, _b;
1745
+ if (isInitializing) {
1746
+ console.log("ReActionView dev tools initialization already in progress, skipping...");
1747
+ return;
1748
+ }
1749
+ const shouldAutoInit = ((_a = document.querySelector(`meta[name="herb-debug-mode"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("content")) === "true" || document.querySelector("[data-herb-debug-erb]") !== null;
1750
+ if (!shouldAutoInit) {
1751
+ console.log("ReActionView debug mode not detected, skipping dev tools initialization");
1752
+ return;
1753
+ }
1754
+ isInitializing = true;
1755
+ try {
1756
+ let projectPath;
1757
+ const railsRoot = (_b = document.querySelector(`meta[name="herb-rails-root"]`)) === null || _b === void 0 ? void 0 : _b.getAttribute("content");
1758
+ if (railsRoot) {
1759
+ projectPath = railsRoot;
1760
+ }
1761
+ initReActionViewDevTools({
1762
+ projectPath,
1763
+ autoInit: true
1764
+ });
1765
+ }
1766
+ catch (error) {
1767
+ console.warn("Could not initialize ReActionView dev tools:", error);
1768
+ }
1769
+ finally {
1770
+ isInitializing = false;
1771
+ }
1772
+ };
1773
+ if (document.readyState === "loading") {
1774
+ document.addEventListener("DOMContentLoaded", initializeDevTools, { once: true });
1775
+ }
1776
+ else {
1777
+ setTimeout(initializeDevTools, 0);
1778
+ }
1779
+ document.addEventListener("turbo:load", initializeDevTools);
1780
+ document.addEventListener("turbo:render", initializeDevTools);
1781
+ document.addEventListener("turbo:visit", initializeDevTools);
1782
+ }
1783
+ if (typeof window !== "undefined") {
1784
+ window.ReActionViewDevTools = {
1785
+ init: initReActionViewDevTools,
1786
+ ReActionViewDevTools,
1787
+ HerbOverlay
1788
+ };
1789
+ }
1790
+
1791
+ exports.HerbOverlay = HerbOverlay;
1792
+ exports.ReActionViewDevTools = ReActionViewDevTools;
1793
+ exports.initReActionViewDevTools = initReActionViewDevTools;
1794
+
1795
+ }));
1796
+ //# sourceMappingURL=reactionview-dev-tools.umd.js.map