@ytspar/devbar 1.0.0-canary.c37df82 → 1.0.0-canary.c511f13

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