@ytspar/devbar 1.0.0-canary.92db425 → 1.0.0-canary.bf42899
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 +62 -2
- package/dist/GlobalDevBar.js +778 -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 +104 -6
- 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/presets.d.ts +57 -0
- package/dist/presets.js +133 -0
- package/dist/storage.d.ts +83 -0
- package/dist/storage.js +182 -0
- package/dist/types.d.ts +26 -1
- package/package.json +1 -1
package/dist/GlobalDevBar.js
CHANGED
|
@@ -8,19 +8,21 @@
|
|
|
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, getStoredThemeMode, getThemeColors, MAX_CONSOLE_LOGS, MAX_RECONNECT_ATTEMPTS, MAX_RECONNECT_DELAY_MS, SCREENSHOT_BLUR_DELAY_MS, SCREENSHOT_NOTIFICATION_MS, SCREENSHOT_SCALE, setStoredThemeMode, STORAGE_KEYS, 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 { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createStyledButton, createSvgIcon, getButtonStyles, } from './ui/index.js';
|
|
16
|
+
import { canvasToDataUrl, copyCanvasToClipboard, delay, formatArgs, prepareForCapture, } from './utils.js';
|
|
17
|
+
const html2canvas = (html2canvasModule.default ??
|
|
18
|
+
html2canvasModule);
|
|
17
19
|
const earlyConsoleCapture = (() => {
|
|
18
20
|
const ssrFallback = {
|
|
19
21
|
errorCount: 0,
|
|
20
22
|
warningCount: 0,
|
|
21
23
|
logs: [],
|
|
22
24
|
originalConsole: null,
|
|
23
|
-
isPatched: false
|
|
25
|
+
isPatched: false,
|
|
24
26
|
};
|
|
25
27
|
// Skip on server-side rendering
|
|
26
28
|
if (typeof window === 'undefined')
|
|
@@ -33,9 +35,9 @@ const earlyConsoleCapture = (() => {
|
|
|
33
35
|
log: console.log,
|
|
34
36
|
error: console.error,
|
|
35
37
|
warn: console.warn,
|
|
36
|
-
info: console.info
|
|
38
|
+
info: console.info,
|
|
37
39
|
},
|
|
38
|
-
isPatched: false
|
|
40
|
+
isPatched: false,
|
|
39
41
|
};
|
|
40
42
|
const captureLog = (level, args) => {
|
|
41
43
|
capture.logs.push({ level, message: formatArgs(args), timestamp: Date.now() });
|
|
@@ -44,10 +46,24 @@ const earlyConsoleCapture = (() => {
|
|
|
44
46
|
};
|
|
45
47
|
// Patch console immediately
|
|
46
48
|
if (!capture.isPatched && capture.originalConsole) {
|
|
47
|
-
console.log = (...args) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
console.log = (...args) => {
|
|
50
|
+
captureLog('log', args);
|
|
51
|
+
capture.originalConsole.log(...args);
|
|
52
|
+
};
|
|
53
|
+
console.error = (...args) => {
|
|
54
|
+
captureLog('error', args);
|
|
55
|
+
capture.errorCount++;
|
|
56
|
+
capture.originalConsole.error(...args);
|
|
57
|
+
};
|
|
58
|
+
console.warn = (...args) => {
|
|
59
|
+
captureLog('warn', args);
|
|
60
|
+
capture.warningCount++;
|
|
61
|
+
capture.originalConsole.warn(...args);
|
|
62
|
+
};
|
|
63
|
+
console.info = (...args) => {
|
|
64
|
+
captureLog('info', args);
|
|
65
|
+
capture.originalConsole.info(...args);
|
|
66
|
+
};
|
|
51
67
|
capture.isPatched = true;
|
|
52
68
|
}
|
|
53
69
|
return capture;
|
|
@@ -80,7 +96,11 @@ export class GlobalDevBar {
|
|
|
80
96
|
this.breakpointInfo = null;
|
|
81
97
|
this.perfStats = null;
|
|
82
98
|
this.lcpValue = null;
|
|
99
|
+
this.clsValue = 0;
|
|
100
|
+
this.inpValue = 0;
|
|
83
101
|
this.reconnectAttempts = 0;
|
|
102
|
+
// Track the position of the connection indicator dot for smooth collapse
|
|
103
|
+
this.lastDotPosition = null;
|
|
84
104
|
this.reconnectTimeout = null;
|
|
85
105
|
this.screenshotTimeout = null;
|
|
86
106
|
this.copiedPathTimeout = null;
|
|
@@ -92,9 +112,22 @@ export class GlobalDevBar {
|
|
|
92
112
|
this.keydownHandler = null;
|
|
93
113
|
this.fcpObserver = null;
|
|
94
114
|
this.lcpObserver = null;
|
|
115
|
+
this.clsObserver = null;
|
|
116
|
+
this.inpObserver = null;
|
|
95
117
|
this.destroyed = false;
|
|
118
|
+
// Theme state
|
|
119
|
+
this.themeMode = 'system';
|
|
120
|
+
this.themeMediaQuery = null;
|
|
121
|
+
this.themeMediaHandler = null;
|
|
122
|
+
// Compact mode state
|
|
123
|
+
this.compactMode = false;
|
|
124
|
+
// Settings popover state
|
|
125
|
+
this.showSettingsPopover = false;
|
|
96
126
|
// Overlay element for modals
|
|
97
127
|
this.overlayElement = null;
|
|
128
|
+
// Initialize debug config first so we can log during construction
|
|
129
|
+
this.debugConfig = normalizeDebugConfig(options.debug);
|
|
130
|
+
this.debug = new DebugLogger(this.debugConfig);
|
|
98
131
|
this.options = {
|
|
99
132
|
position: options.position ?? 'bottom-left',
|
|
100
133
|
accentColor: options.accentColor ?? COLORS.primary,
|
|
@@ -102,6 +135,8 @@ export class GlobalDevBar {
|
|
|
102
135
|
breakpoint: options.showMetrics?.breakpoint ?? true,
|
|
103
136
|
fcp: options.showMetrics?.fcp ?? true,
|
|
104
137
|
lcp: options.showMetrics?.lcp ?? true,
|
|
138
|
+
cls: options.showMetrics?.cls ?? true,
|
|
139
|
+
inp: options.showMetrics?.inp ?? true,
|
|
105
140
|
pageSize: options.showMetrics?.pageSize ?? true,
|
|
106
141
|
},
|
|
107
142
|
showScreenshot: options.showScreenshot ?? true,
|
|
@@ -109,6 +144,7 @@ export class GlobalDevBar {
|
|
|
109
144
|
showTooltips: options.showTooltips ?? true,
|
|
110
145
|
sizeOverrides: options.sizeOverrides,
|
|
111
146
|
};
|
|
147
|
+
this.debug.lifecycle('GlobalDevBar constructed', { options: this.options });
|
|
112
148
|
}
|
|
113
149
|
/**
|
|
114
150
|
* Get tooltip class name(s) if tooltips are enabled, otherwise empty string
|
|
@@ -153,7 +189,7 @@ export class GlobalDevBar {
|
|
|
153
189
|
fontWeight: '600',
|
|
154
190
|
display: 'flex',
|
|
155
191
|
alignItems: 'center',
|
|
156
|
-
justifyContent: 'center'
|
|
192
|
+
justifyContent: 'center',
|
|
157
193
|
});
|
|
158
194
|
badge.textContent = count > 99 ? '!' : String(count);
|
|
159
195
|
return badge;
|
|
@@ -166,7 +202,7 @@ export class GlobalDevBar {
|
|
|
166
202
|
*/
|
|
167
203
|
static registerControl(control) {
|
|
168
204
|
// Remove existing control with same ID
|
|
169
|
-
GlobalDevBar.customControls = GlobalDevBar.customControls.filter(c => c.id !== control.id);
|
|
205
|
+
GlobalDevBar.customControls = GlobalDevBar.customControls.filter((c) => c.id !== control.id);
|
|
170
206
|
GlobalDevBar.customControls.push(control);
|
|
171
207
|
// Trigger re-render of all instances
|
|
172
208
|
const instance = getGlobalInstance();
|
|
@@ -178,7 +214,7 @@ export class GlobalDevBar {
|
|
|
178
214
|
* Unregister a custom control by ID
|
|
179
215
|
*/
|
|
180
216
|
static unregisterControl(id) {
|
|
181
|
-
GlobalDevBar.customControls = GlobalDevBar.customControls.filter(c => c.id !== id);
|
|
217
|
+
GlobalDevBar.customControls = GlobalDevBar.customControls.filter((c) => c.id !== id);
|
|
182
218
|
// Trigger re-render of all instances
|
|
183
219
|
const instance = getGlobalInstance();
|
|
184
220
|
if (instance) {
|
|
@@ -209,10 +245,16 @@ export class GlobalDevBar {
|
|
|
209
245
|
return;
|
|
210
246
|
if (this.destroyed)
|
|
211
247
|
return;
|
|
248
|
+
this.debug.lifecycle('Initializing DevBar');
|
|
212
249
|
// Inject tooltip styles
|
|
213
250
|
this.injectStyles();
|
|
214
251
|
// Copy early captured logs
|
|
215
252
|
this.consoleLogs = [...earlyConsoleCapture.logs];
|
|
253
|
+
this.debug.lifecycle('Copied early console logs', { count: this.consoleLogs.length });
|
|
254
|
+
// Setup theme
|
|
255
|
+
this.setupTheme();
|
|
256
|
+
// Load compact mode from storage
|
|
257
|
+
this.loadCompactMode();
|
|
216
258
|
// Setup WebSocket connection
|
|
217
259
|
this.connectWebSocket();
|
|
218
260
|
// Setup breakpoint detection
|
|
@@ -223,11 +265,19 @@ export class GlobalDevBar {
|
|
|
223
265
|
this.setupKeyboardShortcuts();
|
|
224
266
|
// Initial render
|
|
225
267
|
this.render();
|
|
268
|
+
this.debug.lifecycle('DevBar initialized successfully');
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the current position
|
|
272
|
+
*/
|
|
273
|
+
getPosition() {
|
|
274
|
+
return this.options.position;
|
|
226
275
|
}
|
|
227
276
|
/**
|
|
228
277
|
* Destroy the devbar and cleanup
|
|
229
278
|
*/
|
|
230
279
|
destroy() {
|
|
280
|
+
this.debug.lifecycle('Destroying DevBar');
|
|
231
281
|
this.destroyed = true;
|
|
232
282
|
// Close WebSocket
|
|
233
283
|
this.reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // Prevent reconnection
|
|
@@ -256,6 +306,14 @@ export class GlobalDevBar {
|
|
|
256
306
|
this.fcpObserver.disconnect();
|
|
257
307
|
if (this.lcpObserver)
|
|
258
308
|
this.lcpObserver.disconnect();
|
|
309
|
+
if (this.clsObserver)
|
|
310
|
+
this.clsObserver.disconnect();
|
|
311
|
+
if (this.inpObserver)
|
|
312
|
+
this.inpObserver.disconnect();
|
|
313
|
+
// Remove theme media listener
|
|
314
|
+
if (this.themeMediaQuery && this.themeMediaHandler) {
|
|
315
|
+
this.themeMediaQuery.removeEventListener('change', this.themeMediaHandler);
|
|
316
|
+
}
|
|
259
317
|
// Restore console
|
|
260
318
|
if (earlyConsoleCapture.originalConsole) {
|
|
261
319
|
console.log = earlyConsoleCapture.originalConsole.log;
|
|
@@ -272,6 +330,7 @@ export class GlobalDevBar {
|
|
|
272
330
|
this.overlayElement.remove();
|
|
273
331
|
this.overlayElement = null;
|
|
274
332
|
}
|
|
333
|
+
this.debug.lifecycle('DevBar destroyed');
|
|
275
334
|
}
|
|
276
335
|
injectStyles() {
|
|
277
336
|
const styleId = 'devbar-tooltip-styles';
|
|
@@ -285,17 +344,20 @@ export class GlobalDevBar {
|
|
|
285
344
|
connectWebSocket() {
|
|
286
345
|
if (this.destroyed)
|
|
287
346
|
return;
|
|
347
|
+
this.debug.ws('Connecting to WebSocket', { port: WS_PORT });
|
|
288
348
|
const ws = new WebSocket(`ws://localhost:${WS_PORT}`);
|
|
289
349
|
this.ws = ws;
|
|
290
350
|
ws.onopen = () => {
|
|
291
351
|
this.sweetlinkConnected = true;
|
|
292
352
|
this.reconnectAttempts = 0;
|
|
353
|
+
this.debug.ws('WebSocket connected');
|
|
293
354
|
ws.send(JSON.stringify({ type: 'browser-client-ready' }));
|
|
294
355
|
this.render();
|
|
295
356
|
};
|
|
296
357
|
ws.onmessage = async (event) => {
|
|
297
358
|
try {
|
|
298
359
|
const command = JSON.parse(event.data);
|
|
360
|
+
this.debug.ws('Received command', { type: command.type });
|
|
299
361
|
await this.handleSweetlinkCommand(command);
|
|
300
362
|
}
|
|
301
363
|
catch (e) {
|
|
@@ -304,16 +366,19 @@ export class GlobalDevBar {
|
|
|
304
366
|
};
|
|
305
367
|
ws.onclose = () => {
|
|
306
368
|
this.sweetlinkConnected = false;
|
|
369
|
+
this.debug.ws('WebSocket disconnected');
|
|
307
370
|
this.render();
|
|
308
371
|
// Auto-reconnect with exponential backoff
|
|
309
372
|
if (!this.destroyed && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
310
|
-
const delayMs = BASE_RECONNECT_DELAY_MS *
|
|
373
|
+
const delayMs = BASE_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempts;
|
|
311
374
|
this.reconnectAttempts++;
|
|
375
|
+
this.debug.ws('Scheduling reconnect', { attempt: this.reconnectAttempts, delayMs });
|
|
312
376
|
this.reconnectTimeout = setTimeout(() => this.connectWebSocket(), Math.min(delayMs, MAX_RECONNECT_DELAY_MS));
|
|
313
377
|
}
|
|
314
378
|
};
|
|
315
379
|
ws.onerror = () => {
|
|
316
380
|
// Error will trigger onclose, which handles reconnection
|
|
381
|
+
this.debug.ws('WebSocket error');
|
|
317
382
|
};
|
|
318
383
|
}
|
|
319
384
|
async handleSweetlinkCommand(command) {
|
|
@@ -325,11 +390,20 @@ export class GlobalDevBar {
|
|
|
325
390
|
const targetElement = command.selector
|
|
326
391
|
? document.querySelector(command.selector) || document.body
|
|
327
392
|
: document.body;
|
|
328
|
-
const canvas = await html2canvas(targetElement, {
|
|
393
|
+
const canvas = await html2canvas(targetElement, {
|
|
394
|
+
logging: false,
|
|
395
|
+
useCORS: true,
|
|
396
|
+
allowTaint: true,
|
|
397
|
+
});
|
|
329
398
|
ws.send(JSON.stringify({
|
|
330
399
|
success: true,
|
|
331
|
-
data: {
|
|
332
|
-
|
|
400
|
+
data: {
|
|
401
|
+
screenshot: canvas.toDataURL('image/png'),
|
|
402
|
+
width: canvas.width,
|
|
403
|
+
height: canvas.height,
|
|
404
|
+
selector: command.selector || 'body',
|
|
405
|
+
},
|
|
406
|
+
timestamp: Date.now(),
|
|
333
407
|
}));
|
|
334
408
|
break;
|
|
335
409
|
}
|
|
@@ -337,7 +411,7 @@ export class GlobalDevBar {
|
|
|
337
411
|
let logs = this.consoleLogs;
|
|
338
412
|
if (command.filter) {
|
|
339
413
|
const filter = command.filter.toLowerCase();
|
|
340
|
-
logs = logs.filter(log => log.level.includes(filter) || log.message.toLowerCase().includes(filter));
|
|
414
|
+
logs = logs.filter((log) => log.level.includes(filter) || log.message.toLowerCase().includes(filter));
|
|
341
415
|
}
|
|
342
416
|
ws.send(JSON.stringify({ success: true, data: logs, timestamp: Date.now() }));
|
|
343
417
|
break;
|
|
@@ -348,9 +422,18 @@ export class GlobalDevBar {
|
|
|
348
422
|
const results = elements.map((el) => {
|
|
349
423
|
if (command.property)
|
|
350
424
|
return el[command.property] ?? null;
|
|
351
|
-
return {
|
|
425
|
+
return {
|
|
426
|
+
tagName: el.tagName,
|
|
427
|
+
className: el.className,
|
|
428
|
+
id: el.id,
|
|
429
|
+
textContent: el.textContent?.trim().slice(0, 100),
|
|
430
|
+
};
|
|
352
431
|
});
|
|
353
|
-
ws.send(JSON.stringify({
|
|
432
|
+
ws.send(JSON.stringify({
|
|
433
|
+
success: true,
|
|
434
|
+
data: { count: results.length, results },
|
|
435
|
+
timestamp: Date.now(),
|
|
436
|
+
}));
|
|
354
437
|
}
|
|
355
438
|
break;
|
|
356
439
|
}
|
|
@@ -363,7 +446,11 @@ export class GlobalDevBar {
|
|
|
363
446
|
ws.send(JSON.stringify({ success: true, data: result, timestamp: Date.now() }));
|
|
364
447
|
}
|
|
365
448
|
catch (e) {
|
|
366
|
-
ws.send(JSON.stringify({
|
|
449
|
+
ws.send(JSON.stringify({
|
|
450
|
+
success: false,
|
|
451
|
+
error: e instanceof Error ? e.message : 'Execution failed',
|
|
452
|
+
timestamp: Date.now(),
|
|
453
|
+
}));
|
|
367
454
|
}
|
|
368
455
|
}
|
|
369
456
|
break;
|
|
@@ -427,25 +514,37 @@ export class GlobalDevBar {
|
|
|
427
514
|
this.lastScreenshot = path;
|
|
428
515
|
if (this.screenshotTimeout)
|
|
429
516
|
clearTimeout(this.screenshotTimeout);
|
|
430
|
-
this.screenshotTimeout = setTimeout(() => {
|
|
517
|
+
this.screenshotTimeout = setTimeout(() => {
|
|
518
|
+
this.lastScreenshot = null;
|
|
519
|
+
this.render();
|
|
520
|
+
}, durationMs);
|
|
431
521
|
break;
|
|
432
522
|
case 'designReview':
|
|
433
523
|
this.lastDesignReview = path;
|
|
434
524
|
if (this.designReviewTimeout)
|
|
435
525
|
clearTimeout(this.designReviewTimeout);
|
|
436
|
-
this.designReviewTimeout = setTimeout(() => {
|
|
526
|
+
this.designReviewTimeout = setTimeout(() => {
|
|
527
|
+
this.lastDesignReview = null;
|
|
528
|
+
this.render();
|
|
529
|
+
}, durationMs);
|
|
437
530
|
break;
|
|
438
531
|
case 'outline':
|
|
439
532
|
this.lastOutline = path;
|
|
440
533
|
if (this.outlineTimeout)
|
|
441
534
|
clearTimeout(this.outlineTimeout);
|
|
442
|
-
this.outlineTimeout = setTimeout(() => {
|
|
535
|
+
this.outlineTimeout = setTimeout(() => {
|
|
536
|
+
this.lastOutline = null;
|
|
537
|
+
this.render();
|
|
538
|
+
}, durationMs);
|
|
443
539
|
break;
|
|
444
540
|
case 'schema':
|
|
445
541
|
this.lastSchema = path;
|
|
446
542
|
if (this.schemaTimeout)
|
|
447
543
|
clearTimeout(this.schemaTimeout);
|
|
448
|
-
this.schemaTimeout = setTimeout(() => {
|
|
544
|
+
this.schemaTimeout = setTimeout(() => {
|
|
545
|
+
this.lastSchema = null;
|
|
546
|
+
this.render();
|
|
547
|
+
}, durationMs);
|
|
449
548
|
break;
|
|
450
549
|
}
|
|
451
550
|
this.render();
|
|
@@ -455,20 +554,17 @@ export class GlobalDevBar {
|
|
|
455
554
|
const width = window.innerWidth;
|
|
456
555
|
const height = window.innerHeight;
|
|
457
556
|
// 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';
|
|
557
|
+
const breakpointOrder = [
|
|
558
|
+
'2xl',
|
|
559
|
+
'xl',
|
|
560
|
+
'lg',
|
|
561
|
+
'md',
|
|
562
|
+
'sm',
|
|
563
|
+
];
|
|
564
|
+
const tailwindBreakpoint = breakpointOrder.find((bp) => width >= TAILWIND_BREAKPOINTS[bp].min) ?? 'base';
|
|
469
565
|
this.breakpointInfo = {
|
|
470
566
|
tailwindBreakpoint,
|
|
471
|
-
dimensions: `${width}x${height}
|
|
567
|
+
dimensions: `${width}x${height}`,
|
|
472
568
|
};
|
|
473
569
|
this.render();
|
|
474
570
|
};
|
|
@@ -480,10 +576,14 @@ export class GlobalDevBar {
|
|
|
480
576
|
const updatePerfStats = () => {
|
|
481
577
|
// FCP
|
|
482
578
|
const paintEntries = performance.getEntriesByType('paint');
|
|
483
|
-
const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
|
|
579
|
+
const fcpEntry = paintEntries.find((entry) => entry.name === 'first-contentful-paint');
|
|
484
580
|
const fcp = fcpEntry ? `${Math.round(fcpEntry.startTime)}ms` : '-';
|
|
485
581
|
// LCP (from cached value, updated by observer)
|
|
486
582
|
const lcp = this.lcpValue !== null ? `${Math.round(this.lcpValue)}ms` : '-';
|
|
583
|
+
// CLS (cumulative layout shift)
|
|
584
|
+
const cls = this.clsValue > 0 ? this.clsValue.toFixed(3) : '-';
|
|
585
|
+
// INP (Interaction to Next Paint)
|
|
586
|
+
const inp = this.inpValue > 0 ? `${Math.round(this.inpValue)}ms` : '-';
|
|
487
587
|
// Total Resource Size
|
|
488
588
|
const resources = performance.getEntriesByType('resource');
|
|
489
589
|
let totalBytes = 0;
|
|
@@ -498,7 +598,8 @@ export class GlobalDevBar {
|
|
|
498
598
|
const totalSize = totalBytes > 1024 * 1024
|
|
499
599
|
? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB`
|
|
500
600
|
: `${Math.round(totalBytes / 1024)} KB`;
|
|
501
|
-
this.perfStats = { fcp, lcp, totalSize };
|
|
601
|
+
this.perfStats = { fcp, lcp, cls, inp, totalSize };
|
|
602
|
+
this.debug.perf('Performance stats updated', this.perfStats);
|
|
502
603
|
this.render();
|
|
503
604
|
};
|
|
504
605
|
if (document.readyState === 'complete') {
|
|
@@ -529,6 +630,7 @@ export class GlobalDevBar {
|
|
|
529
630
|
const lastEntry = entries[entries.length - 1];
|
|
530
631
|
if (lastEntry) {
|
|
531
632
|
this.lcpValue = lastEntry.startTime;
|
|
633
|
+
this.debug.perf('LCP updated', { lcp: this.lcpValue });
|
|
532
634
|
updatePerfStats();
|
|
533
635
|
}
|
|
534
636
|
});
|
|
@@ -537,12 +639,56 @@ export class GlobalDevBar {
|
|
|
537
639
|
catch (e) {
|
|
538
640
|
console.warn('[GlobalDevBar] LCP PerformanceObserver not supported', e);
|
|
539
641
|
}
|
|
642
|
+
// CLS Observer (Cumulative Layout Shift)
|
|
643
|
+
try {
|
|
644
|
+
this.clsObserver = new PerformanceObserver((list) => {
|
|
645
|
+
for (const entry of list.getEntries()) {
|
|
646
|
+
// Only count layout shifts without recent user input
|
|
647
|
+
const layoutShift = entry;
|
|
648
|
+
if (!layoutShift.hadRecentInput && layoutShift.value) {
|
|
649
|
+
this.clsValue += layoutShift.value;
|
|
650
|
+
this.debug.perf('CLS updated', { cls: this.clsValue });
|
|
651
|
+
updatePerfStats();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
this.clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
656
|
+
}
|
|
657
|
+
catch (e) {
|
|
658
|
+
console.warn('[GlobalDevBar] CLS PerformanceObserver not supported', e);
|
|
659
|
+
}
|
|
660
|
+
// INP Observer (Interaction to Next Paint)
|
|
661
|
+
try {
|
|
662
|
+
this.inpObserver = new PerformanceObserver((list) => {
|
|
663
|
+
for (const entry of list.getEntries()) {
|
|
664
|
+
const eventEntry = entry;
|
|
665
|
+
if (eventEntry.duration && eventEntry.duration > this.inpValue) {
|
|
666
|
+
this.inpValue = eventEntry.duration;
|
|
667
|
+
this.debug.perf('INP updated', { inp: this.inpValue });
|
|
668
|
+
updatePerfStats();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
// durationThreshold filters out very short interactions
|
|
673
|
+
this.inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 16 });
|
|
674
|
+
}
|
|
675
|
+
catch (e) {
|
|
676
|
+
console.warn('[GlobalDevBar] INP PerformanceObserver not supported', e);
|
|
677
|
+
}
|
|
540
678
|
}
|
|
541
679
|
setupKeyboardShortcuts() {
|
|
542
680
|
this.keydownHandler = (e) => {
|
|
543
|
-
// Close modals on Escape
|
|
681
|
+
// Close modals/popovers on Escape
|
|
544
682
|
if (e.key === 'Escape') {
|
|
545
|
-
if (this.
|
|
683
|
+
if (this.showSettingsPopover) {
|
|
684
|
+
this.showSettingsPopover = false;
|
|
685
|
+
this.render();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (this.consoleFilter ||
|
|
689
|
+
this.showOutlineModal ||
|
|
690
|
+
this.showSchemaModal ||
|
|
691
|
+
this.showDesignReviewConfirm) {
|
|
546
692
|
this.consoleFilter = null;
|
|
547
693
|
this.showOutlineModal = false;
|
|
548
694
|
this.showSchemaModal = false;
|
|
@@ -552,6 +698,12 @@ export class GlobalDevBar {
|
|
|
552
698
|
}
|
|
553
699
|
}
|
|
554
700
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
701
|
+
// Cmd/Ctrl+Shift+M: Toggle compact mode
|
|
702
|
+
if (e.key === 'M' || e.key === 'm') {
|
|
703
|
+
e.preventDefault();
|
|
704
|
+
this.toggleCompactMode();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
555
707
|
if (e.key === 'S' || e.key === 's') {
|
|
556
708
|
e.preventDefault();
|
|
557
709
|
if (this.sweetlinkConnected && !this.capturing) {
|
|
@@ -571,6 +723,69 @@ export class GlobalDevBar {
|
|
|
571
723
|
};
|
|
572
724
|
window.addEventListener('keydown', this.keydownHandler);
|
|
573
725
|
}
|
|
726
|
+
setupTheme() {
|
|
727
|
+
// Load stored theme preference
|
|
728
|
+
this.themeMode = getStoredThemeMode();
|
|
729
|
+
this.debug.state('Theme loaded', { mode: this.themeMode });
|
|
730
|
+
// Listen for system theme changes
|
|
731
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
732
|
+
this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
733
|
+
this.themeMediaHandler = () => {
|
|
734
|
+
if (this.themeMode === 'system') {
|
|
735
|
+
this.debug.state('System theme changed', {
|
|
736
|
+
effectiveTheme: getEffectiveTheme(this.themeMode),
|
|
737
|
+
});
|
|
738
|
+
this.render();
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
this.themeMediaQuery.addEventListener('change', this.themeMediaHandler);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
loadCompactMode() {
|
|
745
|
+
if (typeof localStorage === 'undefined')
|
|
746
|
+
return;
|
|
747
|
+
const stored = localStorage.getItem(STORAGE_KEYS.compactMode);
|
|
748
|
+
this.compactMode = stored === 'true';
|
|
749
|
+
this.debug.state('Compact mode loaded', { compactMode: this.compactMode });
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Get the current theme mode
|
|
753
|
+
*/
|
|
754
|
+
getThemeMode() {
|
|
755
|
+
return this.themeMode;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Set the theme mode
|
|
759
|
+
*/
|
|
760
|
+
setThemeMode(mode) {
|
|
761
|
+
this.themeMode = mode;
|
|
762
|
+
setStoredThemeMode(mode);
|
|
763
|
+
this.debug.state('Theme mode changed', { mode, effectiveTheme: getEffectiveTheme(mode) });
|
|
764
|
+
this.render();
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Get the current effective theme colors
|
|
768
|
+
*/
|
|
769
|
+
getColors() {
|
|
770
|
+
return getThemeColors(this.themeMode);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Toggle compact mode
|
|
774
|
+
*/
|
|
775
|
+
toggleCompactMode() {
|
|
776
|
+
this.compactMode = !this.compactMode;
|
|
777
|
+
if (typeof localStorage !== 'undefined') {
|
|
778
|
+
localStorage.setItem(STORAGE_KEYS.compactMode, String(this.compactMode));
|
|
779
|
+
}
|
|
780
|
+
this.debug.state('Compact mode toggled', { compactMode: this.compactMode });
|
|
781
|
+
this.render();
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Check if compact mode is enabled
|
|
785
|
+
*/
|
|
786
|
+
isCompactMode() {
|
|
787
|
+
return this.compactMode;
|
|
788
|
+
}
|
|
574
789
|
async copyPathToClipboard(path) {
|
|
575
790
|
try {
|
|
576
791
|
await navigator.clipboard.writeText(path);
|
|
@@ -604,7 +819,7 @@ export class GlobalDevBar {
|
|
|
604
819
|
allowTaint: true,
|
|
605
820
|
scale: SCREENSHOT_SCALE,
|
|
606
821
|
width: window.innerWidth,
|
|
607
|
-
windowWidth: window.innerWidth
|
|
822
|
+
windowWidth: window.innerWidth,
|
|
608
823
|
});
|
|
609
824
|
// Restore page state
|
|
610
825
|
cleanup();
|
|
@@ -626,8 +841,33 @@ export class GlobalDevBar {
|
|
|
626
841
|
}
|
|
627
842
|
}
|
|
628
843
|
else {
|
|
629
|
-
const dataUrl = canvasToDataUrl(canvas, {
|
|
844
|
+
const dataUrl = canvasToDataUrl(canvas, {
|
|
845
|
+
format: 'jpeg',
|
|
846
|
+
quality: DEVBAR_SCREENSHOT_QUALITY,
|
|
847
|
+
});
|
|
630
848
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
849
|
+
// Include web vitals metrics
|
|
850
|
+
const webVitals = {};
|
|
851
|
+
if (this.lcpValue !== null)
|
|
852
|
+
webVitals.lcp = Math.round(this.lcpValue);
|
|
853
|
+
if (this.clsValue > 0)
|
|
854
|
+
webVitals.cls = this.clsValue;
|
|
855
|
+
if (this.inpValue > 0)
|
|
856
|
+
webVitals.inp = Math.round(this.inpValue);
|
|
857
|
+
// Get FCP from performance entries
|
|
858
|
+
const fcpEntry = performance
|
|
859
|
+
.getEntriesByType('paint')
|
|
860
|
+
.find((e) => e.name === 'first-contentful-paint');
|
|
861
|
+
if (fcpEntry)
|
|
862
|
+
webVitals.fcp = Math.round(fcpEntry.startTime);
|
|
863
|
+
// Calculate page size
|
|
864
|
+
let pageSize = 0;
|
|
865
|
+
const navEntry = performance.getEntriesByType('navigation')[0];
|
|
866
|
+
if (navEntry)
|
|
867
|
+
pageSize += navEntry.transferSize || 0;
|
|
868
|
+
performance.getEntriesByType('resource').forEach((entry) => {
|
|
869
|
+
pageSize += entry.transferSize || 0;
|
|
870
|
+
});
|
|
631
871
|
this.ws.send(JSON.stringify({
|
|
632
872
|
type: 'save-screenshot',
|
|
633
873
|
data: {
|
|
@@ -636,8 +876,10 @@ export class GlobalDevBar {
|
|
|
636
876
|
height: canvas.height,
|
|
637
877
|
logs: this.consoleLogs,
|
|
638
878
|
url: window.location.href,
|
|
639
|
-
timestamp: Date.now()
|
|
640
|
-
|
|
879
|
+
timestamp: Date.now(),
|
|
880
|
+
webVitals: Object.keys(webVitals).length > 0 ? webVitals : undefined,
|
|
881
|
+
pageSize: pageSize > 0 ? pageSize : undefined,
|
|
882
|
+
},
|
|
641
883
|
}));
|
|
642
884
|
}
|
|
643
885
|
}
|
|
@@ -672,7 +914,7 @@ export class GlobalDevBar {
|
|
|
672
914
|
allowTaint: true,
|
|
673
915
|
scale: 1, // Full quality for design review
|
|
674
916
|
width: window.innerWidth,
|
|
675
|
-
windowWidth: window.innerWidth
|
|
917
|
+
windowWidth: window.innerWidth,
|
|
676
918
|
});
|
|
677
919
|
// Restore page state
|
|
678
920
|
cleanup();
|
|
@@ -687,8 +929,8 @@ export class GlobalDevBar {
|
|
|
687
929
|
height: canvas.height,
|
|
688
930
|
logs: this.consoleLogs,
|
|
689
931
|
url: window.location.href,
|
|
690
|
-
timestamp: Date.now()
|
|
691
|
-
}
|
|
932
|
+
timestamp: Date.now(),
|
|
933
|
+
},
|
|
692
934
|
}));
|
|
693
935
|
}
|
|
694
936
|
}
|
|
@@ -783,8 +1025,8 @@ export class GlobalDevBar {
|
|
|
783
1025
|
markdown,
|
|
784
1026
|
url: window.location.href,
|
|
785
1027
|
title: document.title,
|
|
786
|
-
timestamp: Date.now()
|
|
787
|
-
}
|
|
1028
|
+
timestamp: Date.now(),
|
|
1029
|
+
},
|
|
788
1030
|
}));
|
|
789
1031
|
}
|
|
790
1032
|
}
|
|
@@ -799,8 +1041,8 @@ export class GlobalDevBar {
|
|
|
799
1041
|
markdown,
|
|
800
1042
|
url: window.location.href,
|
|
801
1043
|
title: document.title,
|
|
802
|
-
timestamp: Date.now()
|
|
803
|
-
}
|
|
1044
|
+
timestamp: Date.now(),
|
|
1045
|
+
},
|
|
804
1046
|
}));
|
|
805
1047
|
}
|
|
806
1048
|
}
|
|
@@ -831,6 +1073,9 @@ export class GlobalDevBar {
|
|
|
831
1073
|
if (this.collapsed) {
|
|
832
1074
|
this.renderCollapsed();
|
|
833
1075
|
}
|
|
1076
|
+
else if (this.compactMode) {
|
|
1077
|
+
this.renderCompact();
|
|
1078
|
+
}
|
|
834
1079
|
else {
|
|
835
1080
|
this.renderExpanded();
|
|
836
1081
|
}
|
|
@@ -860,6 +1105,10 @@ export class GlobalDevBar {
|
|
|
860
1105
|
if (this.showDesignReviewConfirm) {
|
|
861
1106
|
this.renderDesignReviewConfirmModal();
|
|
862
1107
|
}
|
|
1108
|
+
// Render settings popover
|
|
1109
|
+
if (this.showSettingsPopover) {
|
|
1110
|
+
this.renderSettingsPopover();
|
|
1111
|
+
}
|
|
863
1112
|
}
|
|
864
1113
|
renderDesignReviewConfirmModal() {
|
|
865
1114
|
const color = BUTTON_COLORS.review;
|
|
@@ -883,7 +1132,12 @@ export class GlobalDevBar {
|
|
|
883
1132
|
Object.assign(title.style, { color, fontSize: '0.875rem', fontWeight: '600' });
|
|
884
1133
|
title.textContent = 'AI Design Review';
|
|
885
1134
|
header.appendChild(title);
|
|
886
|
-
const closeBtn = createStyledButton({
|
|
1135
|
+
const closeBtn = createStyledButton({
|
|
1136
|
+
color: COLORS.textMuted,
|
|
1137
|
+
text: '×',
|
|
1138
|
+
padding: '0',
|
|
1139
|
+
fontSize: '1.25rem',
|
|
1140
|
+
});
|
|
887
1141
|
closeBtn.style.border = 'none';
|
|
888
1142
|
closeBtn.onclick = closeModal;
|
|
889
1143
|
header.appendChild(closeBtn);
|
|
@@ -915,7 +1169,11 @@ export class GlobalDevBar {
|
|
|
915
1169
|
padding: '14px 18px',
|
|
916
1170
|
borderTop: `1px solid ${COLORS.border}`,
|
|
917
1171
|
});
|
|
918
|
-
const cancelBtn = createStyledButton({
|
|
1172
|
+
const cancelBtn = createStyledButton({
|
|
1173
|
+
color: COLORS.textMuted,
|
|
1174
|
+
text: 'Cancel',
|
|
1175
|
+
padding: '8px 16px',
|
|
1176
|
+
});
|
|
919
1177
|
cancelBtn.onclick = closeModal;
|
|
920
1178
|
footer.appendChild(cancelBtn);
|
|
921
1179
|
if (this.apiKeyStatus?.configured) {
|
|
@@ -938,7 +1196,11 @@ export class GlobalDevBar {
|
|
|
938
1196
|
const instructions = document.createElement('div');
|
|
939
1197
|
Object.assign(instructions.style, { marginBottom: '12px' });
|
|
940
1198
|
const instructTitle = document.createElement('div');
|
|
941
|
-
Object.assign(instructTitle.style, {
|
|
1199
|
+
Object.assign(instructTitle.style, {
|
|
1200
|
+
color: COLORS.textSecondary,
|
|
1201
|
+
fontWeight: '600',
|
|
1202
|
+
marginBottom: '8px',
|
|
1203
|
+
});
|
|
942
1204
|
instructTitle.textContent = 'To configure:';
|
|
943
1205
|
instructions.appendChild(instructTitle);
|
|
944
1206
|
const steps = [
|
|
@@ -998,7 +1260,11 @@ export class GlobalDevBar {
|
|
|
998
1260
|
// Model info
|
|
999
1261
|
if (this.apiKeyStatus?.model) {
|
|
1000
1262
|
const modelDiv = document.createElement('div');
|
|
1001
|
-
Object.assign(modelDiv.style, {
|
|
1263
|
+
Object.assign(modelDiv.style, {
|
|
1264
|
+
color: COLORS.textMuted,
|
|
1265
|
+
fontSize: '0.6875rem',
|
|
1266
|
+
marginTop: '12px',
|
|
1267
|
+
});
|
|
1002
1268
|
modelDiv.textContent = `Model: ${this.apiKeyStatus.model}`;
|
|
1003
1269
|
if (this.apiKeyStatus.maskedKey) {
|
|
1004
1270
|
modelDiv.textContent += ` | Key: ${this.apiKeyStatus.maskedKey}`;
|
|
@@ -1011,7 +1277,7 @@ export class GlobalDevBar {
|
|
|
1011
1277
|
const filterType = this.consoleFilter;
|
|
1012
1278
|
if (!filterType)
|
|
1013
1279
|
return;
|
|
1014
|
-
const logs = earlyConsoleCapture.logs.filter(log => log.level === filterType);
|
|
1280
|
+
const logs = earlyConsoleCapture.logs.filter((log) => log.level === filterType);
|
|
1015
1281
|
const color = filterType === 'error' ? BUTTON_COLORS.error : BUTTON_COLORS.warning;
|
|
1016
1282
|
const label = filterType === 'error' ? 'Errors' : 'Warnings';
|
|
1017
1283
|
const popup = document.createElement('div');
|
|
@@ -1120,7 +1386,8 @@ export class GlobalDevBar {
|
|
|
1120
1386
|
wordBreak: 'break-word',
|
|
1121
1387
|
whiteSpace: 'pre-wrap',
|
|
1122
1388
|
});
|
|
1123
|
-
message.textContent =
|
|
1389
|
+
message.textContent =
|
|
1390
|
+
log.message.length > 500 ? `${log.message.slice(0, 500)}...` : log.message;
|
|
1124
1391
|
logItem.appendChild(message);
|
|
1125
1392
|
container.appendChild(logItem);
|
|
1126
1393
|
});
|
|
@@ -1192,7 +1459,7 @@ export class GlobalDevBar {
|
|
|
1192
1459
|
fontSize: '0.6875rem',
|
|
1193
1460
|
marginLeft: '8px',
|
|
1194
1461
|
});
|
|
1195
|
-
const truncatedText = node.text.length > 60 ? node.text.slice(0, 60)
|
|
1462
|
+
const truncatedText = node.text.length > 60 ? `${node.text.slice(0, 60)}...` : node.text;
|
|
1196
1463
|
textSpan.textContent = truncatedText;
|
|
1197
1464
|
nodeEl.appendChild(textSpan);
|
|
1198
1465
|
if (node.id) {
|
|
@@ -1325,7 +1592,7 @@ export class GlobalDevBar {
|
|
|
1325
1592
|
punct: COLORS.textMuted, // gray
|
|
1326
1593
|
};
|
|
1327
1594
|
// 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+)?)|([{}
|
|
1595
|
+
const tokenPattern = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)|([{}[\],])|(\s+)/g;
|
|
1329
1596
|
for (const match of json.matchAll(tokenPattern)) {
|
|
1330
1597
|
const [, str, colon, bool, nullToken, num, punct, whitespace] = match;
|
|
1331
1598
|
if (whitespace) {
|
|
@@ -1412,23 +1679,366 @@ export class GlobalDevBar {
|
|
|
1412
1679
|
container.appendChild(row);
|
|
1413
1680
|
}
|
|
1414
1681
|
}
|
|
1415
|
-
|
|
1682
|
+
/**
|
|
1683
|
+
* Render compact mode - single row with essential controls only
|
|
1684
|
+
* Shows: connection dot, error/warn badges, screenshot button, settings gear
|
|
1685
|
+
*/
|
|
1686
|
+
renderCompact() {
|
|
1416
1687
|
if (!this.container)
|
|
1417
1688
|
return;
|
|
1418
1689
|
const { position, accentColor } = this.options;
|
|
1419
1690
|
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%)' },
|
|
1691
|
+
const positionStyles = {
|
|
1692
|
+
'bottom-left': { bottom: '20px', left: '80px' },
|
|
1693
|
+
'bottom-right': { bottom: '20px', right: '16px' },
|
|
1694
|
+
'top-left': { top: '20px', left: '80px' },
|
|
1695
|
+
'top-right': { top: '20px', right: '16px' },
|
|
1696
|
+
'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
|
|
1430
1697
|
};
|
|
1431
|
-
const posStyle =
|
|
1698
|
+
const posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
|
|
1699
|
+
const wrapper = this.container;
|
|
1700
|
+
// Reset position properties first
|
|
1701
|
+
wrapper.style.top = '';
|
|
1702
|
+
wrapper.style.bottom = '';
|
|
1703
|
+
wrapper.style.left = '';
|
|
1704
|
+
wrapper.style.right = '';
|
|
1705
|
+
wrapper.style.transform = '';
|
|
1706
|
+
Object.assign(wrapper.style, {
|
|
1707
|
+
position: 'fixed',
|
|
1708
|
+
...posStyle,
|
|
1709
|
+
zIndex: '9999',
|
|
1710
|
+
backgroundColor: 'rgba(17, 24, 39, 0.95)',
|
|
1711
|
+
border: `1px solid ${accentColor}`,
|
|
1712
|
+
borderRadius: '20px',
|
|
1713
|
+
color: accentColor,
|
|
1714
|
+
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
|
|
1715
|
+
backdropFilter: 'blur(8px)',
|
|
1716
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
1717
|
+
padding: '6px 10px',
|
|
1718
|
+
display: 'flex',
|
|
1719
|
+
alignItems: 'center',
|
|
1720
|
+
gap: '8px',
|
|
1721
|
+
fontFamily: FONT_MONO,
|
|
1722
|
+
fontSize: '0.6875rem',
|
|
1723
|
+
});
|
|
1724
|
+
// Connection indicator
|
|
1725
|
+
const connIndicator = document.createElement('span');
|
|
1726
|
+
connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
|
|
1727
|
+
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected ? 'Sweetlink connected' : 'Sweetlink disconnected');
|
|
1728
|
+
Object.assign(connIndicator.style, {
|
|
1729
|
+
width: '12px',
|
|
1730
|
+
height: '12px',
|
|
1731
|
+
borderRadius: '50%',
|
|
1732
|
+
display: 'flex',
|
|
1733
|
+
alignItems: 'center',
|
|
1734
|
+
justifyContent: 'center',
|
|
1735
|
+
cursor: 'pointer',
|
|
1736
|
+
});
|
|
1737
|
+
connIndicator.onclick = (e) => {
|
|
1738
|
+
e.stopPropagation();
|
|
1739
|
+
this.collapsed = true;
|
|
1740
|
+
this.debug.state('Collapsed DevBar from compact mode');
|
|
1741
|
+
this.render();
|
|
1742
|
+
};
|
|
1743
|
+
const connDot = document.createElement('span');
|
|
1744
|
+
Object.assign(connDot.style, {
|
|
1745
|
+
width: '6px',
|
|
1746
|
+
height: '6px',
|
|
1747
|
+
borderRadius: '50%',
|
|
1748
|
+
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1749
|
+
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1750
|
+
});
|
|
1751
|
+
connIndicator.appendChild(connDot);
|
|
1752
|
+
wrapper.appendChild(connIndicator);
|
|
1753
|
+
// Error badge
|
|
1754
|
+
if (errorCount > 0) {
|
|
1755
|
+
wrapper.appendChild(this.createConsoleBadge('error', errorCount, BUTTON_COLORS.error));
|
|
1756
|
+
}
|
|
1757
|
+
// Warning badge
|
|
1758
|
+
if (warningCount > 0) {
|
|
1759
|
+
wrapper.appendChild(this.createConsoleBadge('warn', warningCount, BUTTON_COLORS.warning));
|
|
1760
|
+
}
|
|
1761
|
+
// Screenshot button (if enabled)
|
|
1762
|
+
if (this.options.showScreenshot) {
|
|
1763
|
+
wrapper.appendChild(this.createScreenshotButton(accentColor));
|
|
1764
|
+
}
|
|
1765
|
+
// Settings gear button
|
|
1766
|
+
wrapper.appendChild(this.createSettingsButton());
|
|
1767
|
+
// Expand button (double-arrow)
|
|
1768
|
+
const expandBtn = document.createElement('button');
|
|
1769
|
+
expandBtn.type = 'button';
|
|
1770
|
+
expandBtn.className = this.tooltipClass('right');
|
|
1771
|
+
expandBtn.setAttribute('data-tooltip', 'Expand DevBar');
|
|
1772
|
+
Object.assign(expandBtn.style, {
|
|
1773
|
+
display: 'flex',
|
|
1774
|
+
alignItems: 'center',
|
|
1775
|
+
justifyContent: 'center',
|
|
1776
|
+
width: '18px',
|
|
1777
|
+
height: '18px',
|
|
1778
|
+
borderRadius: '50%',
|
|
1779
|
+
border: `1px solid ${accentColor}60`,
|
|
1780
|
+
backgroundColor: 'transparent',
|
|
1781
|
+
color: `${accentColor}99`,
|
|
1782
|
+
cursor: 'pointer',
|
|
1783
|
+
fontSize: '0.5rem',
|
|
1784
|
+
transition: 'all 150ms',
|
|
1785
|
+
});
|
|
1786
|
+
expandBtn.textContent = '⟫';
|
|
1787
|
+
expandBtn.onmouseenter = () => {
|
|
1788
|
+
expandBtn.style.backgroundColor = `${accentColor}20`;
|
|
1789
|
+
expandBtn.style.borderColor = accentColor;
|
|
1790
|
+
expandBtn.style.color = accentColor;
|
|
1791
|
+
};
|
|
1792
|
+
expandBtn.onmouseleave = () => {
|
|
1793
|
+
expandBtn.style.backgroundColor = 'transparent';
|
|
1794
|
+
expandBtn.style.borderColor = `${accentColor}60`;
|
|
1795
|
+
expandBtn.style.color = `${accentColor}99`;
|
|
1796
|
+
};
|
|
1797
|
+
expandBtn.onclick = () => {
|
|
1798
|
+
this.toggleCompactMode();
|
|
1799
|
+
};
|
|
1800
|
+
wrapper.appendChild(expandBtn);
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Create the settings gear button
|
|
1804
|
+
*/
|
|
1805
|
+
createSettingsButton() {
|
|
1806
|
+
const btn = document.createElement('button');
|
|
1807
|
+
btn.type = 'button';
|
|
1808
|
+
btn.className = this.tooltipClass('right');
|
|
1809
|
+
btn.setAttribute('data-tooltip', 'Settings (Cmd+Shift+M: toggle compact)');
|
|
1810
|
+
const isActive = this.showSettingsPopover;
|
|
1811
|
+
const color = COLORS.textSecondary;
|
|
1812
|
+
Object.assign(btn.style, {
|
|
1813
|
+
display: 'flex',
|
|
1814
|
+
alignItems: 'center',
|
|
1815
|
+
justifyContent: 'center',
|
|
1816
|
+
width: '22px',
|
|
1817
|
+
height: '22px',
|
|
1818
|
+
minWidth: '22px',
|
|
1819
|
+
minHeight: '22px',
|
|
1820
|
+
flexShrink: '0',
|
|
1821
|
+
borderRadius: '50%',
|
|
1822
|
+
border: `1px solid ${isActive ? color : `${color}60`}`,
|
|
1823
|
+
backgroundColor: isActive ? `${color}20` : 'transparent',
|
|
1824
|
+
color: isActive ? color : `${color}99`,
|
|
1825
|
+
cursor: 'pointer',
|
|
1826
|
+
transition: 'all 150ms',
|
|
1827
|
+
});
|
|
1828
|
+
btn.onclick = () => {
|
|
1829
|
+
this.showSettingsPopover = !this.showSettingsPopover;
|
|
1830
|
+
this.consoleFilter = null;
|
|
1831
|
+
this.showOutlineModal = false;
|
|
1832
|
+
this.showSchemaModal = false;
|
|
1833
|
+
this.showDesignReviewConfirm = false;
|
|
1834
|
+
this.render();
|
|
1835
|
+
};
|
|
1836
|
+
// Gear icon SVG
|
|
1837
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1838
|
+
svg.setAttribute('width', '12');
|
|
1839
|
+
svg.setAttribute('height', '12');
|
|
1840
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1841
|
+
svg.setAttribute('fill', 'none');
|
|
1842
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
1843
|
+
svg.setAttribute('stroke-width', '2');
|
|
1844
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
1845
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
1846
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1847
|
+
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');
|
|
1848
|
+
svg.appendChild(path);
|
|
1849
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
1850
|
+
circle.setAttribute('cx', '12');
|
|
1851
|
+
circle.setAttribute('cy', '12');
|
|
1852
|
+
circle.setAttribute('r', '3');
|
|
1853
|
+
svg.appendChild(circle);
|
|
1854
|
+
btn.appendChild(svg);
|
|
1855
|
+
return btn;
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Render the settings popover
|
|
1859
|
+
*/
|
|
1860
|
+
renderSettingsPopover() {
|
|
1861
|
+
const { position, accentColor } = this.options;
|
|
1862
|
+
const color = COLORS.textSecondary;
|
|
1863
|
+
const popover = document.createElement('div');
|
|
1864
|
+
popover.setAttribute('data-devbar', 'true');
|
|
1865
|
+
// Position based on devbar position
|
|
1866
|
+
const isTop = position.startsWith('top');
|
|
1867
|
+
const isRight = position.includes('right');
|
|
1868
|
+
Object.assign(popover.style, {
|
|
1869
|
+
position: 'fixed',
|
|
1870
|
+
[isTop ? 'top' : 'bottom']: isTop ? '70px' : '70px',
|
|
1871
|
+
[isRight ? 'right' : 'left']: isRight ? '16px' : '80px',
|
|
1872
|
+
zIndex: '10003',
|
|
1873
|
+
backgroundColor: 'rgba(17, 24, 39, 0.98)',
|
|
1874
|
+
border: `1px solid ${accentColor}`,
|
|
1875
|
+
borderRadius: '8px',
|
|
1876
|
+
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${accentColor}33`,
|
|
1877
|
+
backdropFilter: 'blur(8px)',
|
|
1878
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
1879
|
+
minWidth: '200px',
|
|
1880
|
+
fontFamily: FONT_MONO,
|
|
1881
|
+
});
|
|
1882
|
+
// Header
|
|
1883
|
+
const header = document.createElement('div');
|
|
1884
|
+
Object.assign(header.style, {
|
|
1885
|
+
display: 'flex',
|
|
1886
|
+
alignItems: 'center',
|
|
1887
|
+
justifyContent: 'space-between',
|
|
1888
|
+
padding: '10px 14px',
|
|
1889
|
+
borderBottom: `1px solid ${accentColor}30`,
|
|
1890
|
+
});
|
|
1891
|
+
const title = document.createElement('span');
|
|
1892
|
+
Object.assign(title.style, { color: accentColor, fontSize: '0.75rem', fontWeight: '600' });
|
|
1893
|
+
title.textContent = 'Settings';
|
|
1894
|
+
header.appendChild(title);
|
|
1895
|
+
const closeBtn = createStyledButton({
|
|
1896
|
+
color: COLORS.textMuted,
|
|
1897
|
+
text: '×',
|
|
1898
|
+
padding: '2px 6px',
|
|
1899
|
+
fontSize: '0.875rem',
|
|
1900
|
+
});
|
|
1901
|
+
closeBtn.style.border = 'none';
|
|
1902
|
+
closeBtn.onclick = () => {
|
|
1903
|
+
this.showSettingsPopover = false;
|
|
1904
|
+
this.render();
|
|
1905
|
+
};
|
|
1906
|
+
header.appendChild(closeBtn);
|
|
1907
|
+
popover.appendChild(header);
|
|
1908
|
+
// Theme section
|
|
1909
|
+
const themeSection = document.createElement('div');
|
|
1910
|
+
Object.assign(themeSection.style, { padding: '10px 14px', borderBottom: `1px solid ${color}20` });
|
|
1911
|
+
const themeSectionTitle = document.createElement('div');
|
|
1912
|
+
Object.assign(themeSectionTitle.style, {
|
|
1913
|
+
color,
|
|
1914
|
+
fontSize: '0.625rem',
|
|
1915
|
+
textTransform: 'uppercase',
|
|
1916
|
+
letterSpacing: '0.1em',
|
|
1917
|
+
marginBottom: '8px',
|
|
1918
|
+
});
|
|
1919
|
+
themeSectionTitle.textContent = 'Theme';
|
|
1920
|
+
themeSection.appendChild(themeSectionTitle);
|
|
1921
|
+
const themeOptions = document.createElement('div');
|
|
1922
|
+
Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
|
|
1923
|
+
const themeModes = ['system', 'dark', 'light'];
|
|
1924
|
+
themeModes.forEach((mode) => {
|
|
1925
|
+
const btn = document.createElement('button');
|
|
1926
|
+
const isActive = this.themeMode === mode;
|
|
1927
|
+
Object.assign(btn.style, {
|
|
1928
|
+
padding: '4px 10px',
|
|
1929
|
+
backgroundColor: isActive ? `${accentColor}20` : 'transparent',
|
|
1930
|
+
border: `1px solid ${isActive ? accentColor : `${color}40`}`,
|
|
1931
|
+
borderRadius: '4px',
|
|
1932
|
+
color: isActive ? accentColor : color,
|
|
1933
|
+
fontSize: '0.625rem',
|
|
1934
|
+
cursor: 'pointer',
|
|
1935
|
+
textTransform: 'capitalize',
|
|
1936
|
+
transition: 'all 150ms',
|
|
1937
|
+
});
|
|
1938
|
+
btn.textContent = mode;
|
|
1939
|
+
btn.onclick = () => {
|
|
1940
|
+
this.setThemeMode(mode);
|
|
1941
|
+
};
|
|
1942
|
+
themeOptions.appendChild(btn);
|
|
1943
|
+
});
|
|
1944
|
+
themeSection.appendChild(themeOptions);
|
|
1945
|
+
popover.appendChild(themeSection);
|
|
1946
|
+
// Display section
|
|
1947
|
+
const displaySection = document.createElement('div');
|
|
1948
|
+
Object.assign(displaySection.style, { padding: '10px 14px' });
|
|
1949
|
+
const displaySectionTitle = document.createElement('div');
|
|
1950
|
+
Object.assign(displaySectionTitle.style, {
|
|
1951
|
+
color,
|
|
1952
|
+
fontSize: '0.625rem',
|
|
1953
|
+
textTransform: 'uppercase',
|
|
1954
|
+
letterSpacing: '0.1em',
|
|
1955
|
+
marginBottom: '8px',
|
|
1956
|
+
});
|
|
1957
|
+
displaySectionTitle.textContent = 'Display';
|
|
1958
|
+
displaySection.appendChild(displaySectionTitle);
|
|
1959
|
+
// Compact mode toggle
|
|
1960
|
+
const compactRow = document.createElement('div');
|
|
1961
|
+
Object.assign(compactRow.style, {
|
|
1962
|
+
display: 'flex',
|
|
1963
|
+
alignItems: 'center',
|
|
1964
|
+
justifyContent: 'space-between',
|
|
1965
|
+
});
|
|
1966
|
+
const compactLabel = document.createElement('span');
|
|
1967
|
+
Object.assign(compactLabel.style, { color: COLORS.text, fontSize: '0.6875rem' });
|
|
1968
|
+
compactLabel.textContent = 'Compact Mode';
|
|
1969
|
+
compactRow.appendChild(compactLabel);
|
|
1970
|
+
// Toggle switch
|
|
1971
|
+
const toggle = document.createElement('button');
|
|
1972
|
+
const isCompact = this.compactMode;
|
|
1973
|
+
Object.assign(toggle.style, {
|
|
1974
|
+
width: '32px',
|
|
1975
|
+
height: '18px',
|
|
1976
|
+
borderRadius: '9px',
|
|
1977
|
+
border: 'none',
|
|
1978
|
+
backgroundColor: isCompact ? accentColor : `${color}40`,
|
|
1979
|
+
position: 'relative',
|
|
1980
|
+
cursor: 'pointer',
|
|
1981
|
+
transition: 'all 150ms',
|
|
1982
|
+
});
|
|
1983
|
+
const toggleKnob = document.createElement('span');
|
|
1984
|
+
Object.assign(toggleKnob.style, {
|
|
1985
|
+
position: 'absolute',
|
|
1986
|
+
top: '2px',
|
|
1987
|
+
left: isCompact ? '16px' : '2px',
|
|
1988
|
+
width: '14px',
|
|
1989
|
+
height: '14px',
|
|
1990
|
+
borderRadius: '50%',
|
|
1991
|
+
backgroundColor: '#fff',
|
|
1992
|
+
transition: 'left 150ms',
|
|
1993
|
+
});
|
|
1994
|
+
toggle.appendChild(toggleKnob);
|
|
1995
|
+
toggle.onclick = () => {
|
|
1996
|
+
this.toggleCompactMode();
|
|
1997
|
+
};
|
|
1998
|
+
compactRow.appendChild(toggle);
|
|
1999
|
+
displaySection.appendChild(compactRow);
|
|
2000
|
+
// Keyboard shortcut hint
|
|
2001
|
+
const shortcutHint = document.createElement('div');
|
|
2002
|
+
Object.assign(shortcutHint.style, {
|
|
2003
|
+
color: COLORS.textMuted,
|
|
2004
|
+
fontSize: '0.5625rem',
|
|
2005
|
+
marginTop: '6px',
|
|
2006
|
+
});
|
|
2007
|
+
shortcutHint.textContent = 'Keyboard: Cmd+Shift+M';
|
|
2008
|
+
displaySection.appendChild(shortcutHint);
|
|
2009
|
+
popover.appendChild(displaySection);
|
|
2010
|
+
this.overlayElement = popover;
|
|
2011
|
+
document.body.appendChild(popover);
|
|
2012
|
+
}
|
|
2013
|
+
renderCollapsed() {
|
|
2014
|
+
if (!this.container)
|
|
2015
|
+
return;
|
|
2016
|
+
const { position, accentColor } = this.options;
|
|
2017
|
+
const { errorCount, warningCount } = this.getLogCounts();
|
|
2018
|
+
// Use captured dot position if available, otherwise fall back to preset positions
|
|
2019
|
+
// The 13px offset accounts for half the collapsed circle diameter (26px / 2)
|
|
2020
|
+
let posStyle;
|
|
2021
|
+
if (this.lastDotPosition) {
|
|
2022
|
+
// Position based on where the dot actually was
|
|
2023
|
+
const isTop = position.startsWith('top');
|
|
2024
|
+
posStyle = isTop
|
|
2025
|
+
? { top: `${this.lastDotPosition.top - 13}px`, left: `${this.lastDotPosition.left - 13}px` }
|
|
2026
|
+
: {
|
|
2027
|
+
bottom: `${this.lastDotPosition.bottom - 13}px`,
|
|
2028
|
+
left: `${this.lastDotPosition.left - 13}px`,
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
else {
|
|
2032
|
+
// Fallback preset positions for when no dot position was captured
|
|
2033
|
+
const collapsedPositions = {
|
|
2034
|
+
'bottom-left': { bottom: '27px', left: '86px' },
|
|
2035
|
+
'bottom-right': { bottom: '27px', right: '29px' },
|
|
2036
|
+
'top-left': { top: '27px', left: '86px' },
|
|
2037
|
+
'top-right': { top: '27px', right: '29px' },
|
|
2038
|
+
'bottom-center': { bottom: '19px', left: '50%', transform: 'translateX(-50%)' },
|
|
2039
|
+
};
|
|
2040
|
+
posStyle = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
|
|
2041
|
+
}
|
|
1432
2042
|
const wrapper = this.container;
|
|
1433
2043
|
wrapper.className = this.tooltipClass('left', 'devbar-collapse');
|
|
1434
2044
|
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 +2066,11 @@ export class GlobalDevBar {
|
|
|
1456
2066
|
width: '26px',
|
|
1457
2067
|
height: '26px',
|
|
1458
2068
|
boxSizing: 'border-box',
|
|
1459
|
-
animation: 'devbar-collapse 150ms ease-out'
|
|
2069
|
+
animation: 'devbar-collapse 150ms ease-out',
|
|
1460
2070
|
});
|
|
1461
2071
|
wrapper.onclick = () => {
|
|
1462
2072
|
this.collapsed = false;
|
|
2073
|
+
this.debug.state('Expanded DevBar');
|
|
1463
2074
|
this.render();
|
|
1464
2075
|
};
|
|
1465
2076
|
// Connection indicator dot (same size as in expanded state)
|
|
@@ -1469,7 +2080,7 @@ export class GlobalDevBar {
|
|
|
1469
2080
|
height: '6px',
|
|
1470
2081
|
borderRadius: '50%',
|
|
1471
2082
|
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1472
|
-
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none'
|
|
2083
|
+
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1473
2084
|
});
|
|
1474
2085
|
wrapper.appendChild(dot);
|
|
1475
2086
|
// Error badge (absolute, top-right of circle, shifted left if warning badge exists)
|
|
@@ -1524,10 +2135,21 @@ export class GlobalDevBar {
|
|
|
1524
2135
|
width: sizeOverrides?.width ?? defaultWidth,
|
|
1525
2136
|
maxWidth: sizeOverrides?.maxWidth ?? defaultMaxWidth,
|
|
1526
2137
|
minWidth: sizeOverrides?.minWidth ?? defaultMinWidth,
|
|
1527
|
-
cursor: 'default'
|
|
2138
|
+
cursor: 'default',
|
|
1528
2139
|
});
|
|
1529
2140
|
wrapper.ondblclick = () => {
|
|
2141
|
+
// Capture dot position before collapsing
|
|
2142
|
+
const dotEl = wrapper.querySelector('.devbar-status span span');
|
|
2143
|
+
if (dotEl) {
|
|
2144
|
+
const rect = dotEl.getBoundingClientRect();
|
|
2145
|
+
this.lastDotPosition = {
|
|
2146
|
+
left: rect.left + rect.width / 2,
|
|
2147
|
+
top: rect.top + rect.height / 2,
|
|
2148
|
+
bottom: window.innerHeight - (rect.top + rect.height / 2),
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
1530
2151
|
this.collapsed = true;
|
|
2152
|
+
this.debug.state('Collapsed DevBar (double-click)');
|
|
1531
2153
|
this.render();
|
|
1532
2154
|
};
|
|
1533
2155
|
// Main row - wrapping controlled by CSS media query
|
|
@@ -1544,12 +2166,14 @@ export class GlobalDevBar {
|
|
|
1544
2166
|
boxSizing: 'border-box',
|
|
1545
2167
|
fontFamily: FONT_MONO,
|
|
1546
2168
|
fontSize: '0.6875rem',
|
|
1547
|
-
lineHeight: '1rem'
|
|
2169
|
+
lineHeight: '1rem',
|
|
1548
2170
|
});
|
|
1549
2171
|
// Connection indicator (click to collapse)
|
|
1550
2172
|
const connIndicator = document.createElement('span');
|
|
1551
2173
|
connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
|
|
1552
|
-
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected
|
|
2174
|
+
connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected
|
|
2175
|
+
? 'Sweetlink connected (click to minimize)'
|
|
2176
|
+
: 'Sweetlink disconnected (click to minimize)');
|
|
1553
2177
|
Object.assign(connIndicator.style, {
|
|
1554
2178
|
width: '12px',
|
|
1555
2179
|
height: '12px',
|
|
@@ -1559,11 +2183,19 @@ export class GlobalDevBar {
|
|
|
1559
2183
|
alignItems: 'center',
|
|
1560
2184
|
justifyContent: 'center',
|
|
1561
2185
|
cursor: 'pointer',
|
|
1562
|
-
flexShrink: '0'
|
|
2186
|
+
flexShrink: '0',
|
|
1563
2187
|
});
|
|
1564
2188
|
connIndicator.onclick = (e) => {
|
|
1565
2189
|
e.stopPropagation();
|
|
2190
|
+
// Capture dot position before collapsing (connDot is the inner 6px dot)
|
|
2191
|
+
const rect = connIndicator.getBoundingClientRect();
|
|
2192
|
+
this.lastDotPosition = {
|
|
2193
|
+
left: rect.left + rect.width / 2,
|
|
2194
|
+
top: rect.top + rect.height / 2,
|
|
2195
|
+
bottom: window.innerHeight - (rect.top + rect.height / 2),
|
|
2196
|
+
};
|
|
1566
2197
|
this.collapsed = true;
|
|
2198
|
+
this.debug.state('Collapsed DevBar (connection dot click)');
|
|
1567
2199
|
this.render();
|
|
1568
2200
|
};
|
|
1569
2201
|
const connDot = document.createElement('span');
|
|
@@ -1573,7 +2205,7 @@ export class GlobalDevBar {
|
|
|
1573
2205
|
borderRadius: '50%',
|
|
1574
2206
|
backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
|
|
1575
2207
|
boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
|
|
1576
|
-
transition: 'all 300ms'
|
|
2208
|
+
transition: 'all 300ms',
|
|
1577
2209
|
});
|
|
1578
2210
|
connIndicator.appendChild(connDot);
|
|
1579
2211
|
// Status row wrapper - keeps connection dot, info, and badges together
|
|
@@ -1584,7 +2216,7 @@ export class GlobalDevBar {
|
|
|
1584
2216
|
alignItems: 'center',
|
|
1585
2217
|
gap: '0.5rem',
|
|
1586
2218
|
flexWrap: 'nowrap',
|
|
1587
|
-
flexShrink: '0'
|
|
2219
|
+
flexShrink: '0',
|
|
1588
2220
|
});
|
|
1589
2221
|
statusRow.appendChild(connIndicator);
|
|
1590
2222
|
// Info section
|
|
@@ -1598,7 +2230,7 @@ export class GlobalDevBar {
|
|
|
1598
2230
|
letterSpacing: '0.05em',
|
|
1599
2231
|
flexShrink: '1',
|
|
1600
2232
|
minWidth: '0',
|
|
1601
|
-
overflow: 'visible'
|
|
2233
|
+
overflow: 'visible',
|
|
1602
2234
|
});
|
|
1603
2235
|
// Breakpoint info
|
|
1604
2236
|
if (showMetrics.breakpoint && this.breakpointInfo) {
|
|
@@ -1610,9 +2242,10 @@ export class GlobalDevBar {
|
|
|
1610
2242
|
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
2243
|
let bpText = bp;
|
|
1612
2244
|
if (bp !== 'base') {
|
|
1613
|
-
bpText =
|
|
1614
|
-
|
|
1615
|
-
|
|
2245
|
+
bpText =
|
|
2246
|
+
bp === 'sm'
|
|
2247
|
+
? `${bp} - ${this.breakpointInfo.dimensions.split('x')[0]}`
|
|
2248
|
+
: `${bp} - ${this.breakpointInfo.dimensions}`;
|
|
1616
2249
|
}
|
|
1617
2250
|
bpSpan.textContent = bpText;
|
|
1618
2251
|
infoSection.appendChild(bpSpan);
|
|
@@ -1643,6 +2276,24 @@ export class GlobalDevBar {
|
|
|
1643
2276
|
lcpSpan.textContent = `LCP ${this.perfStats.lcp}`;
|
|
1644
2277
|
infoSection.appendChild(lcpSpan);
|
|
1645
2278
|
}
|
|
2279
|
+
if (showMetrics.cls) {
|
|
2280
|
+
addSeparator();
|
|
2281
|
+
const clsSpan = document.createElement('span');
|
|
2282
|
+
clsSpan.className = this.tooltipClass('left', 'devbar-item');
|
|
2283
|
+
Object.assign(clsSpan.style, { opacity: '0.85', cursor: 'default' });
|
|
2284
|
+
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');
|
|
2285
|
+
clsSpan.textContent = `CLS ${this.perfStats.cls}`;
|
|
2286
|
+
infoSection.appendChild(clsSpan);
|
|
2287
|
+
}
|
|
2288
|
+
if (showMetrics.inp) {
|
|
2289
|
+
addSeparator();
|
|
2290
|
+
const inpSpan = document.createElement('span');
|
|
2291
|
+
inpSpan.className = this.tooltipClass('left', 'devbar-item');
|
|
2292
|
+
Object.assign(inpSpan.style, { opacity: '0.85', cursor: 'default' });
|
|
2293
|
+
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');
|
|
2294
|
+
inpSpan.textContent = `INP ${this.perfStats.inp}`;
|
|
2295
|
+
infoSection.appendChild(inpSpan);
|
|
2296
|
+
}
|
|
1646
2297
|
if (showMetrics.pageSize) {
|
|
1647
2298
|
addSeparator();
|
|
1648
2299
|
const sizeSpan = document.createElement('span');
|
|
@@ -1673,6 +2324,7 @@ export class GlobalDevBar {
|
|
|
1673
2324
|
actionsContainer.appendChild(this.createAIReviewButton());
|
|
1674
2325
|
actionsContainer.appendChild(this.createOutlineButton());
|
|
1675
2326
|
actionsContainer.appendChild(this.createSchemaButton());
|
|
2327
|
+
actionsContainer.appendChild(this.createSettingsButton());
|
|
1676
2328
|
mainRow.appendChild(actionsContainer);
|
|
1677
2329
|
wrapper.appendChild(mainRow);
|
|
1678
2330
|
// Render custom controls row if there are any
|
|
@@ -1690,7 +2342,7 @@ export class GlobalDevBar {
|
|
|
1690
2342
|
fontFamily: FONT_MONO,
|
|
1691
2343
|
fontSize: '0.6875rem',
|
|
1692
2344
|
});
|
|
1693
|
-
GlobalDevBar.customControls.forEach(control => {
|
|
2345
|
+
GlobalDevBar.customControls.forEach((control) => {
|
|
1694
2346
|
const btn = document.createElement('button');
|
|
1695
2347
|
btn.type = 'button';
|
|
1696
2348
|
const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
|
|
@@ -1765,13 +2417,7 @@ export class GlobalDevBar {
|
|
|
1765
2417
|
btn.type = 'button';
|
|
1766
2418
|
btn.className = this.tooltipClass('right');
|
|
1767
2419
|
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' : ''}`;
|
|
2420
|
+
const tooltip = this.getScreenshotTooltip();
|
|
1775
2421
|
btn.setAttribute('data-tooltip', tooltip);
|
|
1776
2422
|
Object.assign(btn.style, {
|
|
1777
2423
|
display: 'flex',
|
|
@@ -1789,7 +2435,7 @@ export class GlobalDevBar {
|
|
|
1789
2435
|
color: hasSuccessState ? accentColor : `${accentColor}99`,
|
|
1790
2436
|
cursor: !this.capturing ? 'pointer' : 'not-allowed',
|
|
1791
2437
|
opacity: '1',
|
|
1792
|
-
transition: 'all 150ms'
|
|
2438
|
+
transition: 'all 150ms',
|
|
1793
2439
|
});
|
|
1794
2440
|
btn.disabled = this.capturing;
|
|
1795
2441
|
btn.onclick = (e) => {
|
|
@@ -1835,17 +2481,47 @@ export class GlobalDevBar {
|
|
|
1835
2481
|
}
|
|
1836
2482
|
return btn;
|
|
1837
2483
|
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Get the tooltip text for the screenshot button based on current state
|
|
2486
|
+
*/
|
|
2487
|
+
getScreenshotTooltip() {
|
|
2488
|
+
if (this.copiedToClipboard) {
|
|
2489
|
+
return 'Copied to clipboard!';
|
|
2490
|
+
}
|
|
2491
|
+
if (this.copiedPath) {
|
|
2492
|
+
return 'Path copied to clipboard!';
|
|
2493
|
+
}
|
|
2494
|
+
if (this.lastScreenshot) {
|
|
2495
|
+
return `Screenshot saved!\n${this.lastScreenshot}\n\nClick to copy path`;
|
|
2496
|
+
}
|
|
2497
|
+
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`;
|
|
2498
|
+
return this.sweetlinkConnected
|
|
2499
|
+
? baseTooltip
|
|
2500
|
+
: `${baseTooltip}\n\nWarning: Sweetlink not connected`;
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Get the tooltip text for the AI review button based on current state
|
|
2504
|
+
*/
|
|
2505
|
+
getAIReviewTooltip() {
|
|
2506
|
+
if (this.designReviewInProgress) {
|
|
2507
|
+
return 'AI Design Review in progress...';
|
|
2508
|
+
}
|
|
2509
|
+
if (this.designReviewError) {
|
|
2510
|
+
return `Design review failed:\n${this.designReviewError}`;
|
|
2511
|
+
}
|
|
2512
|
+
if (this.lastDesignReview) {
|
|
2513
|
+
return `Design review saved to:\n${this.lastDesignReview}`;
|
|
2514
|
+
}
|
|
2515
|
+
const baseTooltip = `AI Design Review\n\nCaptures screenshot and sends to\nClaude for design analysis.\n\nRequires ANTHROPIC_API_KEY.`;
|
|
2516
|
+
return this.sweetlinkConnected
|
|
2517
|
+
? baseTooltip
|
|
2518
|
+
: `${baseTooltip}\n\nWarning: Sweetlink not connected`;
|
|
2519
|
+
}
|
|
1838
2520
|
createAIReviewButton() {
|
|
1839
2521
|
const btn = document.createElement('button');
|
|
1840
2522
|
btn.type = 'button';
|
|
1841
2523
|
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' : ''}`;
|
|
2524
|
+
const tooltip = this.getAIReviewTooltip();
|
|
1849
2525
|
btn.setAttribute('data-tooltip', tooltip);
|
|
1850
2526
|
const hasError = !!this.designReviewError;
|
|
1851
2527
|
const isActive = this.designReviewInProgress || !!this.lastDesignReview || hasError;
|
|
@@ -1945,7 +2621,7 @@ export function initGlobalDevBar(options) {
|
|
|
1945
2621
|
const existing = getGlobalInstance();
|
|
1946
2622
|
if (existing) {
|
|
1947
2623
|
// Check if already initialized with same position - skip re-init during HMR
|
|
1948
|
-
const existingPosition = existing
|
|
2624
|
+
const existingPosition = existing.getPosition();
|
|
1949
2625
|
const newPosition = options?.position ?? 'bottom-left';
|
|
1950
2626
|
if (existingPosition === newPosition) {
|
|
1951
2627
|
return existing;
|