reactionview 0.3.0 → 0.4.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.
@@ -1,148 +1,906 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReActionViewDevTools = {}));
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReActionViewDevTools = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- function styleInject(css, ref) {
8
- if ( ref === void 0 ) ref = {};
9
- var insertAt = ref.insertAt;
7
+ const DEFAULT_RECONNECT_INTERVAL = 1000;
8
+ const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
9
+ class Connection {
10
+ constructor(options) {
11
+ this.socket = null;
12
+ this.reconnectAttempts = 0;
13
+ this.reconnectTimer = null;
14
+ this.givenUp = false;
15
+ this.options = {
16
+ reconnectInterval: DEFAULT_RECONNECT_INTERVAL,
17
+ maxReconnectAttempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
18
+ ...options,
19
+ };
20
+ }
21
+ get reconnectInterval() {
22
+ return this.options.reconnectInterval ?? DEFAULT_RECONNECT_INTERVAL;
23
+ }
24
+ get maxReconnectAttempts() {
25
+ return this.options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
26
+ }
27
+ connect() {
28
+ if (this.socket?.readyState === WebSocket.OPEN)
29
+ return;
30
+ this.givenUp = false;
31
+ this.reconnectAttempts = 0;
32
+ this.attemptConnect();
33
+ }
34
+ disconnect() {
35
+ if (this.reconnectTimer) {
36
+ clearTimeout(this.reconnectTimer);
37
+ this.reconnectTimer = null;
38
+ }
39
+ this.givenUp = false;
40
+ this.reconnectAttempts = this.maxReconnectAttempts;
41
+ if (this.socket) {
42
+ this.socket.close();
43
+ this.socket = null;
44
+ }
45
+ }
46
+ retry() {
47
+ if (this.reconnectTimer) {
48
+ clearTimeout(this.reconnectTimer);
49
+ this.reconnectTimer = null;
50
+ }
51
+ if (this.socket) {
52
+ this.socket.onclose = null;
53
+ this.socket.close();
54
+ this.socket = null;
55
+ }
56
+ this.givenUp = false;
57
+ this.reconnectAttempts = 0;
58
+ this.attemptConnect();
59
+ }
60
+ get hasGivenUp() {
61
+ return this.givenUp;
62
+ }
63
+ attemptConnect() {
64
+ try {
65
+ this.socket = new WebSocket(this.options.url);
66
+ this.socket.onopen = () => {
67
+ this.reconnectAttempts = 0;
68
+ this.options.onConnect?.();
69
+ };
70
+ this.socket.onmessage = (event) => {
71
+ try {
72
+ const message = JSON.parse(event.data);
73
+ this.options.onMessage?.(message);
74
+ }
75
+ catch (error) {
76
+ console.warn("[herb-client] failed to parse message:", error);
77
+ }
78
+ };
79
+ this.socket.onclose = () => {
80
+ console.debug("[herb-client] disconnected from dev server");
81
+ this.options.onDisconnect?.();
82
+ this.scheduleReconnect();
83
+ };
84
+ this.socket.onerror = () => {
85
+ try {
86
+ this.socket?.close();
87
+ }
88
+ catch {
89
+ this.scheduleReconnect();
90
+ }
91
+ };
92
+ }
93
+ catch {
94
+ this.scheduleReconnect();
95
+ }
96
+ }
97
+ scheduleReconnect() {
98
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
99
+ console.debug("[herb-client] gave up reconnecting after %d attempts", this.reconnectAttempts);
100
+ this.givenUp = true;
101
+ this.options.onGivenUp?.();
102
+ return;
103
+ }
104
+ this.reconnectAttempts++;
105
+ const delay = Math.min(this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), 10000);
106
+ this.options.onReconnecting?.(this.reconnectAttempts, this.maxReconnectAttempts, delay);
107
+ this.reconnectTimer = setTimeout(() => {
108
+ this.attemptConnect();
109
+ }, delay);
110
+ }
111
+ }
112
+
113
+ const colors = {
114
+ green: "#22c55e",
115
+ greenDark: "#059669",
116
+ greenLight: "#ecfdf5",
117
+ greenBorder: "#10b981",
118
+ greenGlow: "0 0 4px rgba(34, 197, 94, 0.5)",
119
+ red: "#ef4444",
120
+ redDark: "#991b1b",
121
+ redLight: "#fef2f2",
122
+ amber: "#f59e0b",
123
+ amberDark: "#92400e",
124
+ amberDarker: "#d97706",
125
+ amberLight: "#fffbeb",
126
+ gray: "#6b7280",
127
+ grayLighter: "#a16207",
128
+ };
129
+
130
+ const TOAST_DURATION = 3000;
131
+ const TOAST_FADE_DURATION = 300;
132
+ const TOAST_ID = "herbDevServerToast";
133
+ const TOAST_STYLES = {
134
+ connected: { background: colors.greenLight, border: colors.greenBorder, text: "#065f46", icon: "\u{1F7E2}" },
135
+ disconnected: { background: colors.redLight, border: colors.red, text: colors.redDark, icon: "\u{1F534}" },
136
+ warning: { background: colors.amberLight, border: colors.amber, text: colors.amberDark, icon: "\u{1F7E1}" },
137
+ };
138
+ class Toast {
139
+ static show(message, type) {
140
+ document.getElementById(TOAST_ID)?.remove();
141
+ const style = TOAST_STYLES[type];
142
+ const toast = document.createElement("div");
143
+ toast.id = TOAST_ID;
144
+ toast.style.cssText = `position:fixed;top:36px;right:10px;z-index:999997;background:${style.background};border:1px solid ${style.border};border-radius:8px;padding:8px 14px;font-family:system-ui,sans-serif;font-size:12px;color:${style.text};box-shadow:0 4px 12px rgba(0,0,0,0.1);display:flex;align-items:center;gap:8px;transition:opacity 0.3s ease;`;
145
+ const icon = document.createElement("span");
146
+ icon.textContent = style.icon;
147
+ const text = document.createElement("span");
148
+ text.textContent = message;
149
+ toast.appendChild(icon);
150
+ toast.appendChild(text);
151
+ document.body.appendChild(toast);
152
+ setTimeout(() => {
153
+ toast.style.opacity = "0";
154
+ setTimeout(() => toast.remove(), TOAST_FADE_DURATION);
155
+ }, TOAST_DURATION);
156
+ }
157
+ }
158
+
159
+ class ConnectionDot {
160
+ constructor(client) {
161
+ this.reconnectCountdown = null;
162
+ this.client = client;
163
+ }
164
+ apply() {
165
+ if (this.reconnectCountdown) {
166
+ clearInterval(this.reconnectCountdown);
167
+ this.reconnectCountdown = null;
168
+ }
169
+ const dot = document.getElementById("herbConnectionDot");
170
+ if (!dot)
171
+ return;
172
+ const panelDot = document.getElementById("herbDevServerDot");
173
+ const panelStatus = document.getElementById("herbDevServerStatus");
174
+ const panelRetry = document.getElementById("herbDevServerRetry");
175
+ const retryHandler = (e) => { e.stopPropagation(); this.client.retry(); };
176
+ const state = this.client.getState();
177
+ switch (state) {
178
+ case "connected":
179
+ this.setDotStyle(dot, colors.green, true, true);
180
+ dot.style.cursor = "default";
181
+ dot.title = "Connected to herb dev server";
182
+ dot.onclick = null;
183
+ this.updatePanel(panelDot, panelStatus, panelRetry, {
184
+ dotColor: colors.green,
185
+ statusText: `Dev Server connected (port ${this.client.getPort()})`,
186
+ statusColor: colors.greenDark,
187
+ retryVisible: false,
188
+ });
189
+ break;
190
+ case "disconnected":
191
+ this.setDotStyle(dot, colors.red, false, false);
192
+ dot.style.cursor = "default";
193
+ dot.title = "Disconnected from herb dev server";
194
+ dot.onclick = null;
195
+ this.updatePanel(panelDot, panelStatus, panelRetry, {
196
+ dotColor: colors.red,
197
+ statusText: "Dev Server disconnected",
198
+ statusColor: colors.gray,
199
+ retryVisible: true,
200
+ retryHandler,
201
+ });
202
+ break;
203
+ case "given-up":
204
+ this.setDotStyle(dot, colors.amber, false, false);
205
+ dot.style.cursor = "pointer";
206
+ dot.title = "Connection to herb dev server failed — click to retry";
207
+ dot.onclick = retryHandler;
208
+ this.updatePanel(panelDot, panelStatus, panelRetry, {
209
+ dotColor: colors.amber,
210
+ statusText: "Dev Server not available",
211
+ statusColor: colors.amberDarker,
212
+ retryVisible: true,
213
+ retryHandler,
214
+ });
215
+ break;
216
+ }
217
+ }
218
+ updateReconnectCountdown(attempt, maxAttempts, delay) {
219
+ const panelStatus = document.getElementById("herbDevServerStatus");
220
+ if (!panelStatus)
221
+ return;
222
+ if (this.reconnectCountdown) {
223
+ clearInterval(this.reconnectCountdown);
224
+ this.reconnectCountdown = null;
225
+ }
226
+ let remaining = Math.ceil(delay / 1000);
227
+ panelStatus.textContent = `Retry ${attempt}/${maxAttempts} in ${remaining}s`;
228
+ panelStatus.style.color = colors.gray;
229
+ this.reconnectCountdown = setInterval(() => {
230
+ remaining--;
231
+ if (remaining <= 0) {
232
+ if (this.reconnectCountdown) {
233
+ clearInterval(this.reconnectCountdown);
234
+ this.reconnectCountdown = null;
235
+ }
236
+ panelStatus.textContent = `Retry ${attempt}/${maxAttempts} connecting...`;
237
+ return;
238
+ }
239
+ panelStatus.textContent = `Retry ${attempt}/${maxAttempts} in ${remaining}s`;
240
+ }, 1000);
241
+ }
242
+ updatePanel(panelDot, panelStatus, panelRetry, options) {
243
+ if (panelDot)
244
+ this.setDotStyle(panelDot, options.dotColor, false, false);
245
+ if (panelStatus) {
246
+ panelStatus.textContent = options.statusText;
247
+ panelStatus.style.color = options.statusColor;
248
+ }
249
+ if (panelRetry) {
250
+ panelRetry.style.display = options.retryVisible ? "block" : "none";
251
+ if (options.retryHandler) {
252
+ panelRetry.onclick = options.retryHandler;
253
+ }
254
+ }
255
+ }
256
+ setDotStyle(element, background, glow, pulse) {
257
+ element.style.background = background;
258
+ element.style.boxShadow = glow ? colors.greenGlow : "none";
259
+ element.style.animation = pulse ? "herb-dot-pulse 2s ease-in-out infinite" : "none";
260
+ }
261
+ }
262
+
263
+ const ALERT_ID = "herbProjectMismatchAlert";
264
+ class MismatchAlert {
265
+ static show(serverProject, clientProject) {
266
+ if (document.getElementById(ALERT_ID))
267
+ return;
268
+ const serverName = serverProject.split("/").pop() ?? serverProject;
269
+ const clientName = clientProject.split("/").pop() ?? clientProject;
270
+ const alert = document.createElement("div");
271
+ alert.id = ALERT_ID;
272
+ alert.style.cssText = `position:fixed;top:32px;right:10px;z-index:999998;background:${colors.amberLight};border:1px solid ${colors.amber};border-radius:8px;padding:12px 16px;max-width:320px;font-family:system-ui,sans-serif;font-size:13px;color:${colors.amberDark};box-shadow:0 4px 12px rgba(0,0,0,0.1);display:flex;gap:10px;align-items:flex-start;`;
273
+ const iconElement = document.createElement("span");
274
+ iconElement.style.cssText = "font-size:18px;line-height:1;";
275
+ iconElement.textContent = "\u26A0\uFE0F";
276
+ const content = document.createElement("div");
277
+ content.style.flex = "1";
278
+ const title = document.createElement("div");
279
+ title.style.cssText = "font-weight:600;margin-bottom:4px;";
280
+ title.textContent = "Herb Dev Server mismatch";
281
+ const description = document.createElement("div");
282
+ description.style.cssText = `font-size:12px;color:${colors.grayLighter};`;
283
+ description.textContent = `The dev server is watching ${serverName} but this page is from ${clientName}. Messages will be ignored.`;
284
+ content.appendChild(title);
285
+ content.appendChild(description);
286
+ const dismiss = document.createElement("button");
287
+ dismiss.style.cssText = `background:none;border:none;cursor:pointer;font-size:16px;color:${colors.amberDark};padding:0;line-height:1;`;
288
+ dismiss.textContent = "\u2715";
289
+ dismiss.addEventListener("click", () => alert.remove());
290
+ alert.appendChild(iconElement);
291
+ alert.appendChild(content);
292
+ alert.appendChild(dismiss);
293
+ document.body.appendChild(alert);
294
+ const panelStatus = document.getElementById("herbDevServerStatus");
295
+ const panelDot = document.getElementById("herbDevServerDot");
296
+ if (panelStatus) {
297
+ panelStatus.textContent = `Wrong project (${serverName})`;
298
+ panelStatus.style.color = colors.amberDarker;
299
+ }
300
+ if (panelDot) {
301
+ panelDot.style.background = colors.amber;
302
+ }
303
+ }
304
+ }
305
+
306
+ function applyPatch(message) {
307
+ const selector = `[data-herb-debug-file-relative-path="${message.file}"]`;
308
+ const roots = document.querySelectorAll(selector);
309
+ if (roots.length === 0) {
310
+ console.debug("[herb-client] no roots found for selector:", selector);
311
+ return false;
312
+ }
313
+ let applied = false;
314
+ for (const operation of message.operations) {
315
+ let operationApplied = false;
316
+ for (let i = 0; i < roots.length; i++) {
317
+ if (applyOperation(roots[i], operation)) {
318
+ operationApplied = true;
319
+ }
320
+ else {
321
+ console.debug(`[herb-client] operation not applied to root ${i}:`, roots[i]);
322
+ }
323
+ }
324
+ if (operationApplied) {
325
+ applied = true;
326
+ }
327
+ else {
328
+ console.debug("[herb-client] operation not applied:", operation);
329
+ }
330
+ }
331
+ return applied;
332
+ }
333
+ function applyOperation(root, operation) {
334
+ switch (operation.type) {
335
+ case "text_changed":
336
+ return applyTextChange(root, operation);
337
+ case "attribute_value_changed":
338
+ return applyAttributeChange(root, operation);
339
+ case "attribute_added":
340
+ return applyAttributeAdd(root, operation);
341
+ case "attribute_removed":
342
+ return applyAttributeRemove(root, operation);
343
+ default:
344
+ console.debug(`[herb-client] unhandled operation type: ${operation.type}`);
345
+ return false;
346
+ }
347
+ }
348
+ function parseAttribute(value) {
349
+ const match = value.match(/^([^=]+)="(.*)"$/);
350
+ if (!match)
351
+ return null;
352
+ return { name: match[1], value: match[2] };
353
+ }
354
+ function findTarget(root, operation) {
355
+ if (!operation.old_value)
356
+ return null;
357
+ const attribute = parseAttribute(operation.old_value);
358
+ if (!attribute)
359
+ return null;
360
+ if (root.getAttribute(attribute.name) === attribute.value)
361
+ return root;
362
+ const target = root.querySelector(`[${attribute.name}="${CSS.escape(attribute.value)}"]`);
363
+ return target;
364
+ }
365
+ function findTextTarget(root, operation) {
366
+ if (operation.old_value === null)
367
+ return null;
368
+ const trimmedOld = operation.old_value.trim();
369
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
370
+ let node;
371
+ while ((node = walker.nextNode())) {
372
+ if (node.textContent?.trim() === trimmedOld) {
373
+ return node;
374
+ }
375
+ }
376
+ return null;
377
+ }
378
+ function applyTextChange(root, operation) {
379
+ if (operation.new_value === null)
380
+ return false;
381
+ const textNode = findTextTarget(root, operation);
382
+ if (textNode) {
383
+ textNode.textContent = operation.new_value;
384
+ return true;
385
+ }
386
+ return false;
387
+ }
388
+ function applyAttributeChange(root, operation) {
389
+ if (operation.old_value === null || operation.new_value === null)
390
+ return false;
391
+ const node = findTarget(root, operation);
392
+ if (!node)
393
+ return false;
394
+ const newAttr = parseAttribute(operation.new_value);
395
+ if (!newAttr)
396
+ return false;
397
+ node.setAttribute(newAttr.name, newAttr.value);
398
+ return true;
399
+ }
400
+ function applyAttributeAdd(root, operation) {
401
+ if (operation.new_value === null)
402
+ return false;
403
+ const attribute = parseAttribute(operation.new_value);
404
+ if (!attribute)
405
+ return false;
406
+ const node = findTarget(root, operation) ?? root;
407
+ node.setAttribute(attribute.name, attribute.value);
408
+ return true;
409
+ }
410
+ function applyAttributeRemove(root, operation) {
411
+ if (operation.old_value === null)
412
+ return false;
413
+ const node = findTarget(root, operation);
414
+ if (!node)
415
+ return false;
416
+ const match = operation.old_value.match(/^([^=]+)(?:=".*")?$/);
417
+ if (!match)
418
+ return false;
419
+ node.removeAttribute(match[1]);
420
+ return true;
421
+ }
422
+
423
+ const DEFAULT_PORT = 8592;
424
+ class HerbClient {
425
+ constructor(options = {}) {
426
+ this.state = "disconnected";
427
+ this.hasConnectedBefore = false;
428
+ this.projectMatch = null;
429
+ this.options = options;
430
+ const port = options.port ?? this.detectPort() ?? DEFAULT_PORT;
431
+ const host = options.host ?? "localhost";
432
+ this.port = port;
433
+ this.connectionDot = new ConnectionDot(this);
434
+ this.connection = new Connection({
435
+ url: `ws://${host}:${port}`,
436
+ onMessage: (message) => this.handleMessage(message),
437
+ onConnect: () => this.onConnect(),
438
+ onDisconnect: () => this.onDisconnect(),
439
+ onReconnecting: (attempt, maxAttempts, delay) => this.onReconnecting(attempt, maxAttempts, delay),
440
+ onGivenUp: () => this.onGivenUp(),
441
+ });
442
+ }
443
+ connect() {
444
+ this.connection.connect();
445
+ }
446
+ disconnect() {
447
+ this.connection.disconnect();
448
+ }
449
+ retry() {
450
+ this.updateState("disconnected");
451
+ this.connection.retry();
452
+ }
453
+ getState() {
454
+ return this.state;
455
+ }
456
+ getPort() {
457
+ return this.port;
458
+ }
459
+ applyConnectionDot() {
460
+ this.connectionDot.apply();
461
+ }
462
+ onConnect() {
463
+ const wasDisconnected = this.state === "disconnected" || this.state === "given-up";
464
+ if (this.hasConnectedBefore && wasDisconnected) {
465
+ Toast.show("Herb Dev Server reconnected", "connected");
466
+ }
467
+ this.hasConnectedBefore = true;
468
+ this.updateState("connected");
469
+ this.options.onConnect?.();
470
+ }
471
+ onDisconnect() {
472
+ if (this.hasConnectedBefore && this.state === "connected") {
473
+ Toast.show("Herb Dev Server disconnected", "disconnected");
474
+ }
475
+ this.updateState("disconnected");
476
+ this.options.onDisconnect?.();
477
+ }
478
+ onReconnecting(attempt, maxAttempts, delay) {
479
+ console.debug(`[herb-client] reconnecting (attempt ${attempt}/${maxAttempts}, next try in ${(delay / 1000).toFixed(1)}s)...`);
480
+ this.connectionDot.updateReconnectCountdown(attempt, maxAttempts, delay);
481
+ }
482
+ onGivenUp() {
483
+ this.updateState("given-up");
484
+ Toast.show("Herb Dev Server not available — click the dot to retry", "warning");
485
+ }
486
+ handleMessage(message) {
487
+ if (message.type !== "welcome" && this.projectMatch === false)
488
+ return;
489
+ switch (message.type) {
490
+ case "welcome":
491
+ this.handleWelcome(message);
492
+ break;
493
+ case "patch":
494
+ this.handlePatch(message);
495
+ break;
496
+ case "reload":
497
+ this.handleReload(message);
498
+ break;
499
+ case "error":
500
+ this.handleError(message);
501
+ break;
502
+ case "fixed":
503
+ this.handleFixed(message);
504
+ break;
505
+ }
506
+ }
507
+ handleWelcome(message) {
508
+ const clientProject = document.querySelector('meta[name="herb-project-path"]')?.getAttribute("content");
509
+ if (clientProject && message.project && clientProject !== message.project) {
510
+ this.projectMatch = false;
511
+ console.warn(`[herb-client] project mismatch — server: ${message.project}, client: ${clientProject}. Ignoring messages.`);
512
+ this.updateState("disconnected");
513
+ MismatchAlert.show(message.project, clientProject);
514
+ }
515
+ else {
516
+ this.projectMatch = true;
517
+ }
518
+ }
519
+ handlePatch(message) {
520
+ this.options.onPatch?.(message);
521
+ const applied = applyPatch(message);
522
+ if (!applied) {
523
+ window.location.reload();
524
+ }
525
+ }
526
+ handleReload(message) {
527
+ this.options.onReload?.(message);
528
+ window.location.reload();
529
+ }
530
+ handleError(message) {
531
+ this.options.onError?.(message);
532
+ const overlay = this.getErrorOverlay();
533
+ if (overlay) {
534
+ const errors = message.errors.map((error) => ({
535
+ severity: "error",
536
+ message: error.message,
537
+ name: error.name,
538
+ location: { line: error.line, column: error.column },
539
+ }));
540
+ overlay.showErrors(errors, message.file);
541
+ }
542
+ }
543
+ handleFixed(message) {
544
+ this.options.onFixed?.(message);
545
+ this.getErrorOverlay()?.clearErrors();
546
+ }
547
+ updateState(state) {
548
+ this.state = state;
549
+ this.connectionDot.apply();
550
+ }
551
+ getErrorOverlay() {
552
+ const devTools = window.HerbDevTools;
553
+ return devTools?._errorOverlay ?? devTools?._overlay?.errorOverlay ?? null;
554
+ }
555
+ detectPort() {
556
+ const meta = document.querySelector('meta[name="herb-dev-server-port"]');
557
+ if (meta) {
558
+ const port = parseInt(meta.getAttribute("content") ?? "", 10);
559
+ if (!isNaN(port))
560
+ return port;
561
+ }
562
+ return null;
563
+ }
564
+ }
565
+
566
+ let instance = null;
567
+ function initHerbClient(options = {}) {
568
+ if (instance) {
569
+ instance.disconnect();
570
+ }
571
+ instance = new HerbClient(options);
572
+ window.__herbClient = instance;
573
+ instance.connect();
574
+ return instance;
575
+ }
576
+ function autoInitialize() {
577
+ const debugMeta = document.querySelector('meta[name="herb-debug-mode"]');
578
+ if (!debugMeta || debugMeta.getAttribute("content") !== "true")
579
+ return;
580
+ initHerbClient();
581
+ }
582
+ if (typeof document !== "undefined") {
583
+ if (document.readyState === "loading") {
584
+ document.addEventListener("DOMContentLoaded", autoInitialize);
585
+ }
586
+ else {
587
+ autoInitialize();
588
+ }
589
+ }
10
590
 
11
- if (typeof document === 'undefined') { return; }
591
+ function styleInject(css, ref) {
592
+ if ( ref === void 0 ) ref = {};
593
+ var insertAt = ref.insertAt;
12
594
 
13
- var head = document.head || document.getElementsByTagName('head')[0];
14
- var style = document.createElement('style');
15
- style.type = 'text/css';
595
+ if (typeof document === 'undefined') { return; }
16
596
 
17
- if (insertAt === 'top') {
18
- if (head.firstChild) {
19
- head.insertBefore(style, head.firstChild);
597
+ var head = document.head || document.getElementsByTagName('head')[0];
598
+ var style = document.createElement('style');
599
+ style.type = 'text/css';
600
+
601
+ if (insertAt === 'top') {
602
+ if (head.firstChild) {
603
+ head.insertBefore(style, head.firstChild);
604
+ } else {
605
+ head.appendChild(style);
606
+ }
20
607
  } else {
21
608
  head.appendChild(style);
22
609
  }
23
- } else {
24
- head.appendChild(style);
610
+
611
+ if (style.styleSheet) {
612
+ style.styleSheet.cssText = css;
613
+ } else {
614
+ style.appendChild(document.createTextNode(css));
615
+ }
25
616
  }
26
617
 
27
- if (style.styleSheet) {
28
- style.styleSheet.cssText = css;
29
- } else {
30
- style.appendChild(document.createTextNode(css));
618
+ 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-connection-dot{background:#d1d5db;border-radius:50%;display:block;height:8px;transition:background .3s ease,box-shadow .3s ease;width:8px}@keyframes herb-dot-pulse{0%,to{opacity:1}50%{opacity:.5}}.herb-dev-server-section{align-items:center;border-bottom:1px solid #e5e7eb;display:flex;font-size:11px;gap:8px;min-height:32px;padding:8px 20px}.herb-dev-server-dot{background:#d1d5db;border-radius:50%;flex-shrink:0;height:8px;transition:background .3s ease;width:8px}.herb-dev-server-status{color:#6b7280}.herb-dev-server-retry{background:#fff;border:1px solid #d1d5db;border-radius:4px;color:#374151;cursor:pointer;display:none;font-size:10px;margin-left:auto;padding:2px 8px;transition:background .15s ease,border-color .15s ease}.herb-dev-server-retry:hover{background:#f3f4f6;border-color:#9ca3af}.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-outline-preview{border:2px dotted transparent;border-radius:4px;padding:2px 8px}.herb-outline-view{background-color:#eff6ff;border-color:#3b82f6}.herb-outline-partial{background-color:#ecfdf5;border-color:#10b981}.herb-outline-component{background-color:#fffbeb;border-color:#f59e0b}.herb-outline-erb{background-color:#f5f3ff;border-color:#a78bfa}.herb-toggle-label:hover .herb-toggle-switch{background:#94a3b8}.herb-toggle-label:hover .herb-toggle-input:checked+.herb-toggle-switch{background:#7c3aed}.herb-editor-section{background:linear-gradient(135deg,#fafbfc,#f8f9fa);border-bottom:1px solid #f3f4f6;overflow:hidden;padding:16px 20px;position:relative}.herb-editor-section:before{background:linear-gradient(90deg,transparent,rgba(139,92,246,.2),transparent);content:\"\";height:2px;left:0;position:absolute;right:0;top:0}.herb-editor-label{cursor:default;display:flex;flex-direction:column;gap:10px}.herb-editor-text{align-items:center;color:#6b7280;display:flex;font-size:12px;font-weight:600;gap:6px;letter-spacing:.5px;text-transform:uppercase}.herb-editor-select{appearance:none;background:#fff;border:1.5px solid #e5e7eb;border-radius:8px;box-shadow:0 1px 2px rgba(0,0,0,.05);color:#1f2937;cursor:pointer;font-size:13.5px;font-weight:500;padding:10px 36px 10px 12px;transition:all .2s cubic-bezier(.4,0,.2,1);width:100%}.herb-editor-select,.herb-editor-select option{font-family:Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.herb-editor-select option{font-size:14px;font-weight:400;padding:8px 12px}.herb-editor-select:hover{background-color:#fafafa;border-color:#8b5cf6;box-shadow:0 2px 4px rgba(0,0,0,.08),0 0 0 1px rgba(139,92,246,.1);transform:translateY(-1px)}.herb-editor-select:focus{background-color:#fff;border-color:#8b5cf6;box-shadow:0 0 0 3px rgba(139,92,246,.15),0 2px 8px rgba(139,92,246,.1);outline:none;transform:translateY(-1px)}.herb-editor-select:active{box-shadow:0 1px 2px rgba(0,0,0,.05);transform:translateY(0)}.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}";
619
+ styleInject(css_248z);
620
+
621
+ const optimizationMismatches = new Set();
622
+ let optimizationBadgeInitialized = false;
623
+ function scanForOptimizationMismatches() {
624
+ const templates = document.querySelectorAll('template[data-herb-optimization-mismatch]');
625
+ templates.forEach((template) => {
626
+ optimizationMismatches.add(template.getAttribute('data-filename') || '(unknown)');
627
+ template.remove();
628
+ });
629
+ if (optimizationMismatches.size > 0) {
630
+ renderOptimizationBadge();
631
+ }
31
632
  }
32
- }
633
+ function renderOptimizationBadge() {
634
+ document.querySelector('.herb-optimization-badge')?.remove();
635
+ document.querySelector('.herb-optimization-panel')?.remove();
636
+ const filenames = Array.from(optimizationMismatches);
637
+ const projectPath = document.querySelector('meta[name="herb-project-path"]')?.getAttribute('content') || '';
638
+ const displayNames = filenames.map(f => projectPath && f.startsWith(projectPath) ? f.slice(projectPath.length).replace(/^\//, '') : f);
639
+ const title = `\u26A0\uFE0F ${filenames.length} Compile-Time Optimization Mismatch${filenames.length === 1 ? '' : 'es'}`;
640
+ if (!optimizationBadgeInitialized) {
641
+ optimizationBadgeInitialized = true;
642
+ const style = document.createElement('style');
643
+ style.className = 'herb-optimization-badge-style';
644
+ style.textContent = `
645
+ .herb-floating-menu {
646
+ display: flex;
647
+ flex-direction: row;
648
+ align-items: flex-start;
649
+ }
650
+
651
+ .herb-optimization-badge {
652
+ background: #fffbeb;
653
+ color: #92400e;
654
+ font-size: 11px;
655
+ font-weight: 600;
656
+ padding: 4px 7px;
657
+ border-radius: 0 0 0 10px;
658
+ border: 1px solid #f59e0b;
659
+ border-top: none;
660
+ border-right: none;
661
+ cursor: pointer;
662
+ text-align: center;
663
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
664
+ box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
665
+ z-index: 2147483640;
666
+ transition: all 0.2s ease;
667
+ order: -1;
668
+ }
669
+
670
+ .herb-optimization-badge:hover {
671
+ background: #fef3c7;
672
+ border-color: #d97706;
673
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
674
+ }
675
+
676
+ .herb-floating-menu .herb-optimization-badge + .herb-menu-trigger {
677
+ border-radius: 0 0 0 0;
678
+ }
679
+
680
+ .herb-optimization-panel {
681
+ position: fixed;
682
+ top: 30px;
683
+ right: 8px;
684
+ background: white;
685
+ border: 1px solid #e5e7eb;
686
+ border-radius: 8px;
687
+ width: 420px;
688
+ max-height: 400px;
689
+ overflow-y: auto;
690
+ z-index: 2147483642;
691
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
692
+ font-size: 12px;
693
+ color: #374151;
694
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
695
+ display: none;
696
+ }
33
697
 
34
- var css_248z = ".herb-overlay-label{background:rgba(0,0,0,.8);border-radius:3px;color:#fff;cursor:pointer;display:block;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,monospace;font-size:11px;font-weight:500;left:4px;line-height:1.2;padding:2px 6px;position:absolute;top:-18px;transition:all .2s ease;white-space:nowrap;z-index:1000}.herb-overlay-label:hover{background:rgba(0,0,0,.9);color:#374151;transform:scale(1.02);z-index:1001}[data-herb-debug-outline-type*=view]>.herb-overlay-label{background:#dbeafe;border-color:#93c5fd;color:#1e40af}[data-herb-debug-outline-type*=partial]>.herb-overlay-label{background:#d1fae5;border-color:#6ee7b7;color:#065f46}[data-herb-debug-outline-type*=component]>.herb-overlay-label{background:#fef3c7;border-color:#fcd34d;color:#92400e}[data-herb-debug-outline-type*=erb-output]{transition:all .3s ease}.herb-tooltip{background:#fff;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.12),0 2px 8px rgba(0,0,0,.08);display:flex;flex-direction:column;font-family:SF Mono,Monaco,Inconsolata,Fira Code,monospace;font-size:14px;gap:12px;max-width:calc(100vw - 16px);opacity:0;overflow:visible;padding:16px 20px;pointer-events:none;position:fixed;transition:opacity .2s ease,visibility .2s ease;visibility:hidden;white-space:nowrap;z-index:10001}.herb-tooltip.visible{opacity:1;pointer-events:auto;visibility:visible}.herb-tooltip .herb-location{align-items:center;background:#f8f9fa;border-radius:12px 12px 0 0;color:#6b7280;cursor:pointer;display:flex;font-size:13px;font-weight:500;gap:12px;justify-content:space-between;margin:-16px -20px 0;padding:12px 20px;transition:all .2s ease}.herb-tooltip .herb-location:hover{background:#f1f3f4;color:#374151}.herb-copy-path-btn{background:transparent;border:none;border-radius:4px;color:#6b7280;cursor:pointer;flex-shrink:0;font-size:14px;padding:4px;position:relative;transition:all .2s ease}.herb-copy-path-btn:hover{background:hsla(220,9%,46%,.1);color:#374151}.herb-copy-path-btn:active{transform:scale(.95)}.herb-location:after{background:#1f2937;border-radius:6px;bottom:calc(100% + 8px);color:#fff;content:attr(data-tooltip);font-size:12px;padding:6px 10px;pointer-events:none;white-space:nowrap}.herb-location:after,.herb-location:before{left:50%;opacity:0;position:absolute;transform:translateX(-50%);transition:all .2s ease;visibility:hidden;z-index:10002}.herb-location:before{border:4px solid transparent;border-top-color:#1f2937;bottom:calc(100% + 2px);content:\"\"}.herb-location:hover:after,.herb-location:hover:before{opacity:1;visibility:visible}.herb-location:has(.herb-copy-path-btn:hover):after,.herb-location:has(.herb-copy-path-btn:hover):before{opacity:0!important;visibility:hidden!important}.herb-copy-path-btn:after{background:#1f2937;border-radius:6px;color:#fff;content:attr(data-tooltip);font-size:12px;padding:6px 10px;pointer-events:none;top:-36px;white-space:nowrap}.herb-copy-path-btn:after,.herb-copy-path-btn:before{left:50%;opacity:0;position:absolute;transform:translateX(-50%);transition:all .2s ease;visibility:hidden;z-index:10003}.herb-copy-path-btn:before{border:4px solid transparent;border-bottom-color:#1f2937;content:\"\";top:-6px}.herb-copy-path-btn:hover:after,.herb-copy-path-btn:hover:before{opacity:1;visibility:visible}.herb-tooltip .herb-erb-code{color:#111827;cursor:text;font-size:16px;font-weight:600;letter-spacing:-.025em;user-select:text}.herb-tooltip:before{bottom:-8px;content:\"\";height:8px;left:0;pointer-events:auto;position:absolute;right:0}.herb-tooltip:after{border:6px solid transparent;border-top-color:#e5e7eb;bottom:-6px;content:\"\";left:50%;pointer-events:none;position:absolute;transform:translateX(-50%);z-index:10000}.herb-floating-menu{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;position:fixed;right:0;top:0;z-index:2147483643}.herb-menu-trigger{align-items:center;background:#fff;border:1px solid silver;border-radius:0 0 0 10px;border-right:none;border-top:none;box-shadow:0 1px 3px rgba(0,0,0,.1);cursor:pointer;display:flex;font-size:12px;gap:4px;justify-content:center;padding:4px 7px;position:relative;transition:all .2s ease;z-index:2147483640}.herb-menu-trigger:hover{background:#f9fafb;border-color:#9ca3af;box-shadow:0 4px 12px rgba(0,0,0,.15)}.herb-menu-trigger:active{transform:scale(.98)}.herb-menu-trigger.has-active-options{background:#dbeafe;border-color:#3b82f6}.herb-menu-trigger.has-active-options:hover{background:#bfdbfe;border-color:#2563eb}.herb-menu-trigger.has-active-options .herb-text{color:#1d4ed8}.herb-icon{display:block;font-size:14px;line-height:1}.herb-text{color:#555;font-size:11px;font-weight:600;letter-spacing:.2px}.herb-menu-panel{background:#fff;border:1px solid silver;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);min-width:280px;opacity:0;padding:0;position:absolute;right:10px;top:28px;transform:translateY(-10px) scale(.95);transform-origin:top right;transition:all .3s cubic-bezier(.4,0,.2,1);visibility:hidden}.herb-menu-panel.open{opacity:1;transform:translateY(0) scale(1);visibility:visible}.herb-menu-header{background:#f9fafb;border-bottom:1px solid #e5e7eb;border-radius:8px 8px 0 0;color:#374151;font-size:14px;font-weight:600;padding:16px 20px}.herb-toggle-item{border-bottom:1px solid #f3f4f6;padding:12px 20px}.herb-toggle-item:last-child{border-bottom:none;border-radius:0 0 8px 8px}.herb-nested-toggle{border-left:2px solid #f3f4f6;margin-top:8px;padding-left:24px;transition:all .3s ease}.herb-nested-label{opacity:.8}.herb-nested-label .herb-toggle-text{color:#6b7280;font-size:13px}.herb-nested-switch{background:#e5e7eb;height:20px;width:36px}.herb-nested-switch:after{height:14px;left:3px;top:3px;width:14px}.herb-toggle-input:checked+.herb-nested-switch:after{transform:translateX(16px)}.herb-toggle-label{align-items:center;cursor:pointer;display:flex;gap:12px;user-select:none}.herb-toggle-input{display:none}.herb-toggle-switch{background:#cbd5e1;border-radius:12px;flex-shrink:0;height:24px;position:relative;transition:background .3s ease;width:44px}.herb-toggle-switch:after{background:#fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2);content:\"\";height:18px;left:3px;position:absolute;top:3px;transition:transform .3s ease;width:18px}.herb-toggle-input:checked+.herb-toggle-switch{background:#8b5cf6}.herb-toggle-input:checked+.herb-toggle-switch:after{transform:translateX(20px)}.herb-toggle-text{color:#374151;flex:1;font-size:14px}.herb-outline-preview{border:2px dotted transparent;border-radius:4px;padding:2px 8px}.herb-outline-view{background-color:#eff6ff;border-color:#3b82f6}.herb-outline-partial{background-color:#ecfdf5;border-color:#10b981}.herb-outline-component{background-color:#fffbeb;border-color:#f59e0b}.herb-outline-erb{background-color:#f5f3ff;border-color:#a78bfa}.herb-toggle-label:hover .herb-toggle-switch{background:#94a3b8}.herb-toggle-label:hover .herb-toggle-input:checked+.herb-toggle-switch{background:#7c3aed}.herb-editor-section{background:linear-gradient(135deg,#fafbfc,#f8f9fa);border-bottom:1px solid #f3f4f6;overflow:hidden;padding:16px 20px;position:relative}.herb-editor-section:before{background:linear-gradient(90deg,transparent,rgba(139,92,246,.2),transparent);content:\"\";height:2px;left:0;position:absolute;right:0;top:0}.herb-editor-label{cursor:default;display:flex;flex-direction:column;gap:10px}.herb-editor-text{align-items:center;color:#6b7280;display:flex;font-size:12px;font-weight:600;gap:6px;letter-spacing:.5px;text-transform:uppercase}.herb-editor-select{appearance:none;background:#fff;border:1.5px solid #e5e7eb;border-radius:8px;box-shadow:0 1px 2px rgba(0,0,0,.05);color:#1f2937;cursor:pointer;font-size:13.5px;font-weight:500;padding:10px 36px 10px 12px;transition:all .2s cubic-bezier(.4,0,.2,1);width:100%}.herb-editor-select,.herb-editor-select option{font-family:Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.herb-editor-select option{font-size:14px;font-weight:400;padding:8px 12px}.herb-editor-select:hover{background-color:#fafafa;border-color:#8b5cf6;box-shadow:0 2px 4px rgba(0,0,0,.08),0 0 0 1px rgba(139,92,246,.1);transform:translateY(-1px)}.herb-editor-select:focus{background-color:#fff;border-color:#8b5cf6;box-shadow:0 0 0 3px rgba(139,92,246,.15),0 2px 8px rgba(139,92,246,.1);outline:none;transform:translateY(-1px)}.herb-editor-select:active{box-shadow:0 1px 2px rgba(0,0,0,.05);transform:translateY(0)}.herb-disable-all-section{background:#f9fafb;border-radius:0 0 8px 8px;border-top:1px solid #f3f4f6;padding:16px 20px}.herb-disable-all-btn{background:#ef4444;border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:13px;font-weight:500;padding:8px 16px;transition:background .2s ease;width:100%}.herb-disable-all-btn:hover{background:#dc2626}.herb-disable-all-btn:active{background:#b91c1c}.herb-validation-overlay{align-items:center;backdrop-filter:blur(4px);background:rgba(0,0,0,.8);bottom:0;color:#e5e5e5;display:flex;font-family:SF Mono,Monaco,Cascadia Code,Roboto Mono,Consolas,Courier New,monospace;justify-content:center;left:0;line-height:1.6;overflow-y:auto;padding:20px;position:fixed;right:0;top:0;z-index:2147483640}.herb-validation-panel{background:#000;border:1px solid #374151;border-radius:12px;box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04);display:flex;flex-direction:column;max-height:80vh;max-width:1200px;overflow:hidden;width:100%}.herb-validation-header{align-items:flex-start;background:linear-gradient(135deg,#dc2626,#b91c1c);border-bottom:1px solid #374151;border-radius:12px 12px 0 0;color:#fff;display:flex;flex-shrink:0;gap:16px;justify-content:space-between;padding:20px 24px}.herb-validation-title{font-size:18px;font-weight:600;margin:0}.herb-validation-close{align-items:center;background:hsla(0,0%,100%,.1);border:1px solid hsla(0,0%,100%,.2);border-radius:6px;color:#fff;cursor:pointer;display:flex;flex-shrink:0;font-size:16px;height:32px;justify-content:center;padding:0;transition:all .2s;width:32px}.herb-validation-close:hover{background:hsla(0,0%,100%,.2);border-color:hsla(0,0%,100%,.3)}.herb-file-tabs{background:#262626;border-bottom:1px solid #374151;display:flex;flex-shrink:0;overflow-x:auto}.herb-file-tab{background:none;border:none;border-bottom:3px solid transparent;color:#9ca3af;cursor:pointer;font-size:14px;font-weight:500;padding:12px 16px;transition:all .2s ease;white-space:nowrap}.herb-file-tab:hover{background:#2d2d2d;color:#e5e5e5}.herb-file-tab.active{background:#374151;border-bottom-color:#3b82f6;color:#fff}.herb-validation-content{flex:1;overflow-y:auto;padding:24px}.herb-validator-section{margin-bottom:32px}.herb-validator-section:last-child{margin-bottom:0}.herb-validator-section.hidden{display:none}.herb-validator-header{align-items:center;background:#262626;border-bottom:1px solid #374151;border-radius:8px 8px 0 0;color:#e5e5e5;display:flex;font-size:16px;font-weight:600;justify-content:space-between;padding:12px 16px}.herb-validator-count{background:hsla(0,0%,100%,.2);border-radius:12px;font-size:14px;font-weight:500;padding:2px 8px}.herb-validator-items{background:#111;border:1px solid #374151;border-radius:0 0 8px 8px;border-top:none}.herb-validation-item{background:#111;border-bottom:1px solid #374151;padding:20px}.herb-validation-item:last-child{border-bottom:none;border-radius:0 0 8px 8px}.herb-validation-item.hidden{display:none}.herb-validation-item .herb-validation-header{align-items:center;background:#1a1a1a;border:none;border-bottom:1px solid #374151;color:#9ca3af;display:flex;font-size:13px;gap:12px;margin:-20px -20px 16px;padding:12px 16px}.herb-validation-badge{border-radius:4px;color:#fff;font-size:12px;font-weight:600;letter-spacing:.025em;padding:4px 8px;text-transform:uppercase}.herb-validation-location{color:#9ca3af;font-family:SF Mono,Monaco,Inconsolata,Fira Code,monospace;font-size:13px}.herb-validation-message{background:#1a1a1a;border-bottom:1px solid #374151;color:#fbbf24;font-size:13px;font-weight:500;line-height:1.4;margin:-16px -16px 16px;padding:12px 16px}.herb-code-snippet{background:#1f2937;border-radius:6px;font-family:SF Mono,Monaco,Inconsolata,Fira Code,monospace;margin-bottom:16px;overflow:hidden}.herb-code-line{align-items:stretch;display:flex}.herb-code-line.herb-error-line{background:rgba(239,68,68,.1)}.herb-validation-overlay .herb-line-number{background:#374151;border-right:1px solid #4b5563;color:#9ca3af;flex-shrink:0;font-size:13px;padding:8px 12px;text-align:right;user-select:none;width:40px}.herb-validation-overlay .herb-error-line .herb-line-number{background:#dc2626;color:#fff}.herb-validation-overlay .herb-line-content{color:#e5e7eb;flex:1;font-size:13px;padding:8px 16px;white-space:pre-wrap}.herb-validation-overlay .herb-error-pointer{background:#1f2937;color:#dc2626;font-size:13px;font-weight:700;padding:4px 16px 8px 57px}.herb-validation-suggestion{align-items:flex-start;background:#111;border:1px solid #374151;border-radius:6px;color:#d1d5db;display:flex;font-size:14px;gap:8px;margin-top:16px;padding:12px 16px}.herb-suggestion-icon{color:#10b981;flex-shrink:0;font-size:16px;margin-top:1px}.herb-erb{color:#fbbf24;font-weight:600}.herb-erb-content{color:#34d399}.herb-tag{color:#60a5fa;font-weight:500}.herb-attr{color:#f472b6}.herb-value{color:#a78bfa}.herb-comment{color:#6b7280;font-style:italic}";
35
- styleInject(css_248z);
698
+ .herb-optimization-panel.visible {
699
+ display: block;
700
+ }
36
701
 
37
- class ErrorOverlay {
38
- constructor() {
39
- this.overlay = null;
40
- this.allValidationData = [];
41
- this.isVisible = false;
42
- this.init();
702
+ .herb-optimization-panel-header {
703
+ background: #fffbeb;
704
+ padding: 10px 14px;
705
+ color: #92400e;
706
+ font-weight: 600;
707
+ font-size: 13px;
708
+ display: flex;
709
+ justify-content: space-between;
710
+ align-items: center;
711
+ border-bottom: 1px solid #fde68a;
712
+ border-radius: 8px 8px 0 0;
43
713
  }
44
- init() {
45
- this.detectValidationErrors();
46
- const hasParserErrors = document.querySelector('.herb-parser-error-overlay') !== null;
47
- if (this.getTotalErrorCount() > 0) {
48
- this.createOverlay();
49
- this.setupToggleHandler();
50
- }
51
- else if (hasParserErrors) {
52
- console.log('[ErrorOverlay] Parser error overlay already displayed');
53
- }
54
- else {
55
- console.log('[ErrorOverlay] No errors found, not creating overlay');
56
- }
714
+
715
+ .herb-optimization-panel-close {
716
+ background: none;
717
+ border: none;
718
+ color: #92400e;
719
+ cursor: pointer;
720
+ font-size: 16px;
721
+ padding: 0 4px;
722
+ }
723
+
724
+ .herb-optimization-panel-close:hover {
725
+ color: #78350f;
57
726
  }
58
- detectValidationErrors() {
59
- const templatesToRemove = [];
60
- const validationTemplates = document.querySelectorAll('template[data-herb-validation-error]');
61
- if (validationTemplates.length > 0) {
62
- this.processValidationTemplates(validationTemplates, templatesToRemove);
63
- }
64
- const jsonTemplates = document.querySelectorAll('template[data-herb-validation-errors]');
65
- jsonTemplates.forEach((template, _index) => {
66
- try {
67
- let jsonData = template.textContent?.trim();
68
- if (!jsonData) {
69
- jsonData = template.innerHTML?.trim();
70
- }
71
- if (jsonData) {
72
- const validationData = JSON.parse(jsonData);
73
- this.allValidationData.push(validationData);
74
- templatesToRemove.push(template);
75
- }
76
- }
77
- catch (error) {
78
- console.error('Failed to parse validation errors from template:', error, {
79
- textContent: template.textContent,
80
- innerHTML: template.innerHTML
81
- });
82
- templatesToRemove.push(template);
83
- }
84
- });
85
- const htmlTemplates = document.querySelectorAll('template[data-herb-parser-error]');
86
- htmlTemplates.forEach((template, _index) => {
87
- try {
88
- const htmlContent = template.innerHTML?.trim() || template.textContent?.trim();
89
- if (htmlContent) {
90
- this.displayParserErrorOverlay(htmlContent);
91
- templatesToRemove.push(template);
92
- }
93
- }
94
- catch (error) {
95
- console.error('Failed to process parser error template:', error);
96
- templatesToRemove.push(template);
97
- }
98
- });
99
- templatesToRemove.forEach((template, _index) => template.remove());
727
+
728
+ .herb-optimization-panel-list {
729
+ padding: 4px 0;
100
730
  }
101
- processValidationTemplates(templates, templatesToRemove) {
102
- const validationFragments = [];
103
- const errorMap = new Map();
104
- templates.forEach((template) => {
105
- try {
106
- const metadata = {
107
- severity: template.getAttribute('data-severity') || 'error',
108
- source: template.getAttribute('data-source') || 'unknown',
109
- code: template.getAttribute('data-code') || '',
110
- line: parseInt(template.getAttribute('data-line') || '0'),
111
- column: parseInt(template.getAttribute('data-column') || '0'),
112
- filename: template.getAttribute('data-filename') || 'unknown',
113
- message: template.getAttribute('data-message') || '',
114
- suggestion: template.getAttribute('data-suggestion') || undefined,
115
- timestamp: template.getAttribute('data-timestamp') || new Date().toISOString()
116
- };
117
- const html = template.innerHTML?.trim() || '';
118
- if (html) {
119
- const errorKey = `${metadata.filename}:${metadata.line}:${metadata.column}:${metadata.code}:${metadata.message}`;
120
- if (errorMap.has(errorKey)) {
121
- const existing = errorMap.get(errorKey);
122
- existing.count++;
123
- }
124
- else {
125
- errorMap.set(errorKey, { metadata, html, count: 1 });
126
- }
127
- templatesToRemove.push(template);
128
- }
129
- }
130
- catch (error) {
131
- console.error('Failed to process validation template:', error);
132
- templatesToRemove.push(template);
133
- }
134
- });
135
- validationFragments.push(...errorMap.values());
136
- if (validationFragments.length > 0) {
137
- this.displayValidationOverlay(validationFragments);
138
- }
731
+
732
+ .herb-optimization-panel-item {
733
+ padding: 6px 14px;
734
+ color: #6b7280;
735
+ border-bottom: 1px solid #f3f4f6;
736
+ word-break: break-all;
737
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
738
+ font-size: 11px;
739
+ }
740
+
741
+ .herb-optimization-panel-item:last-child {
742
+ border-bottom: none;
743
+ }
744
+
745
+ .herb-optimization-panel-hint {
746
+ padding: 8px 14px;
747
+ color: #9ca3af;
748
+ font-size: 11px;
749
+ border-top: 1px solid #e5e7eb;
750
+ background: #f9fafb;
751
+ border-radius: 0 0 8px 8px;
139
752
  }
140
- createOverlay() {
141
- if (this.allValidationData.length === 0)
142
- return;
143
- this.overlay = document.createElement('div');
144
- this.overlay.id = 'herb-error-overlay';
145
- this.overlay.innerHTML = `
753
+ `;
754
+ document.head.appendChild(style);
755
+ }
756
+ const panel = document.createElement('div');
757
+ panel.className = 'herb-optimization-panel';
758
+ panel.innerHTML = `
759
+ <div class="herb-optimization-panel-header">
760
+ <span>${title}</span>
761
+ <button class="herb-optimization-panel-close">&times;</button>
762
+ </div>
763
+
764
+ <div class="herb-optimization-panel-list">
765
+ ${displayNames.map(f => `<div class="herb-optimization-panel-item">${f.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>`).join('')}
766
+ </div>
767
+
768
+ <div class="herb-optimization-panel-hint">
769
+ Check Rails log for details. Disable with <code>config.verify_optimizations = false</code>.
770
+ </div>
771
+ `;
772
+ document.body.appendChild(panel);
773
+ panel.querySelector('.herb-optimization-panel-close')?.addEventListener('click', () => {
774
+ panel.classList.remove('visible');
775
+ });
776
+ const badge = document.createElement('div');
777
+ badge.className = 'herb-optimization-badge';
778
+ badge.textContent = `\u26A0\uFE0F ${filenames.length}`;
779
+ badge.title = title;
780
+ badge.addEventListener('click', () => {
781
+ panel.classList.toggle('visible');
782
+ });
783
+ const menu = document.querySelector('.herb-floating-menu');
784
+ if (menu) {
785
+ menu.prepend(badge);
786
+ }
787
+ else {
788
+ badge.style.position = 'fixed';
789
+ badge.style.top = '0';
790
+ badge.style.right = '0';
791
+ document.body.appendChild(badge);
792
+ }
793
+ }
794
+ class ErrorOverlay {
795
+ constructor() {
796
+ this.overlay = null;
797
+ this.allValidationData = [];
798
+ this.isVisible = false;
799
+ this.init();
800
+ }
801
+ init() {
802
+ this.detectValidationErrors();
803
+ scanForOptimizationMismatches();
804
+ const hasParserErrors = document.querySelector('.herb-parser-error-overlay') !== null;
805
+ if (this.getTotalErrorCount() > 0) {
806
+ this.createOverlay();
807
+ this.setupToggleHandler();
808
+ }
809
+ else if (hasParserErrors) {
810
+ console.log('[ErrorOverlay] Parser error overlay already displayed');
811
+ }
812
+ else {
813
+ console.log('[ErrorOverlay] No errors found, not creating overlay');
814
+ }
815
+ }
816
+ detectValidationErrors() {
817
+ const templatesToRemove = [];
818
+ const validationTemplates = document.querySelectorAll('template[data-herb-validation-error]');
819
+ if (validationTemplates.length > 0) {
820
+ this.processValidationTemplates(validationTemplates, templatesToRemove);
821
+ }
822
+ const jsonTemplates = document.querySelectorAll('template[data-herb-validation-errors]');
823
+ jsonTemplates.forEach((template, _index) => {
824
+ try {
825
+ let jsonData = template.textContent?.trim();
826
+ if (!jsonData) {
827
+ jsonData = template.innerHTML?.trim();
828
+ }
829
+ if (jsonData) {
830
+ const validationData = JSON.parse(jsonData);
831
+ this.allValidationData.push(validationData);
832
+ templatesToRemove.push(template);
833
+ }
834
+ }
835
+ catch (error) {
836
+ console.error('Failed to parse validation errors from template:', error, {
837
+ textContent: template.textContent,
838
+ innerHTML: template.innerHTML
839
+ });
840
+ templatesToRemove.push(template);
841
+ }
842
+ });
843
+ const htmlTemplates = document.querySelectorAll('template[data-herb-parser-error]');
844
+ htmlTemplates.forEach((template, _index) => {
845
+ try {
846
+ const htmlContent = template.innerHTML?.trim() || template.textContent?.trim();
847
+ if (htmlContent) {
848
+ this.displayParserErrorOverlay(htmlContent);
849
+ templatesToRemove.push(template);
850
+ }
851
+ }
852
+ catch (error) {
853
+ console.error('Failed to process parser error template:', error);
854
+ templatesToRemove.push(template);
855
+ }
856
+ });
857
+ templatesToRemove.forEach((template, _index) => template.remove());
858
+ }
859
+ processValidationTemplates(templates, templatesToRemove) {
860
+ const validationFragments = [];
861
+ const errorMap = new Map();
862
+ templates.forEach((template) => {
863
+ try {
864
+ const metadata = {
865
+ severity: template.getAttribute('data-severity') || 'error',
866
+ source: template.getAttribute('data-source') || 'unknown',
867
+ code: template.getAttribute('data-code') || '',
868
+ line: parseInt(template.getAttribute('data-line') || '0'),
869
+ column: parseInt(template.getAttribute('data-column') || '0'),
870
+ filename: template.getAttribute('data-filename') || 'unknown',
871
+ message: template.getAttribute('data-message') || '',
872
+ suggestion: template.getAttribute('data-suggestion') || undefined,
873
+ timestamp: template.getAttribute('data-timestamp') || new Date().toISOString()
874
+ };
875
+ const html = template.innerHTML?.trim() || '';
876
+ if (html) {
877
+ const errorKey = `${metadata.filename}:${metadata.line}:${metadata.column}:${metadata.code}:${metadata.message}`;
878
+ if (errorMap.has(errorKey)) {
879
+ const existing = errorMap.get(errorKey);
880
+ existing.count++;
881
+ }
882
+ else {
883
+ errorMap.set(errorKey, { metadata, html, count: 1 });
884
+ }
885
+ templatesToRemove.push(template);
886
+ }
887
+ }
888
+ catch (error) {
889
+ console.error('Failed to process validation template:', error);
890
+ templatesToRemove.push(template);
891
+ }
892
+ });
893
+ validationFragments.push(...errorMap.values());
894
+ if (validationFragments.length > 0) {
895
+ this.displayValidationOverlay(validationFragments);
896
+ }
897
+ }
898
+ createOverlay() {
899
+ if (this.allValidationData.length === 0)
900
+ return;
901
+ this.overlay = document.createElement('div');
902
+ this.overlay.id = 'herb-error-overlay';
903
+ this.overlay.innerHTML = `
146
904
  <style>
147
905
  #herb-error-overlay {
148
906
  position: fixed;
@@ -279,168 +1037,190 @@
279
1037
  </div>
280
1038
  </div>
281
1039
  `;
282
- document.body.appendChild(this.overlay);
283
- const closeBtn = this.overlay.querySelector('.herb-error-close');
284
- closeBtn?.addEventListener('click', () => this.hide());
285
- this.overlay.addEventListener('click', (e) => {
286
- if (e.target === this.overlay) {
287
- this.hide();
288
- }
289
- });
290
- document.addEventListener('keydown', (e) => {
291
- if (e.key === 'Escape' && this.isVisible) {
292
- this.hide();
293
- }
294
- });
295
- }
296
- setupToggleHandler() {
297
- document.addEventListener('keydown', (e) => {
298
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'E') {
299
- e.preventDefault();
300
- this.toggle();
301
- }
302
- });
303
- if (this.hasErrorSeverity()) {
304
- setTimeout(() => this.show(), 100);
305
- }
306
- }
307
- getTotalErrorCount() {
308
- return this.allValidationData.reduce((total, data) => total + data.validationErrors.length, 0);
309
- }
310
- getErrorSummary(errors) {
311
- if (errors.length === 1) {
312
- return '1 error';
313
- }
314
- const errorsBySource = errors.reduce((acc, error) => {
315
- const source = error.source || 'Unknown';
316
- acc[source] = (acc[source] || 0) + 1;
317
- return acc;
318
- }, {});
319
- const sourceKeys = Object.keys(errorsBySource);
320
- if (sourceKeys.length === 1) {
321
- const source = sourceKeys[0];
322
- const count = errorsBySource[source];
323
- const sourceLabel = this.getSourceLabel(source);
324
- return `${count} ${sourceLabel} error${count === 1 ? '' : 's'}`;
325
- }
326
- else {
327
- const parts = sourceKeys.map(source => {
328
- const count = errorsBySource[source];
329
- const sourceLabel = this.getSourceLabel(source);
330
- return `${count} ${sourceLabel}`;
331
- });
332
- return `${errors.length} errors (${parts.join(', ')})`;
333
- }
334
- }
335
- getSourceLabel(source) {
336
- switch (source) {
337
- case 'Parser': return 'parser';
338
- case 'SecurityValidator': return 'security';
339
- case 'NestingValidator': return 'nesting';
340
- case 'AccessibilityValidator': return 'accessibility';
341
- default: return 'validation';
342
- }
343
- }
344
- hasErrorSeverity() {
345
- return this.allValidationData.some(data => data.validationErrors.some(error => error.severity === 'error'));
346
- }
347
- escapeHtml(unsafe) {
348
- return unsafe
349
- .replace(/&/g, '&amp;')
350
- .replace(/</g, '&lt;')
351
- .replace(/>/g, '&gt;')
352
- .replace(/"/g, '&quot;')
353
- .replace(/'/g, '&#039;');
354
- }
355
- show() {
356
- if (this.overlay) {
357
- this.overlay.style.display = 'block';
358
- this.isVisible = true;
359
- }
360
- }
361
- hide() {
362
- if (this.overlay) {
363
- this.overlay.style.display = 'none';
364
- this.isVisible = false;
365
- }
366
- }
367
- toggle() {
368
- if (this.isVisible) {
369
- this.hide();
370
- }
371
- else {
372
- this.show();
373
- }
374
- }
375
- hasErrors() {
376
- return this.getTotalErrorCount() > 0;
377
- }
378
- getErrorCount() {
379
- return this.getTotalErrorCount();
380
- }
381
- displayParserErrorOverlay(htmlContent) {
382
- const existingOverlay = document.querySelector('.herb-parser-error-overlay');
383
- if (existingOverlay) {
384
- existingOverlay.remove();
385
- }
386
- const container = document.createElement('div');
387
- container.innerHTML = htmlContent;
388
- const overlay = container.querySelector('.herb-parser-error-overlay');
389
- if (overlay) {
390
- document.body.appendChild(overlay);
391
- overlay.style.display = 'flex';
392
- }
393
- else {
394
- console.error('[ErrorOverlay] No parser error overlay found in HTML template');
395
- }
396
- }
397
- displayValidationOverlay(fragments) {
398
- const existingOverlay = document.querySelector('.herb-validation-overlay');
399
- if (existingOverlay) {
400
- existingOverlay.remove();
401
- }
402
- const errorsBySource = new Map();
403
- const errorsByFile = new Map();
404
- fragments.forEach(fragment => {
405
- const source = fragment.metadata.source;
406
- if (!errorsBySource.has(source)) {
407
- errorsBySource.set(source, []);
408
- }
409
- errorsBySource.get(source).push(fragment);
410
- const file = fragment.metadata.filename;
411
- if (!errorsByFile.has(file)) {
412
- errorsByFile.set(file, []);
413
- }
414
- errorsByFile.get(file).push(fragment);
415
- });
416
- const errorCount = fragments.filter(f => f.metadata.severity === 'error').reduce((sum, f) => sum + f.count, 0);
417
- const warningCount = fragments.filter(f => f.metadata.severity === 'warning').reduce((sum, f) => sum + f.count, 0);
418
- const totalCount = fragments.reduce((sum, f) => sum + f.count, 0);
419
- const uniqueCount = fragments.length;
420
- const overlayHTML = this.buildValidationOverlayHTML(fragments, errorsBySource, errorsByFile, { errorCount, warningCount, totalCount, uniqueCount });
421
- const overlay = document.createElement('div');
422
- overlay.className = 'herb-validation-overlay';
423
- overlay.innerHTML = overlayHTML;
424
- document.body.appendChild(overlay);
425
- this.setupValidationOverlayHandlers(overlay);
426
- }
427
- buildValidationOverlayHTML(_fragments, errorsBySource, errorsByFile, counts) {
428
- let title = counts.uniqueCount === 1 ? 'Validation Issue' : `Validation Issues`;
429
- if (counts.totalCount !== counts.uniqueCount) {
430
- title += ` (${counts.uniqueCount} unique, ${counts.totalCount} total)`;
431
- }
432
- else {
433
- title += ` (${counts.totalCount})`;
434
- }
435
- const subtitle = [];
436
- if (counts.errorCount > 0)
437
- subtitle.push(`${counts.errorCount} error${counts.errorCount !== 1 ? 's' : ''}`);
438
- if (counts.warningCount > 0)
439
- subtitle.push(`${counts.warningCount} warning${counts.warningCount !== 1 ? 's' : ''}`);
440
- let fileTabs = '';
441
- if (errorsByFile.size > 1) {
442
- const totalErrors = Array.from(errorsByFile.values()).reduce((sum, errors) => sum + errors.length, 0);
443
- fileTabs = `
1040
+ document.body.appendChild(this.overlay);
1041
+ const closeBtn = this.overlay.querySelector('.herb-error-close');
1042
+ closeBtn?.addEventListener('click', () => this.hide());
1043
+ this.overlay.addEventListener('click', (e) => {
1044
+ if (e.target === this.overlay) {
1045
+ this.hide();
1046
+ }
1047
+ });
1048
+ document.addEventListener('keydown', (e) => {
1049
+ if (e.key === 'Escape' && this.isVisible) {
1050
+ this.hide();
1051
+ }
1052
+ });
1053
+ }
1054
+ setupToggleHandler() {
1055
+ document.addEventListener('keydown', (e) => {
1056
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'E') {
1057
+ e.preventDefault();
1058
+ this.toggle();
1059
+ }
1060
+ });
1061
+ if (this.hasErrorSeverity()) {
1062
+ setTimeout(() => this.show(), 100);
1063
+ }
1064
+ }
1065
+ getTotalErrorCount() {
1066
+ return this.allValidationData.reduce((total, data) => total + data.validationErrors.length, 0);
1067
+ }
1068
+ getErrorSummary(errors) {
1069
+ if (errors.length === 1) {
1070
+ return '1 error';
1071
+ }
1072
+ const errorsBySource = errors.reduce((acc, error) => {
1073
+ const source = error.source || 'Unknown';
1074
+ acc[source] = (acc[source] || 0) + 1;
1075
+ return acc;
1076
+ }, {});
1077
+ const sourceKeys = Object.keys(errorsBySource);
1078
+ if (sourceKeys.length === 1) {
1079
+ const source = sourceKeys[0];
1080
+ const count = errorsBySource[source];
1081
+ const sourceLabel = this.getSourceLabel(source);
1082
+ return `${count} ${sourceLabel} error${count === 1 ? '' : 's'}`;
1083
+ }
1084
+ else {
1085
+ const parts = sourceKeys.map(source => {
1086
+ const count = errorsBySource[source];
1087
+ const sourceLabel = this.getSourceLabel(source);
1088
+ return `${count} ${sourceLabel}`;
1089
+ });
1090
+ return `${errors.length} errors (${parts.join(', ')})`;
1091
+ }
1092
+ }
1093
+ getSourceLabel(source) {
1094
+ switch (source) {
1095
+ case 'Parser': return 'parser';
1096
+ case 'SecurityValidator': return 'security';
1097
+ case 'NestingValidator': return 'nesting';
1098
+ case 'AccessibilityValidator': return 'accessibility';
1099
+ default: return 'validation';
1100
+ }
1101
+ }
1102
+ hasErrorSeverity() {
1103
+ return this.allValidationData.some(data => data.validationErrors.some(error => error.severity === 'error'));
1104
+ }
1105
+ escapeHtml(unsafe) {
1106
+ return unsafe
1107
+ .replace(/&/g, '&amp;')
1108
+ .replace(/</g, '&lt;')
1109
+ .replace(/>/g, '&gt;')
1110
+ .replace(/"/g, '&quot;')
1111
+ .replace(/'/g, '&#039;');
1112
+ }
1113
+ show() {
1114
+ if (this.overlay) {
1115
+ this.overlay.style.display = 'block';
1116
+ this.isVisible = true;
1117
+ }
1118
+ }
1119
+ hide() {
1120
+ if (this.overlay) {
1121
+ this.overlay.style.display = 'none';
1122
+ this.isVisible = false;
1123
+ }
1124
+ }
1125
+ toggle() {
1126
+ if (this.isVisible) {
1127
+ this.hide();
1128
+ }
1129
+ else {
1130
+ this.show();
1131
+ }
1132
+ }
1133
+ hasErrors() {
1134
+ return this.getTotalErrorCount() > 0;
1135
+ }
1136
+ getErrorCount() {
1137
+ return this.getTotalErrorCount();
1138
+ }
1139
+ showErrors(errors, filename) {
1140
+ this.allValidationData = this.allValidationData.filter(data => data.filename !== filename);
1141
+ this.allValidationData.push({
1142
+ validationErrors: errors,
1143
+ filename,
1144
+ timestamp: new Date().toISOString(),
1145
+ });
1146
+ if (this.overlay) {
1147
+ this.overlay.remove();
1148
+ this.overlay = null;
1149
+ }
1150
+ this.createOverlay();
1151
+ this.show();
1152
+ }
1153
+ clearErrors() {
1154
+ this.allValidationData = [];
1155
+ if (this.overlay) {
1156
+ this.overlay.remove();
1157
+ this.overlay = null;
1158
+ this.isVisible = false;
1159
+ }
1160
+ }
1161
+ displayParserErrorOverlay(htmlContent) {
1162
+ const existingOverlay = document.querySelector('.herb-parser-error-overlay');
1163
+ if (existingOverlay) {
1164
+ existingOverlay.remove();
1165
+ }
1166
+ const container = document.createElement('div');
1167
+ container.innerHTML = htmlContent;
1168
+ const overlay = container.querySelector('.herb-parser-error-overlay');
1169
+ if (overlay) {
1170
+ document.body.appendChild(overlay);
1171
+ overlay.style.display = 'flex';
1172
+ }
1173
+ else {
1174
+ console.error('[ErrorOverlay] No parser error overlay found in HTML template');
1175
+ }
1176
+ }
1177
+ displayValidationOverlay(fragments) {
1178
+ const existingOverlay = document.querySelector('.herb-validation-overlay');
1179
+ if (existingOverlay) {
1180
+ existingOverlay.remove();
1181
+ }
1182
+ const errorsBySource = new Map();
1183
+ const errorsByFile = new Map();
1184
+ fragments.forEach(fragment => {
1185
+ const source = fragment.metadata.source;
1186
+ if (!errorsBySource.has(source)) {
1187
+ errorsBySource.set(source, []);
1188
+ }
1189
+ errorsBySource.get(source).push(fragment);
1190
+ const file = fragment.metadata.filename;
1191
+ if (!errorsByFile.has(file)) {
1192
+ errorsByFile.set(file, []);
1193
+ }
1194
+ errorsByFile.get(file).push(fragment);
1195
+ });
1196
+ const errorCount = fragments.filter(f => f.metadata.severity === 'error').reduce((sum, f) => sum + f.count, 0);
1197
+ const warningCount = fragments.filter(f => f.metadata.severity === 'warning').reduce((sum, f) => sum + f.count, 0);
1198
+ const totalCount = fragments.reduce((sum, f) => sum + f.count, 0);
1199
+ const uniqueCount = fragments.length;
1200
+ const overlayHTML = this.buildValidationOverlayHTML(fragments, errorsBySource, errorsByFile, { errorCount, warningCount, totalCount, uniqueCount });
1201
+ const overlay = document.createElement('div');
1202
+ overlay.className = 'herb-validation-overlay';
1203
+ overlay.innerHTML = overlayHTML;
1204
+ document.body.appendChild(overlay);
1205
+ this.setupValidationOverlayHandlers(overlay);
1206
+ }
1207
+ buildValidationOverlayHTML(_fragments, errorsBySource, errorsByFile, counts) {
1208
+ let title = counts.uniqueCount === 1 ? 'Validation Issue' : `Validation Issues`;
1209
+ if (counts.totalCount !== counts.uniqueCount) {
1210
+ title += ` (${counts.uniqueCount} unique, ${counts.totalCount} total)`;
1211
+ }
1212
+ else {
1213
+ title += ` (${counts.totalCount})`;
1214
+ }
1215
+ const subtitle = [];
1216
+ if (counts.errorCount > 0)
1217
+ subtitle.push(`${counts.errorCount} error${counts.errorCount !== 1 ? 's' : ''}`);
1218
+ if (counts.warningCount > 0)
1219
+ subtitle.push(`${counts.warningCount} warning${counts.warningCount !== 1 ? 's' : ''}`);
1220
+ let fileTabs = '';
1221
+ if (errorsByFile.size > 1) {
1222
+ const totalErrors = Array.from(errorsByFile.values()).reduce((sum, errors) => sum + errors.length, 0);
1223
+ fileTabs = `
444
1224
  <div class="herb-file-tabs">
445
1225
  <button class="herb-file-tab active" data-file="*">
446
1226
  All (${totalErrors})
@@ -452,8 +1232,8 @@
452
1232
  `).join('')}
453
1233
  </div>
454
1234
  `;
455
- }
456
- const contentSections = Array.from(errorsBySource.entries()).map(([source, sourceFragments]) => `
1235
+ }
1236
+ const contentSections = Array.from(errorsBySource.entries()).map(([source, sourceFragments]) => `
457
1237
  <div class="herb-validator-section" data-source="${this.escapeAttr(source)}">
458
1238
  <div class="herb-validator-header">
459
1239
  <h3>${this.escapeHtml(source.replace('Validator', ''))} Issues (${sourceFragments.length})</h3>
@@ -477,7 +1257,7 @@
477
1257
  </div>
478
1258
  </div>
479
1259
  `).join('');
480
- return `
1260
+ return `
481
1261
  <style>${this.getValidationOverlayStyles()}</style>
482
1262
  <div class="herb-validation-container">
483
1263
  <div class="herb-validation-header">
@@ -501,16 +1281,16 @@
501
1281
  </div>
502
1282
  </div>
503
1283
  `;
504
- }
505
- getDismissHint() {
506
- const template = document.querySelector('template[data-herb-dismiss-hint]');
507
- if (template) {
508
- return template.innerHTML.trim();
509
- }
510
- return `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>.`;
511
- }
512
- getValidationOverlayStyles() {
513
- return `
1284
+ }
1285
+ getDismissHint() {
1286
+ const template = document.querySelector('template[data-herb-dismiss-hint]');
1287
+ if (template) {
1288
+ return template.innerHTML.trim();
1289
+ }
1290
+ return `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>.`;
1291
+ }
1292
+ getValidationOverlayStyles() {
1293
+ return `
514
1294
  .herb-validation-overlay {
515
1295
  position: fixed;
516
1296
  top: 0;
@@ -799,193 +1579,207 @@
799
1579
  .herb-value { color: #98c379; }
800
1580
  .herb-comment { color: #5c6370; font-style: italic; }
801
1581
  `;
802
- }
803
- setupValidationOverlayHandlers(overlay) {
804
- const closeBtn = overlay.querySelector('.herb-close-button');
805
- if (closeBtn) {
806
- closeBtn.addEventListener('click', () => overlay.remove());
807
- }
808
- overlay.addEventListener('click', (e) => {
809
- if (e.target === overlay) {
810
- overlay.remove();
811
- }
812
- });
813
- const escHandler = (e) => {
814
- if (e.key === 'Escape') {
815
- overlay.remove();
816
- document.removeEventListener('keydown', escHandler);
817
- }
818
- };
819
- document.addEventListener('keydown', escHandler);
820
- const fileTabs = overlay.querySelectorAll('.herb-file-tab');
821
- fileTabs.forEach(tab => {
822
- tab.addEventListener('click', () => {
823
- const selectedFile = tab.getAttribute('data-file');
824
- fileTabs.forEach(t => t.classList.remove('active'));
825
- tab.classList.add('active');
826
- const errorContainers = overlay.querySelectorAll('[data-error-file]');
827
- const validatorSections = overlay.querySelectorAll('.herb-validator-section');
828
- errorContainers.forEach(container => {
829
- const containerFile = container.getAttribute('data-error-file');
830
- if (selectedFile === '*' || containerFile === selectedFile) {
831
- container.classList.remove('hidden');
832
- }
833
- else {
834
- container.classList.add('hidden');
835
- }
836
- });
837
- validatorSections.forEach(section => {
838
- const sectionContent = section.querySelector('.herb-validator-content');
839
- const visibleErrors = sectionContent?.querySelectorAll('[data-error-file]:not(.hidden)').length || 0;
840
- const header = section.querySelector('h3');
841
- const source = section.getAttribute('data-source')?.replace('Validator', '') || 'Unknown';
842
- if (header) {
843
- header.textContent = `${source} Issues (${visibleErrors})`;
844
- }
845
- if (visibleErrors === 0) {
846
- section.classList.add('hidden');
847
- }
848
- else {
849
- section.classList.remove('hidden');
850
- }
851
- });
852
- });
853
- });
854
- }
855
- escapeAttr(text) {
856
- return this.escapeHtml(text).replace(/"/g, '&quot;');
857
- }
858
- }
859
-
860
- class HerbOverlay {
861
- constructor(options = {}) {
862
- this.options = options;
863
- this.showingERB = false;
864
- this.showingERBOutlines = false;
865
- this.showingERBHoverReveal = false;
866
- this.showingTooltips = true;
867
- this.showingViewOutlines = false;
868
- this.showingPartialOutlines = false;
869
- this.showingComponentOutlines = false;
870
- this.menuOpen = false;
871
- this.projectPath = '';
872
- this.preferredEditor = 'auto';
873
- this.defaultEditorFromServer = 'vscode';
874
- this.currentlyHoveredERBElement = null;
875
- this.errorOverlay = null;
876
- this.handleRevealedERBClick = (event) => {
877
- event.stopPropagation();
878
- event.preventDefault();
879
- const element = event.currentTarget;
880
- if (!element)
881
- return;
882
- const fullPath = element.getAttribute('data-herb-debug-file-full-path');
883
- const line = element.getAttribute('data-herb-debug-line');
884
- const column = element.getAttribute('data-herb-debug-column');
885
- if (fullPath) {
886
- this.openFileInEditor(fullPath, line ? parseInt(line) : 1, column ? parseInt(column) : 1);
887
- }
888
- };
889
- if (options.autoInit !== false) {
890
- this.init();
891
- }
892
- }
893
- init() {
894
- this.loadProjectPath();
895
- this.loadDefaultEditor();
896
- this.loadSettings();
897
- this.injectMenu();
898
- this.setupMenuToggle();
899
- this.setupToggleSwitches();
900
- this.setupEditorDropdown();
901
- this.initializeErrorOverlay();
902
- this.setupTurboListeners();
903
- this.applySettings();
904
- }
905
- loadProjectPath() {
906
- if (this.options.projectPath) {
907
- this.projectPath = this.options.projectPath;
908
- return;
909
- }
910
- const metaTag = document.querySelector('meta[name="herb-project-path"]');
911
- if (metaTag?.content) {
912
- this.projectPath = metaTag.content;
913
- }
914
- }
915
- loadDefaultEditor() {
916
- const metaTag = document.querySelector('meta[name="herb-default-editor"]');
917
- if (metaTag?.content) {
918
- const defaultEditor = metaTag.content.toLowerCase();
919
- const isValidEditor = HerbOverlay.EDITOR_OPTIONS.some(option => option.value === defaultEditor);
920
- if (isValidEditor) {
921
- this.defaultEditorFromServer = defaultEditor;
922
- }
923
- }
924
- }
925
- loadSettings() {
926
- const savedSettings = localStorage.getItem(HerbOverlay.SETTINGS_KEY);
927
- if (savedSettings) {
928
- try {
929
- const settings = JSON.parse(savedSettings);
930
- this.showingERB = settings.showingERB || false;
931
- this.showingERBOutlines = settings.showingERBOutlines || false;
932
- this.showingERBHoverReveal = settings.showingERBHoverReveal || false;
933
- this.showingTooltips = settings.showingTooltips !== undefined ? settings.showingTooltips : true;
934
- this.showingViewOutlines = settings.showingViewOutlines || false;
935
- this.showingPartialOutlines = settings.showingPartialOutlines || false;
936
- this.showingComponentOutlines = settings.showingComponentOutlines || false;
937
- this.menuOpen = settings.menuOpen || false;
938
- if (settings.preferredEditor) {
939
- this.preferredEditor = settings.preferredEditor;
940
- }
941
- }
942
- catch (e) {
943
- console.warn('Failed to load Herb dev tools settings:', e);
944
- }
945
- }
946
- }
947
- saveSettings() {
948
- const settings = {
949
- showingERB: this.showingERB,
950
- showingERBOutlines: this.showingERBOutlines,
951
- showingERBHoverReveal: this.showingERBHoverReveal,
952
- showingTooltips: this.showingTooltips,
953
- showingViewOutlines: this.showingViewOutlines,
954
- showingPartialOutlines: this.showingPartialOutlines,
955
- showingComponentOutlines: this.showingComponentOutlines,
956
- menuOpen: this.menuOpen,
957
- preferredEditor: this.preferredEditor
958
- };
959
- localStorage.setItem(HerbOverlay.SETTINGS_KEY, JSON.stringify(settings));
960
- this.updateMenuButtonState();
961
- }
962
- updateMenuButtonState() {
963
- const menuTrigger = document.getElementById('herbMenuTrigger');
964
- if (menuTrigger) {
965
- const hasActiveOptions = this.showingERB || this.showingERBOutlines || this.showingViewOutlines || this.showingPartialOutlines || this.showingComponentOutlines;
966
- if (hasActiveOptions) {
967
- menuTrigger.classList.add('has-active-options');
968
- }
969
- else {
970
- menuTrigger.classList.remove('has-active-options');
971
- }
972
- }
973
- }
974
- injectMenu() {
975
- const existingMenu = document.querySelector('.herb-floating-menu');
976
- if (existingMenu) {
977
- return;
978
- }
979
- const menuHTML = `
1582
+ }
1583
+ setupValidationOverlayHandlers(overlay) {
1584
+ const closeBtn = overlay.querySelector('.herb-close-button');
1585
+ if (closeBtn) {
1586
+ closeBtn.addEventListener('click', () => overlay.remove());
1587
+ }
1588
+ overlay.addEventListener('click', (e) => {
1589
+ if (e.target === overlay) {
1590
+ overlay.remove();
1591
+ }
1592
+ });
1593
+ const escHandler = (e) => {
1594
+ if (e.key === 'Escape') {
1595
+ overlay.remove();
1596
+ document.removeEventListener('keydown', escHandler);
1597
+ }
1598
+ };
1599
+ document.addEventListener('keydown', escHandler);
1600
+ const fileTabs = overlay.querySelectorAll('.herb-file-tab');
1601
+ fileTabs.forEach(tab => {
1602
+ tab.addEventListener('click', () => {
1603
+ const selectedFile = tab.getAttribute('data-file');
1604
+ fileTabs.forEach(t => t.classList.remove('active'));
1605
+ tab.classList.add('active');
1606
+ const errorContainers = overlay.querySelectorAll('[data-error-file]');
1607
+ const validatorSections = overlay.querySelectorAll('.herb-validator-section');
1608
+ errorContainers.forEach(container => {
1609
+ const containerFile = container.getAttribute('data-error-file');
1610
+ if (selectedFile === '*' || containerFile === selectedFile) {
1611
+ container.classList.remove('hidden');
1612
+ }
1613
+ else {
1614
+ container.classList.add('hidden');
1615
+ }
1616
+ });
1617
+ validatorSections.forEach(section => {
1618
+ const sectionContent = section.querySelector('.herb-validator-content');
1619
+ const visibleErrors = sectionContent?.querySelectorAll('[data-error-file]:not(.hidden)').length || 0;
1620
+ const header = section.querySelector('h3');
1621
+ const source = section.getAttribute('data-source')?.replace('Validator', '') || 'Unknown';
1622
+ if (header) {
1623
+ header.textContent = `${source} Issues (${visibleErrors})`;
1624
+ }
1625
+ if (visibleErrors === 0) {
1626
+ section.classList.add('hidden');
1627
+ }
1628
+ else {
1629
+ section.classList.remove('hidden');
1630
+ }
1631
+ });
1632
+ });
1633
+ });
1634
+ }
1635
+ escapeAttr(text) {
1636
+ return this.escapeHtml(text).replace(/"/g, '&quot;');
1637
+ }
1638
+ }
1639
+
1640
+ class HerbOverlay {
1641
+ constructor(options = {}) {
1642
+ this.options = options;
1643
+ this.showingERB = false;
1644
+ this.showingERBOutlines = false;
1645
+ this.showingERBHoverReveal = false;
1646
+ this.showingTooltips = true;
1647
+ this.showingViewOutlines = false;
1648
+ this.showingPartialOutlines = false;
1649
+ this.showingComponentOutlines = false;
1650
+ this.menuOpen = false;
1651
+ this.projectPath = '';
1652
+ this.preferredEditor = 'auto';
1653
+ this.defaultEditorFromServer = 'vscode';
1654
+ this.currentlyHoveredERBElement = null;
1655
+ this.errorOverlay = null;
1656
+ this.handleRevealedERBClick = (event) => {
1657
+ event.stopPropagation();
1658
+ event.preventDefault();
1659
+ const element = event.currentTarget;
1660
+ if (!element)
1661
+ return;
1662
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path');
1663
+ const line = element.getAttribute('data-herb-debug-line');
1664
+ const column = element.getAttribute('data-herb-debug-column');
1665
+ if (fullPath) {
1666
+ this.openFileInEditor(fullPath, line ? parseInt(line) : 1, column ? parseInt(column) : 1);
1667
+ }
1668
+ };
1669
+ if (options.autoInit !== false) {
1670
+ this.init();
1671
+ }
1672
+ }
1673
+ syncConnectionDot() {
1674
+ const herbClient = window.__herbClient;
1675
+ if (herbClient) {
1676
+ herbClient.applyConnectionDot();
1677
+ }
1678
+ }
1679
+ init() {
1680
+ this.loadProjectPath();
1681
+ this.loadDefaultEditor();
1682
+ this.loadSettings();
1683
+ this.injectMenu();
1684
+ this.syncConnectionDot();
1685
+ this.setupMenuToggle();
1686
+ this.setupToggleSwitches();
1687
+ this.setupEditorDropdown();
1688
+ this.initializeErrorOverlay();
1689
+ this.setupTurboListeners();
1690
+ this.applySettings();
1691
+ }
1692
+ loadProjectPath() {
1693
+ if (this.options.projectPath) {
1694
+ this.projectPath = this.options.projectPath;
1695
+ return;
1696
+ }
1697
+ const metaTag = document.querySelector('meta[name="herb-project-path"]');
1698
+ if (metaTag?.content) {
1699
+ this.projectPath = metaTag.content;
1700
+ }
1701
+ }
1702
+ loadDefaultEditor() {
1703
+ const metaTag = document.querySelector('meta[name="herb-default-editor"]');
1704
+ if (metaTag?.content) {
1705
+ const defaultEditor = metaTag.content.toLowerCase();
1706
+ const isValidEditor = HerbOverlay.EDITOR_OPTIONS.some(option => option.value === defaultEditor);
1707
+ if (isValidEditor) {
1708
+ this.defaultEditorFromServer = defaultEditor;
1709
+ }
1710
+ }
1711
+ }
1712
+ loadSettings() {
1713
+ const savedSettings = localStorage.getItem(HerbOverlay.SETTINGS_KEY);
1714
+ if (savedSettings) {
1715
+ try {
1716
+ const settings = JSON.parse(savedSettings);
1717
+ this.showingERB = settings.showingERB || false;
1718
+ this.showingERBOutlines = settings.showingERBOutlines || false;
1719
+ this.showingERBHoverReveal = settings.showingERBHoverReveal || false;
1720
+ this.showingTooltips = settings.showingTooltips !== undefined ? settings.showingTooltips : true;
1721
+ this.showingViewOutlines = settings.showingViewOutlines || false;
1722
+ this.showingPartialOutlines = settings.showingPartialOutlines || false;
1723
+ this.showingComponentOutlines = settings.showingComponentOutlines || false;
1724
+ this.menuOpen = settings.menuOpen || false;
1725
+ if (settings.preferredEditor) {
1726
+ this.preferredEditor = settings.preferredEditor;
1727
+ }
1728
+ }
1729
+ catch (e) {
1730
+ console.warn('Failed to load Herb dev tools settings:', e);
1731
+ }
1732
+ }
1733
+ }
1734
+ saveSettings() {
1735
+ const settings = {
1736
+ showingERB: this.showingERB,
1737
+ showingERBOutlines: this.showingERBOutlines,
1738
+ showingERBHoverReveal: this.showingERBHoverReveal,
1739
+ showingTooltips: this.showingTooltips,
1740
+ showingViewOutlines: this.showingViewOutlines,
1741
+ showingPartialOutlines: this.showingPartialOutlines,
1742
+ showingComponentOutlines: this.showingComponentOutlines,
1743
+ menuOpen: this.menuOpen,
1744
+ preferredEditor: this.preferredEditor
1745
+ };
1746
+ localStorage.setItem(HerbOverlay.SETTINGS_KEY, JSON.stringify(settings));
1747
+ this.updateMenuButtonState();
1748
+ }
1749
+ updateMenuButtonState() {
1750
+ const menuTrigger = document.getElementById('herbMenuTrigger');
1751
+ if (menuTrigger) {
1752
+ const hasActiveOptions = this.showingERB || this.showingERBOutlines || this.showingViewOutlines || this.showingPartialOutlines || this.showingComponentOutlines;
1753
+ if (hasActiveOptions) {
1754
+ menuTrigger.classList.add('has-active-options');
1755
+ }
1756
+ else {
1757
+ menuTrigger.classList.remove('has-active-options');
1758
+ }
1759
+ }
1760
+ }
1761
+ injectMenu() {
1762
+ const existingMenu = document.querySelector('.herb-floating-menu');
1763
+ if (existingMenu) {
1764
+ return;
1765
+ }
1766
+ const menuHTML = `
980
1767
  <div class="herb-floating-menu">
981
1768
  <button class="herb-menu-trigger" id="herbMenuTrigger">
982
1769
  <span class="herb-icon">🌿</span>
983
1770
  <span class="herb-text">Herb</span>
1771
+ <span id="herbConnectionDot" class="herb-connection-dot" data-herb-connection-dot></span>
984
1772
  </button>
985
1773
 
986
1774
  <div class="herb-menu-panel" id="herbMenuPanel">
987
1775
  <div class="herb-menu-header">Herb Debug Tools</div>
988
1776
 
1777
+ <div id="herbDevServerSection" class="herb-dev-server-section">
1778
+ <span id="herbDevServerDot" class="herb-dev-server-dot"></span>
1779
+ <span id="herbDevServerStatus" class="herb-dev-server-status">Dev Server</span>
1780
+ <button id="herbDevServerRetry" class="herb-dev-server-retry">Retry</button>
1781
+ </div>
1782
+
989
1783
  <div class="herb-toggle-item">
990
1784
  <label class="herb-toggle-label">
991
1785
  <input type="checkbox" id="herbToggleViewOutlines" class="herb-toggle-input">
@@ -1057,858 +1851,862 @@
1057
1851
  </div>
1058
1852
  </div>
1059
1853
  `;
1060
- document.body.insertAdjacentHTML('beforeend', menuHTML);
1061
- }
1062
- applySettings() {
1063
- this.toggleViewOutlines(this.showingViewOutlines);
1064
- this.togglePartialOutlines(this.showingPartialOutlines);
1065
- this.toggleComponentOutlines(this.showingComponentOutlines);
1066
- this.toggleERBTags(this.showingERB);
1067
- this.toggleERBOutlines(this.showingERBOutlines);
1068
- const menuTrigger = document.getElementById('herbMenuTrigger');
1069
- const menuPanel = document.getElementById('herbMenuPanel');
1070
- if (menuTrigger && menuPanel && this.menuOpen) {
1071
- menuTrigger.classList.add('active');
1072
- menuPanel.classList.add('open');
1073
- }
1074
- }
1075
- setupMenuToggle() {
1076
- const menuTrigger = document.getElementById('herbMenuTrigger');
1077
- const menuPanel = document.getElementById('herbMenuPanel');
1078
- if (menuTrigger && menuPanel) {
1079
- menuTrigger.addEventListener('click', () => {
1080
- this.menuOpen = !this.menuOpen;
1081
- if (this.menuOpen) {
1082
- menuTrigger.classList.add('active');
1083
- menuPanel.classList.add('open');
1084
- }
1085
- else {
1086
- menuTrigger.classList.remove('active');
1087
- menuPanel.classList.remove('open');
1088
- }
1089
- this.saveSettings();
1090
- });
1091
- document.addEventListener('click', (e) => {
1092
- const target = e.target;
1093
- const floatingMenu = document.querySelector('.herb-floating-menu');
1094
- if (floatingMenu && !floatingMenu.contains(target) && this.menuOpen) {
1095
- this.menuOpen = false;
1096
- menuTrigger.classList.remove('active');
1097
- menuPanel.classList.remove('open');
1098
- this.saveSettings();
1099
- }
1100
- });
1101
- }
1102
- }
1103
- setupTurboListeners() {
1104
- document.addEventListener('turbo:load', () => {
1105
- this.reinitializeAfterNavigation();
1106
- });
1107
- document.addEventListener('turbo:render', () => {
1108
- this.reinitializeAfterNavigation();
1109
- });
1110
- document.addEventListener('turbo:visit', () => {
1111
- this.reinitializeAfterNavigation();
1112
- });
1113
- }
1114
- reinitializeAfterNavigation() {
1115
- this.injectMenu();
1116
- this.setupMenuToggle();
1117
- this.setupToggleSwitches();
1118
- this.setupEditorDropdown();
1119
- this.applySettings();
1120
- this.updateMenuButtonState();
1121
- }
1122
- setupToggleSwitches() {
1123
- const toggleViewOutlinesSwitch = document.getElementById('herbToggleViewOutlines');
1124
- if (toggleViewOutlinesSwitch) {
1125
- toggleViewOutlinesSwitch.checked = this.showingViewOutlines;
1126
- toggleViewOutlinesSwitch.addEventListener('change', () => {
1127
- this.toggleViewOutlines(toggleViewOutlinesSwitch.checked);
1128
- });
1129
- }
1130
- const togglePartialOutlinesSwitch = document.getElementById('herbTogglePartialOutlines');
1131
- if (togglePartialOutlinesSwitch) {
1132
- togglePartialOutlinesSwitch.checked = this.showingPartialOutlines;
1133
- togglePartialOutlinesSwitch.addEventListener('change', () => {
1134
- this.togglePartialOutlines(togglePartialOutlinesSwitch.checked);
1135
- });
1136
- }
1137
- const toggleComponentOutlinesSwitch = document.getElementById('herbToggleComponentOutlines');
1138
- if (toggleComponentOutlinesSwitch) {
1139
- toggleComponentOutlinesSwitch.checked = this.showingComponentOutlines;
1140
- toggleComponentOutlinesSwitch.addEventListener('change', () => {
1141
- this.toggleComponentOutlines(toggleComponentOutlinesSwitch.checked);
1142
- });
1143
- }
1144
- const toggleERBSwitch = document.getElementById('herbToggleERB');
1145
- const toggleERBOutlinesSwitch = document.getElementById('herbToggleERBOutlines');
1146
- if (toggleERBSwitch) {
1147
- toggleERBSwitch.checked = this.showingERB;
1148
- toggleERBSwitch.addEventListener('change', () => {
1149
- if (toggleERBSwitch.checked && toggleERBOutlinesSwitch) {
1150
- toggleERBOutlinesSwitch.checked = false;
1151
- this.toggleERBOutlines(false);
1152
- }
1153
- this.toggleERBTags(toggleERBSwitch.checked);
1154
- });
1155
- }
1156
- if (toggleERBOutlinesSwitch) {
1157
- toggleERBOutlinesSwitch.checked = this.showingERBOutlines;
1158
- toggleERBOutlinesSwitch.addEventListener('change', () => {
1159
- if (toggleERBOutlinesSwitch.checked && toggleERBSwitch) {
1160
- toggleERBSwitch.checked = false;
1161
- this.toggleERBTags(false);
1162
- }
1163
- this.toggleERBOutlines(toggleERBOutlinesSwitch.checked);
1164
- this.updateNestedToggleVisibility();
1165
- });
1166
- }
1167
- else {
1168
- console.warn('ERB outlines toggle switch not found');
1169
- }
1170
- const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
1171
- if (toggleERBHoverRevealSwitch) {
1172
- toggleERBHoverRevealSwitch.checked = this.showingERBHoverReveal;
1173
- toggleERBHoverRevealSwitch.addEventListener('change', () => {
1174
- this.toggleERBHoverReveal(toggleERBHoverRevealSwitch.checked);
1175
- });
1176
- }
1177
- const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
1178
- if (toggleTooltipsSwitch) {
1179
- toggleTooltipsSwitch.checked = this.showingTooltips;
1180
- toggleTooltipsSwitch.addEventListener('change', () => {
1181
- this.toggleTooltips(toggleTooltipsSwitch.checked);
1182
- });
1183
- }
1184
- this.updateNestedToggleVisibility();
1185
- const disableAllBtn = document.getElementById('herbDisableAll');
1186
- if (disableAllBtn) {
1187
- disableAllBtn.addEventListener('click', () => {
1188
- this.disableAll();
1189
- });
1190
- }
1191
- }
1192
- setupEditorDropdown() {
1193
- const editorSelect = document.getElementById('herbEditorSelect');
1194
- if (editorSelect) {
1195
- const autoOption = editorSelect.querySelector('option[value="auto"]');
1196
- if (autoOption) {
1197
- const editorLabel = HerbOverlay.EDITOR_OPTIONS.find(opt => opt.value === this.defaultEditorFromServer)?.label || this.defaultEditorFromServer;
1198
- const metaTag = document.querySelector('meta[name="herb-default-editor"]');
1199
- if (metaTag?.content) {
1200
- autoOption.textContent = `Auto (from server): ${editorLabel}`;
1201
- }
1202
- else {
1203
- autoOption.textContent = `Auto (default): ${editorLabel}`;
1204
- }
1205
- }
1206
- editorSelect.value = this.preferredEditor;
1207
- editorSelect.addEventListener('change', () => {
1208
- this.preferredEditor = editorSelect.value;
1209
- this.saveSettings();
1210
- });
1211
- }
1212
- }
1213
- toggleViewOutlines(show) {
1214
- this.showingViewOutlines = show !== undefined ? show : !this.showingViewOutlines;
1215
- const viewOutlines = document.querySelectorAll('[data-herb-debug-outline-type="view"], [data-herb-debug-outline-type*="view"]');
1216
- viewOutlines.forEach((outline) => {
1217
- const element = outline;
1218
- if (this.showingViewOutlines) {
1219
- element.style.outline = '2px dotted #3b82f6';
1220
- element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
1221
- element.classList.add('show-outline');
1222
- this.createOverlayLabel(element, 'view');
1223
- }
1224
- else {
1225
- element.style.outline = 'none';
1226
- element.style.outlineOffset = '0';
1227
- element.classList.remove('show-outline');
1228
- this.removeOverlayLabel(element);
1229
- }
1230
- });
1231
- this.saveSettings();
1232
- }
1233
- togglePartialOutlines(show) {
1234
- this.showingPartialOutlines = show !== undefined ? show : !this.showingPartialOutlines;
1235
- const partialOutlines = document.querySelectorAll('[data-herb-debug-outline-type="partial"], [data-herb-debug-outline-type*="partial"]');
1236
- partialOutlines.forEach((outline) => {
1237
- const element = outline;
1238
- if (this.showingPartialOutlines) {
1239
- element.style.outline = '2px dotted #10b981';
1240
- element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
1241
- element.classList.add('show-outline');
1242
- this.createOverlayLabel(element, 'partial');
1243
- }
1244
- else {
1245
- element.style.outline = 'none';
1246
- element.style.outlineOffset = '0';
1247
- element.classList.remove('show-outline');
1248
- this.removeOverlayLabel(element);
1249
- }
1250
- });
1251
- this.saveSettings();
1252
- }
1253
- toggleComponentOutlines(show) {
1254
- this.showingComponentOutlines = show !== undefined ? show : !this.showingComponentOutlines;
1255
- const componentOutlines = document.querySelectorAll('[data-herb-debug-outline-type="component"], [data-herb-debug-outline-type*="component"]');
1256
- componentOutlines.forEach((outline) => {
1257
- const element = outline;
1258
- if (this.showingComponentOutlines) {
1259
- element.style.outline = '2px dotted #f59e0b';
1260
- element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
1261
- element.classList.add('show-outline');
1262
- this.createOverlayLabel(element, 'component');
1263
- }
1264
- else {
1265
- element.style.outline = 'none';
1266
- element.style.outlineOffset = '0';
1267
- element.classList.remove('show-outline');
1268
- this.removeOverlayLabel(element);
1269
- }
1270
- });
1271
- this.saveSettings();
1272
- }
1273
- createOverlayLabel(element, type) {
1274
- if (element.querySelector('.herb-overlay-label')) {
1275
- return;
1276
- }
1277
- const shortName = element.getAttribute('data-herb-debug-file-name') || '';
1278
- const relativePath = element.getAttribute('data-herb-debug-file-relative-path') || shortName;
1279
- const fullPath = element.getAttribute('data-herb-debug-file-full-path') || relativePath;
1280
- const label = document.createElement('div');
1281
- label.className = 'herb-overlay-label';
1282
- label.textContent = shortName;
1283
- label.setAttribute('data-label-setup', 'true');
1284
- label.addEventListener('mouseenter', () => {
1285
- label.textContent = relativePath;
1286
- document.querySelectorAll('.herb-overlay-label').forEach(otherLabel => {
1287
- otherLabel.style.zIndex = '1000';
1288
- });
1289
- label.style.zIndex = '1002';
1290
- });
1291
- label.addEventListener('mouseleave', () => {
1292
- label.textContent = shortName;
1293
- label.style.zIndex = '1000';
1294
- });
1295
- label.addEventListener('click', (e) => {
1296
- e.stopPropagation();
1297
- this.openFileInEditor(fullPath, 1, 1);
1298
- });
1299
- const shouldAttachToParent = element.getAttribute('data-herb-debug-attach-to-parent') === 'true';
1300
- if (shouldAttachToParent && element.parentElement) {
1301
- const parent = element.parentElement;
1302
- element.style.outline = 'none';
1303
- element.classList.remove('show-outline');
1304
- const outlineColor = type === 'component' ? '#f59e0b' : type === 'partial' ? '#10b981' : '#3b82f6';
1305
- parent.style.outline = `2px dotted ${outlineColor}`;
1306
- parent.style.outlineOffset = parent.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
1307
- parent.classList.add('show-outline');
1308
- parent.setAttribute('data-herb-debug-attached-outline-type', type);
1309
- if (window.getComputedStyle(parent).position === 'static') {
1310
- parent.style.position = 'relative';
1311
- }
1312
- label.style.position = 'absolute';
1313
- label.style.top = '0';
1314
- label.style.left = '0';
1315
- parent.appendChild(label);
1316
- return;
1317
- }
1318
- if (element.localName === 'html' || window.getComputedStyle(element).overflowY !== 'visible') {
1319
- label.style.top = '0';
1320
- }
1321
- if (window.getComputedStyle(element).position === 'static') {
1322
- element.style.position = 'relative';
1323
- }
1324
- element.appendChild(label);
1325
- }
1326
- removeOverlayLabel(element) {
1327
- const shouldAttachToParent = element.getAttribute('data-herb-debug-attach-to-parent') === 'true';
1328
- if (shouldAttachToParent && element.parentElement) {
1329
- const parent = element.parentElement;
1330
- const label = parent.querySelector('.herb-overlay-label');
1331
- if (label) {
1332
- label.remove();
1333
- }
1334
- parent.style.outline = 'none';
1335
- parent.style.outlineOffset = '0';
1336
- parent.classList.remove('show-outline');
1337
- parent.removeAttribute('data-herb-debug-attached-outline-type');
1338
- }
1339
- else {
1340
- const label = element.querySelector('.herb-overlay-label');
1341
- if (label) {
1342
- label.remove();
1343
- }
1344
- }
1345
- }
1346
- resetShowingERB() {
1347
- const elements = document.querySelectorAll('[data-herb-debug-showing-erb');
1348
- elements.forEach(element => {
1349
- const originalContent = element.getAttribute('data-herb-debug-original') || "";
1350
- element.innerHTML = originalContent;
1351
- element.removeAttribute("data-herb-debug-showing-erb");
1352
- });
1353
- }
1354
- toggleERBTags(show) {
1355
- this.showingERB = show !== undefined ? show : !this.showingERB;
1356
- const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
1357
- erbOutputs.forEach((element) => {
1358
- const erbCode = element.getAttribute('data-herb-debug-erb');
1359
- if (this.showingERB && erbCode) {
1360
- // this.resetShowingERB()
1361
- if (!element.hasAttribute('data-herb-debug-original')) {
1362
- element.setAttribute('data-herb-debug-original', element.innerHTML);
1363
- }
1364
- element.textContent = erbCode;
1365
- element.setAttribute("data-herb-debug-showing-erb", "true");
1366
- element.style.background = '#f3e8ff';
1367
- element.style.color = '#7c3aed';
1368
- if (this.showingTooltips) {
1369
- this.addTooltipHoverHandler(element);
1370
- }
1371
- }
1372
- else {
1373
- const originalContent = element.getAttribute('data-herb-debug-original') || "";
1374
- if (element && element.hasAttribute("data-herb-debug-showing-erb")) {
1375
- element.innerHTML = originalContent;
1376
- element.removeAttribute("data-herb-debug-showing-erb");
1377
- }
1378
- element.style.background = 'transparent';
1379
- element.style.color = 'inherit';
1380
- this.removeTooltipHoverHandler(element);
1381
- this.removeHoverTooltip(element);
1382
- }
1383
- });
1384
- this.saveSettings();
1385
- }
1386
- toggleERBOutlines(show) {
1387
- this.showingERBOutlines = show !== undefined ? show : !this.showingERBOutlines;
1388
- this.clearCurrentHoveredERB();
1389
- const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
1390
- erbOutputs.forEach(element => {
1391
- const inserted = element.hasAttribute("data-herb-debug-inserted");
1392
- const needsWrapperToggled = (inserted && !element.children[0]);
1393
- const realElement = element.children[0] || element;
1394
- if (this.showingERBOutlines) {
1395
- realElement.style.outline = '2px dotted #a78bfa';
1396
- realElement.style.outlineOffset = '1px';
1397
- if (needsWrapperToggled) {
1398
- element.style.display = 'inline';
1399
- }
1400
- if (this.showingTooltips) {
1401
- this.addTooltipHoverHandler(element);
1402
- }
1403
- if (this.showingERBHoverReveal) {
1404
- this.addERBHoverReveal(element);
1405
- }
1406
- }
1407
- else {
1408
- realElement.style.outline = 'none';
1409
- realElement.style.outlineOffset = '0';
1410
- if (needsWrapperToggled) {
1411
- element.style.display = 'contents';
1412
- }
1413
- this.removeTooltipHoverHandler(element);
1414
- this.removeHoverTooltip(element);
1415
- this.removeERBHoverReveal(element);
1416
- }
1417
- });
1418
- this.saveSettings();
1419
- }
1420
- updateNestedToggleVisibility() {
1421
- const nestedToggle = document.getElementById('herbERBHoverRevealNested');
1422
- const tooltipsNestedToggle = document.getElementById('herbTooltipsNested');
1423
- if (nestedToggle) {
1424
- nestedToggle.style.display = this.showingERBOutlines ? 'block' : 'none';
1425
- }
1426
- if (tooltipsNestedToggle) {
1427
- tooltipsNestedToggle.style.display = this.showingERBOutlines ? 'block' : 'none';
1428
- }
1429
- }
1430
- toggleERBHoverReveal(show) {
1431
- this.showingERBHoverReveal = show !== undefined ? show : !this.showingERBHoverReveal;
1432
- if (this.showingERBHoverReveal && this.showingTooltips) {
1433
- this.toggleTooltips(false);
1434
- const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
1435
- if (toggleTooltipsSwitch) {
1436
- toggleTooltipsSwitch.checked = false;
1437
- }
1438
- }
1439
- this.clearCurrentHoveredERB();
1440
- const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
1441
- erbOutputs.forEach((el) => {
1442
- const element = el;
1443
- this.removeERBHoverReveal(element);
1444
- if (this.showingERBHoverReveal && this.showingERBOutlines) {
1445
- this.addERBHoverReveal(element);
1446
- }
1447
- });
1448
- this.saveSettings();
1449
- }
1450
- clearCurrentHoveredERB() {
1451
- if (this.currentlyHoveredERBElement) {
1452
- const handlers = this.currentlyHoveredERBElement._erbHoverHandlers;
1453
- if (handlers) {
1454
- handlers.hideERBCode();
1455
- }
1456
- this.currentlyHoveredERBElement = null;
1457
- }
1458
- }
1459
- addERBHoverReveal(element) {
1460
- const erbCode = element.getAttribute('data-herb-debug-erb');
1461
- if (!erbCode)
1462
- return;
1463
- this.removeERBHoverReveal(element);
1464
- if (!element.hasAttribute('data-herb-debug-original')) {
1465
- element.setAttribute('data-herb-debug-original', element.innerHTML);
1466
- }
1467
- const showERBCode = () => {
1468
- if (!this.showingERBHoverReveal || !this.showingERBOutlines) {
1469
- return;
1470
- }
1471
- if (this.currentlyHoveredERBElement === element) {
1472
- return;
1473
- }
1474
- this.clearCurrentHoveredERB();
1475
- this.currentlyHoveredERBElement = element;
1476
- element.style.background = '#f3e8ff';
1477
- element.style.color = '#7c3aed';
1478
- element.style.fontFamily = 'inherit';
1479
- element.style.fontSize = 'inherit';
1480
- element.style.borderRadius = '3px';
1481
- element.style.cursor = 'pointer';
1482
- element.textContent = erbCode;
1483
- element.addEventListener('click', this.handleRevealedERBClick);
1484
- };
1485
- const hideERBCode = () => {
1486
- if (this.currentlyHoveredERBElement === element) {
1487
- this.currentlyHoveredERBElement = null;
1488
- }
1489
- const originalContent = element.getAttribute('data-herb-debug-original');
1490
- if (originalContent) {
1491
- element.innerHTML = originalContent;
1492
- }
1493
- element.style.background = 'transparent';
1494
- element.style.color = 'inherit';
1495
- element.style.fontFamily = 'inherit';
1496
- element.style.fontSize = 'inherit';
1497
- element.style.borderRadius = '0';
1498
- element.style.cursor = 'default';
1499
- element.removeEventListener('click', this.handleRevealedERBClick);
1500
- };
1501
- element._erbHoverHandlers = { showERBCode, hideERBCode };
1502
- element.addEventListener('mouseenter', showERBCode);
1503
- }
1504
- removeERBHoverReveal(element) {
1505
- const handlers = element._erbHoverHandlers;
1506
- if (handlers) {
1507
- element.removeEventListener('mouseenter', handlers.showERBCode);
1508
- delete element._erbHoverHandlers;
1509
- handlers.hideERBCode();
1510
- }
1511
- }
1512
- createHoverTooltip(element, elementForPosition) {
1513
- this.removeHoverTooltip(element);
1514
- const relativePath = element.getAttribute('data-herb-debug-file-relative-path') || element.getAttribute('data-herb-debug-file-name') || '';
1515
- const fullPath = element.getAttribute('data-herb-debug-file-full-path') || relativePath;
1516
- const line = element.getAttribute('data-herb-debug-line') || '';
1517
- const column = element.getAttribute('data-herb-debug-column') || '';
1518
- const erb = element.getAttribute('data-herb-debug-erb') || '';
1519
- if (!relativePath || !erb)
1520
- return;
1521
- const tooltip = document.createElement('div');
1522
- tooltip.className = 'herb-tooltip';
1523
- tooltip.innerHTML = `
1854
+ document.body.insertAdjacentHTML('beforeend', menuHTML);
1855
+ }
1856
+ applySettings() {
1857
+ this.toggleViewOutlines(this.showingViewOutlines);
1858
+ this.togglePartialOutlines(this.showingPartialOutlines);
1859
+ this.toggleComponentOutlines(this.showingComponentOutlines);
1860
+ this.toggleERBTags(this.showingERB);
1861
+ this.toggleERBOutlines(this.showingERBOutlines);
1862
+ const menuTrigger = document.getElementById('herbMenuTrigger');
1863
+ const menuPanel = document.getElementById('herbMenuPanel');
1864
+ if (menuTrigger && menuPanel && this.menuOpen) {
1865
+ menuTrigger.classList.add('active');
1866
+ menuPanel.classList.add('open');
1867
+ }
1868
+ }
1869
+ setupMenuToggle() {
1870
+ const menuTrigger = document.getElementById('herbMenuTrigger');
1871
+ const menuPanel = document.getElementById('herbMenuPanel');
1872
+ if (menuTrigger && menuPanel) {
1873
+ menuTrigger.addEventListener('click', () => {
1874
+ this.menuOpen = !this.menuOpen;
1875
+ if (this.menuOpen) {
1876
+ menuTrigger.classList.add('active');
1877
+ menuPanel.classList.add('open');
1878
+ }
1879
+ else {
1880
+ menuTrigger.classList.remove('active');
1881
+ menuPanel.classList.remove('open');
1882
+ }
1883
+ this.saveSettings();
1884
+ });
1885
+ document.addEventListener('click', (e) => {
1886
+ const target = e.target;
1887
+ const floatingMenu = document.querySelector('.herb-floating-menu');
1888
+ if (floatingMenu && !floatingMenu.contains(target) && this.menuOpen) {
1889
+ this.menuOpen = false;
1890
+ menuTrigger.classList.remove('active');
1891
+ menuPanel.classList.remove('open');
1892
+ this.saveSettings();
1893
+ }
1894
+ });
1895
+ }
1896
+ }
1897
+ setupTurboListeners() {
1898
+ document.addEventListener('turbo:load', () => {
1899
+ this.reinitializeAfterNavigation();
1900
+ });
1901
+ document.addEventListener('turbo:render', () => {
1902
+ this.reinitializeAfterNavigation();
1903
+ });
1904
+ document.addEventListener('turbo:visit', () => {
1905
+ this.reinitializeAfterNavigation();
1906
+ });
1907
+ }
1908
+ reinitializeAfterNavigation() {
1909
+ this.injectMenu();
1910
+ this.syncConnectionDot();
1911
+ this.setupMenuToggle();
1912
+ this.setupToggleSwitches();
1913
+ this.setupEditorDropdown();
1914
+ this.applySettings();
1915
+ this.updateMenuButtonState();
1916
+ }
1917
+ setupToggleSwitches() {
1918
+ const toggleViewOutlinesSwitch = document.getElementById('herbToggleViewOutlines');
1919
+ if (toggleViewOutlinesSwitch) {
1920
+ toggleViewOutlinesSwitch.checked = this.showingViewOutlines;
1921
+ toggleViewOutlinesSwitch.addEventListener('change', () => {
1922
+ this.toggleViewOutlines(toggleViewOutlinesSwitch.checked);
1923
+ });
1924
+ }
1925
+ const togglePartialOutlinesSwitch = document.getElementById('herbTogglePartialOutlines');
1926
+ if (togglePartialOutlinesSwitch) {
1927
+ togglePartialOutlinesSwitch.checked = this.showingPartialOutlines;
1928
+ togglePartialOutlinesSwitch.addEventListener('change', () => {
1929
+ this.togglePartialOutlines(togglePartialOutlinesSwitch.checked);
1930
+ });
1931
+ }
1932
+ const toggleComponentOutlinesSwitch = document.getElementById('herbToggleComponentOutlines');
1933
+ if (toggleComponentOutlinesSwitch) {
1934
+ toggleComponentOutlinesSwitch.checked = this.showingComponentOutlines;
1935
+ toggleComponentOutlinesSwitch.addEventListener('change', () => {
1936
+ this.toggleComponentOutlines(toggleComponentOutlinesSwitch.checked);
1937
+ });
1938
+ }
1939
+ const toggleERBSwitch = document.getElementById('herbToggleERB');
1940
+ const toggleERBOutlinesSwitch = document.getElementById('herbToggleERBOutlines');
1941
+ if (toggleERBSwitch) {
1942
+ toggleERBSwitch.checked = this.showingERB;
1943
+ toggleERBSwitch.addEventListener('change', () => {
1944
+ if (toggleERBSwitch.checked && toggleERBOutlinesSwitch) {
1945
+ toggleERBOutlinesSwitch.checked = false;
1946
+ this.toggleERBOutlines(false);
1947
+ }
1948
+ this.toggleERBTags(toggleERBSwitch.checked);
1949
+ });
1950
+ }
1951
+ if (toggleERBOutlinesSwitch) {
1952
+ toggleERBOutlinesSwitch.checked = this.showingERBOutlines;
1953
+ toggleERBOutlinesSwitch.addEventListener('change', () => {
1954
+ if (toggleERBOutlinesSwitch.checked && toggleERBSwitch) {
1955
+ toggleERBSwitch.checked = false;
1956
+ this.toggleERBTags(false);
1957
+ }
1958
+ this.toggleERBOutlines(toggleERBOutlinesSwitch.checked);
1959
+ this.updateNestedToggleVisibility();
1960
+ });
1961
+ }
1962
+ else {
1963
+ console.warn('ERB outlines toggle switch not found');
1964
+ }
1965
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
1966
+ if (toggleERBHoverRevealSwitch) {
1967
+ toggleERBHoverRevealSwitch.checked = this.showingERBHoverReveal;
1968
+ toggleERBHoverRevealSwitch.addEventListener('change', () => {
1969
+ this.toggleERBHoverReveal(toggleERBHoverRevealSwitch.checked);
1970
+ });
1971
+ }
1972
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
1973
+ if (toggleTooltipsSwitch) {
1974
+ toggleTooltipsSwitch.checked = this.showingTooltips;
1975
+ toggleTooltipsSwitch.addEventListener('change', () => {
1976
+ this.toggleTooltips(toggleTooltipsSwitch.checked);
1977
+ });
1978
+ }
1979
+ this.updateNestedToggleVisibility();
1980
+ const disableAllBtn = document.getElementById('herbDisableAll');
1981
+ if (disableAllBtn) {
1982
+ disableAllBtn.addEventListener('click', () => {
1983
+ this.disableAll();
1984
+ });
1985
+ }
1986
+ }
1987
+ setupEditorDropdown() {
1988
+ const editorSelect = document.getElementById('herbEditorSelect');
1989
+ if (editorSelect) {
1990
+ const autoOption = editorSelect.querySelector('option[value="auto"]');
1991
+ if (autoOption) {
1992
+ const editorLabel = HerbOverlay.EDITOR_OPTIONS.find(opt => opt.value === this.defaultEditorFromServer)?.label || this.defaultEditorFromServer;
1993
+ const metaTag = document.querySelector('meta[name="herb-default-editor"]');
1994
+ if (metaTag?.content) {
1995
+ autoOption.textContent = `Auto (from server): ${editorLabel}`;
1996
+ }
1997
+ else {
1998
+ autoOption.textContent = `Auto (default): ${editorLabel}`;
1999
+ }
2000
+ }
2001
+ editorSelect.value = this.preferredEditor;
2002
+ editorSelect.addEventListener('change', () => {
2003
+ this.preferredEditor = editorSelect.value;
2004
+ this.saveSettings();
2005
+ });
2006
+ }
2007
+ }
2008
+ toggleViewOutlines(show) {
2009
+ this.showingViewOutlines = show !== undefined ? show : !this.showingViewOutlines;
2010
+ const viewOutlines = document.querySelectorAll('[data-herb-debug-outline-type="view"], [data-herb-debug-outline-type*="view"]');
2011
+ viewOutlines.forEach((outline) => {
2012
+ const element = outline;
2013
+ if (this.showingViewOutlines) {
2014
+ element.style.outline = '2px dotted #3b82f6';
2015
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
2016
+ element.classList.add('show-outline');
2017
+ this.createOverlayLabel(element, 'view');
2018
+ }
2019
+ else {
2020
+ element.style.outline = 'none';
2021
+ element.style.outlineOffset = '0';
2022
+ element.classList.remove('show-outline');
2023
+ this.removeOverlayLabel(element);
2024
+ }
2025
+ });
2026
+ this.saveSettings();
2027
+ }
2028
+ togglePartialOutlines(show) {
2029
+ this.showingPartialOutlines = show !== undefined ? show : !this.showingPartialOutlines;
2030
+ const partialOutlines = document.querySelectorAll('[data-herb-debug-outline-type="partial"], [data-herb-debug-outline-type*="partial"]');
2031
+ partialOutlines.forEach((outline) => {
2032
+ const element = outline;
2033
+ if (this.showingPartialOutlines) {
2034
+ element.style.outline = '2px dotted #10b981';
2035
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
2036
+ element.classList.add('show-outline');
2037
+ this.createOverlayLabel(element, 'partial');
2038
+ }
2039
+ else {
2040
+ element.style.outline = 'none';
2041
+ element.style.outlineOffset = '0';
2042
+ element.classList.remove('show-outline');
2043
+ this.removeOverlayLabel(element);
2044
+ }
2045
+ });
2046
+ this.saveSettings();
2047
+ }
2048
+ toggleComponentOutlines(show) {
2049
+ this.showingComponentOutlines = show !== undefined ? show : !this.showingComponentOutlines;
2050
+ const componentOutlines = document.querySelectorAll('[data-herb-debug-outline-type="component"], [data-herb-debug-outline-type*="component"]');
2051
+ componentOutlines.forEach((outline) => {
2052
+ const element = outline;
2053
+ if (this.showingComponentOutlines) {
2054
+ element.style.outline = '2px dotted #f59e0b';
2055
+ element.style.outlineOffset = element.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
2056
+ element.classList.add('show-outline');
2057
+ this.createOverlayLabel(element, 'component');
2058
+ }
2059
+ else {
2060
+ element.style.outline = 'none';
2061
+ element.style.outlineOffset = '0';
2062
+ element.classList.remove('show-outline');
2063
+ this.removeOverlayLabel(element);
2064
+ }
2065
+ });
2066
+ this.saveSettings();
2067
+ }
2068
+ createOverlayLabel(element, type) {
2069
+ if (element.querySelector('.herb-overlay-label')) {
2070
+ return;
2071
+ }
2072
+ const shortName = element.getAttribute('data-herb-debug-file-name') || '';
2073
+ const relativePath = element.getAttribute('data-herb-debug-file-relative-path') || shortName;
2074
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path') || relativePath;
2075
+ const label = document.createElement('div');
2076
+ label.className = 'herb-overlay-label';
2077
+ label.textContent = shortName;
2078
+ label.setAttribute('data-label-setup', 'true');
2079
+ label.addEventListener('mouseenter', () => {
2080
+ label.textContent = relativePath;
2081
+ document.querySelectorAll('.herb-overlay-label').forEach(otherLabel => {
2082
+ otherLabel.style.zIndex = '1000';
2083
+ });
2084
+ label.style.zIndex = '1002';
2085
+ });
2086
+ label.addEventListener('mouseleave', () => {
2087
+ label.textContent = shortName;
2088
+ label.style.zIndex = '1000';
2089
+ });
2090
+ label.addEventListener('click', (e) => {
2091
+ e.stopPropagation();
2092
+ this.openFileInEditor(fullPath, 1, 1);
2093
+ });
2094
+ const shouldAttachToParent = element.getAttribute('data-herb-debug-attach-to-parent') === 'true';
2095
+ if (shouldAttachToParent && element.parentElement) {
2096
+ const parent = element.parentElement;
2097
+ element.style.outline = 'none';
2098
+ element.classList.remove('show-outline');
2099
+ const outlineColor = type === 'component' ? '#f59e0b' : type === 'partial' ? '#10b981' : '#3b82f6';
2100
+ parent.style.outline = `2px dotted ${outlineColor}`;
2101
+ parent.style.outlineOffset = parent.tagName.toLowerCase() === 'html' ? '-2px' : '2px';
2102
+ parent.classList.add('show-outline');
2103
+ parent.setAttribute('data-herb-debug-attached-outline-type', type);
2104
+ if (window.getComputedStyle(parent).position === 'static') {
2105
+ parent.style.position = 'relative';
2106
+ }
2107
+ label.style.position = 'absolute';
2108
+ label.style.top = '0';
2109
+ label.style.left = '0';
2110
+ parent.appendChild(label);
2111
+ return;
2112
+ }
2113
+ if (element.localName === 'html' || window.getComputedStyle(element).overflowY !== 'visible') {
2114
+ label.style.top = '0';
2115
+ }
2116
+ if (window.getComputedStyle(element).position === 'static') {
2117
+ element.style.position = 'relative';
2118
+ }
2119
+ element.appendChild(label);
2120
+ }
2121
+ removeOverlayLabel(element) {
2122
+ const shouldAttachToParent = element.getAttribute('data-herb-debug-attach-to-parent') === 'true';
2123
+ if (shouldAttachToParent && element.parentElement) {
2124
+ const parent = element.parentElement;
2125
+ const label = parent.querySelector('.herb-overlay-label');
2126
+ if (label) {
2127
+ label.remove();
2128
+ }
2129
+ parent.style.outline = 'none';
2130
+ parent.style.outlineOffset = '0';
2131
+ parent.classList.remove('show-outline');
2132
+ parent.removeAttribute('data-herb-debug-attached-outline-type');
2133
+ }
2134
+ else {
2135
+ const label = element.querySelector('.herb-overlay-label');
2136
+ if (label) {
2137
+ label.remove();
2138
+ }
2139
+ }
2140
+ }
2141
+ resetShowingERB() {
2142
+ const elements = document.querySelectorAll('[data-herb-debug-showing-erb');
2143
+ elements.forEach(element => {
2144
+ const originalContent = element.getAttribute('data-herb-debug-original') || "";
2145
+ element.innerHTML = originalContent;
2146
+ element.removeAttribute("data-herb-debug-showing-erb");
2147
+ });
2148
+ }
2149
+ toggleERBTags(show) {
2150
+ this.showingERB = show !== undefined ? show : !this.showingERB;
2151
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
2152
+ erbOutputs.forEach((element) => {
2153
+ const erbCode = element.getAttribute('data-herb-debug-erb');
2154
+ if (this.showingERB && erbCode) {
2155
+ // this.resetShowingERB()
2156
+ if (!element.hasAttribute('data-herb-debug-original')) {
2157
+ element.setAttribute('data-herb-debug-original', element.innerHTML);
2158
+ }
2159
+ element.textContent = erbCode;
2160
+ element.setAttribute("data-herb-debug-showing-erb", "true");
2161
+ element.style.background = '#f3e8ff';
2162
+ element.style.color = '#7c3aed';
2163
+ if (this.showingTooltips) {
2164
+ this.addTooltipHoverHandler(element);
2165
+ }
2166
+ }
2167
+ else {
2168
+ const originalContent = element.getAttribute('data-herb-debug-original') || "";
2169
+ if (element && element.hasAttribute("data-herb-debug-showing-erb")) {
2170
+ element.innerHTML = originalContent;
2171
+ element.removeAttribute("data-herb-debug-showing-erb");
2172
+ }
2173
+ element.style.background = 'transparent';
2174
+ element.style.color = 'inherit';
2175
+ this.removeTooltipHoverHandler(element);
2176
+ this.removeHoverTooltip(element);
2177
+ }
2178
+ });
2179
+ this.saveSettings();
2180
+ }
2181
+ toggleERBOutlines(show) {
2182
+ this.showingERBOutlines = show !== undefined ? show : !this.showingERBOutlines;
2183
+ this.clearCurrentHoveredERB();
2184
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
2185
+ erbOutputs.forEach(element => {
2186
+ const inserted = element.hasAttribute("data-herb-debug-inserted");
2187
+ const needsWrapperToggled = (inserted && !element.children[0]);
2188
+ const realElement = element.children[0] || element;
2189
+ if (this.showingERBOutlines) {
2190
+ realElement.style.outline = '2px dotted #a78bfa';
2191
+ realElement.style.outlineOffset = '1px';
2192
+ if (needsWrapperToggled) {
2193
+ element.style.display = 'inline';
2194
+ }
2195
+ if (this.showingTooltips) {
2196
+ this.addTooltipHoverHandler(element);
2197
+ }
2198
+ if (this.showingERBHoverReveal) {
2199
+ this.addERBHoverReveal(element);
2200
+ }
2201
+ }
2202
+ else {
2203
+ realElement.style.outline = 'none';
2204
+ realElement.style.outlineOffset = '0';
2205
+ if (needsWrapperToggled) {
2206
+ element.style.display = 'contents';
2207
+ }
2208
+ this.removeTooltipHoverHandler(element);
2209
+ this.removeHoverTooltip(element);
2210
+ this.removeERBHoverReveal(element);
2211
+ }
2212
+ });
2213
+ this.saveSettings();
2214
+ }
2215
+ updateNestedToggleVisibility() {
2216
+ const nestedToggle = document.getElementById('herbERBHoverRevealNested');
2217
+ const tooltipsNestedToggle = document.getElementById('herbTooltipsNested');
2218
+ if (nestedToggle) {
2219
+ nestedToggle.style.display = this.showingERBOutlines ? 'block' : 'none';
2220
+ }
2221
+ if (tooltipsNestedToggle) {
2222
+ tooltipsNestedToggle.style.display = this.showingERBOutlines ? 'block' : 'none';
2223
+ }
2224
+ }
2225
+ toggleERBHoverReveal(show) {
2226
+ this.showingERBHoverReveal = show !== undefined ? show : !this.showingERBHoverReveal;
2227
+ if (this.showingERBHoverReveal && this.showingTooltips) {
2228
+ this.toggleTooltips(false);
2229
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
2230
+ if (toggleTooltipsSwitch) {
2231
+ toggleTooltipsSwitch.checked = false;
2232
+ }
2233
+ }
2234
+ this.clearCurrentHoveredERB();
2235
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
2236
+ erbOutputs.forEach((el) => {
2237
+ const element = el;
2238
+ this.removeERBHoverReveal(element);
2239
+ if (this.showingERBHoverReveal && this.showingERBOutlines) {
2240
+ this.addERBHoverReveal(element);
2241
+ }
2242
+ });
2243
+ this.saveSettings();
2244
+ }
2245
+ clearCurrentHoveredERB() {
2246
+ if (this.currentlyHoveredERBElement) {
2247
+ const handlers = this.currentlyHoveredERBElement._erbHoverHandlers;
2248
+ if (handlers) {
2249
+ handlers.hideERBCode();
2250
+ }
2251
+ this.currentlyHoveredERBElement = null;
2252
+ }
2253
+ }
2254
+ addERBHoverReveal(element) {
2255
+ const erbCode = element.getAttribute('data-herb-debug-erb');
2256
+ if (!erbCode)
2257
+ return;
2258
+ this.removeERBHoverReveal(element);
2259
+ if (!element.hasAttribute('data-herb-debug-original')) {
2260
+ element.setAttribute('data-herb-debug-original', element.innerHTML);
2261
+ }
2262
+ const showERBCode = () => {
2263
+ if (!this.showingERBHoverReveal || !this.showingERBOutlines) {
2264
+ return;
2265
+ }
2266
+ if (this.currentlyHoveredERBElement === element) {
2267
+ return;
2268
+ }
2269
+ this.clearCurrentHoveredERB();
2270
+ this.currentlyHoveredERBElement = element;
2271
+ element.style.background = '#f3e8ff';
2272
+ element.style.color = '#7c3aed';
2273
+ element.style.fontFamily = 'inherit';
2274
+ element.style.fontSize = 'inherit';
2275
+ element.style.borderRadius = '3px';
2276
+ element.style.cursor = 'pointer';
2277
+ element.textContent = erbCode;
2278
+ element.addEventListener('click', this.handleRevealedERBClick);
2279
+ };
2280
+ const hideERBCode = () => {
2281
+ if (this.currentlyHoveredERBElement === element) {
2282
+ this.currentlyHoveredERBElement = null;
2283
+ }
2284
+ const originalContent = element.getAttribute('data-herb-debug-original');
2285
+ if (originalContent) {
2286
+ element.innerHTML = originalContent;
2287
+ }
2288
+ element.style.background = 'transparent';
2289
+ element.style.color = 'inherit';
2290
+ element.style.fontFamily = 'inherit';
2291
+ element.style.fontSize = 'inherit';
2292
+ element.style.borderRadius = '0';
2293
+ element.style.cursor = 'default';
2294
+ element.removeEventListener('click', this.handleRevealedERBClick);
2295
+ };
2296
+ element._erbHoverHandlers = { showERBCode, hideERBCode };
2297
+ element.addEventListener('mouseenter', showERBCode);
2298
+ }
2299
+ removeERBHoverReveal(element) {
2300
+ const handlers = element._erbHoverHandlers;
2301
+ if (handlers) {
2302
+ element.removeEventListener('mouseenter', handlers.showERBCode);
2303
+ delete element._erbHoverHandlers;
2304
+ handlers.hideERBCode();
2305
+ }
2306
+ }
2307
+ createHoverTooltip(element, elementForPosition) {
2308
+ this.removeHoverTooltip(element);
2309
+ const relativePath = element.getAttribute('data-herb-debug-file-relative-path') || element.getAttribute('data-herb-debug-file-name') || '';
2310
+ const fullPath = element.getAttribute('data-herb-debug-file-full-path') || relativePath;
2311
+ const line = element.getAttribute('data-herb-debug-line') || '';
2312
+ const column = element.getAttribute('data-herb-debug-column') || '';
2313
+ const erb = element.getAttribute('data-herb-debug-erb') || '';
2314
+ if (!relativePath || !erb)
2315
+ return;
2316
+ const tooltip = document.createElement('div');
2317
+ tooltip.className = 'herb-tooltip';
2318
+ tooltip.innerHTML = `
1524
2319
  <div class="herb-location" data-tooltip="Open in Editor">
1525
2320
  <span class="herb-file-path">${relativePath}:${line}:${column}</span>
1526
2321
  <button class="herb-copy-path-btn" data-tooltip="Copy file path">📋</button>
1527
2322
  </div>
1528
2323
  <div class="herb-erb-code">${erb}</div>
1529
2324
  `;
1530
- let hideTimeout = null;
1531
- const showTooltip = () => {
1532
- if (hideTimeout) {
1533
- clearTimeout(hideTimeout);
1534
- hideTimeout = null;
1535
- }
1536
- tooltip.classList.add('visible');
1537
- };
1538
- const hideTooltip = () => {
1539
- hideTimeout = window.setTimeout(() => {
1540
- tooltip.classList.remove('visible');
1541
- }, 100);
1542
- };
1543
- element.addEventListener('mouseenter', showTooltip);
1544
- element.addEventListener('mouseleave', hideTooltip);
1545
- tooltip.addEventListener('mouseenter', showTooltip);
1546
- tooltip.addEventListener('mouseleave', hideTooltip);
1547
- const locationElement = tooltip.querySelector('.herb-location');
1548
- const openInEditor = (e) => {
1549
- if (e.target.closest('.herb-copy-path-btn')) {
1550
- return;
1551
- }
1552
- e.preventDefault();
1553
- e.stopPropagation();
1554
- this.openFileInEditor(fullPath, parseInt(line), parseInt(column));
1555
- };
1556
- locationElement?.addEventListener('click', openInEditor);
1557
- const copyButton = tooltip.querySelector('.herb-copy-path-btn');
1558
- const copyFilePath = (e) => {
1559
- e.preventDefault();
1560
- e.stopPropagation();
1561
- const textToCopy = `${relativePath}:${line}:${column}`;
1562
- navigator.clipboard.writeText(textToCopy).then(() => {
1563
- copyButton.textContent = '✅';
1564
- setTimeout(() => {
1565
- copyButton.textContent = '📋';
1566
- }, 1000);
1567
- }).catch((err) => {
1568
- console.error('Failed to copy file path:', err);
1569
- });
1570
- };
1571
- copyButton?.addEventListener('click', copyFilePath);
1572
- const positionTooltip = () => {
1573
- const elementRect = elementForPosition.getBoundingClientRect();
1574
- const viewportHeight = window.innerHeight;
1575
- const viewportWidth = window.innerWidth;
1576
- tooltip.style.position = 'fixed';
1577
- tooltip.style.left = '0';
1578
- tooltip.style.top = '0';
1579
- tooltip.style.transform = 'none';
1580
- tooltip.style.bottom = 'auto';
1581
- const actualTooltipRect = tooltip.getBoundingClientRect();
1582
- const tooltipWidth = actualTooltipRect.width;
1583
- const tooltipHeight = actualTooltipRect.height;
1584
- let left = elementRect.left + (elementRect.width / 2) - (tooltipWidth / 2);
1585
- let top = elementRect.top - tooltipHeight - 8;
1586
- if (left < 8) {
1587
- left = 8;
1588
- }
1589
- else if (left + tooltipWidth > viewportWidth - 8) {
1590
- left = viewportWidth - tooltipWidth - 8;
1591
- }
1592
- if (top < 8) {
1593
- top = elementRect.bottom + 8;
1594
- if (top + tooltipHeight > viewportHeight - 8) {
1595
- top = Math.max(8, (viewportHeight - tooltipHeight) / 2);
1596
- }
1597
- }
1598
- if (top + tooltipHeight > viewportHeight - 8) {
1599
- top = viewportHeight - tooltipHeight - 8;
1600
- }
1601
- tooltip.style.position = 'fixed';
1602
- tooltip.style.left = `${left}px`;
1603
- tooltip.style.top = `${top}px`;
1604
- tooltip.style.transform = 'none';
1605
- tooltip.style.bottom = 'auto';
1606
- };
1607
- element._tooltipHandlers = { showTooltip, hideTooltip, openInEditor, copyFilePath, positionTooltip };
1608
- tooltip._tooltipHandlers = { showTooltip, hideTooltip };
1609
- element.appendChild(tooltip);
1610
- setTimeout(positionTooltip, 0);
1611
- window.addEventListener('scroll', positionTooltip, { passive: true });
1612
- window.addEventListener('resize', positionTooltip, { passive: true });
1613
- }
1614
- removeHoverTooltip(element) {
1615
- const tooltip = element.querySelector('.herb-tooltip');
1616
- if (tooltip) {
1617
- const handlers = element._tooltipHandlers;
1618
- const tooltipHandlers = tooltip._tooltipHandlers;
1619
- if (handlers) {
1620
- element.removeEventListener('mouseenter', handlers.showTooltip);
1621
- element.removeEventListener('mouseleave', handlers.hideTooltip);
1622
- const locationElement = tooltip.querySelector('.herb-location');
1623
- locationElement?.removeEventListener('click', handlers.openInEditor);
1624
- const copyButton = tooltip.querySelector('.herb-copy-path-btn');
1625
- copyButton?.removeEventListener('click', handlers.copyFilePath);
1626
- if (handlers.positionTooltip) {
1627
- window.removeEventListener('scroll', handlers.positionTooltip);
1628
- window.removeEventListener('resize', handlers.positionTooltip);
1629
- }
1630
- delete element._tooltipHandlers;
1631
- }
1632
- if (tooltipHandlers) {
1633
- tooltip.removeEventListener('mouseenter', tooltipHandlers.showTooltip);
1634
- tooltip.removeEventListener('mouseleave', tooltipHandlers.hideTooltip);
1635
- delete tooltip._tooltipHandlers;
1636
- }
1637
- tooltip.remove();
1638
- }
1639
- }
1640
- addTooltipHoverHandler(element) {
1641
- this.removeTooltipHoverHandler(element);
1642
- const lazyTooltipHandler = () => {
1643
- if (!this.showingTooltips || !this.showingERBOutlines) {
1644
- return;
1645
- }
1646
- if (element.querySelector('.herb-tooltip')) {
1647
- return;
1648
- }
1649
- this.createHoverTooltip(element, element);
1650
- };
1651
- element._lazyTooltipHandler = lazyTooltipHandler;
1652
- element.addEventListener('mouseenter', lazyTooltipHandler);
1653
- }
1654
- removeTooltipHoverHandler(element) {
1655
- const handler = element._lazyTooltipHandler;
1656
- if (handler) {
1657
- element.removeEventListener('mouseenter', handler);
1658
- delete element._lazyTooltipHandler;
1659
- }
1660
- }
1661
- getEditorUrl(editor, absolutePath, line, column) {
1662
- switch (editor) {
1663
- case 'cursor':
1664
- return `cursor://file/${absolutePath}:${line}:${column}`;
1665
- case 'vscode':
1666
- return `vscode://file/${absolutePath}:${line}:${column}`;
1667
- case 'vscodium':
1668
- return `vscodium://file/${absolutePath}:${line}:${column}`;
1669
- case 'zed':
1670
- return `zed://file/${absolutePath}:${line}:${column}`;
1671
- case 'windsurf':
1672
- return `windsurf://file/${absolutePath}:${line}:${column}`;
1673
- case 'sublime':
1674
- return `subl://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1675
- case 'atom':
1676
- return `atom://core/open/file?filename=${absolutePath}&line=${line}&column=${column}`;
1677
- case 'textmate':
1678
- return `txmt://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1679
- case 'emacs':
1680
- return `emacs://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1681
- case 'idea':
1682
- return `idea://open?file=${absolutePath}&line=${line}&column=${column}`;
1683
- case 'rubymine':
1684
- return `x-mine://open?file=${absolutePath}&line=${line}&column=${column}`;
1685
- case 'nova':
1686
- return `nova://open?path=${absolutePath}&line=${line}&column=${column}`;
1687
- case 'macvim':
1688
- return `mvim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1689
- case 'vim':
1690
- return `vim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1691
- case 'nvim':
1692
- return `nvim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
1693
- default:
1694
- return '';
1695
- }
1696
- }
1697
- openFileInEditor(file, line, column) {
1698
- const absolutePath = file.startsWith('/') ? file : (this.projectPath ? `${this.projectPath}/${file}` : file);
1699
- const editorToUse = this.preferredEditor === 'auto' ? this.defaultEditorFromServer : this.preferredEditor;
1700
- const url = this.getEditorUrl(editorToUse, absolutePath, line, column);
1701
- if (url) {
1702
- try {
1703
- window.open(url, '_self');
1704
- }
1705
- catch (_error) {
1706
- console.log(`Open in editor: ${absolutePath}:${line}:${column}`);
1707
- }
1708
- }
1709
- else {
1710
- console.log(`Open in editor: ${absolutePath}:${line}:${column}`);
1711
- }
1712
- }
1713
- toggleTooltips(show) {
1714
- this.showingTooltips = show !== undefined ? show : !this.showingTooltips;
1715
- if (this.showingTooltips && this.showingERBHoverReveal) {
1716
- this.toggleERBHoverReveal(false);
1717
- const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
1718
- if (toggleERBHoverRevealSwitch) {
1719
- toggleERBHoverRevealSwitch.checked = false;
1720
- }
1721
- }
1722
- const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
1723
- erbOutputs.forEach((element) => {
1724
- if (this.showingERBOutlines && this.showingTooltips) {
1725
- this.addTooltipHoverHandler(element);
1726
- }
1727
- else {
1728
- this.removeTooltipHoverHandler(element);
1729
- this.removeHoverTooltip(element);
1730
- }
1731
- });
1732
- this.saveSettings();
1733
- }
1734
- disableAll() {
1735
- this.clearCurrentHoveredERB();
1736
- this.toggleViewOutlines(false);
1737
- this.togglePartialOutlines(false);
1738
- this.toggleComponentOutlines(false);
1739
- this.toggleERBTags(false);
1740
- this.toggleERBOutlines(false);
1741
- this.toggleERBHoverReveal(false);
1742
- this.toggleTooltips(false);
1743
- const toggleViewOutlinesSwitch = document.getElementById('herbToggleViewOutlines');
1744
- const togglePartialOutlinesSwitch = document.getElementById('herbTogglePartialOutlines');
1745
- const toggleComponentOutlinesSwitch = document.getElementById('herbToggleComponentOutlines');
1746
- const toggleERBSwitch = document.getElementById('herbToggleERB');
1747
- const toggleERBOutlinesSwitch = document.getElementById('herbToggleERBOutlines');
1748
- const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
1749
- const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
1750
- if (toggleViewOutlinesSwitch)
1751
- toggleViewOutlinesSwitch.checked = false;
1752
- if (togglePartialOutlinesSwitch)
1753
- togglePartialOutlinesSwitch.checked = false;
1754
- if (toggleComponentOutlinesSwitch)
1755
- toggleComponentOutlinesSwitch.checked = false;
1756
- if (toggleERBSwitch)
1757
- toggleERBSwitch.checked = false;
1758
- if (toggleERBOutlinesSwitch)
1759
- toggleERBOutlinesSwitch.checked = false;
1760
- if (toggleERBHoverRevealSwitch)
1761
- toggleERBHoverRevealSwitch.checked = false;
1762
- if (toggleTooltipsSwitch)
1763
- toggleTooltipsSwitch.checked = false;
1764
- }
1765
- initializeErrorOverlay() {
1766
- this.errorOverlay = new ErrorOverlay();
1767
- }
1768
- }
1769
- HerbOverlay.SETTINGS_KEY = 'herb-dev-tools-settings';
1770
- HerbOverlay.EDITOR_OPTIONS = [
1771
- { value: 'auto', label: 'Auto (from server via RAILS_EDITOR or EDITOR)' },
1772
- { value: 'atom', label: 'Atom' },
1773
- { value: 'cursor', label: 'Cursor' },
1774
- { value: 'emacs', label: 'Emacs' },
1775
- { value: 'idea', label: 'IntelliJ IDEA' },
1776
- { value: 'macvim', label: 'MacVim' },
1777
- { value: 'nova', label: 'Nova' },
1778
- { value: 'nvim', label: 'Neovim' },
1779
- { value: 'rubymine', label: 'RubyMine' },
1780
- { value: 'sublime', label: 'Sublime Text' },
1781
- { value: 'textmate', label: 'TextMate' },
1782
- { value: 'vim', label: 'Vim' },
1783
- { value: 'vscode', label: 'Visual Studio Code' },
1784
- { value: 'vscodium', label: 'VSCodium' },
1785
- { value: 'windsurf', label: 'Windsurf' },
1786
- { value: 'zed', label: 'Zed' },
1787
- ];
1788
-
1789
- function initHerbDevTools(options = {}) {
1790
- return new HerbOverlay(options);
1791
- }
1792
- if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1793
- const hasDebugMode = document.querySelector('meta[name="herb-debug-mode"]')?.getAttribute('content') === 'true';
1794
- const hasDebugErb = document.querySelector('[data-herb-debug-erb]') !== null;
1795
- const hasValidationErrors = document.querySelector('template[data-herb-validation-errors]') !== null;
1796
- const hasValidationError = document.querySelector('template[data-herb-validation-error]') !== null;
1797
- const hasParserErrors = document.querySelector('template[data-herb-parser-error]') !== null;
1798
- const shouldAutoInit = hasDebugMode || hasDebugErb || hasValidationErrors || hasValidationError || hasParserErrors;
1799
- if (shouldAutoInit) {
1800
- document.addEventListener('DOMContentLoaded', () => {
1801
- initHerbDevTools();
1802
- });
1803
- }
1804
- }
1805
- if (typeof window !== 'undefined') {
1806
- window.HerbDevTools = {
1807
- init: initHerbDevTools,
1808
- HerbOverlay
1809
- };
1810
- }
1811
-
1812
- class ReActionViewDevTools {
1813
- constructor(options = {}) {
1814
- this.options = options;
1815
- this.herbOverlay = null;
1816
- if (options.autoInit !== false) {
1817
- this.init();
1818
- }
1819
- }
1820
- init() {
1821
- if (this.herbOverlay) {
1822
- this.destroy();
1823
- }
1824
- this.herbOverlay = initHerbDevTools({
1825
- projectPath: this.options.projectPath,
1826
- ...this.options
1827
- });
1828
- return this.herbOverlay;
1829
- }
1830
- destroy() {
1831
- if (this.herbOverlay) {
1832
- const existingMenu = document.querySelector(".herb-floating-menu");
1833
- if (existingMenu) {
1834
- existingMenu.remove();
1835
- }
1836
- }
1837
- this.herbOverlay = null;
1838
- }
1839
- getHerbOverlay() {
1840
- return this.herbOverlay;
1841
- }
1842
- static getInstance() {
1843
- return ReActionViewDevTools.instance;
1844
- }
1845
- static setInstance(instance) {
1846
- ReActionViewDevTools.instance = instance;
1847
- }
1848
- }
1849
- ReActionViewDevTools.instance = null;
1850
- function initReActionViewDevTools(options = {}) {
1851
- const existingInstance = ReActionViewDevTools.getInstance();
1852
- if (existingInstance) {
1853
- existingInstance.destroy();
1854
- }
1855
- const instance = new ReActionViewDevTools(options);
1856
- ReActionViewDevTools.setInstance(instance);
1857
- return instance;
1858
- }
1859
- if (typeof window !== "undefined" && typeof document !== "undefined") {
1860
- let isInitializing = false;
1861
- const initializeDevTools = () => {
1862
- var _a, _b;
1863
- if (isInitializing) {
1864
- console.log("ReActionView dev tools initialization already in progress, skipping...");
1865
- return;
1866
- }
1867
- 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;
1868
- if (!shouldAutoInit) {
1869
- console.log("ReActionView debug mode not detected, skipping dev tools initialization");
1870
- return;
1871
- }
1872
- isInitializing = true;
1873
- try {
1874
- let projectPath;
1875
- const railsRoot = (_b = document.querySelector(`meta[name="herb-rails-root"]`)) === null || _b === void 0 ? void 0 : _b.getAttribute("content");
1876
- if (railsRoot) {
1877
- projectPath = railsRoot;
1878
- }
1879
- initReActionViewDevTools({
1880
- projectPath,
1881
- autoInit: true
1882
- });
1883
- }
1884
- catch (error) {
1885
- console.warn("Could not initialize ReActionView dev tools:", error);
1886
- }
1887
- finally {
1888
- isInitializing = false;
1889
- }
1890
- };
1891
- if (document.readyState === "loading") {
1892
- document.addEventListener("DOMContentLoaded", initializeDevTools, { once: true });
1893
- }
1894
- else {
1895
- setTimeout(initializeDevTools, 0);
1896
- }
1897
- document.addEventListener("turbo:load", initializeDevTools);
1898
- document.addEventListener("turbo:render", initializeDevTools);
1899
- document.addEventListener("turbo:visit", initializeDevTools);
1900
- }
1901
- if (typeof window !== "undefined") {
1902
- window.ReActionViewDevTools = {
1903
- init: initReActionViewDevTools,
1904
- ReActionViewDevTools,
1905
- HerbOverlay
1906
- };
1907
- }
1908
-
1909
- exports.HerbOverlay = HerbOverlay;
1910
- exports.ReActionViewDevTools = ReActionViewDevTools;
1911
- exports.initReActionViewDevTools = initReActionViewDevTools;
2325
+ let hideTimeout = null;
2326
+ const showTooltip = () => {
2327
+ if (hideTimeout) {
2328
+ clearTimeout(hideTimeout);
2329
+ hideTimeout = null;
2330
+ }
2331
+ tooltip.classList.add('visible');
2332
+ };
2333
+ const hideTooltip = () => {
2334
+ hideTimeout = window.setTimeout(() => {
2335
+ tooltip.classList.remove('visible');
2336
+ }, 100);
2337
+ };
2338
+ element.addEventListener('mouseenter', showTooltip);
2339
+ element.addEventListener('mouseleave', hideTooltip);
2340
+ tooltip.addEventListener('mouseenter', showTooltip);
2341
+ tooltip.addEventListener('mouseleave', hideTooltip);
2342
+ const locationElement = tooltip.querySelector('.herb-location');
2343
+ const openInEditor = (e) => {
2344
+ if (e.target.closest('.herb-copy-path-btn')) {
2345
+ return;
2346
+ }
2347
+ e.preventDefault();
2348
+ e.stopPropagation();
2349
+ this.openFileInEditor(fullPath, parseInt(line), parseInt(column));
2350
+ };
2351
+ locationElement?.addEventListener('click', openInEditor);
2352
+ const copyButton = tooltip.querySelector('.herb-copy-path-btn');
2353
+ const copyFilePath = (e) => {
2354
+ e.preventDefault();
2355
+ e.stopPropagation();
2356
+ const textToCopy = `${relativePath}:${line}:${column}`;
2357
+ navigator.clipboard.writeText(textToCopy).then(() => {
2358
+ copyButton.textContent = '✅';
2359
+ setTimeout(() => {
2360
+ copyButton.textContent = '📋';
2361
+ }, 1000);
2362
+ }).catch((err) => {
2363
+ console.error('Failed to copy file path:', err);
2364
+ });
2365
+ };
2366
+ copyButton?.addEventListener('click', copyFilePath);
2367
+ const positionTooltip = () => {
2368
+ const elementRect = elementForPosition.getBoundingClientRect();
2369
+ const viewportHeight = window.innerHeight;
2370
+ const viewportWidth = window.innerWidth;
2371
+ tooltip.style.position = 'fixed';
2372
+ tooltip.style.left = '0';
2373
+ tooltip.style.top = '0';
2374
+ tooltip.style.transform = 'none';
2375
+ tooltip.style.bottom = 'auto';
2376
+ const actualTooltipRect = tooltip.getBoundingClientRect();
2377
+ const tooltipWidth = actualTooltipRect.width;
2378
+ const tooltipHeight = actualTooltipRect.height;
2379
+ let left = elementRect.left + (elementRect.width / 2) - (tooltipWidth / 2);
2380
+ let top = elementRect.top - tooltipHeight - 8;
2381
+ if (left < 8) {
2382
+ left = 8;
2383
+ }
2384
+ else if (left + tooltipWidth > viewportWidth - 8) {
2385
+ left = viewportWidth - tooltipWidth - 8;
2386
+ }
2387
+ if (top < 8) {
2388
+ top = elementRect.bottom + 8;
2389
+ if (top + tooltipHeight > viewportHeight - 8) {
2390
+ top = Math.max(8, (viewportHeight - tooltipHeight) / 2);
2391
+ }
2392
+ }
2393
+ if (top + tooltipHeight > viewportHeight - 8) {
2394
+ top = viewportHeight - tooltipHeight - 8;
2395
+ }
2396
+ tooltip.style.position = 'fixed';
2397
+ tooltip.style.left = `${left}px`;
2398
+ tooltip.style.top = `${top}px`;
2399
+ tooltip.style.transform = 'none';
2400
+ tooltip.style.bottom = 'auto';
2401
+ };
2402
+ element._tooltipHandlers = { showTooltip, hideTooltip, openInEditor, copyFilePath, positionTooltip };
2403
+ tooltip._tooltipHandlers = { showTooltip, hideTooltip };
2404
+ element.appendChild(tooltip);
2405
+ setTimeout(positionTooltip, 0);
2406
+ window.addEventListener('scroll', positionTooltip, { passive: true });
2407
+ window.addEventListener('resize', positionTooltip, { passive: true });
2408
+ }
2409
+ removeHoverTooltip(element) {
2410
+ const tooltip = element.querySelector('.herb-tooltip');
2411
+ if (tooltip) {
2412
+ const handlers = element._tooltipHandlers;
2413
+ const tooltipHandlers = tooltip._tooltipHandlers;
2414
+ if (handlers) {
2415
+ element.removeEventListener('mouseenter', handlers.showTooltip);
2416
+ element.removeEventListener('mouseleave', handlers.hideTooltip);
2417
+ const locationElement = tooltip.querySelector('.herb-location');
2418
+ locationElement?.removeEventListener('click', handlers.openInEditor);
2419
+ const copyButton = tooltip.querySelector('.herb-copy-path-btn');
2420
+ copyButton?.removeEventListener('click', handlers.copyFilePath);
2421
+ if (handlers.positionTooltip) {
2422
+ window.removeEventListener('scroll', handlers.positionTooltip);
2423
+ window.removeEventListener('resize', handlers.positionTooltip);
2424
+ }
2425
+ delete element._tooltipHandlers;
2426
+ }
2427
+ if (tooltipHandlers) {
2428
+ tooltip.removeEventListener('mouseenter', tooltipHandlers.showTooltip);
2429
+ tooltip.removeEventListener('mouseleave', tooltipHandlers.hideTooltip);
2430
+ delete tooltip._tooltipHandlers;
2431
+ }
2432
+ tooltip.remove();
2433
+ }
2434
+ }
2435
+ addTooltipHoverHandler(element) {
2436
+ this.removeTooltipHoverHandler(element);
2437
+ const lazyTooltipHandler = () => {
2438
+ if (!this.showingTooltips || !this.showingERBOutlines) {
2439
+ return;
2440
+ }
2441
+ if (element.querySelector('.herb-tooltip')) {
2442
+ return;
2443
+ }
2444
+ this.createHoverTooltip(element, element);
2445
+ };
2446
+ element._lazyTooltipHandler = lazyTooltipHandler;
2447
+ element.addEventListener('mouseenter', lazyTooltipHandler);
2448
+ }
2449
+ removeTooltipHoverHandler(element) {
2450
+ const handler = element._lazyTooltipHandler;
2451
+ if (handler) {
2452
+ element.removeEventListener('mouseenter', handler);
2453
+ delete element._lazyTooltipHandler;
2454
+ }
2455
+ }
2456
+ getEditorUrl(editor, absolutePath, line, column) {
2457
+ switch (editor) {
2458
+ case 'cursor':
2459
+ return `cursor://file/${absolutePath}:${line}:${column}`;
2460
+ case 'vscode':
2461
+ return `vscode://file/${absolutePath}:${line}:${column}`;
2462
+ case 'vscodium':
2463
+ return `vscodium://file/${absolutePath}:${line}:${column}`;
2464
+ case 'zed':
2465
+ return `zed://file/${absolutePath}:${line}:${column}`;
2466
+ case 'windsurf':
2467
+ return `windsurf://file/${absolutePath}:${line}:${column}`;
2468
+ case 'sublime':
2469
+ return `subl://open?url=file://${absolutePath}&line=${line}&column=${column}`;
2470
+ case 'atom':
2471
+ return `atom://core/open/file?filename=${absolutePath}&line=${line}&column=${column}`;
2472
+ case 'textmate':
2473
+ return `txmt://open?url=file://${absolutePath}&line=${line}&column=${column}`;
2474
+ case 'emacs':
2475
+ return `emacs://open?url=file://${absolutePath}&line=${line}&column=${column}`;
2476
+ case 'idea':
2477
+ return `idea://open?file=${absolutePath}&line=${line}&column=${column}`;
2478
+ case 'rubymine':
2479
+ return `x-mine://open?file=${absolutePath}&line=${line}&column=${column}`;
2480
+ case 'nova':
2481
+ return `nova://open?path=${absolutePath}&line=${line}&column=${column}`;
2482
+ case 'macvim':
2483
+ return `mvim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
2484
+ case 'vim':
2485
+ return `vim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
2486
+ case 'nvim':
2487
+ return `nvim://open?url=file://${absolutePath}&line=${line}&column=${column}`;
2488
+ default:
2489
+ return '';
2490
+ }
2491
+ }
2492
+ openFileInEditor(file, line, column) {
2493
+ const absolutePath = file.startsWith('/') ? file : (this.projectPath ? `${this.projectPath}/${file}` : file);
2494
+ const editorToUse = this.preferredEditor === 'auto' ? this.defaultEditorFromServer : this.preferredEditor;
2495
+ const url = this.getEditorUrl(editorToUse, absolutePath, line, column);
2496
+ if (url) {
2497
+ try {
2498
+ window.open(url, '_self');
2499
+ }
2500
+ catch (_error) {
2501
+ console.log(`Open in editor: ${absolutePath}:${line}:${column}`);
2502
+ }
2503
+ }
2504
+ else {
2505
+ console.log(`Open in editor: ${absolutePath}:${line}:${column}`);
2506
+ }
2507
+ }
2508
+ toggleTooltips(show) {
2509
+ this.showingTooltips = show !== undefined ? show : !this.showingTooltips;
2510
+ if (this.showingTooltips && this.showingERBHoverReveal) {
2511
+ this.toggleERBHoverReveal(false);
2512
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
2513
+ if (toggleERBHoverRevealSwitch) {
2514
+ toggleERBHoverRevealSwitch.checked = false;
2515
+ }
2516
+ }
2517
+ const erbOutputs = document.querySelectorAll('[data-herb-debug-outline-type*="erb-output"]');
2518
+ erbOutputs.forEach((element) => {
2519
+ if (this.showingERBOutlines && this.showingTooltips) {
2520
+ this.addTooltipHoverHandler(element);
2521
+ }
2522
+ else {
2523
+ this.removeTooltipHoverHandler(element);
2524
+ this.removeHoverTooltip(element);
2525
+ }
2526
+ });
2527
+ this.saveSettings();
2528
+ }
2529
+ disableAll() {
2530
+ this.clearCurrentHoveredERB();
2531
+ this.toggleViewOutlines(false);
2532
+ this.togglePartialOutlines(false);
2533
+ this.toggleComponentOutlines(false);
2534
+ this.toggleERBTags(false);
2535
+ this.toggleERBOutlines(false);
2536
+ this.toggleERBHoverReveal(false);
2537
+ this.toggleTooltips(false);
2538
+ const toggleViewOutlinesSwitch = document.getElementById('herbToggleViewOutlines');
2539
+ const togglePartialOutlinesSwitch = document.getElementById('herbTogglePartialOutlines');
2540
+ const toggleComponentOutlinesSwitch = document.getElementById('herbToggleComponentOutlines');
2541
+ const toggleERBSwitch = document.getElementById('herbToggleERB');
2542
+ const toggleERBOutlinesSwitch = document.getElementById('herbToggleERBOutlines');
2543
+ const toggleERBHoverRevealSwitch = document.getElementById('herbToggleERBHoverReveal');
2544
+ const toggleTooltipsSwitch = document.getElementById('herbToggleTooltips');
2545
+ if (toggleViewOutlinesSwitch)
2546
+ toggleViewOutlinesSwitch.checked = false;
2547
+ if (togglePartialOutlinesSwitch)
2548
+ togglePartialOutlinesSwitch.checked = false;
2549
+ if (toggleComponentOutlinesSwitch)
2550
+ toggleComponentOutlinesSwitch.checked = false;
2551
+ if (toggleERBSwitch)
2552
+ toggleERBSwitch.checked = false;
2553
+ if (toggleERBOutlinesSwitch)
2554
+ toggleERBOutlinesSwitch.checked = false;
2555
+ if (toggleERBHoverRevealSwitch)
2556
+ toggleERBHoverRevealSwitch.checked = false;
2557
+ if (toggleTooltipsSwitch)
2558
+ toggleTooltipsSwitch.checked = false;
2559
+ }
2560
+ initializeErrorOverlay() {
2561
+ this.errorOverlay = new ErrorOverlay();
2562
+ }
2563
+ }
2564
+ HerbOverlay.SETTINGS_KEY = 'herb-dev-tools-settings';
2565
+ HerbOverlay.EDITOR_OPTIONS = [
2566
+ { value: 'auto', label: 'Auto (from server via RAILS_EDITOR or EDITOR)' },
2567
+ { value: 'atom', label: 'Atom' },
2568
+ { value: 'cursor', label: 'Cursor' },
2569
+ { value: 'emacs', label: 'Emacs' },
2570
+ { value: 'idea', label: 'IntelliJ IDEA' },
2571
+ { value: 'macvim', label: 'MacVim' },
2572
+ { value: 'nova', label: 'Nova' },
2573
+ { value: 'nvim', label: 'Neovim' },
2574
+ { value: 'rubymine', label: 'RubyMine' },
2575
+ { value: 'sublime', label: 'Sublime Text' },
2576
+ { value: 'textmate', label: 'TextMate' },
2577
+ { value: 'vim', label: 'Vim' },
2578
+ { value: 'vscode', label: 'Visual Studio Code' },
2579
+ { value: 'vscodium', label: 'VSCodium' },
2580
+ { value: 'windsurf', label: 'Windsurf' },
2581
+ { value: 'zed', label: 'Zed' },
2582
+ ];
2583
+
2584
+ function initHerbDevTools(options = {}) {
2585
+ const overlay = new HerbOverlay(options);
2586
+ if (typeof window !== 'undefined') {
2587
+ window.HerbDevTools._overlay = overlay;
2588
+ window.HerbDevTools._errorOverlay = overlay.errorOverlay;
2589
+ }
2590
+ return overlay;
2591
+ }
2592
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
2593
+ const hasDebugMode = document.querySelector('meta[name="herb-debug-mode"]')?.getAttribute('content') === 'true';
2594
+ const hasDebugErb = document.querySelector('[data-herb-debug-erb]') !== null;
2595
+ const hasValidationErrors = document.querySelector('template[data-herb-validation-errors]') !== null;
2596
+ const hasValidationError = document.querySelector('template[data-herb-validation-error]') !== null;
2597
+ const hasParserErrors = document.querySelector('template[data-herb-parser-error]') !== null;
2598
+ const hasOptimizationMismatches = document.querySelector('template[data-herb-optimization-mismatch]') !== null;
2599
+ const shouldAutoInit = hasDebugMode || hasDebugErb || hasValidationErrors || hasValidationError || hasParserErrors || hasOptimizationMismatches;
2600
+ if (shouldAutoInit) {
2601
+ document.addEventListener('DOMContentLoaded', () => {
2602
+ initHerbDevTools();
2603
+ });
2604
+ }
2605
+ }
2606
+ if (typeof window !== 'undefined') {
2607
+ window.HerbDevTools = {
2608
+ init: initHerbDevTools,
2609
+ HerbOverlay,
2610
+ ErrorOverlay
2611
+ };
2612
+ }
2613
+
2614
+ class ReActionViewDevTools {
2615
+ constructor(options = {}) {
2616
+ this.options = options;
2617
+ this.herbOverlay = null;
2618
+ if (options.autoInit !== false) {
2619
+ this.init();
2620
+ }
2621
+ }
2622
+ init() {
2623
+ if (this.herbOverlay) {
2624
+ this.destroy();
2625
+ }
2626
+ this.herbOverlay = initHerbDevTools({
2627
+ projectPath: this.options.projectPath,
2628
+ ...this.options
2629
+ });
2630
+ return this.herbOverlay;
2631
+ }
2632
+ destroy() {
2633
+ if (this.herbOverlay) {
2634
+ const existingMenu = document.querySelector(".herb-floating-menu");
2635
+ if (existingMenu) {
2636
+ existingMenu.remove();
2637
+ }
2638
+ }
2639
+ this.herbOverlay = null;
2640
+ }
2641
+ getHerbOverlay() {
2642
+ return this.herbOverlay;
2643
+ }
2644
+ static getInstance() {
2645
+ return ReActionViewDevTools.instance;
2646
+ }
2647
+ static setInstance(instance) {
2648
+ ReActionViewDevTools.instance = instance;
2649
+ }
2650
+ }
2651
+ ReActionViewDevTools.instance = null;
2652
+ function initReActionViewDevTools(options = {}) {
2653
+ const existingInstance = ReActionViewDevTools.getInstance();
2654
+ if (existingInstance) {
2655
+ existingInstance.destroy();
2656
+ }
2657
+ const instance = new ReActionViewDevTools(options);
2658
+ ReActionViewDevTools.setInstance(instance);
2659
+ return instance;
2660
+ }
2661
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
2662
+ let isInitializing = false;
2663
+ const initializeDevTools = () => {
2664
+ var _a, _b, _c;
2665
+ if (isInitializing) {
2666
+ console.log("ReActionView dev tools initialization already in progress, skipping...");
2667
+ return;
2668
+ }
2669
+ 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;
2670
+ if (!shouldAutoInit) {
2671
+ console.log("ReActionView debug mode not detected, skipping dev tools initialization");
2672
+ return;
2673
+ }
2674
+ isInitializing = true;
2675
+ try {
2676
+ const projectPath = (_c = (_b = document.querySelector(`meta[name="herb-project-path"]`)) === null || _b === void 0 ? void 0 : _b.getAttribute("content")) !== null && _c !== void 0 ? _c : undefined;
2677
+ initReActionViewDevTools({
2678
+ projectPath,
2679
+ autoInit: true
2680
+ });
2681
+ }
2682
+ catch (error) {
2683
+ console.warn("Could not initialize ReActionView dev tools:", error);
2684
+ }
2685
+ finally {
2686
+ isInitializing = false;
2687
+ }
2688
+ };
2689
+ if (document.readyState === "loading") {
2690
+ document.addEventListener("DOMContentLoaded", initializeDevTools, { once: true });
2691
+ }
2692
+ else {
2693
+ setTimeout(initializeDevTools, 0);
2694
+ }
2695
+ document.addEventListener("turbo:load", initializeDevTools);
2696
+ document.addEventListener("turbo:render", initializeDevTools);
2697
+ document.addEventListener("turbo:visit", initializeDevTools);
2698
+ }
2699
+ if (typeof window !== "undefined") {
2700
+ window.ReActionViewDevTools = {
2701
+ init: initReActionViewDevTools,
2702
+ ReActionViewDevTools,
2703
+ HerbOverlay
2704
+ };
2705
+ }
2706
+
2707
+ exports.HerbOverlay = HerbOverlay;
2708
+ exports.ReActionViewDevTools = ReActionViewDevTools;
2709
+ exports.initReActionViewDevTools = initReActionViewDevTools;
1912
2710
 
1913
2711
  }));
1914
2712
  //# sourceMappingURL=reactionview-dev-tools.umd.js.map