@ytspar/devbar 1.0.0-canary.c37df82 → 1.0.0-canary.c511f13
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.
- package/dist/GlobalDevBar.d.ts +96 -2
- package/dist/GlobalDevBar.js +1203 -114
- package/dist/accessibility.d.ts +84 -0
- package/dist/accessibility.js +155 -0
- package/dist/constants.d.ts +102 -1
- package/dist/constants.js +113 -13
- package/dist/debug.d.ts +39 -0
- package/dist/debug.js +92 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +19 -7
- package/dist/lazy/index.d.ts +6 -0
- package/dist/lazy/index.js +6 -0
- package/dist/lazy/lazyHtml2Canvas.d.ts +29 -0
- package/dist/lazy/lazyHtml2Canvas.js +37 -0
- package/dist/network.d.ts +92 -0
- package/dist/network.js +176 -0
- package/dist/outline.js +43 -10
- package/dist/presets.d.ts +57 -0
- package/dist/presets.js +133 -0
- package/dist/schema.js +4 -3
- package/dist/settings.d.ts +150 -0
- package/dist/settings.js +292 -0
- package/dist/storage.d.ts +83 -0
- package/dist/storage.js +182 -0
- package/dist/types.d.ts +26 -1
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +2 -2
- package/dist/ui/modals.d.ts +4 -0
- package/dist/ui/modals.js +53 -7
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/package.json +1 -1
package/dist/GlobalDevBar.js
CHANGED
|
@@ -8,19 +8,23 @@
|
|
|
8
8
|
* to avoid React dependency conflicts in host applications.
|
|
9
9
|
*/
|
|
10
10
|
import * as html2canvasModule from 'html2canvas-pro';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { BASE_RECONNECT_DELAY_MS, BUTTON_COLORS, CATEGORY_COLORS, CLIPBOARD_NOTIFICATION_MS, COLORS, DESIGN_REVIEW_NOTIFICATION_MS, DEVBAR_SCREENSHOT_QUALITY, FONT_MONO, getEffectiveTheme, getTheme, getThemeColors, injectThemeCSS, MAX_CONSOLE_LOGS, MAX_PORT_RETRIES, MAX_RECONNECT_ATTEMPTS, MAX_RECONNECT_DELAY_MS, PORT_RETRY_DELAY_MS, PORT_SCAN_RESTART_DELAY_MS, SCREENSHOT_BLUR_DELAY_MS, SCREENSHOT_NOTIFICATION_MS, SCREENSHOT_SCALE, TAILWIND_BREAKPOINTS, TOOLTIP_STYLES, VERIFICATION_TIMEOUT_MS, WS_PORT, WS_PORT_OFFSET, } from './constants.js';
|
|
12
|
+
import { DebugLogger, normalizeDebugConfig } from './debug.js';
|
|
13
13
|
import { extractDocumentOutline, outlineToMarkdown } from './outline.js';
|
|
14
14
|
import { extractPageSchema, schemaToMarkdown } from './schema.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager, } from './settings.js';
|
|
16
|
+
import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createStyledButton, createSvgIcon, getButtonStyles, } from './ui/index.js';
|
|
17
|
+
import { canvasToDataUrl, copyCanvasToClipboard, delay, formatArgs, prepareForCapture, } from './utils.js';
|
|
18
|
+
export { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager } from './settings.js';
|
|
19
|
+
const html2canvas = (html2canvasModule.default ??
|
|
20
|
+
html2canvasModule);
|
|
17
21
|
const earlyConsoleCapture = (() => {
|
|
18
22
|
const ssrFallback = {
|
|
19
23
|
errorCount: 0,
|
|
20
24
|
warningCount: 0,
|
|
21
25
|
logs: [],
|
|
22
26
|
originalConsole: null,
|
|
23
|
-
isPatched: false
|
|
27
|
+
isPatched: false,
|
|
24
28
|
};
|
|
25
29
|
// Skip on server-side rendering
|
|
26
30
|
if (typeof window === 'undefined')
|
|
@@ -33,9 +37,9 @@ const earlyConsoleCapture = (() => {
|
|
|
33
37
|
log: console.log,
|
|
34
38
|
error: console.error,
|
|
35
39
|
warn: console.warn,
|
|
36
|
-
info: console.info
|
|
40
|
+
info: console.info,
|
|
37
41
|
},
|
|
38
|
-
isPatched: false
|
|
42
|
+
isPatched: false,
|
|
39
43
|
};
|
|
40
44
|
const captureLog = (level, args) => {
|
|
41
45
|
capture.logs.push({ level, message: formatArgs(args), timestamp: Date.now() });
|
|
@@ -44,10 +48,24 @@ const earlyConsoleCapture = (() => {
|
|
|
44
48
|
};
|
|
45
49
|
// Patch console immediately
|
|
46
50
|
if (!capture.isPatched && capture.originalConsole) {
|
|
47
|
-
console.log = (...args) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
console.log = (...args) => {
|
|
52
|
+
captureLog('log', args);
|
|
53
|
+
capture.originalConsole.log(...args);
|
|
54
|
+
};
|
|
55
|
+
console.error = (...args) => {
|
|
56
|
+
captureLog('error', args);
|
|
57
|
+
capture.errorCount++;
|
|
58
|
+
capture.originalConsole.error(...args);
|
|
59
|
+
};
|
|
60
|
+
console.warn = (...args) => {
|
|
61
|
+
captureLog('warn', args);
|
|
62
|
+
capture.warningCount++;
|
|
63
|
+
capture.originalConsole.warn(...args);
|
|
64
|
+
};
|
|
65
|
+
console.info = (...args) => {
|
|
66
|
+
captureLog('info', args);
|
|
67
|
+
capture.originalConsole.info(...args);
|
|
68
|
+
};
|
|
51
69
|
capture.isPatched = true;
|
|
52
70
|
}
|
|
53
71
|
return capture;
|
|
@@ -73,6 +91,8 @@ export class GlobalDevBar {
|
|
|
73
91
|
this.apiKeyStatus = null;
|
|
74
92
|
this.lastOutline = null;
|
|
75
93
|
this.lastSchema = null;
|
|
94
|
+
this.savingOutline = false;
|
|
95
|
+
this.savingSchema = false;
|
|
76
96
|
this.consoleFilter = null;
|
|
77
97
|
// Modal states
|
|
78
98
|
this.showOutlineModal = false;
|
|
@@ -80,7 +100,14 @@ export class GlobalDevBar {
|
|
|
80
100
|
this.breakpointInfo = null;
|
|
81
101
|
this.perfStats = null;
|
|
82
102
|
this.lcpValue = null;
|
|
103
|
+
this.clsValue = 0;
|
|
104
|
+
this.inpValue = 0;
|
|
83
105
|
this.reconnectAttempts = 0;
|
|
106
|
+
this.wsVerified = false;
|
|
107
|
+
this.serverProjectDir = null;
|
|
108
|
+
this.verificationTimeout = null;
|
|
109
|
+
// Track the position of the connection indicator dot for smooth collapse
|
|
110
|
+
this.lastDotPosition = null;
|
|
84
111
|
this.reconnectTimeout = null;
|
|
85
112
|
this.screenshotTimeout = null;
|
|
86
113
|
this.copiedPathTimeout = null;
|
|
@@ -92,9 +119,35 @@ export class GlobalDevBar {
|
|
|
92
119
|
this.keydownHandler = null;
|
|
93
120
|
this.fcpObserver = null;
|
|
94
121
|
this.lcpObserver = null;
|
|
122
|
+
this.clsObserver = null;
|
|
123
|
+
this.inpObserver = null;
|
|
95
124
|
this.destroyed = false;
|
|
125
|
+
// Theme state
|
|
126
|
+
this.themeMode = 'system';
|
|
127
|
+
this.themeMediaQuery = null;
|
|
128
|
+
this.themeMediaHandler = null;
|
|
129
|
+
// Compact mode state
|
|
130
|
+
this.compactMode = false;
|
|
131
|
+
// Settings popover state
|
|
132
|
+
this.showSettingsPopover = false;
|
|
96
133
|
// Overlay element for modals
|
|
97
134
|
this.overlayElement = null;
|
|
135
|
+
// Initialize debug config first so we can log during construction
|
|
136
|
+
this.debugConfig = normalizeDebugConfig(options.debug);
|
|
137
|
+
this.debug = new DebugLogger(this.debugConfig);
|
|
138
|
+
// Initialize settings manager
|
|
139
|
+
this.settingsManager = getSettingsManager();
|
|
140
|
+
// Calculate app port from URL for multi-instance support
|
|
141
|
+
if (typeof window !== 'undefined') {
|
|
142
|
+
this.currentAppPort =
|
|
143
|
+
parseInt(window.location.port, 10) || (window.location.protocol === 'https:' ? 443 : 80);
|
|
144
|
+
// Calculate expected WS port (appPort + port offset) like SweetlinkBridge does
|
|
145
|
+
this.baseWsPort = this.currentAppPort > 0 ? this.currentAppPort + WS_PORT_OFFSET : WS_PORT;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.currentAppPort = 0;
|
|
149
|
+
this.baseWsPort = WS_PORT;
|
|
150
|
+
}
|
|
98
151
|
this.options = {
|
|
99
152
|
position: options.position ?? 'bottom-left',
|
|
100
153
|
accentColor: options.accentColor ?? COLORS.primary,
|
|
@@ -102,6 +155,8 @@ export class GlobalDevBar {
|
|
|
102
155
|
breakpoint: options.showMetrics?.breakpoint ?? true,
|
|
103
156
|
fcp: options.showMetrics?.fcp ?? true,
|
|
104
157
|
lcp: options.showMetrics?.lcp ?? true,
|
|
158
|
+
cls: options.showMetrics?.cls ?? true,
|
|
159
|
+
inp: options.showMetrics?.inp ?? true,
|
|
105
160
|
pageSize: options.showMetrics?.pageSize ?? true,
|
|
106
161
|
},
|
|
107
162
|
showScreenshot: options.showScreenshot ?? true,
|
|
@@ -109,6 +164,7 @@ export class GlobalDevBar {
|
|
|
109
164
|
showTooltips: options.showTooltips ?? true,
|
|
110
165
|
sizeOverrides: options.sizeOverrides,
|
|
111
166
|
};
|
|
167
|
+
this.debug.lifecycle('GlobalDevBar constructed', { options: this.options });
|
|
112
168
|
}
|
|
113
169
|
/**
|
|
114
170
|
* Get tooltip class name(s) if tooltips are enabled, otherwise empty string
|
|
@@ -153,7 +209,7 @@ export class GlobalDevBar {
|
|
|
153
209
|
fontWeight: '600',
|
|
154
210
|
display: 'flex',
|
|
155
211
|
alignItems: 'center',
|
|
156
|
-
justifyContent: 'center'
|
|
212
|
+
justifyContent: 'center',
|
|
157
213
|
});
|
|
158
214
|
badge.textContent = count > 99 ? '!' : String(count);
|
|
159
215
|
return badge;
|
|
@@ -166,7 +222,7 @@ export class GlobalDevBar {
|
|
|
166
222
|
*/
|
|
167
223
|
static registerControl(control) {
|
|
168
224
|
// Remove existing control with same ID
|
|
169
|
-
GlobalDevBar.customControls = GlobalDevBar.customControls.filter(c => c.id !== control.id);
|
|
225
|
+
GlobalDevBar.customControls = GlobalDevBar.customControls.filter((c) => c.id !== control.id);
|
|
170
226
|
GlobalDevBar.customControls.push(control);
|
|
171
227
|
// Trigger re-render of all instances
|
|
172
228
|
const instance = getGlobalInstance();
|
|
@@ -178,7 +234,7 @@ export class GlobalDevBar {
|
|
|
178
234
|
* Unregister a custom control by ID
|
|
179
235
|
*/
|
|
180
236
|
static unregisterControl(id) {
|
|
181
|
-
GlobalDevBar.customControls = GlobalDevBar.customControls.filter(c => c.id !== id);
|
|
237
|
+
GlobalDevBar.customControls = GlobalDevBar.customControls.filter((c) => c.id !== id);
|
|
182
238
|
// Trigger re-render of all instances
|
|
183
239
|
const instance = getGlobalInstance();
|
|
184
240
|
if (instance) {
|
|
@@ -209,10 +265,16 @@ export class GlobalDevBar {
|
|
|
209
265
|
return;
|
|
210
266
|
if (this.destroyed)
|
|
211
267
|
return;
|
|
268
|
+
this.debug.lifecycle('Initializing DevBar');
|
|
212
269
|
// Inject tooltip styles
|
|
213
270
|
this.injectStyles();
|
|
214
271
|
// Copy early captured logs
|
|
215
272
|
this.consoleLogs = [...earlyConsoleCapture.logs];
|
|
273
|
+
this.debug.lifecycle('Copied early console logs', { count: this.consoleLogs.length });
|
|
274
|
+
// Setup theme
|
|
275
|
+
this.setupTheme();
|
|
276
|
+
// Load compact mode from storage
|
|
277
|
+
this.loadCompactMode();
|
|
216
278
|
// Setup WebSocket connection
|
|
217
279
|
this.connectWebSocket();
|
|
218
280
|
// Setup breakpoint detection
|
|
@@ -223,16 +285,26 @@ export class GlobalDevBar {
|
|
|
223
285
|
this.setupKeyboardShortcuts();
|
|
224
286
|
// Initial render
|
|
225
287
|
this.render();
|
|
288
|
+
this.debug.lifecycle('DevBar initialized successfully');
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get the current position
|
|
292
|
+
*/
|
|
293
|
+
getPosition() {
|
|
294
|
+
return this.options.position;
|
|
226
295
|
}
|
|
227
296
|
/**
|
|
228
297
|
* Destroy the devbar and cleanup
|
|
229
298
|
*/
|
|
230
299
|
destroy() {
|
|
300
|
+
this.debug.lifecycle('Destroying DevBar');
|
|
231
301
|
this.destroyed = true;
|
|
232
302
|
// Close WebSocket
|
|
233
303
|
this.reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // Prevent reconnection
|
|
234
304
|
if (this.reconnectTimeout)
|
|
235
305
|
clearTimeout(this.reconnectTimeout);
|
|
306
|
+
if (this.verificationTimeout)
|
|
307
|
+
clearTimeout(this.verificationTimeout);
|
|
236
308
|
if (this.ws)
|
|
237
309
|
this.ws.close();
|
|
238
310
|
// Clear timeouts
|
|
@@ -256,6 +328,14 @@ export class GlobalDevBar {
|
|
|
256
328
|
this.fcpObserver.disconnect();
|
|
257
329
|
if (this.lcpObserver)
|
|
258
330
|
this.lcpObserver.disconnect();
|
|
331
|
+
if (this.clsObserver)
|
|
332
|
+
this.clsObserver.disconnect();
|
|
333
|
+
if (this.inpObserver)
|
|
334
|
+
this.inpObserver.disconnect();
|
|
335
|
+
// Remove theme media listener
|
|
336
|
+
if (this.themeMediaQuery && this.themeMediaHandler) {
|
|
337
|
+
this.themeMediaQuery.removeEventListener('change', this.themeMediaHandler);
|
|
338
|
+
}
|
|
259
339
|
// Restore console
|
|
260
340
|
if (earlyConsoleCapture.originalConsole) {
|
|
261
341
|
console.log = earlyConsoleCapture.originalConsole.log;
|
|
@@ -272,6 +352,7 @@ export class GlobalDevBar {
|
|
|
272
352
|
this.overlayElement.remove();
|
|
273
353
|
this.overlayElement = null;
|
|
274
354
|
}
|
|
355
|
+
this.debug.lifecycle('DevBar destroyed');
|
|
275
356
|
}
|
|
276
357
|
injectStyles() {
|
|
277
358
|
const styleId = 'devbar-tooltip-styles';
|
|
@@ -282,20 +363,83 @@ export class GlobalDevBar {
|
|
|
282
363
|
document.head.appendChild(style);
|
|
283
364
|
}
|
|
284
365
|
}
|
|
285
|
-
connectWebSocket() {
|
|
366
|
+
connectWebSocket(port) {
|
|
286
367
|
if (this.destroyed)
|
|
287
368
|
return;
|
|
288
|
-
const
|
|
369
|
+
const targetPort = port ?? this.baseWsPort;
|
|
370
|
+
this.debug.ws('Connecting to WebSocket', { port: targetPort, appPort: this.currentAppPort });
|
|
371
|
+
const ws = new WebSocket(`ws://localhost:${targetPort}`);
|
|
289
372
|
this.ws = ws;
|
|
373
|
+
this.wsVerified = false;
|
|
374
|
+
// Timeout for server-info verification
|
|
375
|
+
this.verificationTimeout = setTimeout(() => {
|
|
376
|
+
if (!this.wsVerified && ws.readyState === WebSocket.OPEN) {
|
|
377
|
+
// Server didn't send server-info (old version) - accept for backwards compatibility
|
|
378
|
+
this.debug.ws('Server is old version (no server-info), accepting for backwards compat');
|
|
379
|
+
this.wsVerified = true;
|
|
380
|
+
this.sweetlinkConnected = true;
|
|
381
|
+
this.reconnectAttempts = 0;
|
|
382
|
+
this.settingsManager.setWebSocket(ws);
|
|
383
|
+
this.settingsManager.setConnected(true);
|
|
384
|
+
ws.send(JSON.stringify({ type: 'load-settings' }));
|
|
385
|
+
this.render();
|
|
386
|
+
}
|
|
387
|
+
}, VERIFICATION_TIMEOUT_MS);
|
|
290
388
|
ws.onopen = () => {
|
|
291
|
-
this.
|
|
292
|
-
this.reconnectAttempts = 0;
|
|
389
|
+
this.debug.ws('WebSocket socket opened, awaiting server-info');
|
|
293
390
|
ws.send(JSON.stringify({ type: 'browser-client-ready' }));
|
|
294
|
-
this.render();
|
|
295
391
|
};
|
|
296
392
|
ws.onmessage = async (event) => {
|
|
297
393
|
try {
|
|
298
|
-
const
|
|
394
|
+
const message = JSON.parse(event.data);
|
|
395
|
+
// Handle server-info for port matching
|
|
396
|
+
if (message.type === 'server-info') {
|
|
397
|
+
if (this.verificationTimeout) {
|
|
398
|
+
clearTimeout(this.verificationTimeout);
|
|
399
|
+
this.verificationTimeout = null;
|
|
400
|
+
}
|
|
401
|
+
const serverAppPort = message.appPort;
|
|
402
|
+
const serverMatchesApp = serverAppPort === null || serverAppPort === this.currentAppPort;
|
|
403
|
+
if (!serverMatchesApp) {
|
|
404
|
+
this.debug.ws('Server mismatch', {
|
|
405
|
+
serverAppPort,
|
|
406
|
+
currentAppPort: this.currentAppPort,
|
|
407
|
+
tryingNextPort: targetPort + 1,
|
|
408
|
+
});
|
|
409
|
+
ws.close();
|
|
410
|
+
// Try next port
|
|
411
|
+
const nextPort = targetPort + 1;
|
|
412
|
+
if (nextPort < this.baseWsPort + MAX_PORT_RETRIES) {
|
|
413
|
+
setTimeout(() => this.connectWebSocket(nextPort), PORT_RETRY_DELAY_MS);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
this.debug.ws('No matching server found, will retry from base port');
|
|
417
|
+
setTimeout(() => this.connectWebSocket(this.baseWsPort), PORT_SCAN_RESTART_DELAY_MS);
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// Server matches - mark as verified and connected
|
|
422
|
+
this.wsVerified = true;
|
|
423
|
+
this.sweetlinkConnected = true;
|
|
424
|
+
this.reconnectAttempts = 0;
|
|
425
|
+
this.serverProjectDir = message.projectDir ?? null;
|
|
426
|
+
this.debug.ws('Server verified', {
|
|
427
|
+
appPort: serverAppPort ?? 'any',
|
|
428
|
+
projectDir: this.serverProjectDir,
|
|
429
|
+
});
|
|
430
|
+
this.settingsManager.setWebSocket(ws);
|
|
431
|
+
this.settingsManager.setConnected(true);
|
|
432
|
+
ws.send(JSON.stringify({ type: 'load-settings' }));
|
|
433
|
+
this.render();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
// Ignore other commands until verified
|
|
437
|
+
if (!this.wsVerified) {
|
|
438
|
+
this.debug.ws('Ignoring command before verification', { type: message.type });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const command = message;
|
|
442
|
+
this.debug.ws('Received command', { type: command.type });
|
|
299
443
|
await this.handleSweetlinkCommand(command);
|
|
300
444
|
}
|
|
301
445
|
catch (e) {
|
|
@@ -303,17 +447,30 @@ export class GlobalDevBar {
|
|
|
303
447
|
}
|
|
304
448
|
};
|
|
305
449
|
ws.onclose = () => {
|
|
306
|
-
this.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.
|
|
450
|
+
if (this.verificationTimeout) {
|
|
451
|
+
clearTimeout(this.verificationTimeout);
|
|
452
|
+
this.verificationTimeout = null;
|
|
453
|
+
}
|
|
454
|
+
// Only reset connection state if we were actually verified/connected
|
|
455
|
+
if (this.wsVerified) {
|
|
456
|
+
this.sweetlinkConnected = false;
|
|
457
|
+
this.wsVerified = false;
|
|
458
|
+
this.serverProjectDir = null;
|
|
459
|
+
this.settingsManager.setConnected(false);
|
|
460
|
+
this.debug.ws('WebSocket disconnected');
|
|
461
|
+
this.render();
|
|
462
|
+
// Auto-reconnect with exponential backoff (start from base port)
|
|
463
|
+
if (!this.destroyed && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
464
|
+
const delayMs = BASE_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempts;
|
|
465
|
+
this.reconnectAttempts++;
|
|
466
|
+
this.debug.ws('Scheduling reconnect', { attempt: this.reconnectAttempts, delayMs });
|
|
467
|
+
this.reconnectTimeout = setTimeout(() => this.connectWebSocket(this.baseWsPort), Math.min(delayMs, MAX_RECONNECT_DELAY_MS));
|
|
468
|
+
}
|
|
313
469
|
}
|
|
314
470
|
};
|
|
315
471
|
ws.onerror = () => {
|
|
316
472
|
// Error will trigger onclose, which handles reconnection
|
|
473
|
+
this.debug.ws('WebSocket error');
|
|
317
474
|
};
|
|
318
475
|
}
|
|
319
476
|
async handleSweetlinkCommand(command) {
|
|
@@ -325,11 +482,20 @@ export class GlobalDevBar {
|
|
|
325
482
|
const targetElement = command.selector
|
|
326
483
|
? document.querySelector(command.selector) || document.body
|
|
327
484
|
: document.body;
|
|
328
|
-
const canvas = await html2canvas(targetElement, {
|
|
485
|
+
const canvas = await html2canvas(targetElement, {
|
|
486
|
+
logging: false,
|
|
487
|
+
useCORS: true,
|
|
488
|
+
allowTaint: true,
|
|
489
|
+
});
|
|
329
490
|
ws.send(JSON.stringify({
|
|
330
491
|
success: true,
|
|
331
|
-
data: {
|
|
332
|
-
|
|
492
|
+
data: {
|
|
493
|
+
screenshot: canvas.toDataURL('image/png'),
|
|
494
|
+
width: canvas.width,
|
|
495
|
+
height: canvas.height,
|
|
496
|
+
selector: command.selector || 'body',
|
|
497
|
+
},
|
|
498
|
+
timestamp: Date.now(),
|
|
333
499
|
}));
|
|
334
500
|
break;
|
|
335
501
|
}
|
|
@@ -337,7 +503,7 @@ export class GlobalDevBar {
|
|
|
337
503
|
let logs = this.consoleLogs;
|
|
338
504
|
if (command.filter) {
|
|
339
505
|
const filter = command.filter.toLowerCase();
|
|
340
|
-
logs = logs.filter(log => log.level.includes(filter) || log.message.toLowerCase().includes(filter));
|
|
506
|
+
logs = logs.filter((log) => log.level.includes(filter) || log.message.toLowerCase().includes(filter));
|
|
341
507
|
}
|
|
342
508
|
ws.send(JSON.stringify({ success: true, data: logs, timestamp: Date.now() }));
|
|
343
509
|
break;
|
|
@@ -348,9 +514,18 @@ export class GlobalDevBar {
|
|
|
348
514
|
const results = elements.map((el) => {
|
|
349
515
|
if (command.property)
|
|
350
516
|
return el[command.property] ?? null;
|
|
351
|
-
return {
|
|
517
|
+
return {
|
|
518
|
+
tagName: el.tagName,
|
|
519
|
+
className: el.className,
|
|
520
|
+
id: el.id,
|
|
521
|
+
textContent: el.textContent?.trim().slice(0, 100),
|
|
522
|
+
};
|
|
352
523
|
});
|
|
353
|
-
ws.send(JSON.stringify({
|
|
524
|
+
ws.send(JSON.stringify({
|
|
525
|
+
success: true,
|
|
526
|
+
data: { count: results.length, results },
|
|
527
|
+
timestamp: Date.now(),
|
|
528
|
+
}));
|
|
354
529
|
}
|
|
355
530
|
break;
|
|
356
531
|
}
|
|
@@ -363,7 +538,11 @@ export class GlobalDevBar {
|
|
|
363
538
|
ws.send(JSON.stringify({ success: true, data: result, timestamp: Date.now() }));
|
|
364
539
|
}
|
|
365
540
|
catch (e) {
|
|
366
|
-
ws.send(JSON.stringify({
|
|
541
|
+
ws.send(JSON.stringify({
|
|
542
|
+
success: false,
|
|
543
|
+
error: e instanceof Error ? e.message : 'Execution failed',
|
|
544
|
+
timestamp: Date.now(),
|
|
545
|
+
}));
|
|
367
546
|
}
|
|
368
547
|
}
|
|
369
548
|
break;
|
|
@@ -413,7 +592,47 @@ export class GlobalDevBar {
|
|
|
413
592
|
case 'schema-error':
|
|
414
593
|
console.error('[GlobalDevBar] Schema save failed:', command.error);
|
|
415
594
|
break;
|
|
595
|
+
case 'settings-loaded':
|
|
596
|
+
this.handleSettingsLoaded(command.settings);
|
|
597
|
+
break;
|
|
598
|
+
case 'settings-saved':
|
|
599
|
+
this.debug.state('Settings saved to server', { path: command.settingsPath });
|
|
600
|
+
break;
|
|
601
|
+
case 'settings-error':
|
|
602
|
+
console.error('[GlobalDevBar] Settings operation failed:', command.error);
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Handle settings loaded from server
|
|
608
|
+
*/
|
|
609
|
+
handleSettingsLoaded(settings) {
|
|
610
|
+
if (!settings) {
|
|
611
|
+
this.debug.state('No server settings found, using local');
|
|
612
|
+
return;
|
|
416
613
|
}
|
|
614
|
+
this.debug.state('Settings loaded from server', settings);
|
|
615
|
+
// Update settings manager
|
|
616
|
+
this.settingsManager.handleSettingsLoaded(settings);
|
|
617
|
+
// Apply settings to local state
|
|
618
|
+
this.applySettings(settings);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Apply settings to the DevBar state and options
|
|
622
|
+
*/
|
|
623
|
+
applySettings(settings) {
|
|
624
|
+
// Update local state
|
|
625
|
+
this.themeMode = settings.themeMode;
|
|
626
|
+
this.compactMode = settings.compactMode;
|
|
627
|
+
// Update options
|
|
628
|
+
this.options.position = settings.position;
|
|
629
|
+
this.options.accentColor = settings.accentColor;
|
|
630
|
+
this.options.showScreenshot = settings.showScreenshot;
|
|
631
|
+
this.options.showConsoleBadges = settings.showConsoleBadges;
|
|
632
|
+
this.options.showTooltips = settings.showTooltips;
|
|
633
|
+
this.options.showMetrics = { ...settings.showMetrics };
|
|
634
|
+
// Re-render with new settings
|
|
635
|
+
this.render();
|
|
417
636
|
}
|
|
418
637
|
/**
|
|
419
638
|
* Handle notification state updates with auto-clear timeout
|
|
@@ -427,25 +646,39 @@ export class GlobalDevBar {
|
|
|
427
646
|
this.lastScreenshot = path;
|
|
428
647
|
if (this.screenshotTimeout)
|
|
429
648
|
clearTimeout(this.screenshotTimeout);
|
|
430
|
-
this.screenshotTimeout = setTimeout(() => {
|
|
649
|
+
this.screenshotTimeout = setTimeout(() => {
|
|
650
|
+
this.lastScreenshot = null;
|
|
651
|
+
this.render();
|
|
652
|
+
}, durationMs);
|
|
431
653
|
break;
|
|
432
654
|
case 'designReview':
|
|
433
655
|
this.lastDesignReview = path;
|
|
434
656
|
if (this.designReviewTimeout)
|
|
435
657
|
clearTimeout(this.designReviewTimeout);
|
|
436
|
-
this.designReviewTimeout = setTimeout(() => {
|
|
658
|
+
this.designReviewTimeout = setTimeout(() => {
|
|
659
|
+
this.lastDesignReview = null;
|
|
660
|
+
this.render();
|
|
661
|
+
}, durationMs);
|
|
437
662
|
break;
|
|
438
663
|
case 'outline':
|
|
664
|
+
this.savingOutline = false;
|
|
439
665
|
this.lastOutline = path;
|
|
440
666
|
if (this.outlineTimeout)
|
|
441
667
|
clearTimeout(this.outlineTimeout);
|
|
442
|
-
this.outlineTimeout = setTimeout(() => {
|
|
668
|
+
this.outlineTimeout = setTimeout(() => {
|
|
669
|
+
this.lastOutline = null;
|
|
670
|
+
this.render();
|
|
671
|
+
}, durationMs);
|
|
443
672
|
break;
|
|
444
673
|
case 'schema':
|
|
674
|
+
this.savingSchema = false;
|
|
445
675
|
this.lastSchema = path;
|
|
446
676
|
if (this.schemaTimeout)
|
|
447
677
|
clearTimeout(this.schemaTimeout);
|
|
448
|
-
this.schemaTimeout = setTimeout(() => {
|
|
678
|
+
this.schemaTimeout = setTimeout(() => {
|
|
679
|
+
this.lastSchema = null;
|
|
680
|
+
this.render();
|
|
681
|
+
}, durationMs);
|
|
449
682
|
break;
|
|
450
683
|
}
|
|
451
684
|
this.render();
|
|
@@ -455,20 +688,17 @@ export class GlobalDevBar {
|
|
|
455
688
|
const width = window.innerWidth;
|
|
456
689
|
const height = window.innerHeight;
|
|
457
690
|
// Determine breakpoint by checking thresholds in descending order
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
tailwindBreakpoint = 'md';
|
|
467
|
-
else if (width >= TAILWIND_BREAKPOINTS.sm.min)
|
|
468
|
-
tailwindBreakpoint = 'sm';
|
|
691
|
+
const breakpointOrder = [
|
|
692
|
+
'2xl',
|
|
693
|
+
'xl',
|
|
694
|
+
'lg',
|
|
695
|
+
'md',
|
|
696
|
+
'sm',
|
|
697
|
+
];
|
|
698
|
+
const tailwindBreakpoint = breakpointOrder.find((bp) => width >= TAILWIND_BREAKPOINTS[bp].min) ?? 'base';
|
|
469
699
|
this.breakpointInfo = {
|
|
470
700
|
tailwindBreakpoint,
|
|
471
|
-
dimensions: `${width}x${height}
|
|
701
|
+
dimensions: `${width}x${height}`,
|
|
472
702
|
};
|
|
473
703
|
this.render();
|
|
474
704
|
};
|
|
@@ -480,10 +710,14 @@ export class GlobalDevBar {
|
|
|
480
710
|
const updatePerfStats = () => {
|
|
481
711
|
// FCP
|
|
482
712
|
const paintEntries = performance.getEntriesByType('paint');
|
|
483
|
-
const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
|
|
713
|
+
const fcpEntry = paintEntries.find((entry) => entry.name === 'first-contentful-paint');
|
|
484
714
|
const fcp = fcpEntry ? `${Math.round(fcpEntry.startTime)}ms` : '-';
|
|
485
715
|
// LCP (from cached value, updated by observer)
|
|
486
716
|
const lcp = this.lcpValue !== null ? `${Math.round(this.lcpValue)}ms` : '-';
|
|
717
|
+
// CLS (cumulative layout shift)
|
|
718
|
+
const cls = this.clsValue > 0 ? this.clsValue.toFixed(3) : '-';
|
|
719
|
+
// INP (Interaction to Next Paint)
|
|
720
|
+
const inp = this.inpValue > 0 ? `${Math.round(this.inpValue)}ms` : '-';
|
|
487
721
|
// Total Resource Size
|
|
488
722
|
const resources = performance.getEntriesByType('resource');
|
|
489
723
|
let totalBytes = 0;
|
|
@@ -498,7 +732,8 @@ export class GlobalDevBar {
|
|
|
498
732
|
const totalSize = totalBytes > 1024 * 1024
|
|
499
733
|
? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB`
|
|
500
734
|
: `${Math.round(totalBytes / 1024)} KB`;
|
|
501
|
-
this.perfStats = { fcp, lcp, totalSize };
|
|
735
|
+
this.perfStats = { fcp, lcp, cls, inp, totalSize };
|
|
736
|
+
this.debug.perf('Performance stats updated', this.perfStats);
|
|
502
737
|
this.render();
|
|
503
738
|
};
|
|
504
739
|
if (document.readyState === 'complete') {
|
|
@@ -529,6 +764,7 @@ export class GlobalDevBar {
|
|
|
529
764
|
const lastEntry = entries[entries.length - 1];
|
|
530
765
|
if (lastEntry) {
|
|
531
766
|
this.lcpValue = lastEntry.startTime;
|
|
767
|
+
this.debug.perf('LCP updated', { lcp: this.lcpValue });
|
|
532
768
|
updatePerfStats();
|
|
533
769
|
}
|
|
534
770
|
});
|
|
@@ -537,12 +773,60 @@ export class GlobalDevBar {
|
|
|
537
773
|
catch (e) {
|
|
538
774
|
console.warn('[GlobalDevBar] LCP PerformanceObserver not supported', e);
|
|
539
775
|
}
|
|
776
|
+
// CLS Observer (Cumulative Layout Shift)
|
|
777
|
+
try {
|
|
778
|
+
this.clsObserver = new PerformanceObserver((list) => {
|
|
779
|
+
for (const entry of list.getEntries()) {
|
|
780
|
+
// Only count layout shifts without recent user input
|
|
781
|
+
const layoutShift = entry;
|
|
782
|
+
if (!layoutShift.hadRecentInput && layoutShift.value) {
|
|
783
|
+
this.clsValue += layoutShift.value;
|
|
784
|
+
this.debug.perf('CLS updated', { cls: this.clsValue });
|
|
785
|
+
updatePerfStats();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
this.clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
790
|
+
}
|
|
791
|
+
catch (e) {
|
|
792
|
+
console.warn('[GlobalDevBar] CLS PerformanceObserver not supported', e);
|
|
793
|
+
}
|
|
794
|
+
// INP Observer (Interaction to Next Paint)
|
|
795
|
+
try {
|
|
796
|
+
this.inpObserver = new PerformanceObserver((list) => {
|
|
797
|
+
for (const entry of list.getEntries()) {
|
|
798
|
+
const eventEntry = entry;
|
|
799
|
+
if (eventEntry.duration && eventEntry.duration > this.inpValue) {
|
|
800
|
+
this.inpValue = eventEntry.duration;
|
|
801
|
+
this.debug.perf('INP updated', { inp: this.inpValue });
|
|
802
|
+
updatePerfStats();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
// durationThreshold filters out very short interactions
|
|
807
|
+
this.inpObserver.observe({
|
|
808
|
+
type: 'event',
|
|
809
|
+
buffered: true,
|
|
810
|
+
durationThreshold: 16,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
catch (e) {
|
|
814
|
+
console.warn('[GlobalDevBar] INP PerformanceObserver not supported', e);
|
|
815
|
+
}
|
|
540
816
|
}
|
|
541
817
|
setupKeyboardShortcuts() {
|
|
542
818
|
this.keydownHandler = (e) => {
|
|
543
|
-
// Close modals on Escape
|
|
819
|
+
// Close modals/popovers on Escape
|
|
544
820
|
if (e.key === 'Escape') {
|
|
545
|
-
if (this.
|
|
821
|
+
if (this.showSettingsPopover) {
|
|
822
|
+
this.showSettingsPopover = false;
|
|
823
|
+
this.render();
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (this.consoleFilter ||
|
|
827
|
+
this.showOutlineModal ||
|
|
828
|
+
this.showSchemaModal ||
|
|
829
|
+
this.showDesignReviewConfirm) {
|
|
546
830
|
this.consoleFilter = null;
|
|
547
831
|
this.showOutlineModal = false;
|
|
548
832
|
this.showSchemaModal = false;
|
|
@@ -552,6 +836,12 @@ export class GlobalDevBar {
|
|
|
552
836
|
}
|
|
553
837
|
}
|
|
554
838
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
839
|
+
// Cmd/Ctrl+Shift+M: Toggle compact mode
|
|
840
|
+
if (e.key === 'M' || e.key === 'm') {
|
|
841
|
+
e.preventDefault();
|
|
842
|
+
this.toggleCompactMode();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
555
845
|
if (e.key === 'S' || e.key === 's') {
|
|
556
846
|
e.preventDefault();
|
|
557
847
|
if (this.sweetlinkConnected && !this.capturing) {
|
|
@@ -571,6 +861,70 @@ export class GlobalDevBar {
|
|
|
571
861
|
};
|
|
572
862
|
window.addEventListener('keydown', this.keydownHandler);
|
|
573
863
|
}
|
|
864
|
+
setupTheme() {
|
|
865
|
+
// Load stored theme preference from settings manager
|
|
866
|
+
const settings = this.settingsManager.getSettings();
|
|
867
|
+
this.themeMode = settings.themeMode;
|
|
868
|
+
this.debug.state('Theme loaded', { mode: this.themeMode });
|
|
869
|
+
// Listen for system theme changes
|
|
870
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
871
|
+
this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
872
|
+
this.themeMediaHandler = () => {
|
|
873
|
+
if (this.themeMode === 'system') {
|
|
874
|
+
// Re-inject theme CSS when system preference changes
|
|
875
|
+
injectThemeCSS(getTheme(this.themeMode));
|
|
876
|
+
this.debug.state('System theme changed', {
|
|
877
|
+
effectiveTheme: getEffectiveTheme(this.themeMode),
|
|
878
|
+
});
|
|
879
|
+
this.render();
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
this.themeMediaQuery.addEventListener('change', this.themeMediaHandler);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
loadCompactMode() {
|
|
886
|
+
const settings = this.settingsManager.getSettings();
|
|
887
|
+
this.compactMode = settings.compactMode;
|
|
888
|
+
this.debug.state('Compact mode loaded', { compactMode: this.compactMode });
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Get the current theme mode
|
|
892
|
+
*/
|
|
893
|
+
getThemeMode() {
|
|
894
|
+
return this.themeMode;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Set the theme mode
|
|
898
|
+
*/
|
|
899
|
+
setThemeMode(mode) {
|
|
900
|
+
this.themeMode = mode;
|
|
901
|
+
this.settingsManager.saveSettings({ themeMode: mode });
|
|
902
|
+
// Inject the appropriate theme CSS variables
|
|
903
|
+
injectThemeCSS(getTheme(mode));
|
|
904
|
+
this.debug.state('Theme mode changed', { mode, effectiveTheme: getEffectiveTheme(mode) });
|
|
905
|
+
this.render();
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Get the current effective theme colors
|
|
909
|
+
*/
|
|
910
|
+
getColors() {
|
|
911
|
+
return getThemeColors(this.themeMode);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Toggle compact mode
|
|
915
|
+
*/
|
|
916
|
+
toggleCompactMode() {
|
|
917
|
+
this.compactMode = !this.compactMode;
|
|
918
|
+
this.settingsManager.saveSettings({ compactMode: this.compactMode });
|
|
919
|
+
this.debug.state('Compact mode toggled', { compactMode: this.compactMode });
|
|
920
|
+
this.render();
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Check if compact mode is enabled
|
|
924
|
+
*/
|
|
925
|
+
isCompactMode() {
|
|
926
|
+
return this.compactMode;
|
|
927
|
+
}
|
|
574
928
|
async copyPathToClipboard(path) {
|
|
575
929
|
try {
|
|
576
930
|
await navigator.clipboard.writeText(path);
|
|
@@ -604,7 +958,7 @@ export class GlobalDevBar {
|
|
|
604
958
|
allowTaint: true,
|
|
605
959
|
scale: SCREENSHOT_SCALE,
|
|
606
960
|
width: window.innerWidth,
|
|
607
|
-
windowWidth: window.innerWidth
|
|
961
|
+
windowWidth: window.innerWidth,
|
|
608
962
|
});
|
|
609
963
|
// Restore page state
|
|
610
964
|
cleanup();
|
|
@@ -626,8 +980,33 @@ export class GlobalDevBar {
|
|
|
626
980
|
}
|
|
627
981
|
}
|
|
628
982
|
else {
|
|
629
|
-
const dataUrl = canvasToDataUrl(canvas, {
|
|
983
|
+
const dataUrl = canvasToDataUrl(canvas, {
|
|
984
|
+
format: 'jpeg',
|
|
985
|
+
quality: DEVBAR_SCREENSHOT_QUALITY,
|
|
986
|
+
});
|
|
630
987
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
988
|
+
// Include web vitals metrics
|
|
989
|
+
const webVitals = {};
|
|
990
|
+
if (this.lcpValue !== null)
|
|
991
|
+
webVitals.lcp = Math.round(this.lcpValue);
|
|
992
|
+
if (this.clsValue > 0)
|
|
993
|
+
webVitals.cls = this.clsValue;
|
|
994
|
+
if (this.inpValue > 0)
|
|
995
|
+
webVitals.inp = Math.round(this.inpValue);
|
|
996
|
+
// Get FCP from performance entries
|
|
997
|
+
const fcpEntry = performance
|
|
998
|
+
.getEntriesByType('paint')
|
|
999
|
+
.find((e) => e.name === 'first-contentful-paint');
|
|
1000
|
+
if (fcpEntry)
|
|
1001
|
+
webVitals.fcp = Math.round(fcpEntry.startTime);
|
|
1002
|
+
// Calculate page size
|
|
1003
|
+
let pageSize = 0;
|
|
1004
|
+
const navEntry = performance.getEntriesByType('navigation')[0];
|
|
1005
|
+
if (navEntry)
|
|
1006
|
+
pageSize += navEntry.transferSize || 0;
|
|
1007
|
+
performance.getEntriesByType('resource').forEach((entry) => {
|
|
1008
|
+
pageSize += entry.transferSize || 0;
|
|
1009
|
+
});
|
|
631
1010
|
this.ws.send(JSON.stringify({
|
|
632
1011
|
type: 'save-screenshot',
|
|
633
1012
|
data: {
|
|
@@ -636,8 +1015,10 @@ export class GlobalDevBar {
|
|
|
636
1015
|
height: canvas.height,
|
|
637
1016
|
logs: this.consoleLogs,
|
|
638
1017
|
url: window.location.href,
|
|
639
|
-
timestamp: Date.now()
|
|
640
|
-
|
|
1018
|
+
timestamp: Date.now(),
|
|
1019
|
+
webVitals: Object.keys(webVitals).length > 0 ? webVitals : undefined,
|
|
1020
|
+
pageSize: pageSize > 0 ? pageSize : undefined,
|
|
1021
|
+
},
|
|
641
1022
|
}));
|
|
642
1023
|
}
|
|
643
1024
|
}
|
|
@@ -672,7 +1053,7 @@ export class GlobalDevBar {
|
|
|
672
1053
|
allowTaint: true,
|
|
673
1054
|
scale: 1, // Full quality for design review
|
|
674
1055
|
width: window.innerWidth,
|
|
675
|
-
windowWidth: window.innerWidth
|
|
1056
|
+
windowWidth: window.innerWidth,
|
|
676
1057
|
});
|
|
677
1058
|
// Restore page state
|
|
678
1059
|
cleanup();
|
|
@@ -687,8 +1068,8 @@ export class GlobalDevBar {
|
|
|
687
1068
|
height: canvas.height,
|
|
688
1069
|
logs: this.consoleLogs,
|
|
689
1070
|
url: window.location.href,
|
|
690
|
-
timestamp: Date.now()
|
|
691
|
-
}
|
|
1071
|
+
timestamp: Date.now(),
|
|
1072
|
+
},
|
|
692
1073
|
}));
|
|
693
1074
|
}
|
|
694
1075
|
}
|
|
@@ -773,9 +1154,13 @@ export class GlobalDevBar {
|
|
|
773
1154
|
this.render();
|
|
774
1155
|
}
|
|
775
1156
|
handleSaveOutline() {
|
|
1157
|
+
if (this.savingOutline)
|
|
1158
|
+
return; // Prevent repeated clicks
|
|
776
1159
|
const outline = extractDocumentOutline();
|
|
777
1160
|
const markdown = outlineToMarkdown(outline);
|
|
778
1161
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1162
|
+
this.savingOutline = true;
|
|
1163
|
+
this.render();
|
|
779
1164
|
this.ws.send(JSON.stringify({
|
|
780
1165
|
type: 'save-outline',
|
|
781
1166
|
data: {
|
|
@@ -783,15 +1168,19 @@ export class GlobalDevBar {
|
|
|
783
1168
|
markdown,
|
|
784
1169
|
url: window.location.href,
|
|
785
1170
|
title: document.title,
|
|
786
|
-
timestamp: Date.now()
|
|
787
|
-
}
|
|
1171
|
+
timestamp: Date.now(),
|
|
1172
|
+
},
|
|
788
1173
|
}));
|
|
789
1174
|
}
|
|
790
1175
|
}
|
|
791
1176
|
handleSaveSchema() {
|
|
1177
|
+
if (this.savingSchema)
|
|
1178
|
+
return; // Prevent repeated clicks
|
|
792
1179
|
const schema = extractPageSchema();
|
|
793
1180
|
const markdown = schemaToMarkdown(schema);
|
|
794
1181
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1182
|
+
this.savingSchema = true;
|
|
1183
|
+
this.render();
|
|
795
1184
|
this.ws.send(JSON.stringify({
|
|
796
1185
|
type: 'save-schema',
|
|
797
1186
|
data: {
|
|
@@ -799,8 +1188,8 @@ export class GlobalDevBar {
|
|
|
799
1188
|
markdown,
|
|
800
1189
|
url: window.location.href,
|
|
801
1190
|
title: document.title,
|
|
802
|
-
timestamp: Date.now()
|
|
803
|
-
}
|
|
1191
|
+
timestamp: Date.now(),
|
|
1192
|
+
},
|
|
804
1193
|
}));
|
|
805
1194
|
}
|
|
806
1195
|
}
|
|
@@ -831,6 +1220,9 @@ export class GlobalDevBar {
|
|
|
831
1220
|
if (this.collapsed) {
|
|
832
1221
|
this.renderCollapsed();
|
|
833
1222
|
}
|
|
1223
|
+
else if (this.compactMode) {
|
|
1224
|
+
this.renderCompact();
|
|
1225
|
+
}
|
|
834
1226
|
else {
|
|
835
1227
|
this.renderExpanded();
|
|
836
1228
|
}
|
|
@@ -860,6 +1252,10 @@ export class GlobalDevBar {
|
|
|
860
1252
|
if (this.showDesignReviewConfirm) {
|
|
861
1253
|
this.renderDesignReviewConfirmModal();
|
|
862
1254
|
}
|
|
1255
|
+
// Render settings popover
|
|
1256
|
+
if (this.showSettingsPopover) {
|
|
1257
|
+
this.renderSettingsPopover();
|
|
1258
|
+
}
|
|
863
1259
|
}
|
|
864
1260
|
renderDesignReviewConfirmModal() {
|
|
865
1261
|
const color = BUTTON_COLORS.review;
|
|
@@ -883,7 +1279,12 @@ export class GlobalDevBar {
|
|
|
883
1279
|
Object.assign(title.style, { color, fontSize: '0.875rem', fontWeight: '600' });
|
|
884
1280
|
title.textContent = 'AI Design Review';
|
|
885
1281
|
header.appendChild(title);
|
|
886
|
-
const closeBtn = createStyledButton({
|
|
1282
|
+
const closeBtn = createStyledButton({
|
|
1283
|
+
color: COLORS.textMuted,
|
|
1284
|
+
text: '×',
|
|
1285
|
+
padding: '0',
|
|
1286
|
+
fontSize: '1.25rem',
|
|
1287
|
+
});
|
|
887
1288
|
closeBtn.style.border = 'none';
|
|
888
1289
|
closeBtn.onclick = closeModal;
|
|
889
1290
|
header.appendChild(closeBtn);
|
|
@@ -915,7 +1316,11 @@ export class GlobalDevBar {
|
|
|
915
1316
|
padding: '14px 18px',
|
|
916
1317
|
borderTop: `1px solid ${COLORS.border}`,
|
|
917
1318
|
});
|
|
918
|
-
const cancelBtn = createStyledButton({
|
|
1319
|
+
const cancelBtn = createStyledButton({
|
|
1320
|
+
color: COLORS.textMuted,
|
|
1321
|
+
text: 'Cancel',
|
|
1322
|
+
padding: '8px 16px',
|
|
1323
|
+
});
|
|
919
1324
|
cancelBtn.onclick = closeModal;
|
|
920
1325
|
footer.appendChild(cancelBtn);
|
|
921
1326
|
if (this.apiKeyStatus?.configured) {
|
|
@@ -938,7 +1343,11 @@ export class GlobalDevBar {
|
|
|
938
1343
|
const instructions = document.createElement('div');
|
|
939
1344
|
Object.assign(instructions.style, { marginBottom: '12px' });
|
|
940
1345
|
const instructTitle = document.createElement('div');
|
|
941
|
-
Object.assign(instructTitle.style, {
|
|
1346
|
+
Object.assign(instructTitle.style, {
|
|
1347
|
+
color: COLORS.textSecondary,
|
|
1348
|
+
fontWeight: '600',
|
|
1349
|
+
marginBottom: '8px',
|
|
1350
|
+
});
|
|
942
1351
|
instructTitle.textContent = 'To configure:';
|
|
943
1352
|
instructions.appendChild(instructTitle);
|
|
944
1353
|
const steps = [
|
|
@@ -998,7 +1407,11 @@ export class GlobalDevBar {
|
|
|
998
1407
|
// Model info
|
|
999
1408
|
if (this.apiKeyStatus?.model) {
|
|
1000
1409
|
const modelDiv = document.createElement('div');
|
|
1001
|
-
Object.assign(modelDiv.style, {
|
|
1410
|
+
Object.assign(modelDiv.style, {
|
|
1411
|
+
color: COLORS.textMuted,
|
|
1412
|
+
fontSize: '0.6875rem',
|
|
1413
|
+
marginTop: '12px',
|
|
1414
|
+
});
|
|
1002
1415
|
modelDiv.textContent = `Model: ${this.apiKeyStatus.model}`;
|
|
1003
1416
|
if (this.apiKeyStatus.maskedKey) {
|
|
1004
1417
|
modelDiv.textContent += ` | Key: ${this.apiKeyStatus.maskedKey}`;
|
|
@@ -1011,7 +1424,7 @@ export class GlobalDevBar {
|
|
|
1011
1424
|
const filterType = this.consoleFilter;
|
|
1012
1425
|
if (!filterType)
|
|
1013
1426
|
return;
|
|
1014
|
-
const logs = earlyConsoleCapture.logs.filter(log => log.level === filterType);
|
|
1427
|
+
const logs = earlyConsoleCapture.logs.filter((log) => log.level === filterType);
|
|
1015
1428
|
const color = filterType === 'error' ? BUTTON_COLORS.error : BUTTON_COLORS.warning;
|
|
1016
1429
|
const label = filterType === 'error' ? 'Errors' : 'Warnings';
|
|
1017
1430
|
const popup = document.createElement('div');
|
|
@@ -1120,7 +1533,8 @@ export class GlobalDevBar {
|
|
|
1120
1533
|
wordBreak: 'break-word',
|
|
1121
1534
|
whiteSpace: 'pre-wrap',
|
|
1122
1535
|
});
|
|
1123
|
-
message.textContent =
|
|
1536
|
+
message.textContent =
|
|
1537
|
+
log.message.length > 500 ? `${log.message.slice(0, 500)}...` : log.message;
|
|
1124
1538
|
logItem.appendChild(message);
|
|
1125
1539
|
container.appendChild(logItem);
|
|
1126
1540
|
});
|
|
@@ -1144,6 +1558,8 @@ export class GlobalDevBar {
|
|
|
1144
1558
|
},
|
|
1145
1559
|
onSave: () => this.handleSaveOutline(),
|
|
1146
1560
|
sweetlinkConnected: this.sweetlinkConnected,
|
|
1561
|
+
isSaving: this.savingOutline,
|
|
1562
|
+
savedPath: this.lastOutline,
|
|
1147
1563
|
});
|
|
1148
1564
|
modal.appendChild(header);
|
|
1149
1565
|
const content = createModalContent();
|
|
@@ -1192,7 +1608,7 @@ export class GlobalDevBar {
|
|
|
1192
1608
|
fontSize: '0.6875rem',
|
|
1193
1609
|
marginLeft: '8px',
|
|
1194
1610
|
});
|
|
1195
|
-
const truncatedText = node.text.length > 60 ? node.text.slice(0, 60)
|
|
1611
|
+
const truncatedText = node.text.length > 60 ? `${node.text.slice(0, 60)}...` : node.text;
|
|
1196
1612
|
textSpan.textContent = truncatedText;
|
|
1197
1613
|
nodeEl.appendChild(textSpan);
|
|
1198
1614
|
if (node.id) {
|
|
@@ -1230,6 +1646,8 @@ export class GlobalDevBar {
|
|
|
1230
1646
|
},
|
|
1231
1647
|
onSave: () => this.handleSaveSchema(),
|
|
1232
1648
|
sweetlinkConnected: this.sweetlinkConnected,
|
|
1649
|
+
isSaving: this.savingSchema,
|
|
1650
|
+
savedPath: this.lastSchema,
|
|
1233
1651
|
});
|
|
1234
1652
|
modal.appendChild(header);
|
|
1235
1653
|
const content = createModalContent();
|
|
@@ -1325,7 +1743,7 @@ export class GlobalDevBar {
|
|
|
1325
1743
|
punct: COLORS.textMuted, // gray
|
|
1326
1744
|
};
|
|
1327
1745
|
// Simple tokenizer for JSON using matchAll for safety
|
|
1328
|
-
const tokenPattern = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)|([{}
|
|
1746
|
+
const tokenPattern = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)|([{}[\],])|(\s+)/g;
|
|
1329
1747
|
for (const match of json.matchAll(tokenPattern)) {
|
|
1330
1748
|
const [, str, colon, bool, nullToken, num, punct, whitespace] = match;
|
|
1331
1749
|
if (whitespace) {
|
|
@@ -1412,23 +1830,627 @@ export class GlobalDevBar {
|
|
|
1412
1830
|
container.appendChild(row);
|
|
1413
1831
|
}
|
|
1414
1832
|
}
|
|
1415
|
-
|
|
1833
|
+
/**
|
|
1834
|
+
* Render compact mode - single row with essential controls only
|
|
1835
|
+
* Shows: connection dot, error/warn badges, screenshot button, settings gear
|
|
1836
|
+
*/
|
|
1837
|
+
renderCompact() {
|
|
1416
1838
|
if (!this.container)
|
|
1417
1839
|
return;
|
|
1418
1840
|
const { position, accentColor } = this.options;
|
|
1419
1841
|
const { errorCount, warningCount } = this.getLogCounts();
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
'bottom-
|
|
1426
|
-
'bottom-right': { bottom: '27px', right: '29px' },
|
|
1427
|
-
'top-left': { top: '27px', left: '86px' },
|
|
1428
|
-
'top-right': { top: '27px', right: '29px' },
|
|
1429
|
-
'bottom-center': { bottom: '19px', left: '50%', transform: 'translateX(-50%)' },
|
|
1842
|
+
const positionStyles = {
|
|
1843
|
+
'bottom-left': { bottom: '20px', left: '80px' },
|
|
1844
|
+
'bottom-right': { bottom: '20px', right: '16px' },
|
|
1845
|
+
'top-left': { top: '20px', left: '80px' },
|
|
1846
|
+
'top-right': { top: '20px', right: '16px' },
|
|
1847
|
+
'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
|
|
1430
1848
|
};
|
|
1431
|
-
const posStyle =
|
|
1849
|
+
const posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
|
|
1850
|
+
const wrapper = this.container;
|
|
1851
|
+
// Reset position properties first
|
|
1852
|
+
wrapper.style.top = '';
|
|
1853
|
+
wrapper.style.bottom = '';
|
|
1854
|
+
wrapper.style.left = '';
|
|
1855
|
+
wrapper.style.right = '';
|
|
1856
|
+
wrapper.style.transform = '';
|
|
1857
|
+
Object.assign(wrapper.style, {
|
|
1858
|
+
position: 'fixed',
|
|
1859
|
+
...posStyle,
|
|
1860
|
+
zIndex: '9999',
|
|
1861
|
+
backgroundColor: 'rgba(17, 24, 39, 0.95)',
|
|
1862
|
+
border: `1px solid ${accentColor}`,
|
|
1863
|
+
borderRadius: '20px',
|
|
1864
|
+
color: accentColor,
|
|
1865
|
+
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
|
|
1866
|
+
backdropFilter: 'blur(8px)',
|
|
1867
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
1868
|
+
padding: '6px 10px',
|
|
1869
|
+
display: 'flex',
|
|
1870
|
+
alignItems: 'center',
|
|
1871
|
+
gap: '8px',
|
|
1872
|
+
fontFamily: FONT_MONO,
|
|
1873
|
+
fontSize: '0.6875rem',
|
|
1874
|
+
});
|
|
1875
|
+
// Connection indicator
|
|
1876
|
+
const connIndicator = document.createElement('span');
|
|
1877
|
+
connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
|
|
1878
|
+
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected ? 'Sweetlink connected' : 'Sweetlink disconnected');
|
|
1879
|
+
Object.assign(connIndicator.style, {
|
|
1880
|
+
width: '12px',
|
|
1881
|
+
height: '12px',
|
|
1882
|
+
borderRadius: '50%',
|
|
1883
|
+
display: 'flex',
|
|
1884
|
+
alignItems: 'center',
|
|
1885
|
+
justifyContent: 'center',
|
|
1886
|
+
cursor: 'pointer',
|
|
1887
|
+
});
|
|
1888
|
+
connIndicator.onclick = (e) => {
|
|
1889
|
+
e.stopPropagation();
|
|
1890
|
+
this.collapsed = true;
|
|
1891
|
+
this.debug.state('Collapsed DevBar from compact mode');
|
|
1892
|
+
this.render();
|
|
1893
|
+
};
|
|
1894
|
+
const connDot = document.createElement('span');
|
|
1895
|
+
Object.assign(connDot.style, {
|
|
1896
|
+
width: '6px',
|
|
1897
|
+
height: '6px',
|
|
1898
|
+
borderRadius: '50%',
|
|
1899
|
+
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1900
|
+
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1901
|
+
});
|
|
1902
|
+
connIndicator.appendChild(connDot);
|
|
1903
|
+
wrapper.appendChild(connIndicator);
|
|
1904
|
+
// Error badge
|
|
1905
|
+
if (errorCount > 0) {
|
|
1906
|
+
wrapper.appendChild(this.createConsoleBadge('error', errorCount, BUTTON_COLORS.error));
|
|
1907
|
+
}
|
|
1908
|
+
// Warning badge
|
|
1909
|
+
if (warningCount > 0) {
|
|
1910
|
+
wrapper.appendChild(this.createConsoleBadge('warn', warningCount, BUTTON_COLORS.warning));
|
|
1911
|
+
}
|
|
1912
|
+
// Screenshot button (if enabled)
|
|
1913
|
+
if (this.options.showScreenshot) {
|
|
1914
|
+
wrapper.appendChild(this.createScreenshotButton(accentColor));
|
|
1915
|
+
}
|
|
1916
|
+
// Settings gear button
|
|
1917
|
+
wrapper.appendChild(this.createSettingsButton());
|
|
1918
|
+
// Expand button (double-arrow)
|
|
1919
|
+
const expandBtn = document.createElement('button');
|
|
1920
|
+
expandBtn.type = 'button';
|
|
1921
|
+
expandBtn.className = this.tooltipClass('right');
|
|
1922
|
+
expandBtn.setAttribute('data-tooltip', 'Expand DevBar');
|
|
1923
|
+
Object.assign(expandBtn.style, {
|
|
1924
|
+
display: 'flex',
|
|
1925
|
+
alignItems: 'center',
|
|
1926
|
+
justifyContent: 'center',
|
|
1927
|
+
width: '18px',
|
|
1928
|
+
height: '18px',
|
|
1929
|
+
borderRadius: '50%',
|
|
1930
|
+
border: `1px solid ${accentColor}60`,
|
|
1931
|
+
backgroundColor: 'transparent',
|
|
1932
|
+
color: `${accentColor}99`,
|
|
1933
|
+
cursor: 'pointer',
|
|
1934
|
+
fontSize: '0.5rem',
|
|
1935
|
+
transition: 'all 150ms',
|
|
1936
|
+
});
|
|
1937
|
+
expandBtn.textContent = '⟫';
|
|
1938
|
+
expandBtn.onmouseenter = () => {
|
|
1939
|
+
expandBtn.style.backgroundColor = `${accentColor}20`;
|
|
1940
|
+
expandBtn.style.borderColor = accentColor;
|
|
1941
|
+
expandBtn.style.color = accentColor;
|
|
1942
|
+
};
|
|
1943
|
+
expandBtn.onmouseleave = () => {
|
|
1944
|
+
expandBtn.style.backgroundColor = 'transparent';
|
|
1945
|
+
expandBtn.style.borderColor = `${accentColor}60`;
|
|
1946
|
+
expandBtn.style.color = `${accentColor}99`;
|
|
1947
|
+
};
|
|
1948
|
+
expandBtn.onclick = () => {
|
|
1949
|
+
this.toggleCompactMode();
|
|
1950
|
+
};
|
|
1951
|
+
wrapper.appendChild(expandBtn);
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Create the settings gear button
|
|
1955
|
+
*/
|
|
1956
|
+
createSettingsButton() {
|
|
1957
|
+
const btn = document.createElement('button');
|
|
1958
|
+
btn.type = 'button';
|
|
1959
|
+
btn.className = this.tooltipClass('right');
|
|
1960
|
+
btn.setAttribute('data-tooltip', 'Settings (Cmd+Shift+M: toggle compact)');
|
|
1961
|
+
const isActive = this.showSettingsPopover;
|
|
1962
|
+
const color = COLORS.textSecondary;
|
|
1963
|
+
Object.assign(btn.style, {
|
|
1964
|
+
display: 'flex',
|
|
1965
|
+
alignItems: 'center',
|
|
1966
|
+
justifyContent: 'center',
|
|
1967
|
+
width: '22px',
|
|
1968
|
+
height: '22px',
|
|
1969
|
+
minWidth: '22px',
|
|
1970
|
+
minHeight: '22px',
|
|
1971
|
+
flexShrink: '0',
|
|
1972
|
+
borderRadius: '50%',
|
|
1973
|
+
border: `1px solid ${isActive ? color : `${color}60`}`,
|
|
1974
|
+
backgroundColor: isActive ? `${color}20` : 'transparent',
|
|
1975
|
+
color: isActive ? color : `${color}99`,
|
|
1976
|
+
cursor: 'pointer',
|
|
1977
|
+
transition: 'all 150ms',
|
|
1978
|
+
});
|
|
1979
|
+
btn.onclick = () => {
|
|
1980
|
+
this.showSettingsPopover = !this.showSettingsPopover;
|
|
1981
|
+
this.consoleFilter = null;
|
|
1982
|
+
this.showOutlineModal = false;
|
|
1983
|
+
this.showSchemaModal = false;
|
|
1984
|
+
this.showDesignReviewConfirm = false;
|
|
1985
|
+
this.render();
|
|
1986
|
+
};
|
|
1987
|
+
// Gear icon SVG
|
|
1988
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1989
|
+
svg.setAttribute('width', '12');
|
|
1990
|
+
svg.setAttribute('height', '12');
|
|
1991
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1992
|
+
svg.setAttribute('fill', 'none');
|
|
1993
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
1994
|
+
svg.setAttribute('stroke-width', '2');
|
|
1995
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
1996
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
1997
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1998
|
+
path.setAttribute('d', 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z');
|
|
1999
|
+
svg.appendChild(path);
|
|
2000
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
2001
|
+
circle.setAttribute('cx', '12');
|
|
2002
|
+
circle.setAttribute('cy', '12');
|
|
2003
|
+
circle.setAttribute('r', '3');
|
|
2004
|
+
svg.appendChild(circle);
|
|
2005
|
+
btn.appendChild(svg);
|
|
2006
|
+
return btn;
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Create the compact mode toggle button with chevron icon
|
|
2010
|
+
*/
|
|
2011
|
+
createCompactToggleButton() {
|
|
2012
|
+
const btn = document.createElement('button');
|
|
2013
|
+
btn.type = 'button';
|
|
2014
|
+
btn.className = this.tooltipClass('right');
|
|
2015
|
+
const isCompact = this.compactMode;
|
|
2016
|
+
const tooltip = isCompact ? 'Expand (Cmd+Shift+M)' : 'Compact (Cmd+Shift+M)';
|
|
2017
|
+
btn.setAttribute('data-tooltip', tooltip);
|
|
2018
|
+
const { accentColor } = this.options;
|
|
2019
|
+
const iconColor = COLORS.textSecondary;
|
|
2020
|
+
Object.assign(btn.style, {
|
|
2021
|
+
display: 'flex',
|
|
2022
|
+
alignItems: 'center',
|
|
2023
|
+
justifyContent: 'center',
|
|
2024
|
+
width: '22px',
|
|
2025
|
+
height: '22px',
|
|
2026
|
+
minWidth: '22px',
|
|
2027
|
+
minHeight: '22px',
|
|
2028
|
+
flexShrink: '0',
|
|
2029
|
+
borderRadius: '50%',
|
|
2030
|
+
border: `1px solid ${accentColor}60`,
|
|
2031
|
+
backgroundColor: 'transparent',
|
|
2032
|
+
color: `${iconColor}99`,
|
|
2033
|
+
cursor: 'pointer',
|
|
2034
|
+
transition: 'all 150ms',
|
|
2035
|
+
});
|
|
2036
|
+
btn.onmouseenter = () => {
|
|
2037
|
+
btn.style.borderColor = accentColor;
|
|
2038
|
+
btn.style.backgroundColor = `${accentColor}20`;
|
|
2039
|
+
btn.style.color = iconColor;
|
|
2040
|
+
};
|
|
2041
|
+
btn.onmouseleave = () => {
|
|
2042
|
+
btn.style.borderColor = `${accentColor}60`;
|
|
2043
|
+
btn.style.backgroundColor = 'transparent';
|
|
2044
|
+
btn.style.color = `${iconColor}99`;
|
|
2045
|
+
};
|
|
2046
|
+
btn.onclick = () => {
|
|
2047
|
+
this.toggleCompactMode();
|
|
2048
|
+
};
|
|
2049
|
+
// Chevron icon SVG - points right when expanded, left when compact
|
|
2050
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
2051
|
+
svg.setAttribute('width', '12');
|
|
2052
|
+
svg.setAttribute('height', '12');
|
|
2053
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
2054
|
+
svg.setAttribute('fill', 'none');
|
|
2055
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
2056
|
+
svg.setAttribute('stroke-width', '2');
|
|
2057
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
2058
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
2059
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
2060
|
+
// Left chevron (<) when expanded to shrink, right chevron (>) when compact to expand
|
|
2061
|
+
path.setAttribute('points', isCompact ? '9 18 15 12 9 6' : '15 18 9 12 15 6');
|
|
2062
|
+
svg.appendChild(path);
|
|
2063
|
+
btn.appendChild(svg);
|
|
2064
|
+
return btn;
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Create a settings section with title
|
|
2068
|
+
*/
|
|
2069
|
+
createSettingsSection(title, hasBorder = true) {
|
|
2070
|
+
const color = COLORS.textSecondary;
|
|
2071
|
+
const section = document.createElement('div');
|
|
2072
|
+
Object.assign(section.style, {
|
|
2073
|
+
padding: '10px 14px',
|
|
2074
|
+
borderBottom: hasBorder ? `1px solid ${color}20` : 'none',
|
|
2075
|
+
});
|
|
2076
|
+
const sectionTitle = document.createElement('div');
|
|
2077
|
+
Object.assign(sectionTitle.style, {
|
|
2078
|
+
color,
|
|
2079
|
+
fontSize: '0.625rem',
|
|
2080
|
+
textTransform: 'uppercase',
|
|
2081
|
+
letterSpacing: '0.1em',
|
|
2082
|
+
marginBottom: '8px',
|
|
2083
|
+
});
|
|
2084
|
+
sectionTitle.textContent = title;
|
|
2085
|
+
section.appendChild(sectionTitle);
|
|
2086
|
+
return section;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Create a toggle switch row
|
|
2090
|
+
*/
|
|
2091
|
+
createToggleRow(label, checked, accentColor, onChange) {
|
|
2092
|
+
const color = COLORS.textSecondary;
|
|
2093
|
+
const row = document.createElement('div');
|
|
2094
|
+
Object.assign(row.style, {
|
|
2095
|
+
display: 'flex',
|
|
2096
|
+
alignItems: 'center',
|
|
2097
|
+
justifyContent: 'space-between',
|
|
2098
|
+
marginBottom: '6px',
|
|
2099
|
+
});
|
|
2100
|
+
const labelEl = document.createElement('span');
|
|
2101
|
+
Object.assign(labelEl.style, { color: COLORS.text, fontSize: '0.6875rem' });
|
|
2102
|
+
labelEl.textContent = label;
|
|
2103
|
+
row.appendChild(labelEl);
|
|
2104
|
+
const toggle = document.createElement('button');
|
|
2105
|
+
Object.assign(toggle.style, {
|
|
2106
|
+
width: '32px',
|
|
2107
|
+
height: '18px',
|
|
2108
|
+
borderRadius: '9px',
|
|
2109
|
+
border: 'none',
|
|
2110
|
+
backgroundColor: checked ? accentColor : `${color}40`,
|
|
2111
|
+
position: 'relative',
|
|
2112
|
+
cursor: 'pointer',
|
|
2113
|
+
transition: 'all 150ms',
|
|
2114
|
+
flexShrink: '0',
|
|
2115
|
+
});
|
|
2116
|
+
const knob = document.createElement('span');
|
|
2117
|
+
Object.assign(knob.style, {
|
|
2118
|
+
position: 'absolute',
|
|
2119
|
+
top: '2px',
|
|
2120
|
+
left: checked ? '16px' : '2px',
|
|
2121
|
+
width: '14px',
|
|
2122
|
+
height: '14px',
|
|
2123
|
+
borderRadius: '50%',
|
|
2124
|
+
backgroundColor: '#fff',
|
|
2125
|
+
transition: 'left 150ms',
|
|
2126
|
+
});
|
|
2127
|
+
toggle.appendChild(knob);
|
|
2128
|
+
toggle.onclick = onChange;
|
|
2129
|
+
row.appendChild(toggle);
|
|
2130
|
+
return row;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Render the settings popover
|
|
2134
|
+
*/
|
|
2135
|
+
renderSettingsPopover() {
|
|
2136
|
+
const { position, accentColor } = this.options;
|
|
2137
|
+
const color = COLORS.textSecondary;
|
|
2138
|
+
const popover = document.createElement('div');
|
|
2139
|
+
popover.setAttribute('data-devbar', 'true');
|
|
2140
|
+
// Position based on devbar position
|
|
2141
|
+
const isTop = position.startsWith('top');
|
|
2142
|
+
const isRight = position.includes('right');
|
|
2143
|
+
Object.assign(popover.style, {
|
|
2144
|
+
position: 'fixed',
|
|
2145
|
+
[isTop ? 'top' : 'bottom']: isTop ? '70px' : '70px',
|
|
2146
|
+
[isRight ? 'right' : 'left']: isRight ? '16px' : '80px',
|
|
2147
|
+
zIndex: '10003',
|
|
2148
|
+
backgroundColor: 'rgba(17, 24, 39, 0.98)',
|
|
2149
|
+
border: `1px solid ${accentColor}`,
|
|
2150
|
+
borderRadius: '8px',
|
|
2151
|
+
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${accentColor}33`,
|
|
2152
|
+
backdropFilter: 'blur(8px)',
|
|
2153
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
2154
|
+
minWidth: '240px',
|
|
2155
|
+
maxWidth: '280px',
|
|
2156
|
+
maxHeight: 'calc(100vh - 100px)',
|
|
2157
|
+
overflowY: 'auto',
|
|
2158
|
+
fontFamily: FONT_MONO,
|
|
2159
|
+
});
|
|
2160
|
+
// Header
|
|
2161
|
+
const header = document.createElement('div');
|
|
2162
|
+
Object.assign(header.style, {
|
|
2163
|
+
display: 'flex',
|
|
2164
|
+
alignItems: 'center',
|
|
2165
|
+
justifyContent: 'space-between',
|
|
2166
|
+
padding: '10px 14px',
|
|
2167
|
+
borderBottom: `1px solid ${accentColor}30`,
|
|
2168
|
+
position: 'sticky',
|
|
2169
|
+
top: '0',
|
|
2170
|
+
backgroundColor: 'rgba(17, 24, 39, 0.98)',
|
|
2171
|
+
zIndex: '1',
|
|
2172
|
+
});
|
|
2173
|
+
const title = document.createElement('span');
|
|
2174
|
+
Object.assign(title.style, { color: accentColor, fontSize: '0.75rem', fontWeight: '600' });
|
|
2175
|
+
title.textContent = 'Settings';
|
|
2176
|
+
header.appendChild(title);
|
|
2177
|
+
const closeBtn = createStyledButton({
|
|
2178
|
+
color: COLORS.textMuted,
|
|
2179
|
+
text: '×',
|
|
2180
|
+
padding: '2px 6px',
|
|
2181
|
+
fontSize: '0.875rem',
|
|
2182
|
+
});
|
|
2183
|
+
closeBtn.style.border = 'none';
|
|
2184
|
+
closeBtn.onclick = () => {
|
|
2185
|
+
this.showSettingsPopover = false;
|
|
2186
|
+
this.render();
|
|
2187
|
+
};
|
|
2188
|
+
header.appendChild(closeBtn);
|
|
2189
|
+
popover.appendChild(header);
|
|
2190
|
+
// ========== THEME SECTION ==========
|
|
2191
|
+
const themeSection = this.createSettingsSection('Theme');
|
|
2192
|
+
const themeOptions = document.createElement('div');
|
|
2193
|
+
Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
|
|
2194
|
+
const themeModes = ['system', 'dark', 'light'];
|
|
2195
|
+
themeModes.forEach((mode) => {
|
|
2196
|
+
const btn = document.createElement('button');
|
|
2197
|
+
const isActive = this.themeMode === mode;
|
|
2198
|
+
Object.assign(btn.style, {
|
|
2199
|
+
padding: '4px 10px',
|
|
2200
|
+
backgroundColor: isActive ? `${accentColor}20` : 'transparent',
|
|
2201
|
+
border: `1px solid ${isActive ? accentColor : `${color}40`}`,
|
|
2202
|
+
borderRadius: '4px',
|
|
2203
|
+
color: isActive ? accentColor : color,
|
|
2204
|
+
fontSize: '0.625rem',
|
|
2205
|
+
cursor: 'pointer',
|
|
2206
|
+
textTransform: 'capitalize',
|
|
2207
|
+
transition: 'all 150ms',
|
|
2208
|
+
});
|
|
2209
|
+
btn.textContent = mode;
|
|
2210
|
+
btn.onclick = () => {
|
|
2211
|
+
this.setThemeMode(mode);
|
|
2212
|
+
};
|
|
2213
|
+
themeOptions.appendChild(btn);
|
|
2214
|
+
});
|
|
2215
|
+
themeSection.appendChild(themeOptions);
|
|
2216
|
+
popover.appendChild(themeSection);
|
|
2217
|
+
// ========== DISPLAY SECTION ==========
|
|
2218
|
+
const displaySection = this.createSettingsSection('Display');
|
|
2219
|
+
// Position mini-map selector
|
|
2220
|
+
const positionRow = document.createElement('div');
|
|
2221
|
+
Object.assign(positionRow.style, { marginBottom: '10px' });
|
|
2222
|
+
const posLabel = document.createElement('div');
|
|
2223
|
+
Object.assign(posLabel.style, {
|
|
2224
|
+
color: COLORS.text,
|
|
2225
|
+
fontSize: '0.6875rem',
|
|
2226
|
+
marginBottom: '6px',
|
|
2227
|
+
});
|
|
2228
|
+
posLabel.textContent = 'Position';
|
|
2229
|
+
positionRow.appendChild(posLabel);
|
|
2230
|
+
// Mini-map container
|
|
2231
|
+
const miniMap = document.createElement('div');
|
|
2232
|
+
Object.assign(miniMap.style, {
|
|
2233
|
+
position: 'relative',
|
|
2234
|
+
width: '100%',
|
|
2235
|
+
height: '50px',
|
|
2236
|
+
backgroundColor: 'rgba(10, 15, 26, 0.6)',
|
|
2237
|
+
border: `1px solid ${color}30`,
|
|
2238
|
+
borderRadius: '4px',
|
|
2239
|
+
});
|
|
2240
|
+
const positionConfigs = [
|
|
2241
|
+
{ value: 'top-left', style: { top: '8px', left: '10%' }, title: 'Top Left' },
|
|
2242
|
+
{ value: 'top-right', style: { top: '8px', right: '6%' }, title: 'Top Right' },
|
|
2243
|
+
{ value: 'bottom-left', style: { bottom: '8px', left: '10%' }, title: 'Bottom Left' },
|
|
2244
|
+
{ value: 'bottom-right', style: { bottom: '8px', right: '6%' }, title: 'Bottom Right' },
|
|
2245
|
+
{
|
|
2246
|
+
value: 'bottom-center',
|
|
2247
|
+
style: { bottom: '6px', left: '50%', transform: 'translateX(-50%)' },
|
|
2248
|
+
title: 'Bottom Center',
|
|
2249
|
+
},
|
|
2250
|
+
];
|
|
2251
|
+
positionConfigs.forEach(({ value, style, title }) => {
|
|
2252
|
+
const indicator = document.createElement('button');
|
|
2253
|
+
const isActive = this.options.position === value;
|
|
2254
|
+
Object.assign(indicator.style, {
|
|
2255
|
+
position: 'absolute',
|
|
2256
|
+
width: '20px',
|
|
2257
|
+
height: '6px',
|
|
2258
|
+
backgroundColor: isActive ? accentColor : `${color}60`,
|
|
2259
|
+
border: `1px solid ${isActive ? accentColor : `${color}40`}`,
|
|
2260
|
+
borderRadius: '2px',
|
|
2261
|
+
cursor: 'pointer',
|
|
2262
|
+
padding: '0',
|
|
2263
|
+
transition: 'all 150ms',
|
|
2264
|
+
boxShadow: isActive ? `0 0 8px ${accentColor}60` : 'none',
|
|
2265
|
+
...style,
|
|
2266
|
+
});
|
|
2267
|
+
indicator.title = title;
|
|
2268
|
+
indicator.onclick = () => {
|
|
2269
|
+
this.options.position = value;
|
|
2270
|
+
this.settingsManager.saveSettings({ position: value });
|
|
2271
|
+
this.render();
|
|
2272
|
+
};
|
|
2273
|
+
// Hover effect
|
|
2274
|
+
indicator.onmouseenter = () => {
|
|
2275
|
+
if (!isActive) {
|
|
2276
|
+
indicator.style.backgroundColor = accentColor;
|
|
2277
|
+
indicator.style.borderColor = accentColor;
|
|
2278
|
+
indicator.style.boxShadow = `0 0 6px ${accentColor}40`;
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
indicator.onmouseleave = () => {
|
|
2282
|
+
if (!isActive) {
|
|
2283
|
+
indicator.style.backgroundColor = `${color}60`;
|
|
2284
|
+
indicator.style.borderColor = `${color}40`;
|
|
2285
|
+
indicator.style.boxShadow = 'none';
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
miniMap.appendChild(indicator);
|
|
2289
|
+
});
|
|
2290
|
+
positionRow.appendChild(miniMap);
|
|
2291
|
+
displaySection.appendChild(positionRow);
|
|
2292
|
+
// Compact mode toggle
|
|
2293
|
+
displaySection.appendChild(this.createToggleRow('Compact Mode', this.compactMode, accentColor, () => {
|
|
2294
|
+
this.toggleCompactMode();
|
|
2295
|
+
}));
|
|
2296
|
+
// Keyboard shortcut hint
|
|
2297
|
+
const shortcutHint = document.createElement('div');
|
|
2298
|
+
Object.assign(shortcutHint.style, {
|
|
2299
|
+
color: COLORS.textMuted,
|
|
2300
|
+
fontSize: '0.5625rem',
|
|
2301
|
+
marginTop: '2px',
|
|
2302
|
+
marginBottom: '8px',
|
|
2303
|
+
});
|
|
2304
|
+
shortcutHint.textContent = 'Keyboard: Cmd+Shift+M';
|
|
2305
|
+
displaySection.appendChild(shortcutHint);
|
|
2306
|
+
// Accent color
|
|
2307
|
+
const accentRow = document.createElement('div');
|
|
2308
|
+
Object.assign(accentRow.style, { marginBottom: '6px' });
|
|
2309
|
+
const accentLabel = document.createElement('div');
|
|
2310
|
+
Object.assign(accentLabel.style, {
|
|
2311
|
+
color: COLORS.text,
|
|
2312
|
+
fontSize: '0.6875rem',
|
|
2313
|
+
marginBottom: '6px',
|
|
2314
|
+
});
|
|
2315
|
+
accentLabel.textContent = 'Accent Color';
|
|
2316
|
+
accentRow.appendChild(accentLabel);
|
|
2317
|
+
const colorSwatches = document.createElement('div');
|
|
2318
|
+
Object.assign(colorSwatches.style, {
|
|
2319
|
+
display: 'flex',
|
|
2320
|
+
gap: '6px',
|
|
2321
|
+
flexWrap: 'wrap',
|
|
2322
|
+
});
|
|
2323
|
+
ACCENT_COLOR_PRESETS.forEach(({ name, value }) => {
|
|
2324
|
+
const swatch = document.createElement('button');
|
|
2325
|
+
const isActive = this.options.accentColor === value;
|
|
2326
|
+
Object.assign(swatch.style, {
|
|
2327
|
+
width: '24px',
|
|
2328
|
+
height: '24px',
|
|
2329
|
+
borderRadius: '50%',
|
|
2330
|
+
backgroundColor: value,
|
|
2331
|
+
border: isActive ? '2px solid #fff' : '2px solid transparent',
|
|
2332
|
+
cursor: 'pointer',
|
|
2333
|
+
transition: 'all 150ms',
|
|
2334
|
+
boxShadow: isActive ? `0 0 8px ${value}` : 'none',
|
|
2335
|
+
});
|
|
2336
|
+
swatch.title = name;
|
|
2337
|
+
swatch.onclick = () => {
|
|
2338
|
+
this.options.accentColor = value;
|
|
2339
|
+
this.settingsManager.saveSettings({ accentColor: value });
|
|
2340
|
+
this.render();
|
|
2341
|
+
};
|
|
2342
|
+
colorSwatches.appendChild(swatch);
|
|
2343
|
+
});
|
|
2344
|
+
accentRow.appendChild(colorSwatches);
|
|
2345
|
+
displaySection.appendChild(accentRow);
|
|
2346
|
+
popover.appendChild(displaySection);
|
|
2347
|
+
// ========== FEATURES SECTION ==========
|
|
2348
|
+
const featuresSection = this.createSettingsSection('Features');
|
|
2349
|
+
featuresSection.appendChild(this.createToggleRow('Screenshot Button', this.options.showScreenshot, accentColor, () => {
|
|
2350
|
+
this.options.showScreenshot = !this.options.showScreenshot;
|
|
2351
|
+
this.settingsManager.saveSettings({ showScreenshot: this.options.showScreenshot });
|
|
2352
|
+
this.render();
|
|
2353
|
+
}));
|
|
2354
|
+
featuresSection.appendChild(this.createToggleRow('Console Badges', this.options.showConsoleBadges, accentColor, () => {
|
|
2355
|
+
this.options.showConsoleBadges = !this.options.showConsoleBadges;
|
|
2356
|
+
this.settingsManager.saveSettings({ showConsoleBadges: this.options.showConsoleBadges });
|
|
2357
|
+
this.render();
|
|
2358
|
+
}));
|
|
2359
|
+
featuresSection.appendChild(this.createToggleRow('Tooltips', this.options.showTooltips, accentColor, () => {
|
|
2360
|
+
this.options.showTooltips = !this.options.showTooltips;
|
|
2361
|
+
this.settingsManager.saveSettings({ showTooltips: this.options.showTooltips });
|
|
2362
|
+
this.render();
|
|
2363
|
+
}));
|
|
2364
|
+
popover.appendChild(featuresSection);
|
|
2365
|
+
// ========== METRICS SECTION ==========
|
|
2366
|
+
const metricsSection = this.createSettingsSection('Metrics');
|
|
2367
|
+
const metricsToggles = [
|
|
2368
|
+
{ key: 'breakpoint', label: 'Breakpoint' },
|
|
2369
|
+
{ key: 'fcp', label: 'FCP' },
|
|
2370
|
+
{ key: 'lcp', label: 'LCP' },
|
|
2371
|
+
{ key: 'cls', label: 'CLS' },
|
|
2372
|
+
{ key: 'inp', label: 'INP' },
|
|
2373
|
+
{ key: 'pageSize', label: 'Page Size' },
|
|
2374
|
+
];
|
|
2375
|
+
metricsToggles.forEach(({ key, label }) => {
|
|
2376
|
+
const currentValue = this.options.showMetrics[key] ?? true;
|
|
2377
|
+
metricsSection.appendChild(this.createToggleRow(label, currentValue, accentColor, () => {
|
|
2378
|
+
this.options.showMetrics[key] = !this.options.showMetrics[key];
|
|
2379
|
+
this.settingsManager.saveSettings({
|
|
2380
|
+
showMetrics: {
|
|
2381
|
+
breakpoint: this.options.showMetrics.breakpoint ?? true,
|
|
2382
|
+
fcp: this.options.showMetrics.fcp ?? true,
|
|
2383
|
+
lcp: this.options.showMetrics.lcp ?? true,
|
|
2384
|
+
cls: this.options.showMetrics.cls ?? true,
|
|
2385
|
+
inp: this.options.showMetrics.inp ?? true,
|
|
2386
|
+
pageSize: this.options.showMetrics.pageSize ?? true,
|
|
2387
|
+
},
|
|
2388
|
+
});
|
|
2389
|
+
this.render();
|
|
2390
|
+
}));
|
|
2391
|
+
});
|
|
2392
|
+
popover.appendChild(metricsSection);
|
|
2393
|
+
// ========== RESET SECTION ==========
|
|
2394
|
+
const resetSection = document.createElement('div');
|
|
2395
|
+
Object.assign(resetSection.style, {
|
|
2396
|
+
padding: '10px 14px',
|
|
2397
|
+
borderTop: `1px solid ${color}20`,
|
|
2398
|
+
});
|
|
2399
|
+
const resetBtn = createStyledButton({
|
|
2400
|
+
color: COLORS.textMuted,
|
|
2401
|
+
text: 'Reset to Defaults',
|
|
2402
|
+
padding: '6px 12px',
|
|
2403
|
+
fontSize: '0.625rem',
|
|
2404
|
+
});
|
|
2405
|
+
Object.assign(resetBtn.style, {
|
|
2406
|
+
width: '100%',
|
|
2407
|
+
justifyContent: 'center',
|
|
2408
|
+
});
|
|
2409
|
+
resetBtn.onclick = () => {
|
|
2410
|
+
this.resetToDefaults();
|
|
2411
|
+
};
|
|
2412
|
+
resetSection.appendChild(resetBtn);
|
|
2413
|
+
popover.appendChild(resetSection);
|
|
2414
|
+
this.overlayElement = popover;
|
|
2415
|
+
document.body.appendChild(popover);
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Reset all settings to defaults
|
|
2419
|
+
*/
|
|
2420
|
+
resetToDefaults() {
|
|
2421
|
+
this.settingsManager.resetToDefaults();
|
|
2422
|
+
const defaults = DEFAULT_SETTINGS;
|
|
2423
|
+
this.applySettings(defaults);
|
|
2424
|
+
}
|
|
2425
|
+
renderCollapsed() {
|
|
2426
|
+
if (!this.container)
|
|
2427
|
+
return;
|
|
2428
|
+
const { position, accentColor } = this.options;
|
|
2429
|
+
const { errorCount, warningCount } = this.getLogCounts();
|
|
2430
|
+
// Use captured dot position if available, otherwise fall back to preset positions
|
|
2431
|
+
// The 13px offset accounts for half the collapsed circle diameter (26px / 2)
|
|
2432
|
+
let posStyle;
|
|
2433
|
+
if (this.lastDotPosition) {
|
|
2434
|
+
// Position based on where the dot actually was
|
|
2435
|
+
const isTop = position.startsWith('top');
|
|
2436
|
+
posStyle = isTop
|
|
2437
|
+
? { top: `${this.lastDotPosition.top - 13}px`, left: `${this.lastDotPosition.left - 13}px` }
|
|
2438
|
+
: {
|
|
2439
|
+
bottom: `${this.lastDotPosition.bottom - 13}px`,
|
|
2440
|
+
left: `${this.lastDotPosition.left - 13}px`,
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
else {
|
|
2444
|
+
// Fallback preset positions for when no dot position was captured
|
|
2445
|
+
const collapsedPositions = {
|
|
2446
|
+
'bottom-left': { bottom: '27px', left: '86px' },
|
|
2447
|
+
'bottom-right': { bottom: '27px', right: '29px' },
|
|
2448
|
+
'top-left': { top: '27px', left: '86px' },
|
|
2449
|
+
'top-right': { top: '27px', right: '29px' },
|
|
2450
|
+
'bottom-center': { bottom: '19px', left: '50%', transform: 'translateX(-50%)' },
|
|
2451
|
+
};
|
|
2452
|
+
posStyle = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
|
|
2453
|
+
}
|
|
1432
2454
|
const wrapper = this.container;
|
|
1433
2455
|
wrapper.className = this.tooltipClass('left', 'devbar-collapse');
|
|
1434
2456
|
wrapper.setAttribute('data-tooltip', `Click to expand DevBar${this.sweetlinkConnected ? ' (Sweetlink connected)' : ' (Sweetlink not connected)'}${errorCount > 0 ? `\n${errorCount} console error${errorCount === 1 ? '' : 's'}` : ''}`);
|
|
@@ -1456,10 +2478,11 @@ export class GlobalDevBar {
|
|
|
1456
2478
|
width: '26px',
|
|
1457
2479
|
height: '26px',
|
|
1458
2480
|
boxSizing: 'border-box',
|
|
1459
|
-
animation: 'devbar-collapse 150ms ease-out'
|
|
2481
|
+
animation: 'devbar-collapse 150ms ease-out',
|
|
1460
2482
|
});
|
|
1461
2483
|
wrapper.onclick = () => {
|
|
1462
2484
|
this.collapsed = false;
|
|
2485
|
+
this.debug.state('Expanded DevBar');
|
|
1463
2486
|
this.render();
|
|
1464
2487
|
};
|
|
1465
2488
|
// Connection indicator dot (same size as in expanded state)
|
|
@@ -1469,7 +2492,7 @@ export class GlobalDevBar {
|
|
|
1469
2492
|
height: '6px',
|
|
1470
2493
|
borderRadius: '50%',
|
|
1471
2494
|
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1472
|
-
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none'
|
|
2495
|
+
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1473
2496
|
});
|
|
1474
2497
|
wrapper.appendChild(dot);
|
|
1475
2498
|
// Error badge (absolute, top-right of circle, shifted left if warning badge exists)
|
|
@@ -1524,10 +2547,21 @@ export class GlobalDevBar {
|
|
|
1524
2547
|
width: sizeOverrides?.width ?? defaultWidth,
|
|
1525
2548
|
maxWidth: sizeOverrides?.maxWidth ?? defaultMaxWidth,
|
|
1526
2549
|
minWidth: sizeOverrides?.minWidth ?? defaultMinWidth,
|
|
1527
|
-
cursor: 'default'
|
|
2550
|
+
cursor: 'default',
|
|
1528
2551
|
});
|
|
1529
2552
|
wrapper.ondblclick = () => {
|
|
2553
|
+
// Capture dot position before collapsing
|
|
2554
|
+
const dotEl = wrapper.querySelector('.devbar-status span span');
|
|
2555
|
+
if (dotEl) {
|
|
2556
|
+
const rect = dotEl.getBoundingClientRect();
|
|
2557
|
+
this.lastDotPosition = {
|
|
2558
|
+
left: rect.left + rect.width / 2,
|
|
2559
|
+
top: rect.top + rect.height / 2,
|
|
2560
|
+
bottom: window.innerHeight - (rect.top + rect.height / 2),
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
1530
2563
|
this.collapsed = true;
|
|
2564
|
+
this.debug.state('Collapsed DevBar (double-click)');
|
|
1531
2565
|
this.render();
|
|
1532
2566
|
};
|
|
1533
2567
|
// Main row - wrapping controlled by CSS media query
|
|
@@ -1544,12 +2578,14 @@ export class GlobalDevBar {
|
|
|
1544
2578
|
boxSizing: 'border-box',
|
|
1545
2579
|
fontFamily: FONT_MONO,
|
|
1546
2580
|
fontSize: '0.6875rem',
|
|
1547
|
-
lineHeight: '1rem'
|
|
2581
|
+
lineHeight: '1rem',
|
|
1548
2582
|
});
|
|
1549
2583
|
// Connection indicator (click to collapse)
|
|
1550
2584
|
const connIndicator = document.createElement('span');
|
|
1551
2585
|
connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
|
|
1552
|
-
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected
|
|
2586
|
+
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected
|
|
2587
|
+
? 'Sweetlink connected (click to minimize)'
|
|
2588
|
+
: 'Sweetlink disconnected (click to minimize)');
|
|
1553
2589
|
Object.assign(connIndicator.style, {
|
|
1554
2590
|
width: '12px',
|
|
1555
2591
|
height: '12px',
|
|
@@ -1559,11 +2595,19 @@ export class GlobalDevBar {
|
|
|
1559
2595
|
alignItems: 'center',
|
|
1560
2596
|
justifyContent: 'center',
|
|
1561
2597
|
cursor: 'pointer',
|
|
1562
|
-
flexShrink: '0'
|
|
2598
|
+
flexShrink: '0',
|
|
1563
2599
|
});
|
|
1564
2600
|
connIndicator.onclick = (e) => {
|
|
1565
2601
|
e.stopPropagation();
|
|
2602
|
+
// Capture dot position before collapsing (connDot is the inner 6px dot)
|
|
2603
|
+
const rect = connIndicator.getBoundingClientRect();
|
|
2604
|
+
this.lastDotPosition = {
|
|
2605
|
+
left: rect.left + rect.width / 2,
|
|
2606
|
+
top: rect.top + rect.height / 2,
|
|
2607
|
+
bottom: window.innerHeight - (rect.top + rect.height / 2),
|
|
2608
|
+
};
|
|
1566
2609
|
this.collapsed = true;
|
|
2610
|
+
this.debug.state('Collapsed DevBar (connection dot click)');
|
|
1567
2611
|
this.render();
|
|
1568
2612
|
};
|
|
1569
2613
|
const connDot = document.createElement('span');
|
|
@@ -1573,7 +2617,7 @@ export class GlobalDevBar {
|
|
|
1573
2617
|
borderRadius: '50%',
|
|
1574
2618
|
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1575
2619
|
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1576
|
-
transition: 'all 300ms'
|
|
2620
|
+
transition: 'all 300ms',
|
|
1577
2621
|
});
|
|
1578
2622
|
connIndicator.appendChild(connDot);
|
|
1579
2623
|
// Status row wrapper - keeps connection dot, info, and badges together
|
|
@@ -1584,7 +2628,7 @@ export class GlobalDevBar {
|
|
|
1584
2628
|
alignItems: 'center',
|
|
1585
2629
|
gap: '0.5rem',
|
|
1586
2630
|
flexWrap: 'nowrap',
|
|
1587
|
-
flexShrink: '0'
|
|
2631
|
+
flexShrink: '0',
|
|
1588
2632
|
});
|
|
1589
2633
|
statusRow.appendChild(connIndicator);
|
|
1590
2634
|
// Info section
|
|
@@ -1598,7 +2642,7 @@ export class GlobalDevBar {
|
|
|
1598
2642
|
letterSpacing: '0.05em',
|
|
1599
2643
|
flexShrink: '1',
|
|
1600
2644
|
minWidth: '0',
|
|
1601
|
-
overflow: 'visible'
|
|
2645
|
+
overflow: 'visible',
|
|
1602
2646
|
});
|
|
1603
2647
|
// Breakpoint info
|
|
1604
2648
|
if (showMetrics.breakpoint && this.breakpointInfo) {
|
|
@@ -1610,9 +2654,10 @@ export class GlobalDevBar {
|
|
|
1610
2654
|
bpSpan.setAttribute('data-tooltip', `Tailwind Breakpoint: ${bp}\n${breakpointData?.label || ''}\n\nViewport: ${this.breakpointInfo.dimensions}\n\nBreakpoints:\nbase: <640px | sm: >=640px\nmd: >=768px | lg: >=1024px\nxl: >=1280px | 2xl: >=1536px`);
|
|
1611
2655
|
let bpText = bp;
|
|
1612
2656
|
if (bp !== 'base') {
|
|
1613
|
-
bpText =
|
|
1614
|
-
|
|
1615
|
-
|
|
2657
|
+
bpText =
|
|
2658
|
+
bp === 'sm'
|
|
2659
|
+
? `${bp} - ${this.breakpointInfo.dimensions.split('x')[0]}`
|
|
2660
|
+
: `${bp} - ${this.breakpointInfo.dimensions}`;
|
|
1616
2661
|
}
|
|
1617
2662
|
bpSpan.textContent = bpText;
|
|
1618
2663
|
infoSection.appendChild(bpSpan);
|
|
@@ -1643,6 +2688,24 @@ export class GlobalDevBar {
|
|
|
1643
2688
|
lcpSpan.textContent = `LCP ${this.perfStats.lcp}`;
|
|
1644
2689
|
infoSection.appendChild(lcpSpan);
|
|
1645
2690
|
}
|
|
2691
|
+
if (showMetrics.cls) {
|
|
2692
|
+
addSeparator();
|
|
2693
|
+
const clsSpan = document.createElement('span');
|
|
2694
|
+
clsSpan.className = this.tooltipClass('left', 'devbar-item');
|
|
2695
|
+
Object.assign(clsSpan.style, { opacity: '0.85', cursor: 'default' });
|
|
2696
|
+
clsSpan.setAttribute('data-tooltip', 'Cumulative Layout Shift (CLS): Visual stability score.\nHigher values mean more unexpected layout shifts.\n\nGood: <0.1\nNeeds work: 0.1-0.25\nPoor: >0.25');
|
|
2697
|
+
clsSpan.textContent = `CLS ${this.perfStats.cls}`;
|
|
2698
|
+
infoSection.appendChild(clsSpan);
|
|
2699
|
+
}
|
|
2700
|
+
if (showMetrics.inp) {
|
|
2701
|
+
addSeparator();
|
|
2702
|
+
const inpSpan = document.createElement('span');
|
|
2703
|
+
inpSpan.className = this.tooltipClass('left', 'devbar-item');
|
|
2704
|
+
Object.assign(inpSpan.style, { opacity: '0.85', cursor: 'default' });
|
|
2705
|
+
inpSpan.setAttribute('data-tooltip', 'Interaction to Next Paint (INP): Responsiveness to user input.\nMeasures the longest interaction delay.\n\nGood: <200ms\nNeeds work: 200-500ms\nPoor: >500ms');
|
|
2706
|
+
inpSpan.textContent = `INP ${this.perfStats.inp}`;
|
|
2707
|
+
infoSection.appendChild(inpSpan);
|
|
2708
|
+
}
|
|
1646
2709
|
if (showMetrics.pageSize) {
|
|
1647
2710
|
addSeparator();
|
|
1648
2711
|
const sizeSpan = document.createElement('span');
|
|
@@ -1673,6 +2736,8 @@ export class GlobalDevBar {
|
|
|
1673
2736
|
actionsContainer.appendChild(this.createAIReviewButton());
|
|
1674
2737
|
actionsContainer.appendChild(this.createOutlineButton());
|
|
1675
2738
|
actionsContainer.appendChild(this.createSchemaButton());
|
|
2739
|
+
actionsContainer.appendChild(this.createSettingsButton());
|
|
2740
|
+
actionsContainer.appendChild(this.createCompactToggleButton());
|
|
1676
2741
|
mainRow.appendChild(actionsContainer);
|
|
1677
2742
|
wrapper.appendChild(mainRow);
|
|
1678
2743
|
// Render custom controls row if there are any
|
|
@@ -1690,7 +2755,7 @@ export class GlobalDevBar {
|
|
|
1690
2755
|
fontFamily: FONT_MONO,
|
|
1691
2756
|
fontSize: '0.6875rem',
|
|
1692
2757
|
});
|
|
1693
|
-
GlobalDevBar.customControls.forEach(control => {
|
|
2758
|
+
GlobalDevBar.customControls.forEach((control) => {
|
|
1694
2759
|
const btn = document.createElement('button');
|
|
1695
2760
|
btn.type = 'button';
|
|
1696
2761
|
const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
|
|
@@ -1765,13 +2830,7 @@ export class GlobalDevBar {
|
|
|
1765
2830
|
btn.type = 'button';
|
|
1766
2831
|
btn.className = this.tooltipClass('right');
|
|
1767
2832
|
const hasSuccessState = this.copiedToClipboard || this.copiedPath || this.lastScreenshot;
|
|
1768
|
-
const tooltip = this.
|
|
1769
|
-
? 'Copied to clipboard!'
|
|
1770
|
-
: this.copiedPath
|
|
1771
|
-
? 'Path copied to clipboard!'
|
|
1772
|
-
: this.lastScreenshot
|
|
1773
|
-
? `Screenshot saved!\n${this.lastScreenshot}\n\nClick to copy path`
|
|
1774
|
-
: `Screenshot\n\nClick: Save to file\nShift+Click: Copy to clipboard\n\nKeyboard:\nCmd/Ctrl+Shift+S: Save\nCmd/Ctrl+Shift+C: Copy${!this.sweetlinkConnected ? '\n\nWarning: Sweetlink not connected' : ''}`;
|
|
2833
|
+
const tooltip = this.getScreenshotTooltip();
|
|
1775
2834
|
btn.setAttribute('data-tooltip', tooltip);
|
|
1776
2835
|
Object.assign(btn.style, {
|
|
1777
2836
|
display: 'flex',
|
|
@@ -1789,7 +2848,7 @@ export class GlobalDevBar {
|
|
|
1789
2848
|
color: hasSuccessState ? accentColor : `${accentColor}99`,
|
|
1790
2849
|
cursor: !this.capturing ? 'pointer' : 'not-allowed',
|
|
1791
2850
|
opacity: '1',
|
|
1792
|
-
transition: 'all 150ms'
|
|
2851
|
+
transition: 'all 150ms',
|
|
1793
2852
|
});
|
|
1794
2853
|
btn.disabled = this.capturing;
|
|
1795
2854
|
btn.onclick = (e) => {
|
|
@@ -1835,17 +2894,47 @@ export class GlobalDevBar {
|
|
|
1835
2894
|
}
|
|
1836
2895
|
return btn;
|
|
1837
2896
|
}
|
|
2897
|
+
/**
|
|
2898
|
+
* Get the tooltip text for the screenshot button based on current state
|
|
2899
|
+
*/
|
|
2900
|
+
getScreenshotTooltip() {
|
|
2901
|
+
if (this.copiedToClipboard) {
|
|
2902
|
+
return 'Copied to clipboard!';
|
|
2903
|
+
}
|
|
2904
|
+
if (this.copiedPath) {
|
|
2905
|
+
return 'Path copied to clipboard!';
|
|
2906
|
+
}
|
|
2907
|
+
if (this.lastScreenshot) {
|
|
2908
|
+
return `Screenshot saved!\n${this.lastScreenshot}\n\nClick to copy path`;
|
|
2909
|
+
}
|
|
2910
|
+
const baseTooltip = `Screenshot\n\nClick: Save to file\nShift+Click: Copy to clipboard\n\nKeyboard:\nCmd/Ctrl+Shift+S: Save\nCmd/Ctrl+Shift+C: Copy`;
|
|
2911
|
+
return this.sweetlinkConnected
|
|
2912
|
+
? baseTooltip
|
|
2913
|
+
: `${baseTooltip}\n\nWarning: Sweetlink not connected`;
|
|
2914
|
+
}
|
|
2915
|
+
/**
|
|
2916
|
+
* Get the tooltip text for the AI review button based on current state
|
|
2917
|
+
*/
|
|
2918
|
+
getAIReviewTooltip() {
|
|
2919
|
+
if (this.designReviewInProgress) {
|
|
2920
|
+
return 'AI Design Review in progress...';
|
|
2921
|
+
}
|
|
2922
|
+
if (this.designReviewError) {
|
|
2923
|
+
return `Design review failed:\n${this.designReviewError}`;
|
|
2924
|
+
}
|
|
2925
|
+
if (this.lastDesignReview) {
|
|
2926
|
+
return `Design review saved to:\n${this.lastDesignReview}`;
|
|
2927
|
+
}
|
|
2928
|
+
const baseTooltip = `AI Design Review\n\nCaptures screenshot and sends to\nClaude for design analysis.\n\nRequires ANTHROPIC_API_KEY.`;
|
|
2929
|
+
return this.sweetlinkConnected
|
|
2930
|
+
? baseTooltip
|
|
2931
|
+
: `${baseTooltip}\n\nWarning: Sweetlink not connected`;
|
|
2932
|
+
}
|
|
1838
2933
|
createAIReviewButton() {
|
|
1839
2934
|
const btn = document.createElement('button');
|
|
1840
2935
|
btn.type = 'button';
|
|
1841
2936
|
btn.className = this.tooltipClass('right');
|
|
1842
|
-
const tooltip = this.
|
|
1843
|
-
? 'AI Design Review in progress...'
|
|
1844
|
-
: this.designReviewError
|
|
1845
|
-
? `Design review failed:\n${this.designReviewError}`
|
|
1846
|
-
: this.lastDesignReview
|
|
1847
|
-
? `Design review saved to:\n${this.lastDesignReview}`
|
|
1848
|
-
: `AI Design Review\n\nCaptures screenshot and sends to\nClaude for design analysis.\n\nRequires ANTHROPIC_API_KEY.${!this.sweetlinkConnected ? '\n\nWarning: Sweetlink not connected' : ''}`;
|
|
2937
|
+
const tooltip = this.getAIReviewTooltip();
|
|
1849
2938
|
btn.setAttribute('data-tooltip', tooltip);
|
|
1850
2939
|
const hasError = !!this.designReviewError;
|
|
1851
2940
|
const isActive = this.designReviewInProgress || !!this.lastDesignReview || hasError;
|
|
@@ -1945,7 +3034,7 @@ export function initGlobalDevBar(options) {
|
|
|
1945
3034
|
const existing = getGlobalInstance();
|
|
1946
3035
|
if (existing) {
|
|
1947
3036
|
// Check if already initialized with same position - skip re-init during HMR
|
|
1948
|
-
const existingPosition = existing
|
|
3037
|
+
const existingPosition = existing.getPosition();
|
|
1949
3038
|
const newPosition = options?.position ?? 'bottom-left';
|
|
1950
3039
|
if (existingPosition === newPosition) {
|
|
1951
3040
|
return existing;
|