@herb-tools/dev-tools 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,945 @@
1
+ import { Diagnostic } from '@herb-tools/core';
2
+
3
+ export interface ValidationError extends Omit<Diagnostic, 'location'> {
4
+ location?: {
5
+ line: number;
6
+ column: number;
7
+ };
8
+ suggestion?: string;
9
+ }
10
+
11
+ export interface ValidationData {
12
+ validationErrors: ValidationError[];
13
+ filename: string;
14
+ timestamp: string;
15
+ }
16
+
17
+ export class ErrorOverlay {
18
+ private overlay: HTMLElement | null = null;
19
+ private allValidationData: ValidationData[] = [];
20
+ private isVisible = false;
21
+
22
+ constructor() {
23
+ this.init();
24
+ }
25
+
26
+ private init() {
27
+ this.detectValidationErrors();
28
+
29
+ const hasParserErrors = document.querySelector('.herb-parser-error-overlay') !== null;
30
+
31
+ if (this.getTotalErrorCount() > 0) {
32
+ this.createOverlay();
33
+ this.setupToggleHandler();
34
+ } else if (hasParserErrors) {
35
+ console.log('[ErrorOverlay] Parser error overlay already displayed');
36
+ } else {
37
+ console.log('[ErrorOverlay] No errors found, not creating overlay');
38
+ }
39
+ }
40
+
41
+ private detectValidationErrors() {
42
+ const templatesToRemove: HTMLTemplateElement[] = [];
43
+ const validationTemplates = document.querySelectorAll('template[data-herb-validation-error]') as NodeListOf<HTMLTemplateElement>;
44
+
45
+ if (validationTemplates.length > 0) {
46
+ this.processValidationTemplates(validationTemplates, templatesToRemove);
47
+ }
48
+
49
+ const jsonTemplates = document.querySelectorAll('template[data-herb-validation-errors]') as NodeListOf<HTMLTemplateElement>;
50
+
51
+ jsonTemplates.forEach((template, _index) => {
52
+ try {
53
+ let jsonData = template.textContent?.trim();
54
+
55
+ if (!jsonData) {
56
+ jsonData = template.innerHTML?.trim();
57
+ }
58
+
59
+ if (jsonData) {
60
+ const validationData = JSON.parse(jsonData) as ValidationData;
61
+ this.allValidationData.push(validationData);
62
+
63
+ templatesToRemove.push(template);
64
+ }
65
+ } catch (error) {
66
+ console.error('Failed to parse validation errors from template:', error, {
67
+ textContent: template.textContent,
68
+ innerHTML: template.innerHTML
69
+ });
70
+
71
+ templatesToRemove.push(template);
72
+ }
73
+ });
74
+
75
+ const htmlTemplates = document.querySelectorAll('template[data-herb-parser-error]') as NodeListOf<HTMLTemplateElement>;
76
+
77
+ htmlTemplates.forEach((template, _index) => {
78
+ try {
79
+ let htmlContent = template.innerHTML?.trim() || template.textContent?.trim();
80
+
81
+ if (htmlContent) {
82
+ this.displayParserErrorOverlay(htmlContent);
83
+ templatesToRemove.push(template);
84
+ }
85
+ } catch (error) {
86
+ console.error('Failed to process parser error template:', error);
87
+ templatesToRemove.push(template);
88
+ }
89
+ });
90
+
91
+ templatesToRemove.forEach((template, _index) => template.remove());
92
+ }
93
+
94
+ private processValidationTemplates(templates: NodeListOf<HTMLTemplateElement>, templatesToRemove: HTMLTemplateElement[]) {
95
+ const validationFragments: Array<{
96
+ metadata: {
97
+ severity: string;
98
+ source: string;
99
+ code: string;
100
+ line: number;
101
+ column: number;
102
+ filename: string;
103
+ message: string;
104
+ suggestion?: string;
105
+ timestamp: string;
106
+ };
107
+ html: string;
108
+ count: number;
109
+ }> = [];
110
+
111
+ const errorMap = new Map<string, typeof validationFragments[0]>();
112
+
113
+ templates.forEach((template) => {
114
+ try {
115
+ const metadata = {
116
+ severity: template.getAttribute('data-severity') || 'error',
117
+ source: template.getAttribute('data-source') || 'unknown',
118
+ code: template.getAttribute('data-code') || '',
119
+ line: parseInt(template.getAttribute('data-line') || '0'),
120
+ column: parseInt(template.getAttribute('data-column') || '0'),
121
+ filename: template.getAttribute('data-filename') || 'unknown',
122
+ message: template.getAttribute('data-message') || '',
123
+ suggestion: template.getAttribute('data-suggestion') || undefined,
124
+ timestamp: template.getAttribute('data-timestamp') || new Date().toISOString()
125
+ };
126
+
127
+ const html = template.innerHTML?.trim() || '';
128
+
129
+ if (html) {
130
+ const errorKey = `${metadata.filename}:${metadata.line}:${metadata.column}:${metadata.code}:${metadata.message}`;
131
+
132
+ if (errorMap.has(errorKey)) {
133
+ const existing = errorMap.get(errorKey)!;
134
+ existing.count++;
135
+ } else {
136
+ errorMap.set(errorKey, { metadata, html, count: 1 });
137
+ }
138
+
139
+ templatesToRemove.push(template);
140
+ }
141
+ } catch (error) {
142
+ console.error('Failed to process validation template:', error);
143
+ templatesToRemove.push(template);
144
+ }
145
+ });
146
+
147
+ validationFragments.push(...errorMap.values());
148
+
149
+ if (validationFragments.length > 0) {
150
+ this.displayValidationOverlay(validationFragments);
151
+ }
152
+ }
153
+
154
+ private createOverlay() {
155
+ if (this.allValidationData.length === 0) return;
156
+
157
+ this.overlay = document.createElement('div');
158
+ this.overlay.id = 'herb-error-overlay';
159
+ this.overlay.innerHTML = `
160
+ <style>
161
+ #herb-error-overlay {
162
+ position: fixed;
163
+ top: 0;
164
+ left: 0;
165
+ right: 0;
166
+ bottom: 0;
167
+ background: rgba(0, 0, 0, 0.8);
168
+ z-index: 10000;
169
+ display: none;
170
+ overflow: auto;
171
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
172
+ }
173
+
174
+ .herb-error-content {
175
+ background: #1a1a1a;
176
+ margin: 20px auto;
177
+ padding: 20px;
178
+ border-radius: 8px;
179
+ max-width: 800px;
180
+ color: #fff;
181
+ }
182
+
183
+ .herb-error-header {
184
+ display: flex;
185
+ justify-content: space-between;
186
+ align-items: center;
187
+ margin-bottom: 20px;
188
+ border-bottom: 1px solid #333;
189
+ padding-bottom: 10px;
190
+ }
191
+
192
+ .herb-error-title {
193
+ font-size: 18px;
194
+ font-weight: 600;
195
+ color: #ff6b6b;
196
+ }
197
+
198
+ .herb-error-close {
199
+ background: none;
200
+ border: none;
201
+ color: #fff;
202
+ font-size: 20px;
203
+ cursor: pointer;
204
+ padding: 5px;
205
+ }
206
+
207
+ .herb-error-file-section {
208
+ margin-bottom: 20px;
209
+ }
210
+
211
+ .herb-error-file {
212
+ font-size: 14px;
213
+ color: #888;
214
+ margin-bottom: 10px;
215
+ font-weight: 600;
216
+ }
217
+
218
+ .herb-error-item {
219
+ background: #2a2a2a;
220
+ border-radius: 6px;
221
+ padding: 15px;
222
+ margin-bottom: 10px;
223
+ border-left: 4px solid #ff6b6b;
224
+ }
225
+
226
+ .herb-error-item.warning {
227
+ border-left-color: #ffd93d;
228
+ }
229
+
230
+ .herb-error-item.info {
231
+ border-left-color: #4ecdc4;
232
+ }
233
+
234
+ .herb-error-item.hint {
235
+ border-left-color: #95a5a6;
236
+ }
237
+
238
+ .herb-error-message {
239
+ font-size: 14px;
240
+ margin-bottom: 8px;
241
+ line-height: 1.4;
242
+ }
243
+
244
+ .herb-error-location {
245
+ font-size: 12px;
246
+ color: #888;
247
+ margin-bottom: 8px;
248
+ }
249
+
250
+ .herb-error-suggestion {
251
+ font-size: 12px;
252
+ color: #4ecdc4;
253
+ font-style: italic;
254
+ }
255
+
256
+ .herb-error-source {
257
+ font-size: 11px;
258
+ color: #666;
259
+ text-align: right;
260
+ }
261
+
262
+ .herb-file-separator {
263
+ border-top: 1px solid #444;
264
+ margin: 20px 0;
265
+ }
266
+ </style>
267
+
268
+ <div class="herb-error-content">
269
+ <div class="herb-error-header">
270
+ <div class="herb-error-title">
271
+ Errors (${this.getTotalErrorCount()})
272
+ </div>
273
+ <button class="herb-error-close">&times;</button>
274
+ </div>
275
+
276
+ <div class="herb-error-files">
277
+ ${this.allValidationData.map((validationData, index) => `
278
+ ${index > 0 ? '<div class="herb-file-separator"></div>' : ''}
279
+ <div class="herb-error-file-section">
280
+ <div class="herb-error-file">${validationData.filename} (${this.getErrorSummary(validationData.validationErrors)})</div>
281
+ <div class="herb-error-list">
282
+ ${validationData.validationErrors.map(error => `
283
+ <div class="herb-error-item ${error.severity}">
284
+ <div class="herb-error-message">${this.escapeHtml(error.message)}</div>
285
+ ${error.location ? `<div class="herb-error-location">Line ${error.location.line}, Column ${error.location.column}</div>` : ''}
286
+ ${error.suggestion ? `<div class="herb-error-suggestion">💡 ${this.escapeHtml(error.suggestion)}</div>` : ''}
287
+ <div class="herb-error-source">${error.source}${error.code ? ` (${error.code})` : ''}</div>
288
+ </div>
289
+ `).join('')}
290
+ </div>
291
+ </div>
292
+ `).join('')}
293
+ </div>
294
+ </div>
295
+ `;
296
+
297
+ document.body.appendChild(this.overlay);
298
+
299
+ const closeBtn = this.overlay.querySelector('.herb-error-close');
300
+ closeBtn?.addEventListener('click', () => this.hide());
301
+
302
+ this.overlay.addEventListener('click', (e) => {
303
+ if (e.target === this.overlay) {
304
+ this.hide();
305
+ }
306
+ });
307
+
308
+ document.addEventListener('keydown', (e) => {
309
+ if (e.key === 'Escape' && this.isVisible) {
310
+ this.hide();
311
+ }
312
+ });
313
+ }
314
+
315
+ private setupToggleHandler() {
316
+ document.addEventListener('keydown', (e) => {
317
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'E') {
318
+ e.preventDefault();
319
+ this.toggle();
320
+ }
321
+ });
322
+
323
+ if (this.hasErrorSeverity()) {
324
+ setTimeout(() => this.show(), 100);
325
+ }
326
+ }
327
+
328
+ private getTotalErrorCount(): number {
329
+ return this.allValidationData.reduce((total, data) => total + data.validationErrors.length, 0);
330
+ }
331
+
332
+ private getErrorSummary(errors: ValidationError[]): string {
333
+ if (errors.length === 1) {
334
+ return '1 error';
335
+ }
336
+
337
+ const errorsBySource = errors.reduce((acc, error) => {
338
+ const source = error.source || 'Unknown';
339
+ acc[source] = (acc[source] || 0) + 1;
340
+ return acc;
341
+ }, {} as Record<string, number>);
342
+
343
+ const sourceKeys = Object.keys(errorsBySource);
344
+ if (sourceKeys.length === 1) {
345
+ const source = sourceKeys[0];
346
+ const count = errorsBySource[source];
347
+ const sourceLabel = this.getSourceLabel(source);
348
+
349
+ return `${count} ${sourceLabel} error${count === 1 ? '' : 's'}`;
350
+ } else {
351
+ const parts = sourceKeys.map(source => {
352
+ const count = errorsBySource[source];
353
+ const sourceLabel = this.getSourceLabel(source);
354
+ return `${count} ${sourceLabel}`;
355
+ });
356
+
357
+ return `${errors.length} errors (${parts.join(', ')})`;
358
+ }
359
+ }
360
+
361
+ private getSourceLabel(source: string): string {
362
+ switch (source) {
363
+ case 'Parser': return 'parser';
364
+ case 'SecurityValidator': return 'security';
365
+ case 'NestingValidator': return 'nesting';
366
+ case 'AccessibilityValidator': return 'accessibility';
367
+ default: return 'validation';
368
+ }
369
+ }
370
+
371
+ private hasErrorSeverity(): boolean {
372
+ return this.allValidationData.some(data =>
373
+ data.validationErrors.some(error => error.severity === 'error')
374
+ );
375
+ }
376
+
377
+ private escapeHtml(unsafe: string): string {
378
+ return unsafe
379
+ .replace(/&/g, '&amp;')
380
+ .replace(/</g, '&lt;')
381
+ .replace(/>/g, '&gt;')
382
+ .replace(/"/g, '&quot;')
383
+ .replace(/'/g, '&#039;');
384
+ }
385
+
386
+ public show() {
387
+ if (this.overlay) {
388
+ this.overlay.style.display = 'block';
389
+ this.isVisible = true;
390
+ }
391
+ }
392
+
393
+ public hide() {
394
+ if (this.overlay) {
395
+ this.overlay.style.display = 'none';
396
+ this.isVisible = false;
397
+ }
398
+ }
399
+
400
+ public toggle() {
401
+ if (this.isVisible) {
402
+ this.hide();
403
+ } else {
404
+ this.show();
405
+ }
406
+ }
407
+
408
+ public hasErrors(): boolean {
409
+ return this.getTotalErrorCount() > 0;
410
+ }
411
+
412
+ public getErrorCount(): number {
413
+ return this.getTotalErrorCount();
414
+ }
415
+
416
+ private displayParserErrorOverlay(htmlContent: string) {
417
+ const existingOverlay = document.querySelector('.herb-parser-error-overlay');
418
+ if (existingOverlay) {
419
+ existingOverlay.remove();
420
+ }
421
+
422
+ const container = document.createElement('div');
423
+ container.innerHTML = htmlContent;
424
+
425
+ const overlay = container.querySelector('.herb-parser-error-overlay') as HTMLElement;
426
+
427
+ if (overlay) {
428
+ document.body.appendChild(overlay);
429
+ overlay.style.display = 'flex';
430
+ } else {
431
+ console.error('[ErrorOverlay] No parser error overlay found in HTML template');
432
+ }
433
+ }
434
+
435
+ private displayValidationOverlay(fragments: Array<{
436
+ metadata: {
437
+ severity: string;
438
+ source: string;
439
+ code: string;
440
+ line: number;
441
+ column: number;
442
+ filename: string;
443
+ message: string;
444
+ suggestion?: string;
445
+ timestamp: string;
446
+ };
447
+ html: string;
448
+ count: number;
449
+ }>) {
450
+ const existingOverlay = document.querySelector('.herb-validation-overlay');
451
+ if (existingOverlay) {
452
+ existingOverlay.remove();
453
+ }
454
+
455
+ const errorsBySource = new Map<string, typeof fragments>();
456
+ const errorsByFile = new Map<string, typeof fragments>();
457
+
458
+ fragments.forEach(fragment => {
459
+ const source = fragment.metadata.source;
460
+
461
+ if (!errorsBySource.has(source)) {
462
+ errorsBySource.set(source, []);
463
+ }
464
+
465
+ errorsBySource.get(source)!.push(fragment);
466
+
467
+ const file = fragment.metadata.filename;
468
+
469
+ if (!errorsByFile.has(file)) {
470
+ errorsByFile.set(file, []);
471
+ }
472
+ errorsByFile.get(file)!.push(fragment);
473
+ });
474
+
475
+ const errorCount = fragments.filter(f => f.metadata.severity === 'error').reduce((sum, f) => sum + f.count, 0);
476
+ const warningCount = fragments.filter(f => f.metadata.severity === 'warning').reduce((sum, f) => sum + f.count, 0);
477
+ const totalCount = fragments.reduce((sum, f) => sum + f.count, 0);
478
+ const uniqueCount = fragments.length;
479
+
480
+ const overlayHTML = this.buildValidationOverlayHTML(
481
+ fragments,
482
+ errorsBySource,
483
+ errorsByFile,
484
+ { errorCount, warningCount, totalCount, uniqueCount }
485
+ );
486
+
487
+ const overlay = document.createElement('div');
488
+ overlay.className = 'herb-validation-overlay';
489
+ overlay.innerHTML = overlayHTML;
490
+
491
+ document.body.appendChild(overlay);
492
+
493
+ this.setupValidationOverlayHandlers(overlay);
494
+ }
495
+
496
+ private buildValidationOverlayHTML(
497
+ _fragments: Array<any>,
498
+ errorsBySource: Map<string, any[]>,
499
+ errorsByFile: Map<string, any[]>,
500
+ counts: { errorCount: number; warningCount: number; totalCount: number; uniqueCount: number }
501
+ ): string {
502
+ let title = counts.uniqueCount === 1 ? 'Validation Issue' : `Validation Issues`;
503
+
504
+ if (counts.totalCount !== counts.uniqueCount) {
505
+ title += ` (${counts.uniqueCount} unique, ${counts.totalCount} total)`;
506
+ } else {
507
+ title += ` (${counts.totalCount})`;
508
+ }
509
+
510
+ const subtitle = [];
511
+
512
+ if (counts.errorCount > 0) subtitle.push(`${counts.errorCount} error${counts.errorCount !== 1 ? 's' : ''}`);
513
+ if (counts.warningCount > 0) subtitle.push(`${counts.warningCount} warning${counts.warningCount !== 1 ? 's' : ''}`);
514
+
515
+ let fileTabs = '';
516
+
517
+ if (errorsByFile.size > 1) {
518
+ const totalErrors = Array.from(errorsByFile.values()).reduce((sum, errors) => sum + errors.length, 0);
519
+
520
+ fileTabs = `
521
+ <div class="herb-file-tabs">
522
+ <button class="herb-file-tab active" data-file="*">
523
+ All (${totalErrors})
524
+ </button>
525
+ ${Array.from(errorsByFile.entries()).map(([file, errors]) => `
526
+ <button class="herb-file-tab" data-file="${this.escapeAttr(file)}">
527
+ ${this.escapeHtml(file)} (${errors.length})
528
+ </button>
529
+ `).join('')}
530
+ </div>
531
+ `;
532
+ }
533
+
534
+ const contentSections = Array.from(errorsBySource.entries()).map(([source, sourceFragments]) => `
535
+ <div class="herb-validator-section" data-source="${this.escapeAttr(source)}">
536
+ <div class="herb-validator-header">
537
+ <h3>${this.escapeHtml(source.replace('Validator', ''))} Issues (${sourceFragments.length})</h3>
538
+ </div>
539
+ <div class="herb-validator-content">
540
+ ${sourceFragments.map(f => {
541
+ const fileAttribute = `data-error-file="${this.escapeAttr(f.metadata.filename)}"`;
542
+
543
+ if (f.count > 1) {
544
+ return `
545
+ <div class="herb-validation-item-wrapper" ${fileAttribute}>
546
+ ${f.html}
547
+ <div class="herb-occurrence-badge" title="This error occurs ${f.count} times in the template">
548
+ <span class="herb-occurrence-icon">⚠</span>
549
+ <span class="herb-occurrence-count">×${f.count}</span>
550
+ </div>
551
+ </div>
552
+ `;
553
+ }
554
+ return `<div class="herb-validation-error-container" ${fileAttribute}>${f.html}</div>`;
555
+ }).join('')}
556
+ </div>
557
+ </div>
558
+ `).join('');
559
+
560
+ return `
561
+ <style>${this.getValidationOverlayStyles()}</style>
562
+ <div class="herb-validation-container">
563
+ <div class="herb-validation-header">
564
+ <div class="herb-validation-header-content">
565
+ <div class="herb-validation-title">
566
+ <span class="herb-validation-icon">⚠️</span>
567
+ ${title}
568
+ </div>
569
+ <div class="herb-validation-subtitle">${subtitle.join(', ')}</div>
570
+ </div>
571
+ <button class="herb-close-button" title="Close (Esc)">×</button>
572
+ </div>
573
+ ${fileTabs}
574
+ <div class="herb-validation-content">
575
+ ${contentSections}
576
+ </div>
577
+ <div class="herb-dismiss-hint" style="padding-left: 24px; padding-right: 24px; padding-bottom: 12px;">
578
+ 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>
579
+
580
+ 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>.
581
+ </div>
582
+ </div>
583
+ `;
584
+ }
585
+
586
+ private getValidationOverlayStyles(): string {
587
+ return `
588
+ .herb-validation-overlay {
589
+ position: fixed;
590
+ top: 0;
591
+ left: 0;
592
+ right: 0;
593
+ bottom: 0;
594
+ background: rgba(0, 0, 0, 0.8);
595
+ backdrop-filter: blur(4px);
596
+ z-index: 9999;
597
+ display: flex;
598
+ align-items: center;
599
+ justify-content: center;
600
+ padding: 20px;
601
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
602
+ color: #e5e5e5;
603
+ line-height: 1.6;
604
+ }
605
+
606
+ .herb-validation-container {
607
+ background: #000000;
608
+ border: 1px solid #374151;
609
+ border-radius: 12px;
610
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
611
+ max-width: 1200px;
612
+ max-height: 80vh;
613
+ width: 100%;
614
+ display: flex;
615
+ flex-direction: column;
616
+ overflow: hidden;
617
+ }
618
+
619
+ .herb-validation-header {
620
+ background: linear-gradient(135deg, #f59e0b, #d97706);
621
+ padding: 20px 24px;
622
+ border-bottom: 1px solid #374151;
623
+ display: flex;
624
+ justify-content: space-between;
625
+ align-items: flex-start;
626
+ }
627
+
628
+ .herb-validation-title {
629
+ font-size: 18px;
630
+ font-weight: 600;
631
+ color: white;
632
+ display: flex;
633
+ align-items: center;
634
+ gap: 12px;
635
+ }
636
+
637
+ .herb-validation-subtitle {
638
+ font-size: 14px;
639
+ color: rgba(255, 255, 255, 0.9);
640
+ margin-top: 4px;
641
+ }
642
+
643
+ .herb-file-tabs {
644
+ background: #1a1a1a;
645
+ border-bottom: 1px solid #374151;
646
+ padding: 0;
647
+ display: flex;
648
+ overflow-x: auto;
649
+ }
650
+
651
+ .herb-file-tab {
652
+ background: transparent;
653
+ border: none;
654
+ color: #9ca3af;
655
+ padding: 12px 20px;
656
+ cursor: pointer;
657
+ font-size: 13px;
658
+ white-space: nowrap;
659
+ transition: all 0.2s;
660
+ border-bottom: 2px solid transparent;
661
+ }
662
+
663
+ .herb-file-tab:hover {
664
+ background: #262626;
665
+ color: #e5e5e5;
666
+ }
667
+
668
+ .herb-file-tab.active {
669
+ color: #f59e0b;
670
+ border-bottom-color: #f59e0b;
671
+ background: #262626;
672
+ }
673
+
674
+ .herb-validation-content {
675
+ flex: 1;
676
+ overflow-y: auto;
677
+ padding: 24px;
678
+ display: flex;
679
+ flex-direction: column;
680
+ gap: 24px;
681
+ }
682
+
683
+ .herb-validator-section {
684
+ background: #0f0f0f;
685
+ border: 1px solid #2d2d2d;
686
+ border-radius: 8px;
687
+ }
688
+
689
+ .herb-validator-header {
690
+ background: #1a1a1a;
691
+ padding: 12px 16px;
692
+ border-bottom: 1px solid #2d2d2d;
693
+ }
694
+
695
+ .herb-validator-header h3 {
696
+ margin: 0;
697
+ font-size: 14px;
698
+ font-weight: 500;
699
+ color: #e5e5e5;
700
+ }
701
+
702
+ .herb-validator-content {
703
+ padding: 16px;
704
+ display: flex;
705
+ flex-direction: column;
706
+ gap: 16px;
707
+ }
708
+
709
+ .herb-validation-item {
710
+ border-left: 3px solid #4a4a4a;
711
+ padding-left: 16px;
712
+ }
713
+
714
+ .herb-validation-item[data-severity="error"] {
715
+ border-left-color: #7f1d1d;
716
+ }
717
+
718
+ .herb-validation-item[data-severity="warning"] {
719
+ border-left-color: #78350f;
720
+ }
721
+
722
+ .herb-validation-item[data-severity="info"] {
723
+ border-left-color: #1e3a8a;
724
+ }
725
+
726
+ .herb-validation-header {
727
+ display: flex;
728
+ align-items: center;
729
+ gap: 8px;
730
+ margin-bottom: 8px;
731
+ }
732
+
733
+ .herb-validation-badge {
734
+ padding: 2px 8px;
735
+ border-radius: 4px;
736
+ font-size: 11px;
737
+ font-weight: 600;
738
+ color: white;
739
+ text-transform: uppercase;
740
+ }
741
+
742
+ .herb-validation-location {
743
+ font-size: 12px;
744
+ color: #9ca3af;
745
+ }
746
+
747
+ .herb-validation-message {
748
+ font-size: 14px;
749
+ margin-bottom: 12px;
750
+ line-height: 1.5;
751
+ }
752
+
753
+ .herb-code-snippet {
754
+ background: #1a1a1a;
755
+ border: 1px solid #2d2d2d;
756
+ border-radius: 4px;
757
+ padding: 12px;
758
+ overflow-x: auto;
759
+ }
760
+
761
+ .herb-code-line {
762
+ display: flex;
763
+ align-items: flex-start;
764
+ min-height: 20px;
765
+ font-size: 13px;
766
+ line-height: 1.5;
767
+ }
768
+
769
+ .herb-line-number {
770
+ color: #6b7280;
771
+ width: 40px;
772
+ text-align: right;
773
+ padding-right: 16px;
774
+ user-select: none;
775
+ flex-shrink: 0;
776
+ }
777
+
778
+ .herb-line-content {
779
+ flex: 1;
780
+ white-space: pre;
781
+ font-family: inherit;
782
+ }
783
+
784
+ .herb-error-line {
785
+ background: rgba(220, 38, 38, 0.1);
786
+ }
787
+
788
+ .herb-error-line .herb-line-number {
789
+ color: #dc2626;
790
+ font-weight: 600;
791
+ }
792
+
793
+ .herb-error-pointer {
794
+ color: #dc2626;
795
+ font-weight: bold;
796
+ margin-left: 56px;
797
+ font-size: 12px;
798
+ }
799
+
800
+ .herb-validation-suggestion {
801
+ background: #1e3a1e;
802
+ border: 1px solid #10b981;
803
+ border-radius: 4px;
804
+ padding: 8px 12px;
805
+ margin-top: 8px;
806
+ font-size: 13px;
807
+ color: #10b981;
808
+ display: flex;
809
+ align-items: center;
810
+ gap: 8px;
811
+ }
812
+
813
+ .herb-validation-item-wrapper,
814
+ .herb-validation-error-container {
815
+ position: relative;
816
+ }
817
+
818
+ .herb-validation-error-container.hidden,
819
+ .herb-validation-item-wrapper.hidden,
820
+ .herb-validator-section.hidden {
821
+ display: none;
822
+ }
823
+
824
+ .herb-occurrence-badge {
825
+ position: absolute;
826
+ top: 8px;
827
+ right: 8px;
828
+ background: #dc2626;
829
+ color: white;
830
+ padding: 4px 8px;
831
+ border-radius: 12px;
832
+ font-size: 12px;
833
+ font-weight: 600;
834
+ display: flex;
835
+ align-items: center;
836
+ gap: 4px;
837
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
838
+ }
839
+
840
+ .herb-occurrence-icon {
841
+ font-size: 10px;
842
+ }
843
+
844
+ .herb-occurrence-count {
845
+ font-weight: bold;
846
+ }
847
+
848
+ .herb-close-button {
849
+ background: rgba(255, 255, 255, 0.1);
850
+ border: 1px solid rgba(255, 255, 255, 0.2);
851
+ color: white;
852
+ width: 32px;
853
+ height: 32px;
854
+ border-radius: 6px;
855
+ display: flex;
856
+ align-items: center;
857
+ justify-content: center;
858
+ cursor: pointer;
859
+ font-size: 16px;
860
+ transition: all 0.2s;
861
+ }
862
+
863
+ .herb-close-button:hover {
864
+ background: rgba(255, 255, 255, 0.2);
865
+ border-color: rgba(255, 255, 255, 0.3);
866
+ }
867
+
868
+ /* Syntax highlighting */
869
+ .herb-erb { color: #61dafb; }
870
+ .herb-erb-content { color: #c678dd; }
871
+ .herb-tag { color: #e06c75; }
872
+ .herb-attr { color: #d19a66; }
873
+ .herb-value { color: #98c379; }
874
+ .herb-comment { color: #5c6370; font-style: italic; }
875
+ `;
876
+ }
877
+
878
+ private setupValidationOverlayHandlers(overlay: HTMLElement) {
879
+ const closeBtn = overlay.querySelector('.herb-close-button');
880
+
881
+ if (closeBtn) {
882
+ closeBtn.addEventListener('click', () => overlay.remove());
883
+ }
884
+
885
+ overlay.addEventListener('click', (e) => {
886
+ if (e.target === overlay) {
887
+ overlay.remove();
888
+ }
889
+ });
890
+
891
+ const escHandler = (e: KeyboardEvent) => {
892
+ if (e.key === 'Escape') {
893
+ overlay.remove();
894
+ document.removeEventListener('keydown', escHandler);
895
+ }
896
+ };
897
+
898
+ document.addEventListener('keydown', escHandler);
899
+
900
+ const fileTabs = overlay.querySelectorAll('.herb-file-tab');
901
+
902
+ fileTabs.forEach(tab => {
903
+ tab.addEventListener('click', () => {
904
+ const selectedFile = tab.getAttribute('data-file');
905
+
906
+ fileTabs.forEach(t => t.classList.remove('active'));
907
+ tab.classList.add('active');
908
+
909
+ const errorContainers = overlay.querySelectorAll('[data-error-file]');
910
+ const validatorSections = overlay.querySelectorAll('.herb-validator-section');
911
+
912
+ errorContainers.forEach(container => {
913
+ const containerFile = container.getAttribute('data-error-file');
914
+ if (selectedFile === '*' || containerFile === selectedFile) {
915
+ container.classList.remove('hidden');
916
+ } else {
917
+ container.classList.add('hidden');
918
+ }
919
+ });
920
+
921
+ validatorSections.forEach(section => {
922
+ const sectionContent = section.querySelector('.herb-validator-content');
923
+ const visibleErrors = sectionContent?.querySelectorAll('[data-error-file]:not(.hidden)').length || 0;
924
+
925
+ const header = section.querySelector('h3');
926
+ const source = section.getAttribute('data-source')?.replace('Validator', '') || 'Unknown';
927
+
928
+ if (header) {
929
+ header.textContent = `${source} Issues (${visibleErrors})`;
930
+ }
931
+
932
+ if (visibleErrors === 0) {
933
+ section.classList.add('hidden');
934
+ } else {
935
+ section.classList.remove('hidden');
936
+ }
937
+ });
938
+ });
939
+ });
940
+ }
941
+
942
+ private escapeAttr(text: string): string {
943
+ return this.escapeHtml(text).replace(/"/g, '&quot;');
944
+ }
945
+ }