@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.
@@ -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 { 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, 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 { createSvgIcon, getButtonStyles, createStyledButton, createModalOverlay, createModalBox, createModalHeader, createModalContent, createEmptyMessage, createInfoBox, } from './ui/index.js';
16
- const html2canvas = (html2canvasModule.default ?? html2canvasModule);
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) => { 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); };
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 * Math.pow(2, this.reconnectAttempts);
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, { logging: false, useCORS: true, allowTaint: true });
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: { screenshot: canvas.toDataURL('image/png'), width: canvas.width, height: canvas.height, selector: command.selector || 'body' },
332
- timestamp: Date.now()
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 { tagName: el.tagName, className: el.className, id: el.id, textContent: el.textContent?.trim().slice(0, 100) };
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({ success: true, data: { count: results.length, results }, timestamp: Date.now() }));
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({ success: false, error: e instanceof Error ? e.message : 'Execution failed', timestamp: Date.now() }));
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(() => { this.lastScreenshot = null; this.render(); }, durationMs);
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(() => { this.lastDesignReview = null; this.render(); }, durationMs);
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(() => { this.lastOutline = null; this.render(); }, durationMs);
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(() => { this.lastSchema = null; this.render(); }, durationMs);
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
- 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';
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.consoleFilter || this.showOutlineModal || this.showSchemaModal || this.showDesignReviewConfirm) {
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, { format: 'jpeg', quality: DEVBAR_SCREENSHOT_QUALITY });
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({ color: COLORS.textMuted, text: '×', padding: '0', fontSize: '1.25rem' });
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({ color: COLORS.textMuted, text: 'Cancel', padding: '8px 16px' });
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, { color: COLORS.textSecondary, fontWeight: '600', marginBottom: '8px' });
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, { color: COLORS.textMuted, fontSize: '0.6875rem', marginTop: '12px' });
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 = log.message.length > 500 ? log.message.slice(0, 500) + '...' : log.message;
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) + '...' : node.text;
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+)?)|([{}\[\],])|(\s+)/g;
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
- renderCollapsed() {
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
- // 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%)' },
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 = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
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 ? 'Sweetlink connected (click to minimize)' : 'Sweetlink disconnected (click to minimize)');
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 = bp === 'sm'
1614
- ? `${bp} - ${this.breakpointInfo.dimensions.split('x')[0]}`
1615
- : `${bp} - ${this.breakpointInfo.dimensions}`;
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.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' : ''}`;
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.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' : ''}`;
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['options']?.position ?? 'bottom-left';
2624
+ const existingPosition = existing.getPosition();
1949
2625
  const newPosition = options?.position ?? 'bottom-left';
1950
2626
  if (existingPosition === newPosition) {
1951
2627
  return existing;