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,3 +1,587 @@
1
+ const DEFAULT_RECONNECT_INTERVAL = 1000;
2
+ const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
3
+ class Connection {
4
+ constructor(options) {
5
+ this.socket = null;
6
+ this.reconnectAttempts = 0;
7
+ this.reconnectTimer = null;
8
+ this.givenUp = false;
9
+ this.options = {
10
+ reconnectInterval: DEFAULT_RECONNECT_INTERVAL,
11
+ maxReconnectAttempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
12
+ ...options,
13
+ };
14
+ }
15
+ get reconnectInterval() {
16
+ return this.options.reconnectInterval ?? DEFAULT_RECONNECT_INTERVAL;
17
+ }
18
+ get maxReconnectAttempts() {
19
+ return this.options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
20
+ }
21
+ connect() {
22
+ if (this.socket?.readyState === WebSocket.OPEN)
23
+ return;
24
+ this.givenUp = false;
25
+ this.reconnectAttempts = 0;
26
+ this.attemptConnect();
27
+ }
28
+ disconnect() {
29
+ if (this.reconnectTimer) {
30
+ clearTimeout(this.reconnectTimer);
31
+ this.reconnectTimer = null;
32
+ }
33
+ this.givenUp = false;
34
+ this.reconnectAttempts = this.maxReconnectAttempts;
35
+ if (this.socket) {
36
+ this.socket.close();
37
+ this.socket = null;
38
+ }
39
+ }
40
+ retry() {
41
+ if (this.reconnectTimer) {
42
+ clearTimeout(this.reconnectTimer);
43
+ this.reconnectTimer = null;
44
+ }
45
+ if (this.socket) {
46
+ this.socket.onclose = null;
47
+ this.socket.close();
48
+ this.socket = null;
49
+ }
50
+ this.givenUp = false;
51
+ this.reconnectAttempts = 0;
52
+ this.attemptConnect();
53
+ }
54
+ get hasGivenUp() {
55
+ return this.givenUp;
56
+ }
57
+ attemptConnect() {
58
+ try {
59
+ this.socket = new WebSocket(this.options.url);
60
+ this.socket.onopen = () => {
61
+ this.reconnectAttempts = 0;
62
+ this.options.onConnect?.();
63
+ };
64
+ this.socket.onmessage = (event) => {
65
+ try {
66
+ const message = JSON.parse(event.data);
67
+ this.options.onMessage?.(message);
68
+ }
69
+ catch (error) {
70
+ console.warn("[herb-client] failed to parse message:", error);
71
+ }
72
+ };
73
+ this.socket.onclose = () => {
74
+ console.debug("[herb-client] disconnected from dev server");
75
+ this.options.onDisconnect?.();
76
+ this.scheduleReconnect();
77
+ };
78
+ this.socket.onerror = () => {
79
+ try {
80
+ this.socket?.close();
81
+ }
82
+ catch {
83
+ this.scheduleReconnect();
84
+ }
85
+ };
86
+ }
87
+ catch {
88
+ this.scheduleReconnect();
89
+ }
90
+ }
91
+ scheduleReconnect() {
92
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
93
+ console.debug("[herb-client] gave up reconnecting after %d attempts", this.reconnectAttempts);
94
+ this.givenUp = true;
95
+ this.options.onGivenUp?.();
96
+ return;
97
+ }
98
+ this.reconnectAttempts++;
99
+ const delay = Math.min(this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), 10000);
100
+ this.options.onReconnecting?.(this.reconnectAttempts, this.maxReconnectAttempts, delay);
101
+ this.reconnectTimer = setTimeout(() => {
102
+ this.attemptConnect();
103
+ }, delay);
104
+ }
105
+ }
106
+
107
+ const colors = {
108
+ green: "#22c55e",
109
+ greenDark: "#059669",
110
+ greenLight: "#ecfdf5",
111
+ greenBorder: "#10b981",
112
+ greenGlow: "0 0 4px rgba(34, 197, 94, 0.5)",
113
+ red: "#ef4444",
114
+ redDark: "#991b1b",
115
+ redLight: "#fef2f2",
116
+ amber: "#f59e0b",
117
+ amberDark: "#92400e",
118
+ amberDarker: "#d97706",
119
+ amberLight: "#fffbeb",
120
+ gray: "#6b7280",
121
+ grayLighter: "#a16207",
122
+ };
123
+
124
+ const TOAST_DURATION = 3000;
125
+ const TOAST_FADE_DURATION = 300;
126
+ const TOAST_ID = "herbDevServerToast";
127
+ const TOAST_STYLES = {
128
+ connected: { background: colors.greenLight, border: colors.greenBorder, text: "#065f46", icon: "\u{1F7E2}" },
129
+ disconnected: { background: colors.redLight, border: colors.red, text: colors.redDark, icon: "\u{1F534}" },
130
+ warning: { background: colors.amberLight, border: colors.amber, text: colors.amberDark, icon: "\u{1F7E1}" },
131
+ };
132
+ class Toast {
133
+ static show(message, type) {
134
+ document.getElementById(TOAST_ID)?.remove();
135
+ const style = TOAST_STYLES[type];
136
+ const toast = document.createElement("div");
137
+ toast.id = TOAST_ID;
138
+ 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;`;
139
+ const icon = document.createElement("span");
140
+ icon.textContent = style.icon;
141
+ const text = document.createElement("span");
142
+ text.textContent = message;
143
+ toast.appendChild(icon);
144
+ toast.appendChild(text);
145
+ document.body.appendChild(toast);
146
+ setTimeout(() => {
147
+ toast.style.opacity = "0";
148
+ setTimeout(() => toast.remove(), TOAST_FADE_DURATION);
149
+ }, TOAST_DURATION);
150
+ }
151
+ }
152
+
153
+ class ConnectionDot {
154
+ constructor(client) {
155
+ this.reconnectCountdown = null;
156
+ this.client = client;
157
+ }
158
+ apply() {
159
+ if (this.reconnectCountdown) {
160
+ clearInterval(this.reconnectCountdown);
161
+ this.reconnectCountdown = null;
162
+ }
163
+ const dot = document.getElementById("herbConnectionDot");
164
+ if (!dot)
165
+ return;
166
+ const panelDot = document.getElementById("herbDevServerDot");
167
+ const panelStatus = document.getElementById("herbDevServerStatus");
168
+ const panelRetry = document.getElementById("herbDevServerRetry");
169
+ const retryHandler = (e) => { e.stopPropagation(); this.client.retry(); };
170
+ const state = this.client.getState();
171
+ switch (state) {
172
+ case "connected":
173
+ this.setDotStyle(dot, colors.green, true, true);
174
+ dot.style.cursor = "default";
175
+ dot.title = "Connected to herb dev server";
176
+ dot.onclick = null;
177
+ this.updatePanel(panelDot, panelStatus, panelRetry, {
178
+ dotColor: colors.green,
179
+ statusText: `Dev Server connected (port ${this.client.getPort()})`,
180
+ statusColor: colors.greenDark,
181
+ retryVisible: false,
182
+ });
183
+ break;
184
+ case "disconnected":
185
+ this.setDotStyle(dot, colors.red, false, false);
186
+ dot.style.cursor = "default";
187
+ dot.title = "Disconnected from herb dev server";
188
+ dot.onclick = null;
189
+ this.updatePanel(panelDot, panelStatus, panelRetry, {
190
+ dotColor: colors.red,
191
+ statusText: "Dev Server disconnected",
192
+ statusColor: colors.gray,
193
+ retryVisible: true,
194
+ retryHandler,
195
+ });
196
+ break;
197
+ case "given-up":
198
+ this.setDotStyle(dot, colors.amber, false, false);
199
+ dot.style.cursor = "pointer";
200
+ dot.title = "Connection to herb dev server failed — click to retry";
201
+ dot.onclick = retryHandler;
202
+ this.updatePanel(panelDot, panelStatus, panelRetry, {
203
+ dotColor: colors.amber,
204
+ statusText: "Dev Server not available",
205
+ statusColor: colors.amberDarker,
206
+ retryVisible: true,
207
+ retryHandler,
208
+ });
209
+ break;
210
+ }
211
+ }
212
+ updateReconnectCountdown(attempt, maxAttempts, delay) {
213
+ const panelStatus = document.getElementById("herbDevServerStatus");
214
+ if (!panelStatus)
215
+ return;
216
+ if (this.reconnectCountdown) {
217
+ clearInterval(this.reconnectCountdown);
218
+ this.reconnectCountdown = null;
219
+ }
220
+ let remaining = Math.ceil(delay / 1000);
221
+ panelStatus.textContent = `Retry ${attempt}/${maxAttempts} in ${remaining}s`;
222
+ panelStatus.style.color = colors.gray;
223
+ this.reconnectCountdown = setInterval(() => {
224
+ remaining--;
225
+ if (remaining <= 0) {
226
+ if (this.reconnectCountdown) {
227
+ clearInterval(this.reconnectCountdown);
228
+ this.reconnectCountdown = null;
229
+ }
230
+ panelStatus.textContent = `Retry ${attempt}/${maxAttempts} connecting...`;
231
+ return;
232
+ }
233
+ panelStatus.textContent = `Retry ${attempt}/${maxAttempts} in ${remaining}s`;
234
+ }, 1000);
235
+ }
236
+ updatePanel(panelDot, panelStatus, panelRetry, options) {
237
+ if (panelDot)
238
+ this.setDotStyle(panelDot, options.dotColor, false, false);
239
+ if (panelStatus) {
240
+ panelStatus.textContent = options.statusText;
241
+ panelStatus.style.color = options.statusColor;
242
+ }
243
+ if (panelRetry) {
244
+ panelRetry.style.display = options.retryVisible ? "block" : "none";
245
+ if (options.retryHandler) {
246
+ panelRetry.onclick = options.retryHandler;
247
+ }
248
+ }
249
+ }
250
+ setDotStyle(element, background, glow, pulse) {
251
+ element.style.background = background;
252
+ element.style.boxShadow = glow ? colors.greenGlow : "none";
253
+ element.style.animation = pulse ? "herb-dot-pulse 2s ease-in-out infinite" : "none";
254
+ }
255
+ }
256
+
257
+ const ALERT_ID = "herbProjectMismatchAlert";
258
+ class MismatchAlert {
259
+ static show(serverProject, clientProject) {
260
+ if (document.getElementById(ALERT_ID))
261
+ return;
262
+ const serverName = serverProject.split("/").pop() ?? serverProject;
263
+ const clientName = clientProject.split("/").pop() ?? clientProject;
264
+ const alert = document.createElement("div");
265
+ alert.id = ALERT_ID;
266
+ 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;`;
267
+ const iconElement = document.createElement("span");
268
+ iconElement.style.cssText = "font-size:18px;line-height:1;";
269
+ iconElement.textContent = "\u26A0\uFE0F";
270
+ const content = document.createElement("div");
271
+ content.style.flex = "1";
272
+ const title = document.createElement("div");
273
+ title.style.cssText = "font-weight:600;margin-bottom:4px;";
274
+ title.textContent = "Herb Dev Server mismatch";
275
+ const description = document.createElement("div");
276
+ description.style.cssText = `font-size:12px;color:${colors.grayLighter};`;
277
+ description.textContent = `The dev server is watching ${serverName} but this page is from ${clientName}. Messages will be ignored.`;
278
+ content.appendChild(title);
279
+ content.appendChild(description);
280
+ const dismiss = document.createElement("button");
281
+ dismiss.style.cssText = `background:none;border:none;cursor:pointer;font-size:16px;color:${colors.amberDark};padding:0;line-height:1;`;
282
+ dismiss.textContent = "\u2715";
283
+ dismiss.addEventListener("click", () => alert.remove());
284
+ alert.appendChild(iconElement);
285
+ alert.appendChild(content);
286
+ alert.appendChild(dismiss);
287
+ document.body.appendChild(alert);
288
+ const panelStatus = document.getElementById("herbDevServerStatus");
289
+ const panelDot = document.getElementById("herbDevServerDot");
290
+ if (panelStatus) {
291
+ panelStatus.textContent = `Wrong project (${serverName})`;
292
+ panelStatus.style.color = colors.amberDarker;
293
+ }
294
+ if (panelDot) {
295
+ panelDot.style.background = colors.amber;
296
+ }
297
+ }
298
+ }
299
+
300
+ function applyPatch(message) {
301
+ const selector = `[data-herb-debug-file-relative-path="${message.file}"]`;
302
+ const roots = document.querySelectorAll(selector);
303
+ if (roots.length === 0) {
304
+ console.debug("[herb-client] no roots found for selector:", selector);
305
+ return false;
306
+ }
307
+ let applied = false;
308
+ for (const operation of message.operations) {
309
+ let operationApplied = false;
310
+ for (let i = 0; i < roots.length; i++) {
311
+ if (applyOperation(roots[i], operation)) {
312
+ operationApplied = true;
313
+ }
314
+ else {
315
+ console.debug(`[herb-client] operation not applied to root ${i}:`, roots[i]);
316
+ }
317
+ }
318
+ if (operationApplied) {
319
+ applied = true;
320
+ }
321
+ else {
322
+ console.debug("[herb-client] operation not applied:", operation);
323
+ }
324
+ }
325
+ return applied;
326
+ }
327
+ function applyOperation(root, operation) {
328
+ switch (operation.type) {
329
+ case "text_changed":
330
+ return applyTextChange(root, operation);
331
+ case "attribute_value_changed":
332
+ return applyAttributeChange(root, operation);
333
+ case "attribute_added":
334
+ return applyAttributeAdd(root, operation);
335
+ case "attribute_removed":
336
+ return applyAttributeRemove(root, operation);
337
+ default:
338
+ console.debug(`[herb-client] unhandled operation type: ${operation.type}`);
339
+ return false;
340
+ }
341
+ }
342
+ function parseAttribute(value) {
343
+ const match = value.match(/^([^=]+)="(.*)"$/);
344
+ if (!match)
345
+ return null;
346
+ return { name: match[1], value: match[2] };
347
+ }
348
+ function findTarget(root, operation) {
349
+ if (!operation.old_value)
350
+ return null;
351
+ const attribute = parseAttribute(operation.old_value);
352
+ if (!attribute)
353
+ return null;
354
+ if (root.getAttribute(attribute.name) === attribute.value)
355
+ return root;
356
+ const target = root.querySelector(`[${attribute.name}="${CSS.escape(attribute.value)}"]`);
357
+ return target;
358
+ }
359
+ function findTextTarget(root, operation) {
360
+ if (operation.old_value === null)
361
+ return null;
362
+ const trimmedOld = operation.old_value.trim();
363
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
364
+ let node;
365
+ while ((node = walker.nextNode())) {
366
+ if (node.textContent?.trim() === trimmedOld) {
367
+ return node;
368
+ }
369
+ }
370
+ return null;
371
+ }
372
+ function applyTextChange(root, operation) {
373
+ if (operation.new_value === null)
374
+ return false;
375
+ const textNode = findTextTarget(root, operation);
376
+ if (textNode) {
377
+ textNode.textContent = operation.new_value;
378
+ return true;
379
+ }
380
+ return false;
381
+ }
382
+ function applyAttributeChange(root, operation) {
383
+ if (operation.old_value === null || operation.new_value === null)
384
+ return false;
385
+ const node = findTarget(root, operation);
386
+ if (!node)
387
+ return false;
388
+ const newAttr = parseAttribute(operation.new_value);
389
+ if (!newAttr)
390
+ return false;
391
+ node.setAttribute(newAttr.name, newAttr.value);
392
+ return true;
393
+ }
394
+ function applyAttributeAdd(root, operation) {
395
+ if (operation.new_value === null)
396
+ return false;
397
+ const attribute = parseAttribute(operation.new_value);
398
+ if (!attribute)
399
+ return false;
400
+ const node = findTarget(root, operation) ?? root;
401
+ node.setAttribute(attribute.name, attribute.value);
402
+ return true;
403
+ }
404
+ function applyAttributeRemove(root, operation) {
405
+ if (operation.old_value === null)
406
+ return false;
407
+ const node = findTarget(root, operation);
408
+ if (!node)
409
+ return false;
410
+ const match = operation.old_value.match(/^([^=]+)(?:=".*")?$/);
411
+ if (!match)
412
+ return false;
413
+ node.removeAttribute(match[1]);
414
+ return true;
415
+ }
416
+
417
+ const DEFAULT_PORT = 8592;
418
+ class HerbClient {
419
+ constructor(options = {}) {
420
+ this.state = "disconnected";
421
+ this.hasConnectedBefore = false;
422
+ this.projectMatch = null;
423
+ this.options = options;
424
+ const port = options.port ?? this.detectPort() ?? DEFAULT_PORT;
425
+ const host = options.host ?? "localhost";
426
+ this.port = port;
427
+ this.connectionDot = new ConnectionDot(this);
428
+ this.connection = new Connection({
429
+ url: `ws://${host}:${port}`,
430
+ onMessage: (message) => this.handleMessage(message),
431
+ onConnect: () => this.onConnect(),
432
+ onDisconnect: () => this.onDisconnect(),
433
+ onReconnecting: (attempt, maxAttempts, delay) => this.onReconnecting(attempt, maxAttempts, delay),
434
+ onGivenUp: () => this.onGivenUp(),
435
+ });
436
+ }
437
+ connect() {
438
+ this.connection.connect();
439
+ }
440
+ disconnect() {
441
+ this.connection.disconnect();
442
+ }
443
+ retry() {
444
+ this.updateState("disconnected");
445
+ this.connection.retry();
446
+ }
447
+ getState() {
448
+ return this.state;
449
+ }
450
+ getPort() {
451
+ return this.port;
452
+ }
453
+ applyConnectionDot() {
454
+ this.connectionDot.apply();
455
+ }
456
+ onConnect() {
457
+ const wasDisconnected = this.state === "disconnected" || this.state === "given-up";
458
+ if (this.hasConnectedBefore && wasDisconnected) {
459
+ Toast.show("Herb Dev Server reconnected", "connected");
460
+ }
461
+ this.hasConnectedBefore = true;
462
+ this.updateState("connected");
463
+ this.options.onConnect?.();
464
+ }
465
+ onDisconnect() {
466
+ if (this.hasConnectedBefore && this.state === "connected") {
467
+ Toast.show("Herb Dev Server disconnected", "disconnected");
468
+ }
469
+ this.updateState("disconnected");
470
+ this.options.onDisconnect?.();
471
+ }
472
+ onReconnecting(attempt, maxAttempts, delay) {
473
+ console.debug(`[herb-client] reconnecting (attempt ${attempt}/${maxAttempts}, next try in ${(delay / 1000).toFixed(1)}s)...`);
474
+ this.connectionDot.updateReconnectCountdown(attempt, maxAttempts, delay);
475
+ }
476
+ onGivenUp() {
477
+ this.updateState("given-up");
478
+ Toast.show("Herb Dev Server not available — click the dot to retry", "warning");
479
+ }
480
+ handleMessage(message) {
481
+ if (message.type !== "welcome" && this.projectMatch === false)
482
+ return;
483
+ switch (message.type) {
484
+ case "welcome":
485
+ this.handleWelcome(message);
486
+ break;
487
+ case "patch":
488
+ this.handlePatch(message);
489
+ break;
490
+ case "reload":
491
+ this.handleReload(message);
492
+ break;
493
+ case "error":
494
+ this.handleError(message);
495
+ break;
496
+ case "fixed":
497
+ this.handleFixed(message);
498
+ break;
499
+ }
500
+ }
501
+ handleWelcome(message) {
502
+ const clientProject = document.querySelector('meta[name="herb-project-path"]')?.getAttribute("content");
503
+ if (clientProject && message.project && clientProject !== message.project) {
504
+ this.projectMatch = false;
505
+ console.warn(`[herb-client] project mismatch — server: ${message.project}, client: ${clientProject}. Ignoring messages.`);
506
+ this.updateState("disconnected");
507
+ MismatchAlert.show(message.project, clientProject);
508
+ }
509
+ else {
510
+ this.projectMatch = true;
511
+ }
512
+ }
513
+ handlePatch(message) {
514
+ this.options.onPatch?.(message);
515
+ const applied = applyPatch(message);
516
+ if (!applied) {
517
+ window.location.reload();
518
+ }
519
+ }
520
+ handleReload(message) {
521
+ this.options.onReload?.(message);
522
+ window.location.reload();
523
+ }
524
+ handleError(message) {
525
+ this.options.onError?.(message);
526
+ const overlay = this.getErrorOverlay();
527
+ if (overlay) {
528
+ const errors = message.errors.map((error) => ({
529
+ severity: "error",
530
+ message: error.message,
531
+ name: error.name,
532
+ location: { line: error.line, column: error.column },
533
+ }));
534
+ overlay.showErrors(errors, message.file);
535
+ }
536
+ }
537
+ handleFixed(message) {
538
+ this.options.onFixed?.(message);
539
+ this.getErrorOverlay()?.clearErrors();
540
+ }
541
+ updateState(state) {
542
+ this.state = state;
543
+ this.connectionDot.apply();
544
+ }
545
+ getErrorOverlay() {
546
+ const devTools = window.HerbDevTools;
547
+ return devTools?._errorOverlay ?? devTools?._overlay?.errorOverlay ?? null;
548
+ }
549
+ detectPort() {
550
+ const meta = document.querySelector('meta[name="herb-dev-server-port"]');
551
+ if (meta) {
552
+ const port = parseInt(meta.getAttribute("content") ?? "", 10);
553
+ if (!isNaN(port))
554
+ return port;
555
+ }
556
+ return null;
557
+ }
558
+ }
559
+
560
+ let instance = null;
561
+ function initHerbClient(options = {}) {
562
+ if (instance) {
563
+ instance.disconnect();
564
+ }
565
+ instance = new HerbClient(options);
566
+ window.__herbClient = instance;
567
+ instance.connect();
568
+ return instance;
569
+ }
570
+ function autoInitialize() {
571
+ const debugMeta = document.querySelector('meta[name="herb-debug-mode"]');
572
+ if (!debugMeta || debugMeta.getAttribute("content") !== "true")
573
+ return;
574
+ initHerbClient();
575
+ }
576
+ if (typeof document !== "undefined") {
577
+ if (document.readyState === "loading") {
578
+ document.addEventListener("DOMContentLoaded", autoInitialize);
579
+ }
580
+ else {
581
+ autoInitialize();
582
+ }
583
+ }
584
+
1
585
  function styleInject(css, ref) {
2
586
  if ( ref === void 0 ) ref = {};
3
587
  var insertAt = ref.insertAt;
@@ -25,9 +609,182 @@ function styleInject(css, ref) {
25
609
  }
26
610
  }
27
611
 
28
- var css_248z = ".herb-overlay-label{background:rgba(0,0,0,.8);border-radius:3px;color:#fff;cursor:pointer;display:block;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,monospace;font-size:11px;font-weight:500;left:4px;line-height:1.2;padding:2px 6px;position:absolute;top:-18px;transition:all .2s ease;white-space:nowrap;z-index:1000}.herb-overlay-label:hover{background:rgba(0,0,0,.9);color:#374151;transform:scale(1.02);z-index:1001}[data-herb-debug-outline-type*=view]>.herb-overlay-label{background:#dbeafe;border-color:#93c5fd;color:#1e40af}[data-herb-debug-outline-type*=partial]>.herb-overlay-label{background:#d1fae5;border-color:#6ee7b7;color:#065f46}[data-herb-debug-outline-type*=component]>.herb-overlay-label{background:#fef3c7;border-color:#fcd34d;color:#92400e}[data-herb-debug-outline-type*=erb-output]{transition:all .3s ease}.herb-tooltip{background:#fff;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.12),0 2px 8px rgba(0,0,0,.08);display:flex;flex-direction:column;font-family:SF Mono,Monaco,Inconsolata,Fira Code,monospace;font-size:14px;gap:12px;max-width:calc(100vw - 16px);opacity:0;overflow:visible;padding:16px 20px;pointer-events:none;position:fixed;transition:opacity .2s ease,visibility .2s ease;visibility:hidden;white-space:nowrap;z-index:10001}.herb-tooltip.visible{opacity:1;pointer-events:auto;visibility:visible}.herb-tooltip .herb-location{align-items:center;background:#f8f9fa;border-radius:12px 12px 0 0;color:#6b7280;cursor:pointer;display:flex;font-size:13px;font-weight:500;gap:12px;justify-content:space-between;margin:-16px -20px 0;padding:12px 20px;transition:all .2s ease}.herb-tooltip .herb-location:hover{background:#f1f3f4;color:#374151}.herb-copy-path-btn{background:transparent;border:none;border-radius:4px;color:#6b7280;cursor:pointer;flex-shrink:0;font-size:14px;padding:4px;position:relative;transition:all .2s ease}.herb-copy-path-btn:hover{background:hsla(220,9%,46%,.1);color:#374151}.herb-copy-path-btn:active{transform:scale(.95)}.herb-location:after{background:#1f2937;border-radius:6px;bottom:calc(100% + 8px);color:#fff;content:attr(data-tooltip);font-size:12px;padding:6px 10px;pointer-events:none;white-space:nowrap}.herb-location:after,.herb-location:before{left:50%;opacity:0;position:absolute;transform:translateX(-50%);transition:all .2s ease;visibility:hidden;z-index:10002}.herb-location:before{border:4px solid transparent;border-top-color:#1f2937;bottom:calc(100% + 2px);content:\"\"}.herb-location:hover:after,.herb-location:hover:before{opacity:1;visibility:visible}.herb-location:has(.herb-copy-path-btn:hover):after,.herb-location:has(.herb-copy-path-btn:hover):before{opacity:0!important;visibility:hidden!important}.herb-copy-path-btn:after{background:#1f2937;border-radius:6px;color:#fff;content:attr(data-tooltip);font-size:12px;padding:6px 10px;pointer-events:none;top:-36px;white-space:nowrap}.herb-copy-path-btn:after,.herb-copy-path-btn:before{left:50%;opacity:0;position:absolute;transform:translateX(-50%);transition:all .2s ease;visibility:hidden;z-index:10003}.herb-copy-path-btn:before{border:4px solid transparent;border-bottom-color:#1f2937;content:\"\";top:-6px}.herb-copy-path-btn:hover:after,.herb-copy-path-btn:hover:before{opacity:1;visibility:visible}.herb-tooltip .herb-erb-code{color:#111827;cursor:text;font-size:16px;font-weight:600;letter-spacing:-.025em;user-select:text}.herb-tooltip:before{bottom:-8px;content:\"\";height:8px;left:0;pointer-events:auto;position:absolute;right:0}.herb-tooltip:after{border:6px solid transparent;border-top-color:#e5e7eb;bottom:-6px;content:\"\";left:50%;pointer-events:none;position:absolute;transform:translateX(-50%);z-index:10000}.herb-floating-menu{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;position:fixed;right:0;top:0;z-index:2147483643}.herb-menu-trigger{align-items:center;background:#fff;border:1px solid silver;border-radius:0 0 0 10px;border-right:none;border-top:none;box-shadow:0 1px 3px rgba(0,0,0,.1);cursor:pointer;display:flex;font-size:12px;gap:4px;justify-content:center;padding:4px 7px;position:relative;transition:all .2s ease;z-index:2147483640}.herb-menu-trigger:hover{background:#f9fafb;border-color:#9ca3af;box-shadow:0 4px 12px rgba(0,0,0,.15)}.herb-menu-trigger:active{transform:scale(.98)}.herb-menu-trigger.has-active-options{background:#dbeafe;border-color:#3b82f6}.herb-menu-trigger.has-active-options:hover{background:#bfdbfe;border-color:#2563eb}.herb-menu-trigger.has-active-options .herb-text{color:#1d4ed8}.herb-icon{display:block;font-size:14px;line-height:1}.herb-text{color:#555;font-size:11px;font-weight:600;letter-spacing:.2px}.herb-menu-panel{background:#fff;border:1px solid silver;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);min-width:280px;opacity:0;padding:0;position:absolute;right:10px;top:28px;transform:translateY(-10px) scale(.95);transform-origin:top right;transition:all .3s cubic-bezier(.4,0,.2,1);visibility:hidden}.herb-menu-panel.open{opacity:1;transform:translateY(0) scale(1);visibility:visible}.herb-menu-header{background:#f9fafb;border-bottom:1px solid #e5e7eb;border-radius:8px 8px 0 0;color:#374151;font-size:14px;font-weight:600;padding:16px 20px}.herb-toggle-item{border-bottom:1px solid #f3f4f6;padding:12px 20px}.herb-toggle-item:last-child{border-bottom:none;border-radius:0 0 8px 8px}.herb-nested-toggle{border-left:2px solid #f3f4f6;margin-top:8px;padding-left:24px;transition:all .3s ease}.herb-nested-label{opacity:.8}.herb-nested-label .herb-toggle-text{color:#6b7280;font-size:13px}.herb-nested-switch{background:#e5e7eb;height:20px;width:36px}.herb-nested-switch:after{height:14px;left:3px;top:3px;width:14px}.herb-toggle-input:checked+.herb-nested-switch:after{transform:translateX(16px)}.herb-toggle-label{align-items:center;cursor:pointer;display:flex;gap:12px;user-select:none}.herb-toggle-input{display:none}.herb-toggle-switch{background:#cbd5e1;border-radius:12px;flex-shrink:0;height:24px;position:relative;transition:background .3s ease;width:44px}.herb-toggle-switch:after{background:#fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2);content:\"\";height:18px;left:3px;position:absolute;top:3px;transition:transform .3s ease;width:18px}.herb-toggle-input:checked+.herb-toggle-switch{background:#8b5cf6}.herb-toggle-input:checked+.herb-toggle-switch:after{transform:translateX(20px)}.herb-toggle-text{color:#374151;flex:1;font-size:14px}.herb-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}";
612
+ 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}";
29
613
  styleInject(css_248z);
30
614
 
615
+ const optimizationMismatches = new Set();
616
+ let optimizationBadgeInitialized = false;
617
+ function scanForOptimizationMismatches() {
618
+ const templates = document.querySelectorAll('template[data-herb-optimization-mismatch]');
619
+ templates.forEach((template) => {
620
+ optimizationMismatches.add(template.getAttribute('data-filename') || '(unknown)');
621
+ template.remove();
622
+ });
623
+ if (optimizationMismatches.size > 0) {
624
+ renderOptimizationBadge();
625
+ }
626
+ }
627
+ function renderOptimizationBadge() {
628
+ document.querySelector('.herb-optimization-badge')?.remove();
629
+ document.querySelector('.herb-optimization-panel')?.remove();
630
+ const filenames = Array.from(optimizationMismatches);
631
+ const projectPath = document.querySelector('meta[name="herb-project-path"]')?.getAttribute('content') || '';
632
+ const displayNames = filenames.map(f => projectPath && f.startsWith(projectPath) ? f.slice(projectPath.length).replace(/^\//, '') : f);
633
+ const title = `\u26A0\uFE0F ${filenames.length} Compile-Time Optimization Mismatch${filenames.length === 1 ? '' : 'es'}`;
634
+ if (!optimizationBadgeInitialized) {
635
+ optimizationBadgeInitialized = true;
636
+ const style = document.createElement('style');
637
+ style.className = 'herb-optimization-badge-style';
638
+ style.textContent = `
639
+ .herb-floating-menu {
640
+ display: flex;
641
+ flex-direction: row;
642
+ align-items: flex-start;
643
+ }
644
+
645
+ .herb-optimization-badge {
646
+ background: #fffbeb;
647
+ color: #92400e;
648
+ font-size: 11px;
649
+ font-weight: 600;
650
+ padding: 4px 7px;
651
+ border-radius: 0 0 0 10px;
652
+ border: 1px solid #f59e0b;
653
+ border-top: none;
654
+ border-right: none;
655
+ cursor: pointer;
656
+ text-align: center;
657
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
658
+ box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
659
+ z-index: 2147483640;
660
+ transition: all 0.2s ease;
661
+ order: -1;
662
+ }
663
+
664
+ .herb-optimization-badge:hover {
665
+ background: #fef3c7;
666
+ border-color: #d97706;
667
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
668
+ }
669
+
670
+ .herb-floating-menu .herb-optimization-badge + .herb-menu-trigger {
671
+ border-radius: 0 0 0 0;
672
+ }
673
+
674
+ .herb-optimization-panel {
675
+ position: fixed;
676
+ top: 30px;
677
+ right: 8px;
678
+ background: white;
679
+ border: 1px solid #e5e7eb;
680
+ border-radius: 8px;
681
+ width: 420px;
682
+ max-height: 400px;
683
+ overflow-y: auto;
684
+ z-index: 2147483642;
685
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
686
+ font-size: 12px;
687
+ color: #374151;
688
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
689
+ display: none;
690
+ }
691
+
692
+ .herb-optimization-panel.visible {
693
+ display: block;
694
+ }
695
+
696
+ .herb-optimization-panel-header {
697
+ background: #fffbeb;
698
+ padding: 10px 14px;
699
+ color: #92400e;
700
+ font-weight: 600;
701
+ font-size: 13px;
702
+ display: flex;
703
+ justify-content: space-between;
704
+ align-items: center;
705
+ border-bottom: 1px solid #fde68a;
706
+ border-radius: 8px 8px 0 0;
707
+ }
708
+
709
+ .herb-optimization-panel-close {
710
+ background: none;
711
+ border: none;
712
+ color: #92400e;
713
+ cursor: pointer;
714
+ font-size: 16px;
715
+ padding: 0 4px;
716
+ }
717
+
718
+ .herb-optimization-panel-close:hover {
719
+ color: #78350f;
720
+ }
721
+
722
+ .herb-optimization-panel-list {
723
+ padding: 4px 0;
724
+ }
725
+
726
+ .herb-optimization-panel-item {
727
+ padding: 6px 14px;
728
+ color: #6b7280;
729
+ border-bottom: 1px solid #f3f4f6;
730
+ word-break: break-all;
731
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
732
+ font-size: 11px;
733
+ }
734
+
735
+ .herb-optimization-panel-item:last-child {
736
+ border-bottom: none;
737
+ }
738
+
739
+ .herb-optimization-panel-hint {
740
+ padding: 8px 14px;
741
+ color: #9ca3af;
742
+ font-size: 11px;
743
+ border-top: 1px solid #e5e7eb;
744
+ background: #f9fafb;
745
+ border-radius: 0 0 8px 8px;
746
+ }
747
+ `;
748
+ document.head.appendChild(style);
749
+ }
750
+ const panel = document.createElement('div');
751
+ panel.className = 'herb-optimization-panel';
752
+ panel.innerHTML = `
753
+ <div class="herb-optimization-panel-header">
754
+ <span>${title}</span>
755
+ <button class="herb-optimization-panel-close">&times;</button>
756
+ </div>
757
+
758
+ <div class="herb-optimization-panel-list">
759
+ ${displayNames.map(f => `<div class="herb-optimization-panel-item">${f.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>`).join('')}
760
+ </div>
761
+
762
+ <div class="herb-optimization-panel-hint">
763
+ Check Rails log for details. Disable with <code>config.verify_optimizations = false</code>.
764
+ </div>
765
+ `;
766
+ document.body.appendChild(panel);
767
+ panel.querySelector('.herb-optimization-panel-close')?.addEventListener('click', () => {
768
+ panel.classList.remove('visible');
769
+ });
770
+ const badge = document.createElement('div');
771
+ badge.className = 'herb-optimization-badge';
772
+ badge.textContent = `\u26A0\uFE0F ${filenames.length}`;
773
+ badge.title = title;
774
+ badge.addEventListener('click', () => {
775
+ panel.classList.toggle('visible');
776
+ });
777
+ const menu = document.querySelector('.herb-floating-menu');
778
+ if (menu) {
779
+ menu.prepend(badge);
780
+ }
781
+ else {
782
+ badge.style.position = 'fixed';
783
+ badge.style.top = '0';
784
+ badge.style.right = '0';
785
+ document.body.appendChild(badge);
786
+ }
787
+ }
31
788
  class ErrorOverlay {
32
789
  constructor() {
33
790
  this.overlay = null;
@@ -37,6 +794,7 @@ class ErrorOverlay {
37
794
  }
38
795
  init() {
39
796
  this.detectValidationErrors();
797
+ scanForOptimizationMismatches();
40
798
  const hasParserErrors = document.querySelector('.herb-parser-error-overlay') !== null;
41
799
  if (this.getTotalErrorCount() > 0) {
42
800
  this.createOverlay();
@@ -372,6 +1130,28 @@ class ErrorOverlay {
372
1130
  getErrorCount() {
373
1131
  return this.getTotalErrorCount();
374
1132
  }
1133
+ showErrors(errors, filename) {
1134
+ this.allValidationData = this.allValidationData.filter(data => data.filename !== filename);
1135
+ this.allValidationData.push({
1136
+ validationErrors: errors,
1137
+ filename,
1138
+ timestamp: new Date().toISOString(),
1139
+ });
1140
+ if (this.overlay) {
1141
+ this.overlay.remove();
1142
+ this.overlay = null;
1143
+ }
1144
+ this.createOverlay();
1145
+ this.show();
1146
+ }
1147
+ clearErrors() {
1148
+ this.allValidationData = [];
1149
+ if (this.overlay) {
1150
+ this.overlay.remove();
1151
+ this.overlay = null;
1152
+ this.isVisible = false;
1153
+ }
1154
+ }
375
1155
  displayParserErrorOverlay(htmlContent) {
376
1156
  const existingOverlay = document.querySelector('.herb-parser-error-overlay');
377
1157
  if (existingOverlay) {
@@ -884,11 +1664,18 @@ class HerbOverlay {
884
1664
  this.init();
885
1665
  }
886
1666
  }
1667
+ syncConnectionDot() {
1668
+ const herbClient = window.__herbClient;
1669
+ if (herbClient) {
1670
+ herbClient.applyConnectionDot();
1671
+ }
1672
+ }
887
1673
  init() {
888
1674
  this.loadProjectPath();
889
1675
  this.loadDefaultEditor();
890
1676
  this.loadSettings();
891
1677
  this.injectMenu();
1678
+ this.syncConnectionDot();
892
1679
  this.setupMenuToggle();
893
1680
  this.setupToggleSwitches();
894
1681
  this.setupEditorDropdown();
@@ -975,11 +1762,18 @@ class HerbOverlay {
975
1762
  <button class="herb-menu-trigger" id="herbMenuTrigger">
976
1763
  <span class="herb-icon">🌿</span>
977
1764
  <span class="herb-text">Herb</span>
1765
+ <span id="herbConnectionDot" class="herb-connection-dot" data-herb-connection-dot></span>
978
1766
  </button>
979
1767
 
980
1768
  <div class="herb-menu-panel" id="herbMenuPanel">
981
1769
  <div class="herb-menu-header">Herb Debug Tools</div>
982
1770
 
1771
+ <div id="herbDevServerSection" class="herb-dev-server-section">
1772
+ <span id="herbDevServerDot" class="herb-dev-server-dot"></span>
1773
+ <span id="herbDevServerStatus" class="herb-dev-server-status">Dev Server</span>
1774
+ <button id="herbDevServerRetry" class="herb-dev-server-retry">Retry</button>
1775
+ </div>
1776
+
983
1777
  <div class="herb-toggle-item">
984
1778
  <label class="herb-toggle-label">
985
1779
  <input type="checkbox" id="herbToggleViewOutlines" class="herb-toggle-input">
@@ -1107,6 +1901,7 @@ class HerbOverlay {
1107
1901
  }
1108
1902
  reinitializeAfterNavigation() {
1109
1903
  this.injectMenu();
1904
+ this.syncConnectionDot();
1110
1905
  this.setupMenuToggle();
1111
1906
  this.setupToggleSwitches();
1112
1907
  this.setupEditorDropdown();
@@ -1781,7 +2576,12 @@ HerbOverlay.EDITOR_OPTIONS = [
1781
2576
  ];
1782
2577
 
1783
2578
  function initHerbDevTools(options = {}) {
1784
- return new HerbOverlay(options);
2579
+ const overlay = new HerbOverlay(options);
2580
+ if (typeof window !== 'undefined') {
2581
+ window.HerbDevTools._overlay = overlay;
2582
+ window.HerbDevTools._errorOverlay = overlay.errorOverlay;
2583
+ }
2584
+ return overlay;
1785
2585
  }
1786
2586
  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1787
2587
  const hasDebugMode = document.querySelector('meta[name="herb-debug-mode"]')?.getAttribute('content') === 'true';
@@ -1789,7 +2589,8 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1789
2589
  const hasValidationErrors = document.querySelector('template[data-herb-validation-errors]') !== null;
1790
2590
  const hasValidationError = document.querySelector('template[data-herb-validation-error]') !== null;
1791
2591
  const hasParserErrors = document.querySelector('template[data-herb-parser-error]') !== null;
1792
- const shouldAutoInit = hasDebugMode || hasDebugErb || hasValidationErrors || hasValidationError || hasParserErrors;
2592
+ const hasOptimizationMismatches = document.querySelector('template[data-herb-optimization-mismatch]') !== null;
2593
+ const shouldAutoInit = hasDebugMode || hasDebugErb || hasValidationErrors || hasValidationError || hasParserErrors || hasOptimizationMismatches;
1793
2594
  if (shouldAutoInit) {
1794
2595
  document.addEventListener('DOMContentLoaded', () => {
1795
2596
  initHerbDevTools();
@@ -1799,7 +2600,8 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1799
2600
  if (typeof window !== 'undefined') {
1800
2601
  window.HerbDevTools = {
1801
2602
  init: initHerbDevTools,
1802
- HerbOverlay
2603
+ HerbOverlay,
2604
+ ErrorOverlay
1803
2605
  };
1804
2606
  }
1805
2607
 
@@ -1853,7 +2655,7 @@ function initReActionViewDevTools(options = {}) {
1853
2655
  if (typeof window !== "undefined" && typeof document !== "undefined") {
1854
2656
  let isInitializing = false;
1855
2657
  const initializeDevTools = () => {
1856
- var _a, _b;
2658
+ var _a, _b, _c;
1857
2659
  if (isInitializing) {
1858
2660
  console.log("ReActionView dev tools initialization already in progress, skipping...");
1859
2661
  return;
@@ -1865,11 +2667,7 @@ if (typeof window !== "undefined" && typeof document !== "undefined") {
1865
2667
  }
1866
2668
  isInitializing = true;
1867
2669
  try {
1868
- let projectPath;
1869
- const railsRoot = (_b = document.querySelector(`meta[name="herb-rails-root"]`)) === null || _b === void 0 ? void 0 : _b.getAttribute("content");
1870
- if (railsRoot) {
1871
- projectPath = railsRoot;
1872
- }
2670
+ 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;
1873
2671
  initReActionViewDevTools({
1874
2672
  projectPath,
1875
2673
  autoInit: true