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.
- checksums.yaml +4 -4
- data/README.md +6 -0
- data/app/assets/javascripts/reactionview-dev-tools.esm.js +808 -10
- data/app/assets/javascripts/reactionview-dev-tools.esm.js.map +1 -1
- data/app/assets/javascripts/reactionview-dev-tools.umd.js +2124 -1326
- data/app/assets/javascripts/reactionview-dev-tools.umd.js.map +1 -1
- data/lib/generators/reactionview/install_generator.rb +6 -0
- data/lib/reactionview/asset_manifest.rb +88 -0
- data/lib/reactionview/config.rb +31 -0
- data/lib/reactionview/middleware/asset_manifest_check.rb +94 -0
- data/lib/reactionview/railtie.rb +10 -2
- data/lib/reactionview/stale_asset_manifest_error.rb +9 -0
- data/lib/reactionview/template/handlers/erb.rb +41 -5
- data/lib/reactionview/template/handlers/herb.rb +42 -26
- data/lib/reactionview/template/local_template.rb +22 -0
- data/lib/reactionview/version.rb +1 -1
- data/lib/reactionview.rb +2 -0
- data/reactionview.gemspec +2 -1
- metadata +24 -6
|
@@ -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">×</button>
|
|
756
|
+
</div>
|
|
757
|
+
|
|
758
|
+
<div class="herb-optimization-panel-list">
|
|
759
|
+
${displayNames.map(f => `<div class="herb-optimization-panel-item">${f.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')}</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
|
-
|
|
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
|
|
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
|
-
|
|
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
|