@ytspar/devbar 1.0.0-canary.db7454c → 1.0.0-canary.deff6e9

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 +4 -0
  2. package/dist/GlobalDevBar.d.ts +111 -204
  3. package/dist/GlobalDevBar.d.ts.map +1 -0
  4. package/dist/GlobalDevBar.js +153 -2746
  5. package/dist/GlobalDevBar.js.map +1 -0
  6. package/dist/accessibility.d.ts +1 -0
  7. package/dist/accessibility.d.ts.map +1 -0
  8. package/dist/accessibility.js +1 -0
  9. package/dist/accessibility.js.map +1 -0
  10. package/dist/constants.d.ts +38 -16
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +98 -125
  13. package/dist/constants.js.map +1 -0
  14. package/dist/debug.d.ts +4 -3
  15. package/dist/debug.d.ts.map +1 -0
  16. package/dist/debug.js +6 -4
  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 +3 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +6 -1
  25. package/dist/index.js.map +1 -0
  26. package/dist/lazy/index.d.ts +1 -0
  27. package/dist/lazy/index.d.ts.map +1 -0
  28. package/dist/lazy/index.js +1 -0
  29. package/dist/lazy/index.js.map +1 -0
  30. package/dist/lazy/lazyHtml2Canvas.d.ts +5 -0
  31. package/dist/lazy/lazyHtml2Canvas.d.ts.map +1 -0
  32. package/dist/lazy/lazyHtml2Canvas.js +1 -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 +1947 -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 +325 -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 +1 -0
  71. package/dist/network.d.ts.map +1 -0
  72. package/dist/network.js +5 -6
  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 +1 -0
  77. package/dist/outline.js.map +1 -0
  78. package/dist/presets.d.ts +9 -8
  79. package/dist/presets.d.ts.map +1 -0
  80. package/dist/presets.js +9 -8
  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 +3 -3
  85. package/dist/schema.js.map +1 -0
  86. package/dist/settings.d.ts +7 -8
  87. package/dist/settings.d.ts.map +1 -0
  88. package/dist/settings.js +21 -17
  89. package/dist/settings.js.map +1 -0
  90. package/dist/storage.d.ts +1 -0
  91. package/dist/storage.d.ts.map +1 -0
  92. package/dist/storage.js +1 -0
  93. package/dist/storage.js.map +1 -0
  94. package/dist/types.d.ts +5 -4
  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 +3 -1
  111. package/dist/ui/index.d.ts.map +1 -0
  112. package/dist/ui/index.js +3 -1
  113. package/dist/ui/index.js.map +1 -0
  114. package/dist/ui/modals.d.ts +3 -2
  115. package/dist/ui/modals.d.ts.map +1 -0
  116. package/dist/ui/modals.js +3 -2
  117. package/dist/ui/modals.js.map +1 -0
  118. package/dist/utils.d.ts +3 -2
  119. package/dist/utils.d.ts.map +1 -0
  120. package/dist/utils.js +3 -2
  121. package/dist/utils.js.map +1 -0
  122. package/package.json +17 -14
@@ -4,134 +4,115 @@
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 { 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';
18
+ import { CSS_COLORS, DEVBAR_STYLES, getThemeColors, MAX_RECONNECT_ATTEMPTS, WS_PORT, WS_PORT_OFFSET, } from './constants.js';
12
19
  import { DebugLogger, normalizeDebugConfig } from './debug.js';
13
- import { extractDocumentOutline, outlineToMarkdown } from './outline.js';
14
- import { extractPageSchema, schemaToMarkdown } from './schema.js';
15
- import { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager, } from './settings.js';
16
- import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createStyledButton, createSvgIcon, getButtonStyles, } from './ui/index.js';
17
- import { canvasToDataUrl, copyCanvasToClipboard, delay, formatArgs, prepareForCapture, } from './utils.js';
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';
18
29
  export { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager } from './settings.js';
19
- const html2canvas = (html2canvasModule.default ??
20
- html2canvasModule);
21
- const earlyConsoleCapture = (() => {
22
- const ssrFallback = {
23
- errorCount: 0,
24
- warningCount: 0,
25
- logs: [],
26
- originalConsole: null,
27
- isPatched: false,
28
- };
29
- // Skip on server-side rendering
30
- if (typeof window === 'undefined')
31
- return ssrFallback;
32
- const capture = {
33
- errorCount: 0,
34
- warningCount: 0,
35
- logs: [],
36
- originalConsole: {
37
- log: console.log,
38
- error: console.error,
39
- warn: console.warn,
40
- info: console.info,
41
- },
42
- isPatched: false,
43
- };
44
- const captureLog = (level, args) => {
45
- capture.logs.push({ level, message: formatArgs(args), timestamp: Date.now() });
46
- if (capture.logs.length > MAX_CONSOLE_LOGS)
47
- capture.logs = capture.logs.slice(-MAX_CONSOLE_LOGS);
48
- };
49
- // Patch console immediately
50
- if (!capture.isPatched && capture.originalConsole) {
51
- console.log = (...args) => {
52
- captureLog('log', args);
53
- capture.originalConsole.log(...args);
54
- };
55
- console.error = (...args) => {
56
- captureLog('error', args);
57
- capture.errorCount++;
58
- capture.originalConsole.error(...args);
59
- };
60
- console.warn = (...args) => {
61
- captureLog('warn', args);
62
- capture.warningCount++;
63
- capture.originalConsole.warn(...args);
64
- };
65
- console.info = (...args) => {
66
- captureLog('info', args);
67
- capture.originalConsole.info(...args);
68
- };
69
- capture.isPatched = true;
70
- }
71
- return capture;
72
- })();
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();
73
37
  // ============================================================================
74
38
  // GlobalDevBar Class
75
39
  // ============================================================================
76
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;
77
115
  constructor(options = {}) {
78
- this.container = null;
79
- this.ws = null;
80
- this.consoleLogs = [];
81
- this.sweetlinkConnected = false;
82
- this.collapsed = false;
83
- this.capturing = false;
84
- this.copiedToClipboard = false;
85
- this.copiedPath = false;
86
- this.lastScreenshot = null;
87
- this.designReviewInProgress = false;
88
- this.lastDesignReview = null;
89
- this.designReviewError = null;
90
- this.showDesignReviewConfirm = false;
91
- this.apiKeyStatus = null;
92
- this.lastOutline = null;
93
- this.lastSchema = null;
94
- this.savingOutline = false;
95
- this.savingSchema = false;
96
- this.consoleFilter = null;
97
- // Modal states
98
- this.showOutlineModal = false;
99
- this.showSchemaModal = false;
100
- this.breakpointInfo = null;
101
- this.perfStats = null;
102
- this.lcpValue = null;
103
- this.clsValue = 0;
104
- this.inpValue = 0;
105
- this.reconnectAttempts = 0;
106
- this.wsVerified = false;
107
- this.serverProjectDir = null;
108
- this.verificationTimeout = null;
109
- // Track the position of the connection indicator dot for smooth collapse
110
- this.lastDotPosition = null;
111
- this.reconnectTimeout = null;
112
- this.screenshotTimeout = null;
113
- this.copiedPathTimeout = null;
114
- this.designReviewTimeout = null;
115
- this.designReviewErrorTimeout = null;
116
- this.outlineTimeout = null;
117
- this.schemaTimeout = null;
118
- this.resizeHandler = null;
119
- this.keydownHandler = null;
120
- this.fcpObserver = null;
121
- this.lcpObserver = null;
122
- this.clsObserver = null;
123
- this.inpObserver = null;
124
- this.destroyed = false;
125
- // Theme state
126
- this.themeMode = 'system';
127
- this.themeMediaQuery = null;
128
- this.themeMediaHandler = null;
129
- // Compact mode state
130
- this.compactMode = false;
131
- // Settings popover state
132
- this.showSettingsPopover = false;
133
- // Overlay element for modals
134
- this.overlayElement = null;
135
116
  // Initialize debug config first so we can log during construction
136
117
  this.debugConfig = normalizeDebugConfig(options.debug);
137
118
  this.debug = new DebugLogger(this.debugConfig);
@@ -150,7 +131,7 @@ export class GlobalDevBar {
150
131
  }
151
132
  this.options = {
152
133
  position: options.position ?? 'bottom-left',
153
- accentColor: options.accentColor ?? COLORS.primary,
134
+ accentColor: options.accentColor ?? CSS_COLORS.primary,
154
135
  showMetrics: {
155
136
  breakpoint: options.showMetrics?.breakpoint ?? true,
156
137
  fcp: options.showMetrics?.fcp ?? true,
@@ -167,28 +148,20 @@ export class GlobalDevBar {
167
148
  this.debug.lifecycle('GlobalDevBar constructed', { options: this.options });
168
149
  }
169
150
  /**
170
- * Get tooltip class name(s) if tooltips are enabled, otherwise empty string
151
+ * Get current error, warning, and info counts from the log array
171
152
  */
172
- tooltipClass(direction = 'left', ...additionalClasses) {
173
- if (!this.options.showTooltips) {
174
- return additionalClasses.join(' ');
175
- }
176
- return ['devbar-tooltip', `devbar-tooltip-${direction}`, ...additionalClasses].join(' ');
153
+ getLogCounts() {
154
+ return {
155
+ errorCount: consoleCapture.getErrorCount(),
156
+ warningCount: consoleCapture.getWarningCount(),
157
+ infoCount: consoleCapture.getInfoCount(),
158
+ };
177
159
  }
178
160
  /**
179
- * Get current error and warning counts from the log array
161
+ * Reset position style properties on an element to clear stale values
180
162
  */
181
- getLogCounts() {
182
- const logs = earlyConsoleCapture.logs;
183
- let errorCount = 0;
184
- let warningCount = 0;
185
- for (const log of logs) {
186
- if (log.level === 'error')
187
- errorCount++;
188
- else if (log.level === 'warn')
189
- warningCount++;
190
- }
191
- return { errorCount, warningCount };
163
+ resetPositionStyles(element) {
164
+ Object.assign(element.style, { top: '', bottom: '', left: '', right: '', transform: '' });
192
165
  }
193
166
  /**
194
167
  * Create a collapsed count badge (used for error/warning counts in minimized state)
@@ -266,23 +239,29 @@ export class GlobalDevBar {
266
239
  if (this.destroyed)
267
240
  return;
268
241
  this.debug.lifecycle('Initializing DevBar');
269
- // Inject tooltip styles
242
+ // Inject animation and utility CSS
270
243
  this.injectStyles();
271
- // Copy early captured logs
272
- this.consoleLogs = [...earlyConsoleCapture.logs];
273
- this.debug.lifecycle('Copied early console logs', { count: this.consoleLogs.length });
244
+ // Copy captured logs
245
+ this.consoleLogs = consoleCapture.getLogs();
246
+ this.debug.lifecycle('Copied console logs', { count: this.consoleLogs.length });
247
+ // Subscribe to log changes for real-time badge updates
248
+ this.logChangeListener = () => {
249
+ this.consoleLogs = consoleCapture.getLogs();
250
+ this.render();
251
+ };
252
+ consoleCapture.addListener(this.logChangeListener);
274
253
  // Setup theme
275
- this.setupTheme();
254
+ setupTheme(this);
276
255
  // Load compact mode from storage
277
- this.loadCompactMode();
256
+ loadCompactMode(this);
278
257
  // Setup WebSocket connection
279
258
  this.connectWebSocket();
280
259
  // Setup breakpoint detection
281
- this.setupBreakpointDetection();
260
+ setupBreakpointDetection(this);
282
261
  // Setup performance monitoring
283
- this.setupPerformanceMonitoring();
262
+ setupPerformanceMonitoring(this);
284
263
  // Setup keyboard shortcuts
285
- this.setupKeyboardShortcuts();
264
+ setupKeyboardShortcuts(this);
286
265
  // Initial render
287
266
  this.render();
288
267
  this.debug.lifecycle('DevBar initialized successfully');
@@ -303,8 +282,6 @@ export class GlobalDevBar {
303
282
  this.reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // Prevent reconnection
304
283
  if (this.reconnectTimeout)
305
284
  clearTimeout(this.reconnectTimeout);
306
- if (this.verificationTimeout)
307
- clearTimeout(this.verificationTimeout);
308
285
  if (this.ws)
309
286
  this.ws.close();
310
287
  // Clear timeouts
@@ -318,6 +295,8 @@ export class GlobalDevBar {
318
295
  clearTimeout(this.outlineTimeout);
319
296
  if (this.schemaTimeout)
320
297
  clearTimeout(this.schemaTimeout);
298
+ if (this.consoleLogsTimeout)
299
+ clearTimeout(this.consoleLogsTimeout);
321
300
  // Remove event listeners
322
301
  if (this.resizeHandler)
323
302
  window.removeEventListener('resize', this.resizeHandler);
@@ -336,13 +315,13 @@ export class GlobalDevBar {
336
315
  if (this.themeMediaQuery && this.themeMediaHandler) {
337
316
  this.themeMediaQuery.removeEventListener('change', this.themeMediaHandler);
338
317
  }
339
- // Restore console
340
- if (earlyConsoleCapture.originalConsole) {
341
- console.log = earlyConsoleCapture.originalConsole.log;
342
- console.error = earlyConsoleCapture.originalConsole.error;
343
- console.warn = earlyConsoleCapture.originalConsole.warn;
344
- console.info = earlyConsoleCapture.originalConsole.info;
318
+ // Remove log change listener
319
+ if (this.logChangeListener) {
320
+ consoleCapture.removeListener(this.logChangeListener);
321
+ this.logChangeListener = null;
345
322
  }
323
+ // Restore console
324
+ consoleCapture.stop();
346
325
  // Remove DOM elements
347
326
  if (this.container) {
348
327
  this.container.remove();
@@ -355,267 +334,20 @@ export class GlobalDevBar {
355
334
  this.debug.lifecycle('DevBar destroyed');
356
335
  }
357
336
  injectStyles() {
358
- const styleId = 'devbar-tooltip-styles';
337
+ const styleId = 'devbar-styles';
359
338
  if (!document.getElementById(styleId)) {
360
339
  const style = document.createElement('style');
361
340
  style.id = styleId;
362
- style.textContent = TOOLTIP_STYLES;
341
+ style.textContent = DEVBAR_STYLES;
363
342
  document.head.appendChild(style);
364
343
  }
365
344
  }
345
+ // Delegate to module functions, binding `this` as state
366
346
  connectWebSocket(port) {
367
- if (this.destroyed)
368
- return;
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}`);
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);
388
- ws.onopen = () => {
389
- this.debug.ws('WebSocket socket opened, awaiting server-info');
390
- ws.send(JSON.stringify({ type: 'browser-client-ready' }));
391
- };
392
- ws.onmessage = async (event) => {
393
- try {
394
- const message = JSON.parse(event.data);
395
- // Handle server-info for port matching
396
- if (message.type === 'server-info') {
397
- if (this.verificationTimeout) {
398
- clearTimeout(this.verificationTimeout);
399
- this.verificationTimeout = null;
400
- }
401
- const serverAppPort = message.appPort;
402
- const serverMatchesApp = serverAppPort === null || serverAppPort === this.currentAppPort;
403
- if (!serverMatchesApp) {
404
- this.debug.ws('Server mismatch', {
405
- serverAppPort,
406
- currentAppPort: this.currentAppPort,
407
- tryingNextPort: targetPort + 1,
408
- });
409
- ws.close();
410
- // Try next port
411
- const nextPort = targetPort + 1;
412
- if (nextPort < this.baseWsPort + MAX_PORT_RETRIES) {
413
- setTimeout(() => this.connectWebSocket(nextPort), PORT_RETRY_DELAY_MS);
414
- }
415
- else {
416
- this.debug.ws('No matching server found, will retry from base port');
417
- setTimeout(() => this.connectWebSocket(this.baseWsPort), PORT_SCAN_RESTART_DELAY_MS);
418
- }
419
- return;
420
- }
421
- // Server matches - mark as verified and connected
422
- this.wsVerified = true;
423
- this.sweetlinkConnected = true;
424
- this.reconnectAttempts = 0;
425
- this.serverProjectDir = message.projectDir ?? null;
426
- this.debug.ws('Server verified', {
427
- appPort: serverAppPort ?? 'any',
428
- projectDir: this.serverProjectDir,
429
- });
430
- this.settingsManager.setWebSocket(ws);
431
- this.settingsManager.setConnected(true);
432
- ws.send(JSON.stringify({ type: 'load-settings' }));
433
- this.render();
434
- return;
435
- }
436
- // Ignore other commands until verified
437
- if (!this.wsVerified) {
438
- this.debug.ws('Ignoring command before verification', { type: message.type });
439
- return;
440
- }
441
- const command = message;
442
- this.debug.ws('Received command', { type: command.type });
443
- await this.handleSweetlinkCommand(command);
444
- }
445
- catch (e) {
446
- console.error('[GlobalDevBar] Error handling command:', e);
447
- }
448
- };
449
- ws.onclose = () => {
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
- }
469
- }
470
- };
471
- ws.onerror = () => {
472
- // Error will trigger onclose, which handles reconnection
473
- this.debug.ws('WebSocket error');
474
- };
475
- }
476
- async handleSweetlinkCommand(command) {
477
- const ws = this.ws;
478
- if (!ws || ws.readyState !== WebSocket.OPEN)
479
- return;
480
- switch (command.type) {
481
- case 'screenshot': {
482
- const targetElement = command.selector
483
- ? document.querySelector(command.selector) || document.body
484
- : document.body;
485
- const canvas = await html2canvas(targetElement, {
486
- logging: false,
487
- useCORS: true,
488
- allowTaint: true,
489
- });
490
- ws.send(JSON.stringify({
491
- success: true,
492
- data: {
493
- screenshot: canvas.toDataURL('image/png'),
494
- width: canvas.width,
495
- height: canvas.height,
496
- selector: command.selector || 'body',
497
- },
498
- timestamp: Date.now(),
499
- }));
500
- break;
501
- }
502
- case 'get-logs': {
503
- let logs = this.consoleLogs;
504
- if (command.filter) {
505
- const filter = command.filter.toLowerCase();
506
- logs = logs.filter((log) => log.level.includes(filter) || log.message.toLowerCase().includes(filter));
507
- }
508
- ws.send(JSON.stringify({ success: true, data: logs, timestamp: Date.now() }));
509
- break;
510
- }
511
- case 'query-dom': {
512
- if (command.selector) {
513
- const elements = Array.from(document.querySelectorAll(command.selector));
514
- const results = elements.map((el) => {
515
- if (command.property)
516
- return el[command.property] ?? null;
517
- return {
518
- tagName: el.tagName,
519
- className: el.className,
520
- id: el.id,
521
- textContent: el.textContent?.trim().slice(0, 100),
522
- };
523
- });
524
- ws.send(JSON.stringify({
525
- success: true,
526
- data: { count: results.length, results },
527
- timestamp: Date.now(),
528
- }));
529
- }
530
- break;
531
- }
532
- case 'exec-js': {
533
- if (command.code) {
534
- try {
535
- // Use indirect eval to avoid strict mode issues
536
- const indirectEval = eval;
537
- const result = indirectEval(command.code);
538
- ws.send(JSON.stringify({ success: true, data: result, timestamp: Date.now() }));
539
- }
540
- catch (e) {
541
- ws.send(JSON.stringify({
542
- success: false,
543
- error: e instanceof Error ? e.message : 'Execution failed',
544
- timestamp: Date.now(),
545
- }));
546
- }
547
- }
548
- break;
549
- }
550
- case 'screenshot-saved':
551
- this.handleNotification('screenshot', command.path, SCREENSHOT_NOTIFICATION_MS);
552
- break;
553
- case 'design-review-saved':
554
- this.designReviewInProgress = false;
555
- this.handleNotification('designReview', command.reviewPath, DESIGN_REVIEW_NOTIFICATION_MS);
556
- break;
557
- case 'design-review-error':
558
- this.designReviewInProgress = false;
559
- this.designReviewError = command.error || 'Unknown error';
560
- console.error('[GlobalDevBar] Design review failed:', command.error);
561
- // Clear error after notification duration
562
- if (this.designReviewErrorTimeout)
563
- clearTimeout(this.designReviewErrorTimeout);
564
- this.designReviewErrorTimeout = setTimeout(() => {
565
- this.designReviewError = null;
566
- this.render();
567
- }, DESIGN_REVIEW_NOTIFICATION_MS);
568
- this.render();
569
- break;
570
- case 'api-key-status': {
571
- // Properties are at top level of the response
572
- const response = command;
573
- this.apiKeyStatus = {
574
- configured: response.configured ?? false,
575
- maskedKey: response.maskedKey,
576
- model: response.model,
577
- pricing: response.pricing,
578
- };
579
- // Re-render to update the confirmation modal
580
- this.render();
581
- break;
582
- }
583
- case 'outline-saved':
584
- this.handleNotification('outline', command.outlinePath, SCREENSHOT_NOTIFICATION_MS);
585
- break;
586
- case 'outline-error':
587
- console.error('[GlobalDevBar] Outline save failed:', command.error);
588
- break;
589
- case 'schema-saved':
590
- this.handleNotification('schema', command.schemaPath, SCREENSHOT_NOTIFICATION_MS);
591
- break;
592
- case 'schema-error':
593
- console.error('[GlobalDevBar] Schema save failed:', command.error);
594
- break;
595
- case 'settings-loaded':
596
- this.handleSettingsLoaded(command.settings);
597
- break;
598
- case 'settings-saved':
599
- this.debug.state('Settings saved to server', { path: command.settingsPath });
600
- break;
601
- case 'settings-error':
602
- console.error('[GlobalDevBar] Settings operation failed:', command.error);
603
- break;
604
- }
347
+ connectWebSocket(this, port);
605
348
  }
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);
349
+ handleNotification(type, path, durationMs) {
350
+ handleNotification(this, type, path, durationMs);
619
351
  }
620
352
  /**
621
353
  * Apply settings to the DevBar state and options
@@ -634,258 +366,14 @@ export class GlobalDevBar {
634
366
  // Re-render with new settings
635
367
  this.render();
636
368
  }
637
- /**
638
- * Handle notification state updates with auto-clear timeout
639
- */
640
- handleNotification(type, path, durationMs) {
641
- if (!path)
642
- return;
643
- // Update the appropriate state
644
- switch (type) {
645
- case 'screenshot':
646
- this.lastScreenshot = path;
647
- if (this.screenshotTimeout)
648
- clearTimeout(this.screenshotTimeout);
649
- this.screenshotTimeout = setTimeout(() => {
650
- this.lastScreenshot = null;
651
- this.render();
652
- }, durationMs);
653
- break;
654
- case 'designReview':
655
- this.lastDesignReview = path;
656
- if (this.designReviewTimeout)
657
- clearTimeout(this.designReviewTimeout);
658
- this.designReviewTimeout = setTimeout(() => {
659
- this.lastDesignReview = null;
660
- this.render();
661
- }, durationMs);
662
- break;
663
- case 'outline':
664
- this.savingOutline = false;
665
- this.lastOutline = path;
666
- if (this.outlineTimeout)
667
- clearTimeout(this.outlineTimeout);
668
- this.outlineTimeout = setTimeout(() => {
669
- this.lastOutline = null;
670
- this.render();
671
- }, durationMs);
672
- break;
673
- case 'schema':
674
- this.savingSchema = false;
675
- this.lastSchema = path;
676
- if (this.schemaTimeout)
677
- clearTimeout(this.schemaTimeout);
678
- this.schemaTimeout = setTimeout(() => {
679
- this.lastSchema = null;
680
- this.render();
681
- }, durationMs);
682
- break;
683
- }
369
+ clearConsoleLogs() {
370
+ consoleCapture.clear();
371
+ this.consoleLogs = [];
372
+ this.consoleFilter = null;
684
373
  this.render();
685
374
  }
686
- setupBreakpointDetection() {
687
- const updateBreakpointInfo = () => {
688
- const width = window.innerWidth;
689
- const height = window.innerHeight;
690
- // Determine breakpoint by checking thresholds in descending order
691
- const breakpointOrder = [
692
- '2xl',
693
- 'xl',
694
- 'lg',
695
- 'md',
696
- 'sm',
697
- ];
698
- const tailwindBreakpoint = breakpointOrder.find((bp) => width >= TAILWIND_BREAKPOINTS[bp].min) ?? 'base';
699
- this.breakpointInfo = {
700
- tailwindBreakpoint,
701
- dimensions: `${width}x${height}`,
702
- };
703
- this.render();
704
- };
705
- updateBreakpointInfo();
706
- this.resizeHandler = updateBreakpointInfo;
707
- window.addEventListener('resize', this.resizeHandler);
708
- }
709
- setupPerformanceMonitoring() {
710
- const updatePerfStats = () => {
711
- // FCP
712
- const paintEntries = performance.getEntriesByType('paint');
713
- const fcpEntry = paintEntries.find((entry) => entry.name === 'first-contentful-paint');
714
- const fcp = fcpEntry ? `${Math.round(fcpEntry.startTime)}ms` : '-';
715
- // LCP (from cached value, updated by observer)
716
- const lcp = this.lcpValue !== null ? `${Math.round(this.lcpValue)}ms` : '-';
717
- // CLS (cumulative layout shift)
718
- const cls = this.clsValue > 0 ? this.clsValue.toFixed(3) : '-';
719
- // INP (Interaction to Next Paint)
720
- const inp = this.inpValue > 0 ? `${Math.round(this.inpValue)}ms` : '-';
721
- // Total Resource Size
722
- const resources = performance.getEntriesByType('resource');
723
- let totalBytes = 0;
724
- const navEntry = performance.getEntriesByType('navigation')[0];
725
- if (navEntry) {
726
- totalBytes += navEntry.transferSize || 0;
727
- }
728
- resources.forEach((entry) => {
729
- const resourceEntry = entry;
730
- totalBytes += resourceEntry.transferSize || 0;
731
- });
732
- const totalSize = totalBytes > 1024 * 1024
733
- ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB`
734
- : `${Math.round(totalBytes / 1024)} KB`;
735
- this.perfStats = { fcp, lcp, cls, inp, totalSize };
736
- this.debug.perf('Performance stats updated', this.perfStats);
737
- this.render();
738
- };
739
- if (document.readyState === 'complete') {
740
- setTimeout(updatePerfStats, 100);
741
- }
742
- else {
743
- window.addEventListener('load', () => setTimeout(updatePerfStats, 100));
744
- }
745
- // FCP Observer
746
- try {
747
- this.fcpObserver = new PerformanceObserver((list) => {
748
- const entries = list.getEntries();
749
- entries.forEach((entry) => {
750
- if (entry.name === 'first-contentful-paint') {
751
- updatePerfStats();
752
- }
753
- });
754
- });
755
- this.fcpObserver.observe({ type: 'paint', buffered: true });
756
- }
757
- catch (e) {
758
- console.warn('[GlobalDevBar] FCP PerformanceObserver not supported', e);
759
- }
760
- // LCP Observer
761
- try {
762
- this.lcpObserver = new PerformanceObserver((list) => {
763
- const entries = list.getEntries();
764
- const lastEntry = entries[entries.length - 1];
765
- if (lastEntry) {
766
- this.lcpValue = lastEntry.startTime;
767
- this.debug.perf('LCP updated', { lcp: this.lcpValue });
768
- updatePerfStats();
769
- }
770
- });
771
- this.lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
772
- }
773
- catch (e) {
774
- console.warn('[GlobalDevBar] LCP PerformanceObserver not supported', e);
775
- }
776
- // CLS Observer (Cumulative Layout Shift)
777
- try {
778
- this.clsObserver = new PerformanceObserver((list) => {
779
- for (const entry of list.getEntries()) {
780
- // Only count layout shifts without recent user input
781
- const layoutShift = entry;
782
- if (!layoutShift.hadRecentInput && layoutShift.value) {
783
- this.clsValue += layoutShift.value;
784
- this.debug.perf('CLS updated', { cls: this.clsValue });
785
- updatePerfStats();
786
- }
787
- }
788
- });
789
- this.clsObserver.observe({ type: 'layout-shift', buffered: true });
790
- }
791
- catch (e) {
792
- console.warn('[GlobalDevBar] CLS PerformanceObserver not supported', e);
793
- }
794
- // INP Observer (Interaction to Next Paint)
795
- try {
796
- this.inpObserver = new PerformanceObserver((list) => {
797
- for (const entry of list.getEntries()) {
798
- const eventEntry = entry;
799
- if (eventEntry.duration && eventEntry.duration > this.inpValue) {
800
- this.inpValue = eventEntry.duration;
801
- this.debug.perf('INP updated', { inp: this.inpValue });
802
- updatePerfStats();
803
- }
804
- }
805
- });
806
- // durationThreshold filters out very short interactions
807
- this.inpObserver.observe({
808
- type: 'event',
809
- buffered: true,
810
- durationThreshold: 16,
811
- });
812
- }
813
- catch (e) {
814
- console.warn('[GlobalDevBar] INP PerformanceObserver not supported', e);
815
- }
816
- }
817
- setupKeyboardShortcuts() {
818
- this.keydownHandler = (e) => {
819
- // Close modals/popovers on Escape
820
- if (e.key === 'Escape') {
821
- if (this.showSettingsPopover) {
822
- this.showSettingsPopover = false;
823
- this.render();
824
- return;
825
- }
826
- if (this.consoleFilter ||
827
- this.showOutlineModal ||
828
- this.showSchemaModal ||
829
- this.showDesignReviewConfirm) {
830
- this.consoleFilter = null;
831
- this.showOutlineModal = false;
832
- this.showSchemaModal = false;
833
- this.showDesignReviewConfirm = false;
834
- this.render();
835
- return;
836
- }
837
- }
838
- if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
839
- // Cmd/Ctrl+Shift+M: Toggle compact mode
840
- if (e.key === 'M' || e.key === 'm') {
841
- e.preventDefault();
842
- this.toggleCompactMode();
843
- return;
844
- }
845
- if (e.key === 'S' || e.key === 's') {
846
- e.preventDefault();
847
- if (this.sweetlinkConnected && !this.capturing) {
848
- this.handleScreenshot(false);
849
- }
850
- }
851
- else if (e.key === 'C' || e.key === 'c') {
852
- const selection = window.getSelection();
853
- if (!selection || selection.toString().length === 0) {
854
- e.preventDefault();
855
- if (!this.capturing) {
856
- this.handleScreenshot(true);
857
- }
858
- }
859
- }
860
- }
861
- };
862
- window.addEventListener('keydown', this.keydownHandler);
863
- }
864
- setupTheme() {
865
- // Load stored theme preference from settings manager
866
- const settings = this.settingsManager.getSettings();
867
- this.themeMode = settings.themeMode;
868
- this.debug.state('Theme loaded', { mode: this.themeMode });
869
- // Listen for system theme changes
870
- if (typeof window !== 'undefined' && window.matchMedia) {
871
- this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
872
- this.themeMediaHandler = () => {
873
- if (this.themeMode === 'system') {
874
- // Re-inject theme CSS when system preference changes
875
- injectThemeCSS(getTheme(this.themeMode));
876
- this.debug.state('System theme changed', {
877
- effectiveTheme: getEffectiveTheme(this.themeMode),
878
- });
879
- this.render();
880
- }
881
- };
882
- this.themeMediaQuery.addEventListener('change', this.themeMediaHandler);
883
- }
884
- }
885
- loadCompactMode() {
886
- const settings = this.settingsManager.getSettings();
887
- this.compactMode = settings.compactMode;
888
- this.debug.state('Compact mode loaded', { compactMode: this.compactMode });
375
+ handleScreenshot(copyToClipboard) {
376
+ return moduleHandleScreenshot(this, copyToClipboard);
889
377
  }
890
378
  /**
891
379
  * Get the current theme mode
@@ -897,12 +385,7 @@ export class GlobalDevBar {
897
385
  * Set the theme mode
898
386
  */
899
387
  setThemeMode(mode) {
900
- this.themeMode = mode;
901
- this.settingsManager.saveSettings({ themeMode: mode });
902
- // Inject the appropriate theme CSS variables
903
- injectThemeCSS(getTheme(mode));
904
- this.debug.state('Theme mode changed', { mode, effectiveTheme: getEffectiveTheme(mode) });
905
- this.render();
388
+ moduleSetThemeMode(this, mode);
906
389
  }
907
390
  /**
908
391
  * Get the current effective theme colors
@@ -925,2090 +408,13 @@ export class GlobalDevBar {
925
408
  isCompactMode() {
926
409
  return this.compactMode;
927
410
  }
928
- async copyPathToClipboard(path) {
929
- try {
930
- await navigator.clipboard.writeText(path);
931
- this.copiedPath = true;
932
- if (this.copiedPathTimeout)
933
- clearTimeout(this.copiedPathTimeout);
934
- this.copiedPathTimeout = setTimeout(() => {
935
- this.copiedPath = false;
936
- this.render();
937
- }, CLIPBOARD_NOTIFICATION_MS);
938
- this.render();
939
- }
940
- catch (error) {
941
- console.error('[GlobalDevBar] Failed to copy path:', error);
942
- }
943
- }
944
- async handleScreenshot(copyToClipboard = false) {
945
- if (this.capturing)
946
- return;
947
- if (!copyToClipboard && !this.sweetlinkConnected)
948
- return;
949
- let cleanup = null;
950
- try {
951
- this.capturing = true;
952
- this.render();
953
- cleanup = prepareForCapture();
954
- await delay(SCREENSHOT_BLUR_DELAY_MS);
955
- const canvas = await html2canvas(document.body, {
956
- logging: false,
957
- useCORS: true,
958
- allowTaint: true,
959
- scale: SCREENSHOT_SCALE,
960
- width: window.innerWidth,
961
- windowWidth: window.innerWidth,
962
- });
963
- // Restore page state
964
- cleanup();
965
- cleanup = null;
966
- if (copyToClipboard) {
967
- try {
968
- await copyCanvasToClipboard(canvas);
969
- this.copiedToClipboard = true;
970
- this.render();
971
- if (this.screenshotTimeout)
972
- clearTimeout(this.screenshotTimeout);
973
- this.screenshotTimeout = setTimeout(() => {
974
- this.copiedToClipboard = false;
975
- this.render();
976
- }, CLIPBOARD_NOTIFICATION_MS);
977
- }
978
- catch (e) {
979
- console.error('[GlobalDevBar] Failed to copy to clipboard:', e);
980
- }
981
- }
982
- else {
983
- const dataUrl = canvasToDataUrl(canvas, {
984
- format: 'jpeg',
985
- quality: DEVBAR_SCREENSHOT_QUALITY,
986
- });
987
- if (this.ws?.readyState === WebSocket.OPEN) {
988
- // Include web vitals metrics
989
- const webVitals = {};
990
- if (this.lcpValue !== null)
991
- webVitals.lcp = Math.round(this.lcpValue);
992
- if (this.clsValue > 0)
993
- webVitals.cls = this.clsValue;
994
- if (this.inpValue > 0)
995
- webVitals.inp = Math.round(this.inpValue);
996
- // Get FCP from performance entries
997
- const fcpEntry = performance
998
- .getEntriesByType('paint')
999
- .find((e) => e.name === 'first-contentful-paint');
1000
- if (fcpEntry)
1001
- webVitals.fcp = Math.round(fcpEntry.startTime);
1002
- // Calculate page size
1003
- let pageSize = 0;
1004
- const navEntry = performance.getEntriesByType('navigation')[0];
1005
- if (navEntry)
1006
- pageSize += navEntry.transferSize || 0;
1007
- performance.getEntriesByType('resource').forEach((entry) => {
1008
- pageSize += entry.transferSize || 0;
1009
- });
1010
- this.ws.send(JSON.stringify({
1011
- type: 'save-screenshot',
1012
- data: {
1013
- screenshot: dataUrl,
1014
- width: canvas.width,
1015
- height: canvas.height,
1016
- logs: this.consoleLogs,
1017
- url: window.location.href,
1018
- timestamp: Date.now(),
1019
- webVitals: Object.keys(webVitals).length > 0 ? webVitals : undefined,
1020
- pageSize: pageSize > 0 ? pageSize : undefined,
1021
- },
1022
- }));
1023
- }
1024
- }
1025
- }
1026
- catch (e) {
1027
- console.error('[GlobalDevBar] Screenshot failed:', e);
1028
- if (cleanup)
1029
- cleanup();
1030
- }
1031
- finally {
1032
- this.capturing = false;
1033
- this.render();
1034
- }
1035
- }
1036
- async handleDesignReview() {
1037
- if (this.designReviewInProgress || !this.sweetlinkConnected)
1038
- return;
1039
- let cleanup = null;
1040
- try {
1041
- this.designReviewInProgress = true;
1042
- this.designReviewError = null; // Clear any previous error
1043
- if (this.designReviewErrorTimeout) {
1044
- clearTimeout(this.designReviewErrorTimeout);
1045
- this.designReviewErrorTimeout = null;
1046
- }
1047
- this.render();
1048
- cleanup = prepareForCapture();
1049
- await delay(SCREENSHOT_BLUR_DELAY_MS);
1050
- const canvas = await html2canvas(document.body, {
1051
- logging: false,
1052
- useCORS: true,
1053
- allowTaint: true,
1054
- scale: 1, // Full quality for design review
1055
- width: window.innerWidth,
1056
- windowWidth: window.innerWidth,
1057
- });
1058
- // Restore page state
1059
- cleanup();
1060
- cleanup = null;
1061
- const dataUrl = canvasToDataUrl(canvas, { format: 'png' });
1062
- if (this.ws?.readyState === WebSocket.OPEN) {
1063
- this.ws.send(JSON.stringify({
1064
- type: 'design-review-screenshot',
1065
- data: {
1066
- screenshot: dataUrl,
1067
- width: canvas.width,
1068
- height: canvas.height,
1069
- logs: this.consoleLogs,
1070
- url: window.location.href,
1071
- timestamp: Date.now(),
1072
- },
1073
- }));
1074
- }
1075
- }
1076
- catch (e) {
1077
- console.error('[GlobalDevBar] Design review failed:', e);
1078
- if (cleanup)
1079
- cleanup();
1080
- this.designReviewInProgress = false;
1081
- this.render();
1082
- }
1083
- }
1084
- /**
1085
- * Show the design review confirmation modal
1086
- * Checks API key status first
1087
- */
1088
- showDesignReviewConfirmation() {
1089
- if (!this.sweetlinkConnected)
1090
- return;
1091
- // Request API key status from server
1092
- if (this.ws?.readyState === WebSocket.OPEN) {
1093
- this.ws.send(JSON.stringify({ type: 'check-api-key' }));
1094
- }
1095
- // Show the confirmation modal
1096
- this.showDesignReviewConfirm = true;
1097
- this.showOutlineModal = false;
1098
- this.showSchemaModal = false;
1099
- this.consoleFilter = null;
1100
- this.render();
1101
- }
1102
- /**
1103
- * Calculate estimated cost for design review based on viewport size
1104
- */
1105
- calculateCostEstimate() {
1106
- if (!this.apiKeyStatus?.pricing)
1107
- return null;
1108
- // Image token estimation for Claude Vision:
1109
- // Images are resized to fit within a bounding box, then tokenized
1110
- // Rough estimate: ~1 token per 1.5x1.5 pixels, or (width * height) / 750
1111
- const width = window.innerWidth;
1112
- const height = window.innerHeight;
1113
- const imageTokens = Math.ceil((width * height) / 750);
1114
- // Prompt is ~500 tokens, output up to 2048 tokens
1115
- const promptTokens = 500;
1116
- const estimatedOutputTokens = 1500; // Conservative estimate
1117
- const totalInputTokens = imageTokens + promptTokens;
1118
- const { input: inputPrice, output: outputPrice } = this.apiKeyStatus.pricing;
1119
- const inputCost = (totalInputTokens / 1000000) * inputPrice;
1120
- const outputCost = (estimatedOutputTokens / 1000000) * outputPrice;
1121
- const totalCost = inputCost + outputCost;
1122
- return {
1123
- tokens: totalInputTokens + estimatedOutputTokens,
1124
- cost: totalCost < 0.01 ? '<$0.01' : `~$${totalCost.toFixed(2)}`,
1125
- };
1126
- }
1127
- /**
1128
- * Close the design review confirmation modal
1129
- */
1130
- closeDesignReviewConfirm() {
1131
- this.showDesignReviewConfirm = false;
1132
- this.apiKeyStatus = null; // Reset so it's re-fetched next time
1133
- this.render();
1134
- }
1135
- /**
1136
- * Proceed with design review after confirmation
1137
- */
1138
- proceedWithDesignReview() {
1139
- this.showDesignReviewConfirm = false;
1140
- this.handleDesignReview();
1141
- }
1142
- handleDocumentOutline() {
1143
- // Toggle outline modal
1144
- this.showOutlineModal = !this.showOutlineModal;
1145
- this.showSchemaModal = false;
1146
- this.consoleFilter = null;
1147
- this.render();
1148
- }
1149
- handlePageSchema() {
1150
- // Toggle schema modal
1151
- this.showSchemaModal = !this.showSchemaModal;
1152
- this.showOutlineModal = false;
1153
- this.consoleFilter = null;
1154
- this.render();
1155
- }
1156
- handleSaveOutline() {
1157
- if (this.savingOutline)
1158
- return; // Prevent repeated clicks
1159
- const outline = extractDocumentOutline();
1160
- const markdown = outlineToMarkdown(outline);
1161
- if (this.ws?.readyState === WebSocket.OPEN) {
1162
- this.savingOutline = true;
1163
- this.render();
1164
- this.ws.send(JSON.stringify({
1165
- type: 'save-outline',
1166
- data: {
1167
- outline,
1168
- markdown,
1169
- url: window.location.href,
1170
- title: document.title,
1171
- timestamp: Date.now(),
1172
- },
1173
- }));
1174
- }
1175
- }
1176
- handleSaveSchema() {
1177
- if (this.savingSchema)
1178
- return; // Prevent repeated clicks
1179
- const schema = extractPageSchema();
1180
- const markdown = schemaToMarkdown(schema);
1181
- if (this.ws?.readyState === WebSocket.OPEN) {
1182
- this.savingSchema = true;
1183
- this.render();
1184
- this.ws.send(JSON.stringify({
1185
- type: 'save-schema',
1186
- data: {
1187
- schema,
1188
- markdown,
1189
- url: window.location.href,
1190
- title: document.title,
1191
- timestamp: Date.now(),
1192
- },
1193
- }));
1194
- }
1195
- }
1196
- clearConsoleLogs() {
1197
- // Clear the logs array
1198
- earlyConsoleCapture.logs = [];
1199
- earlyConsoleCapture.errorCount = 0;
1200
- earlyConsoleCapture.warningCount = 0;
1201
- this.consoleLogs = [];
1202
- this.consoleFilter = null;
1203
- this.render();
1204
- }
1205
411
  // ============================================================================
1206
- // Render Methods (Using Safe DOM Methods)
412
+ // Render (delegates to rendering module)
1207
413
  // ============================================================================
1208
414
  render() {
1209
- if (this.destroyed)
1210
- return;
1211
- if (typeof document === 'undefined')
1212
- return;
1213
- // Remove existing container if any
1214
- if (this.container) {
1215
- this.container.remove();
1216
- }
1217
- // Create new container
1218
- this.container = document.createElement('div');
1219
- this.container.setAttribute('data-devbar', 'true');
1220
- if (this.collapsed) {
1221
- this.renderCollapsed();
1222
- }
1223
- else if (this.compactMode) {
1224
- this.renderCompact();
1225
- }
1226
- else {
1227
- this.renderExpanded();
1228
- }
1229
- document.body.appendChild(this.container);
1230
- // Render overlays/modals
1231
- this.renderOverlays();
1232
- }
1233
- renderOverlays() {
1234
- // Remove existing overlay
1235
- if (this.overlayElement) {
1236
- this.overlayElement.remove();
1237
- this.overlayElement = null;
1238
- }
1239
- // Render console popup if filter is active
1240
- if (this.consoleFilter) {
1241
- this.renderConsolePopup();
1242
- }
1243
- // Render outline modal
1244
- if (this.showOutlineModal) {
1245
- this.renderOutlineModal();
1246
- }
1247
- // Render schema modal
1248
- if (this.showSchemaModal) {
1249
- this.renderSchemaModal();
1250
- }
1251
- // Render design review confirmation modal
1252
- if (this.showDesignReviewConfirm) {
1253
- this.renderDesignReviewConfirmModal();
1254
- }
1255
- // Render settings popover
1256
- if (this.showSettingsPopover) {
1257
- this.renderSettingsPopover();
1258
- }
1259
- }
1260
- renderDesignReviewConfirmModal() {
1261
- const color = BUTTON_COLORS.review;
1262
- const closeModal = () => this.closeDesignReviewConfirm();
1263
- const overlay = createModalOverlay(closeModal);
1264
- // Override z-index for this modal to be above others
1265
- overlay.style.zIndex = '10003';
1266
- const modal = createModalBox(color);
1267
- modal.style.maxWidth = '450px';
1268
- // Header with title and close button
1269
- const header = document.createElement('div');
1270
- Object.assign(header.style, {
1271
- display: 'flex',
1272
- alignItems: 'center',
1273
- justifyContent: 'space-between',
1274
- padding: '14px 18px',
1275
- borderBottom: `1px solid ${color}40`,
1276
- backgroundColor: `${color}15`,
1277
- });
1278
- const title = document.createElement('span');
1279
- Object.assign(title.style, { color, fontSize: '0.875rem', fontWeight: '600' });
1280
- title.textContent = 'AI Design Review';
1281
- header.appendChild(title);
1282
- const closeBtn = createStyledButton({
1283
- color: COLORS.textMuted,
1284
- text: '×',
1285
- padding: '0',
1286
- fontSize: '1.25rem',
1287
- });
1288
- closeBtn.style.border = 'none';
1289
- closeBtn.onclick = closeModal;
1290
- header.appendChild(closeBtn);
1291
- modal.appendChild(header);
1292
- // Content
1293
- const content = document.createElement('div');
1294
- Object.assign(content.style, {
1295
- padding: '18px',
1296
- color: COLORS.text,
1297
- fontSize: '0.8125rem',
1298
- lineHeight: '1.6',
1299
- });
1300
- if (this.apiKeyStatus === null) {
1301
- content.appendChild(createEmptyMessage('Checking API key configuration...'));
1302
- }
1303
- else if (!this.apiKeyStatus.configured) {
1304
- content.appendChild(this.renderApiKeyNotConfiguredContent());
1305
- }
1306
- else {
1307
- content.appendChild(this.renderApiKeyConfiguredContent());
1308
- }
1309
- modal.appendChild(content);
1310
- // Footer with buttons
1311
- const footer = document.createElement('div');
1312
- Object.assign(footer.style, {
1313
- display: 'flex',
1314
- justifyContent: 'flex-end',
1315
- gap: '10px',
1316
- padding: '14px 18px',
1317
- borderTop: `1px solid ${COLORS.border}`,
1318
- });
1319
- const cancelBtn = createStyledButton({
1320
- color: COLORS.textMuted,
1321
- text: 'Cancel',
1322
- padding: '8px 16px',
1323
- });
1324
- cancelBtn.onclick = closeModal;
1325
- footer.appendChild(cancelBtn);
1326
- if (this.apiKeyStatus?.configured) {
1327
- const proceedBtn = createStyledButton({ color, text: 'Run Review', padding: '8px 16px' });
1328
- proceedBtn.style.backgroundColor = `${color}20`;
1329
- proceedBtn.onclick = () => this.proceedWithDesignReview();
1330
- footer.appendChild(proceedBtn);
1331
- }
1332
- modal.appendChild(footer);
1333
- overlay.appendChild(modal);
1334
- document.body.appendChild(overlay);
1335
- }
1336
- /**
1337
- * Render content when API key is not configured
1338
- */
1339
- renderApiKeyNotConfiguredContent() {
1340
- const wrapper = document.createElement('div');
1341
- wrapper.appendChild(createInfoBox(COLORS.error, 'API Key Not Configured', 'The ANTHROPIC_API_KEY environment variable is not set.'));
1342
- // Instructions
1343
- const instructions = document.createElement('div');
1344
- Object.assign(instructions.style, { marginBottom: '12px' });
1345
- const instructTitle = document.createElement('div');
1346
- Object.assign(instructTitle.style, {
1347
- color: COLORS.textSecondary,
1348
- fontWeight: '600',
1349
- marginBottom: '8px',
1350
- });
1351
- instructTitle.textContent = 'To configure:';
1352
- instructions.appendChild(instructTitle);
1353
- const steps = [
1354
- { text: '1. Get an API key from console.anthropic.com', highlight: false },
1355
- { text: '2. Add to your .env file:', highlight: false },
1356
- { text: ' ANTHROPIC_API_KEY=sk-ant-...', highlight: true },
1357
- { text: '3. Restart your dev server', highlight: false },
1358
- ];
1359
- steps.forEach(({ text, highlight }) => {
1360
- const stepDiv = document.createElement('div');
1361
- Object.assign(stepDiv.style, {
1362
- color: highlight ? COLORS.primary : COLORS.textMuted,
1363
- fontSize: '0.75rem',
1364
- marginBottom: '4px',
1365
- fontFamily: FONT_MONO,
1366
- });
1367
- stepDiv.textContent = text;
1368
- instructions.appendChild(stepDiv);
1369
- });
1370
- wrapper.appendChild(instructions);
1371
- return wrapper;
1372
- }
1373
- /**
1374
- * Render content when API key is configured (cost estimate and model info)
1375
- */
1376
- renderApiKeyConfiguredContent() {
1377
- const wrapper = document.createElement('div');
1378
- Object.assign(wrapper.style, { marginBottom: '16px' });
1379
- const desc = document.createElement('p');
1380
- Object.assign(desc.style, { color: COLORS.textSecondary, marginBottom: '12px' });
1381
- desc.textContent = 'This will capture a screenshot and send it to Claude for design analysis.';
1382
- wrapper.appendChild(desc);
1383
- // Cost estimate
1384
- const estimate = this.calculateCostEstimate();
1385
- if (estimate) {
1386
- const costBox = createInfoBox(COLORS.primary, 'Estimated Cost', []);
1387
- // Remove default margin and adjust padding
1388
- costBox.style.marginBottom = '0';
1389
- costBox.style.padding = '12px';
1390
- const costDetails = document.createElement('div');
1391
- Object.assign(costDetails.style, {
1392
- display: 'flex',
1393
- justifyContent: 'space-between',
1394
- color: COLORS.textSecondary,
1395
- fontSize: '0.75rem',
1396
- });
1397
- const tokensSpan = document.createElement('span');
1398
- tokensSpan.textContent = `~${estimate.tokens.toLocaleString()} tokens`;
1399
- costDetails.appendChild(tokensSpan);
1400
- const priceSpan = document.createElement('span');
1401
- Object.assign(priceSpan.style, { color: COLORS.warning, fontWeight: '600' });
1402
- priceSpan.textContent = estimate.cost;
1403
- costDetails.appendChild(priceSpan);
1404
- costBox.appendChild(costDetails);
1405
- wrapper.appendChild(costBox);
1406
- }
1407
- // Model info
1408
- if (this.apiKeyStatus?.model) {
1409
- const modelDiv = document.createElement('div');
1410
- Object.assign(modelDiv.style, {
1411
- color: COLORS.textMuted,
1412
- fontSize: '0.6875rem',
1413
- marginTop: '12px',
1414
- });
1415
- modelDiv.textContent = `Model: ${this.apiKeyStatus.model}`;
1416
- if (this.apiKeyStatus.maskedKey) {
1417
- modelDiv.textContent += ` | Key: ${this.apiKeyStatus.maskedKey}`;
1418
- }
1419
- wrapper.appendChild(modelDiv);
1420
- }
1421
- return wrapper;
1422
- }
1423
- renderConsolePopup() {
1424
- const filterType = this.consoleFilter;
1425
- if (!filterType)
1426
- return;
1427
- const logs = earlyConsoleCapture.logs.filter((log) => log.level === filterType);
1428
- const color = filterType === 'error' ? BUTTON_COLORS.error : BUTTON_COLORS.warning;
1429
- const label = filterType === 'error' ? 'Errors' : 'Warnings';
1430
- const popup = document.createElement('div');
1431
- popup.setAttribute('data-devbar', 'true');
1432
- Object.assign(popup.style, {
1433
- position: 'fixed',
1434
- bottom: '60px',
1435
- left: '80px',
1436
- zIndex: '10002',
1437
- backgroundColor: 'rgba(17, 24, 39, 0.98)',
1438
- border: `1px solid ${color}`,
1439
- borderRadius: '8px',
1440
- boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${color}33`,
1441
- backdropFilter: 'blur(8px)',
1442
- WebkitBackdropFilter: 'blur(8px)',
1443
- minWidth: '400px',
1444
- maxWidth: '600px',
1445
- maxHeight: '400px',
1446
- display: 'flex',
1447
- flexDirection: 'column',
1448
- fontFamily: FONT_MONO,
1449
- });
1450
- // Header
1451
- const header = document.createElement('div');
1452
- Object.assign(header.style, {
1453
- display: 'flex',
1454
- alignItems: 'center',
1455
- justifyContent: 'space-between',
1456
- padding: '10px 14px',
1457
- borderBottom: `1px solid ${color}40`,
1458
- });
1459
- const title = document.createElement('span');
1460
- Object.assign(title.style, { color, fontSize: '0.8125rem', fontWeight: '600' });
1461
- title.textContent = `Console ${label} (${logs.length})`;
1462
- header.appendChild(title);
1463
- const headerButtons = document.createElement('div');
1464
- Object.assign(headerButtons.style, { display: 'flex', gap: '8px' });
1465
- // Clear button
1466
- const clearBtn = createStyledButton({
1467
- color,
1468
- text: 'Clear All',
1469
- padding: '4px 10px',
1470
- borderRadius: '4px',
1471
- fontSize: '0.6875rem',
1472
- });
1473
- clearBtn.onclick = () => this.clearConsoleLogs();
1474
- headerButtons.appendChild(clearBtn);
1475
- // Close button - match Clear button padding for consistent height
1476
- const closeBtn = createStyledButton({
1477
- color,
1478
- text: '×',
1479
- padding: '4px 8px',
1480
- borderRadius: '4px',
1481
- fontSize: '0.75rem',
1482
- });
1483
- closeBtn.onclick = () => {
1484
- this.consoleFilter = null;
1485
- this.render();
1486
- };
1487
- headerButtons.appendChild(closeBtn);
1488
- header.appendChild(headerButtons);
1489
- popup.appendChild(header);
1490
- // Content
1491
- const content = document.createElement('div');
1492
- Object.assign(content.style, { flex: '1', overflow: 'auto', padding: '8px 0' });
1493
- if (logs.length === 0) {
1494
- const emptyMsg = document.createElement('div');
1495
- Object.assign(emptyMsg.style, {
1496
- padding: '20px',
1497
- textAlign: 'center',
1498
- color: COLORS.textMuted,
1499
- fontSize: '0.75rem',
1500
- });
1501
- emptyMsg.textContent = `No ${filterType}s recorded`;
1502
- content.appendChild(emptyMsg);
1503
- }
1504
- else {
1505
- this.renderConsoleLogs(content, logs, color);
1506
- }
1507
- popup.appendChild(content);
1508
- this.overlayElement = popup;
1509
- document.body.appendChild(popup);
1510
- }
1511
- /**
1512
- * Render console log items into a container
1513
- */
1514
- renderConsoleLogs(container, logs, color) {
1515
- logs.forEach((log, index) => {
1516
- const logItem = document.createElement('div');
1517
- Object.assign(logItem.style, {
1518
- padding: '8px 14px',
1519
- borderBottom: index < logs.length - 1 ? '1px solid rgba(255, 255, 255, 0.05)' : 'none',
1520
- });
1521
- const timestamp = document.createElement('span');
1522
- Object.assign(timestamp.style, {
1523
- color: COLORS.textMuted,
1524
- fontSize: '0.625rem',
1525
- marginRight: '8px',
1526
- });
1527
- timestamp.textContent = new Date(log.timestamp).toLocaleTimeString();
1528
- logItem.appendChild(timestamp);
1529
- const message = document.createElement('span');
1530
- Object.assign(message.style, {
1531
- color,
1532
- fontSize: '0.6875rem',
1533
- wordBreak: 'break-word',
1534
- whiteSpace: 'pre-wrap',
1535
- });
1536
- message.textContent =
1537
- log.message.length > 500 ? `${log.message.slice(0, 500)}...` : log.message;
1538
- logItem.appendChild(message);
1539
- container.appendChild(logItem);
1540
- });
1541
- }
1542
- renderOutlineModal() {
1543
- const outline = extractDocumentOutline();
1544
- const color = BUTTON_COLORS.outline;
1545
- const closeModal = () => {
1546
- this.showOutlineModal = false;
1547
- this.render();
1548
- };
1549
- const overlay = createModalOverlay(closeModal);
1550
- const modal = createModalBox(color);
1551
- const header = createModalHeader({
1552
- color,
1553
- title: 'Document Outline',
1554
- onClose: closeModal,
1555
- onCopyMd: async () => {
1556
- const markdown = outlineToMarkdown(outline);
1557
- await navigator.clipboard.writeText(markdown);
1558
- },
1559
- onSave: () => this.handleSaveOutline(),
1560
- sweetlinkConnected: this.sweetlinkConnected,
1561
- isSaving: this.savingOutline,
1562
- savedPath: this.lastOutline,
1563
- });
1564
- modal.appendChild(header);
1565
- const content = createModalContent();
1566
- if (outline.length === 0) {
1567
- content.appendChild(createEmptyMessage('No semantic elements found in this document'));
1568
- }
1569
- else {
1570
- this.renderOutlineNodes(outline, content, 0);
1571
- }
1572
- modal.appendChild(content);
1573
- overlay.appendChild(modal);
1574
- this.overlayElement = overlay;
1575
- document.body.appendChild(overlay);
1576
- }
1577
- /**
1578
- * Recursively render outline nodes into a container element
1579
- */
1580
- renderOutlineNodes(nodes, parentEl, depth) {
1581
- for (const node of nodes) {
1582
- const nodeEl = document.createElement('div');
1583
- Object.assign(nodeEl.style, {
1584
- padding: `4px 0 4px ${depth * 16}px`,
1585
- });
1586
- const tagSpan = document.createElement('span');
1587
- const categoryColor = CATEGORY_COLORS[node.category || 'other'] || CATEGORY_COLORS.other;
1588
- Object.assign(tagSpan.style, {
1589
- color: categoryColor,
1590
- fontSize: '0.6875rem',
1591
- fontWeight: '500',
1592
- });
1593
- tagSpan.textContent = `<${node.tagName}>`;
1594
- nodeEl.appendChild(tagSpan);
1595
- if (node.category) {
1596
- const categorySpan = document.createElement('span');
1597
- Object.assign(categorySpan.style, {
1598
- color: COLORS.textMuted,
1599
- fontSize: '0.625rem',
1600
- marginLeft: '6px',
1601
- });
1602
- categorySpan.textContent = `[${node.category}]`;
1603
- nodeEl.appendChild(categorySpan);
1604
- }
1605
- const textSpan = document.createElement('span');
1606
- Object.assign(textSpan.style, {
1607
- color: '#d1d5db',
1608
- fontSize: '0.6875rem',
1609
- marginLeft: '8px',
1610
- });
1611
- const truncatedText = node.text.length > 60 ? `${node.text.slice(0, 60)}...` : node.text;
1612
- textSpan.textContent = truncatedText;
1613
- nodeEl.appendChild(textSpan);
1614
- if (node.id) {
1615
- const idSpan = document.createElement('span');
1616
- Object.assign(idSpan.style, {
1617
- color: '#9ca3af',
1618
- fontSize: '0.625rem',
1619
- marginLeft: '6px',
1620
- });
1621
- idSpan.textContent = `#${node.id}`;
1622
- nodeEl.appendChild(idSpan);
1623
- }
1624
- parentEl.appendChild(nodeEl);
1625
- if (node.children.length > 0) {
1626
- this.renderOutlineNodes(node.children, parentEl, depth + 1);
1627
- }
1628
- }
1629
- }
1630
- renderSchemaModal() {
1631
- const schema = extractPageSchema();
1632
- const color = BUTTON_COLORS.schema;
1633
- const closeModal = () => {
1634
- this.showSchemaModal = false;
1635
- this.render();
1636
- };
1637
- const overlay = createModalOverlay(closeModal);
1638
- const modal = createModalBox(color);
1639
- const header = createModalHeader({
1640
- color,
1641
- title: 'Page Schema',
1642
- onClose: closeModal,
1643
- onCopyMd: async () => {
1644
- const markdown = schemaToMarkdown(schema);
1645
- await navigator.clipboard.writeText(markdown);
1646
- },
1647
- onSave: () => this.handleSaveSchema(),
1648
- sweetlinkConnected: this.sweetlinkConnected,
1649
- isSaving: this.savingSchema,
1650
- savedPath: this.lastSchema,
1651
- });
1652
- modal.appendChild(header);
1653
- const content = createModalContent();
1654
- const hasContent = schema.jsonLd.length > 0 ||
1655
- Object.keys(schema.openGraph).length > 0 ||
1656
- Object.keys(schema.twitter).length > 0 ||
1657
- Object.keys(schema.metaTags).length > 0;
1658
- if (!hasContent) {
1659
- content.appendChild(createEmptyMessage('No structured data found on this page'));
1660
- }
1661
- else {
1662
- this.renderSchemaSection(content, 'JSON-LD', schema.jsonLd, color);
1663
- this.renderSchemaSection(content, 'Open Graph', schema.openGraph, COLORS.info);
1664
- this.renderSchemaSection(content, 'Twitter Cards', schema.twitter, COLORS.cyan);
1665
- this.renderSchemaSection(content, 'Meta Tags', schema.metaTags, COLORS.textMuted);
1666
- }
1667
- modal.appendChild(content);
1668
- overlay.appendChild(modal);
1669
- this.overlayElement = overlay;
1670
- document.body.appendChild(overlay);
1671
- }
1672
- /**
1673
- * Render a section of schema data (either array or key-value object)
1674
- */
1675
- renderSchemaSection(container, title, items, color) {
1676
- const isEmpty = Array.isArray(items) ? items.length === 0 : Object.keys(items).length === 0;
1677
- if (isEmpty)
1678
- return;
1679
- const section = document.createElement('div');
1680
- section.style.marginBottom = '20px';
1681
- const sectionTitle = document.createElement('h3');
1682
- Object.assign(sectionTitle.style, {
1683
- color,
1684
- fontSize: '0.8125rem',
1685
- fontWeight: '600',
1686
- marginBottom: '10px',
1687
- borderBottom: `1px solid ${color}40`,
1688
- paddingBottom: '6px',
1689
- });
1690
- sectionTitle.textContent = title;
1691
- section.appendChild(sectionTitle);
1692
- if (Array.isArray(items)) {
1693
- this.renderJsonLdItems(section, items);
1694
- }
1695
- else {
1696
- this.renderKeyValueItems(section, items);
1697
- }
1698
- container.appendChild(section);
1699
- }
1700
- /**
1701
- * Render JSON-LD items as formatted code blocks with syntax highlighting
1702
- */
1703
- renderJsonLdItems(container, items) {
1704
- items.forEach((item, i) => {
1705
- const itemEl = document.createElement('div');
1706
- itemEl.style.marginBottom = '10px';
1707
- const itemTitle = document.createElement('div');
1708
- Object.assign(itemTitle.style, {
1709
- color: '#9ca3af',
1710
- fontSize: '0.6875rem',
1711
- marginBottom: '4px',
1712
- });
1713
- itemTitle.textContent = `Schema ${i + 1}`;
1714
- itemEl.appendChild(itemTitle);
1715
- const codeEl = document.createElement('pre');
1716
- Object.assign(codeEl.style, {
1717
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
1718
- borderRadius: '4px',
1719
- padding: '10px',
1720
- overflow: 'auto',
1721
- fontSize: '0.625rem',
1722
- margin: '0',
1723
- maxHeight: '300px', // Taller for more content
1724
- });
1725
- // Syntax highlight the JSON using DOM methods for safety
1726
- this.appendHighlightedJson(codeEl, JSON.stringify(item, null, 2));
1727
- itemEl.appendChild(codeEl);
1728
- container.appendChild(itemEl);
1729
- });
1730
- }
1731
- /**
1732
- * Append syntax-highlighted JSON to an element using safe DOM methods
1733
- * Uses textContent for all text to prevent XSS
1734
- */
1735
- appendHighlightedJson(container, json) {
1736
- // Color map for different token types
1737
- const colors = {
1738
- key: COLORS.primary, // green
1739
- string: COLORS.warning, // amber/yellow
1740
- number: COLORS.purple, // purple
1741
- boolean: COLORS.info, // blue
1742
- nullVal: COLORS.error, // red
1743
- punct: COLORS.textMuted, // gray
1744
- };
1745
- // Simple tokenizer for JSON using matchAll for safety
1746
- const tokenPattern = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)|([{}[\],])|(\s+)/g;
1747
- for (const match of json.matchAll(tokenPattern)) {
1748
- const [, str, colon, bool, nullToken, num, punct, whitespace] = match;
1749
- if (whitespace) {
1750
- container.appendChild(document.createTextNode(whitespace));
1751
- }
1752
- else if (str !== undefined) {
1753
- const span = document.createElement('span');
1754
- span.style.color = colon ? colors.key : colors.string;
1755
- span.textContent = str;
1756
- container.appendChild(span);
1757
- if (colon) {
1758
- const colonSpan = document.createElement('span');
1759
- colonSpan.style.color = colors.punct;
1760
- colonSpan.textContent = ':';
1761
- container.appendChild(colonSpan);
1762
- }
1763
- }
1764
- else if (bool) {
1765
- const span = document.createElement('span');
1766
- span.style.color = colors.boolean;
1767
- span.textContent = bool;
1768
- container.appendChild(span);
1769
- }
1770
- else if (nullToken) {
1771
- const span = document.createElement('span');
1772
- span.style.color = colors.nullVal;
1773
- span.textContent = nullToken;
1774
- container.appendChild(span);
1775
- }
1776
- else if (num) {
1777
- const span = document.createElement('span');
1778
- span.style.color = colors.number;
1779
- span.textContent = num;
1780
- container.appendChild(span);
1781
- }
1782
- else if (punct) {
1783
- const span = document.createElement('span');
1784
- span.style.color = colors.punct;
1785
- span.textContent = punct;
1786
- container.appendChild(span);
1787
- }
1788
- }
1789
- }
1790
- /**
1791
- * Render key-value pairs as rows with ellipsis overflow and hover tooltip
1792
- */
1793
- renderKeyValueItems(container, items) {
1794
- for (const [key, value] of Object.entries(items)) {
1795
- const row = document.createElement('div');
1796
- Object.assign(row.style, {
1797
- display: 'flex',
1798
- marginBottom: '4px',
1799
- alignItems: 'flex-start',
1800
- });
1801
- const keyEl = document.createElement('span');
1802
- Object.assign(keyEl.style, {
1803
- color: '#9ca3af',
1804
- fontSize: '0.6875rem',
1805
- width: '120px',
1806
- minWidth: '120px',
1807
- maxWidth: '120px',
1808
- flexShrink: '0',
1809
- overflow: 'hidden',
1810
- textOverflow: 'ellipsis',
1811
- whiteSpace: 'nowrap',
1812
- });
1813
- keyEl.textContent = key;
1814
- // Show full key on hover if it might be truncated
1815
- if (key.length > 18) {
1816
- keyEl.title = key;
1817
- }
1818
- row.appendChild(keyEl);
1819
- const valueEl = document.createElement('span');
1820
- const strValue = String(value);
1821
- Object.assign(valueEl.style, {
1822
- color: '#d1d5db',
1823
- fontSize: '0.6875rem',
1824
- flex: '1',
1825
- wordBreak: 'break-word',
1826
- whiteSpace: 'pre-wrap',
1827
- });
1828
- valueEl.textContent = strValue;
1829
- row.appendChild(valueEl);
1830
- container.appendChild(row);
1831
- }
1832
- }
1833
- /**
1834
- * Render compact mode - single row with essential controls only
1835
- * Shows: connection dot, error/warn badges, screenshot button, settings gear
1836
- */
1837
- renderCompact() {
1838
- if (!this.container)
1839
- return;
1840
- const { position, accentColor } = this.options;
1841
- const { errorCount, warningCount } = this.getLogCounts();
1842
- const positionStyles = {
1843
- 'bottom-left': { bottom: '20px', left: '80px' },
1844
- 'bottom-right': { bottom: '20px', right: '16px' },
1845
- 'top-left': { top: '20px', left: '80px' },
1846
- 'top-right': { top: '20px', right: '16px' },
1847
- 'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
1848
- };
1849
- const posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
1850
- const wrapper = this.container;
1851
- // Reset position properties first
1852
- wrapper.style.top = '';
1853
- wrapper.style.bottom = '';
1854
- wrapper.style.left = '';
1855
- wrapper.style.right = '';
1856
- wrapper.style.transform = '';
1857
- Object.assign(wrapper.style, {
1858
- position: 'fixed',
1859
- ...posStyle,
1860
- zIndex: '9999',
1861
- backgroundColor: 'rgba(17, 24, 39, 0.95)',
1862
- border: `1px solid ${accentColor}`,
1863
- borderRadius: '20px',
1864
- color: accentColor,
1865
- boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
1866
- backdropFilter: 'blur(8px)',
1867
- WebkitBackdropFilter: 'blur(8px)',
1868
- padding: '6px 10px',
1869
- display: 'flex',
1870
- alignItems: 'center',
1871
- gap: '8px',
1872
- fontFamily: FONT_MONO,
1873
- fontSize: '0.6875rem',
1874
- });
1875
- // Connection indicator
1876
- const connIndicator = document.createElement('span');
1877
- connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
1878
- connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected ? 'Sweetlink connected' : 'Sweetlink disconnected');
1879
- Object.assign(connIndicator.style, {
1880
- width: '12px',
1881
- height: '12px',
1882
- borderRadius: '50%',
1883
- display: 'flex',
1884
- alignItems: 'center',
1885
- justifyContent: 'center',
1886
- cursor: 'pointer',
1887
- });
1888
- connIndicator.onclick = (e) => {
1889
- e.stopPropagation();
1890
- this.collapsed = true;
1891
- this.debug.state('Collapsed DevBar from compact mode');
1892
- this.render();
1893
- };
1894
- const connDot = document.createElement('span');
1895
- Object.assign(connDot.style, {
1896
- width: '6px',
1897
- height: '6px',
1898
- borderRadius: '50%',
1899
- backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
1900
- boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
1901
- });
1902
- connIndicator.appendChild(connDot);
1903
- wrapper.appendChild(connIndicator);
1904
- // Error badge
1905
- if (errorCount > 0) {
1906
- wrapper.appendChild(this.createConsoleBadge('error', errorCount, BUTTON_COLORS.error));
1907
- }
1908
- // Warning badge
1909
- if (warningCount > 0) {
1910
- wrapper.appendChild(this.createConsoleBadge('warn', warningCount, BUTTON_COLORS.warning));
1911
- }
1912
- // Screenshot button (if enabled)
1913
- if (this.options.showScreenshot) {
1914
- wrapper.appendChild(this.createScreenshotButton(accentColor));
1915
- }
1916
- // Settings gear button
1917
- wrapper.appendChild(this.createSettingsButton());
1918
- // Expand button (double-arrow)
1919
- const expandBtn = document.createElement('button');
1920
- expandBtn.type = 'button';
1921
- expandBtn.className = this.tooltipClass('right');
1922
- expandBtn.setAttribute('data-tooltip', 'Expand DevBar');
1923
- Object.assign(expandBtn.style, {
1924
- display: 'flex',
1925
- alignItems: 'center',
1926
- justifyContent: 'center',
1927
- width: '18px',
1928
- height: '18px',
1929
- borderRadius: '50%',
1930
- border: `1px solid ${accentColor}60`,
1931
- backgroundColor: 'transparent',
1932
- color: `${accentColor}99`,
1933
- cursor: 'pointer',
1934
- fontSize: '0.5rem',
1935
- transition: 'all 150ms',
1936
- });
1937
- expandBtn.textContent = '⟫';
1938
- expandBtn.onmouseenter = () => {
1939
- expandBtn.style.backgroundColor = `${accentColor}20`;
1940
- expandBtn.style.borderColor = accentColor;
1941
- expandBtn.style.color = accentColor;
1942
- };
1943
- expandBtn.onmouseleave = () => {
1944
- expandBtn.style.backgroundColor = 'transparent';
1945
- expandBtn.style.borderColor = `${accentColor}60`;
1946
- expandBtn.style.color = `${accentColor}99`;
1947
- };
1948
- expandBtn.onclick = () => {
1949
- this.toggleCompactMode();
1950
- };
1951
- wrapper.appendChild(expandBtn);
1952
- }
1953
- /**
1954
- * Create the settings gear button
1955
- */
1956
- createSettingsButton() {
1957
- const btn = document.createElement('button');
1958
- btn.type = 'button';
1959
- btn.className = this.tooltipClass('right');
1960
- btn.setAttribute('data-tooltip', 'Settings (Cmd+Shift+M: toggle compact)');
1961
- const isActive = this.showSettingsPopover;
1962
- const color = COLORS.textSecondary;
1963
- Object.assign(btn.style, {
1964
- display: 'flex',
1965
- alignItems: 'center',
1966
- justifyContent: 'center',
1967
- width: '22px',
1968
- height: '22px',
1969
- minWidth: '22px',
1970
- minHeight: '22px',
1971
- flexShrink: '0',
1972
- borderRadius: '50%',
1973
- border: `1px solid ${isActive ? color : `${color}60`}`,
1974
- backgroundColor: isActive ? `${color}20` : 'transparent',
1975
- color: isActive ? color : `${color}99`,
1976
- cursor: 'pointer',
1977
- transition: 'all 150ms',
1978
- });
1979
- btn.onclick = () => {
1980
- this.showSettingsPopover = !this.showSettingsPopover;
1981
- this.consoleFilter = null;
1982
- this.showOutlineModal = false;
1983
- this.showSchemaModal = false;
1984
- this.showDesignReviewConfirm = false;
1985
- this.render();
1986
- };
1987
- // Gear icon SVG
1988
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1989
- svg.setAttribute('width', '12');
1990
- svg.setAttribute('height', '12');
1991
- svg.setAttribute('viewBox', '0 0 24 24');
1992
- svg.setAttribute('fill', 'none');
1993
- svg.setAttribute('stroke', 'currentColor');
1994
- svg.setAttribute('stroke-width', '2');
1995
- svg.setAttribute('stroke-linecap', 'round');
1996
- svg.setAttribute('stroke-linejoin', 'round');
1997
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1998
- path.setAttribute('d', 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z');
1999
- svg.appendChild(path);
2000
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2001
- circle.setAttribute('cx', '12');
2002
- circle.setAttribute('cy', '12');
2003
- circle.setAttribute('r', '3');
2004
- svg.appendChild(circle);
2005
- btn.appendChild(svg);
2006
- return btn;
2007
- }
2008
- /**
2009
- * Create the compact mode toggle button with chevron icon
2010
- */
2011
- createCompactToggleButton() {
2012
- const btn = document.createElement('button');
2013
- btn.type = 'button';
2014
- btn.className = this.tooltipClass('right');
2015
- const isCompact = this.compactMode;
2016
- const tooltip = isCompact ? 'Expand (Cmd+Shift+M)' : 'Compact (Cmd+Shift+M)';
2017
- btn.setAttribute('data-tooltip', tooltip);
2018
- const { accentColor } = this.options;
2019
- const iconColor = COLORS.textSecondary;
2020
- Object.assign(btn.style, {
2021
- display: 'flex',
2022
- alignItems: 'center',
2023
- justifyContent: 'center',
2024
- width: '22px',
2025
- height: '22px',
2026
- minWidth: '22px',
2027
- minHeight: '22px',
2028
- flexShrink: '0',
2029
- borderRadius: '50%',
2030
- border: `1px solid ${accentColor}60`,
2031
- backgroundColor: 'transparent',
2032
- color: `${iconColor}99`,
2033
- cursor: 'pointer',
2034
- transition: 'all 150ms',
2035
- });
2036
- btn.onmouseenter = () => {
2037
- btn.style.borderColor = accentColor;
2038
- btn.style.backgroundColor = `${accentColor}20`;
2039
- btn.style.color = iconColor;
2040
- };
2041
- btn.onmouseleave = () => {
2042
- btn.style.borderColor = `${accentColor}60`;
2043
- btn.style.backgroundColor = 'transparent';
2044
- btn.style.color = `${iconColor}99`;
2045
- };
2046
- btn.onclick = () => {
2047
- this.toggleCompactMode();
2048
- };
2049
- // Chevron icon SVG - points right when expanded, left when compact
2050
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2051
- svg.setAttribute('width', '12');
2052
- svg.setAttribute('height', '12');
2053
- svg.setAttribute('viewBox', '0 0 24 24');
2054
- svg.setAttribute('fill', 'none');
2055
- svg.setAttribute('stroke', 'currentColor');
2056
- svg.setAttribute('stroke-width', '2');
2057
- svg.setAttribute('stroke-linecap', 'round');
2058
- svg.setAttribute('stroke-linejoin', 'round');
2059
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
2060
- // Left chevron (<) when expanded to shrink, right chevron (>) when compact to expand
2061
- path.setAttribute('points', isCompact ? '9 18 15 12 9 6' : '15 18 9 12 15 6');
2062
- svg.appendChild(path);
2063
- btn.appendChild(svg);
2064
- return btn;
2065
- }
2066
- /**
2067
- * Create a settings section with title
2068
- */
2069
- createSettingsSection(title, hasBorder = true) {
2070
- const color = COLORS.textSecondary;
2071
- const section = document.createElement('div');
2072
- Object.assign(section.style, {
2073
- padding: '10px 14px',
2074
- borderBottom: hasBorder ? `1px solid ${color}20` : 'none',
2075
- });
2076
- const sectionTitle = document.createElement('div');
2077
- Object.assign(sectionTitle.style, {
2078
- color,
2079
- fontSize: '0.625rem',
2080
- textTransform: 'uppercase',
2081
- letterSpacing: '0.1em',
2082
- marginBottom: '8px',
2083
- });
2084
- sectionTitle.textContent = title;
2085
- section.appendChild(sectionTitle);
2086
- return section;
2087
- }
2088
- /**
2089
- * Create a toggle switch row
2090
- */
2091
- createToggleRow(label, checked, accentColor, onChange) {
2092
- const color = COLORS.textSecondary;
2093
- const row = document.createElement('div');
2094
- Object.assign(row.style, {
2095
- display: 'flex',
2096
- alignItems: 'center',
2097
- justifyContent: 'space-between',
2098
- marginBottom: '6px',
2099
- });
2100
- const labelEl = document.createElement('span');
2101
- Object.assign(labelEl.style, { color: COLORS.text, fontSize: '0.6875rem' });
2102
- labelEl.textContent = label;
2103
- row.appendChild(labelEl);
2104
- const toggle = document.createElement('button');
2105
- Object.assign(toggle.style, {
2106
- width: '32px',
2107
- height: '18px',
2108
- borderRadius: '9px',
2109
- border: 'none',
2110
- backgroundColor: checked ? accentColor : `${color}40`,
2111
- position: 'relative',
2112
- cursor: 'pointer',
2113
- transition: 'all 150ms',
2114
- flexShrink: '0',
2115
- });
2116
- const knob = document.createElement('span');
2117
- Object.assign(knob.style, {
2118
- position: 'absolute',
2119
- top: '2px',
2120
- left: checked ? '16px' : '2px',
2121
- width: '14px',
2122
- height: '14px',
2123
- borderRadius: '50%',
2124
- backgroundColor: '#fff',
2125
- transition: 'left 150ms',
2126
- });
2127
- toggle.appendChild(knob);
2128
- toggle.onclick = onChange;
2129
- row.appendChild(toggle);
2130
- return row;
2131
- }
2132
- /**
2133
- * Render the settings popover
2134
- */
2135
- renderSettingsPopover() {
2136
- const { position, accentColor } = this.options;
2137
- const color = COLORS.textSecondary;
2138
- const popover = document.createElement('div');
2139
- popover.setAttribute('data-devbar', 'true');
2140
- // Position based on devbar position
2141
- const isTop = position.startsWith('top');
2142
- const isRight = position.includes('right');
2143
- Object.assign(popover.style, {
2144
- position: 'fixed',
2145
- [isTop ? 'top' : 'bottom']: isTop ? '70px' : '70px',
2146
- [isRight ? 'right' : 'left']: isRight ? '16px' : '80px',
2147
- zIndex: '10003',
2148
- backgroundColor: 'rgba(17, 24, 39, 0.98)',
2149
- border: `1px solid ${accentColor}`,
2150
- borderRadius: '8px',
2151
- boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${accentColor}33`,
2152
- backdropFilter: 'blur(8px)',
2153
- WebkitBackdropFilter: 'blur(8px)',
2154
- minWidth: '240px',
2155
- maxWidth: '280px',
2156
- maxHeight: 'calc(100vh - 100px)',
2157
- overflowY: 'auto',
2158
- fontFamily: FONT_MONO,
2159
- });
2160
- // Header
2161
- const header = document.createElement('div');
2162
- Object.assign(header.style, {
2163
- display: 'flex',
2164
- alignItems: 'center',
2165
- justifyContent: 'space-between',
2166
- padding: '10px 14px',
2167
- borderBottom: `1px solid ${accentColor}30`,
2168
- position: 'sticky',
2169
- top: '0',
2170
- backgroundColor: 'rgba(17, 24, 39, 0.98)',
2171
- zIndex: '1',
2172
- });
2173
- const title = document.createElement('span');
2174
- Object.assign(title.style, { color: accentColor, fontSize: '0.75rem', fontWeight: '600' });
2175
- title.textContent = 'Settings';
2176
- header.appendChild(title);
2177
- const closeBtn = createStyledButton({
2178
- color: COLORS.textMuted,
2179
- text: '×',
2180
- padding: '2px 6px',
2181
- fontSize: '0.875rem',
2182
- });
2183
- closeBtn.style.border = 'none';
2184
- closeBtn.onclick = () => {
2185
- this.showSettingsPopover = false;
2186
- this.render();
2187
- };
2188
- header.appendChild(closeBtn);
2189
- popover.appendChild(header);
2190
- // ========== THEME SECTION ==========
2191
- const themeSection = this.createSettingsSection('Theme');
2192
- const themeOptions = document.createElement('div');
2193
- Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
2194
- const themeModes = ['system', 'dark', 'light'];
2195
- themeModes.forEach((mode) => {
2196
- const btn = document.createElement('button');
2197
- const isActive = this.themeMode === mode;
2198
- Object.assign(btn.style, {
2199
- padding: '4px 10px',
2200
- backgroundColor: isActive ? `${accentColor}20` : 'transparent',
2201
- border: `1px solid ${isActive ? accentColor : `${color}40`}`,
2202
- borderRadius: '4px',
2203
- color: isActive ? accentColor : color,
2204
- fontSize: '0.625rem',
2205
- cursor: 'pointer',
2206
- textTransform: 'capitalize',
2207
- transition: 'all 150ms',
2208
- });
2209
- btn.textContent = mode;
2210
- btn.onclick = () => {
2211
- this.setThemeMode(mode);
2212
- };
2213
- themeOptions.appendChild(btn);
2214
- });
2215
- themeSection.appendChild(themeOptions);
2216
- popover.appendChild(themeSection);
2217
- // ========== DISPLAY SECTION ==========
2218
- const displaySection = this.createSettingsSection('Display');
2219
- // Position mini-map selector
2220
- const positionRow = document.createElement('div');
2221
- Object.assign(positionRow.style, { marginBottom: '10px' });
2222
- const posLabel = document.createElement('div');
2223
- Object.assign(posLabel.style, {
2224
- color: COLORS.text,
2225
- fontSize: '0.6875rem',
2226
- marginBottom: '6px',
2227
- });
2228
- posLabel.textContent = 'Position';
2229
- positionRow.appendChild(posLabel);
2230
- // Mini-map container
2231
- const miniMap = document.createElement('div');
2232
- Object.assign(miniMap.style, {
2233
- position: 'relative',
2234
- width: '100%',
2235
- height: '50px',
2236
- backgroundColor: 'rgba(10, 15, 26, 0.6)',
2237
- border: `1px solid ${color}30`,
2238
- borderRadius: '4px',
2239
- });
2240
- const positionConfigs = [
2241
- { value: 'top-left', style: { top: '8px', left: '10%' }, title: 'Top Left' },
2242
- { value: 'top-right', style: { top: '8px', right: '6%' }, title: 'Top Right' },
2243
- { value: 'bottom-left', style: { bottom: '8px', left: '10%' }, title: 'Bottom Left' },
2244
- { value: 'bottom-right', style: { bottom: '8px', right: '6%' }, title: 'Bottom Right' },
2245
- {
2246
- value: 'bottom-center',
2247
- style: { bottom: '6px', left: '50%', transform: 'translateX(-50%)' },
2248
- title: 'Bottom Center',
2249
- },
2250
- ];
2251
- positionConfigs.forEach(({ value, style, title }) => {
2252
- const indicator = document.createElement('button');
2253
- const isActive = this.options.position === value;
2254
- Object.assign(indicator.style, {
2255
- position: 'absolute',
2256
- width: '20px',
2257
- height: '6px',
2258
- backgroundColor: isActive ? accentColor : `${color}60`,
2259
- border: `1px solid ${isActive ? accentColor : `${color}40`}`,
2260
- borderRadius: '2px',
2261
- cursor: 'pointer',
2262
- padding: '0',
2263
- transition: 'all 150ms',
2264
- boxShadow: isActive ? `0 0 8px ${accentColor}60` : 'none',
2265
- ...style,
2266
- });
2267
- indicator.title = title;
2268
- indicator.onclick = () => {
2269
- this.options.position = value;
2270
- this.settingsManager.saveSettings({ position: value });
2271
- this.render();
2272
- };
2273
- // Hover effect
2274
- indicator.onmouseenter = () => {
2275
- if (!isActive) {
2276
- indicator.style.backgroundColor = accentColor;
2277
- indicator.style.borderColor = accentColor;
2278
- indicator.style.boxShadow = `0 0 6px ${accentColor}40`;
2279
- }
2280
- };
2281
- indicator.onmouseleave = () => {
2282
- if (!isActive) {
2283
- indicator.style.backgroundColor = `${color}60`;
2284
- indicator.style.borderColor = `${color}40`;
2285
- indicator.style.boxShadow = 'none';
2286
- }
2287
- };
2288
- miniMap.appendChild(indicator);
2289
- });
2290
- positionRow.appendChild(miniMap);
2291
- displaySection.appendChild(positionRow);
2292
- // Compact mode toggle
2293
- displaySection.appendChild(this.createToggleRow('Compact Mode', this.compactMode, accentColor, () => {
2294
- this.toggleCompactMode();
2295
- }));
2296
- // Keyboard shortcut hint
2297
- const shortcutHint = document.createElement('div');
2298
- Object.assign(shortcutHint.style, {
2299
- color: COLORS.textMuted,
2300
- fontSize: '0.5625rem',
2301
- marginTop: '2px',
2302
- marginBottom: '8px',
2303
- });
2304
- shortcutHint.textContent = 'Keyboard: Cmd+Shift+M';
2305
- displaySection.appendChild(shortcutHint);
2306
- // Accent color
2307
- const accentRow = document.createElement('div');
2308
- Object.assign(accentRow.style, { marginBottom: '6px' });
2309
- const accentLabel = document.createElement('div');
2310
- Object.assign(accentLabel.style, {
2311
- color: COLORS.text,
2312
- fontSize: '0.6875rem',
2313
- marginBottom: '6px',
2314
- });
2315
- accentLabel.textContent = 'Accent Color';
2316
- accentRow.appendChild(accentLabel);
2317
- const colorSwatches = document.createElement('div');
2318
- Object.assign(colorSwatches.style, {
2319
- display: 'flex',
2320
- gap: '6px',
2321
- flexWrap: 'wrap',
2322
- });
2323
- ACCENT_COLOR_PRESETS.forEach(({ name, value }) => {
2324
- const swatch = document.createElement('button');
2325
- const isActive = this.options.accentColor === value;
2326
- Object.assign(swatch.style, {
2327
- width: '24px',
2328
- height: '24px',
2329
- borderRadius: '50%',
2330
- backgroundColor: value,
2331
- border: isActive ? '2px solid #fff' : '2px solid transparent',
2332
- cursor: 'pointer',
2333
- transition: 'all 150ms',
2334
- boxShadow: isActive ? `0 0 8px ${value}` : 'none',
2335
- });
2336
- swatch.title = name;
2337
- swatch.onclick = () => {
2338
- this.options.accentColor = value;
2339
- this.settingsManager.saveSettings({ accentColor: value });
2340
- this.render();
2341
- };
2342
- colorSwatches.appendChild(swatch);
2343
- });
2344
- accentRow.appendChild(colorSwatches);
2345
- displaySection.appendChild(accentRow);
2346
- popover.appendChild(displaySection);
2347
- // ========== FEATURES SECTION ==========
2348
- const featuresSection = this.createSettingsSection('Features');
2349
- featuresSection.appendChild(this.createToggleRow('Screenshot Button', this.options.showScreenshot, accentColor, () => {
2350
- this.options.showScreenshot = !this.options.showScreenshot;
2351
- this.settingsManager.saveSettings({ showScreenshot: this.options.showScreenshot });
2352
- this.render();
2353
- }));
2354
- featuresSection.appendChild(this.createToggleRow('Console Badges', this.options.showConsoleBadges, accentColor, () => {
2355
- this.options.showConsoleBadges = !this.options.showConsoleBadges;
2356
- this.settingsManager.saveSettings({ showConsoleBadges: this.options.showConsoleBadges });
2357
- this.render();
2358
- }));
2359
- featuresSection.appendChild(this.createToggleRow('Tooltips', this.options.showTooltips, accentColor, () => {
2360
- this.options.showTooltips = !this.options.showTooltips;
2361
- this.settingsManager.saveSettings({ showTooltips: this.options.showTooltips });
2362
- this.render();
2363
- }));
2364
- popover.appendChild(featuresSection);
2365
- // ========== METRICS SECTION ==========
2366
- const metricsSection = this.createSettingsSection('Metrics');
2367
- const metricsToggles = [
2368
- { key: 'breakpoint', label: 'Breakpoint' },
2369
- { key: 'fcp', label: 'FCP' },
2370
- { key: 'lcp', label: 'LCP' },
2371
- { key: 'cls', label: 'CLS' },
2372
- { key: 'inp', label: 'INP' },
2373
- { key: 'pageSize', label: 'Page Size' },
2374
- ];
2375
- metricsToggles.forEach(({ key, label }) => {
2376
- const currentValue = this.options.showMetrics[key] ?? true;
2377
- metricsSection.appendChild(this.createToggleRow(label, currentValue, accentColor, () => {
2378
- this.options.showMetrics[key] = !this.options.showMetrics[key];
2379
- this.settingsManager.saveSettings({
2380
- showMetrics: {
2381
- breakpoint: this.options.showMetrics.breakpoint ?? true,
2382
- fcp: this.options.showMetrics.fcp ?? true,
2383
- lcp: this.options.showMetrics.lcp ?? true,
2384
- cls: this.options.showMetrics.cls ?? true,
2385
- inp: this.options.showMetrics.inp ?? true,
2386
- pageSize: this.options.showMetrics.pageSize ?? true,
2387
- },
2388
- });
2389
- this.render();
2390
- }));
2391
- });
2392
- popover.appendChild(metricsSection);
2393
- // ========== RESET SECTION ==========
2394
- const resetSection = document.createElement('div');
2395
- Object.assign(resetSection.style, {
2396
- padding: '10px 14px',
2397
- borderTop: `1px solid ${color}20`,
2398
- });
2399
- const resetBtn = createStyledButton({
2400
- color: COLORS.textMuted,
2401
- text: 'Reset to Defaults',
2402
- padding: '6px 12px',
2403
- fontSize: '0.625rem',
2404
- });
2405
- Object.assign(resetBtn.style, {
2406
- width: '100%',
2407
- justifyContent: 'center',
2408
- });
2409
- resetBtn.onclick = () => {
2410
- this.resetToDefaults();
2411
- };
2412
- resetSection.appendChild(resetBtn);
2413
- popover.appendChild(resetSection);
2414
- this.overlayElement = popover;
2415
- document.body.appendChild(popover);
2416
- }
2417
- /**
2418
- * Reset all settings to defaults
2419
- */
2420
- resetToDefaults() {
2421
- this.settingsManager.resetToDefaults();
2422
- const defaults = DEFAULT_SETTINGS;
2423
- this.applySettings(defaults);
2424
- }
2425
- renderCollapsed() {
2426
- if (!this.container)
2427
- return;
2428
- const { position, accentColor } = this.options;
2429
- const { errorCount, warningCount } = this.getLogCounts();
2430
- // Use captured dot position if available, otherwise fall back to preset positions
2431
- // The 13px offset accounts for half the collapsed circle diameter (26px / 2)
2432
- let posStyle;
2433
- if (this.lastDotPosition) {
2434
- // Position based on where the dot actually was
2435
- const isTop = position.startsWith('top');
2436
- posStyle = isTop
2437
- ? { top: `${this.lastDotPosition.top - 13}px`, left: `${this.lastDotPosition.left - 13}px` }
2438
- : {
2439
- bottom: `${this.lastDotPosition.bottom - 13}px`,
2440
- left: `${this.lastDotPosition.left - 13}px`,
2441
- };
2442
- }
2443
- else {
2444
- // Fallback preset positions for when no dot position was captured
2445
- const collapsedPositions = {
2446
- 'bottom-left': { bottom: '27px', left: '86px' },
2447
- 'bottom-right': { bottom: '27px', right: '29px' },
2448
- 'top-left': { top: '27px', left: '86px' },
2449
- 'top-right': { top: '27px', right: '29px' },
2450
- 'bottom-center': { bottom: '19px', left: '50%', transform: 'translateX(-50%)' },
2451
- };
2452
- posStyle = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
2453
- }
2454
- const wrapper = this.container;
2455
- wrapper.className = this.tooltipClass('left', 'devbar-collapse');
2456
- wrapper.setAttribute('data-tooltip', `Click to expand DevBar${this.sweetlinkConnected ? ' (Sweetlink connected)' : ' (Sweetlink not connected)'}${errorCount > 0 ? `\n${errorCount} console error${errorCount === 1 ? '' : 's'}` : ''}`);
2457
- // Reset position properties first to avoid stale values
2458
- wrapper.style.top = '';
2459
- wrapper.style.bottom = '';
2460
- wrapper.style.left = '';
2461
- wrapper.style.right = '';
2462
- wrapper.style.transform = '';
2463
- Object.assign(wrapper.style, {
2464
- position: 'fixed',
2465
- ...posStyle,
2466
- zIndex: '9999',
2467
- backgroundColor: 'rgba(17, 24, 39, 0.95)',
2468
- border: `1px solid ${accentColor}`,
2469
- borderRadius: '50%',
2470
- color: accentColor,
2471
- boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
2472
- backdropFilter: 'blur(8px)',
2473
- WebkitBackdropFilter: 'blur(8px)',
2474
- cursor: 'pointer',
2475
- display: 'flex',
2476
- alignItems: 'center',
2477
- justifyContent: 'center',
2478
- width: '26px',
2479
- height: '26px',
2480
- boxSizing: 'border-box',
2481
- animation: 'devbar-collapse 150ms ease-out',
2482
- });
2483
- wrapper.onclick = () => {
2484
- this.collapsed = false;
2485
- this.debug.state('Expanded DevBar');
2486
- this.render();
2487
- };
2488
- // Connection indicator dot (same size as in expanded state)
2489
- const dot = document.createElement('span');
2490
- Object.assign(dot.style, {
2491
- width: '6px',
2492
- height: '6px',
2493
- borderRadius: '50%',
2494
- backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
2495
- boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
2496
- });
2497
- wrapper.appendChild(dot);
2498
- // Error badge (absolute, top-right of circle, shifted left if warning badge exists)
2499
- if (errorCount > 0) {
2500
- wrapper.appendChild(this.createCollapsedBadge(errorCount, 'rgba(239, 68, 68, 0.95)', warningCount > 0 ? '12px' : '-6px'));
2501
- }
2502
- // Warning badge (absolute, top-right)
2503
- if (warningCount > 0) {
2504
- wrapper.appendChild(this.createCollapsedBadge(warningCount, 'rgba(245, 158, 11, 0.95)', '-6px'));
2505
- }
2506
- }
2507
- renderExpanded() {
2508
- if (!this.container)
2509
- return;
2510
- const { position, accentColor, showMetrics, showScreenshot, showConsoleBadges } = this.options;
2511
- const { errorCount, warningCount } = this.getLogCounts();
2512
- const positionStyles = {
2513
- 'bottom-left': { bottom: '20px', left: '80px' },
2514
- 'bottom-right': { bottom: '20px', right: '16px' },
2515
- 'top-left': { top: '20px', left: '80px' },
2516
- 'top-right': { top: '20px', right: '16px' },
2517
- 'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
2518
- };
2519
- const posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
2520
- const isCentered = position === 'bottom-center';
2521
- const sizeOverrides = this.options.sizeOverrides;
2522
- const wrapper = this.container;
2523
- // Reset position properties first to avoid stale values from previous renders
2524
- wrapper.style.top = '';
2525
- wrapper.style.bottom = '';
2526
- wrapper.style.left = '';
2527
- wrapper.style.right = '';
2528
- wrapper.style.transform = '';
2529
- // Calculate size values with overrides or defaults
2530
- // Width always fit-content, maxWidth prevents overlap with other dev bars
2531
- // BASE breakpoint (<640px) wraps buttons to centered second row via CSS
2532
- const defaultWidth = 'fit-content';
2533
- const defaultMinWidth = 'auto';
2534
- const defaultMaxWidth = isCentered ? 'calc(100vw - 140px)' : 'calc(100vw - 32px)';
2535
- Object.assign(wrapper.style, {
2536
- position: 'fixed',
2537
- ...posStyle,
2538
- zIndex: '9999',
2539
- backgroundColor: 'rgba(17, 24, 39, 0.95)',
2540
- border: `1px solid ${accentColor}`,
2541
- borderRadius: '12px',
2542
- color: accentColor,
2543
- boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
2544
- backdropFilter: 'blur(8px)',
2545
- WebkitBackdropFilter: 'blur(8px)',
2546
- boxSizing: 'border-box',
2547
- width: sizeOverrides?.width ?? defaultWidth,
2548
- maxWidth: sizeOverrides?.maxWidth ?? defaultMaxWidth,
2549
- minWidth: sizeOverrides?.minWidth ?? defaultMinWidth,
2550
- cursor: 'default',
2551
- });
2552
- wrapper.ondblclick = () => {
2553
- // Capture dot position before collapsing
2554
- const dotEl = wrapper.querySelector('.devbar-status span span');
2555
- if (dotEl) {
2556
- const rect = dotEl.getBoundingClientRect();
2557
- this.lastDotPosition = {
2558
- left: rect.left + rect.width / 2,
2559
- top: rect.top + rect.height / 2,
2560
- bottom: window.innerHeight - (rect.top + rect.height / 2),
2561
- };
2562
- }
2563
- this.collapsed = true;
2564
- this.debug.state('Collapsed DevBar (double-click)');
2565
- this.render();
2566
- };
2567
- // Main row - wrapping controlled by CSS media query
2568
- const mainRow = document.createElement('div');
2569
- mainRow.className = 'devbar-main';
2570
- Object.assign(mainRow.style, {
2571
- display: 'flex',
2572
- alignItems: 'center',
2573
- alignContent: 'flex-start',
2574
- justifyContent: 'flex-start',
2575
- gap: '0.5rem',
2576
- padding: '0.5rem 0.75rem',
2577
- minWidth: '0',
2578
- boxSizing: 'border-box',
2579
- fontFamily: FONT_MONO,
2580
- fontSize: '0.6875rem',
2581
- lineHeight: '1rem',
2582
- });
2583
- // Connection indicator (click to collapse)
2584
- const connIndicator = document.createElement('span');
2585
- connIndicator.className = this.tooltipClass('left', 'devbar-clickable');
2586
- connIndicator.setAttribute('data-tooltip', this.sweetlinkConnected
2587
- ? 'Sweetlink connected (click to minimize)'
2588
- : 'Sweetlink disconnected (click to minimize)');
2589
- Object.assign(connIndicator.style, {
2590
- width: '12px',
2591
- height: '12px',
2592
- borderRadius: '50%',
2593
- backgroundColor: 'transparent',
2594
- display: 'flex',
2595
- alignItems: 'center',
2596
- justifyContent: 'center',
2597
- cursor: 'pointer',
2598
- flexShrink: '0',
2599
- });
2600
- connIndicator.onclick = (e) => {
2601
- e.stopPropagation();
2602
- // Capture dot position before collapsing (connDot is the inner 6px dot)
2603
- const rect = connIndicator.getBoundingClientRect();
2604
- this.lastDotPosition = {
2605
- left: rect.left + rect.width / 2,
2606
- top: rect.top + rect.height / 2,
2607
- bottom: window.innerHeight - (rect.top + rect.height / 2),
2608
- };
2609
- this.collapsed = true;
2610
- this.debug.state('Collapsed DevBar (connection dot click)');
2611
- this.render();
2612
- };
2613
- const connDot = document.createElement('span');
2614
- Object.assign(connDot.style, {
2615
- width: '6px',
2616
- height: '6px',
2617
- borderRadius: '50%',
2618
- backgroundColor: this.sweetlinkConnected ? COLORS.primary : COLORS.textMuted,
2619
- boxShadow: this.sweetlinkConnected ? `0 0 6px ${COLORS.primary}` : 'none',
2620
- transition: 'all 300ms',
2621
- });
2622
- connIndicator.appendChild(connDot);
2623
- // Status row wrapper - keeps connection dot, info, and badges together
2624
- const statusRow = document.createElement('div');
2625
- statusRow.className = 'devbar-status';
2626
- Object.assign(statusRow.style, {
2627
- display: 'flex',
2628
- alignItems: 'center',
2629
- gap: '0.5rem',
2630
- flexWrap: 'nowrap',
2631
- flexShrink: '0',
2632
- });
2633
- statusRow.appendChild(connIndicator);
2634
- // Info section
2635
- const infoSection = document.createElement('div');
2636
- infoSection.className = 'devbar-info';
2637
- Object.assign(infoSection.style, {
2638
- display: 'flex',
2639
- alignItems: 'center',
2640
- gap: '0.5rem',
2641
- textTransform: 'uppercase',
2642
- letterSpacing: '0.05em',
2643
- flexShrink: '1',
2644
- minWidth: '0',
2645
- overflow: 'visible',
2646
- });
2647
- // Breakpoint info
2648
- if (showMetrics.breakpoint && this.breakpointInfo) {
2649
- const bp = this.breakpointInfo.tailwindBreakpoint;
2650
- const breakpointData = TAILWIND_BREAKPOINTS[bp];
2651
- const bpSpan = document.createElement('span');
2652
- bpSpan.className = this.tooltipClass('left', 'devbar-item');
2653
- Object.assign(bpSpan.style, { opacity: '0.9', cursor: 'default' });
2654
- bpSpan.setAttribute('data-tooltip', `Tailwind Breakpoint: ${bp}\n${breakpointData?.label || ''}\n\nViewport: ${this.breakpointInfo.dimensions}\n\nBreakpoints:\nbase: <640px | sm: >=640px\nmd: >=768px | lg: >=1024px\nxl: >=1280px | 2xl: >=1536px`);
2655
- let bpText = bp;
2656
- if (bp !== 'base') {
2657
- bpText =
2658
- bp === 'sm'
2659
- ? `${bp} - ${this.breakpointInfo.dimensions.split('x')[0]}`
2660
- : `${bp} - ${this.breakpointInfo.dimensions}`;
2661
- }
2662
- bpSpan.textContent = bpText;
2663
- infoSection.appendChild(bpSpan);
2664
- }
2665
- // Performance stats
2666
- if (this.perfStats) {
2667
- const addSeparator = () => {
2668
- const sep = document.createElement('span');
2669
- sep.style.opacity = '0.4';
2670
- sep.textContent = '|';
2671
- infoSection.appendChild(sep);
2672
- };
2673
- if (showMetrics.fcp) {
2674
- addSeparator();
2675
- const fcpSpan = document.createElement('span');
2676
- fcpSpan.className = this.tooltipClass('left', 'devbar-item');
2677
- Object.assign(fcpSpan.style, { opacity: '0.85', cursor: 'default' });
2678
- 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');
2679
- fcpSpan.textContent = `FCP ${this.perfStats.fcp}`;
2680
- infoSection.appendChild(fcpSpan);
2681
- }
2682
- if (showMetrics.lcp) {
2683
- addSeparator();
2684
- const lcpSpan = document.createElement('span');
2685
- lcpSpan.className = this.tooltipClass('left', 'devbar-item');
2686
- Object.assign(lcpSpan.style, { opacity: '0.85', cursor: 'default' });
2687
- 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');
2688
- lcpSpan.textContent = `LCP ${this.perfStats.lcp}`;
2689
- infoSection.appendChild(lcpSpan);
2690
- }
2691
- if (showMetrics.cls) {
2692
- addSeparator();
2693
- const clsSpan = document.createElement('span');
2694
- clsSpan.className = this.tooltipClass('left', 'devbar-item');
2695
- Object.assign(clsSpan.style, { opacity: '0.85', cursor: 'default' });
2696
- clsSpan.setAttribute('data-tooltip', 'Cumulative Layout Shift (CLS): Visual stability score.\nHigher values mean more unexpected layout shifts.\n\nGood: <0.1\nNeeds work: 0.1-0.25\nPoor: >0.25');
2697
- clsSpan.textContent = `CLS ${this.perfStats.cls}`;
2698
- infoSection.appendChild(clsSpan);
2699
- }
2700
- if (showMetrics.inp) {
2701
- addSeparator();
2702
- const inpSpan = document.createElement('span');
2703
- inpSpan.className = this.tooltipClass('left', 'devbar-item');
2704
- Object.assign(inpSpan.style, { opacity: '0.85', cursor: 'default' });
2705
- inpSpan.setAttribute('data-tooltip', 'Interaction to Next Paint (INP): Responsiveness to user input.\nMeasures the longest interaction delay.\n\nGood: <200ms\nNeeds work: 200-500ms\nPoor: >500ms');
2706
- inpSpan.textContent = `INP ${this.perfStats.inp}`;
2707
- infoSection.appendChild(inpSpan);
2708
- }
2709
- if (showMetrics.pageSize) {
2710
- addSeparator();
2711
- const sizeSpan = document.createElement('span');
2712
- sizeSpan.className = this.tooltipClass('left', 'devbar-item');
2713
- Object.assign(sizeSpan.style, { opacity: '0.7', cursor: 'default' });
2714
- sizeSpan.setAttribute('data-tooltip', 'Total page size (compressed/transferred).\nIncludes HTML, CSS, JS, images, and other resources.');
2715
- sizeSpan.textContent = this.perfStats.totalSize;
2716
- infoSection.appendChild(sizeSpan);
2717
- }
2718
- }
2719
- statusRow.appendChild(infoSection);
2720
- // Console badges - add to status row so they stay with info
2721
- if (showConsoleBadges) {
2722
- if (errorCount > 0) {
2723
- statusRow.appendChild(this.createConsoleBadge('error', errorCount, BUTTON_COLORS.error));
2724
- }
2725
- if (warningCount > 0) {
2726
- statusRow.appendChild(this.createConsoleBadge('warn', warningCount, BUTTON_COLORS.warning));
2727
- }
2728
- }
2729
- mainRow.appendChild(statusRow);
2730
- // Action buttons - always render container for consistent height
2731
- const actionsContainer = document.createElement('div');
2732
- actionsContainer.className = 'devbar-actions';
2733
- if (showScreenshot) {
2734
- actionsContainer.appendChild(this.createScreenshotButton(accentColor));
2735
- }
2736
- actionsContainer.appendChild(this.createAIReviewButton());
2737
- actionsContainer.appendChild(this.createOutlineButton());
2738
- actionsContainer.appendChild(this.createSchemaButton());
2739
- actionsContainer.appendChild(this.createSettingsButton());
2740
- actionsContainer.appendChild(this.createCompactToggleButton());
2741
- mainRow.appendChild(actionsContainer);
2742
- wrapper.appendChild(mainRow);
2743
- // Render custom controls row if there are any
2744
- if (GlobalDevBar.customControls.length > 0) {
2745
- const customRow = document.createElement('div');
2746
- Object.assign(customRow.style, {
2747
- display: 'flex',
2748
- flexWrap: 'wrap',
2749
- alignItems: 'center',
2750
- gap: '0.5rem',
2751
- padding: '0 0.75rem 0.5rem 0.75rem',
2752
- borderTop: `1px solid ${accentColor}30`,
2753
- marginTop: '0',
2754
- paddingTop: '0.5rem',
2755
- fontFamily: FONT_MONO,
2756
- fontSize: '0.6875rem',
2757
- });
2758
- GlobalDevBar.customControls.forEach((control) => {
2759
- const btn = document.createElement('button');
2760
- btn.type = 'button';
2761
- const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
2762
- const isActive = control.active ?? false;
2763
- const isDisabled = control.disabled ?? false;
2764
- Object.assign(btn.style, {
2765
- padding: '4px 10px',
2766
- backgroundColor: isActive ? `${color}33` : 'transparent',
2767
- border: `1px solid ${isActive ? color : `${color}60`}`,
2768
- borderRadius: '6px',
2769
- color: isActive ? color : `${color}99`,
2770
- fontSize: '0.625rem',
2771
- cursor: isDisabled ? 'not-allowed' : 'pointer',
2772
- opacity: isDisabled ? '0.5' : '1',
2773
- transition: 'all 150ms',
2774
- });
2775
- btn.textContent = control.label;
2776
- btn.disabled = isDisabled;
2777
- if (!isDisabled) {
2778
- btn.onmouseenter = () => {
2779
- btn.style.backgroundColor = `${color}20`;
2780
- btn.style.borderColor = color;
2781
- btn.style.color = color;
2782
- };
2783
- btn.onmouseleave = () => {
2784
- btn.style.backgroundColor = isActive ? `${color}33` : 'transparent';
2785
- btn.style.borderColor = isActive ? color : `${color}60`;
2786
- btn.style.color = isActive ? color : `${color}99`;
2787
- };
2788
- btn.onclick = () => control.onClick();
2789
- }
2790
- customRow.appendChild(btn);
2791
- });
2792
- wrapper.appendChild(customRow);
2793
- }
2794
- }
2795
- /**
2796
- * Create a console badge for error/warning counts
2797
- */
2798
- createConsoleBadge(type, count, color) {
2799
- const label = type === 'error' ? 'error' : 'warning';
2800
- const isActive = this.consoleFilter === type;
2801
- const badge = document.createElement('span');
2802
- badge.className = this.tooltipClass('right', 'devbar-badge');
2803
- badge.setAttribute('data-tooltip', `${count} console ${label}${count === 1 ? '' : 's'} (click to view)`);
2804
- Object.assign(badge.style, {
2805
- display: 'flex',
2806
- alignItems: 'center',
2807
- justifyContent: 'center',
2808
- minWidth: '18px',
2809
- height: '18px',
2810
- padding: '0 5px',
2811
- borderRadius: '9999px',
2812
- backgroundColor: isActive ? color : `${color}E6`,
2813
- color: '#fff',
2814
- fontSize: '0.625rem',
2815
- fontWeight: '600',
2816
- cursor: 'pointer',
2817
- boxShadow: isActive ? `0 0 8px ${color}CC` : 'none',
2818
- });
2819
- badge.textContent = count > 99 ? '99+' : String(count);
2820
- badge.onclick = () => {
2821
- this.consoleFilter = this.consoleFilter === type ? null : type;
2822
- this.showOutlineModal = false;
2823
- this.showSchemaModal = false;
2824
- this.render();
2825
- };
2826
- return badge;
2827
- }
2828
- createScreenshotButton(accentColor) {
2829
- const btn = document.createElement('button');
2830
- btn.type = 'button';
2831
- btn.className = this.tooltipClass('right');
2832
- const hasSuccessState = this.copiedToClipboard || this.copiedPath || this.lastScreenshot;
2833
- const tooltip = this.getScreenshotTooltip();
2834
- btn.setAttribute('data-tooltip', tooltip);
2835
- Object.assign(btn.style, {
2836
- display: 'flex',
2837
- alignItems: 'center',
2838
- justifyContent: 'center',
2839
- width: '22px',
2840
- height: '22px',
2841
- minWidth: '22px',
2842
- minHeight: '22px',
2843
- flexShrink: '0',
2844
- borderRadius: '50%',
2845
- border: '1px solid',
2846
- borderColor: hasSuccessState ? accentColor : `${accentColor}80`,
2847
- backgroundColor: hasSuccessState ? `${accentColor}33` : 'transparent',
2848
- color: hasSuccessState ? accentColor : `${accentColor}99`,
2849
- cursor: !this.capturing ? 'pointer' : 'not-allowed',
2850
- opacity: '1',
2851
- transition: 'all 150ms',
2852
- });
2853
- btn.disabled = this.capturing;
2854
- btn.onclick = (e) => {
2855
- // If we have a saved screenshot path, clicking copies the path
2856
- if (this.lastScreenshot && !e.shiftKey) {
2857
- this.copyPathToClipboard(this.lastScreenshot);
2858
- }
2859
- else {
2860
- this.handleScreenshot(e.shiftKey);
2861
- }
2862
- };
2863
- // Button content
2864
- if (this.copiedToClipboard || this.copiedPath || this.lastScreenshot) {
2865
- btn.textContent = '✓';
2866
- btn.style.fontSize = '0.6rem';
2867
- }
2868
- else if (this.capturing) {
2869
- btn.textContent = '...';
2870
- btn.style.fontSize = '0.5rem';
2871
- }
2872
- else {
2873
- // Camera icon SVG
2874
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2875
- svg.setAttribute('width', '12');
2876
- svg.setAttribute('height', '12');
2877
- svg.setAttribute('viewBox', '0 0 50.8 50.8');
2878
- svg.style.stroke = 'currentColor';
2879
- svg.style.fill = 'none';
2880
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
2881
- g.setAttribute('stroke-linecap', 'round');
2882
- g.setAttribute('stroke-linejoin', 'round');
2883
- g.setAttribute('stroke-width', '4');
2884
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2885
- path.setAttribute('d', 'M19.844 7.938H7.938v11.905m0 11.113v11.906h11.905m23.019-11.906v11.906H30.956m11.906-23.018V7.938H30.956');
2886
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2887
- circle.setAttribute('cx', '25.4');
2888
- circle.setAttribute('cy', '25.4');
2889
- circle.setAttribute('r', '8.731');
2890
- g.appendChild(path);
2891
- g.appendChild(circle);
2892
- svg.appendChild(g);
2893
- btn.appendChild(svg);
2894
- }
2895
- return btn;
2896
- }
2897
- /**
2898
- * Get the tooltip text for the screenshot button based on current state
2899
- */
2900
- getScreenshotTooltip() {
2901
- if (this.copiedToClipboard) {
2902
- return 'Copied to clipboard!';
2903
- }
2904
- if (this.copiedPath) {
2905
- return 'Path copied to clipboard!';
2906
- }
2907
- if (this.lastScreenshot) {
2908
- return `Screenshot saved!\n${this.lastScreenshot}\n\nClick to copy path`;
2909
- }
2910
- const baseTooltip = `Screenshot\n\nClick: Save to file\nShift+Click: Copy to clipboard\n\nKeyboard:\nCmd/Ctrl+Shift+S: Save\nCmd/Ctrl+Shift+C: Copy`;
2911
- return this.sweetlinkConnected
2912
- ? baseTooltip
2913
- : `${baseTooltip}\n\nWarning: Sweetlink not connected`;
2914
- }
2915
- /**
2916
- * Get the tooltip text for the AI review button based on current state
2917
- */
2918
- getAIReviewTooltip() {
2919
- if (this.designReviewInProgress) {
2920
- return 'AI Design Review in progress...';
2921
- }
2922
- if (this.designReviewError) {
2923
- return `Design review failed:\n${this.designReviewError}`;
2924
- }
2925
- if (this.lastDesignReview) {
2926
- return `Design review saved to:\n${this.lastDesignReview}`;
2927
- }
2928
- const baseTooltip = `AI Design Review\n\nCaptures screenshot and sends to\nClaude for design analysis.\n\nRequires ANTHROPIC_API_KEY.`;
2929
- return this.sweetlinkConnected
2930
- ? baseTooltip
2931
- : `${baseTooltip}\n\nWarning: Sweetlink not connected`;
2932
- }
2933
- createAIReviewButton() {
2934
- const btn = document.createElement('button');
2935
- btn.type = 'button';
2936
- btn.className = this.tooltipClass('right');
2937
- const tooltip = this.getAIReviewTooltip();
2938
- btn.setAttribute('data-tooltip', tooltip);
2939
- const hasError = !!this.designReviewError;
2940
- const isActive = this.designReviewInProgress || !!this.lastDesignReview || hasError;
2941
- const isDisabled = this.designReviewInProgress || !this.sweetlinkConnected;
2942
- // Use error color (red) when there's an error, otherwise normal review color
2943
- const buttonColor = hasError ? '#ef4444' : BUTTON_COLORS.review;
2944
- Object.assign(btn.style, getButtonStyles(buttonColor, isActive, isDisabled));
2945
- if (!this.sweetlinkConnected)
2946
- btn.style.opacity = '0.5';
2947
- btn.disabled = isDisabled;
2948
- btn.onclick = () => this.showDesignReviewConfirmation();
2949
- if (this.designReviewInProgress) {
2950
- btn.textContent = '~';
2951
- btn.style.fontSize = '0.5rem';
2952
- btn.style.animation = 'pulse 1s infinite';
2953
- }
2954
- else if (this.designReviewError) {
2955
- // Show 'x' for error state
2956
- btn.textContent = '×';
2957
- btn.style.fontSize = '0.875rem';
2958
- btn.style.fontWeight = 'bold';
2959
- }
2960
- else if (this.lastDesignReview) {
2961
- btn.textContent = 'v';
2962
- btn.style.fontSize = '0.5rem';
2963
- }
2964
- else {
2965
- 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 }));
2966
- }
2967
- return btn;
2968
- }
2969
- createOutlineButton() {
2970
- const btn = document.createElement('button');
2971
- btn.type = 'button';
2972
- btn.className = this.tooltipClass('right');
2973
- const tooltip = this.lastOutline
2974
- ? `Outline saved to:\n${this.lastOutline}`
2975
- : `Document Outline\n\nView page heading structure and\nsave as markdown.`;
2976
- btn.setAttribute('data-tooltip', tooltip);
2977
- const isActive = this.showOutlineModal || !!this.lastOutline;
2978
- Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.outline, isActive, false));
2979
- btn.onclick = () => this.handleDocumentOutline();
2980
- if (this.lastOutline) {
2981
- btn.textContent = 'v';
2982
- btn.style.fontSize = '0.5rem';
2983
- }
2984
- else {
2985
- btn.appendChild(createSvgIcon('M3 4h18v2H3V4zm0 7h12v2H3v-2zm0 7h18v2H3v-2z', { fill: true }));
2986
- }
2987
- return btn;
2988
- }
2989
- createSchemaButton() {
2990
- const btn = document.createElement('button');
2991
- btn.type = 'button';
2992
- btn.className = this.tooltipClass('right');
2993
- const tooltip = this.lastSchema
2994
- ? `Schema saved to:\n${this.lastSchema}`
2995
- : `Page Schema\n\nView JSON-LD, Open Graph, and\nother structured data.`;
2996
- btn.setAttribute('data-tooltip', tooltip);
2997
- const isActive = this.showSchemaModal || !!this.lastSchema;
2998
- Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.schema, isActive, false));
2999
- btn.onclick = () => this.handlePageSchema();
3000
- if (this.lastSchema) {
3001
- btn.textContent = 'v';
3002
- btn.style.fontSize = '0.5rem';
3003
- }
3004
- else {
3005
- 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 }));
3006
- }
3007
- return btn;
415
+ moduleRender(this, consoleCapture, GlobalDevBar.customControls);
3008
416
  }
3009
417
  }
3010
- // Static storage for custom controls
3011
- GlobalDevBar.customControls = [];
3012
418
  // ============================================================================
3013
419
  // Convenience Functions
3014
420
  // ============================================================================
@@ -3064,5 +470,6 @@ export function destroyGlobalDevBar() {
3064
470
  setGlobalInstance(null);
3065
471
  }
3066
472
  }
3067
- // Re-export console capture for external use
3068
- export { earlyConsoleCapture };
473
+ // Re-export console capture instance for backward compatibility
474
+ export { consoleCapture as earlyConsoleCapture };
475
+ //# sourceMappingURL=GlobalDevBar.js.map