reactionview 0.0.1 → 0.1.1

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