@ytspar/devbar 1.0.0-canary.2b99e1e → 1.0.0-canary.2bfb9ad
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.
- package/README.md +4 -0
- package/dist/GlobalDevBar.d.ts +111 -227
- package/dist/GlobalDevBar.d.ts.map +1 -0
- package/dist/GlobalDevBar.js +155 -2959
- package/dist/GlobalDevBar.js.map +1 -0
- package/dist/accessibility.d.ts +1 -0
- package/dist/accessibility.d.ts.map +1 -0
- package/dist/accessibility.js +1 -0
- package/dist/accessibility.js.map +1 -0
- package/dist/constants.d.ts +38 -14
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +98 -123
- package/dist/constants.js.map +1 -0
- package/dist/debug.d.ts +4 -3
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +6 -4
- package/dist/debug.js.map +1 -0
- package/dist/earlyConsoleCapture.d.ts +4 -31
- package/dist/earlyConsoleCapture.d.ts.map +1 -0
- package/dist/earlyConsoleCapture.js +4 -74
- package/dist/earlyConsoleCapture.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -0
- package/dist/lazy/index.d.ts +1 -0
- package/dist/lazy/index.d.ts.map +1 -0
- package/dist/lazy/index.js +1 -0
- package/dist/lazy/index.js.map +1 -0
- package/dist/lazy/lazyHtml2Canvas.d.ts +5 -0
- package/dist/lazy/lazyHtml2Canvas.d.ts.map +1 -0
- package/dist/lazy/lazyHtml2Canvas.js +1 -0
- package/dist/lazy/lazyHtml2Canvas.js.map +1 -0
- package/dist/modules/index.d.ts +15 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +14 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/keyboard.d.ts +15 -0
- package/dist/modules/keyboard.d.ts.map +1 -0
- package/dist/modules/keyboard.js +60 -0
- package/dist/modules/keyboard.js.map +1 -0
- package/dist/modules/performance.d.ts +26 -0
- package/dist/modules/performance.d.ts.map +1 -0
- package/dist/modules/performance.js +197 -0
- package/dist/modules/performance.js.map +1 -0
- package/dist/modules/rendering.d.ts +20 -0
- package/dist/modules/rendering.d.ts.map +1 -0
- package/dist/modules/rendering.js +2005 -0
- package/dist/modules/rendering.js.map +1 -0
- package/dist/modules/screenshot.d.ts +62 -0
- package/dist/modules/screenshot.d.ts.map +1 -0
- package/dist/modules/screenshot.js +350 -0
- package/dist/modules/screenshot.js.map +1 -0
- package/dist/modules/theme.d.ts +20 -0
- package/dist/modules/theme.d.ts.map +1 -0
- package/dist/modules/theme.js +60 -0
- package/dist/modules/theme.js.map +1 -0
- package/dist/modules/tooltips.d.ts +74 -0
- package/dist/modules/tooltips.d.ts.map +1 -0
- package/dist/modules/tooltips.js +553 -0
- package/dist/modules/tooltips.js.map +1 -0
- package/dist/modules/types.d.ts +118 -0
- package/dist/modules/types.d.ts.map +1 -0
- package/dist/modules/types.js +9 -0
- package/dist/modules/types.js.map +1 -0
- package/dist/modules/websocket.d.ts +16 -0
- package/dist/modules/websocket.d.ts.map +1 -0
- package/dist/modules/websocket.js +314 -0
- package/dist/modules/websocket.js.map +1 -0
- package/dist/network.d.ts +1 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +5 -6
- package/dist/network.js.map +1 -0
- package/dist/outline.d.ts +1 -0
- package/dist/outline.d.ts.map +1 -0
- package/dist/outline.js +1 -0
- package/dist/outline.js.map +1 -0
- package/dist/presets.d.ts +9 -8
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +9 -8
- package/dist/presets.js.map +1 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +3 -3
- package/dist/schema.js.map +1 -0
- package/dist/settings.d.ts +14 -8
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +23 -17
- package/dist/settings.js.map +1 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +1 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +7 -4
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -2
- package/dist/types.js.map +1 -0
- package/dist/ui/buttons.d.ts +3 -2
- package/dist/ui/buttons.d.ts.map +1 -0
- package/dist/ui/buttons.js +3 -2
- package/dist/ui/buttons.js.map +1 -0
- package/dist/ui/cards.d.ts +37 -0
- package/dist/ui/cards.d.ts.map +1 -0
- package/dist/ui/cards.js +134 -0
- package/dist/ui/cards.js.map +1 -0
- package/dist/ui/icons.d.ts +71 -2
- package/dist/ui/icons.d.ts.map +1 -0
- package/dist/ui/icons.js +148 -2
- package/dist/ui/icons.js.map +1 -0
- package/dist/ui/index.d.ts +3 -1
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +3 -1
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/modals.d.ts +6 -3
- package/dist/ui/modals.d.ts.map +1 -0
- package/dist/ui/modals.js +24 -13
- package/dist/ui/modals.js.map +1 -0
- package/dist/utils.d.ts +7 -2
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +20 -2
- package/dist/utils.js.map +1 -0
- package/package.json +17 -14
package/dist/GlobalDevBar.js
CHANGED
|
@@ -4,150 +4,115 @@
|
|
|
4
4
|
* A development toolbar that displays breakpoint info, performance stats,
|
|
5
5
|
* console error/warning counts, and provides screenshot capabilities via Sweetlink.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Framework-agnostic — no React, Vue, or other framework dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Implementation is split across focused modules in ./modules/:
|
|
10
|
+
* - websocket.ts — WebSocket connection, reconnection, port scanning, message handling
|
|
11
|
+
* - screenshot.ts — Screenshot capture, design review, clipboard operations
|
|
12
|
+
* - rendering.ts — renderBar(), renderConsolePopup(), renderModal(), all DOM creation
|
|
13
|
+
* - performance.ts — setupPerformanceMonitoring(), FCP/LCP/CLS/INP observers
|
|
14
|
+
* - theme.ts — setupTheme(), toggleTheme(), theme media query handling
|
|
15
|
+
* - keyboard.ts — setupKeyboardShortcuts(), handleKeydown()
|
|
16
|
+
* - tooltips.ts — tooltip creation, positioning, and management helpers
|
|
9
17
|
*/
|
|
10
|
-
import
|
|
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';
|
|
18
|
+
import { CSS_COLORS, DEVBAR_STYLES, getThemeColors, MAX_RECONNECT_ATTEMPTS, WS_PORT, WS_PORT_OFFSET, } from './constants.js';
|
|
12
19
|
import { DebugLogger, normalizeDebugConfig } from './debug.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
20
|
+
import { getSettingsManager, } from './settings.js';
|
|
21
|
+
import { ConsoleCapture } from '@ytspar/sweetlink/browser/consoleCapture';
|
|
22
|
+
// Import module functions
|
|
23
|
+
import { connectWebSocket, handleNotification } from './modules/websocket.js';
|
|
24
|
+
import { handleScreenshot as moduleHandleScreenshot } from './modules/screenshot.js';
|
|
25
|
+
import { render as moduleRender } from './modules/rendering.js';
|
|
26
|
+
import { setupBreakpointDetection, setupPerformanceMonitoring, } from './modules/performance.js';
|
|
27
|
+
import { setupKeyboardShortcuts } from './modules/keyboard.js';
|
|
28
|
+
import { setupTheme, loadCompactMode, setThemeMode as moduleSetThemeMode, } from './modules/theme.js';
|
|
18
29
|
export { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, getSettingsManager } from './settings.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
originalConsole: null,
|
|
27
|
-
isPatched: false,
|
|
28
|
-
};
|
|
29
|
-
// Skip on server-side rendering
|
|
30
|
-
if (typeof window === 'undefined')
|
|
31
|
-
return ssrFallback;
|
|
32
|
-
const capture = {
|
|
33
|
-
errorCount: 0,
|
|
34
|
-
warningCount: 0,
|
|
35
|
-
logs: [],
|
|
36
|
-
originalConsole: {
|
|
37
|
-
log: console.log,
|
|
38
|
-
error: console.error,
|
|
39
|
-
warn: console.warn,
|
|
40
|
-
info: console.info,
|
|
41
|
-
},
|
|
42
|
-
isPatched: false,
|
|
43
|
-
};
|
|
44
|
-
const captureLog = (level, args) => {
|
|
45
|
-
capture.logs.push({ level, message: formatArgs(args), timestamp: Date.now() });
|
|
46
|
-
if (capture.logs.length > MAX_CONSOLE_LOGS)
|
|
47
|
-
capture.logs = capture.logs.slice(-MAX_CONSOLE_LOGS);
|
|
48
|
-
};
|
|
49
|
-
// Patch console immediately
|
|
50
|
-
if (!capture.isPatched && capture.originalConsole) {
|
|
51
|
-
console.log = (...args) => {
|
|
52
|
-
captureLog('log', args);
|
|
53
|
-
capture.originalConsole.log(...args);
|
|
54
|
-
};
|
|
55
|
-
console.error = (...args) => {
|
|
56
|
-
captureLog('error', args);
|
|
57
|
-
capture.errorCount++;
|
|
58
|
-
capture.originalConsole.error(...args);
|
|
59
|
-
};
|
|
60
|
-
console.warn = (...args) => {
|
|
61
|
-
captureLog('warn', args);
|
|
62
|
-
capture.warningCount++;
|
|
63
|
-
capture.originalConsole.warn(...args);
|
|
64
|
-
};
|
|
65
|
-
console.info = (...args) => {
|
|
66
|
-
captureLog('info', args);
|
|
67
|
-
capture.originalConsole.info(...args);
|
|
68
|
-
};
|
|
69
|
-
capture.isPatched = true;
|
|
70
|
-
}
|
|
71
|
-
return capture;
|
|
72
|
-
})();
|
|
30
|
+
// html2canvas is lazy-loaded via getHtml2Canvas() to avoid bundling ~400KB upfront
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Console Capture (single implementation from @ytspar/sweetlink)
|
|
33
|
+
// ============================================================================
|
|
34
|
+
const consoleCapture = new ConsoleCapture({ trackCounts: true });
|
|
35
|
+
consoleCapture.importEarlyLogs();
|
|
36
|
+
consoleCapture.start();
|
|
73
37
|
// ============================================================================
|
|
74
38
|
// GlobalDevBar Class
|
|
75
39
|
// ============================================================================
|
|
76
40
|
export class GlobalDevBar {
|
|
41
|
+
// Static storage for custom controls
|
|
42
|
+
static customControls = [];
|
|
43
|
+
// -- Public state exposed via DevBarState interface for modules --
|
|
44
|
+
options;
|
|
45
|
+
debugConfig;
|
|
46
|
+
debug;
|
|
47
|
+
container = null;
|
|
48
|
+
ws = null;
|
|
49
|
+
consoleLogs = [];
|
|
50
|
+
sweetlinkConnected = false;
|
|
51
|
+
collapsed = false;
|
|
52
|
+
capturing = false;
|
|
53
|
+
copiedToClipboard = false;
|
|
54
|
+
copiedPath = false;
|
|
55
|
+
lastScreenshot = null;
|
|
56
|
+
designReviewInProgress = false;
|
|
57
|
+
lastDesignReview = null;
|
|
58
|
+
designReviewError = null;
|
|
59
|
+
showDesignReviewConfirm = false;
|
|
60
|
+
apiKeyStatus = null;
|
|
61
|
+
lastOutline = null;
|
|
62
|
+
lastSchema = null;
|
|
63
|
+
savingOutline = false;
|
|
64
|
+
savingSchema = false;
|
|
65
|
+
consoleFilter = null;
|
|
66
|
+
savingConsoleLogs = false;
|
|
67
|
+
lastConsoleLogs = null;
|
|
68
|
+
consoleLogsTimeout;
|
|
69
|
+
// Modal states
|
|
70
|
+
showOutlineModal = false;
|
|
71
|
+
showSchemaModal = false;
|
|
72
|
+
// Track active HTML tooltips for cleanup on re-render
|
|
73
|
+
activeTooltips = new Set();
|
|
74
|
+
breakpointInfo = null;
|
|
75
|
+
perfStats = null;
|
|
76
|
+
lcpValue = null;
|
|
77
|
+
clsValue = 0;
|
|
78
|
+
inpValue = 0;
|
|
79
|
+
reconnectAttempts = 0;
|
|
80
|
+
// Port scanning state for multi-instance support
|
|
81
|
+
currentAppPort;
|
|
82
|
+
baseWsPort;
|
|
83
|
+
wsVerified = false;
|
|
84
|
+
serverProjectDir = null;
|
|
85
|
+
// Track the position of the connection indicator dot for smooth collapse
|
|
86
|
+
lastDotPosition = null;
|
|
87
|
+
reconnectTimeout = null;
|
|
88
|
+
screenshotTimeout = null;
|
|
89
|
+
copiedPathTimeout = null;
|
|
90
|
+
designReviewTimeout = null;
|
|
91
|
+
designReviewErrorTimeout = null;
|
|
92
|
+
outlineTimeout = null;
|
|
93
|
+
schemaTimeout = null;
|
|
94
|
+
resizeHandler = null;
|
|
95
|
+
keydownHandler = null;
|
|
96
|
+
fcpObserver = null;
|
|
97
|
+
lcpObserver = null;
|
|
98
|
+
clsObserver = null;
|
|
99
|
+
inpObserver = null;
|
|
100
|
+
destroyed = false;
|
|
101
|
+
// Theme state
|
|
102
|
+
themeMode = 'system';
|
|
103
|
+
themeMediaQuery = null;
|
|
104
|
+
themeMediaHandler = null;
|
|
105
|
+
// Compact mode state
|
|
106
|
+
compactMode = false;
|
|
107
|
+
// Settings popover state
|
|
108
|
+
showSettingsPopover = false;
|
|
109
|
+
// Overlay element for modals
|
|
110
|
+
overlayElement = null;
|
|
111
|
+
// Settings manager for persistence
|
|
112
|
+
settingsManager;
|
|
113
|
+
// Console log listener for real-time badge updates
|
|
114
|
+
logChangeListener = null;
|
|
77
115
|
constructor(options = {}) {
|
|
78
|
-
this.container = null;
|
|
79
|
-
this.ws = null;
|
|
80
|
-
this.consoleLogs = [];
|
|
81
|
-
this.sweetlinkConnected = false;
|
|
82
|
-
this.collapsed = false;
|
|
83
|
-
this.capturing = false;
|
|
84
|
-
this.copiedToClipboard = false;
|
|
85
|
-
this.copiedPath = false;
|
|
86
|
-
this.lastScreenshot = null;
|
|
87
|
-
this.designReviewInProgress = false;
|
|
88
|
-
this.lastDesignReview = null;
|
|
89
|
-
this.designReviewError = null;
|
|
90
|
-
this.showDesignReviewConfirm = false;
|
|
91
|
-
this.apiKeyStatus = null;
|
|
92
|
-
this.lastOutline = null;
|
|
93
|
-
this.lastSchema = null;
|
|
94
|
-
this.savingOutline = false;
|
|
95
|
-
this.savingSchema = false;
|
|
96
|
-
this.consoleFilter = null;
|
|
97
|
-
// Modal states
|
|
98
|
-
this.showOutlineModal = false;
|
|
99
|
-
this.showSchemaModal = false;
|
|
100
|
-
this.breakpointInfo = null;
|
|
101
|
-
this.perfStats = null;
|
|
102
|
-
this.lcpValue = null;
|
|
103
|
-
this.clsValue = 0;
|
|
104
|
-
this.inpValue = 0;
|
|
105
|
-
this.reconnectAttempts = 0;
|
|
106
|
-
this.wsVerified = false;
|
|
107
|
-
this.serverProjectDir = null;
|
|
108
|
-
// 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
116
|
// Initialize debug config first so we can log during construction
|
|
152
117
|
this.debugConfig = normalizeDebugConfig(options.debug);
|
|
153
118
|
this.debug = new DebugLogger(this.debugConfig);
|
|
@@ -166,7 +131,7 @@ export class GlobalDevBar {
|
|
|
166
131
|
}
|
|
167
132
|
this.options = {
|
|
168
133
|
position: options.position ?? 'bottom-left',
|
|
169
|
-
accentColor: options.accentColor ??
|
|
134
|
+
accentColor: options.accentColor ?? CSS_COLORS.primary,
|
|
170
135
|
showMetrics: {
|
|
171
136
|
breakpoint: options.showMetrics?.breakpoint ?? true,
|
|
172
137
|
fcp: options.showMetrics?.fcp ?? true,
|
|
@@ -178,33 +143,26 @@ export class GlobalDevBar {
|
|
|
178
143
|
showScreenshot: options.showScreenshot ?? true,
|
|
179
144
|
showConsoleBadges: options.showConsoleBadges ?? true,
|
|
180
145
|
showTooltips: options.showTooltips ?? true,
|
|
146
|
+
saveLocation: options.saveLocation ?? 'download',
|
|
181
147
|
sizeOverrides: options.sizeOverrides,
|
|
182
148
|
};
|
|
183
149
|
this.debug.lifecycle('GlobalDevBar constructed', { options: this.options });
|
|
184
150
|
}
|
|
185
151
|
/**
|
|
186
|
-
* Get
|
|
152
|
+
* Get current error, warning, and info counts from the log array
|
|
187
153
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
154
|
+
getLogCounts() {
|
|
155
|
+
return {
|
|
156
|
+
errorCount: consoleCapture.getErrorCount(),
|
|
157
|
+
warningCount: consoleCapture.getWarningCount(),
|
|
158
|
+
infoCount: consoleCapture.getInfoCount(),
|
|
159
|
+
};
|
|
193
160
|
}
|
|
194
161
|
/**
|
|
195
|
-
*
|
|
162
|
+
* Reset position style properties on an element to clear stale values
|
|
196
163
|
*/
|
|
197
|
-
|
|
198
|
-
|
|
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 };
|
|
164
|
+
resetPositionStyles(element) {
|
|
165
|
+
Object.assign(element.style, { top: '', bottom: '', left: '', right: '', transform: '' });
|
|
208
166
|
}
|
|
209
167
|
/**
|
|
210
168
|
* Create a collapsed count badge (used for error/warning counts in minimized state)
|
|
@@ -282,23 +240,29 @@ export class GlobalDevBar {
|
|
|
282
240
|
if (this.destroyed)
|
|
283
241
|
return;
|
|
284
242
|
this.debug.lifecycle('Initializing DevBar');
|
|
285
|
-
// Inject
|
|
243
|
+
// Inject animation and utility CSS
|
|
286
244
|
this.injectStyles();
|
|
287
|
-
// Copy
|
|
288
|
-
this.consoleLogs =
|
|
289
|
-
this.debug.lifecycle('Copied
|
|
245
|
+
// Copy captured logs
|
|
246
|
+
this.consoleLogs = consoleCapture.getLogs();
|
|
247
|
+
this.debug.lifecycle('Copied console logs', { count: this.consoleLogs.length });
|
|
248
|
+
// Subscribe to log changes for real-time badge updates
|
|
249
|
+
this.logChangeListener = () => {
|
|
250
|
+
this.consoleLogs = consoleCapture.getLogs();
|
|
251
|
+
this.render();
|
|
252
|
+
};
|
|
253
|
+
consoleCapture.addListener(this.logChangeListener);
|
|
290
254
|
// Setup theme
|
|
291
|
-
|
|
255
|
+
setupTheme(this);
|
|
292
256
|
// Load compact mode from storage
|
|
293
|
-
|
|
257
|
+
loadCompactMode(this);
|
|
294
258
|
// Setup WebSocket connection
|
|
295
259
|
this.connectWebSocket();
|
|
296
260
|
// Setup breakpoint detection
|
|
297
|
-
|
|
261
|
+
setupBreakpointDetection(this);
|
|
298
262
|
// Setup performance monitoring
|
|
299
|
-
|
|
263
|
+
setupPerformanceMonitoring(this);
|
|
300
264
|
// Setup keyboard shortcuts
|
|
301
|
-
|
|
265
|
+
setupKeyboardShortcuts(this);
|
|
302
266
|
// Initial render
|
|
303
267
|
this.render();
|
|
304
268
|
this.debug.lifecycle('DevBar initialized successfully');
|
|
@@ -332,6 +296,8 @@ export class GlobalDevBar {
|
|
|
332
296
|
clearTimeout(this.outlineTimeout);
|
|
333
297
|
if (this.schemaTimeout)
|
|
334
298
|
clearTimeout(this.schemaTimeout);
|
|
299
|
+
if (this.consoleLogsTimeout)
|
|
300
|
+
clearTimeout(this.consoleLogsTimeout);
|
|
335
301
|
// Remove event listeners
|
|
336
302
|
if (this.resizeHandler)
|
|
337
303
|
window.removeEventListener('resize', this.resizeHandler);
|
|
@@ -350,13 +316,13 @@ export class GlobalDevBar {
|
|
|
350
316
|
if (this.themeMediaQuery && this.themeMediaHandler) {
|
|
351
317
|
this.themeMediaQuery.removeEventListener('change', this.themeMediaHandler);
|
|
352
318
|
}
|
|
353
|
-
//
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
console.warn = earlyConsoleCapture.originalConsole.warn;
|
|
358
|
-
console.info = earlyConsoleCapture.originalConsole.info;
|
|
319
|
+
// Remove log change listener
|
|
320
|
+
if (this.logChangeListener) {
|
|
321
|
+
consoleCapture.removeListener(this.logChangeListener);
|
|
322
|
+
this.logChangeListener = null;
|
|
359
323
|
}
|
|
324
|
+
// Restore console
|
|
325
|
+
consoleCapture.stop();
|
|
360
326
|
// Remove DOM elements
|
|
361
327
|
if (this.container) {
|
|
362
328
|
this.container.remove();
|
|
@@ -369,245 +335,20 @@ export class GlobalDevBar {
|
|
|
369
335
|
this.debug.lifecycle('DevBar destroyed');
|
|
370
336
|
}
|
|
371
337
|
injectStyles() {
|
|
372
|
-
const styleId = 'devbar-
|
|
338
|
+
const styleId = 'devbar-styles';
|
|
373
339
|
if (!document.getElementById(styleId)) {
|
|
374
340
|
const style = document.createElement('style');
|
|
375
341
|
style.id = styleId;
|
|
376
|
-
style.textContent =
|
|
342
|
+
style.textContent = DEVBAR_STYLES;
|
|
377
343
|
document.head.appendChild(style);
|
|
378
344
|
}
|
|
379
345
|
}
|
|
346
|
+
// Delegate to module functions, binding `this` as state
|
|
380
347
|
connectWebSocket(port) {
|
|
381
|
-
|
|
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
|
-
};
|
|
348
|
+
connectWebSocket(this, port);
|
|
467
349
|
}
|
|
468
|
-
|
|
469
|
-
|
|
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);
|
|
350
|
+
handleNotification(type, path, durationMs) {
|
|
351
|
+
handleNotification(this, type, path, durationMs);
|
|
611
352
|
}
|
|
612
353
|
/**
|
|
613
354
|
* Apply settings to the DevBar state and options
|
|
@@ -622,264 +363,19 @@ export class GlobalDevBar {
|
|
|
622
363
|
this.options.showScreenshot = settings.showScreenshot;
|
|
623
364
|
this.options.showConsoleBadges = settings.showConsoleBadges;
|
|
624
365
|
this.options.showTooltips = settings.showTooltips;
|
|
366
|
+
this.options.saveLocation = settings.saveLocation;
|
|
625
367
|
this.options.showMetrics = { ...settings.showMetrics };
|
|
626
368
|
// Re-render with new settings
|
|
627
369
|
this.render();
|
|
628
370
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
}
|
|
371
|
+
clearConsoleLogs() {
|
|
372
|
+
consoleCapture.clear();
|
|
373
|
+
this.consoleLogs = [];
|
|
374
|
+
this.consoleFilter = null;
|
|
676
375
|
this.render();
|
|
677
376
|
}
|
|
678
|
-
|
|
679
|
-
|
|
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 });
|
|
377
|
+
handleScreenshot(copyToClipboard) {
|
|
378
|
+
return moduleHandleScreenshot(this, copyToClipboard);
|
|
883
379
|
}
|
|
884
380
|
/**
|
|
885
381
|
* Get the current theme mode
|
|
@@ -891,12 +387,7 @@ export class GlobalDevBar {
|
|
|
891
387
|
* Set the theme mode
|
|
892
388
|
*/
|
|
893
389
|
setThemeMode(mode) {
|
|
894
|
-
this
|
|
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();
|
|
390
|
+
moduleSetThemeMode(this, mode);
|
|
900
391
|
}
|
|
901
392
|
/**
|
|
902
393
|
* Get the current effective theme colors
|
|
@@ -919,2309 +410,13 @@ export class GlobalDevBar {
|
|
|
919
410
|
isCompactMode() {
|
|
920
411
|
return this.compactMode;
|
|
921
412
|
}
|
|
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
413
|
// ============================================================================
|
|
1200
|
-
// Render
|
|
414
|
+
// Render (delegates to rendering module)
|
|
1201
415
|
// ============================================================================
|
|
1202
416
|
render() {
|
|
1203
|
-
|
|
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;
|
|
417
|
+
moduleRender(this, consoleCapture, GlobalDevBar.customControls);
|
|
3221
418
|
}
|
|
3222
419
|
}
|
|
3223
|
-
// Static storage for custom controls
|
|
3224
|
-
GlobalDevBar.customControls = [];
|
|
3225
420
|
// ============================================================================
|
|
3226
421
|
// Convenience Functions
|
|
3227
422
|
// ============================================================================
|
|
@@ -3277,5 +472,6 @@ export function destroyGlobalDevBar() {
|
|
|
3277
472
|
setGlobalInstance(null);
|
|
3278
473
|
}
|
|
3279
474
|
}
|
|
3280
|
-
// Re-export console capture for
|
|
3281
|
-
export { earlyConsoleCapture };
|
|
475
|
+
// Re-export console capture instance for backward compatibility
|
|
476
|
+
export { consoleCapture as earlyConsoleCapture };
|
|
477
|
+
//# sourceMappingURL=GlobalDevBar.js.map
|