@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.
@@ -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 { MAX_CONSOLE_LOGS, DEVBAR_SCREENSHOT_QUALITY, MAX_RECONNECT_ATTEMPTS, BASE_RECONNECT_DELAY_MS, MAX_RECONNECT_DELAY_MS, WS_PORT, SCREENSHOT_NOTIFICATION_MS, CLIPBOARD_NOTIFICATION_MS, DESIGN_REVIEW_NOTIFICATION_MS, SCREENSHOT_BLUR_DELAY_MS, SCREENSHOT_SCALE, TAILWIND_BREAKPOINTS, BUTTON_COLORS, CATEGORY_COLORS, TOOLTIP_STYLES, COLORS, FONT_MONO, } from './constants.js';
12
- import { formatArgs, canvasToDataUrl, prepareForCapture, delay, copyCanvasToClipboard, } from './utils.js';
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 { createSvgIcon, getButtonStyles, createStyledButton, createModalOverlay, createModalBox, createModalHeader, createModalContent, createEmptyMessage, createInfoBox, } from './ui/index.js';
16
- const html2canvas = (html2canvasModule.default ?? html2canvasModule);
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) => { captureLog('log', args); capture.originalConsole.log(...args); };
48
- console.error = (...args) => { captureLog('error', args); capture.errorCount++; capture.originalConsole.error(...args); };
49
- console.warn = (...args) => { captureLog('warn', args); capture.warningCount++; capture.originalConsole.warn(...args); };
50
- console.info = (...args) => { captureLog('info', args); capture.originalConsole.info(...args); };
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 * Math.pow(2, this.reconnectAttempts);
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, { logging: false, useCORS: true, allowTaint: true });
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: { screenshot: canvas.toDataURL('image/png'), width: canvas.width, height: canvas.height, selector: command.selector || 'body' },
332
- timestamp: Date.now()
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 { tagName: el.tagName, className: el.className, id: el.id, textContent: el.textContent?.trim().slice(0, 100) };
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({ success: true, data: { count: results.length, results }, timestamp: Date.now() }));
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({ success: false, error: e instanceof Error ? e.message : 'Execution failed', timestamp: Date.now() }));
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(() => { this.lastScreenshot = null; this.render(); }, durationMs);
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(() => { this.lastDesignReview = null; this.render(); }, durationMs);
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(() => { this.lastOutline = null; this.render(); }, durationMs);
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(() => { this.lastSchema = null; this.render(); }, durationMs);
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
- let tailwindBreakpoint = 'base';
459
- if (width >= TAILWIND_BREAKPOINTS['2xl'].min)
460
- tailwindBreakpoint = '2xl';
461
- else if (width >= TAILWIND_BREAKPOINTS.xl.min)
462
- tailwindBreakpoint = 'xl';
463
- else if (width >= TAILWIND_BREAKPOINTS.lg.min)
464
- tailwindBreakpoint = 'lg';
465
- else if (width >= TAILWIND_BREAKPOINTS.md.min)
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.consoleFilter || this.showOutlineModal || this.showSchemaModal || this.showDesignReviewConfirm) {
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, { format: 'jpeg', quality: DEVBAR_SCREENSHOT_QUALITY });
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({ color: COLORS.textMuted, text: '×', padding: '0', fontSize: '1.25rem' });
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({ color: COLORS.textMuted, text: 'Cancel', padding: '8px 16px' });
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, { color: COLORS.textSecondary, fontWeight: '600', marginBottom: '8px' });
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, { color: COLORS.textMuted, fontSize: '0.6875rem', marginTop: '12px' });
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 = log.message.length > 500 ? log.message.slice(0, 500) + '...' : log.message;
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) + '...' : node.text;
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+)?)|([{}\[\],])|(\s+)/g;
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
- renderCollapsed() {
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
- // Calculate position so the collapsed dot aligns with where it appears in expanded state
1421
- // Expanded: left:80 + border:1 + padding:12 + half-indicator:6 = 99px horizontal center
1422
- // Expanded: bottom:20 + border:1 + padding:8 + half-row-height:11 = 40px vertical center (approx)
1423
- // Collapsed circle diameter: 26px, so offset by 13px from center
1424
- const collapsedPositions = {
1425
- 'bottom-left': { bottom: '27px', left: '86px' },
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 = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
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 ? 'Sweetlink connected (click to minimize)' : 'Sweetlink disconnected (click to minimize)');
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 = bp === 'sm'
1614
- ? `${bp} - ${this.breakpointInfo.dimensions.split('x')[0]}`
1615
- : `${bp} - ${this.breakpointInfo.dimensions}`;
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.copiedToClipboard
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.designReviewInProgress
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['options']?.position ?? 'bottom-left';
2936
+ const existingPosition = existing.getPosition();
1949
2937
  const newPosition = options?.position ?? 'bottom-left';
1950
2938
  if (existingPosition === newPosition) {
1951
2939
  return existing;