@ytspar/devbar 1.0.0-canary.e1f65d0 → 1.0.0-canary.e666c79

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.
Files changed (122) hide show
  1. package/README.md +9 -0
  2. package/dist/GlobalDevBar.d.ts +131 -130
  3. package/dist/GlobalDevBar.d.ts.map +1 -0
  4. package/dist/GlobalDevBar.js +230 -1732
  5. package/dist/GlobalDevBar.js.map +1 -0
  6. package/dist/accessibility.d.ts +85 -0
  7. package/dist/accessibility.d.ts.map +1 -0
  8. package/dist/accessibility.js +156 -0
  9. package/dist/accessibility.js.map +1 -0
  10. package/dist/constants.d.ts +132 -9
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +201 -120
  13. package/dist/constants.js.map +1 -0
  14. package/dist/debug.d.ts +40 -0
  15. package/dist/debug.d.ts.map +1 -0
  16. package/dist/debug.js +94 -0
  17. package/dist/debug.js.map +1 -0
  18. package/dist/earlyConsoleCapture.d.ts +4 -31
  19. package/dist/earlyConsoleCapture.d.ts.map +1 -0
  20. package/dist/earlyConsoleCapture.js +4 -74
  21. package/dist/earlyConsoleCapture.js.map +1 -0
  22. package/dist/index.d.ts +14 -5
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +25 -8
  25. package/dist/index.js.map +1 -0
  26. package/dist/lazy/index.d.ts +7 -0
  27. package/dist/lazy/index.d.ts.map +1 -0
  28. package/dist/lazy/index.js +7 -0
  29. package/dist/lazy/index.js.map +1 -0
  30. package/dist/lazy/lazyHtml2Canvas.d.ts +34 -0
  31. package/dist/lazy/lazyHtml2Canvas.d.ts.map +1 -0
  32. package/dist/lazy/lazyHtml2Canvas.js +38 -0
  33. package/dist/lazy/lazyHtml2Canvas.js.map +1 -0
  34. package/dist/modules/index.d.ts +15 -0
  35. package/dist/modules/index.d.ts.map +1 -0
  36. package/dist/modules/index.js +14 -0
  37. package/dist/modules/index.js.map +1 -0
  38. package/dist/modules/keyboard.d.ts +15 -0
  39. package/dist/modules/keyboard.d.ts.map +1 -0
  40. package/dist/modules/keyboard.js +60 -0
  41. package/dist/modules/keyboard.js.map +1 -0
  42. package/dist/modules/performance.d.ts +26 -0
  43. package/dist/modules/performance.d.ts.map +1 -0
  44. package/dist/modules/performance.js +197 -0
  45. package/dist/modules/performance.js.map +1 -0
  46. package/dist/modules/rendering.d.ts +20 -0
  47. package/dist/modules/rendering.d.ts.map +1 -0
  48. package/dist/modules/rendering.js +2007 -0
  49. package/dist/modules/rendering.js.map +1 -0
  50. package/dist/modules/screenshot.d.ts +62 -0
  51. package/dist/modules/screenshot.d.ts.map +1 -0
  52. package/dist/modules/screenshot.js +352 -0
  53. package/dist/modules/screenshot.js.map +1 -0
  54. package/dist/modules/theme.d.ts +20 -0
  55. package/dist/modules/theme.d.ts.map +1 -0
  56. package/dist/modules/theme.js +60 -0
  57. package/dist/modules/theme.js.map +1 -0
  58. package/dist/modules/tooltips.d.ts +74 -0
  59. package/dist/modules/tooltips.d.ts.map +1 -0
  60. package/dist/modules/tooltips.js +553 -0
  61. package/dist/modules/tooltips.js.map +1 -0
  62. package/dist/modules/types.d.ts +118 -0
  63. package/dist/modules/types.d.ts.map +1 -0
  64. package/dist/modules/types.js +9 -0
  65. package/dist/modules/types.js.map +1 -0
  66. package/dist/modules/websocket.d.ts +16 -0
  67. package/dist/modules/websocket.d.ts.map +1 -0
  68. package/dist/modules/websocket.js +314 -0
  69. package/dist/modules/websocket.js.map +1 -0
  70. package/dist/network.d.ts +93 -0
  71. package/dist/network.d.ts.map +1 -0
  72. package/dist/network.js +175 -0
  73. package/dist/network.js.map +1 -0
  74. package/dist/outline.d.ts +1 -0
  75. package/dist/outline.d.ts.map +1 -0
  76. package/dist/outline.js +44 -10
  77. package/dist/outline.js.map +1 -0
  78. package/dist/presets.d.ts +58 -0
  79. package/dist/presets.d.ts.map +1 -0
  80. package/dist/presets.js +134 -0
  81. package/dist/presets.js.map +1 -0
  82. package/dist/schema.d.ts +1 -0
  83. package/dist/schema.d.ts.map +1 -0
  84. package/dist/schema.js +7 -6
  85. package/dist/schema.js.map +1 -0
  86. package/dist/settings.d.ts +164 -0
  87. package/dist/settings.d.ts.map +1 -0
  88. package/dist/settings.js +310 -0
  89. package/dist/settings.js.map +1 -0
  90. package/dist/storage.d.ts +84 -0
  91. package/dist/storage.d.ts.map +1 -0
  92. package/dist/storage.js +183 -0
  93. package/dist/storage.js.map +1 -0
  94. package/dist/types.d.ts +31 -3
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/types.js +3 -2
  97. package/dist/types.js.map +1 -0
  98. package/dist/ui/buttons.d.ts +3 -2
  99. package/dist/ui/buttons.d.ts.map +1 -0
  100. package/dist/ui/buttons.js +3 -2
  101. package/dist/ui/buttons.js.map +1 -0
  102. package/dist/ui/cards.d.ts +37 -0
  103. package/dist/ui/cards.d.ts.map +1 -0
  104. package/dist/ui/cards.js +134 -0
  105. package/dist/ui/cards.js.map +1 -0
  106. package/dist/ui/icons.d.ts +71 -2
  107. package/dist/ui/icons.d.ts.map +1 -0
  108. package/dist/ui/icons.js +148 -2
  109. package/dist/ui/icons.js.map +1 -0
  110. package/dist/ui/index.d.ts +5 -3
  111. package/dist/ui/index.d.ts.map +1 -0
  112. package/dist/ui/index.js +5 -3
  113. package/dist/ui/index.js.map +1 -0
  114. package/dist/ui/modals.d.ts +9 -2
  115. package/dist/ui/modals.d.ts.map +1 -0
  116. package/dist/ui/modals.js +70 -11
  117. package/dist/ui/modals.js.map +1 -0
  118. package/dist/utils.d.ts +8 -3
  119. package/dist/utils.d.ts.map +1 -0
  120. package/dist/utils.js +21 -3
  121. package/dist/utils.js.map +1 -0
  122. package/package.json +17 -14
@@ -4,135 +4,165 @@
4
4
  * A development toolbar that displays breakpoint info, performance stats,
5
5
  * console error/warning counts, and provides screenshot capabilities via Sweetlink.
6
6
  *
7
- * This is a vanilla JS replacement for the React-based GlobalDevBar component
8
- * to avoid React dependency conflicts in host applications.
7
+ * Framework-agnostic no React, Vue, or other framework dependencies.
8
+ *
9
+ * Implementation is split across focused modules in ./modules/:
10
+ * - websocket.ts — WebSocket connection, reconnection, port scanning, message handling
11
+ * - screenshot.ts — Screenshot capture, design review, clipboard operations
12
+ * - rendering.ts — renderBar(), renderConsolePopup(), renderModal(), all DOM creation
13
+ * - performance.ts — setupPerformanceMonitoring(), FCP/LCP/CLS/INP observers
14
+ * - theme.ts — setupTheme(), toggleTheme(), theme media query handling
15
+ * - keyboard.ts — setupKeyboardShortcuts(), handleKeydown()
16
+ * - tooltips.ts — tooltip creation, positioning, and management helpers
9
17
  */
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';
13
- import { extractDocumentOutline, outlineToMarkdown } from './outline.js';
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);
17
- const earlyConsoleCapture = (() => {
18
- const ssrFallback = {
19
- errorCount: 0,
20
- warningCount: 0,
21
- logs: [],
22
- originalConsole: null,
23
- isPatched: false
24
- };
25
- // Skip on server-side rendering
26
- if (typeof window === 'undefined')
27
- return ssrFallback;
28
- const capture = {
29
- errorCount: 0,
30
- warningCount: 0,
31
- logs: [],
32
- originalConsole: {
33
- log: console.log,
34
- error: console.error,
35
- warn: console.warn,
36
- info: console.info
37
- },
38
- isPatched: false
39
- };
40
- const captureLog = (level, args) => {
41
- capture.logs.push({ level, message: formatArgs(args), timestamp: Date.now() });
42
- if (capture.logs.length > MAX_CONSOLE_LOGS)
43
- capture.logs = capture.logs.slice(-MAX_CONSOLE_LOGS);
44
- };
45
- // Patch console immediately
46
- 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
- capture.isPatched = true;
52
- }
53
- return capture;
54
- })();
18
+ import { CSS_COLORS, DEVBAR_STYLES, getThemeColors, MAX_RECONNECT_ATTEMPTS, WS_PORT, WS_PORT_OFFSET, } from './constants.js';
19
+ import { DebugLogger, normalizeDebugConfig } from './debug.js';
20
+ import { getSettingsManager, } from './settings.js';
21
+ import { ConsoleCapture } from '@ytspar/sweetlink/browser/consoleCapture';
22
+ // Import module functions
23
+ import { connectWebSocket, handleNotification } from './modules/websocket.js';
24
+ import { handleScreenshot as moduleHandleScreenshot } from './modules/screenshot.js';
25
+ import { render as moduleRender } from './modules/rendering.js';
26
+ import { setupBreakpointDetection, setupPerformanceMonitoring, } from './modules/performance.js';
27
+ import { setupKeyboardShortcuts } from './modules/keyboard.js';
28
+ import { setupTheme, loadCompactMode, setThemeMode as moduleSetThemeMode, } from './modules/theme.js';
29
+ export { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager } from './settings.js';
30
+ // html2canvas is lazy-loaded via getHtml2Canvas() to avoid bundling ~400KB upfront
31
+ // ============================================================================
32
+ // Console Capture (single implementation from @ytspar/sweetlink)
33
+ // ============================================================================
34
+ const consoleCapture = new ConsoleCapture({ trackCounts: true });
35
+ consoleCapture.importEarlyLogs();
36
+ consoleCapture.start();
55
37
  // ============================================================================
56
38
  // GlobalDevBar Class
57
39
  // ============================================================================
58
40
  export class GlobalDevBar {
41
+ // Static storage for custom controls
42
+ static customControls = [];
43
+ // -- Public state exposed via DevBarState interface for modules --
44
+ options;
45
+ debugConfig;
46
+ debug;
47
+ container = null;
48
+ ws = null;
49
+ consoleLogs = [];
50
+ sweetlinkConnected = false;
51
+ collapsed = false;
52
+ capturing = false;
53
+ copiedToClipboard = false;
54
+ copiedPath = false;
55
+ lastScreenshot = null;
56
+ designReviewInProgress = false;
57
+ lastDesignReview = null;
58
+ designReviewError = null;
59
+ showDesignReviewConfirm = false;
60
+ apiKeyStatus = null;
61
+ lastOutline = null;
62
+ lastSchema = null;
63
+ savingOutline = false;
64
+ savingSchema = false;
65
+ consoleFilter = null;
66
+ savingConsoleLogs = false;
67
+ lastConsoleLogs = null;
68
+ consoleLogsTimeout;
69
+ // Modal states
70
+ showOutlineModal = false;
71
+ showSchemaModal = false;
72
+ // Track active HTML tooltips for cleanup on re-render
73
+ activeTooltips = new Set();
74
+ breakpointInfo = null;
75
+ perfStats = null;
76
+ lcpValue = null;
77
+ clsValue = 0;
78
+ inpValue = 0;
79
+ reconnectAttempts = 0;
80
+ // Port scanning state for multi-instance support
81
+ currentAppPort;
82
+ baseWsPort;
83
+ wsVerified = false;
84
+ serverProjectDir = null;
85
+ // Track the position of the connection indicator dot for smooth collapse
86
+ lastDotPosition = null;
87
+ reconnectTimeout = null;
88
+ screenshotTimeout = null;
89
+ copiedPathTimeout = null;
90
+ designReviewTimeout = null;
91
+ designReviewErrorTimeout = null;
92
+ outlineTimeout = null;
93
+ schemaTimeout = null;
94
+ resizeHandler = null;
95
+ keydownHandler = null;
96
+ fcpObserver = null;
97
+ lcpObserver = null;
98
+ clsObserver = null;
99
+ inpObserver = null;
100
+ destroyed = false;
101
+ // Theme state
102
+ themeMode = 'system';
103
+ themeMediaQuery = null;
104
+ themeMediaHandler = null;
105
+ // Compact mode state
106
+ compactMode = false;
107
+ // Settings popover state
108
+ showSettingsPopover = false;
109
+ // Overlay element for modals
110
+ overlayElement = null;
111
+ // Settings manager for persistence
112
+ settingsManager;
113
+ // Console log listener for real-time badge updates
114
+ logChangeListener = null;
59
115
  constructor(options = {}) {
60
- this.container = null;
61
- this.ws = null;
62
- this.consoleLogs = [];
63
- this.sweetlinkConnected = false;
64
- this.collapsed = false;
65
- this.capturing = false;
66
- this.copiedToClipboard = false;
67
- this.copiedPath = false;
68
- this.lastScreenshot = null;
69
- this.designReviewInProgress = false;
70
- this.lastDesignReview = null;
71
- this.designReviewError = null;
72
- this.showDesignReviewConfirm = false;
73
- this.apiKeyStatus = null;
74
- this.lastOutline = null;
75
- this.lastSchema = null;
76
- this.consoleFilter = null;
77
- // Modal states
78
- this.showOutlineModal = false;
79
- this.showSchemaModal = false;
80
- this.breakpointInfo = null;
81
- this.perfStats = null;
82
- this.lcpValue = null;
83
- this.reconnectAttempts = 0;
84
- this.reconnectTimeout = null;
85
- this.screenshotTimeout = null;
86
- this.copiedPathTimeout = null;
87
- this.designReviewTimeout = null;
88
- this.designReviewErrorTimeout = null;
89
- this.outlineTimeout = null;
90
- this.schemaTimeout = null;
91
- this.resizeHandler = null;
92
- this.keydownHandler = null;
93
- this.fcpObserver = null;
94
- this.lcpObserver = null;
95
- this.destroyed = false;
96
- // Overlay element for modals
97
- this.overlayElement = null;
116
+ // Initialize debug config first so we can log during construction
117
+ this.debugConfig = normalizeDebugConfig(options.debug);
118
+ this.debug = new DebugLogger(this.debugConfig);
119
+ // Initialize settings manager
120
+ this.settingsManager = getSettingsManager();
121
+ // Calculate app port from URL for multi-instance support
122
+ if (typeof window !== 'undefined') {
123
+ this.currentAppPort =
124
+ parseInt(window.location.port, 10) || (window.location.protocol === 'https:' ? 443 : 80);
125
+ // Calculate expected WS port (appPort + port offset) like SweetlinkBridge does
126
+ this.baseWsPort = this.currentAppPort > 0 ? this.currentAppPort + WS_PORT_OFFSET : WS_PORT;
127
+ }
128
+ else {
129
+ this.currentAppPort = 0;
130
+ this.baseWsPort = WS_PORT;
131
+ }
98
132
  this.options = {
99
133
  position: options.position ?? 'bottom-left',
100
- accentColor: options.accentColor ?? COLORS.primary,
134
+ accentColor: options.accentColor ?? CSS_COLORS.primary,
101
135
  showMetrics: {
102
136
  breakpoint: options.showMetrics?.breakpoint ?? true,
103
137
  fcp: options.showMetrics?.fcp ?? true,
104
138
  lcp: options.showMetrics?.lcp ?? true,
139
+ cls: options.showMetrics?.cls ?? true,
140
+ inp: options.showMetrics?.inp ?? true,
105
141
  pageSize: options.showMetrics?.pageSize ?? true,
106
142
  },
107
143
  showScreenshot: options.showScreenshot ?? true,
108
144
  showConsoleBadges: options.showConsoleBadges ?? true,
109
145
  showTooltips: options.showTooltips ?? true,
146
+ saveLocation: options.saveLocation ?? 'auto',
110
147
  sizeOverrides: options.sizeOverrides,
111
148
  };
149
+ this.debug.lifecycle('GlobalDevBar constructed', { options: this.options });
112
150
  }
113
151
  /**
114
- * Get tooltip class name(s) if tooltips are enabled, otherwise empty string
152
+ * Get current error, warning, and info counts from the log array
115
153
  */
116
- tooltipClass(direction = 'left', ...additionalClasses) {
117
- if (!this.options.showTooltips) {
118
- return additionalClasses.join(' ');
119
- }
120
- return ['devbar-tooltip', `devbar-tooltip-${direction}`, ...additionalClasses].join(' ');
154
+ getLogCounts() {
155
+ return {
156
+ errorCount: consoleCapture.getErrorCount(),
157
+ warningCount: consoleCapture.getWarningCount(),
158
+ infoCount: consoleCapture.getInfoCount(),
159
+ };
121
160
  }
122
161
  /**
123
- * Get current error and warning counts from the log array
162
+ * Reset position style properties on an element to clear stale values
124
163
  */
125
- getLogCounts() {
126
- const logs = earlyConsoleCapture.logs;
127
- let errorCount = 0;
128
- let warningCount = 0;
129
- for (const log of logs) {
130
- if (log.level === 'error')
131
- errorCount++;
132
- else if (log.level === 'warn')
133
- warningCount++;
134
- }
135
- return { errorCount, warningCount };
164
+ resetPositionStyles(element) {
165
+ Object.assign(element.style, { top: '', bottom: '', left: '', right: '', transform: '' });
136
166
  }
137
167
  /**
138
168
  * Create a collapsed count badge (used for error/warning counts in minimized state)
@@ -153,7 +183,7 @@ export class GlobalDevBar {
153
183
  fontWeight: '600',
154
184
  display: 'flex',
155
185
  alignItems: 'center',
156
- justifyContent: 'center'
186
+ justifyContent: 'center',
157
187
  });
158
188
  badge.textContent = count > 99 ? '!' : String(count);
159
189
  return badge;
@@ -166,7 +196,7 @@ export class GlobalDevBar {
166
196
  */
167
197
  static registerControl(control) {
168
198
  // Remove existing control with same ID
169
- GlobalDevBar.customControls = GlobalDevBar.customControls.filter(c => c.id !== control.id);
199
+ GlobalDevBar.customControls = GlobalDevBar.customControls.filter((c) => c.id !== control.id);
170
200
  GlobalDevBar.customControls.push(control);
171
201
  // Trigger re-render of all instances
172
202
  const instance = getGlobalInstance();
@@ -178,7 +208,7 @@ export class GlobalDevBar {
178
208
  * Unregister a custom control by ID
179
209
  */
180
210
  static unregisterControl(id) {
181
- GlobalDevBar.customControls = GlobalDevBar.customControls.filter(c => c.id !== id);
211
+ GlobalDevBar.customControls = GlobalDevBar.customControls.filter((c) => c.id !== id);
182
212
  // Trigger re-render of all instances
183
213
  const instance = getGlobalInstance();
184
214
  if (instance) {
@@ -209,25 +239,45 @@ export class GlobalDevBar {
209
239
  return;
210
240
  if (this.destroyed)
211
241
  return;
212
- // Inject tooltip styles
242
+ this.debug.lifecycle('Initializing DevBar');
243
+ // Inject animation and utility CSS
213
244
  this.injectStyles();
214
- // Copy early captured logs
215
- this.consoleLogs = [...earlyConsoleCapture.logs];
245
+ // Copy captured logs
246
+ this.consoleLogs = consoleCapture.getLogs();
247
+ this.debug.lifecycle('Copied console logs', { count: this.consoleLogs.length });
248
+ // Subscribe to log changes for real-time badge updates
249
+ this.logChangeListener = () => {
250
+ this.consoleLogs = consoleCapture.getLogs();
251
+ this.render();
252
+ };
253
+ consoleCapture.addListener(this.logChangeListener);
254
+ // Setup theme
255
+ setupTheme(this);
256
+ // Load compact mode from storage
257
+ loadCompactMode(this);
216
258
  // Setup WebSocket connection
217
259
  this.connectWebSocket();
218
260
  // Setup breakpoint detection
219
- this.setupBreakpointDetection();
261
+ setupBreakpointDetection(this);
220
262
  // Setup performance monitoring
221
- this.setupPerformanceMonitoring();
263
+ setupPerformanceMonitoring(this);
222
264
  // Setup keyboard shortcuts
223
- this.setupKeyboardShortcuts();
265
+ setupKeyboardShortcuts(this);
224
266
  // Initial render
225
267
  this.render();
268
+ this.debug.lifecycle('DevBar initialized successfully');
269
+ }
270
+ /**
271
+ * Get the current position
272
+ */
273
+ getPosition() {
274
+ return this.options.position;
226
275
  }
227
276
  /**
228
277
  * Destroy the devbar and cleanup
229
278
  */
230
279
  destroy() {
280
+ this.debug.lifecycle('Destroying DevBar');
231
281
  this.destroyed = true;
232
282
  // Close WebSocket
233
283
  this.reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // Prevent reconnection
@@ -246,6 +296,8 @@ export class GlobalDevBar {
246
296
  clearTimeout(this.outlineTimeout);
247
297
  if (this.schemaTimeout)
248
298
  clearTimeout(this.schemaTimeout);
299
+ if (this.consoleLogsTimeout)
300
+ clearTimeout(this.consoleLogsTimeout);
249
301
  // Remove event listeners
250
302
  if (this.resizeHandler)
251
303
  window.removeEventListener('resize', this.resizeHandler);
@@ -256,13 +308,21 @@ export class GlobalDevBar {
256
308
  this.fcpObserver.disconnect();
257
309
  if (this.lcpObserver)
258
310
  this.lcpObserver.disconnect();
259
- // Restore console
260
- if (earlyConsoleCapture.originalConsole) {
261
- console.log = earlyConsoleCapture.originalConsole.log;
262
- console.error = earlyConsoleCapture.originalConsole.error;
263
- console.warn = earlyConsoleCapture.originalConsole.warn;
264
- console.info = earlyConsoleCapture.originalConsole.info;
311
+ if (this.clsObserver)
312
+ this.clsObserver.disconnect();
313
+ if (this.inpObserver)
314
+ this.inpObserver.disconnect();
315
+ // Remove theme media listener
316
+ if (this.themeMediaQuery && this.themeMediaHandler) {
317
+ this.themeMediaQuery.removeEventListener('change', this.themeMediaHandler);
318
+ }
319
+ // Remove log change listener
320
+ if (this.logChangeListener) {
321
+ consoleCapture.removeListener(this.logChangeListener);
322
+ this.logChangeListener = null;
265
323
  }
324
+ // Restore console
325
+ consoleCapture.stop();
266
326
  // Remove DOM elements
267
327
  if (this.container) {
268
328
  this.container.remove();
@@ -272,1654 +332,91 @@ export class GlobalDevBar {
272
332
  this.overlayElement.remove();
273
333
  this.overlayElement = null;
274
334
  }
335
+ this.debug.lifecycle('DevBar destroyed');
275
336
  }
276
337
  injectStyles() {
277
- const styleId = 'devbar-tooltip-styles';
338
+ const styleId = 'devbar-styles';
278
339
  if (!document.getElementById(styleId)) {
279
340
  const style = document.createElement('style');
280
341
  style.id = styleId;
281
- style.textContent = TOOLTIP_STYLES;
342
+ style.textContent = DEVBAR_STYLES;
282
343
  document.head.appendChild(style);
283
344
  }
284
345
  }
285
- connectWebSocket() {
286
- if (this.destroyed)
287
- return;
288
- const ws = new WebSocket(`ws://localhost:${WS_PORT}`);
289
- this.ws = ws;
290
- ws.onopen = () => {
291
- this.sweetlinkConnected = true;
292
- this.reconnectAttempts = 0;
293
- ws.send(JSON.stringify({ type: 'browser-client-ready' }));
294
- this.render();
295
- };
296
- ws.onmessage = async (event) => {
297
- try {
298
- const command = JSON.parse(event.data);
299
- await this.handleSweetlinkCommand(command);
300
- }
301
- catch (e) {
302
- console.error('[GlobalDevBar] Error handling command:', e);
303
- }
304
- };
305
- 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));
313
- }
314
- };
315
- ws.onerror = () => {
316
- // Error will trigger onclose, which handles reconnection
317
- };
346
+ // Delegate to module functions, binding `this` as state
347
+ connectWebSocket(port) {
348
+ connectWebSocket(this, port);
318
349
  }
319
- async handleSweetlinkCommand(command) {
320
- const ws = this.ws;
321
- if (!ws || ws.readyState !== WebSocket.OPEN)
322
- return;
323
- switch (command.type) {
324
- case 'screenshot': {
325
- const targetElement = command.selector
326
- ? document.querySelector(command.selector) || document.body
327
- : document.body;
328
- const canvas = await html2canvas(targetElement, { logging: false, useCORS: true, allowTaint: true });
329
- ws.send(JSON.stringify({
330
- success: true,
331
- data: { screenshot: canvas.toDataURL('image/png'), width: canvas.width, height: canvas.height, selector: command.selector || 'body' },
332
- timestamp: Date.now()
333
- }));
334
- break;
335
- }
336
- case 'get-logs': {
337
- let logs = this.consoleLogs;
338
- if (command.filter) {
339
- const filter = command.filter.toLowerCase();
340
- logs = logs.filter(log => log.level.includes(filter) || log.message.toLowerCase().includes(filter));
341
- }
342
- ws.send(JSON.stringify({ success: true, data: logs, timestamp: Date.now() }));
343
- break;
344
- }
345
- case 'query-dom': {
346
- if (command.selector) {
347
- const elements = Array.from(document.querySelectorAll(command.selector));
348
- const results = elements.map((el) => {
349
- if (command.property)
350
- return el[command.property] ?? null;
351
- return { tagName: el.tagName, className: el.className, id: el.id, textContent: el.textContent?.trim().slice(0, 100) };
352
- });
353
- ws.send(JSON.stringify({ success: true, data: { count: results.length, results }, timestamp: Date.now() }));
354
- }
355
- break;
356
- }
357
- case 'exec-js': {
358
- if (command.code) {
359
- try {
360
- // Use indirect eval to avoid strict mode issues
361
- const indirectEval = eval;
362
- const result = indirectEval(command.code);
363
- ws.send(JSON.stringify({ success: true, data: result, timestamp: Date.now() }));
364
- }
365
- catch (e) {
366
- ws.send(JSON.stringify({ success: false, error: e instanceof Error ? e.message : 'Execution failed', timestamp: Date.now() }));
367
- }
368
- }
369
- break;
370
- }
371
- case 'screenshot-saved':
372
- this.handleNotification('screenshot', command.path, SCREENSHOT_NOTIFICATION_MS);
373
- break;
374
- case 'design-review-saved':
375
- this.designReviewInProgress = false;
376
- this.handleNotification('designReview', command.reviewPath, DESIGN_REVIEW_NOTIFICATION_MS);
377
- break;
378
- case 'design-review-error':
379
- this.designReviewInProgress = false;
380
- this.designReviewError = command.error || 'Unknown error';
381
- console.error('[GlobalDevBar] Design review failed:', command.error);
382
- // Clear error after notification duration
383
- if (this.designReviewErrorTimeout)
384
- clearTimeout(this.designReviewErrorTimeout);
385
- this.designReviewErrorTimeout = setTimeout(() => {
386
- this.designReviewError = null;
387
- this.render();
388
- }, DESIGN_REVIEW_NOTIFICATION_MS);
389
- this.render();
390
- break;
391
- case 'api-key-status': {
392
- // Properties are at top level of the response
393
- const response = command;
394
- this.apiKeyStatus = {
395
- configured: response.configured ?? false,
396
- maskedKey: response.maskedKey,
397
- model: response.model,
398
- pricing: response.pricing,
399
- };
400
- // Re-render to update the confirmation modal
401
- this.render();
402
- break;
403
- }
404
- case 'outline-saved':
405
- this.handleNotification('outline', command.outlinePath, SCREENSHOT_NOTIFICATION_MS);
406
- break;
407
- case 'outline-error':
408
- console.error('[GlobalDevBar] Outline save failed:', command.error);
409
- break;
410
- case 'schema-saved':
411
- this.handleNotification('schema', command.schemaPath, SCREENSHOT_NOTIFICATION_MS);
412
- break;
413
- case 'schema-error':
414
- console.error('[GlobalDevBar] Schema save failed:', command.error);
415
- break;
416
- }
417
- }
418
- /**
419
- * Handle notification state updates with auto-clear timeout
420
- */
421
350
  handleNotification(type, path, durationMs) {
422
- if (!path)
423
- return;
424
- // Update the appropriate state
425
- switch (type) {
426
- case 'screenshot':
427
- this.lastScreenshot = path;
428
- if (this.screenshotTimeout)
429
- clearTimeout(this.screenshotTimeout);
430
- this.screenshotTimeout = setTimeout(() => { this.lastScreenshot = null; this.render(); }, durationMs);
431
- break;
432
- case 'designReview':
433
- this.lastDesignReview = path;
434
- if (this.designReviewTimeout)
435
- clearTimeout(this.designReviewTimeout);
436
- this.designReviewTimeout = setTimeout(() => { this.lastDesignReview = null; this.render(); }, durationMs);
437
- break;
438
- case 'outline':
439
- this.lastOutline = path;
440
- if (this.outlineTimeout)
441
- clearTimeout(this.outlineTimeout);
442
- this.outlineTimeout = setTimeout(() => { this.lastOutline = null; this.render(); }, durationMs);
443
- break;
444
- case 'schema':
445
- this.lastSchema = path;
446
- if (this.schemaTimeout)
447
- clearTimeout(this.schemaTimeout);
448
- this.schemaTimeout = setTimeout(() => { this.lastSchema = null; this.render(); }, durationMs);
449
- break;
450
- }
451
- this.render();
452
- }
453
- setupBreakpointDetection() {
454
- const updateBreakpointInfo = () => {
455
- const width = window.innerWidth;
456
- const height = window.innerHeight;
457
- // 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';
469
- this.breakpointInfo = {
470
- tailwindBreakpoint,
471
- dimensions: `${width}x${height}`
472
- };
473
- this.render();
474
- };
475
- updateBreakpointInfo();
476
- this.resizeHandler = updateBreakpointInfo;
477
- window.addEventListener('resize', this.resizeHandler);
478
- }
479
- setupPerformanceMonitoring() {
480
- const updatePerfStats = () => {
481
- // FCP
482
- const paintEntries = performance.getEntriesByType('paint');
483
- const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
484
- const fcp = fcpEntry ? `${Math.round(fcpEntry.startTime)}ms` : '-';
485
- // LCP (from cached value, updated by observer)
486
- const lcp = this.lcpValue !== null ? `${Math.round(this.lcpValue)}ms` : '-';
487
- // Total Resource Size
488
- const resources = performance.getEntriesByType('resource');
489
- let totalBytes = 0;
490
- const navEntry = performance.getEntriesByType('navigation')[0];
491
- if (navEntry) {
492
- totalBytes += navEntry.transferSize || 0;
493
- }
494
- resources.forEach((entry) => {
495
- const resourceEntry = entry;
496
- totalBytes += resourceEntry.transferSize || 0;
497
- });
498
- const totalSize = totalBytes > 1024 * 1024
499
- ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB`
500
- : `${Math.round(totalBytes / 1024)} KB`;
501
- this.perfStats = { fcp, lcp, totalSize };
502
- this.render();
503
- };
504
- if (document.readyState === 'complete') {
505
- setTimeout(updatePerfStats, 100);
506
- }
507
- else {
508
- window.addEventListener('load', () => setTimeout(updatePerfStats, 100));
509
- }
510
- // FCP Observer
511
- try {
512
- this.fcpObserver = new PerformanceObserver((list) => {
513
- const entries = list.getEntries();
514
- entries.forEach((entry) => {
515
- if (entry.name === 'first-contentful-paint') {
516
- updatePerfStats();
517
- }
518
- });
519
- });
520
- this.fcpObserver.observe({ type: 'paint', buffered: true });
521
- }
522
- catch (e) {
523
- console.warn('[GlobalDevBar] FCP PerformanceObserver not supported', e);
524
- }
525
- // LCP Observer
526
- try {
527
- this.lcpObserver = new PerformanceObserver((list) => {
528
- const entries = list.getEntries();
529
- const lastEntry = entries[entries.length - 1];
530
- if (lastEntry) {
531
- this.lcpValue = lastEntry.startTime;
532
- updatePerfStats();
533
- }
534
- });
535
- this.lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
536
- }
537
- catch (e) {
538
- console.warn('[GlobalDevBar] LCP PerformanceObserver not supported', e);
539
- }
540
- }
541
- setupKeyboardShortcuts() {
542
- this.keydownHandler = (e) => {
543
- // Close modals on Escape
544
- if (e.key === 'Escape') {
545
- if (this.consoleFilter || this.showOutlineModal || this.showSchemaModal || this.showDesignReviewConfirm) {
546
- this.consoleFilter = null;
547
- this.showOutlineModal = false;
548
- this.showSchemaModal = false;
549
- this.showDesignReviewConfirm = false;
550
- this.render();
551
- return;
552
- }
553
- }
554
- if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
555
- if (e.key === 'S' || e.key === 's') {
556
- e.preventDefault();
557
- if (this.sweetlinkConnected && !this.capturing) {
558
- this.handleScreenshot(false);
559
- }
560
- }
561
- else if (e.key === 'C' || e.key === 'c') {
562
- const selection = window.getSelection();
563
- if (!selection || selection.toString().length === 0) {
564
- e.preventDefault();
565
- if (!this.capturing) {
566
- this.handleScreenshot(true);
567
- }
568
- }
569
- }
570
- }
571
- };
572
- window.addEventListener('keydown', this.keydownHandler);
573
- }
574
- async copyPathToClipboard(path) {
575
- try {
576
- await navigator.clipboard.writeText(path);
577
- this.copiedPath = true;
578
- if (this.copiedPathTimeout)
579
- clearTimeout(this.copiedPathTimeout);
580
- this.copiedPathTimeout = setTimeout(() => {
581
- this.copiedPath = false;
582
- this.render();
583
- }, CLIPBOARD_NOTIFICATION_MS);
584
- this.render();
585
- }
586
- catch (error) {
587
- console.error('[GlobalDevBar] Failed to copy path:', error);
588
- }
589
- }
590
- async handleScreenshot(copyToClipboard = false) {
591
- if (this.capturing)
592
- return;
593
- if (!copyToClipboard && !this.sweetlinkConnected)
594
- return;
595
- let cleanup = null;
596
- try {
597
- this.capturing = true;
598
- this.render();
599
- cleanup = prepareForCapture();
600
- await delay(SCREENSHOT_BLUR_DELAY_MS);
601
- const canvas = await html2canvas(document.body, {
602
- logging: false,
603
- useCORS: true,
604
- allowTaint: true,
605
- scale: SCREENSHOT_SCALE,
606
- width: window.innerWidth,
607
- windowWidth: window.innerWidth
608
- });
609
- // Restore page state
610
- cleanup();
611
- cleanup = null;
612
- if (copyToClipboard) {
613
- try {
614
- await copyCanvasToClipboard(canvas);
615
- this.copiedToClipboard = true;
616
- this.render();
617
- if (this.screenshotTimeout)
618
- clearTimeout(this.screenshotTimeout);
619
- this.screenshotTimeout = setTimeout(() => {
620
- this.copiedToClipboard = false;
621
- this.render();
622
- }, CLIPBOARD_NOTIFICATION_MS);
623
- }
624
- catch (e) {
625
- console.error('[GlobalDevBar] Failed to copy to clipboard:', e);
626
- }
627
- }
628
- else {
629
- const dataUrl = canvasToDataUrl(canvas, { format: 'jpeg', quality: DEVBAR_SCREENSHOT_QUALITY });
630
- if (this.ws?.readyState === WebSocket.OPEN) {
631
- this.ws.send(JSON.stringify({
632
- type: 'save-screenshot',
633
- data: {
634
- screenshot: dataUrl,
635
- width: canvas.width,
636
- height: canvas.height,
637
- logs: this.consoleLogs,
638
- url: window.location.href,
639
- timestamp: Date.now()
640
- }
641
- }));
642
- }
643
- }
644
- }
645
- catch (e) {
646
- console.error('[GlobalDevBar] Screenshot failed:', e);
647
- if (cleanup)
648
- cleanup();
649
- }
650
- finally {
651
- this.capturing = false;
652
- this.render();
653
- }
654
- }
655
- async handleDesignReview() {
656
- if (this.designReviewInProgress || !this.sweetlinkConnected)
657
- return;
658
- let cleanup = null;
659
- try {
660
- this.designReviewInProgress = true;
661
- this.designReviewError = null; // Clear any previous error
662
- if (this.designReviewErrorTimeout) {
663
- clearTimeout(this.designReviewErrorTimeout);
664
- this.designReviewErrorTimeout = null;
665
- }
666
- this.render();
667
- cleanup = prepareForCapture();
668
- await delay(SCREENSHOT_BLUR_DELAY_MS);
669
- const canvas = await html2canvas(document.body, {
670
- logging: false,
671
- useCORS: true,
672
- allowTaint: true,
673
- scale: 1, // Full quality for design review
674
- width: window.innerWidth,
675
- windowWidth: window.innerWidth
676
- });
677
- // Restore page state
678
- cleanup();
679
- cleanup = null;
680
- const dataUrl = canvasToDataUrl(canvas, { format: 'png' });
681
- if (this.ws?.readyState === WebSocket.OPEN) {
682
- this.ws.send(JSON.stringify({
683
- type: 'design-review-screenshot',
684
- data: {
685
- screenshot: dataUrl,
686
- width: canvas.width,
687
- height: canvas.height,
688
- logs: this.consoleLogs,
689
- url: window.location.href,
690
- timestamp: Date.now()
691
- }
692
- }));
693
- }
694
- }
695
- catch (e) {
696
- console.error('[GlobalDevBar] Design review failed:', e);
697
- if (cleanup)
698
- cleanup();
699
- this.designReviewInProgress = false;
700
- this.render();
701
- }
351
+ handleNotification(this, type, path, durationMs);
702
352
  }
703
353
  /**
704
- * Show the design review confirmation modal
705
- * Checks API key status first
354
+ * Apply settings to the DevBar state and options
706
355
  */
707
- showDesignReviewConfirmation() {
708
- if (!this.sweetlinkConnected)
709
- return;
710
- // Request API key status from server
711
- if (this.ws?.readyState === WebSocket.OPEN) {
712
- this.ws.send(JSON.stringify({ type: 'check-api-key' }));
713
- }
714
- // Show the confirmation modal
715
- this.showDesignReviewConfirm = true;
716
- this.showOutlineModal = false;
717
- this.showSchemaModal = false;
718
- this.consoleFilter = null;
356
+ applySettings(settings) {
357
+ // Update local state
358
+ this.themeMode = settings.themeMode;
359
+ this.compactMode = settings.compactMode;
360
+ // Update options
361
+ this.options.position = settings.position;
362
+ this.options.accentColor = settings.accentColor;
363
+ this.options.showScreenshot = settings.showScreenshot;
364
+ this.options.showConsoleBadges = settings.showConsoleBadges;
365
+ this.options.showTooltips = settings.showTooltips;
366
+ this.options.saveLocation = settings.saveLocation;
367
+ this.options.showMetrics = { ...settings.showMetrics };
368
+ // Re-render with new settings
719
369
  this.render();
720
370
  }
721
- /**
722
- * Calculate estimated cost for design review based on viewport size
723
- */
724
- calculateCostEstimate() {
725
- if (!this.apiKeyStatus?.pricing)
726
- return null;
727
- // Image token estimation for Claude Vision:
728
- // Images are resized to fit within a bounding box, then tokenized
729
- // Rough estimate: ~1 token per 1.5x1.5 pixels, or (width * height) / 750
730
- const width = window.innerWidth;
731
- const height = window.innerHeight;
732
- const imageTokens = Math.ceil((width * height) / 750);
733
- // Prompt is ~500 tokens, output up to 2048 tokens
734
- const promptTokens = 500;
735
- const estimatedOutputTokens = 1500; // Conservative estimate
736
- const totalInputTokens = imageTokens + promptTokens;
737
- const { input: inputPrice, output: outputPrice } = this.apiKeyStatus.pricing;
738
- const inputCost = (totalInputTokens / 1000000) * inputPrice;
739
- const outputCost = (estimatedOutputTokens / 1000000) * outputPrice;
740
- const totalCost = inputCost + outputCost;
741
- return {
742
- tokens: totalInputTokens + estimatedOutputTokens,
743
- cost: totalCost < 0.01 ? '<$0.01' : `~$${totalCost.toFixed(2)}`,
744
- };
745
- }
746
- /**
747
- * Close the design review confirmation modal
748
- */
749
- closeDesignReviewConfirm() {
750
- this.showDesignReviewConfirm = false;
751
- this.apiKeyStatus = null; // Reset so it's re-fetched next time
752
- this.render();
753
- }
754
- /**
755
- * Proceed with design review after confirmation
756
- */
757
- proceedWithDesignReview() {
758
- this.showDesignReviewConfirm = false;
759
- this.handleDesignReview();
760
- }
761
- handleDocumentOutline() {
762
- // Toggle outline modal
763
- this.showOutlineModal = !this.showOutlineModal;
764
- this.showSchemaModal = false;
765
- this.consoleFilter = null;
766
- this.render();
767
- }
768
- handlePageSchema() {
769
- // Toggle schema modal
770
- this.showSchemaModal = !this.showSchemaModal;
771
- this.showOutlineModal = false;
772
- this.consoleFilter = null;
773
- this.render();
774
- }
775
- handleSaveOutline() {
776
- const outline = extractDocumentOutline();
777
- const markdown = outlineToMarkdown(outline);
778
- if (this.ws?.readyState === WebSocket.OPEN) {
779
- this.ws.send(JSON.stringify({
780
- type: 'save-outline',
781
- data: {
782
- outline,
783
- markdown,
784
- url: window.location.href,
785
- title: document.title,
786
- timestamp: Date.now()
787
- }
788
- }));
789
- }
790
- }
791
- handleSaveSchema() {
792
- const schema = extractPageSchema();
793
- const markdown = schemaToMarkdown(schema);
794
- if (this.ws?.readyState === WebSocket.OPEN) {
795
- this.ws.send(JSON.stringify({
796
- type: 'save-schema',
797
- data: {
798
- schema,
799
- markdown,
800
- url: window.location.href,
801
- title: document.title,
802
- timestamp: Date.now()
803
- }
804
- }));
805
- }
806
- }
807
371
  clearConsoleLogs() {
808
- // Clear the logs array
809
- earlyConsoleCapture.logs = [];
810
- earlyConsoleCapture.errorCount = 0;
811
- earlyConsoleCapture.warningCount = 0;
372
+ consoleCapture.clear();
812
373
  this.consoleLogs = [];
813
374
  this.consoleFilter = null;
814
375
  this.render();
815
376
  }
816
- // ============================================================================
817
- // Render Methods (Using Safe DOM Methods)
818
- // ============================================================================
819
- render() {
820
- if (this.destroyed)
821
- return;
822
- if (typeof document === 'undefined')
823
- return;
824
- // Remove existing container if any
825
- if (this.container) {
826
- this.container.remove();
827
- }
828
- // Create new container
829
- this.container = document.createElement('div');
830
- this.container.setAttribute('data-devbar', 'true');
831
- if (this.collapsed) {
832
- this.renderCollapsed();
833
- }
834
- else {
835
- this.renderExpanded();
836
- }
837
- document.body.appendChild(this.container);
838
- // Render overlays/modals
839
- this.renderOverlays();
840
- }
841
- renderOverlays() {
842
- // Remove existing overlay
843
- if (this.overlayElement) {
844
- this.overlayElement.remove();
845
- this.overlayElement = null;
846
- }
847
- // Render console popup if filter is active
848
- if (this.consoleFilter) {
849
- this.renderConsolePopup();
850
- }
851
- // Render outline modal
852
- if (this.showOutlineModal) {
853
- this.renderOutlineModal();
854
- }
855
- // Render schema modal
856
- if (this.showSchemaModal) {
857
- this.renderSchemaModal();
858
- }
859
- // Render design review confirmation modal
860
- if (this.showDesignReviewConfirm) {
861
- this.renderDesignReviewConfirmModal();
862
- }
863
- }
864
- renderDesignReviewConfirmModal() {
865
- const color = BUTTON_COLORS.review;
866
- const closeModal = () => this.closeDesignReviewConfirm();
867
- const overlay = createModalOverlay(closeModal);
868
- // Override z-index for this modal to be above others
869
- overlay.style.zIndex = '10003';
870
- const modal = createModalBox(color);
871
- modal.style.maxWidth = '450px';
872
- // Header with title and close button
873
- const header = document.createElement('div');
874
- Object.assign(header.style, {
875
- display: 'flex',
876
- alignItems: 'center',
877
- justifyContent: 'space-between',
878
- padding: '14px 18px',
879
- borderBottom: `1px solid ${color}40`,
880
- backgroundColor: `${color}15`,
881
- });
882
- const title = document.createElement('span');
883
- Object.assign(title.style, { color, fontSize: '0.875rem', fontWeight: '600' });
884
- title.textContent = 'AI Design Review';
885
- header.appendChild(title);
886
- const closeBtn = createStyledButton({ color: COLORS.textMuted, text: '×', padding: '0', fontSize: '1.25rem' });
887
- closeBtn.style.border = 'none';
888
- closeBtn.onclick = closeModal;
889
- header.appendChild(closeBtn);
890
- modal.appendChild(header);
891
- // Content
892
- const content = document.createElement('div');
893
- Object.assign(content.style, {
894
- padding: '18px',
895
- color: COLORS.text,
896
- fontSize: '0.8125rem',
897
- lineHeight: '1.6',
898
- });
899
- if (this.apiKeyStatus === null) {
900
- content.appendChild(createEmptyMessage('Checking API key configuration...'));
901
- }
902
- else if (!this.apiKeyStatus.configured) {
903
- content.appendChild(this.renderApiKeyNotConfiguredContent());
904
- }
905
- else {
906
- content.appendChild(this.renderApiKeyConfiguredContent());
907
- }
908
- modal.appendChild(content);
909
- // Footer with buttons
910
- const footer = document.createElement('div');
911
- Object.assign(footer.style, {
912
- display: 'flex',
913
- justifyContent: 'flex-end',
914
- gap: '10px',
915
- padding: '14px 18px',
916
- borderTop: `1px solid ${COLORS.border}`,
917
- });
918
- const cancelBtn = createStyledButton({ color: COLORS.textMuted, text: 'Cancel', padding: '8px 16px' });
919
- cancelBtn.onclick = closeModal;
920
- footer.appendChild(cancelBtn);
921
- if (this.apiKeyStatus?.configured) {
922
- const proceedBtn = createStyledButton({ color, text: 'Run Review', padding: '8px 16px' });
923
- proceedBtn.style.backgroundColor = `${color}20`;
924
- proceedBtn.onclick = () => this.proceedWithDesignReview();
925
- footer.appendChild(proceedBtn);
926
- }
927
- modal.appendChild(footer);
928
- overlay.appendChild(modal);
929
- document.body.appendChild(overlay);
930
- }
931
- /**
932
- * Render content when API key is not configured
933
- */
934
- renderApiKeyNotConfiguredContent() {
935
- const wrapper = document.createElement('div');
936
- wrapper.appendChild(createInfoBox(COLORS.error, 'API Key Not Configured', 'The ANTHROPIC_API_KEY environment variable is not set.'));
937
- // Instructions
938
- const instructions = document.createElement('div');
939
- Object.assign(instructions.style, { marginBottom: '12px' });
940
- const instructTitle = document.createElement('div');
941
- Object.assign(instructTitle.style, { color: COLORS.textSecondary, fontWeight: '600', marginBottom: '8px' });
942
- instructTitle.textContent = 'To configure:';
943
- instructions.appendChild(instructTitle);
944
- const steps = [
945
- { text: '1. Get an API key from console.anthropic.com', highlight: false },
946
- { text: '2. Add to your .env file:', highlight: false },
947
- { text: ' ANTHROPIC_API_KEY=sk-ant-...', highlight: true },
948
- { text: '3. Restart your dev server', highlight: false },
949
- ];
950
- steps.forEach(({ text, highlight }) => {
951
- const stepDiv = document.createElement('div');
952
- Object.assign(stepDiv.style, {
953
- color: highlight ? COLORS.primary : COLORS.textMuted,
954
- fontSize: '0.75rem',
955
- marginBottom: '4px',
956
- fontFamily: FONT_MONO,
957
- });
958
- stepDiv.textContent = text;
959
- instructions.appendChild(stepDiv);
960
- });
961
- wrapper.appendChild(instructions);
962
- return wrapper;
963
- }
964
- /**
965
- * Render content when API key is configured (cost estimate and model info)
966
- */
967
- renderApiKeyConfiguredContent() {
968
- const wrapper = document.createElement('div');
969
- Object.assign(wrapper.style, { marginBottom: '16px' });
970
- const desc = document.createElement('p');
971
- Object.assign(desc.style, { color: COLORS.textSecondary, marginBottom: '12px' });
972
- desc.textContent = 'This will capture a screenshot and send it to Claude for design analysis.';
973
- wrapper.appendChild(desc);
974
- // Cost estimate
975
- const estimate = this.calculateCostEstimate();
976
- if (estimate) {
977
- const costBox = createInfoBox(COLORS.primary, 'Estimated Cost', []);
978
- // Remove default margin and adjust padding
979
- costBox.style.marginBottom = '0';
980
- costBox.style.padding = '12px';
981
- const costDetails = document.createElement('div');
982
- Object.assign(costDetails.style, {
983
- display: 'flex',
984
- justifyContent: 'space-between',
985
- color: COLORS.textSecondary,
986
- fontSize: '0.75rem',
987
- });
988
- const tokensSpan = document.createElement('span');
989
- tokensSpan.textContent = `~${estimate.tokens.toLocaleString()} tokens`;
990
- costDetails.appendChild(tokensSpan);
991
- const priceSpan = document.createElement('span');
992
- Object.assign(priceSpan.style, { color: COLORS.warning, fontWeight: '600' });
993
- priceSpan.textContent = estimate.cost;
994
- costDetails.appendChild(priceSpan);
995
- costBox.appendChild(costDetails);
996
- wrapper.appendChild(costBox);
997
- }
998
- // Model info
999
- if (this.apiKeyStatus?.model) {
1000
- const modelDiv = document.createElement('div');
1001
- Object.assign(modelDiv.style, { color: COLORS.textMuted, fontSize: '0.6875rem', marginTop: '12px' });
1002
- modelDiv.textContent = `Model: ${this.apiKeyStatus.model}`;
1003
- if (this.apiKeyStatus.maskedKey) {
1004
- modelDiv.textContent += ` | Key: ${this.apiKeyStatus.maskedKey}`;
1005
- }
1006
- wrapper.appendChild(modelDiv);
1007
- }
1008
- return wrapper;
1009
- }
1010
- renderConsolePopup() {
1011
- const filterType = this.consoleFilter;
1012
- if (!filterType)
1013
- return;
1014
- const logs = earlyConsoleCapture.logs.filter(log => log.level === filterType);
1015
- const color = filterType === 'error' ? BUTTON_COLORS.error : BUTTON_COLORS.warning;
1016
- const label = filterType === 'error' ? 'Errors' : 'Warnings';
1017
- const popup = document.createElement('div');
1018
- popup.setAttribute('data-devbar', 'true');
1019
- Object.assign(popup.style, {
1020
- position: 'fixed',
1021
- bottom: '60px',
1022
- left: '80px',
1023
- zIndex: '10002',
1024
- backgroundColor: 'rgba(17, 24, 39, 0.98)',
1025
- border: `1px solid ${color}`,
1026
- borderRadius: '8px',
1027
- boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${color}33`,
1028
- backdropFilter: 'blur(8px)',
1029
- WebkitBackdropFilter: 'blur(8px)',
1030
- minWidth: '400px',
1031
- maxWidth: '600px',
1032
- maxHeight: '400px',
1033
- display: 'flex',
1034
- flexDirection: 'column',
1035
- fontFamily: FONT_MONO,
1036
- });
1037
- // Header
1038
- const header = document.createElement('div');
1039
- Object.assign(header.style, {
1040
- display: 'flex',
1041
- alignItems: 'center',
1042
- justifyContent: 'space-between',
1043
- padding: '10px 14px',
1044
- borderBottom: `1px solid ${color}40`,
1045
- });
1046
- const title = document.createElement('span');
1047
- Object.assign(title.style, { color, fontSize: '0.8125rem', fontWeight: '600' });
1048
- title.textContent = `Console ${label} (${logs.length})`;
1049
- header.appendChild(title);
1050
- const headerButtons = document.createElement('div');
1051
- Object.assign(headerButtons.style, { display: 'flex', gap: '8px' });
1052
- // Clear button
1053
- const clearBtn = createStyledButton({
1054
- color,
1055
- text: 'Clear All',
1056
- padding: '4px 10px',
1057
- borderRadius: '4px',
1058
- fontSize: '0.6875rem',
1059
- });
1060
- clearBtn.onclick = () => this.clearConsoleLogs();
1061
- headerButtons.appendChild(clearBtn);
1062
- // Close button - match Clear button padding for consistent height
1063
- const closeBtn = createStyledButton({
1064
- color,
1065
- text: '×',
1066
- padding: '4px 8px',
1067
- borderRadius: '4px',
1068
- fontSize: '0.75rem',
1069
- });
1070
- closeBtn.onclick = () => {
1071
- this.consoleFilter = null;
1072
- this.render();
1073
- };
1074
- headerButtons.appendChild(closeBtn);
1075
- header.appendChild(headerButtons);
1076
- popup.appendChild(header);
1077
- // Content
1078
- const content = document.createElement('div');
1079
- Object.assign(content.style, { flex: '1', overflow: 'auto', padding: '8px 0' });
1080
- if (logs.length === 0) {
1081
- const emptyMsg = document.createElement('div');
1082
- Object.assign(emptyMsg.style, {
1083
- padding: '20px',
1084
- textAlign: 'center',
1085
- color: COLORS.textMuted,
1086
- fontSize: '0.75rem',
1087
- });
1088
- emptyMsg.textContent = `No ${filterType}s recorded`;
1089
- content.appendChild(emptyMsg);
1090
- }
1091
- else {
1092
- this.renderConsoleLogs(content, logs, color);
1093
- }
1094
- popup.appendChild(content);
1095
- this.overlayElement = popup;
1096
- document.body.appendChild(popup);
377
+ handleScreenshot(copyToClipboard) {
378
+ return moduleHandleScreenshot(this, copyToClipboard);
1097
379
  }
1098
380
  /**
1099
- * Render console log items into a container
381
+ * Get the current theme mode
1100
382
  */
1101
- renderConsoleLogs(container, logs, color) {
1102
- logs.forEach((log, index) => {
1103
- const logItem = document.createElement('div');
1104
- Object.assign(logItem.style, {
1105
- padding: '8px 14px',
1106
- borderBottom: index < logs.length - 1 ? '1px solid rgba(255, 255, 255, 0.05)' : 'none',
1107
- });
1108
- const timestamp = document.createElement('span');
1109
- Object.assign(timestamp.style, {
1110
- color: COLORS.textMuted,
1111
- fontSize: '0.625rem',
1112
- marginRight: '8px',
1113
- });
1114
- timestamp.textContent = new Date(log.timestamp).toLocaleTimeString();
1115
- logItem.appendChild(timestamp);
1116
- const message = document.createElement('span');
1117
- Object.assign(message.style, {
1118
- color,
1119
- fontSize: '0.6875rem',
1120
- wordBreak: 'break-word',
1121
- whiteSpace: 'pre-wrap',
1122
- });
1123
- message.textContent = log.message.length > 500 ? log.message.slice(0, 500) + '...' : log.message;
1124
- logItem.appendChild(message);
1125
- container.appendChild(logItem);
1126
- });
1127
- }
1128
- renderOutlineModal() {
1129
- const outline = extractDocumentOutline();
1130
- const color = BUTTON_COLORS.outline;
1131
- const closeModal = () => {
1132
- this.showOutlineModal = false;
1133
- this.render();
1134
- };
1135
- const overlay = createModalOverlay(closeModal);
1136
- const modal = createModalBox(color);
1137
- const header = createModalHeader({
1138
- color,
1139
- title: 'Document Outline',
1140
- onClose: closeModal,
1141
- onCopyMd: async () => {
1142
- const markdown = outlineToMarkdown(outline);
1143
- await navigator.clipboard.writeText(markdown);
1144
- },
1145
- onSave: () => this.handleSaveOutline(),
1146
- sweetlinkConnected: this.sweetlinkConnected,
1147
- });
1148
- modal.appendChild(header);
1149
- const content = createModalContent();
1150
- if (outline.length === 0) {
1151
- content.appendChild(createEmptyMessage('No semantic elements found in this document'));
1152
- }
1153
- else {
1154
- this.renderOutlineNodes(outline, content, 0);
1155
- }
1156
- modal.appendChild(content);
1157
- overlay.appendChild(modal);
1158
- this.overlayElement = overlay;
1159
- document.body.appendChild(overlay);
383
+ getThemeMode() {
384
+ return this.themeMode;
1160
385
  }
1161
386
  /**
1162
- * Recursively render outline nodes into a container element
387
+ * Set the theme mode
1163
388
  */
1164
- renderOutlineNodes(nodes, parentEl, depth) {
1165
- for (const node of nodes) {
1166
- const nodeEl = document.createElement('div');
1167
- Object.assign(nodeEl.style, {
1168
- padding: `4px 0 4px ${depth * 16}px`,
1169
- });
1170
- const tagSpan = document.createElement('span');
1171
- const categoryColor = CATEGORY_COLORS[node.category || 'other'] || CATEGORY_COLORS.other;
1172
- Object.assign(tagSpan.style, {
1173
- color: categoryColor,
1174
- fontSize: '0.6875rem',
1175
- fontWeight: '500',
1176
- });
1177
- tagSpan.textContent = `<${node.tagName}>`;
1178
- nodeEl.appendChild(tagSpan);
1179
- if (node.category) {
1180
- const categorySpan = document.createElement('span');
1181
- Object.assign(categorySpan.style, {
1182
- color: COLORS.textMuted,
1183
- fontSize: '0.625rem',
1184
- marginLeft: '6px',
1185
- });
1186
- categorySpan.textContent = `[${node.category}]`;
1187
- nodeEl.appendChild(categorySpan);
1188
- }
1189
- const textSpan = document.createElement('span');
1190
- Object.assign(textSpan.style, {
1191
- color: '#d1d5db',
1192
- fontSize: '0.6875rem',
1193
- marginLeft: '8px',
1194
- });
1195
- const truncatedText = node.text.length > 60 ? node.text.slice(0, 60) + '...' : node.text;
1196
- textSpan.textContent = truncatedText;
1197
- nodeEl.appendChild(textSpan);
1198
- if (node.id) {
1199
- const idSpan = document.createElement('span');
1200
- Object.assign(idSpan.style, {
1201
- color: '#9ca3af',
1202
- fontSize: '0.625rem',
1203
- marginLeft: '6px',
1204
- });
1205
- idSpan.textContent = `#${node.id}`;
1206
- nodeEl.appendChild(idSpan);
1207
- }
1208
- parentEl.appendChild(nodeEl);
1209
- if (node.children.length > 0) {
1210
- this.renderOutlineNodes(node.children, parentEl, depth + 1);
1211
- }
1212
- }
1213
- }
1214
- renderSchemaModal() {
1215
- const schema = extractPageSchema();
1216
- const color = BUTTON_COLORS.schema;
1217
- const closeModal = () => {
1218
- this.showSchemaModal = false;
1219
- this.render();
1220
- };
1221
- const overlay = createModalOverlay(closeModal);
1222
- const modal = createModalBox(color);
1223
- const header = createModalHeader({
1224
- color,
1225
- title: 'Page Schema',
1226
- onClose: closeModal,
1227
- onCopyMd: async () => {
1228
- const markdown = schemaToMarkdown(schema);
1229
- await navigator.clipboard.writeText(markdown);
1230
- },
1231
- onSave: () => this.handleSaveSchema(),
1232
- sweetlinkConnected: this.sweetlinkConnected,
1233
- });
1234
- modal.appendChild(header);
1235
- const content = createModalContent();
1236
- const hasContent = schema.jsonLd.length > 0 ||
1237
- Object.keys(schema.openGraph).length > 0 ||
1238
- Object.keys(schema.twitter).length > 0 ||
1239
- Object.keys(schema.metaTags).length > 0;
1240
- if (!hasContent) {
1241
- content.appendChild(createEmptyMessage('No structured data found on this page'));
1242
- }
1243
- else {
1244
- this.renderSchemaSection(content, 'JSON-LD', schema.jsonLd, color);
1245
- this.renderSchemaSection(content, 'Open Graph', schema.openGraph, COLORS.info);
1246
- this.renderSchemaSection(content, 'Twitter Cards', schema.twitter, COLORS.cyan);
1247
- this.renderSchemaSection(content, 'Meta Tags', schema.metaTags, COLORS.textMuted);
1248
- }
1249
- modal.appendChild(content);
1250
- overlay.appendChild(modal);
1251
- this.overlayElement = overlay;
1252
- document.body.appendChild(overlay);
389
+ setThemeMode(mode) {
390
+ moduleSetThemeMode(this, mode);
1253
391
  }
1254
392
  /**
1255
- * Render a section of schema data (either array or key-value object)
393
+ * Get the current effective theme colors
1256
394
  */
1257
- renderSchemaSection(container, title, items, color) {
1258
- const isEmpty = Array.isArray(items) ? items.length === 0 : Object.keys(items).length === 0;
1259
- if (isEmpty)
1260
- return;
1261
- const section = document.createElement('div');
1262
- section.style.marginBottom = '20px';
1263
- const sectionTitle = document.createElement('h3');
1264
- Object.assign(sectionTitle.style, {
1265
- color,
1266
- fontSize: '0.8125rem',
1267
- fontWeight: '600',
1268
- marginBottom: '10px',
1269
- borderBottom: `1px solid ${color}40`,
1270
- paddingBottom: '6px',
1271
- });
1272
- sectionTitle.textContent = title;
1273
- section.appendChild(sectionTitle);
1274
- if (Array.isArray(items)) {
1275
- this.renderJsonLdItems(section, items);
1276
- }
1277
- else {
1278
- this.renderKeyValueItems(section, items);
1279
- }
1280
- container.appendChild(section);
1281
- }
1282
- /**
1283
- * Render JSON-LD items as formatted code blocks with syntax highlighting
1284
- */
1285
- renderJsonLdItems(container, items) {
1286
- items.forEach((item, i) => {
1287
- const itemEl = document.createElement('div');
1288
- itemEl.style.marginBottom = '10px';
1289
- const itemTitle = document.createElement('div');
1290
- Object.assign(itemTitle.style, {
1291
- color: '#9ca3af',
1292
- fontSize: '0.6875rem',
1293
- marginBottom: '4px',
1294
- });
1295
- itemTitle.textContent = `Schema ${i + 1}`;
1296
- itemEl.appendChild(itemTitle);
1297
- const codeEl = document.createElement('pre');
1298
- Object.assign(codeEl.style, {
1299
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
1300
- borderRadius: '4px',
1301
- padding: '10px',
1302
- overflow: 'auto',
1303
- fontSize: '0.625rem',
1304
- margin: '0',
1305
- maxHeight: '300px', // Taller for more content
1306
- });
1307
- // Syntax highlight the JSON using DOM methods for safety
1308
- this.appendHighlightedJson(codeEl, JSON.stringify(item, null, 2));
1309
- itemEl.appendChild(codeEl);
1310
- container.appendChild(itemEl);
1311
- });
395
+ getColors() {
396
+ return getThemeColors(this.themeMode);
1312
397
  }
1313
398
  /**
1314
- * Append syntax-highlighted JSON to an element using safe DOM methods
1315
- * Uses textContent for all text to prevent XSS
399
+ * Toggle compact mode
1316
400
  */
1317
- appendHighlightedJson(container, json) {
1318
- // Color map for different token types
1319
- const colors = {
1320
- key: COLORS.primary, // green
1321
- string: COLORS.warning, // amber/yellow
1322
- number: COLORS.purple, // purple
1323
- boolean: COLORS.info, // blue
1324
- nullVal: COLORS.error, // red
1325
- punct: COLORS.textMuted, // gray
1326
- };
1327
- // 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;
1329
- for (const match of json.matchAll(tokenPattern)) {
1330
- const [, str, colon, bool, nullToken, num, punct, whitespace] = match;
1331
- if (whitespace) {
1332
- container.appendChild(document.createTextNode(whitespace));
1333
- }
1334
- else if (str !== undefined) {
1335
- const span = document.createElement('span');
1336
- span.style.color = colon ? colors.key : colors.string;
1337
- span.textContent = str;
1338
- container.appendChild(span);
1339
- if (colon) {
1340
- const colonSpan = document.createElement('span');
1341
- colonSpan.style.color = colors.punct;
1342
- colonSpan.textContent = ':';
1343
- container.appendChild(colonSpan);
1344
- }
1345
- }
1346
- else if (bool) {
1347
- const span = document.createElement('span');
1348
- span.style.color = colors.boolean;
1349
- span.textContent = bool;
1350
- container.appendChild(span);
1351
- }
1352
- else if (nullToken) {
1353
- const span = document.createElement('span');
1354
- span.style.color = colors.nullVal;
1355
- span.textContent = nullToken;
1356
- container.appendChild(span);
1357
- }
1358
- else if (num) {
1359
- const span = document.createElement('span');
1360
- span.style.color = colors.number;
1361
- span.textContent = num;
1362
- container.appendChild(span);
1363
- }
1364
- else if (punct) {
1365
- const span = document.createElement('span');
1366
- span.style.color = colors.punct;
1367
- span.textContent = punct;
1368
- container.appendChild(span);
1369
- }
1370
- }
1371
- }
1372
- /**
1373
- * Render key-value pairs as rows with ellipsis overflow and hover tooltip
1374
- */
1375
- renderKeyValueItems(container, items) {
1376
- for (const [key, value] of Object.entries(items)) {
1377
- const row = document.createElement('div');
1378
- Object.assign(row.style, {
1379
- display: 'flex',
1380
- marginBottom: '4px',
1381
- alignItems: 'flex-start',
1382
- });
1383
- const keyEl = document.createElement('span');
1384
- Object.assign(keyEl.style, {
1385
- color: '#9ca3af',
1386
- fontSize: '0.6875rem',
1387
- width: '120px',
1388
- minWidth: '120px',
1389
- maxWidth: '120px',
1390
- flexShrink: '0',
1391
- overflow: 'hidden',
1392
- textOverflow: 'ellipsis',
1393
- whiteSpace: 'nowrap',
1394
- });
1395
- keyEl.textContent = key;
1396
- // Show full key on hover if it might be truncated
1397
- if (key.length > 18) {
1398
- keyEl.title = key;
1399
- }
1400
- row.appendChild(keyEl);
1401
- const valueEl = document.createElement('span');
1402
- const strValue = String(value);
1403
- Object.assign(valueEl.style, {
1404
- color: '#d1d5db',
1405
- fontSize: '0.6875rem',
1406
- flex: '1',
1407
- wordBreak: 'break-word',
1408
- whiteSpace: 'pre-wrap',
1409
- });
1410
- valueEl.textContent = strValue;
1411
- row.appendChild(valueEl);
1412
- container.appendChild(row);
1413
- }
1414
- }
1415
- renderCollapsed() {
1416
- if (!this.container)
1417
- return;
1418
- const { position, accentColor } = this.options;
1419
- 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%)' },
1430
- };
1431
- const posStyle = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
1432
- const wrapper = this.container;
1433
- wrapper.className = this.tooltipClass('left', 'devbar-collapse');
1434
- wrapper.setAttribute('data-tooltip', `Click to expand DevBar${this.sweetlinkConnected ? ' (Sweetlink connected)' : ' (Sweetlink not connected)'}${errorCount > 0 ? `\n${errorCount} console error${errorCount === 1 ? '' : 's'}` : ''}`);
1435
- // Reset position properties first to avoid stale values
1436
- wrapper.style.top = '';
1437
- wrapper.style.bottom = '';
1438
- wrapper.style.left = '';
1439
- wrapper.style.right = '';
1440
- wrapper.style.transform = '';
1441
- Object.assign(wrapper.style, {
1442
- position: 'fixed',
1443
- ...posStyle,
1444
- zIndex: '9999',
1445
- backgroundColor: 'rgba(17, 24, 39, 0.95)',
1446
- border: `1px solid ${accentColor}`,
1447
- borderRadius: '50%',
1448
- color: accentColor,
1449
- boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
1450
- backdropFilter: 'blur(8px)',
1451
- WebkitBackdropFilter: 'blur(8px)',
1452
- cursor: 'pointer',
1453
- display: 'flex',
1454
- alignItems: 'center',
1455
- justifyContent: 'center',
1456
- width: '26px',
1457
- height: '26px',
1458
- boxSizing: 'border-box',
1459
- animation: 'devbar-collapse 150ms ease-out'
1460
- });
1461
- wrapper.onclick = () => {
1462
- this.collapsed = false;
1463
- this.render();
1464
- };
1465
- // Connection indicator dot (same size as in expanded state)
1466
- const dot = document.createElement('span');
1467
- Object.assign(dot.style, {
1468
- width: '6px',
1469
- height: '6px',
1470
- borderRadius: '50%',
1471
- backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
1472
- boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none'
1473
- });
1474
- wrapper.appendChild(dot);
1475
- // Error badge (absolute, top-right of circle, shifted left if warning badge exists)
1476
- if (errorCount > 0) {
1477
- wrapper.appendChild(this.createCollapsedBadge(errorCount, 'rgba(239, 68, 68, 0.95)', warningCount > 0 ? '12px' : '-6px'));
1478
- }
1479
- // Warning badge (absolute, top-right)
1480
- if (warningCount > 0) {
1481
- wrapper.appendChild(this.createCollapsedBadge(warningCount, 'rgba(245, 158, 11, 0.95)', '-6px'));
1482
- }
1483
- }
1484
- renderExpanded() {
1485
- if (!this.container)
1486
- return;
1487
- const { position, accentColor, showMetrics, showScreenshot, showConsoleBadges } = this.options;
1488
- const { errorCount, warningCount } = this.getLogCounts();
1489
- const positionStyles = {
1490
- 'bottom-left': { bottom: '20px', left: '80px' },
1491
- 'bottom-right': { bottom: '20px', right: '16px' },
1492
- 'top-left': { top: '20px', left: '80px' },
1493
- 'top-right': { top: '20px', right: '16px' },
1494
- 'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
1495
- };
1496
- const posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
1497
- const isCentered = position === 'bottom-center';
1498
- const sizeOverrides = this.options.sizeOverrides;
1499
- const wrapper = this.container;
1500
- // Reset position properties first to avoid stale values from previous renders
1501
- wrapper.style.top = '';
1502
- wrapper.style.bottom = '';
1503
- wrapper.style.left = '';
1504
- wrapper.style.right = '';
1505
- wrapper.style.transform = '';
1506
- // Calculate size values with overrides or defaults
1507
- // Width always fit-content, maxWidth prevents overlap with other dev bars
1508
- // BASE breakpoint (<640px) wraps buttons to centered second row via CSS
1509
- const defaultWidth = 'fit-content';
1510
- const defaultMinWidth = 'auto';
1511
- const defaultMaxWidth = isCentered ? 'calc(100vw - 140px)' : 'calc(100vw - 32px)';
1512
- Object.assign(wrapper.style, {
1513
- position: 'fixed',
1514
- ...posStyle,
1515
- zIndex: '9999',
1516
- backgroundColor: 'rgba(17, 24, 39, 0.95)',
1517
- border: `1px solid ${accentColor}`,
1518
- borderRadius: '12px',
1519
- color: accentColor,
1520
- boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
1521
- backdropFilter: 'blur(8px)',
1522
- WebkitBackdropFilter: 'blur(8px)',
1523
- boxSizing: 'border-box',
1524
- width: sizeOverrides?.width ?? defaultWidth,
1525
- maxWidth: sizeOverrides?.maxWidth ?? defaultMaxWidth,
1526
- minWidth: sizeOverrides?.minWidth ?? defaultMinWidth,
1527
- cursor: 'default'
1528
- });
1529
- wrapper.ondblclick = () => {
1530
- this.collapsed = true;
1531
- this.render();
1532
- };
1533
- // Main row - wrapping controlled by CSS media query
1534
- const mainRow = document.createElement('div');
1535
- mainRow.className = 'devbar-main';
1536
- Object.assign(mainRow.style, {
1537
- display: 'flex',
1538
- alignItems: 'center',
1539
- alignContent: 'flex-start',
1540
- justifyContent: 'flex-start',
1541
- gap: '0.5rem',
1542
- padding: '0.5rem 0.75rem',
1543
- minWidth: '0',
1544
- boxSizing: 'border-box',
1545
- fontFamily: FONT_MONO,
1546
- fontSize: '0.6875rem',
1547
- lineHeight: '1rem'
1548
- });
1549
- // Connection indicator (click to collapse)
1550
- const connIndicator = document.createElement('span');
1551
- connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
1552
- connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected ? 'Sweetlink connected (click to minimize)' : 'Sweetlink disconnected (click to minimize)');
1553
- Object.assign(connIndicator.style, {
1554
- width: '12px',
1555
- height: '12px',
1556
- borderRadius: '50%',
1557
- backgroundColor: 'transparent',
1558
- display: 'flex',
1559
- alignItems: 'center',
1560
- justifyContent: 'center',
1561
- cursor: 'pointer',
1562
- flexShrink: '0'
1563
- });
1564
- connIndicator.onclick = (e) => {
1565
- e.stopPropagation();
1566
- this.collapsed = true;
1567
- this.render();
1568
- };
1569
- const connDot = document.createElement('span');
1570
- Object.assign(connDot.style, {
1571
- width: '6px',
1572
- height: '6px',
1573
- borderRadius: '50%',
1574
- backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
1575
- boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
1576
- transition: 'all 300ms'
1577
- });
1578
- connIndicator.appendChild(connDot);
1579
- // Status row wrapper - keeps connection dot, info, and badges together
1580
- const statusRow = document.createElement('div');
1581
- statusRow.className = 'devbar-status';
1582
- Object.assign(statusRow.style, {
1583
- display: 'flex',
1584
- alignItems: 'center',
1585
- gap: '0.5rem',
1586
- flexWrap: 'nowrap',
1587
- flexShrink: '0'
1588
- });
1589
- statusRow.appendChild(connIndicator);
1590
- // Info section
1591
- const infoSection = document.createElement('div');
1592
- infoSection.className = 'devbar-info';
1593
- Object.assign(infoSection.style, {
1594
- display: 'flex',
1595
- alignItems: 'center',
1596
- gap: '0.5rem',
1597
- textTransform: 'uppercase',
1598
- letterSpacing: '0.05em',
1599
- flexShrink: '1',
1600
- minWidth: '0',
1601
- overflow: 'visible'
1602
- });
1603
- // Breakpoint info
1604
- if (showMetrics.breakpoint && this.breakpointInfo) {
1605
- const bp = this.breakpointInfo.tailwindBreakpoint;
1606
- const breakpointData = TAILWIND_BREAKPOINTS[bp];
1607
- const bpSpan = document.createElement('span');
1608
- bpSpan.className = this.tooltipClass('left', 'devbar-item');
1609
- Object.assign(bpSpan.style, { opacity: '0.9', cursor: 'default' });
1610
- 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
- let bpText = bp;
1612
- if (bp !== 'base') {
1613
- bpText = bp === 'sm'
1614
- ? `${bp} - ${this.breakpointInfo.dimensions.split('x')[0]}`
1615
- : `${bp} - ${this.breakpointInfo.dimensions}`;
1616
- }
1617
- bpSpan.textContent = bpText;
1618
- infoSection.appendChild(bpSpan);
1619
- }
1620
- // Performance stats
1621
- if (this.perfStats) {
1622
- const addSeparator = () => {
1623
- const sep = document.createElement('span');
1624
- sep.style.opacity = '0.4';
1625
- sep.textContent = '|';
1626
- infoSection.appendChild(sep);
1627
- };
1628
- if (showMetrics.fcp) {
1629
- addSeparator();
1630
- const fcpSpan = document.createElement('span');
1631
- fcpSpan.className = this.tooltipClass('left', 'devbar-item');
1632
- Object.assign(fcpSpan.style, { opacity: '0.85', cursor: 'default' });
1633
- fcpSpan.setAttribute('data-tooltip', 'First Contentful Paint (FCP): Time until first text/image renders.\n\nGood: <1.8s\nNeeds work: 1.8-3s\nPoor: >3s');
1634
- fcpSpan.textContent = `FCP ${this.perfStats.fcp}`;
1635
- infoSection.appendChild(fcpSpan);
1636
- }
1637
- if (showMetrics.lcp) {
1638
- addSeparator();
1639
- const lcpSpan = document.createElement('span');
1640
- lcpSpan.className = this.tooltipClass('left', 'devbar-item');
1641
- Object.assign(lcpSpan.style, { opacity: '0.85', cursor: 'default' });
1642
- lcpSpan.setAttribute('data-tooltip', 'Largest Contentful Paint (LCP): Time until largest visible element renders.\n\nGood: <2.5s\nNeeds work: 2.5-4s\nPoor: >4s');
1643
- lcpSpan.textContent = `LCP ${this.perfStats.lcp}`;
1644
- infoSection.appendChild(lcpSpan);
1645
- }
1646
- if (showMetrics.pageSize) {
1647
- addSeparator();
1648
- const sizeSpan = document.createElement('span');
1649
- sizeSpan.className = this.tooltipClass('left', 'devbar-item');
1650
- Object.assign(sizeSpan.style, { opacity: '0.7', cursor: 'default' });
1651
- sizeSpan.setAttribute('data-tooltip', 'Total page size (compressed/transferred).\nIncludes HTML, CSS, JS, images, and other resources.');
1652
- sizeSpan.textContent = this.perfStats.totalSize;
1653
- infoSection.appendChild(sizeSpan);
1654
- }
1655
- }
1656
- statusRow.appendChild(infoSection);
1657
- // Console badges - add to status row so they stay with info
1658
- if (showConsoleBadges) {
1659
- if (errorCount > 0) {
1660
- statusRow.appendChild(this.createConsoleBadge('error', errorCount, BUTTON_COLORS.error));
1661
- }
1662
- if (warningCount > 0) {
1663
- statusRow.appendChild(this.createConsoleBadge('warn', warningCount, BUTTON_COLORS.warning));
1664
- }
1665
- }
1666
- mainRow.appendChild(statusRow);
1667
- // Action buttons - always render container for consistent height
1668
- const actionsContainer = document.createElement('div');
1669
- actionsContainer.className = 'devbar-actions';
1670
- if (showScreenshot) {
1671
- actionsContainer.appendChild(this.createScreenshotButton(accentColor));
1672
- }
1673
- actionsContainer.appendChild(this.createAIReviewButton());
1674
- actionsContainer.appendChild(this.createOutlineButton());
1675
- actionsContainer.appendChild(this.createSchemaButton());
1676
- mainRow.appendChild(actionsContainer);
1677
- wrapper.appendChild(mainRow);
1678
- // Render custom controls row if there are any
1679
- if (GlobalDevBar.customControls.length > 0) {
1680
- const customRow = document.createElement('div');
1681
- Object.assign(customRow.style, {
1682
- display: 'flex',
1683
- flexWrap: 'wrap',
1684
- alignItems: 'center',
1685
- gap: '0.5rem',
1686
- padding: '0 0.75rem 0.5rem 0.75rem',
1687
- borderTop: `1px solid ${accentColor}30`,
1688
- marginTop: '0',
1689
- paddingTop: '0.5rem',
1690
- fontFamily: FONT_MONO,
1691
- fontSize: '0.6875rem',
1692
- });
1693
- GlobalDevBar.customControls.forEach(control => {
1694
- const btn = document.createElement('button');
1695
- btn.type = 'button';
1696
- const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
1697
- const isActive = control.active ?? false;
1698
- const isDisabled = control.disabled ?? false;
1699
- Object.assign(btn.style, {
1700
- padding: '4px 10px',
1701
- backgroundColor: isActive ? `${color}33` : 'transparent',
1702
- border: `1px solid ${isActive ? color : `${color}60`}`,
1703
- borderRadius: '6px',
1704
- color: isActive ? color : `${color}99`,
1705
- fontSize: '0.625rem',
1706
- cursor: isDisabled ? 'not-allowed' : 'pointer',
1707
- opacity: isDisabled ? '0.5' : '1',
1708
- transition: 'all 150ms',
1709
- });
1710
- btn.textContent = control.label;
1711
- btn.disabled = isDisabled;
1712
- if (!isDisabled) {
1713
- btn.onmouseenter = () => {
1714
- btn.style.backgroundColor = `${color}20`;
1715
- btn.style.borderColor = color;
1716
- btn.style.color = color;
1717
- };
1718
- btn.onmouseleave = () => {
1719
- btn.style.backgroundColor = isActive ? `${color}33` : 'transparent';
1720
- btn.style.borderColor = isActive ? color : `${color}60`;
1721
- btn.style.color = isActive ? color : `${color}99`;
1722
- };
1723
- btn.onclick = () => control.onClick();
1724
- }
1725
- customRow.appendChild(btn);
1726
- });
1727
- wrapper.appendChild(customRow);
1728
- }
401
+ toggleCompactMode() {
402
+ this.compactMode = !this.compactMode;
403
+ this.settingsManager.saveSettings({ compactMode: this.compactMode });
404
+ this.debug.state('Compact mode toggled', { compactMode: this.compactMode });
405
+ this.render();
1729
406
  }
1730
407
  /**
1731
- * Create a console badge for error/warning counts
408
+ * Check if compact mode is enabled
1732
409
  */
1733
- createConsoleBadge(type, count, color) {
1734
- const label = type === 'error' ? 'error' : 'warning';
1735
- const isActive = this.consoleFilter === type;
1736
- const badge = document.createElement('span');
1737
- badge.className = this.tooltipClass('right', 'devbar-badge');
1738
- badge.setAttribute('data-tooltip', `${count} console ${label}${count === 1 ? '' : 's'} (click to view)`);
1739
- Object.assign(badge.style, {
1740
- display: 'flex',
1741
- alignItems: 'center',
1742
- justifyContent: 'center',
1743
- minWidth: '18px',
1744
- height: '18px',
1745
- padding: '0 5px',
1746
- borderRadius: '9999px',
1747
- backgroundColor: isActive ? color : `${color}E6`,
1748
- color: '#fff',
1749
- fontSize: '0.625rem',
1750
- fontWeight: '600',
1751
- cursor: 'pointer',
1752
- boxShadow: isActive ? `0 0 8px ${color}CC` : 'none',
1753
- });
1754
- badge.textContent = count > 99 ? '99+' : String(count);
1755
- badge.onclick = () => {
1756
- this.consoleFilter = this.consoleFilter === type ? null : type;
1757
- this.showOutlineModal = false;
1758
- this.showSchemaModal = false;
1759
- this.render();
1760
- };
1761
- return badge;
1762
- }
1763
- createScreenshotButton(accentColor) {
1764
- const btn = document.createElement('button');
1765
- btn.type = 'button';
1766
- btn.className = this.tooltipClass('right');
1767
- 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' : ''}`;
1775
- btn.setAttribute('data-tooltip', tooltip);
1776
- Object.assign(btn.style, {
1777
- display: 'flex',
1778
- alignItems: 'center',
1779
- justifyContent: 'center',
1780
- width: '22px',
1781
- height: '22px',
1782
- minWidth: '22px',
1783
- minHeight: '22px',
1784
- flexShrink: '0',
1785
- borderRadius: '50%',
1786
- border: '1px solid',
1787
- borderColor: hasSuccessState ? accentColor : `${accentColor}80`,
1788
- backgroundColor: hasSuccessState ? `${accentColor}33` : 'transparent',
1789
- color: hasSuccessState ? accentColor : `${accentColor}99`,
1790
- cursor: !this.capturing ? 'pointer' : 'not-allowed',
1791
- opacity: '1',
1792
- transition: 'all 150ms'
1793
- });
1794
- btn.disabled = this.capturing;
1795
- btn.onclick = (e) => {
1796
- // If we have a saved screenshot path, clicking copies the path
1797
- if (this.lastScreenshot && !e.shiftKey) {
1798
- this.copyPathToClipboard(this.lastScreenshot);
1799
- }
1800
- else {
1801
- this.handleScreenshot(e.shiftKey);
1802
- }
1803
- };
1804
- // Button content
1805
- if (this.copiedToClipboard || this.copiedPath || this.lastScreenshot) {
1806
- btn.textContent = '✓';
1807
- btn.style.fontSize = '0.6rem';
1808
- }
1809
- else if (this.capturing) {
1810
- btn.textContent = '...';
1811
- btn.style.fontSize = '0.5rem';
1812
- }
1813
- else {
1814
- // Camera icon SVG
1815
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1816
- svg.setAttribute('width', '12');
1817
- svg.setAttribute('height', '12');
1818
- svg.setAttribute('viewBox', '0 0 50.8 50.8');
1819
- svg.style.stroke = 'currentColor';
1820
- svg.style.fill = 'none';
1821
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
1822
- g.setAttribute('stroke-linecap', 'round');
1823
- g.setAttribute('stroke-linejoin', 'round');
1824
- g.setAttribute('stroke-width', '4');
1825
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1826
- path.setAttribute('d', 'M19.844 7.938H7.938v11.905m0 11.113v11.906h11.905m23.019-11.906v11.906H30.956m11.906-23.018V7.938H30.956');
1827
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1828
- circle.setAttribute('cx', '25.4');
1829
- circle.setAttribute('cy', '25.4');
1830
- circle.setAttribute('r', '8.731');
1831
- g.appendChild(path);
1832
- g.appendChild(circle);
1833
- svg.appendChild(g);
1834
- btn.appendChild(svg);
1835
- }
1836
- return btn;
410
+ isCompactMode() {
411
+ return this.compactMode;
1837
412
  }
1838
- createAIReviewButton() {
1839
- const btn = document.createElement('button');
1840
- btn.type = 'button';
1841
- 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' : ''}`;
1849
- btn.setAttribute('data-tooltip', tooltip);
1850
- const hasError = !!this.designReviewError;
1851
- const isActive = this.designReviewInProgress || !!this.lastDesignReview || hasError;
1852
- const isDisabled = this.designReviewInProgress || !this.sweetlinkConnected;
1853
- // Use error color (red) when there's an error, otherwise normal review color
1854
- const buttonColor = hasError ? '#ef4444' : BUTTON_COLORS.review;
1855
- Object.assign(btn.style, getButtonStyles(buttonColor, isActive, isDisabled));
1856
- if (!this.sweetlinkConnected)
1857
- btn.style.opacity = '0.5';
1858
- btn.disabled = isDisabled;
1859
- btn.onclick = () => this.showDesignReviewConfirmation();
1860
- if (this.designReviewInProgress) {
1861
- btn.textContent = '~';
1862
- btn.style.fontSize = '0.5rem';
1863
- btn.style.animation = 'pulse 1s infinite';
1864
- }
1865
- else if (this.designReviewError) {
1866
- // Show 'x' for error state
1867
- btn.textContent = '×';
1868
- btn.style.fontSize = '0.875rem';
1869
- btn.style.fontWeight = 'bold';
1870
- }
1871
- else if (this.lastDesignReview) {
1872
- btn.textContent = 'v';
1873
- btn.style.fontSize = '0.5rem';
1874
- }
1875
- else {
1876
- btn.appendChild(createSvgIcon('M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z', { fill: true }));
1877
- }
1878
- return btn;
1879
- }
1880
- createOutlineButton() {
1881
- const btn = document.createElement('button');
1882
- btn.type = 'button';
1883
- btn.className = this.tooltipClass('right');
1884
- const tooltip = this.lastOutline
1885
- ? `Outline saved to:\n${this.lastOutline}`
1886
- : `Document Outline\n\nView page heading structure and\nsave as markdown.`;
1887
- btn.setAttribute('data-tooltip', tooltip);
1888
- const isActive = this.showOutlineModal || !!this.lastOutline;
1889
- Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.outline, isActive, false));
1890
- btn.onclick = () => this.handleDocumentOutline();
1891
- if (this.lastOutline) {
1892
- btn.textContent = 'v';
1893
- btn.style.fontSize = '0.5rem';
1894
- }
1895
- else {
1896
- btn.appendChild(createSvgIcon('M3 4h18v2H3V4zm0 7h12v2H3v-2zm0 7h18v2H3v-2z', { fill: true }));
1897
- }
1898
- return btn;
1899
- }
1900
- createSchemaButton() {
1901
- const btn = document.createElement('button');
1902
- btn.type = 'button';
1903
- btn.className = this.tooltipClass('right');
1904
- const tooltip = this.lastSchema
1905
- ? `Schema saved to:\n${this.lastSchema}`
1906
- : `Page Schema\n\nView JSON-LD, Open Graph, and\nother structured data.`;
1907
- btn.setAttribute('data-tooltip', tooltip);
1908
- const isActive = this.showSchemaModal || !!this.lastSchema;
1909
- Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.schema, isActive, false));
1910
- btn.onclick = () => this.handlePageSchema();
1911
- if (this.lastSchema) {
1912
- btn.textContent = 'v';
1913
- btn.style.fontSize = '0.5rem';
1914
- }
1915
- else {
1916
- btn.appendChild(createSvgIcon('M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z', { fill: true }));
1917
- }
1918
- return btn;
413
+ // ============================================================================
414
+ // Render (delegates to rendering module)
415
+ // ============================================================================
416
+ render() {
417
+ moduleRender(this, consoleCapture, GlobalDevBar.customControls);
1919
418
  }
1920
419
  }
1921
- // Static storage for custom controls
1922
- GlobalDevBar.customControls = [];
1923
420
  // ============================================================================
1924
421
  // Convenience Functions
1925
422
  // ============================================================================
@@ -1945,7 +442,7 @@ export function initGlobalDevBar(options) {
1945
442
  const existing = getGlobalInstance();
1946
443
  if (existing) {
1947
444
  // Check if already initialized with same position - skip re-init during HMR
1948
- const existingPosition = existing['options']?.position ?? 'bottom-left';
445
+ const existingPosition = existing.getPosition();
1949
446
  const newPosition = options?.position ?? 'bottom-left';
1950
447
  if (existingPosition === newPosition) {
1951
448
  return existing;
@@ -1975,5 +472,6 @@ export function destroyGlobalDevBar() {
1975
472
  setGlobalInstance(null);
1976
473
  }
1977
474
  }
1978
- // Re-export console capture for external use
1979
- export { earlyConsoleCapture };
475
+ // Re-export console capture instance for backward compatibility
476
+ export { consoleCapture as earlyConsoleCapture };
477
+ //# sourceMappingURL=GlobalDevBar.js.map