@ytspar/devbar 1.0.0-canary.bf42899 → 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,12 +8,14 @@
8
8
  * to avoid React dependency conflicts in host applications.
9
9
  */
10
10
  import * as html2canvasModule from 'html2canvas-pro';
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';
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
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 { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager, } from './settings.js';
15
16
  import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createStyledButton, createSvgIcon, getButtonStyles, } from './ui/index.js';
16
17
  import { canvasToDataUrl, copyCanvasToClipboard, delay, formatArgs, prepareForCapture, } from './utils.js';
18
+ export { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager } from './settings.js';
17
19
  const html2canvas = (html2canvasModule.default ??
18
20
  html2canvasModule);
19
21
  const earlyConsoleCapture = (() => {
@@ -89,6 +91,8 @@ export class GlobalDevBar {
89
91
  this.apiKeyStatus = null;
90
92
  this.lastOutline = null;
91
93
  this.lastSchema = null;
94
+ this.savingOutline = false;
95
+ this.savingSchema = false;
92
96
  this.consoleFilter = null;
93
97
  // Modal states
94
98
  this.showOutlineModal = false;
@@ -99,6 +103,9 @@ export class GlobalDevBar {
99
103
  this.clsValue = 0;
100
104
  this.inpValue = 0;
101
105
  this.reconnectAttempts = 0;
106
+ this.wsVerified = false;
107
+ this.serverProjectDir = null;
108
+ this.verificationTimeout = null;
102
109
  // Track the position of the connection indicator dot for smooth collapse
103
110
  this.lastDotPosition = null;
104
111
  this.reconnectTimeout = null;
@@ -128,6 +135,19 @@ export class GlobalDevBar {
128
135
  // Initialize debug config first so we can log during construction
129
136
  this.debugConfig = normalizeDebugConfig(options.debug);
130
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
+ }
131
151
  this.options = {
132
152
  position: options.position ?? 'bottom-left',
133
153
  accentColor: options.accentColor ?? COLORS.primary,
@@ -283,6 +303,8 @@ export class GlobalDevBar {
283
303
  this.reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // Prevent reconnection
284
304
  if (this.reconnectTimeout)
285
305
  clearTimeout(this.reconnectTimeout);
306
+ if (this.verificationTimeout)
307
+ clearTimeout(this.verificationTimeout);
286
308
  if (this.ws)
287
309
  this.ws.close();
288
310
  // Clear timeouts
@@ -341,22 +363,82 @@ export class GlobalDevBar {
341
363
  document.head.appendChild(style);
342
364
  }
343
365
  }
344
- connectWebSocket() {
366
+ connectWebSocket(port) {
345
367
  if (this.destroyed)
346
368
  return;
347
- this.debug.ws('Connecting to WebSocket', { port: WS_PORT });
348
- 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}`);
349
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);
350
388
  ws.onopen = () => {
351
- this.sweetlinkConnected = true;
352
- this.reconnectAttempts = 0;
353
- this.debug.ws('WebSocket connected');
389
+ this.debug.ws('WebSocket socket opened, awaiting server-info');
354
390
  ws.send(JSON.stringify({ type: 'browser-client-ready' }));
355
- this.render();
356
391
  };
357
392
  ws.onmessage = async (event) => {
358
393
  try {
359
- 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;
360
442
  this.debug.ws('Received command', { type: command.type });
361
443
  await this.handleSweetlinkCommand(command);
362
444
  }
@@ -365,15 +447,25 @@ export class GlobalDevBar {
365
447
  }
366
448
  };
367
449
  ws.onclose = () => {
368
- this.sweetlinkConnected = false;
369
- this.debug.ws('WebSocket disconnected');
370
- this.render();
371
- // Auto-reconnect with exponential backoff
372
- if (!this.destroyed && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
373
- const delayMs = BASE_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempts;
374
- this.reconnectAttempts++;
375
- this.debug.ws('Scheduling reconnect', { attempt: this.reconnectAttempts, delayMs });
376
- 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
+ }
377
469
  }
378
470
  };
379
471
  ws.onerror = () => {
@@ -500,8 +592,48 @@ export class GlobalDevBar {
500
592
  case 'schema-error':
501
593
  console.error('[GlobalDevBar] Schema save failed:', command.error);
502
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;
503
604
  }
504
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;
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();
636
+ }
505
637
  /**
506
638
  * Handle notification state updates with auto-clear timeout
507
639
  */
@@ -529,6 +661,7 @@ export class GlobalDevBar {
529
661
  }, durationMs);
530
662
  break;
531
663
  case 'outline':
664
+ this.savingOutline = false;
532
665
  this.lastOutline = path;
533
666
  if (this.outlineTimeout)
534
667
  clearTimeout(this.outlineTimeout);
@@ -538,6 +671,7 @@ export class GlobalDevBar {
538
671
  }, durationMs);
539
672
  break;
540
673
  case 'schema':
674
+ this.savingSchema = false;
541
675
  this.lastSchema = path;
542
676
  if (this.schemaTimeout)
543
677
  clearTimeout(this.schemaTimeout);
@@ -670,7 +804,11 @@ export class GlobalDevBar {
670
804
  }
671
805
  });
672
806
  // durationThreshold filters out very short interactions
673
- this.inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 16 });
807
+ this.inpObserver.observe({
808
+ type: 'event',
809
+ buffered: true,
810
+ durationThreshold: 16,
811
+ });
674
812
  }
675
813
  catch (e) {
676
814
  console.warn('[GlobalDevBar] INP PerformanceObserver not supported', e);
@@ -724,14 +862,17 @@ export class GlobalDevBar {
724
862
  window.addEventListener('keydown', this.keydownHandler);
725
863
  }
726
864
  setupTheme() {
727
- // Load stored theme preference
728
- this.themeMode = getStoredThemeMode();
865
+ // Load stored theme preference from settings manager
866
+ const settings = this.settingsManager.getSettings();
867
+ this.themeMode = settings.themeMode;
729
868
  this.debug.state('Theme loaded', { mode: this.themeMode });
730
869
  // Listen for system theme changes
731
870
  if (typeof window !== 'undefined' && window.matchMedia) {
732
871
  this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
733
872
  this.themeMediaHandler = () => {
734
873
  if (this.themeMode === 'system') {
874
+ // Re-inject theme CSS when system preference changes
875
+ injectThemeCSS(getTheme(this.themeMode));
735
876
  this.debug.state('System theme changed', {
736
877
  effectiveTheme: getEffectiveTheme(this.themeMode),
737
878
  });
@@ -742,10 +883,8 @@ export class GlobalDevBar {
742
883
  }
743
884
  }
744
885
  loadCompactMode() {
745
- if (typeof localStorage === 'undefined')
746
- return;
747
- const stored = localStorage.getItem(STORAGE_KEYS.compactMode);
748
- this.compactMode = stored === 'true';
886
+ const settings = this.settingsManager.getSettings();
887
+ this.compactMode = settings.compactMode;
749
888
  this.debug.state('Compact mode loaded', { compactMode: this.compactMode });
750
889
  }
751
890
  /**
@@ -759,7 +898,9 @@ export class GlobalDevBar {
759
898
  */
760
899
  setThemeMode(mode) {
761
900
  this.themeMode = mode;
762
- setStoredThemeMode(mode);
901
+ this.settingsManager.saveSettings({ themeMode: mode });
902
+ // Inject the appropriate theme CSS variables
903
+ injectThemeCSS(getTheme(mode));
763
904
  this.debug.state('Theme mode changed', { mode, effectiveTheme: getEffectiveTheme(mode) });
764
905
  this.render();
765
906
  }
@@ -774,9 +915,7 @@ export class GlobalDevBar {
774
915
  */
775
916
  toggleCompactMode() {
776
917
  this.compactMode = !this.compactMode;
777
- if (typeof localStorage !== 'undefined') {
778
- localStorage.setItem(STORAGE_KEYS.compactMode, String(this.compactMode));
779
- }
918
+ this.settingsManager.saveSettings({ compactMode: this.compactMode });
780
919
  this.debug.state('Compact mode toggled', { compactMode: this.compactMode });
781
920
  this.render();
782
921
  }
@@ -1015,9 +1154,13 @@ export class GlobalDevBar {
1015
1154
  this.render();
1016
1155
  }
1017
1156
  handleSaveOutline() {
1157
+ if (this.savingOutline)
1158
+ return; // Prevent repeated clicks
1018
1159
  const outline = extractDocumentOutline();
1019
1160
  const markdown = outlineToMarkdown(outline);
1020
1161
  if (this.ws?.readyState === WebSocket.OPEN) {
1162
+ this.savingOutline = true;
1163
+ this.render();
1021
1164
  this.ws.send(JSON.stringify({
1022
1165
  type: 'save-outline',
1023
1166
  data: {
@@ -1031,9 +1174,13 @@ export class GlobalDevBar {
1031
1174
  }
1032
1175
  }
1033
1176
  handleSaveSchema() {
1177
+ if (this.savingSchema)
1178
+ return; // Prevent repeated clicks
1034
1179
  const schema = extractPageSchema();
1035
1180
  const markdown = schemaToMarkdown(schema);
1036
1181
  if (this.ws?.readyState === WebSocket.OPEN) {
1182
+ this.savingSchema = true;
1183
+ this.render();
1037
1184
  this.ws.send(JSON.stringify({
1038
1185
  type: 'save-schema',
1039
1186
  data: {
@@ -1411,6 +1558,8 @@ export class GlobalDevBar {
1411
1558
  },
1412
1559
  onSave: () => this.handleSaveOutline(),
1413
1560
  sweetlinkConnected: this.sweetlinkConnected,
1561
+ isSaving: this.savingOutline,
1562
+ savedPath: this.lastOutline,
1414
1563
  });
1415
1564
  modal.appendChild(header);
1416
1565
  const content = createModalContent();
@@ -1497,6 +1646,8 @@ export class GlobalDevBar {
1497
1646
  },
1498
1647
  onSave: () => this.handleSaveSchema(),
1499
1648
  sweetlinkConnected: this.sweetlinkConnected,
1649
+ isSaving: this.savingSchema,
1650
+ savedPath: this.lastSchema,
1500
1651
  });
1501
1652
  modal.appendChild(header);
1502
1653
  const content = createModalContent();
@@ -1854,6 +2005,130 @@ export class GlobalDevBar {
1854
2005
  btn.appendChild(svg);
1855
2006
  return btn;
1856
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
+ }
1857
2132
  /**
1858
2133
  * Render the settings popover
1859
2134
  */
@@ -1876,7 +2151,10 @@ export class GlobalDevBar {
1876
2151
  boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${accentColor}33`,
1877
2152
  backdropFilter: 'blur(8px)',
1878
2153
  WebkitBackdropFilter: 'blur(8px)',
1879
- minWidth: '200px',
2154
+ minWidth: '240px',
2155
+ maxWidth: '280px',
2156
+ maxHeight: 'calc(100vh - 100px)',
2157
+ overflowY: 'auto',
1880
2158
  fontFamily: FONT_MONO,
1881
2159
  });
1882
2160
  // Header
@@ -1887,6 +2165,10 @@ export class GlobalDevBar {
1887
2165
  justifyContent: 'space-between',
1888
2166
  padding: '10px 14px',
1889
2167
  borderBottom: `1px solid ${accentColor}30`,
2168
+ position: 'sticky',
2169
+ top: '0',
2170
+ backgroundColor: 'rgba(17, 24, 39, 0.98)',
2171
+ zIndex: '1',
1890
2172
  });
1891
2173
  const title = document.createElement('span');
1892
2174
  Object.assign(title.style, { color: accentColor, fontSize: '0.75rem', fontWeight: '600' });
@@ -1905,19 +2187,8 @@ export class GlobalDevBar {
1905
2187
  };
1906
2188
  header.appendChild(closeBtn);
1907
2189
  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);
2190
+ // ========== THEME SECTION ==========
2191
+ const themeSection = this.createSettingsSection('Theme');
1921
2192
  const themeOptions = document.createElement('div');
1922
2193
  Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
1923
2194
  const themeModes = ['system', 'dark', 'light'];
@@ -1943,73 +2214,214 @@ export class GlobalDevBar {
1943
2214
  });
1944
2215
  themeSection.appendChild(themeOptions);
1945
2216
  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',
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',
1965
2227
  });
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`,
2228
+ posLabel.textContent = 'Position';
2229
+ positionRow.appendChild(posLabel);
2230
+ // Mini-map container
2231
+ const miniMap = document.createElement('div');
2232
+ Object.assign(miniMap.style, {
1979
2233
  position: 'relative',
1980
- cursor: 'pointer',
1981
- transition: 'all 150ms',
2234
+ width: '100%',
2235
+ height: '50px',
2236
+ backgroundColor: 'rgba(10, 15, 26, 0.6)',
2237
+ border: `1px solid ${color}30`,
2238
+ borderRadius: '4px',
1982
2239
  });
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',
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);
1993
2289
  });
1994
- toggle.appendChild(toggleKnob);
1995
- toggle.onclick = () => {
2290
+ positionRow.appendChild(miniMap);
2291
+ displaySection.appendChild(positionRow);
2292
+ // Compact mode toggle
2293
+ displaySection.appendChild(this.createToggleRow('Compact Mode', this.compactMode, accentColor, () => {
1996
2294
  this.toggleCompactMode();
1997
- };
1998
- compactRow.appendChild(toggle);
1999
- displaySection.appendChild(compactRow);
2295
+ }));
2000
2296
  // Keyboard shortcut hint
2001
2297
  const shortcutHint = document.createElement('div');
2002
2298
  Object.assign(shortcutHint.style, {
2003
2299
  color: COLORS.textMuted,
2004
2300
  fontSize: '0.5625rem',
2005
- marginTop: '6px',
2301
+ marginTop: '2px',
2302
+ marginBottom: '8px',
2006
2303
  });
2007
2304
  shortcutHint.textContent = 'Keyboard: Cmd+Shift+M';
2008
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);
2009
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);
2010
2414
  this.overlayElement = popover;
2011
2415
  document.body.appendChild(popover);
2012
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
+ }
2013
2425
  renderCollapsed() {
2014
2426
  if (!this.container)
2015
2427
  return;
@@ -2325,6 +2737,7 @@ export class GlobalDevBar {
2325
2737
  actionsContainer.appendChild(this.createOutlineButton());
2326
2738
  actionsContainer.appendChild(this.createSchemaButton());
2327
2739
  actionsContainer.appendChild(this.createSettingsButton());
2740
+ actionsContainer.appendChild(this.createCompactToggleButton());
2328
2741
  mainRow.appendChild(actionsContainer);
2329
2742
  wrapper.appendChild(mainRow);
2330
2743
  // Render custom controls row if there are any