@@ -14,6 +14,194 @@ export interface ValidationData {
1414 timestamp : string ;
1515}
1616
17+ const optimizationMismatches : Set < string > = new Set ( ) ;
18+ let optimizationBadgeInitialized = false ;
19+
20+ function scanForOptimizationMismatches ( ) {
21+ const templates = document . querySelectorAll ( 'template[data-herb-optimization-mismatch]' ) as NodeListOf < HTMLTemplateElement > ;
22+
23+ templates . forEach ( ( template ) => {
24+ optimizationMismatches . add ( template . getAttribute ( 'data-filename' ) || '(unknown)' ) ;
25+ template . remove ( ) ;
26+ } ) ;
27+
28+ if ( optimizationMismatches . size > 0 ) {
29+ renderOptimizationBadge ( ) ;
30+ }
31+ }
32+
33+ function renderOptimizationBadge ( ) {
34+ document . querySelector ( '.herb-optimization-badge' ) ?. remove ( ) ;
35+ document . querySelector ( '.herb-optimization-panel' ) ?. remove ( ) ;
36+
37+ const filenames = Array . from ( optimizationMismatches ) ;
38+ const projectPath = document . querySelector ( 'meta[name="herb-project-path"]' ) ?. getAttribute ( 'content' ) || '' ;
39+ const displayNames = filenames . map ( f => projectPath && f . startsWith ( projectPath ) ? f . slice ( projectPath . length ) . replace ( / ^ \/ / , '' ) : f ) ;
40+ const title = `\u26A0\uFE0F ${ filenames . length } Compile-Time Optimization Mismatch${ filenames . length === 1 ? '' : 'es' } ` ;
41+
42+ if ( ! optimizationBadgeInitialized ) {
43+ optimizationBadgeInitialized = true ;
44+
45+ const style = document . createElement ( 'style' ) ;
46+ style . className = 'herb-optimization-badge-style' ;
47+
48+ style . textContent = `
49+ .herb-floating-menu {
50+ display: flex;
51+ flex-direction: row;
52+ align-items: flex-start;
53+ }
54+
55+ .herb-optimization-badge {
56+ background: #fffbeb;
57+ color: #92400e;
58+ font-size: 11px;
59+ font-weight: 600;
60+ padding: 4px 7px;
61+ border-radius: 0 0 0 10px;
62+ border: 1px solid #f59e0b;
63+ border-top: none;
64+ border-right: none;
65+ cursor: pointer;
66+ text-align: center;
67+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
68+ box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
69+ z-index: 2147483640;
70+ transition: all 0.2s ease;
71+ order: -1;
72+ }
73+
74+ .herb-optimization-badge:hover {
75+ background: #fef3c7;
76+ border-color: #d97706;
77+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
78+ }
79+
80+ .herb-floating-menu .herb-optimization-badge + .herb-menu-trigger {
81+ border-radius: 0 0 0 0;
82+ }
83+
84+ .herb-optimization-panel {
85+ position: fixed;
86+ top: 30px;
87+ right: 8px;
88+ background: white;
89+ border: 1px solid #e5e7eb;
90+ border-radius: 8px;
91+ width: 420px;
92+ max-height: 400px;
93+ overflow-y: auto;
94+ z-index: 2147483642;
95+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
96+ font-size: 12px;
97+ color: #374151;
98+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
99+ display: none;
100+ }
101+
102+ .herb-optimization-panel.visible {
103+ display: block;
104+ }
105+
106+ .herb-optimization-panel-header {
107+ background: #fffbeb;
108+ padding: 10px 14px;
109+ color: #92400e;
110+ font-weight: 600;
111+ font-size: 13px;
112+ display: flex;
113+ justify-content: space-between;
114+ align-items: center;
115+ border-bottom: 1px solid #fde68a;
116+ border-radius: 8px 8px 0 0;
117+ }
118+
119+ .herb-optimization-panel-close {
120+ background: none;
121+ border: none;
122+ color: #92400e;
123+ cursor: pointer;
124+ font-size: 16px;
125+ padding: 0 4px;
126+ }
127+
128+ .herb-optimization-panel-close:hover {
129+ color: #78350f;
130+ }
131+
132+ .herb-optimization-panel-list {
133+ padding: 4px 0;
134+ }
135+
136+ .herb-optimization-panel-item {
137+ padding: 6px 14px;
138+ color: #6b7280;
139+ border-bottom: 1px solid #f3f4f6;
140+ word-break: break-all;
141+ font-family: 'SF Mono', Monaco, Consolas, monospace;
142+ font-size: 11px;
143+ }
144+
145+ .herb-optimization-panel-item:last-child {
146+ border-bottom: none;
147+ }
148+
149+ .herb-optimization-panel-hint {
150+ padding: 8px 14px;
151+ color: #9ca3af;
152+ font-size: 11px;
153+ border-top: 1px solid #e5e7eb;
154+ background: #f9fafb;
155+ border-radius: 0 0 8px 8px;
156+ }
157+ ` ;
158+
159+ document . head . appendChild ( style ) ;
160+ }
161+
162+ const panel = document . createElement ( 'div' ) ;
163+ panel . className = 'herb-optimization-panel' ;
164+
165+ panel . innerHTML = `
166+ <div class="herb-optimization-panel-header">
167+ <span>${ title } </span>
168+ <button class="herb-optimization-panel-close">×</button>
169+ </div>
170+
171+ <div class="herb-optimization-panel-list">
172+ ${ displayNames . map ( f => `<div class="herb-optimization-panel-item">${ f . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) } </div>` ) . join ( '' ) }
173+ </div>
174+
175+ <div class="herb-optimization-panel-hint">
176+ Check Rails log for details. Disable with <code>config.verify_optimizations = false</code>.
177+ </div>
178+ ` ;
179+ document . body . appendChild ( panel ) ;
180+
181+ panel . querySelector ( '.herb-optimization-panel-close' ) ?. addEventListener ( 'click' , ( ) => {
182+ panel . classList . remove ( 'visible' ) ;
183+ } ) ;
184+
185+ const badge = document . createElement ( 'div' ) ;
186+ badge . className = 'herb-optimization-badge' ;
187+ badge . textContent = `\u26A0\uFE0F ${ filenames . length } ` ;
188+ badge . title = title ;
189+
190+ badge . addEventListener ( 'click' , ( ) => {
191+ panel . classList . toggle ( 'visible' ) ;
192+ } ) ;
193+
194+ const menu = document . querySelector ( '.herb-floating-menu' ) ;
195+ if ( menu ) {
196+ menu . prepend ( badge ) ;
197+ } else {
198+ badge . style . position = 'fixed' ;
199+ badge . style . top = '0' ;
200+ badge . style . right = '0' ;
201+ document . body . appendChild ( badge ) ;
202+ }
203+ }
204+
17205export class ErrorOverlay {
18206 private overlay : HTMLElement | null = null ;
19207 private allValidationData : ValidationData [ ] = [ ] ;
@@ -25,6 +213,7 @@ export class ErrorOverlay {
25213
26214 private init ( ) {
27215 this . detectValidationErrors ( ) ;
216+ scanForOptimizationMismatches ( ) ;
28217
29218 const hasParserErrors = document . querySelector ( '.herb-parser-error-overlay' ) !== null ;
30219
0 commit comments