@ytspar/devbar 0.0.1 → 1.0.0-canary.2b99e1e

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