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