@ytspar/devbar 1.0.0-canary.c37df82 → 1.0.0-canary.cdf7fa2
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 +89 -2
- package/dist/GlobalDevBar.js +1090 -102
- package/dist/accessibility.d.ts +84 -0
- package/dist/accessibility.js +155 -0
- package/dist/constants.d.ts +91 -0
- package/dist/constants.js +102 -12
- 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.js +5 -3
- 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, getThemeColors, MAX_CONSOLE_LOGS, MAX_RECONNECT_ATTEMPTS, MAX_RECONNECT_DELAY_MS, SCREENSHOT_BLUR_DELAY_MS, SCREENSHOT_NOTIFICATION_MS, SCREENSHOT_SCALE, TAILWIND_BREAKPOINTS, TOOLTIP_STYLES, WS_PORT, } 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;
|
|
@@ -80,7 +98,11 @@ export class GlobalDevBar {
|
|
|
80
98
|
this.breakpointInfo = null;
|
|
81
99
|
this.perfStats = null;
|
|
82
100
|
this.lcpValue = null;
|
|
101
|
+
this.clsValue = 0;
|
|
102
|
+
this.inpValue = 0;
|
|
83
103
|
this.reconnectAttempts = 0;
|
|
104
|
+
// Track the position of the connection indicator dot for smooth collapse
|
|
105
|
+
this.lastDotPosition = null;
|
|
84
106
|
this.reconnectTimeout = null;
|
|
85
107
|
this.screenshotTimeout = null;
|
|
86
108
|
this.copiedPathTimeout = null;
|
|
@@ -92,9 +114,24 @@ export class GlobalDevBar {
|
|
|
92
114
|
this.keydownHandler = null;
|
|
93
115
|
this.fcpObserver = null;
|
|
94
116
|
this.lcpObserver = null;
|
|
117
|
+
this.clsObserver = null;
|
|
118
|
+
this.inpObserver = null;
|
|
95
119
|
this.destroyed = false;
|
|
120
|
+
// Theme state
|
|
121
|
+
this.themeMode = 'system';
|
|
122
|
+
this.themeMediaQuery = null;
|
|
123
|
+
this.themeMediaHandler = null;
|
|
124
|
+
// Compact mode state
|
|
125
|
+
this.compactMode = false;
|
|
126
|
+
// Settings popover state
|
|
127
|
+
this.showSettingsPopover = false;
|
|
96
128
|
// Overlay element for modals
|
|
97
129
|
this.overlayElement = null;
|
|
130
|
+
// Initialize debug config first so we can log during construction
|
|
131
|
+
this.debugConfig = normalizeDebugConfig(options.debug);
|
|
132
|
+
this.debug = new DebugLogger(this.debugConfig);
|
|
133
|
+
// Initialize settings manager
|
|
134
|
+
this.settingsManager = getSettingsManager();
|
|
98
135
|
this.options = {
|
|
99
136
|
position: options.position ?? 'bottom-left',
|
|
100
137
|
accentColor: options.accentColor ?? COLORS.primary,
|
|
@@ -102,6 +139,8 @@ export class GlobalDevBar {
|
|
|
102
139
|
breakpoint: options.showMetrics?.breakpoint ?? true,
|
|
103
140
|
fcp: options.showMetrics?.fcp ?? true,
|
|
104
141
|
lcp: options.showMetrics?.lcp ?? true,
|
|
142
|
+
cls: options.showMetrics?.cls ?? true,
|
|
143
|
+
inp: options.showMetrics?.inp ?? true,
|
|
105
144
|
pageSize: options.showMetrics?.pageSize ?? true,
|
|
106
145
|
},
|
|
107
146
|
showScreenshot: options.showScreenshot ?? true,
|
|
@@ -109,6 +148,7 @@ export class GlobalDevBar {
|
|
|
109
148
|
showTooltips: options.showTooltips ?? true,
|
|
110
149
|
sizeOverrides: options.sizeOverrides,
|
|
111
150
|
};
|
|
151
|
+
this.debug.lifecycle('GlobalDevBar constructed', { options: this.options });
|
|
112
152
|
}
|
|
113
153
|
/**
|
|
114
154
|
* Get tooltip class name(s) if tooltips are enabled, otherwise empty string
|
|
@@ -153,7 +193,7 @@ export class GlobalDevBar {
|
|
|
153
193
|
fontWeight: '600',
|
|
154
194
|
display: 'flex',
|
|
155
195
|
alignItems: 'center',
|
|
156
|
-
justifyContent: 'center'
|
|
196
|
+
justifyContent: 'center',
|
|
157
197
|
});
|
|
158
198
|
badge.textContent = count > 99 ? '!' : String(count);
|
|
159
199
|
return badge;
|
|
@@ -166,7 +206,7 @@ export class GlobalDevBar {
|
|
|
166
206
|
*/
|
|
167
207
|
static registerControl(control) {
|
|
168
208
|
// Remove existing control with same ID
|
|
169
|
-
GlobalDevBar.customControls = GlobalDevBar.customControls.filter(c => c.id !== control.id);
|
|
209
|
+
GlobalDevBar.customControls = GlobalDevBar.customControls.filter((c) => c.id !== control.id);
|
|
170
210
|
GlobalDevBar.customControls.push(control);
|
|
171
211
|
// Trigger re-render of all instances
|
|
172
212
|
const instance = getGlobalInstance();
|
|
@@ -178,7 +218,7 @@ export class GlobalDevBar {
|
|
|
178
218
|
* Unregister a custom control by ID
|
|
179
219
|
*/
|
|
180
220
|
static unregisterControl(id) {
|
|
181
|
-
GlobalDevBar.customControls = GlobalDevBar.customControls.filter(c => c.id !== id);
|
|
221
|
+
GlobalDevBar.customControls = GlobalDevBar.customControls.filter((c) => c.id !== id);
|
|
182
222
|
// Trigger re-render of all instances
|
|
183
223
|
const instance = getGlobalInstance();
|
|
184
224
|
if (instance) {
|
|
@@ -209,10 +249,16 @@ export class GlobalDevBar {
|
|
|
209
249
|
return;
|
|
210
250
|
if (this.destroyed)
|
|
211
251
|
return;
|
|
252
|
+
this.debug.lifecycle('Initializing DevBar');
|
|
212
253
|
// Inject tooltip styles
|
|
213
254
|
this.injectStyles();
|
|
214
255
|
// Copy early captured logs
|
|
215
256
|
this.consoleLogs = [...earlyConsoleCapture.logs];
|
|
257
|
+
this.debug.lifecycle('Copied early console logs', { count: this.consoleLogs.length });
|
|
258
|
+
// Setup theme
|
|
259
|
+
this.setupTheme();
|
|
260
|
+
// Load compact mode from storage
|
|
261
|
+
this.loadCompactMode();
|
|
216
262
|
// Setup WebSocket connection
|
|
217
263
|
this.connectWebSocket();
|
|
218
264
|
// Setup breakpoint detection
|
|
@@ -223,11 +269,19 @@ export class GlobalDevBar {
|
|
|
223
269
|
this.setupKeyboardShortcuts();
|
|
224
270
|
// Initial render
|
|
225
271
|
this.render();
|
|
272
|
+
this.debug.lifecycle('DevBar initialized successfully');
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get the current position
|
|
276
|
+
*/
|
|
277
|
+
getPosition() {
|
|
278
|
+
return this.options.position;
|
|
226
279
|
}
|
|
227
280
|
/**
|
|
228
281
|
* Destroy the devbar and cleanup
|
|
229
282
|
*/
|
|
230
283
|
destroy() {
|
|
284
|
+
this.debug.lifecycle('Destroying DevBar');
|
|
231
285
|
this.destroyed = true;
|
|
232
286
|
// Close WebSocket
|
|
233
287
|
this.reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // Prevent reconnection
|
|
@@ -256,6 +310,14 @@ export class GlobalDevBar {
|
|
|
256
310
|
this.fcpObserver.disconnect();
|
|
257
311
|
if (this.lcpObserver)
|
|
258
312
|
this.lcpObserver.disconnect();
|
|
313
|
+
if (this.clsObserver)
|
|
314
|
+
this.clsObserver.disconnect();
|
|
315
|
+
if (this.inpObserver)
|
|
316
|
+
this.inpObserver.disconnect();
|
|
317
|
+
// Remove theme media listener
|
|
318
|
+
if (this.themeMediaQuery && this.themeMediaHandler) {
|
|
319
|
+
this.themeMediaQuery.removeEventListener('change', this.themeMediaHandler);
|
|
320
|
+
}
|
|
259
321
|
// Restore console
|
|
260
322
|
if (earlyConsoleCapture.originalConsole) {
|
|
261
323
|
console.log = earlyConsoleCapture.originalConsole.log;
|
|
@@ -272,6 +334,7 @@ export class GlobalDevBar {
|
|
|
272
334
|
this.overlayElement.remove();
|
|
273
335
|
this.overlayElement = null;
|
|
274
336
|
}
|
|
337
|
+
this.debug.lifecycle('DevBar destroyed');
|
|
275
338
|
}
|
|
276
339
|
injectStyles() {
|
|
277
340
|
const styleId = 'devbar-tooltip-styles';
|
|
@@ -285,17 +348,25 @@ export class GlobalDevBar {
|
|
|
285
348
|
connectWebSocket() {
|
|
286
349
|
if (this.destroyed)
|
|
287
350
|
return;
|
|
351
|
+
this.debug.ws('Connecting to WebSocket', { port: WS_PORT });
|
|
288
352
|
const ws = new WebSocket(`ws://localhost:${WS_PORT}`);
|
|
289
353
|
this.ws = ws;
|
|
290
354
|
ws.onopen = () => {
|
|
291
355
|
this.sweetlinkConnected = true;
|
|
292
356
|
this.reconnectAttempts = 0;
|
|
357
|
+
this.debug.ws('WebSocket connected');
|
|
358
|
+
// Update settings manager with WebSocket connection
|
|
359
|
+
this.settingsManager.setWebSocket(ws);
|
|
360
|
+
this.settingsManager.setConnected(true);
|
|
293
361
|
ws.send(JSON.stringify({ type: 'browser-client-ready' }));
|
|
362
|
+
// Request settings from server
|
|
363
|
+
ws.send(JSON.stringify({ type: 'load-settings' }));
|
|
294
364
|
this.render();
|
|
295
365
|
};
|
|
296
366
|
ws.onmessage = async (event) => {
|
|
297
367
|
try {
|
|
298
368
|
const command = JSON.parse(event.data);
|
|
369
|
+
this.debug.ws('Received command', { type: command.type });
|
|
299
370
|
await this.handleSweetlinkCommand(command);
|
|
300
371
|
}
|
|
301
372
|
catch (e) {
|
|
@@ -304,16 +375,20 @@ export class GlobalDevBar {
|
|
|
304
375
|
};
|
|
305
376
|
ws.onclose = () => {
|
|
306
377
|
this.sweetlinkConnected = false;
|
|
378
|
+
this.settingsManager.setConnected(false);
|
|
379
|
+
this.debug.ws('WebSocket disconnected');
|
|
307
380
|
this.render();
|
|
308
381
|
// Auto-reconnect with exponential backoff
|
|
309
382
|
if (!this.destroyed && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
310
|
-
const delayMs = BASE_RECONNECT_DELAY_MS *
|
|
383
|
+
const delayMs = BASE_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempts;
|
|
311
384
|
this.reconnectAttempts++;
|
|
385
|
+
this.debug.ws('Scheduling reconnect', { attempt: this.reconnectAttempts, delayMs });
|
|
312
386
|
this.reconnectTimeout = setTimeout(() => this.connectWebSocket(), Math.min(delayMs, MAX_RECONNECT_DELAY_MS));
|
|
313
387
|
}
|
|
314
388
|
};
|
|
315
389
|
ws.onerror = () => {
|
|
316
390
|
// Error will trigger onclose, which handles reconnection
|
|
391
|
+
this.debug.ws('WebSocket error');
|
|
317
392
|
};
|
|
318
393
|
}
|
|
319
394
|
async handleSweetlinkCommand(command) {
|
|
@@ -325,11 +400,20 @@ export class GlobalDevBar {
|
|
|
325
400
|
const targetElement = command.selector
|
|
326
401
|
? document.querySelector(command.selector) || document.body
|
|
327
402
|
: document.body;
|
|
328
|
-
const canvas = await html2canvas(targetElement, {
|
|
403
|
+
const canvas = await html2canvas(targetElement, {
|
|
404
|
+
logging: false,
|
|
405
|
+
useCORS: true,
|
|
406
|
+
allowTaint: true,
|
|
407
|
+
});
|
|
329
408
|
ws.send(JSON.stringify({
|
|
330
409
|
success: true,
|
|
331
|
-
data: {
|
|
332
|
-
|
|
410
|
+
data: {
|
|
411
|
+
screenshot: canvas.toDataURL('image/png'),
|
|
412
|
+
width: canvas.width,
|
|
413
|
+
height: canvas.height,
|
|
414
|
+
selector: command.selector || 'body',
|
|
415
|
+
},
|
|
416
|
+
timestamp: Date.now(),
|
|
333
417
|
}));
|
|
334
418
|
break;
|
|
335
419
|
}
|
|
@@ -337,7 +421,7 @@ export class GlobalDevBar {
|
|
|
337
421
|
let logs = this.consoleLogs;
|
|
338
422
|
if (command.filter) {
|
|
339
423
|
const filter = command.filter.toLowerCase();
|
|
340
|
-
logs = logs.filter(log => log.level.includes(filter) || log.message.toLowerCase().includes(filter));
|
|
424
|
+
logs = logs.filter((log) => log.level.includes(filter) || log.message.toLowerCase().includes(filter));
|
|
341
425
|
}
|
|
342
426
|
ws.send(JSON.stringify({ success: true, data: logs, timestamp: Date.now() }));
|
|
343
427
|
break;
|
|
@@ -348,9 +432,18 @@ export class GlobalDevBar {
|
|
|
348
432
|
const results = elements.map((el) => {
|
|
349
433
|
if (command.property)
|
|
350
434
|
return el[command.property] ?? null;
|
|
351
|
-
return {
|
|
435
|
+
return {
|
|
436
|
+
tagName: el.tagName,
|
|
437
|
+
className: el.className,
|
|
438
|
+
id: el.id,
|
|
439
|
+
textContent: el.textContent?.trim().slice(0, 100),
|
|
440
|
+
};
|
|
352
441
|
});
|
|
353
|
-
ws.send(JSON.stringify({
|
|
442
|
+
ws.send(JSON.stringify({
|
|
443
|
+
success: true,
|
|
444
|
+
data: { count: results.length, results },
|
|
445
|
+
timestamp: Date.now(),
|
|
446
|
+
}));
|
|
354
447
|
}
|
|
355
448
|
break;
|
|
356
449
|
}
|
|
@@ -363,7 +456,11 @@ export class GlobalDevBar {
|
|
|
363
456
|
ws.send(JSON.stringify({ success: true, data: result, timestamp: Date.now() }));
|
|
364
457
|
}
|
|
365
458
|
catch (e) {
|
|
366
|
-
ws.send(JSON.stringify({
|
|
459
|
+
ws.send(JSON.stringify({
|
|
460
|
+
success: false,
|
|
461
|
+
error: e instanceof Error ? e.message : 'Execution failed',
|
|
462
|
+
timestamp: Date.now(),
|
|
463
|
+
}));
|
|
367
464
|
}
|
|
368
465
|
}
|
|
369
466
|
break;
|
|
@@ -413,8 +510,48 @@ export class GlobalDevBar {
|
|
|
413
510
|
case 'schema-error':
|
|
414
511
|
console.error('[GlobalDevBar] Schema save failed:', command.error);
|
|
415
512
|
break;
|
|
513
|
+
case 'settings-loaded':
|
|
514
|
+
this.handleSettingsLoaded(command.settings);
|
|
515
|
+
break;
|
|
516
|
+
case 'settings-saved':
|
|
517
|
+
this.debug.state('Settings saved to server', { path: command.settingsPath });
|
|
518
|
+
break;
|
|
519
|
+
case 'settings-error':
|
|
520
|
+
console.error('[GlobalDevBar] Settings operation failed:', command.error);
|
|
521
|
+
break;
|
|
416
522
|
}
|
|
417
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Handle settings loaded from server
|
|
526
|
+
*/
|
|
527
|
+
handleSettingsLoaded(settings) {
|
|
528
|
+
if (!settings) {
|
|
529
|
+
this.debug.state('No server settings found, using local');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
this.debug.state('Settings loaded from server', settings);
|
|
533
|
+
// Update settings manager
|
|
534
|
+
this.settingsManager.handleSettingsLoaded(settings);
|
|
535
|
+
// Apply settings to local state
|
|
536
|
+
this.applySettings(settings);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Apply settings to the DevBar state and options
|
|
540
|
+
*/
|
|
541
|
+
applySettings(settings) {
|
|
542
|
+
// Update local state
|
|
543
|
+
this.themeMode = settings.themeMode;
|
|
544
|
+
this.compactMode = settings.compactMode;
|
|
545
|
+
// Update options
|
|
546
|
+
this.options.position = settings.position;
|
|
547
|
+
this.options.accentColor = settings.accentColor;
|
|
548
|
+
this.options.showScreenshot = settings.showScreenshot;
|
|
549
|
+
this.options.showConsoleBadges = settings.showConsoleBadges;
|
|
550
|
+
this.options.showTooltips = settings.showTooltips;
|
|
551
|
+
this.options.showMetrics = { ...settings.showMetrics };
|
|
552
|
+
// Re-render with new settings
|
|
553
|
+
this.render();
|
|
554
|
+
}
|
|
418
555
|
/**
|
|
419
556
|
* Handle notification state updates with auto-clear timeout
|
|
420
557
|
*/
|
|
@@ -427,25 +564,37 @@ export class GlobalDevBar {
|
|
|
427
564
|
this.lastScreenshot = path;
|
|
428
565
|
if (this.screenshotTimeout)
|
|
429
566
|
clearTimeout(this.screenshotTimeout);
|
|
430
|
-
this.screenshotTimeout = setTimeout(() => {
|
|
567
|
+
this.screenshotTimeout = setTimeout(() => {
|
|
568
|
+
this.lastScreenshot = null;
|
|
569
|
+
this.render();
|
|
570
|
+
}, durationMs);
|
|
431
571
|
break;
|
|
432
572
|
case 'designReview':
|
|
433
573
|
this.lastDesignReview = path;
|
|
434
574
|
if (this.designReviewTimeout)
|
|
435
575
|
clearTimeout(this.designReviewTimeout);
|
|
436
|
-
this.designReviewTimeout = setTimeout(() => {
|
|
576
|
+
this.designReviewTimeout = setTimeout(() => {
|
|
577
|
+
this.lastDesignReview = null;
|
|
578
|
+
this.render();
|
|
579
|
+
}, durationMs);
|
|
437
580
|
break;
|
|
438
581
|
case 'outline':
|
|
439
582
|
this.lastOutline = path;
|
|
440
583
|
if (this.outlineTimeout)
|
|
441
584
|
clearTimeout(this.outlineTimeout);
|
|
442
|
-
this.outlineTimeout = setTimeout(() => {
|
|
585
|
+
this.outlineTimeout = setTimeout(() => {
|
|
586
|
+
this.lastOutline = null;
|
|
587
|
+
this.render();
|
|
588
|
+
}, durationMs);
|
|
443
589
|
break;
|
|
444
590
|
case 'schema':
|
|
445
591
|
this.lastSchema = path;
|
|
446
592
|
if (this.schemaTimeout)
|
|
447
593
|
clearTimeout(this.schemaTimeout);
|
|
448
|
-
this.schemaTimeout = setTimeout(() => {
|
|
594
|
+
this.schemaTimeout = setTimeout(() => {
|
|
595
|
+
this.lastSchema = null;
|
|
596
|
+
this.render();
|
|
597
|
+
}, durationMs);
|
|
449
598
|
break;
|
|
450
599
|
}
|
|
451
600
|
this.render();
|
|
@@ -455,20 +604,17 @@ export class GlobalDevBar {
|
|
|
455
604
|
const width = window.innerWidth;
|
|
456
605
|
const height = window.innerHeight;
|
|
457
606
|
// 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';
|
|
607
|
+
const breakpointOrder = [
|
|
608
|
+
'2xl',
|
|
609
|
+
'xl',
|
|
610
|
+
'lg',
|
|
611
|
+
'md',
|
|
612
|
+
'sm',
|
|
613
|
+
];
|
|
614
|
+
const tailwindBreakpoint = breakpointOrder.find((bp) => width >= TAILWIND_BREAKPOINTS[bp].min) ?? 'base';
|
|
469
615
|
this.breakpointInfo = {
|
|
470
616
|
tailwindBreakpoint,
|
|
471
|
-
dimensions: `${width}x${height}
|
|
617
|
+
dimensions: `${width}x${height}`,
|
|
472
618
|
};
|
|
473
619
|
this.render();
|
|
474
620
|
};
|
|
@@ -480,10 +626,14 @@ export class GlobalDevBar {
|
|
|
480
626
|
const updatePerfStats = () => {
|
|
481
627
|
// FCP
|
|
482
628
|
const paintEntries = performance.getEntriesByType('paint');
|
|
483
|
-
const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
|
|
629
|
+
const fcpEntry = paintEntries.find((entry) => entry.name === 'first-contentful-paint');
|
|
484
630
|
const fcp = fcpEntry ? `${Math.round(fcpEntry.startTime)}ms` : '-';
|
|
485
631
|
// LCP (from cached value, updated by observer)
|
|
486
632
|
const lcp = this.lcpValue !== null ? `${Math.round(this.lcpValue)}ms` : '-';
|
|
633
|
+
// CLS (cumulative layout shift)
|
|
634
|
+
const cls = this.clsValue > 0 ? this.clsValue.toFixed(3) : '-';
|
|
635
|
+
// INP (Interaction to Next Paint)
|
|
636
|
+
const inp = this.inpValue > 0 ? `${Math.round(this.inpValue)}ms` : '-';
|
|
487
637
|
// Total Resource Size
|
|
488
638
|
const resources = performance.getEntriesByType('resource');
|
|
489
639
|
let totalBytes = 0;
|
|
@@ -498,7 +648,8 @@ export class GlobalDevBar {
|
|
|
498
648
|
const totalSize = totalBytes > 1024 * 1024
|
|
499
649
|
? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB`
|
|
500
650
|
: `${Math.round(totalBytes / 1024)} KB`;
|
|
501
|
-
this.perfStats = { fcp, lcp, totalSize };
|
|
651
|
+
this.perfStats = { fcp, lcp, cls, inp, totalSize };
|
|
652
|
+
this.debug.perf('Performance stats updated', this.perfStats);
|
|
502
653
|
this.render();
|
|
503
654
|
};
|
|
504
655
|
if (document.readyState === 'complete') {
|
|
@@ -529,6 +680,7 @@ export class GlobalDevBar {
|
|
|
529
680
|
const lastEntry = entries[entries.length - 1];
|
|
530
681
|
if (lastEntry) {
|
|
531
682
|
this.lcpValue = lastEntry.startTime;
|
|
683
|
+
this.debug.perf('LCP updated', { lcp: this.lcpValue });
|
|
532
684
|
updatePerfStats();
|
|
533
685
|
}
|
|
534
686
|
});
|
|
@@ -537,12 +689,60 @@ export class GlobalDevBar {
|
|
|
537
689
|
catch (e) {
|
|
538
690
|
console.warn('[GlobalDevBar] LCP PerformanceObserver not supported', e);
|
|
539
691
|
}
|
|
692
|
+
// CLS Observer (Cumulative Layout Shift)
|
|
693
|
+
try {
|
|
694
|
+
this.clsObserver = new PerformanceObserver((list) => {
|
|
695
|
+
for (const entry of list.getEntries()) {
|
|
696
|
+
// Only count layout shifts without recent user input
|
|
697
|
+
const layoutShift = entry;
|
|
698
|
+
if (!layoutShift.hadRecentInput && layoutShift.value) {
|
|
699
|
+
this.clsValue += layoutShift.value;
|
|
700
|
+
this.debug.perf('CLS updated', { cls: this.clsValue });
|
|
701
|
+
updatePerfStats();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
this.clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
706
|
+
}
|
|
707
|
+
catch (e) {
|
|
708
|
+
console.warn('[GlobalDevBar] CLS PerformanceObserver not supported', e);
|
|
709
|
+
}
|
|
710
|
+
// INP Observer (Interaction to Next Paint)
|
|
711
|
+
try {
|
|
712
|
+
this.inpObserver = new PerformanceObserver((list) => {
|
|
713
|
+
for (const entry of list.getEntries()) {
|
|
714
|
+
const eventEntry = entry;
|
|
715
|
+
if (eventEntry.duration && eventEntry.duration > this.inpValue) {
|
|
716
|
+
this.inpValue = eventEntry.duration;
|
|
717
|
+
this.debug.perf('INP updated', { inp: this.inpValue });
|
|
718
|
+
updatePerfStats();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
// durationThreshold filters out very short interactions
|
|
723
|
+
this.inpObserver.observe({
|
|
724
|
+
type: 'event',
|
|
725
|
+
buffered: true,
|
|
726
|
+
durationThreshold: 16,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
catch (e) {
|
|
730
|
+
console.warn('[GlobalDevBar] INP PerformanceObserver not supported', e);
|
|
731
|
+
}
|
|
540
732
|
}
|
|
541
733
|
setupKeyboardShortcuts() {
|
|
542
734
|
this.keydownHandler = (e) => {
|
|
543
|
-
// Close modals on Escape
|
|
735
|
+
// Close modals/popovers on Escape
|
|
544
736
|
if (e.key === 'Escape') {
|
|
545
|
-
if (this.
|
|
737
|
+
if (this.showSettingsPopover) {
|
|
738
|
+
this.showSettingsPopover = false;
|
|
739
|
+
this.render();
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (this.consoleFilter ||
|
|
743
|
+
this.showOutlineModal ||
|
|
744
|
+
this.showSchemaModal ||
|
|
745
|
+
this.showDesignReviewConfirm) {
|
|
546
746
|
this.consoleFilter = null;
|
|
547
747
|
this.showOutlineModal = false;
|
|
548
748
|
this.showSchemaModal = false;
|
|
@@ -552,6 +752,12 @@ export class GlobalDevBar {
|
|
|
552
752
|
}
|
|
553
753
|
}
|
|
554
754
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
755
|
+
// Cmd/Ctrl+Shift+M: Toggle compact mode
|
|
756
|
+
if (e.key === 'M' || e.key === 'm') {
|
|
757
|
+
e.preventDefault();
|
|
758
|
+
this.toggleCompactMode();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
555
761
|
if (e.key === 'S' || e.key === 's') {
|
|
556
762
|
e.preventDefault();
|
|
557
763
|
if (this.sweetlinkConnected && !this.capturing) {
|
|
@@ -571,6 +777,66 @@ export class GlobalDevBar {
|
|
|
571
777
|
};
|
|
572
778
|
window.addEventListener('keydown', this.keydownHandler);
|
|
573
779
|
}
|
|
780
|
+
setupTheme() {
|
|
781
|
+
// Load stored theme preference from settings manager
|
|
782
|
+
const settings = this.settingsManager.getSettings();
|
|
783
|
+
this.themeMode = settings.themeMode;
|
|
784
|
+
this.debug.state('Theme loaded', { mode: this.themeMode });
|
|
785
|
+
// Listen for system theme changes
|
|
786
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
787
|
+
this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
788
|
+
this.themeMediaHandler = () => {
|
|
789
|
+
if (this.themeMode === 'system') {
|
|
790
|
+
this.debug.state('System theme changed', {
|
|
791
|
+
effectiveTheme: getEffectiveTheme(this.themeMode),
|
|
792
|
+
});
|
|
793
|
+
this.render();
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
this.themeMediaQuery.addEventListener('change', this.themeMediaHandler);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
loadCompactMode() {
|
|
800
|
+
const settings = this.settingsManager.getSettings();
|
|
801
|
+
this.compactMode = settings.compactMode;
|
|
802
|
+
this.debug.state('Compact mode loaded', { compactMode: this.compactMode });
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Get the current theme mode
|
|
806
|
+
*/
|
|
807
|
+
getThemeMode() {
|
|
808
|
+
return this.themeMode;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Set the theme mode
|
|
812
|
+
*/
|
|
813
|
+
setThemeMode(mode) {
|
|
814
|
+
this.themeMode = mode;
|
|
815
|
+
this.settingsManager.saveSettings({ themeMode: mode });
|
|
816
|
+
this.debug.state('Theme mode changed', { mode, effectiveTheme: getEffectiveTheme(mode) });
|
|
817
|
+
this.render();
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Get the current effective theme colors
|
|
821
|
+
*/
|
|
822
|
+
getColors() {
|
|
823
|
+
return getThemeColors(this.themeMode);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Toggle compact mode
|
|
827
|
+
*/
|
|
828
|
+
toggleCompactMode() {
|
|
829
|
+
this.compactMode = !this.compactMode;
|
|
830
|
+
this.settingsManager.saveSettings({ compactMode: this.compactMode });
|
|
831
|
+
this.debug.state('Compact mode toggled', { compactMode: this.compactMode });
|
|
832
|
+
this.render();
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Check if compact mode is enabled
|
|
836
|
+
*/
|
|
837
|
+
isCompactMode() {
|
|
838
|
+
return this.compactMode;
|
|
839
|
+
}
|
|
574
840
|
async copyPathToClipboard(path) {
|
|
575
841
|
try {
|
|
576
842
|
await navigator.clipboard.writeText(path);
|
|
@@ -604,7 +870,7 @@ export class GlobalDevBar {
|
|
|
604
870
|
allowTaint: true,
|
|
605
871
|
scale: SCREENSHOT_SCALE,
|
|
606
872
|
width: window.innerWidth,
|
|
607
|
-
windowWidth: window.innerWidth
|
|
873
|
+
windowWidth: window.innerWidth,
|
|
608
874
|
});
|
|
609
875
|
// Restore page state
|
|
610
876
|
cleanup();
|
|
@@ -626,8 +892,33 @@ export class GlobalDevBar {
|
|
|
626
892
|
}
|
|
627
893
|
}
|
|
628
894
|
else {
|
|
629
|
-
const dataUrl = canvasToDataUrl(canvas, {
|
|
895
|
+
const dataUrl = canvasToDataUrl(canvas, {
|
|
896
|
+
format: 'jpeg',
|
|
897
|
+
quality: DEVBAR_SCREENSHOT_QUALITY,
|
|
898
|
+
});
|
|
630
899
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
900
|
+
// Include web vitals metrics
|
|
901
|
+
const webVitals = {};
|
|
902
|
+
if (this.lcpValue !== null)
|
|
903
|
+
webVitals.lcp = Math.round(this.lcpValue);
|
|
904
|
+
if (this.clsValue > 0)
|
|
905
|
+
webVitals.cls = this.clsValue;
|
|
906
|
+
if (this.inpValue > 0)
|
|
907
|
+
webVitals.inp = Math.round(this.inpValue);
|
|
908
|
+
// Get FCP from performance entries
|
|
909
|
+
const fcpEntry = performance
|
|
910
|
+
.getEntriesByType('paint')
|
|
911
|
+
.find((e) => e.name === 'first-contentful-paint');
|
|
912
|
+
if (fcpEntry)
|
|
913
|
+
webVitals.fcp = Math.round(fcpEntry.startTime);
|
|
914
|
+
// Calculate page size
|
|
915
|
+
let pageSize = 0;
|
|
916
|
+
const navEntry = performance.getEntriesByType('navigation')[0];
|
|
917
|
+
if (navEntry)
|
|
918
|
+
pageSize += navEntry.transferSize || 0;
|
|
919
|
+
performance.getEntriesByType('resource').forEach((entry) => {
|
|
920
|
+
pageSize += entry.transferSize || 0;
|
|
921
|
+
});
|
|
631
922
|
this.ws.send(JSON.stringify({
|
|
632
923
|
type: 'save-screenshot',
|
|
633
924
|
data: {
|
|
@@ -636,8 +927,10 @@ export class GlobalDevBar {
|
|
|
636
927
|
height: canvas.height,
|
|
637
928
|
logs: this.consoleLogs,
|
|
638
929
|
url: window.location.href,
|
|
639
|
-
timestamp: Date.now()
|
|
640
|
-
|
|
930
|
+
timestamp: Date.now(),
|
|
931
|
+
webVitals: Object.keys(webVitals).length > 0 ? webVitals : undefined,
|
|
932
|
+
pageSize: pageSize > 0 ? pageSize : undefined,
|
|
933
|
+
},
|
|
641
934
|
}));
|
|
642
935
|
}
|
|
643
936
|
}
|
|
@@ -672,7 +965,7 @@ export class GlobalDevBar {
|
|
|
672
965
|
allowTaint: true,
|
|
673
966
|
scale: 1, // Full quality for design review
|
|
674
967
|
width: window.innerWidth,
|
|
675
|
-
windowWidth: window.innerWidth
|
|
968
|
+
windowWidth: window.innerWidth,
|
|
676
969
|
});
|
|
677
970
|
// Restore page state
|
|
678
971
|
cleanup();
|
|
@@ -687,8 +980,8 @@ export class GlobalDevBar {
|
|
|
687
980
|
height: canvas.height,
|
|
688
981
|
logs: this.consoleLogs,
|
|
689
982
|
url: window.location.href,
|
|
690
|
-
timestamp: Date.now()
|
|
691
|
-
}
|
|
983
|
+
timestamp: Date.now(),
|
|
984
|
+
},
|
|
692
985
|
}));
|
|
693
986
|
}
|
|
694
987
|
}
|
|
@@ -783,8 +1076,8 @@ export class GlobalDevBar {
|
|
|
783
1076
|
markdown,
|
|
784
1077
|
url: window.location.href,
|
|
785
1078
|
title: document.title,
|
|
786
|
-
timestamp: Date.now()
|
|
787
|
-
}
|
|
1079
|
+
timestamp: Date.now(),
|
|
1080
|
+
},
|
|
788
1081
|
}));
|
|
789
1082
|
}
|
|
790
1083
|
}
|
|
@@ -799,8 +1092,8 @@ export class GlobalDevBar {
|
|
|
799
1092
|
markdown,
|
|
800
1093
|
url: window.location.href,
|
|
801
1094
|
title: document.title,
|
|
802
|
-
timestamp: Date.now()
|
|
803
|
-
}
|
|
1095
|
+
timestamp: Date.now(),
|
|
1096
|
+
},
|
|
804
1097
|
}));
|
|
805
1098
|
}
|
|
806
1099
|
}
|
|
@@ -831,6 +1124,9 @@ export class GlobalDevBar {
|
|
|
831
1124
|
if (this.collapsed) {
|
|
832
1125
|
this.renderCollapsed();
|
|
833
1126
|
}
|
|
1127
|
+
else if (this.compactMode) {
|
|
1128
|
+
this.renderCompact();
|
|
1129
|
+
}
|
|
834
1130
|
else {
|
|
835
1131
|
this.renderExpanded();
|
|
836
1132
|
}
|
|
@@ -860,6 +1156,10 @@ export class GlobalDevBar {
|
|
|
860
1156
|
if (this.showDesignReviewConfirm) {
|
|
861
1157
|
this.renderDesignReviewConfirmModal();
|
|
862
1158
|
}
|
|
1159
|
+
// Render settings popover
|
|
1160
|
+
if (this.showSettingsPopover) {
|
|
1161
|
+
this.renderSettingsPopover();
|
|
1162
|
+
}
|
|
863
1163
|
}
|
|
864
1164
|
renderDesignReviewConfirmModal() {
|
|
865
1165
|
const color = BUTTON_COLORS.review;
|
|
@@ -883,7 +1183,12 @@ export class GlobalDevBar {
|
|
|
883
1183
|
Object.assign(title.style, { color, fontSize: '0.875rem', fontWeight: '600' });
|
|
884
1184
|
title.textContent = 'AI Design Review';
|
|
885
1185
|
header.appendChild(title);
|
|
886
|
-
const closeBtn = createStyledButton({
|
|
1186
|
+
const closeBtn = createStyledButton({
|
|
1187
|
+
color: COLORS.textMuted,
|
|
1188
|
+
text: '×',
|
|
1189
|
+
padding: '0',
|
|
1190
|
+
fontSize: '1.25rem',
|
|
1191
|
+
});
|
|
887
1192
|
closeBtn.style.border = 'none';
|
|
888
1193
|
closeBtn.onclick = closeModal;
|
|
889
1194
|
header.appendChild(closeBtn);
|
|
@@ -915,7 +1220,11 @@ export class GlobalDevBar {
|
|
|
915
1220
|
padding: '14px 18px',
|
|
916
1221
|
borderTop: `1px solid ${COLORS.border}`,
|
|
917
1222
|
});
|
|
918
|
-
const cancelBtn = createStyledButton({
|
|
1223
|
+
const cancelBtn = createStyledButton({
|
|
1224
|
+
color: COLORS.textMuted,
|
|
1225
|
+
text: 'Cancel',
|
|
1226
|
+
padding: '8px 16px',
|
|
1227
|
+
});
|
|
919
1228
|
cancelBtn.onclick = closeModal;
|
|
920
1229
|
footer.appendChild(cancelBtn);
|
|
921
1230
|
if (this.apiKeyStatus?.configured) {
|
|
@@ -938,7 +1247,11 @@ export class GlobalDevBar {
|
|
|
938
1247
|
const instructions = document.createElement('div');
|
|
939
1248
|
Object.assign(instructions.style, { marginBottom: '12px' });
|
|
940
1249
|
const instructTitle = document.createElement('div');
|
|
941
|
-
Object.assign(instructTitle.style, {
|
|
1250
|
+
Object.assign(instructTitle.style, {
|
|
1251
|
+
color: COLORS.textSecondary,
|
|
1252
|
+
fontWeight: '600',
|
|
1253
|
+
marginBottom: '8px',
|
|
1254
|
+
});
|
|
942
1255
|
instructTitle.textContent = 'To configure:';
|
|
943
1256
|
instructions.appendChild(instructTitle);
|
|
944
1257
|
const steps = [
|
|
@@ -998,7 +1311,11 @@ export class GlobalDevBar {
|
|
|
998
1311
|
// Model info
|
|
999
1312
|
if (this.apiKeyStatus?.model) {
|
|
1000
1313
|
const modelDiv = document.createElement('div');
|
|
1001
|
-
Object.assign(modelDiv.style, {
|
|
1314
|
+
Object.assign(modelDiv.style, {
|
|
1315
|
+
color: COLORS.textMuted,
|
|
1316
|
+
fontSize: '0.6875rem',
|
|
1317
|
+
marginTop: '12px',
|
|
1318
|
+
});
|
|
1002
1319
|
modelDiv.textContent = `Model: ${this.apiKeyStatus.model}`;
|
|
1003
1320
|
if (this.apiKeyStatus.maskedKey) {
|
|
1004
1321
|
modelDiv.textContent += ` | Key: ${this.apiKeyStatus.maskedKey}`;
|
|
@@ -1011,7 +1328,7 @@ export class GlobalDevBar {
|
|
|
1011
1328
|
const filterType = this.consoleFilter;
|
|
1012
1329
|
if (!filterType)
|
|
1013
1330
|
return;
|
|
1014
|
-
const logs = earlyConsoleCapture.logs.filter(log => log.level === filterType);
|
|
1331
|
+
const logs = earlyConsoleCapture.logs.filter((log) => log.level === filterType);
|
|
1015
1332
|
const color = filterType === 'error' ? BUTTON_COLORS.error : BUTTON_COLORS.warning;
|
|
1016
1333
|
const label = filterType === 'error' ? 'Errors' : 'Warnings';
|
|
1017
1334
|
const popup = document.createElement('div');
|
|
@@ -1120,7 +1437,8 @@ export class GlobalDevBar {
|
|
|
1120
1437
|
wordBreak: 'break-word',
|
|
1121
1438
|
whiteSpace: 'pre-wrap',
|
|
1122
1439
|
});
|
|
1123
|
-
message.textContent =
|
|
1440
|
+
message.textContent =
|
|
1441
|
+
log.message.length > 500 ? `${log.message.slice(0, 500)}...` : log.message;
|
|
1124
1442
|
logItem.appendChild(message);
|
|
1125
1443
|
container.appendChild(logItem);
|
|
1126
1444
|
});
|
|
@@ -1192,7 +1510,7 @@ export class GlobalDevBar {
|
|
|
1192
1510
|
fontSize: '0.6875rem',
|
|
1193
1511
|
marginLeft: '8px',
|
|
1194
1512
|
});
|
|
1195
|
-
const truncatedText = node.text.length > 60 ? node.text.slice(0, 60)
|
|
1513
|
+
const truncatedText = node.text.length > 60 ? `${node.text.slice(0, 60)}...` : node.text;
|
|
1196
1514
|
textSpan.textContent = truncatedText;
|
|
1197
1515
|
nodeEl.appendChild(textSpan);
|
|
1198
1516
|
if (node.id) {
|
|
@@ -1325,7 +1643,7 @@ export class GlobalDevBar {
|
|
|
1325
1643
|
punct: COLORS.textMuted, // gray
|
|
1326
1644
|
};
|
|
1327
1645
|
// 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+)?)|([{}
|
|
1646
|
+
const tokenPattern = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)|([{}[\],])|(\s+)/g;
|
|
1329
1647
|
for (const match of json.matchAll(tokenPattern)) {
|
|
1330
1648
|
const [, str, colon, bool, nullToken, num, punct, whitespace] = match;
|
|
1331
1649
|
if (whitespace) {
|
|
@@ -1412,23 +1730,626 @@ export class GlobalDevBar {
|
|
|
1412
1730
|
container.appendChild(row);
|
|
1413
1731
|
}
|
|
1414
1732
|
}
|
|
1415
|
-
|
|
1733
|
+
/**
|
|
1734
|
+
* Render compact mode - single row with essential controls only
|
|
1735
|
+
* Shows: connection dot, error/warn badges, screenshot button, settings gear
|
|
1736
|
+
*/
|
|
1737
|
+
renderCompact() {
|
|
1416
1738
|
if (!this.container)
|
|
1417
1739
|
return;
|
|
1418
1740
|
const { position, accentColor } = this.options;
|
|
1419
1741
|
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%)' },
|
|
1742
|
+
const positionStyles = {
|
|
1743
|
+
'bottom-left': { bottom: '20px', left: '80px' },
|
|
1744
|
+
'bottom-right': { bottom: '20px', right: '16px' },
|
|
1745
|
+
'top-left': { top: '20px', left: '80px' },
|
|
1746
|
+
'top-right': { top: '20px', right: '16px' },
|
|
1747
|
+
'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
|
|
1430
1748
|
};
|
|
1431
|
-
const posStyle =
|
|
1749
|
+
const posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
|
|
1750
|
+
const wrapper = this.container;
|
|
1751
|
+
// Reset position properties first
|
|
1752
|
+
wrapper.style.top = '';
|
|
1753
|
+
wrapper.style.bottom = '';
|
|
1754
|
+
wrapper.style.left = '';
|
|
1755
|
+
wrapper.style.right = '';
|
|
1756
|
+
wrapper.style.transform = '';
|
|
1757
|
+
Object.assign(wrapper.style, {
|
|
1758
|
+
position: 'fixed',
|
|
1759
|
+
...posStyle,
|
|
1760
|
+
zIndex: '9999',
|
|
1761
|
+
backgroundColor: 'rgba(17, 24, 39, 0.95)',
|
|
1762
|
+
border: `1px solid ${accentColor}`,
|
|
1763
|
+
borderRadius: '20px',
|
|
1764
|
+
color: accentColor,
|
|
1765
|
+
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
|
|
1766
|
+
backdropFilter: 'blur(8px)',
|
|
1767
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
1768
|
+
padding: '6px 10px',
|
|
1769
|
+
display: 'flex',
|
|
1770
|
+
alignItems: 'center',
|
|
1771
|
+
gap: '8px',
|
|
1772
|
+
fontFamily: FONT_MONO,
|
|
1773
|
+
fontSize: '0.6875rem',
|
|
1774
|
+
});
|
|
1775
|
+
// Connection indicator
|
|
1776
|
+
const connIndicator = document.createElement('span');
|
|
1777
|
+
connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
|
|
1778
|
+
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected ? 'Sweetlink connected' : 'Sweetlink disconnected');
|
|
1779
|
+
Object.assign(connIndicator.style, {
|
|
1780
|
+
width: '12px',
|
|
1781
|
+
height: '12px',
|
|
1782
|
+
borderRadius: '50%',
|
|
1783
|
+
display: 'flex',
|
|
1784
|
+
alignItems: 'center',
|
|
1785
|
+
justifyContent: 'center',
|
|
1786
|
+
cursor: 'pointer',
|
|
1787
|
+
});
|
|
1788
|
+
connIndicator.onclick = (e) => {
|
|
1789
|
+
e.stopPropagation();
|
|
1790
|
+
this.collapsed = true;
|
|
1791
|
+
this.debug.state('Collapsed DevBar from compact mode');
|
|
1792
|
+
this.render();
|
|
1793
|
+
};
|
|
1794
|
+
const connDot = document.createElement('span');
|
|
1795
|
+
Object.assign(connDot.style, {
|
|
1796
|
+
width: '6px',
|
|
1797
|
+
height: '6px',
|
|
1798
|
+
borderRadius: '50%',
|
|
1799
|
+
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1800
|
+
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1801
|
+
});
|
|
1802
|
+
connIndicator.appendChild(connDot);
|
|
1803
|
+
wrapper.appendChild(connIndicator);
|
|
1804
|
+
// Error badge
|
|
1805
|
+
if (errorCount > 0) {
|
|
1806
|
+
wrapper.appendChild(this.createConsoleBadge('error', errorCount, BUTTON_COLORS.error));
|
|
1807
|
+
}
|
|
1808
|
+
// Warning badge
|
|
1809
|
+
if (warningCount > 0) {
|
|
1810
|
+
wrapper.appendChild(this.createConsoleBadge('warn', warningCount, BUTTON_COLORS.warning));
|
|
1811
|
+
}
|
|
1812
|
+
// Screenshot button (if enabled)
|
|
1813
|
+
if (this.options.showScreenshot) {
|
|
1814
|
+
wrapper.appendChild(this.createScreenshotButton(accentColor));
|
|
1815
|
+
}
|
|
1816
|
+
// Settings gear button
|
|
1817
|
+
wrapper.appendChild(this.createSettingsButton());
|
|
1818
|
+
// Expand button (double-arrow)
|
|
1819
|
+
const expandBtn = document.createElement('button');
|
|
1820
|
+
expandBtn.type = 'button';
|
|
1821
|
+
expandBtn.className = this.tooltipClass('right');
|
|
1822
|
+
expandBtn.setAttribute('data-tooltip', 'Expand DevBar');
|
|
1823
|
+
Object.assign(expandBtn.style, {
|
|
1824
|
+
display: 'flex',
|
|
1825
|
+
alignItems: 'center',
|
|
1826
|
+
justifyContent: 'center',
|
|
1827
|
+
width: '18px',
|
|
1828
|
+
height: '18px',
|
|
1829
|
+
borderRadius: '50%',
|
|
1830
|
+
border: `1px solid ${accentColor}60`,
|
|
1831
|
+
backgroundColor: 'transparent',
|
|
1832
|
+
color: `${accentColor}99`,
|
|
1833
|
+
cursor: 'pointer',
|
|
1834
|
+
fontSize: '0.5rem',
|
|
1835
|
+
transition: 'all 150ms',
|
|
1836
|
+
});
|
|
1837
|
+
expandBtn.textContent = '⟫';
|
|
1838
|
+
expandBtn.onmouseenter = () => {
|
|
1839
|
+
expandBtn.style.backgroundColor = `${accentColor}20`;
|
|
1840
|
+
expandBtn.style.borderColor = accentColor;
|
|
1841
|
+
expandBtn.style.color = accentColor;
|
|
1842
|
+
};
|
|
1843
|
+
expandBtn.onmouseleave = () => {
|
|
1844
|
+
expandBtn.style.backgroundColor = 'transparent';
|
|
1845
|
+
expandBtn.style.borderColor = `${accentColor}60`;
|
|
1846
|
+
expandBtn.style.color = `${accentColor}99`;
|
|
1847
|
+
};
|
|
1848
|
+
expandBtn.onclick = () => {
|
|
1849
|
+
this.toggleCompactMode();
|
|
1850
|
+
};
|
|
1851
|
+
wrapper.appendChild(expandBtn);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Create the settings gear button
|
|
1855
|
+
*/
|
|
1856
|
+
createSettingsButton() {
|
|
1857
|
+
const btn = document.createElement('button');
|
|
1858
|
+
btn.type = 'button';
|
|
1859
|
+
btn.className = this.tooltipClass('right');
|
|
1860
|
+
btn.setAttribute('data-tooltip', 'Settings (Cmd+Shift+M: toggle compact)');
|
|
1861
|
+
const isActive = this.showSettingsPopover;
|
|
1862
|
+
const color = COLORS.textSecondary;
|
|
1863
|
+
Object.assign(btn.style, {
|
|
1864
|
+
display: 'flex',
|
|
1865
|
+
alignItems: 'center',
|
|
1866
|
+
justifyContent: 'center',
|
|
1867
|
+
width: '22px',
|
|
1868
|
+
height: '22px',
|
|
1869
|
+
minWidth: '22px',
|
|
1870
|
+
minHeight: '22px',
|
|
1871
|
+
flexShrink: '0',
|
|
1872
|
+
borderRadius: '50%',
|
|
1873
|
+
border: `1px solid ${isActive ? color : `${color}60`}`,
|
|
1874
|
+
backgroundColor: isActive ? `${color}20` : 'transparent',
|
|
1875
|
+
color: isActive ? color : `${color}99`,
|
|
1876
|
+
cursor: 'pointer',
|
|
1877
|
+
transition: 'all 150ms',
|
|
1878
|
+
});
|
|
1879
|
+
btn.onclick = () => {
|
|
1880
|
+
this.showSettingsPopover = !this.showSettingsPopover;
|
|
1881
|
+
this.consoleFilter = null;
|
|
1882
|
+
this.showOutlineModal = false;
|
|
1883
|
+
this.showSchemaModal = false;
|
|
1884
|
+
this.showDesignReviewConfirm = false;
|
|
1885
|
+
this.render();
|
|
1886
|
+
};
|
|
1887
|
+
// Gear icon SVG
|
|
1888
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1889
|
+
svg.setAttribute('width', '12');
|
|
1890
|
+
svg.setAttribute('height', '12');
|
|
1891
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1892
|
+
svg.setAttribute('fill', 'none');
|
|
1893
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
1894
|
+
svg.setAttribute('stroke-width', '2');
|
|
1895
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
1896
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
1897
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1898
|
+
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');
|
|
1899
|
+
svg.appendChild(path);
|
|
1900
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
1901
|
+
circle.setAttribute('cx', '12');
|
|
1902
|
+
circle.setAttribute('cy', '12');
|
|
1903
|
+
circle.setAttribute('r', '3');
|
|
1904
|
+
svg.appendChild(circle);
|
|
1905
|
+
btn.appendChild(svg);
|
|
1906
|
+
return btn;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Create the compact mode toggle button with chevron icon
|
|
1910
|
+
*/
|
|
1911
|
+
createCompactToggleButton() {
|
|
1912
|
+
const btn = document.createElement('button');
|
|
1913
|
+
btn.type = 'button';
|
|
1914
|
+
btn.className = this.tooltipClass('right');
|
|
1915
|
+
const isCompact = this.compactMode;
|
|
1916
|
+
const tooltip = isCompact ? 'Expand (Cmd+Shift+M)' : 'Compact (Cmd+Shift+M)';
|
|
1917
|
+
btn.setAttribute('data-tooltip', tooltip);
|
|
1918
|
+
const color = COLORS.textSecondary;
|
|
1919
|
+
Object.assign(btn.style, {
|
|
1920
|
+
display: 'flex',
|
|
1921
|
+
alignItems: 'center',
|
|
1922
|
+
justifyContent: 'center',
|
|
1923
|
+
width: '22px',
|
|
1924
|
+
height: '22px',
|
|
1925
|
+
minWidth: '22px',
|
|
1926
|
+
minHeight: '22px',
|
|
1927
|
+
flexShrink: '0',
|
|
1928
|
+
borderRadius: '50%',
|
|
1929
|
+
border: `1px solid ${color}60`,
|
|
1930
|
+
backgroundColor: 'transparent',
|
|
1931
|
+
color: `${color}99`,
|
|
1932
|
+
cursor: 'pointer',
|
|
1933
|
+
transition: 'all 150ms',
|
|
1934
|
+
});
|
|
1935
|
+
btn.onmouseenter = () => {
|
|
1936
|
+
btn.style.borderColor = color;
|
|
1937
|
+
btn.style.backgroundColor = `${color}20`;
|
|
1938
|
+
btn.style.color = color;
|
|
1939
|
+
};
|
|
1940
|
+
btn.onmouseleave = () => {
|
|
1941
|
+
btn.style.borderColor = `${color}60`;
|
|
1942
|
+
btn.style.backgroundColor = 'transparent';
|
|
1943
|
+
btn.style.color = `${color}99`;
|
|
1944
|
+
};
|
|
1945
|
+
btn.onclick = () => {
|
|
1946
|
+
this.toggleCompactMode();
|
|
1947
|
+
};
|
|
1948
|
+
// Chevron icon SVG - points right when expanded, left when compact
|
|
1949
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1950
|
+
svg.setAttribute('width', '12');
|
|
1951
|
+
svg.setAttribute('height', '12');
|
|
1952
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1953
|
+
svg.setAttribute('fill', 'none');
|
|
1954
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
1955
|
+
svg.setAttribute('stroke-width', '2');
|
|
1956
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
1957
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
1958
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
1959
|
+
// Right chevron (>) when not compact, left chevron (<) when compact
|
|
1960
|
+
path.setAttribute('points', isCompact ? '15 18 9 12 15 6' : '9 18 15 12 9 6');
|
|
1961
|
+
svg.appendChild(path);
|
|
1962
|
+
btn.appendChild(svg);
|
|
1963
|
+
return btn;
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Create a settings section with title
|
|
1967
|
+
*/
|
|
1968
|
+
createSettingsSection(title, hasBorder = true) {
|
|
1969
|
+
const color = COLORS.textSecondary;
|
|
1970
|
+
const section = document.createElement('div');
|
|
1971
|
+
Object.assign(section.style, {
|
|
1972
|
+
padding: '10px 14px',
|
|
1973
|
+
borderBottom: hasBorder ? `1px solid ${color}20` : 'none',
|
|
1974
|
+
});
|
|
1975
|
+
const sectionTitle = document.createElement('div');
|
|
1976
|
+
Object.assign(sectionTitle.style, {
|
|
1977
|
+
color,
|
|
1978
|
+
fontSize: '0.625rem',
|
|
1979
|
+
textTransform: 'uppercase',
|
|
1980
|
+
letterSpacing: '0.1em',
|
|
1981
|
+
marginBottom: '8px',
|
|
1982
|
+
});
|
|
1983
|
+
sectionTitle.textContent = title;
|
|
1984
|
+
section.appendChild(sectionTitle);
|
|
1985
|
+
return section;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Create a toggle switch row
|
|
1989
|
+
*/
|
|
1990
|
+
createToggleRow(label, checked, accentColor, onChange) {
|
|
1991
|
+
const color = COLORS.textSecondary;
|
|
1992
|
+
const row = document.createElement('div');
|
|
1993
|
+
Object.assign(row.style, {
|
|
1994
|
+
display: 'flex',
|
|
1995
|
+
alignItems: 'center',
|
|
1996
|
+
justifyContent: 'space-between',
|
|
1997
|
+
marginBottom: '6px',
|
|
1998
|
+
});
|
|
1999
|
+
const labelEl = document.createElement('span');
|
|
2000
|
+
Object.assign(labelEl.style, { color: COLORS.text, fontSize: '0.6875rem' });
|
|
2001
|
+
labelEl.textContent = label;
|
|
2002
|
+
row.appendChild(labelEl);
|
|
2003
|
+
const toggle = document.createElement('button');
|
|
2004
|
+
Object.assign(toggle.style, {
|
|
2005
|
+
width: '32px',
|
|
2006
|
+
height: '18px',
|
|
2007
|
+
borderRadius: '9px',
|
|
2008
|
+
border: 'none',
|
|
2009
|
+
backgroundColor: checked ? accentColor : `${color}40`,
|
|
2010
|
+
position: 'relative',
|
|
2011
|
+
cursor: 'pointer',
|
|
2012
|
+
transition: 'all 150ms',
|
|
2013
|
+
flexShrink: '0',
|
|
2014
|
+
});
|
|
2015
|
+
const knob = document.createElement('span');
|
|
2016
|
+
Object.assign(knob.style, {
|
|
2017
|
+
position: 'absolute',
|
|
2018
|
+
top: '2px',
|
|
2019
|
+
left: checked ? '16px' : '2px',
|
|
2020
|
+
width: '14px',
|
|
2021
|
+
height: '14px',
|
|
2022
|
+
borderRadius: '50%',
|
|
2023
|
+
backgroundColor: '#fff',
|
|
2024
|
+
transition: 'left 150ms',
|
|
2025
|
+
});
|
|
2026
|
+
toggle.appendChild(knob);
|
|
2027
|
+
toggle.onclick = onChange;
|
|
2028
|
+
row.appendChild(toggle);
|
|
2029
|
+
return row;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Render the settings popover
|
|
2033
|
+
*/
|
|
2034
|
+
renderSettingsPopover() {
|
|
2035
|
+
const { position, accentColor } = this.options;
|
|
2036
|
+
const color = COLORS.textSecondary;
|
|
2037
|
+
const popover = document.createElement('div');
|
|
2038
|
+
popover.setAttribute('data-devbar', 'true');
|
|
2039
|
+
// Position based on devbar position
|
|
2040
|
+
const isTop = position.startsWith('top');
|
|
2041
|
+
const isRight = position.includes('right');
|
|
2042
|
+
Object.assign(popover.style, {
|
|
2043
|
+
position: 'fixed',
|
|
2044
|
+
[isTop ? 'top' : 'bottom']: isTop ? '70px' : '70px',
|
|
2045
|
+
[isRight ? 'right' : 'left']: isRight ? '16px' : '80px',
|
|
2046
|
+
zIndex: '10003',
|
|
2047
|
+
backgroundColor: 'rgba(17, 24, 39, 0.98)',
|
|
2048
|
+
border: `1px solid ${accentColor}`,
|
|
2049
|
+
borderRadius: '8px',
|
|
2050
|
+
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${accentColor}33`,
|
|
2051
|
+
backdropFilter: 'blur(8px)',
|
|
2052
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
2053
|
+
minWidth: '240px',
|
|
2054
|
+
maxWidth: '280px',
|
|
2055
|
+
maxHeight: 'calc(100vh - 100px)',
|
|
2056
|
+
overflowY: 'auto',
|
|
2057
|
+
fontFamily: FONT_MONO,
|
|
2058
|
+
});
|
|
2059
|
+
// Header
|
|
2060
|
+
const header = document.createElement('div');
|
|
2061
|
+
Object.assign(header.style, {
|
|
2062
|
+
display: 'flex',
|
|
2063
|
+
alignItems: 'center',
|
|
2064
|
+
justifyContent: 'space-between',
|
|
2065
|
+
padding: '10px 14px',
|
|
2066
|
+
borderBottom: `1px solid ${accentColor}30`,
|
|
2067
|
+
position: 'sticky',
|
|
2068
|
+
top: '0',
|
|
2069
|
+
backgroundColor: 'rgba(17, 24, 39, 0.98)',
|
|
2070
|
+
zIndex: '1',
|
|
2071
|
+
});
|
|
2072
|
+
const title = document.createElement('span');
|
|
2073
|
+
Object.assign(title.style, { color: accentColor, fontSize: '0.75rem', fontWeight: '600' });
|
|
2074
|
+
title.textContent = 'Settings';
|
|
2075
|
+
header.appendChild(title);
|
|
2076
|
+
const closeBtn = createStyledButton({
|
|
2077
|
+
color: COLORS.textMuted,
|
|
2078
|
+
text: '×',
|
|
2079
|
+
padding: '2px 6px',
|
|
2080
|
+
fontSize: '0.875rem',
|
|
2081
|
+
});
|
|
2082
|
+
closeBtn.style.border = 'none';
|
|
2083
|
+
closeBtn.onclick = () => {
|
|
2084
|
+
this.showSettingsPopover = false;
|
|
2085
|
+
this.render();
|
|
2086
|
+
};
|
|
2087
|
+
header.appendChild(closeBtn);
|
|
2088
|
+
popover.appendChild(header);
|
|
2089
|
+
// ========== THEME SECTION ==========
|
|
2090
|
+
const themeSection = this.createSettingsSection('Theme');
|
|
2091
|
+
const themeOptions = document.createElement('div');
|
|
2092
|
+
Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
|
|
2093
|
+
const themeModes = ['system', 'dark', 'light'];
|
|
2094
|
+
themeModes.forEach((mode) => {
|
|
2095
|
+
const btn = document.createElement('button');
|
|
2096
|
+
const isActive = this.themeMode === mode;
|
|
2097
|
+
Object.assign(btn.style, {
|
|
2098
|
+
padding: '4px 10px',
|
|
2099
|
+
backgroundColor: isActive ? `${accentColor}20` : 'transparent',
|
|
2100
|
+
border: `1px solid ${isActive ? accentColor : `${color}40`}`,
|
|
2101
|
+
borderRadius: '4px',
|
|
2102
|
+
color: isActive ? accentColor : color,
|
|
2103
|
+
fontSize: '0.625rem',
|
|
2104
|
+
cursor: 'pointer',
|
|
2105
|
+
textTransform: 'capitalize',
|
|
2106
|
+
transition: 'all 150ms',
|
|
2107
|
+
});
|
|
2108
|
+
btn.textContent = mode;
|
|
2109
|
+
btn.onclick = () => {
|
|
2110
|
+
this.setThemeMode(mode);
|
|
2111
|
+
};
|
|
2112
|
+
themeOptions.appendChild(btn);
|
|
2113
|
+
});
|
|
2114
|
+
themeSection.appendChild(themeOptions);
|
|
2115
|
+
popover.appendChild(themeSection);
|
|
2116
|
+
// ========== DISPLAY SECTION ==========
|
|
2117
|
+
const displaySection = this.createSettingsSection('Display');
|
|
2118
|
+
// Position mini-map selector
|
|
2119
|
+
const positionRow = document.createElement('div');
|
|
2120
|
+
Object.assign(positionRow.style, { marginBottom: '10px' });
|
|
2121
|
+
const posLabel = document.createElement('div');
|
|
2122
|
+
Object.assign(posLabel.style, {
|
|
2123
|
+
color: COLORS.text,
|
|
2124
|
+
fontSize: '0.6875rem',
|
|
2125
|
+
marginBottom: '6px',
|
|
2126
|
+
});
|
|
2127
|
+
posLabel.textContent = 'Position';
|
|
2128
|
+
positionRow.appendChild(posLabel);
|
|
2129
|
+
// Mini-map container
|
|
2130
|
+
const miniMap = document.createElement('div');
|
|
2131
|
+
Object.assign(miniMap.style, {
|
|
2132
|
+
position: 'relative',
|
|
2133
|
+
width: '100%',
|
|
2134
|
+
height: '50px',
|
|
2135
|
+
backgroundColor: 'rgba(10, 15, 26, 0.6)',
|
|
2136
|
+
border: `1px solid ${color}30`,
|
|
2137
|
+
borderRadius: '4px',
|
|
2138
|
+
});
|
|
2139
|
+
const positionConfigs = [
|
|
2140
|
+
{ value: 'top-left', style: { top: '8px', left: '10%' }, title: 'Top Left' },
|
|
2141
|
+
{ value: 'top-right', style: { top: '8px', right: '6%' }, title: 'Top Right' },
|
|
2142
|
+
{ value: 'bottom-left', style: { bottom: '8px', left: '10%' }, title: 'Bottom Left' },
|
|
2143
|
+
{ value: 'bottom-right', style: { bottom: '8px', right: '6%' }, title: 'Bottom Right' },
|
|
2144
|
+
{
|
|
2145
|
+
value: 'bottom-center',
|
|
2146
|
+
style: { bottom: '6px', left: '50%', transform: 'translateX(-50%)' },
|
|
2147
|
+
title: 'Bottom Center',
|
|
2148
|
+
},
|
|
2149
|
+
];
|
|
2150
|
+
positionConfigs.forEach(({ value, style, title }) => {
|
|
2151
|
+
const indicator = document.createElement('button');
|
|
2152
|
+
const isActive = this.options.position === value;
|
|
2153
|
+
Object.assign(indicator.style, {
|
|
2154
|
+
position: 'absolute',
|
|
2155
|
+
width: '20px',
|
|
2156
|
+
height: '6px',
|
|
2157
|
+
backgroundColor: isActive ? accentColor : `${color}60`,
|
|
2158
|
+
border: `1px solid ${isActive ? accentColor : `${color}40`}`,
|
|
2159
|
+
borderRadius: '2px',
|
|
2160
|
+
cursor: 'pointer',
|
|
2161
|
+
padding: '0',
|
|
2162
|
+
transition: 'all 150ms',
|
|
2163
|
+
boxShadow: isActive ? `0 0 8px ${accentColor}60` : 'none',
|
|
2164
|
+
...style,
|
|
2165
|
+
});
|
|
2166
|
+
indicator.title = title;
|
|
2167
|
+
indicator.onclick = () => {
|
|
2168
|
+
this.options.position = value;
|
|
2169
|
+
this.settingsManager.saveSettings({ position: value });
|
|
2170
|
+
this.render();
|
|
2171
|
+
};
|
|
2172
|
+
// Hover effect
|
|
2173
|
+
indicator.onmouseenter = () => {
|
|
2174
|
+
if (!isActive) {
|
|
2175
|
+
indicator.style.backgroundColor = accentColor;
|
|
2176
|
+
indicator.style.borderColor = accentColor;
|
|
2177
|
+
indicator.style.boxShadow = `0 0 6px ${accentColor}40`;
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
indicator.onmouseleave = () => {
|
|
2181
|
+
if (!isActive) {
|
|
2182
|
+
indicator.style.backgroundColor = `${color}60`;
|
|
2183
|
+
indicator.style.borderColor = `${color}40`;
|
|
2184
|
+
indicator.style.boxShadow = 'none';
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
miniMap.appendChild(indicator);
|
|
2188
|
+
});
|
|
2189
|
+
positionRow.appendChild(miniMap);
|
|
2190
|
+
displaySection.appendChild(positionRow);
|
|
2191
|
+
// Compact mode toggle
|
|
2192
|
+
displaySection.appendChild(this.createToggleRow('Compact Mode', this.compactMode, accentColor, () => {
|
|
2193
|
+
this.toggleCompactMode();
|
|
2194
|
+
}));
|
|
2195
|
+
// Keyboard shortcut hint
|
|
2196
|
+
const shortcutHint = document.createElement('div');
|
|
2197
|
+
Object.assign(shortcutHint.style, {
|
|
2198
|
+
color: COLORS.textMuted,
|
|
2199
|
+
fontSize: '0.5625rem',
|
|
2200
|
+
marginTop: '2px',
|
|
2201
|
+
marginBottom: '8px',
|
|
2202
|
+
});
|
|
2203
|
+
shortcutHint.textContent = 'Keyboard: Cmd+Shift+M';
|
|
2204
|
+
displaySection.appendChild(shortcutHint);
|
|
2205
|
+
// Accent color
|
|
2206
|
+
const accentRow = document.createElement('div');
|
|
2207
|
+
Object.assign(accentRow.style, { marginBottom: '6px' });
|
|
2208
|
+
const accentLabel = document.createElement('div');
|
|
2209
|
+
Object.assign(accentLabel.style, {
|
|
2210
|
+
color: COLORS.text,
|
|
2211
|
+
fontSize: '0.6875rem',
|
|
2212
|
+
marginBottom: '6px',
|
|
2213
|
+
});
|
|
2214
|
+
accentLabel.textContent = 'Accent Color';
|
|
2215
|
+
accentRow.appendChild(accentLabel);
|
|
2216
|
+
const colorSwatches = document.createElement('div');
|
|
2217
|
+
Object.assign(colorSwatches.style, {
|
|
2218
|
+
display: 'flex',
|
|
2219
|
+
gap: '6px',
|
|
2220
|
+
flexWrap: 'wrap',
|
|
2221
|
+
});
|
|
2222
|
+
ACCENT_COLOR_PRESETS.forEach(({ name, value }) => {
|
|
2223
|
+
const swatch = document.createElement('button');
|
|
2224
|
+
const isActive = this.options.accentColor === value;
|
|
2225
|
+
Object.assign(swatch.style, {
|
|
2226
|
+
width: '24px',
|
|
2227
|
+
height: '24px',
|
|
2228
|
+
borderRadius: '50%',
|
|
2229
|
+
backgroundColor: value,
|
|
2230
|
+
border: isActive ? '2px solid #fff' : '2px solid transparent',
|
|
2231
|
+
cursor: 'pointer',
|
|
2232
|
+
transition: 'all 150ms',
|
|
2233
|
+
boxShadow: isActive ? `0 0 8px ${value}` : 'none',
|
|
2234
|
+
});
|
|
2235
|
+
swatch.title = name;
|
|
2236
|
+
swatch.onclick = () => {
|
|
2237
|
+
this.options.accentColor = value;
|
|
2238
|
+
this.settingsManager.saveSettings({ accentColor: value });
|
|
2239
|
+
this.render();
|
|
2240
|
+
};
|
|
2241
|
+
colorSwatches.appendChild(swatch);
|
|
2242
|
+
});
|
|
2243
|
+
accentRow.appendChild(colorSwatches);
|
|
2244
|
+
displaySection.appendChild(accentRow);
|
|
2245
|
+
popover.appendChild(displaySection);
|
|
2246
|
+
// ========== FEATURES SECTION ==========
|
|
2247
|
+
const featuresSection = this.createSettingsSection('Features');
|
|
2248
|
+
featuresSection.appendChild(this.createToggleRow('Screenshot Button', this.options.showScreenshot, accentColor, () => {
|
|
2249
|
+
this.options.showScreenshot = !this.options.showScreenshot;
|
|
2250
|
+
this.settingsManager.saveSettings({ showScreenshot: this.options.showScreenshot });
|
|
2251
|
+
this.render();
|
|
2252
|
+
}));
|
|
2253
|
+
featuresSection.appendChild(this.createToggleRow('Console Badges', this.options.showConsoleBadges, accentColor, () => {
|
|
2254
|
+
this.options.showConsoleBadges = !this.options.showConsoleBadges;
|
|
2255
|
+
this.settingsManager.saveSettings({ showConsoleBadges: this.options.showConsoleBadges });
|
|
2256
|
+
this.render();
|
|
2257
|
+
}));
|
|
2258
|
+
featuresSection.appendChild(this.createToggleRow('Tooltips', this.options.showTooltips, accentColor, () => {
|
|
2259
|
+
this.options.showTooltips = !this.options.showTooltips;
|
|
2260
|
+
this.settingsManager.saveSettings({ showTooltips: this.options.showTooltips });
|
|
2261
|
+
this.render();
|
|
2262
|
+
}));
|
|
2263
|
+
popover.appendChild(featuresSection);
|
|
2264
|
+
// ========== METRICS SECTION ==========
|
|
2265
|
+
const metricsSection = this.createSettingsSection('Metrics');
|
|
2266
|
+
const metricsToggles = [
|
|
2267
|
+
{ key: 'breakpoint', label: 'Breakpoint' },
|
|
2268
|
+
{ key: 'fcp', label: 'FCP' },
|
|
2269
|
+
{ key: 'lcp', label: 'LCP' },
|
|
2270
|
+
{ key: 'cls', label: 'CLS' },
|
|
2271
|
+
{ key: 'inp', label: 'INP' },
|
|
2272
|
+
{ key: 'pageSize', label: 'Page Size' },
|
|
2273
|
+
];
|
|
2274
|
+
metricsToggles.forEach(({ key, label }) => {
|
|
2275
|
+
const currentValue = this.options.showMetrics[key] ?? true;
|
|
2276
|
+
metricsSection.appendChild(this.createToggleRow(label, currentValue, accentColor, () => {
|
|
2277
|
+
this.options.showMetrics[key] = !this.options.showMetrics[key];
|
|
2278
|
+
this.settingsManager.saveSettings({
|
|
2279
|
+
showMetrics: {
|
|
2280
|
+
breakpoint: this.options.showMetrics.breakpoint ?? true,
|
|
2281
|
+
fcp: this.options.showMetrics.fcp ?? true,
|
|
2282
|
+
lcp: this.options.showMetrics.lcp ?? true,
|
|
2283
|
+
cls: this.options.showMetrics.cls ?? true,
|
|
2284
|
+
inp: this.options.showMetrics.inp ?? true,
|
|
2285
|
+
pageSize: this.options.showMetrics.pageSize ?? true,
|
|
2286
|
+
},
|
|
2287
|
+
});
|
|
2288
|
+
this.render();
|
|
2289
|
+
}));
|
|
2290
|
+
});
|
|
2291
|
+
popover.appendChild(metricsSection);
|
|
2292
|
+
// ========== RESET SECTION ==========
|
|
2293
|
+
const resetSection = document.createElement('div');
|
|
2294
|
+
Object.assign(resetSection.style, {
|
|
2295
|
+
padding: '10px 14px',
|
|
2296
|
+
borderTop: `1px solid ${color}20`,
|
|
2297
|
+
});
|
|
2298
|
+
const resetBtn = createStyledButton({
|
|
2299
|
+
color: COLORS.textMuted,
|
|
2300
|
+
text: 'Reset to Defaults',
|
|
2301
|
+
padding: '6px 12px',
|
|
2302
|
+
fontSize: '0.625rem',
|
|
2303
|
+
});
|
|
2304
|
+
Object.assign(resetBtn.style, {
|
|
2305
|
+
width: '100%',
|
|
2306
|
+
justifyContent: 'center',
|
|
2307
|
+
});
|
|
2308
|
+
resetBtn.onclick = () => {
|
|
2309
|
+
this.resetToDefaults();
|
|
2310
|
+
};
|
|
2311
|
+
resetSection.appendChild(resetBtn);
|
|
2312
|
+
popover.appendChild(resetSection);
|
|
2313
|
+
this.overlayElement = popover;
|
|
2314
|
+
document.body.appendChild(popover);
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Reset all settings to defaults
|
|
2318
|
+
*/
|
|
2319
|
+
resetToDefaults() {
|
|
2320
|
+
this.settingsManager.resetToDefaults();
|
|
2321
|
+
const defaults = DEFAULT_SETTINGS;
|
|
2322
|
+
this.applySettings(defaults);
|
|
2323
|
+
}
|
|
2324
|
+
renderCollapsed() {
|
|
2325
|
+
if (!this.container)
|
|
2326
|
+
return;
|
|
2327
|
+
const { position, accentColor } = this.options;
|
|
2328
|
+
const { errorCount, warningCount } = this.getLogCounts();
|
|
2329
|
+
// Use captured dot position if available, otherwise fall back to preset positions
|
|
2330
|
+
// The 13px offset accounts for half the collapsed circle diameter (26px / 2)
|
|
2331
|
+
let posStyle;
|
|
2332
|
+
if (this.lastDotPosition) {
|
|
2333
|
+
// Position based on where the dot actually was
|
|
2334
|
+
const isTop = position.startsWith('top');
|
|
2335
|
+
posStyle = isTop
|
|
2336
|
+
? { top: `${this.lastDotPosition.top - 13}px`, left: `${this.lastDotPosition.left - 13}px` }
|
|
2337
|
+
: {
|
|
2338
|
+
bottom: `${this.lastDotPosition.bottom - 13}px`,
|
|
2339
|
+
left: `${this.lastDotPosition.left - 13}px`,
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
else {
|
|
2343
|
+
// Fallback preset positions for when no dot position was captured
|
|
2344
|
+
const collapsedPositions = {
|
|
2345
|
+
'bottom-left': { bottom: '27px', left: '86px' },
|
|
2346
|
+
'bottom-right': { bottom: '27px', right: '29px' },
|
|
2347
|
+
'top-left': { top: '27px', left: '86px' },
|
|
2348
|
+
'top-right': { top: '27px', right: '29px' },
|
|
2349
|
+
'bottom-center': { bottom: '19px', left: '50%', transform: 'translateX(-50%)' },
|
|
2350
|
+
};
|
|
2351
|
+
posStyle = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
|
|
2352
|
+
}
|
|
1432
2353
|
const wrapper = this.container;
|
|
1433
2354
|
wrapper.className = this.tooltipClass('left', 'devbar-collapse');
|
|
1434
2355
|
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 +2377,11 @@ export class GlobalDevBar {
|
|
|
1456
2377
|
width: '26px',
|
|
1457
2378
|
height: '26px',
|
|
1458
2379
|
boxSizing: 'border-box',
|
|
1459
|
-
animation: 'devbar-collapse 150ms ease-out'
|
|
2380
|
+
animation: 'devbar-collapse 150ms ease-out',
|
|
1460
2381
|
});
|
|
1461
2382
|
wrapper.onclick = () => {
|
|
1462
2383
|
this.collapsed = false;
|
|
2384
|
+
this.debug.state('Expanded DevBar');
|
|
1463
2385
|
this.render();
|
|
1464
2386
|
};
|
|
1465
2387
|
// Connection indicator dot (same size as in expanded state)
|
|
@@ -1469,7 +2391,7 @@ export class GlobalDevBar {
|
|
|
1469
2391
|
height: '6px',
|
|
1470
2392
|
borderRadius: '50%',
|
|
1471
2393
|
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1472
|
-
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none'
|
|
2394
|
+
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1473
2395
|
});
|
|
1474
2396
|
wrapper.appendChild(dot);
|
|
1475
2397
|
// Error badge (absolute, top-right of circle, shifted left if warning badge exists)
|
|
@@ -1524,10 +2446,21 @@ export class GlobalDevBar {
|
|
|
1524
2446
|
width: sizeOverrides?.width ?? defaultWidth,
|
|
1525
2447
|
maxWidth: sizeOverrides?.maxWidth ?? defaultMaxWidth,
|
|
1526
2448
|
minWidth: sizeOverrides?.minWidth ?? defaultMinWidth,
|
|
1527
|
-
cursor: 'default'
|
|
2449
|
+
cursor: 'default',
|
|
1528
2450
|
});
|
|
1529
2451
|
wrapper.ondblclick = () => {
|
|
2452
|
+
// Capture dot position before collapsing
|
|
2453
|
+
const dotEl = wrapper.querySelector('.devbar-status span span');
|
|
2454
|
+
if (dotEl) {
|
|
2455
|
+
const rect = dotEl.getBoundingClientRect();
|
|
2456
|
+
this.lastDotPosition = {
|
|
2457
|
+
left: rect.left + rect.width / 2,
|
|
2458
|
+
top: rect.top + rect.height / 2,
|
|
2459
|
+
bottom: window.innerHeight - (rect.top + rect.height / 2),
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
1530
2462
|
this.collapsed = true;
|
|
2463
|
+
this.debug.state('Collapsed DevBar (double-click)');
|
|
1531
2464
|
this.render();
|
|
1532
2465
|
};
|
|
1533
2466
|
// Main row - wrapping controlled by CSS media query
|
|
@@ -1544,12 +2477,14 @@ export class GlobalDevBar {
|
|
|
1544
2477
|
boxSizing: 'border-box',
|
|
1545
2478
|
fontFamily: FONT_MONO,
|
|
1546
2479
|
fontSize: '0.6875rem',
|
|
1547
|
-
lineHeight: '1rem'
|
|
2480
|
+
lineHeight: '1rem',
|
|
1548
2481
|
});
|
|
1549
2482
|
// Connection indicator (click to collapse)
|
|
1550
2483
|
const connIndicator = document.createElement('span');
|
|
1551
2484
|
connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
|
|
1552
|
-
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected
|
|
2485
|
+
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected
|
|
2486
|
+
? 'Sweetlink connected (click to minimize)'
|
|
2487
|
+
: 'Sweetlink disconnected (click to minimize)');
|
|
1553
2488
|
Object.assign(connIndicator.style, {
|
|
1554
2489
|
width: '12px',
|
|
1555
2490
|
height: '12px',
|
|
@@ -1559,11 +2494,19 @@ export class GlobalDevBar {
|
|
|
1559
2494
|
alignItems: 'center',
|
|
1560
2495
|
justifyContent: 'center',
|
|
1561
2496
|
cursor: 'pointer',
|
|
1562
|
-
flexShrink: '0'
|
|
2497
|
+
flexShrink: '0',
|
|
1563
2498
|
});
|
|
1564
2499
|
connIndicator.onclick = (e) => {
|
|
1565
2500
|
e.stopPropagation();
|
|
2501
|
+
// Capture dot position before collapsing (connDot is the inner 6px dot)
|
|
2502
|
+
const rect = connIndicator.getBoundingClientRect();
|
|
2503
|
+
this.lastDotPosition = {
|
|
2504
|
+
left: rect.left + rect.width / 2,
|
|
2505
|
+
top: rect.top + rect.height / 2,
|
|
2506
|
+
bottom: window.innerHeight - (rect.top + rect.height / 2),
|
|
2507
|
+
};
|
|
1566
2508
|
this.collapsed = true;
|
|
2509
|
+
this.debug.state('Collapsed DevBar (connection dot click)');
|
|
1567
2510
|
this.render();
|
|
1568
2511
|
};
|
|
1569
2512
|
const connDot = document.createElement('span');
|
|
@@ -1573,7 +2516,7 @@ export class GlobalDevBar {
|
|
|
1573
2516
|
borderRadius: '50%',
|
|
1574
2517
|
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1575
2518
|
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1576
|
-
transition: 'all 300ms'
|
|
2519
|
+
transition: 'all 300ms',
|
|
1577
2520
|
});
|
|
1578
2521
|
connIndicator.appendChild(connDot);
|
|
1579
2522
|
// Status row wrapper - keeps connection dot, info, and badges together
|
|
@@ -1584,7 +2527,7 @@ export class GlobalDevBar {
|
|
|
1584
2527
|
alignItems: 'center',
|
|
1585
2528
|
gap: '0.5rem',
|
|
1586
2529
|
flexWrap: 'nowrap',
|
|
1587
|
-
flexShrink: '0'
|
|
2530
|
+
flexShrink: '0',
|
|
1588
2531
|
});
|
|
1589
2532
|
statusRow.appendChild(connIndicator);
|
|
1590
2533
|
// Info section
|
|
@@ -1598,7 +2541,7 @@ export class GlobalDevBar {
|
|
|
1598
2541
|
letterSpacing: '0.05em',
|
|
1599
2542
|
flexShrink: '1',
|
|
1600
2543
|
minWidth: '0',
|
|
1601
|
-
overflow: 'visible'
|
|
2544
|
+
overflow: 'visible',
|
|
1602
2545
|
});
|
|
1603
2546
|
// Breakpoint info
|
|
1604
2547
|
if (showMetrics.breakpoint && this.breakpointInfo) {
|
|
@@ -1610,9 +2553,10 @@ export class GlobalDevBar {
|
|
|
1610
2553
|
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
2554
|
let bpText = bp;
|
|
1612
2555
|
if (bp !== 'base') {
|
|
1613
|
-
bpText =
|
|
1614
|
-
|
|
1615
|
-
|
|
2556
|
+
bpText =
|
|
2557
|
+
bp === 'sm'
|
|
2558
|
+
? `${bp} - ${this.breakpointInfo.dimensions.split('x')[0]}`
|
|
2559
|
+
: `${bp} - ${this.breakpointInfo.dimensions}`;
|
|
1616
2560
|
}
|
|
1617
2561
|
bpSpan.textContent = bpText;
|
|
1618
2562
|
infoSection.appendChild(bpSpan);
|
|
@@ -1643,6 +2587,24 @@ export class GlobalDevBar {
|
|
|
1643
2587
|
lcpSpan.textContent = `LCP ${this.perfStats.lcp}`;
|
|
1644
2588
|
infoSection.appendChild(lcpSpan);
|
|
1645
2589
|
}
|
|
2590
|
+
if (showMetrics.cls) {
|
|
2591
|
+
addSeparator();
|
|
2592
|
+
const clsSpan = document.createElement('span');
|
|
2593
|
+
clsSpan.className = this.tooltipClass('left', 'devbar-item');
|
|
2594
|
+
Object.assign(clsSpan.style, { opacity: '0.85', cursor: 'default' });
|
|
2595
|
+
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');
|
|
2596
|
+
clsSpan.textContent = `CLS ${this.perfStats.cls}`;
|
|
2597
|
+
infoSection.appendChild(clsSpan);
|
|
2598
|
+
}
|
|
2599
|
+
if (showMetrics.inp) {
|
|
2600
|
+
addSeparator();
|
|
2601
|
+
const inpSpan = document.createElement('span');
|
|
2602
|
+
inpSpan.className = this.tooltipClass('left', 'devbar-item');
|
|
2603
|
+
Object.assign(inpSpan.style, { opacity: '0.85', cursor: 'default' });
|
|
2604
|
+
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');
|
|
2605
|
+
inpSpan.textContent = `INP ${this.perfStats.inp}`;
|
|
2606
|
+
infoSection.appendChild(inpSpan);
|
|
2607
|
+
}
|
|
1646
2608
|
if (showMetrics.pageSize) {
|
|
1647
2609
|
addSeparator();
|
|
1648
2610
|
const sizeSpan = document.createElement('span');
|
|
@@ -1673,6 +2635,8 @@ export class GlobalDevBar {
|
|
|
1673
2635
|
actionsContainer.appendChild(this.createAIReviewButton());
|
|
1674
2636
|
actionsContainer.appendChild(this.createOutlineButton());
|
|
1675
2637
|
actionsContainer.appendChild(this.createSchemaButton());
|
|
2638
|
+
actionsContainer.appendChild(this.createSettingsButton());
|
|
2639
|
+
actionsContainer.appendChild(this.createCompactToggleButton());
|
|
1676
2640
|
mainRow.appendChild(actionsContainer);
|
|
1677
2641
|
wrapper.appendChild(mainRow);
|
|
1678
2642
|
// Render custom controls row if there are any
|
|
@@ -1690,7 +2654,7 @@ export class GlobalDevBar {
|
|
|
1690
2654
|
fontFamily: FONT_MONO,
|
|
1691
2655
|
fontSize: '0.6875rem',
|
|
1692
2656
|
});
|
|
1693
|
-
GlobalDevBar.customControls.forEach(control => {
|
|
2657
|
+
GlobalDevBar.customControls.forEach((control) => {
|
|
1694
2658
|
const btn = document.createElement('button');
|
|
1695
2659
|
btn.type = 'button';
|
|
1696
2660
|
const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
|
|
@@ -1765,13 +2729,7 @@ export class GlobalDevBar {
|
|
|
1765
2729
|
btn.type = 'button';
|
|
1766
2730
|
btn.className = this.tooltipClass('right');
|
|
1767
2731
|
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' : ''}`;
|
|
2732
|
+
const tooltip = this.getScreenshotTooltip();
|
|
1775
2733
|
btn.setAttribute('data-tooltip', tooltip);
|
|
1776
2734
|
Object.assign(btn.style, {
|
|
1777
2735
|
display: 'flex',
|
|
@@ -1789,7 +2747,7 @@ export class GlobalDevBar {
|
|
|
1789
2747
|
color: hasSuccessState ? accentColor : `${accentColor}99`,
|
|
1790
2748
|
cursor: !this.capturing ? 'pointer' : 'not-allowed',
|
|
1791
2749
|
opacity: '1',
|
|
1792
|
-
transition: 'all 150ms'
|
|
2750
|
+
transition: 'all 150ms',
|
|
1793
2751
|
});
|
|
1794
2752
|
btn.disabled = this.capturing;
|
|
1795
2753
|
btn.onclick = (e) => {
|
|
@@ -1835,17 +2793,47 @@ export class GlobalDevBar {
|
|
|
1835
2793
|
}
|
|
1836
2794
|
return btn;
|
|
1837
2795
|
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Get the tooltip text for the screenshot button based on current state
|
|
2798
|
+
*/
|
|
2799
|
+
getScreenshotTooltip() {
|
|
2800
|
+
if (this.copiedToClipboard) {
|
|
2801
|
+
return 'Copied to clipboard!';
|
|
2802
|
+
}
|
|
2803
|
+
if (this.copiedPath) {
|
|
2804
|
+
return 'Path copied to clipboard!';
|
|
2805
|
+
}
|
|
2806
|
+
if (this.lastScreenshot) {
|
|
2807
|
+
return `Screenshot saved!\n${this.lastScreenshot}\n\nClick to copy path`;
|
|
2808
|
+
}
|
|
2809
|
+
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`;
|
|
2810
|
+
return this.sweetlinkConnected
|
|
2811
|
+
? baseTooltip
|
|
2812
|
+
: `${baseTooltip}\n\nWarning: Sweetlink not connected`;
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Get the tooltip text for the AI review button based on current state
|
|
2816
|
+
*/
|
|
2817
|
+
getAIReviewTooltip() {
|
|
2818
|
+
if (this.designReviewInProgress) {
|
|
2819
|
+
return 'AI Design Review in progress...';
|
|
2820
|
+
}
|
|
2821
|
+
if (this.designReviewError) {
|
|
2822
|
+
return `Design review failed:\n${this.designReviewError}`;
|
|
2823
|
+
}
|
|
2824
|
+
if (this.lastDesignReview) {
|
|
2825
|
+
return `Design review saved to:\n${this.lastDesignReview}`;
|
|
2826
|
+
}
|
|
2827
|
+
const baseTooltip = `AI Design Review\n\nCaptures screenshot and sends to\nClaude for design analysis.\n\nRequires ANTHROPIC_API_KEY.`;
|
|
2828
|
+
return this.sweetlinkConnected
|
|
2829
|
+
? baseTooltip
|
|
2830
|
+
: `${baseTooltip}\n\nWarning: Sweetlink not connected`;
|
|
2831
|
+
}
|
|
1838
2832
|
createAIReviewButton() {
|
|
1839
2833
|
const btn = document.createElement('button');
|
|
1840
2834
|
btn.type = 'button';
|
|
1841
2835
|
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' : ''}`;
|
|
2836
|
+
const tooltip = this.getAIReviewTooltip();
|
|
1849
2837
|
btn.setAttribute('data-tooltip', tooltip);
|
|
1850
2838
|
const hasError = !!this.designReviewError;
|
|
1851
2839
|
const isActive = this.designReviewInProgress || !!this.lastDesignReview || hasError;
|
|
@@ -1945,7 +2933,7 @@ export function initGlobalDevBar(options) {
|
|
|
1945
2933
|
const existing = getGlobalInstance();
|
|
1946
2934
|
if (existing) {
|
|
1947
2935
|
// Check if already initialized with same position - skip re-init during HMR
|
|
1948
|
-
const existingPosition = existing
|
|
2936
|
+
const existingPosition = existing.getPosition();
|
|
1949
2937
|
const newPosition = options?.position ?? 'bottom-left';
|
|
1950
2938
|
if (existingPosition === newPosition) {
|
|
1951
2939
|
return existing;
|