@ytspar/devbar 1.4.0 → 1.4.2
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/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -1
- package/dist/modules/rendering/buttons.d.ts +19 -0
- package/dist/modules/rendering/buttons.d.ts.map +1 -0
- package/dist/modules/rendering/buttons.js +369 -0
- package/dist/modules/rendering/buttons.js.map +1 -0
- package/dist/modules/rendering/collapsed.d.ts +6 -0
- package/dist/modules/rendering/collapsed.d.ts.map +1 -0
- package/dist/modules/rendering/collapsed.js +124 -0
- package/dist/modules/rendering/collapsed.js.map +1 -0
- package/dist/modules/rendering/common.d.ts +21 -0
- package/dist/modules/rendering/common.d.ts.map +1 -0
- package/dist/modules/rendering/common.js +60 -0
- package/dist/modules/rendering/common.js.map +1 -0
- package/dist/modules/rendering/compact.d.ts +6 -0
- package/dist/modules/rendering/compact.d.ts.map +1 -0
- package/dist/modules/rendering/compact.js +107 -0
- package/dist/modules/rendering/compact.js.map +1 -0
- package/dist/modules/rendering/console.d.ts +7 -0
- package/dist/modules/rendering/console.d.ts.map +1 -0
- package/dist/modules/rendering/console.js +78 -0
- package/dist/modules/rendering/console.js.map +1 -0
- package/dist/modules/rendering/expanded.d.ts +13 -0
- package/dist/modules/rendering/expanded.d.ts.map +1 -0
- package/dist/modules/rendering/expanded.js +439 -0
- package/dist/modules/rendering/expanded.js.map +1 -0
- package/dist/modules/rendering/index.d.ts +22 -0
- package/dist/modules/rendering/index.d.ts.map +1 -0
- package/dist/modules/rendering/index.js +109 -0
- package/dist/modules/rendering/index.js.map +1 -0
- package/dist/modules/rendering/modals.d.ts +9 -0
- package/dist/modules/rendering/modals.d.ts.map +1 -0
- package/dist/modules/rendering/modals.js +1068 -0
- package/dist/modules/rendering/modals.js.map +1 -0
- package/dist/modules/rendering/settings.d.ts +6 -0
- package/dist/modules/rendering/settings.d.ts.map +1 -0
- package/dist/modules/rendering/settings.js +605 -0
- package/dist/modules/rendering/settings.js.map +1 -0
- package/dist/modules/rendering.d.ts +15 -16
- package/dist/modules/rendering.d.ts.map +1 -1
- package/dist/modules/rendering.js +16 -2902
- package/dist/modules/rendering.js.map +1 -1
- package/dist/modules/tooltips.d.ts +6 -4
- package/dist/modules/tooltips.d.ts.map +1 -1
- package/dist/modules/tooltips.js +121 -145
- package/dist/modules/tooltips.js.map +1 -1
- package/dist/ui/buttons.js +9 -9
- package/dist/ui/buttons.js.map +1 -1
- package/dist/ui/cards.js +3 -3
- package/dist/ui/cards.js.map +1 -1
- package/dist/ui/modals.js +7 -7
- package/dist/ui/modals.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,2905 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rendering
|
|
3
|
-
* console popups, modals, settings popover, and all DOM-creation UI code.
|
|
2
|
+
* Rendering barrel - re-exports from sub-modules under rendering/.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* Used to animate the collapsed circle to the same spot as the connection dot.
|
|
21
|
-
*/
|
|
22
|
-
function captureDotPosition(state, element) {
|
|
23
|
-
const rect = element.getBoundingClientRect();
|
|
24
|
-
state.lastDotPosition = {
|
|
25
|
-
left: rect.left + rect.width / 2,
|
|
26
|
-
top: rect.top + rect.height / 2,
|
|
27
|
-
bottom: window.innerHeight - (rect.top + rect.height / 2),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Create the connection indicator (outer wrapper + inner colored dot).
|
|
32
|
-
* The caller is responsible for attaching tooltip and click handlers, since
|
|
33
|
-
* those differ between compact and expanded modes.
|
|
34
|
-
*/
|
|
35
|
-
function createConnectionIndicator(state) {
|
|
36
|
-
const connIndicator = document.createElement('span');
|
|
37
|
-
connIndicator.className = 'devbar-clickable';
|
|
38
|
-
Object.assign(connIndicator.style, {
|
|
39
|
-
width: '12px',
|
|
40
|
-
height: '12px',
|
|
41
|
-
borderRadius: '50%',
|
|
42
|
-
backgroundColor: 'transparent',
|
|
43
|
-
display: 'flex',
|
|
44
|
-
alignItems: 'center',
|
|
45
|
-
justifyContent: 'center',
|
|
46
|
-
cursor: 'pointer',
|
|
47
|
-
flexShrink: '0',
|
|
48
|
-
});
|
|
49
|
-
const connDot = document.createElement('span');
|
|
50
|
-
connDot.className = 'devbar-conn-dot';
|
|
51
|
-
Object.assign(connDot.style, {
|
|
52
|
-
width: '6px',
|
|
53
|
-
height: '6px',
|
|
54
|
-
borderRadius: '50%',
|
|
55
|
-
backgroundColor: state.sweetlinkConnected ? CSS_COLORS.primary : CSS_COLORS.textMuted,
|
|
56
|
-
boxShadow: state.sweetlinkConnected ? `0 0 6px ${CSS_COLORS.primary}` : 'none',
|
|
57
|
-
transition: 'all 300ms',
|
|
58
|
-
});
|
|
59
|
-
connIndicator.appendChild(connDot);
|
|
60
|
-
return connIndicator;
|
|
61
|
-
}
|
|
62
|
-
/** Prevents re-entrant render calls during rapid clicks */
|
|
63
|
-
let renderGuard = false;
|
|
64
|
-
/**
|
|
65
|
-
* Main render dispatch - creates container and delegates to appropriate renderer.
|
|
66
|
-
*/
|
|
67
|
-
export function render(state, consoleCaptureSingleton, customControls) {
|
|
68
|
-
if (state.destroyed)
|
|
69
|
-
return;
|
|
70
|
-
if (typeof document === 'undefined')
|
|
71
|
-
return;
|
|
72
|
-
if (renderGuard)
|
|
73
|
-
return;
|
|
74
|
-
renderGuard = true;
|
|
75
|
-
// Clear any orphaned tooltips from previous render
|
|
76
|
-
clearAllTooltips(state);
|
|
77
|
-
// Remove existing overlay if any (modals append to body, need explicit cleanup)
|
|
78
|
-
if (state.overlayElement) {
|
|
79
|
-
state.overlayElement.remove();
|
|
80
|
-
state.overlayElement = null;
|
|
81
|
-
document.body.style.overflow = '';
|
|
82
|
-
}
|
|
83
|
-
// Remove existing container if any
|
|
84
|
-
if (state.container) {
|
|
85
|
-
state.container.remove();
|
|
86
|
-
}
|
|
87
|
-
// Create new container and append immediately so the devbar stays visible
|
|
88
|
-
// even if content or overlay rendering throws
|
|
89
|
-
state.container = document.createElement('div');
|
|
90
|
-
state.container.setAttribute('data-devbar', 'true');
|
|
91
|
-
state.container.setAttribute('role', 'toolbar');
|
|
92
|
-
state.container.setAttribute('aria-label', 'DevBar');
|
|
93
|
-
document.body.appendChild(state.container);
|
|
94
|
-
try {
|
|
95
|
-
if (state.collapsed) {
|
|
96
|
-
renderCollapsed(state);
|
|
97
|
-
}
|
|
98
|
-
else if (state.compactMode) {
|
|
99
|
-
renderCompact(state);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
renderExpanded(state, customControls);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
console.error('[GlobalDevBar] Render failed:', e);
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
renderOverlays(state, consoleCaptureSingleton);
|
|
110
|
-
}
|
|
111
|
-
catch (e) {
|
|
112
|
-
console.error('[GlobalDevBar] Overlay render failed:', e);
|
|
113
|
-
}
|
|
114
|
-
// Lock body scroll while a modal overlay is open
|
|
115
|
-
if (state.overlayElement) {
|
|
116
|
-
document.body.style.overflow = 'hidden';
|
|
117
|
-
}
|
|
118
|
-
renderGuard = false;
|
|
119
|
-
}
|
|
120
|
-
function renderOverlays(state, consoleCaptureSingleton) {
|
|
121
|
-
// Safety: only one overlay at a time. First match wins; close the rest.
|
|
122
|
-
// (Overlay cleanup already performed by render() before calling this.)
|
|
123
|
-
if (state.consoleFilter) {
|
|
124
|
-
const filter = state.consoleFilter;
|
|
125
|
-
closeAllModals(state);
|
|
126
|
-
state.consoleFilter = filter;
|
|
127
|
-
renderConsolePopup(state, consoleCaptureSingleton);
|
|
128
|
-
}
|
|
129
|
-
else if (state.showOutlineModal) {
|
|
130
|
-
closeAllModals(state);
|
|
131
|
-
state.showOutlineModal = true;
|
|
132
|
-
renderOutlineModal(state);
|
|
133
|
-
}
|
|
134
|
-
else if (state.showSchemaModal) {
|
|
135
|
-
closeAllModals(state);
|
|
136
|
-
state.showSchemaModal = true;
|
|
137
|
-
renderSchemaModal(state);
|
|
138
|
-
}
|
|
139
|
-
else if (state.showA11yModal) {
|
|
140
|
-
closeAllModals(state);
|
|
141
|
-
state.showA11yModal = true;
|
|
142
|
-
renderA11yModal(state);
|
|
143
|
-
}
|
|
144
|
-
else if (state.showDesignReviewConfirm) {
|
|
145
|
-
closeAllModals(state);
|
|
146
|
-
state.showDesignReviewConfirm = true;
|
|
147
|
-
renderDesignReviewConfirmModal(state);
|
|
148
|
-
}
|
|
149
|
-
else if (state.showSettingsPopover) {
|
|
150
|
-
closeAllModals(state);
|
|
151
|
-
state.showSettingsPopover = true;
|
|
152
|
-
renderSettingsPopover(state);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// ============================================================================
|
|
156
|
-
// Collapsed State
|
|
157
|
-
// ============================================================================
|
|
158
|
-
function renderCollapsed(state) {
|
|
159
|
-
if (!state.container)
|
|
160
|
-
return;
|
|
161
|
-
const { position, accentColor } = state.options;
|
|
162
|
-
const { errorCount, warningCount } = state.getLogCounts();
|
|
163
|
-
// Use captured dot position if available, otherwise fall back to preset positions
|
|
164
|
-
// The 13px offset accounts for half the collapsed circle diameter (26px / 2)
|
|
165
|
-
let posStyle;
|
|
166
|
-
if (state.lastDotPosition) {
|
|
167
|
-
// Position based on where the dot actually was
|
|
168
|
-
const isTop = position.startsWith('top');
|
|
169
|
-
posStyle = isTop
|
|
170
|
-
? { top: `${state.lastDotPosition.top - 13}px`, left: `${state.lastDotPosition.left - 13}px` }
|
|
171
|
-
: {
|
|
172
|
-
bottom: `${state.lastDotPosition.bottom - 13}px`,
|
|
173
|
-
left: `${state.lastDotPosition.left - 13}px`,
|
|
174
|
-
};
|
|
175
|
-
// Clear after use so expand doesn't re-use stale values
|
|
176
|
-
state.lastDotPosition = null;
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
// Fallback preset positions for when no dot position was captured
|
|
180
|
-
const collapsedPositions = {
|
|
181
|
-
'bottom-left': { bottom: '27px', left: '86px' },
|
|
182
|
-
'bottom-right': { bottom: '27px', right: '29px' },
|
|
183
|
-
'top-left': { top: '27px', left: '86px' },
|
|
184
|
-
'top-right': { top: '27px', right: '29px' },
|
|
185
|
-
'bottom-center': { bottom: '19px', left: '50%', transform: 'translateX(-50%)' },
|
|
186
|
-
};
|
|
187
|
-
posStyle = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
|
|
188
|
-
}
|
|
189
|
-
const wrapper = state.container;
|
|
190
|
-
wrapper.className = 'devbar-collapse';
|
|
191
|
-
state.resetPositionStyles(wrapper);
|
|
192
|
-
// Set CSS variable for accent color (used by pulse animation)
|
|
193
|
-
wrapper.style.setProperty('--devbar-color-accent', accentColor);
|
|
194
|
-
Object.assign(wrapper.style, {
|
|
195
|
-
position: 'fixed',
|
|
196
|
-
...posStyle,
|
|
197
|
-
zIndex: '9999',
|
|
198
|
-
backgroundColor: 'var(--devbar-color-bg-card)',
|
|
199
|
-
border: `1px solid ${accentColor}`,
|
|
200
|
-
borderRadius: '50%',
|
|
201
|
-
color: accentColor,
|
|
202
|
-
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
|
|
203
|
-
backdropFilter: 'blur(8px)',
|
|
204
|
-
WebkitBackdropFilter: 'blur(8px)',
|
|
205
|
-
cursor: 'pointer',
|
|
206
|
-
display: 'flex',
|
|
207
|
-
alignItems: 'center',
|
|
208
|
-
justifyContent: 'center',
|
|
209
|
-
width: '26px',
|
|
210
|
-
height: '26px',
|
|
211
|
-
boxSizing: 'border-box',
|
|
212
|
-
animation: 'devbar-collapse 150ms ease-out, devbar-collapsed-pulse 2s ease-in-out 0.2s 3',
|
|
213
|
-
});
|
|
214
|
-
wrapper.onclick = () => {
|
|
215
|
-
state.collapsed = false;
|
|
216
|
-
state.debug.state('Expanded DevBar');
|
|
217
|
-
state.render();
|
|
218
|
-
};
|
|
219
|
-
// Create inner container for dot + chevron
|
|
220
|
-
const innerContainer = document.createElement('span');
|
|
221
|
-
Object.assign(innerContainer.style, {
|
|
222
|
-
display: 'flex',
|
|
223
|
-
alignItems: 'center',
|
|
224
|
-
justifyContent: 'center',
|
|
225
|
-
position: 'relative',
|
|
226
|
-
});
|
|
227
|
-
// Connection indicator dot (same size as in expanded state)
|
|
228
|
-
const dot = document.createElement('span');
|
|
229
|
-
Object.assign(dot.style, {
|
|
230
|
-
width: '6px',
|
|
231
|
-
height: '6px',
|
|
232
|
-
borderRadius: '50%',
|
|
233
|
-
backgroundColor: state.sweetlinkConnected ? CSS_COLORS.primary : CSS_COLORS.textMuted,
|
|
234
|
-
boxShadow: state.sweetlinkConnected ? `0 0 6px ${CSS_COLORS.primary}` : 'none',
|
|
235
|
-
transition: 'transform 150ms ease-out, opacity 150ms ease-out',
|
|
236
|
-
});
|
|
237
|
-
innerContainer.appendChild(dot);
|
|
238
|
-
// Expand chevron indicator (appears on hover)
|
|
239
|
-
const chevron = document.createElement('span');
|
|
240
|
-
Object.assign(chevron.style, {
|
|
241
|
-
position: 'absolute',
|
|
242
|
-
width: '100%',
|
|
243
|
-
height: '100%',
|
|
244
|
-
display: 'flex',
|
|
245
|
-
alignItems: 'center',
|
|
246
|
-
justifyContent: 'center',
|
|
247
|
-
opacity: '0',
|
|
248
|
-
transition: 'opacity 150ms ease-out',
|
|
249
|
-
fontSize: '10px',
|
|
250
|
-
color: accentColor,
|
|
251
|
-
});
|
|
252
|
-
chevron.textContent = '\u2197';
|
|
253
|
-
innerContainer.appendChild(chevron);
|
|
254
|
-
attachTextTooltip(state, wrapper, () => `Click to expand DevBar${state.sweetlinkConnected ? ' (Sweetlink connected)' : ' (Sweetlink not connected)'}${errorCount > 0 ? `\n${errorCount} console error${errorCount === 1 ? '' : 's'}` : ''}`, {
|
|
255
|
-
onEnter: () => {
|
|
256
|
-
dot.style.opacity = '0';
|
|
257
|
-
dot.style.transform = 'scale(0)';
|
|
258
|
-
chevron.style.opacity = '1';
|
|
259
|
-
},
|
|
260
|
-
onLeave: () => {
|
|
261
|
-
dot.style.opacity = '1';
|
|
262
|
-
dot.style.transform = 'scale(1)';
|
|
263
|
-
chevron.style.opacity = '0';
|
|
264
|
-
},
|
|
265
|
-
});
|
|
266
|
-
wrapper.appendChild(innerContainer);
|
|
267
|
-
// Error badge (absolute, top-right of circle, shifted left if warning badge exists)
|
|
268
|
-
if (errorCount > 0) {
|
|
269
|
-
wrapper.appendChild(state.createCollapsedBadge(errorCount, 'rgba(239, 68, 68, 0.95)', warningCount > 0 ? '12px' : '-6px'));
|
|
270
|
-
}
|
|
271
|
-
// Warning badge (absolute, top-right)
|
|
272
|
-
if (warningCount > 0) {
|
|
273
|
-
wrapper.appendChild(state.createCollapsedBadge(warningCount, 'rgba(245, 158, 11, 0.95)', '-6px'));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// ============================================================================
|
|
277
|
-
// Compact State
|
|
278
|
-
// ============================================================================
|
|
279
|
-
function renderCompact(state) {
|
|
280
|
-
if (!state.container)
|
|
281
|
-
return;
|
|
282
|
-
const { position, accentColor } = state.options;
|
|
283
|
-
const { errorCount, warningCount, infoCount } = state.getLogCounts();
|
|
284
|
-
// Simple position styles - same anchor points as expanded mode
|
|
285
|
-
const positionStyles = {
|
|
286
|
-
'bottom-left': { bottom: '20px', left: '80px' },
|
|
287
|
-
'bottom-right': { bottom: '20px', right: '16px' },
|
|
288
|
-
'top-left': { top: '20px', left: '80px' },
|
|
289
|
-
'top-right': { top: '20px', right: '16px' },
|
|
290
|
-
'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
|
|
291
|
-
};
|
|
292
|
-
const posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
|
|
293
|
-
const wrapper = state.container;
|
|
294
|
-
state.resetPositionStyles(wrapper);
|
|
295
|
-
Object.assign(wrapper.style, {
|
|
296
|
-
position: 'fixed',
|
|
297
|
-
...posStyle,
|
|
298
|
-
zIndex: '9999',
|
|
299
|
-
backgroundColor: 'var(--devbar-color-bg-card)',
|
|
300
|
-
border: `1px solid ${accentColor}`,
|
|
301
|
-
borderRadius: '20px',
|
|
302
|
-
color: accentColor,
|
|
303
|
-
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
|
|
304
|
-
backdropFilter: 'blur(8px)',
|
|
305
|
-
WebkitBackdropFilter: 'blur(8px)',
|
|
306
|
-
padding: '6px 10px',
|
|
307
|
-
display: 'flex',
|
|
308
|
-
alignItems: 'center',
|
|
309
|
-
gap: '8px',
|
|
310
|
-
fontFamily: FONT_MONO,
|
|
311
|
-
fontSize: '0.6875rem',
|
|
312
|
-
});
|
|
313
|
-
// Connection indicator
|
|
314
|
-
const connIndicator = createConnectionIndicator(state);
|
|
315
|
-
const connDot = connIndicator.querySelector('.devbar-conn-dot');
|
|
316
|
-
attachTextTooltip(state, connIndicator, () => state.sweetlinkConnected ? 'Sweetlink connected' : 'Sweetlink disconnected');
|
|
317
|
-
connIndicator.onclick = (e) => {
|
|
318
|
-
e.stopPropagation();
|
|
319
|
-
captureDotPosition(state, connDot);
|
|
320
|
-
state.collapsed = true;
|
|
321
|
-
state.debug.state('Collapsed DevBar from compact mode');
|
|
322
|
-
state.render();
|
|
323
|
-
};
|
|
324
|
-
wrapper.appendChild(connIndicator);
|
|
325
|
-
// Error badge
|
|
326
|
-
if (errorCount > 0) {
|
|
327
|
-
wrapper.appendChild(createConsoleBadge(state, 'error', errorCount, BUTTON_COLORS.error));
|
|
328
|
-
}
|
|
329
|
-
// Warning badge
|
|
330
|
-
if (warningCount > 0) {
|
|
331
|
-
wrapper.appendChild(createConsoleBadge(state, 'warn', warningCount, BUTTON_COLORS.warning));
|
|
332
|
-
}
|
|
333
|
-
// Info badge
|
|
334
|
-
if (infoCount > 0) {
|
|
335
|
-
wrapper.appendChild(createConsoleBadge(state, 'info', infoCount, BUTTON_COLORS.info));
|
|
336
|
-
}
|
|
337
|
-
// Screenshot button (if enabled)
|
|
338
|
-
if (state.options.showScreenshot) {
|
|
339
|
-
wrapper.appendChild(createScreenshotButton(state, accentColor));
|
|
340
|
-
}
|
|
341
|
-
// Settings gear button
|
|
342
|
-
wrapper.appendChild(createSettingsButton(state));
|
|
343
|
-
// Expand button (double-arrow)
|
|
344
|
-
const expandBtn = document.createElement('button');
|
|
345
|
-
expandBtn.type = 'button';
|
|
346
|
-
Object.assign(expandBtn.style, {
|
|
347
|
-
display: 'flex',
|
|
348
|
-
alignItems: 'center',
|
|
349
|
-
justifyContent: 'center',
|
|
350
|
-
width: '18px',
|
|
351
|
-
height: '18px',
|
|
352
|
-
borderRadius: '50%',
|
|
353
|
-
border: `1px solid ${accentColor}60`,
|
|
354
|
-
backgroundColor: 'transparent',
|
|
355
|
-
color: `${accentColor}99`,
|
|
356
|
-
cursor: 'pointer',
|
|
357
|
-
fontSize: '0.5rem',
|
|
358
|
-
transition: 'all 150ms',
|
|
359
|
-
});
|
|
360
|
-
expandBtn.textContent = '\u27EB';
|
|
361
|
-
attachTextTooltip(state, expandBtn, () => 'Expand DevBar', {
|
|
362
|
-
onEnter: () => {
|
|
363
|
-
expandBtn.style.backgroundColor = `${accentColor}20`;
|
|
364
|
-
expandBtn.style.borderColor = accentColor;
|
|
365
|
-
expandBtn.style.color = accentColor;
|
|
366
|
-
},
|
|
367
|
-
onLeave: () => {
|
|
368
|
-
expandBtn.style.backgroundColor = 'transparent';
|
|
369
|
-
expandBtn.style.borderColor = `${accentColor}60`;
|
|
370
|
-
expandBtn.style.color = `${accentColor}99`;
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
|
-
expandBtn.onclick = () => {
|
|
374
|
-
state.toggleCompactMode();
|
|
375
|
-
};
|
|
376
|
-
wrapper.appendChild(expandBtn);
|
|
377
|
-
}
|
|
378
|
-
// ============================================================================
|
|
379
|
-
// Expanded State — Helper Functions
|
|
380
|
-
// ============================================================================
|
|
381
|
-
/**
|
|
382
|
-
* Compute the CSS position for the expanded devbar wrapper.
|
|
383
|
-
* Uses the captured dot position when available for smooth collapse/expand transitions.
|
|
384
|
-
*/
|
|
385
|
-
function computeExpandedPosition(state, position, isCentered) {
|
|
386
|
-
// Dot offset from container edge in expanded mode:
|
|
387
|
-
// border (1px) + padding (12px) + half indicator (6px) = 19px from left
|
|
388
|
-
// border (1px) + padding (8px) + half indicator (6px) = 15px from top
|
|
389
|
-
const DOT_OFFSET_LEFT = 19;
|
|
390
|
-
const DOT_OFFSET_TOP = 15;
|
|
391
|
-
// Use captured dot position to align the expanded bar's dot with where it was
|
|
392
|
-
// Always use top/left positioning for precise alignment
|
|
393
|
-
if (state.lastDotPosition && !isCentered) {
|
|
394
|
-
const isRight = position.endsWith('right');
|
|
395
|
-
let posStyle;
|
|
396
|
-
if (isRight) {
|
|
397
|
-
// For right-aligned, fall back to default
|
|
398
|
-
const isTop = position.startsWith('top');
|
|
399
|
-
posStyle = isTop ? { top: '20px', right: '16px' } : { bottom: '20px', right: '16px' };
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
// Use top positioning for precise dot alignment
|
|
403
|
-
posStyle = {
|
|
404
|
-
top: `${state.lastDotPosition.top - DOT_OFFSET_TOP}px`,
|
|
405
|
-
left: `${state.lastDotPosition.left - DOT_OFFSET_LEFT}px`,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
// Clear the position after using it
|
|
409
|
-
state.lastDotPosition = null;
|
|
410
|
-
return posStyle;
|
|
411
|
-
}
|
|
412
|
-
const positionStyles = {
|
|
413
|
-
'bottom-left': { bottom: '20px', left: '80px' },
|
|
414
|
-
'bottom-right': { bottom: '20px', right: '16px' },
|
|
415
|
-
'top-left': { top: '20px', left: '80px' },
|
|
416
|
-
'top-right': { top: '20px', right: '16px' },
|
|
417
|
-
'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
|
|
418
|
-
};
|
|
419
|
-
return positionStyles[position] ?? positionStyles['bottom-left'];
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Style the expanded wrapper container and attach the double-click-to-collapse handler.
|
|
423
|
-
*/
|
|
424
|
-
function styleExpandedWrapper(state, wrapper, posStyle, accentColor, isCentered) {
|
|
425
|
-
state.resetPositionStyles(wrapper);
|
|
426
|
-
const sizeOverrides = state.options.sizeOverrides;
|
|
427
|
-
// Calculate size values with overrides or defaults
|
|
428
|
-
// Use fit-content so DevBar only takes space it needs, but allow expansion up to max
|
|
429
|
-
// Centered: 16px margin each side. Left/right: 80px for Next.js bar + 16px margin
|
|
430
|
-
const defaultWidth = 'fit-content';
|
|
431
|
-
const defaultMinWidth = 'auto';
|
|
432
|
-
const defaultMaxWidth = isCentered ? 'calc(100vw - 32px)' : 'calc(100vw - 96px)';
|
|
433
|
-
Object.assign(wrapper.style, {
|
|
434
|
-
position: 'fixed',
|
|
435
|
-
...posStyle,
|
|
436
|
-
zIndex: '9999',
|
|
437
|
-
backgroundColor: 'var(--devbar-color-bg-card)',
|
|
438
|
-
border: `1px solid ${accentColor}`,
|
|
439
|
-
borderRadius: '12px',
|
|
440
|
-
color: accentColor,
|
|
441
|
-
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
|
|
442
|
-
backdropFilter: 'blur(8px)',
|
|
443
|
-
WebkitBackdropFilter: 'blur(8px)',
|
|
444
|
-
boxSizing: 'border-box',
|
|
445
|
-
width: sizeOverrides?.width ?? defaultWidth,
|
|
446
|
-
maxWidth: sizeOverrides?.maxWidth ?? defaultMaxWidth,
|
|
447
|
-
minWidth: sizeOverrides?.minWidth ?? defaultMinWidth,
|
|
448
|
-
cursor: 'default',
|
|
449
|
-
});
|
|
450
|
-
wrapper.ondblclick = (e) => {
|
|
451
|
-
// Ignore double-clicks on interactive elements (buttons, inputs, selects)
|
|
452
|
-
// to prevent rapid settings-button clicks from collapsing the devbar
|
|
453
|
-
const target = e.target;
|
|
454
|
-
if (target?.closest('button, input, select, a'))
|
|
455
|
-
return;
|
|
456
|
-
const dotEl = wrapper.querySelector('.devbar-status span span');
|
|
457
|
-
if (dotEl) {
|
|
458
|
-
captureDotPosition(state, dotEl);
|
|
459
|
-
}
|
|
460
|
-
state.collapsed = true;
|
|
461
|
-
state.debug.state('Collapsed DevBar (double-click)');
|
|
462
|
-
state.render();
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Create the main row flex container used in expanded mode.
|
|
467
|
-
*/
|
|
468
|
-
function createExpandedMainRow() {
|
|
469
|
-
const mainRow = document.createElement('div');
|
|
470
|
-
mainRow.className = 'devbar-main';
|
|
471
|
-
Object.assign(mainRow.style, {
|
|
472
|
-
display: 'flex',
|
|
473
|
-
alignItems: 'center',
|
|
474
|
-
alignContent: 'flex-start',
|
|
475
|
-
justifyContent: 'flex-start',
|
|
476
|
-
gap: '0.5rem',
|
|
477
|
-
padding: '0.5rem 0.75rem',
|
|
478
|
-
minWidth: '0',
|
|
479
|
-
boxSizing: 'border-box',
|
|
480
|
-
fontFamily: FONT_MONO,
|
|
481
|
-
fontSize: '0.6875rem',
|
|
482
|
-
lineHeight: '1rem',
|
|
483
|
-
});
|
|
484
|
-
return mainRow;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Create the connection indicator configured to collapse the devbar on click.
|
|
488
|
-
*/
|
|
489
|
-
function createExpandedConnectionIndicator(state) {
|
|
490
|
-
const connIndicator = createConnectionIndicator(state);
|
|
491
|
-
attachTextTooltip(state, connIndicator, () => state.sweetlinkConnected
|
|
492
|
-
? 'Sweetlink connected (click to minimize)'
|
|
493
|
-
: 'Sweetlink disconnected (click to minimize)');
|
|
494
|
-
connIndicator.onclick = (e) => {
|
|
495
|
-
e.stopPropagation();
|
|
496
|
-
captureDotPosition(state, connIndicator);
|
|
497
|
-
state.collapsed = true;
|
|
498
|
-
state.debug.state('Collapsed DevBar (connection dot click)');
|
|
499
|
-
state.render();
|
|
500
|
-
};
|
|
501
|
-
return connIndicator;
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Create the info section containing breakpoint display and performance metrics.
|
|
505
|
-
*/
|
|
506
|
-
function createInfoSection(state, showMetrics) {
|
|
507
|
-
const infoSection = document.createElement('div');
|
|
508
|
-
infoSection.className = 'devbar-info';
|
|
509
|
-
Object.assign(infoSection.style, {
|
|
510
|
-
display: 'flex',
|
|
511
|
-
alignItems: 'center',
|
|
512
|
-
gap: '0.5rem',
|
|
513
|
-
textTransform: 'uppercase',
|
|
514
|
-
letterSpacing: '0.05em',
|
|
515
|
-
flexShrink: '1',
|
|
516
|
-
minWidth: '0',
|
|
517
|
-
overflow: 'visible',
|
|
518
|
-
});
|
|
519
|
-
// Breakpoint info
|
|
520
|
-
if (showMetrics.breakpoint && state.breakpointInfo) {
|
|
521
|
-
appendBreakpointInfo(state, infoSection);
|
|
522
|
-
}
|
|
523
|
-
// Performance stats with responsive visibility
|
|
524
|
-
if (state.perfStats) {
|
|
525
|
-
appendPerformanceMetrics(state, infoSection, showMetrics);
|
|
526
|
-
}
|
|
527
|
-
return infoSection;
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Append the Tailwind breakpoint indicator to the info section.
|
|
531
|
-
*/
|
|
532
|
-
function appendBreakpointInfo(state, infoSection) {
|
|
533
|
-
if (!state.breakpointInfo)
|
|
534
|
-
return;
|
|
535
|
-
const bp = state.breakpointInfo.tailwindBreakpoint;
|
|
536
|
-
const breakpointData = TAILWIND_BREAKPOINTS[bp];
|
|
537
|
-
const bpSpan = document.createElement('span');
|
|
538
|
-
bpSpan.className = 'devbar-item';
|
|
539
|
-
Object.assign(bpSpan.style, { opacity: '0.9', cursor: 'default' });
|
|
540
|
-
// Use HTML tooltip for breakpoint info
|
|
541
|
-
attachBreakpointTooltip(state, bpSpan, bp, state.breakpointInfo.dimensions, breakpointData?.label || '');
|
|
542
|
-
let bpText = bp;
|
|
543
|
-
if (bp !== 'base') {
|
|
544
|
-
bpText =
|
|
545
|
-
bp === 'sm'
|
|
546
|
-
? `${bp} - ${state.breakpointInfo.dimensions.split('x')[0]}`
|
|
547
|
-
: `${bp} - ${state.breakpointInfo.dimensions}`;
|
|
548
|
-
}
|
|
549
|
-
bpSpan.textContent = bpText;
|
|
550
|
-
infoSection.appendChild(bpSpan);
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Build the metric configuration map from current perf stats.
|
|
554
|
-
*/
|
|
555
|
-
function buildMetricConfigs(perfStats) {
|
|
556
|
-
return {
|
|
557
|
-
fcp: {
|
|
558
|
-
label: 'FCP',
|
|
559
|
-
value: perfStats.fcp,
|
|
560
|
-
title: 'First Contentful Paint (FCP)',
|
|
561
|
-
description: 'Time until the first text or image renders on screen.',
|
|
562
|
-
thresholds: { good: '<1.8s', needsWork: '1.8-3s', poor: '>3s' },
|
|
563
|
-
},
|
|
564
|
-
lcp: {
|
|
565
|
-
label: 'LCP',
|
|
566
|
-
value: perfStats.lcp,
|
|
567
|
-
title: 'Largest Contentful Paint (LCP)',
|
|
568
|
-
description: 'Time until the largest visible element renders on screen.',
|
|
569
|
-
thresholds: { good: '<2.5s', needsWork: '2.5-4s', poor: '>4s' },
|
|
570
|
-
},
|
|
571
|
-
cls: {
|
|
572
|
-
label: 'CLS',
|
|
573
|
-
value: perfStats.cls,
|
|
574
|
-
title: 'Cumulative Layout Shift (CLS)',
|
|
575
|
-
description: 'Visual stability score. Higher values mean more unexpected layout shifts.',
|
|
576
|
-
thresholds: { good: '<0.1', needsWork: '0.1-0.25', poor: '>0.25' },
|
|
577
|
-
},
|
|
578
|
-
inp: {
|
|
579
|
-
label: 'INP',
|
|
580
|
-
value: perfStats.inp,
|
|
581
|
-
title: 'Interaction to Next Paint (INP)',
|
|
582
|
-
description: 'Responsiveness to user input. Measures the longest interaction delay.',
|
|
583
|
-
thresholds: { good: '<200ms', needsWork: '200-500ms', poor: '>500ms' },
|
|
584
|
-
},
|
|
585
|
-
pageSize: {
|
|
586
|
-
label: '',
|
|
587
|
-
value: perfStats.totalSize,
|
|
588
|
-
title: 'Total Page Size',
|
|
589
|
-
description: 'Compressed/transferred size including HTML, CSS, JS, images, and other resources.',
|
|
590
|
-
},
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* Append performance metric spans (visible metrics + hidden-metrics ellipsis) to the info section.
|
|
595
|
-
*/
|
|
596
|
-
function appendPerformanceMetrics(state, infoSection, showMetrics) {
|
|
597
|
-
if (!state.perfStats)
|
|
598
|
-
return;
|
|
599
|
-
const { visible, hidden } = getResponsiveMetricVisibility(state);
|
|
600
|
-
const metricConfigs = buildMetricConfigs(state.perfStats);
|
|
601
|
-
const addSeparator = () => {
|
|
602
|
-
const sep = document.createElement('span');
|
|
603
|
-
sep.style.opacity = '0.4';
|
|
604
|
-
sep.textContent = '|';
|
|
605
|
-
infoSection.appendChild(sep);
|
|
606
|
-
};
|
|
607
|
-
// Render visible metrics
|
|
608
|
-
for (const metric of visible) {
|
|
609
|
-
if (!showMetrics[metric])
|
|
610
|
-
continue;
|
|
611
|
-
const config = metricConfigs[metric];
|
|
612
|
-
addSeparator();
|
|
613
|
-
const span = document.createElement('span');
|
|
614
|
-
span.className = 'devbar-item';
|
|
615
|
-
Object.assign(span.style, {
|
|
616
|
-
opacity: metric === 'pageSize' ? '0.7' : '0.85',
|
|
617
|
-
cursor: 'default',
|
|
618
|
-
});
|
|
619
|
-
span.textContent = config.label ? `${config.label} ${config.value}` : config.value;
|
|
620
|
-
if (config.thresholds) {
|
|
621
|
-
attachMetricTooltip(state, span, config.title, config.description, config.thresholds);
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
attachInfoTooltip(state, span, config.title, config.description);
|
|
625
|
-
}
|
|
626
|
-
infoSection.appendChild(span);
|
|
627
|
-
}
|
|
628
|
-
// Render ellipsis button for hidden metrics
|
|
629
|
-
const hiddenMetricsEnabled = hidden.filter((m) => showMetrics[m]);
|
|
630
|
-
if (hiddenMetricsEnabled.length > 0) {
|
|
631
|
-
addSeparator();
|
|
632
|
-
appendHiddenMetricsEllipsis(state, infoSection, hiddenMetricsEnabled, metricConfigs);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Append the ellipsis button that reveals hidden metrics in a click-toggle tooltip.
|
|
637
|
-
*/
|
|
638
|
-
function appendHiddenMetricsEllipsis(state, infoSection, hiddenMetricsEnabled, metricConfigs) {
|
|
639
|
-
const ellipsisBtn = document.createElement('span');
|
|
640
|
-
ellipsisBtn.className = 'devbar-item devbar-clickable';
|
|
641
|
-
Object.assign(ellipsisBtn.style, {
|
|
642
|
-
opacity: '0.7',
|
|
643
|
-
cursor: 'pointer',
|
|
644
|
-
padding: '0 2px',
|
|
645
|
-
});
|
|
646
|
-
ellipsisBtn.textContent = '\u00B7\u00B7\u00B7';
|
|
647
|
-
// Attach click-toggle tooltip showing hidden metrics (for mobile support)
|
|
648
|
-
attachClickToggleTooltip(state, ellipsisBtn, (tooltip) => {
|
|
649
|
-
addTooltipTitle(state, tooltip, 'More Metrics');
|
|
650
|
-
const metricsContainer = document.createElement('div');
|
|
651
|
-
Object.assign(metricsContainer.style, {
|
|
652
|
-
display: 'flex',
|
|
653
|
-
flexDirection: 'column',
|
|
654
|
-
gap: '6px',
|
|
655
|
-
marginTop: '8px',
|
|
656
|
-
});
|
|
657
|
-
for (const metric of hiddenMetricsEnabled) {
|
|
658
|
-
const config = metricConfigs[metric];
|
|
659
|
-
const row = document.createElement('div');
|
|
660
|
-
Object.assign(row.style, {
|
|
661
|
-
display: 'flex',
|
|
662
|
-
justifyContent: 'space-between',
|
|
663
|
-
gap: '12px',
|
|
664
|
-
});
|
|
665
|
-
const labelSpan = document.createElement('span');
|
|
666
|
-
Object.assign(labelSpan.style, { color: CSS_COLORS.textMuted });
|
|
667
|
-
labelSpan.textContent = config.title.split('(')[0].trim();
|
|
668
|
-
const valueSpan = document.createElement('span');
|
|
669
|
-
Object.assign(valueSpan.style, { color: CSS_COLORS.text, fontWeight: '500' });
|
|
670
|
-
valueSpan.textContent = config.value;
|
|
671
|
-
row.appendChild(labelSpan);
|
|
672
|
-
row.appendChild(valueSpan);
|
|
673
|
-
metricsContainer.appendChild(row);
|
|
674
|
-
}
|
|
675
|
-
tooltip.appendChild(metricsContainer);
|
|
676
|
-
});
|
|
677
|
-
infoSection.appendChild(ellipsisBtn);
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Create the status row containing the connection indicator, info section, and console badges.
|
|
681
|
-
*/
|
|
682
|
-
function createStatusRow(state, showMetrics, showConsoleBadges, errorCount, warningCount, infoCount) {
|
|
683
|
-
const connIndicator = createExpandedConnectionIndicator(state);
|
|
684
|
-
const statusRow = document.createElement('div');
|
|
685
|
-
statusRow.className = 'devbar-status';
|
|
686
|
-
Object.assign(statusRow.style, {
|
|
687
|
-
display: 'flex',
|
|
688
|
-
alignItems: 'center',
|
|
689
|
-
gap: '0.5rem',
|
|
690
|
-
flexWrap: 'nowrap',
|
|
691
|
-
flexShrink: '0',
|
|
692
|
-
});
|
|
693
|
-
statusRow.appendChild(connIndicator);
|
|
694
|
-
const infoSection = createInfoSection(state, showMetrics);
|
|
695
|
-
statusRow.appendChild(infoSection);
|
|
696
|
-
// Console badges - add to status row so they stay with info
|
|
697
|
-
if (showConsoleBadges) {
|
|
698
|
-
if (errorCount > 0) {
|
|
699
|
-
statusRow.appendChild(createConsoleBadge(state, 'error', errorCount, BUTTON_COLORS.error));
|
|
700
|
-
}
|
|
701
|
-
if (warningCount > 0) {
|
|
702
|
-
statusRow.appendChild(createConsoleBadge(state, 'warn', warningCount, BUTTON_COLORS.warning));
|
|
703
|
-
}
|
|
704
|
-
if (infoCount > 0) {
|
|
705
|
-
statusRow.appendChild(createConsoleBadge(state, 'info', infoCount, BUTTON_COLORS.info));
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
return statusRow;
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Create the action buttons container (screenshot, AI review, outline, schema, a11y, settings, compact).
|
|
712
|
-
*/
|
|
713
|
-
function createActionButtonsContainer(state, showScreenshot, accentColor) {
|
|
714
|
-
const actionsContainer = document.createElement('div');
|
|
715
|
-
actionsContainer.className = 'devbar-actions';
|
|
716
|
-
if (showScreenshot) {
|
|
717
|
-
actionsContainer.appendChild(createScreenshotButton(state, accentColor));
|
|
718
|
-
}
|
|
719
|
-
actionsContainer.appendChild(createAIReviewButton(state));
|
|
720
|
-
actionsContainer.appendChild(createOutlineButton(state));
|
|
721
|
-
actionsContainer.appendChild(createSchemaButton(state));
|
|
722
|
-
actionsContainer.appendChild(createA11yButton(state));
|
|
723
|
-
actionsContainer.appendChild(createSettingsButton(state));
|
|
724
|
-
actionsContainer.appendChild(createCompactToggleButton(state));
|
|
725
|
-
return actionsContainer;
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Create the custom controls row for user-defined buttons.
|
|
729
|
-
* Returns null if there are no custom controls.
|
|
730
|
-
*/
|
|
731
|
-
function createCustomControlsRow(customControls, accentColor) {
|
|
732
|
-
if (customControls.length === 0)
|
|
733
|
-
return null;
|
|
734
|
-
const customRow = document.createElement('div');
|
|
735
|
-
Object.assign(customRow.style, {
|
|
736
|
-
display: 'flex',
|
|
737
|
-
flexWrap: 'wrap',
|
|
738
|
-
alignItems: 'center',
|
|
739
|
-
gap: '0.5rem',
|
|
740
|
-
padding: '0 0.75rem 0.5rem 0.75rem',
|
|
741
|
-
borderTop: `1px solid ${accentColor}30`,
|
|
742
|
-
marginTop: '0',
|
|
743
|
-
paddingTop: '0.5rem',
|
|
744
|
-
fontFamily: FONT_MONO,
|
|
745
|
-
fontSize: '0.6875rem',
|
|
746
|
-
});
|
|
747
|
-
customControls.forEach((control) => {
|
|
748
|
-
const btn = document.createElement('button');
|
|
749
|
-
btn.type = 'button';
|
|
750
|
-
const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
|
|
751
|
-
const isActive = control.active ?? false;
|
|
752
|
-
const isDisabled = control.disabled ?? false;
|
|
753
|
-
Object.assign(btn.style, {
|
|
754
|
-
padding: '4px 10px',
|
|
755
|
-
backgroundColor: isActive ? `${color}33` : 'transparent',
|
|
756
|
-
border: `1px solid ${isActive ? color : `${color}60`}`,
|
|
757
|
-
borderRadius: '6px',
|
|
758
|
-
color: isActive ? color : `${color}99`,
|
|
759
|
-
fontSize: '0.625rem',
|
|
760
|
-
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
761
|
-
opacity: isDisabled ? '0.5' : '1',
|
|
762
|
-
transition: 'all 150ms',
|
|
763
|
-
});
|
|
764
|
-
btn.textContent = control.label;
|
|
765
|
-
btn.disabled = isDisabled;
|
|
766
|
-
if (!isDisabled) {
|
|
767
|
-
btn.onmouseenter = () => {
|
|
768
|
-
btn.style.backgroundColor = `${color}20`;
|
|
769
|
-
btn.style.borderColor = color;
|
|
770
|
-
btn.style.color = color;
|
|
771
|
-
};
|
|
772
|
-
btn.onmouseleave = () => {
|
|
773
|
-
btn.style.backgroundColor = isActive ? `${color}33` : 'transparent';
|
|
774
|
-
btn.style.borderColor = isActive ? color : `${color}60`;
|
|
775
|
-
btn.style.color = isActive ? color : `${color}99`;
|
|
776
|
-
};
|
|
777
|
-
btn.onclick = () => control.onClick();
|
|
778
|
-
}
|
|
779
|
-
customRow.appendChild(btn);
|
|
780
|
-
});
|
|
781
|
-
return customRow;
|
|
782
|
-
}
|
|
783
|
-
// ============================================================================
|
|
784
|
-
// Expanded State — Orchestrator
|
|
785
|
-
// ============================================================================
|
|
786
|
-
function renderExpanded(state, customControls) {
|
|
787
|
-
if (!state.container)
|
|
788
|
-
return;
|
|
789
|
-
const { position, accentColor, showMetrics, showScreenshot, showConsoleBadges } = state.options;
|
|
790
|
-
const { errorCount, warningCount, infoCount } = state.getLogCounts();
|
|
791
|
-
const isCentered = position === 'bottom-center';
|
|
792
|
-
const wrapper = state.container;
|
|
793
|
-
// 1. Position and style the wrapper
|
|
794
|
-
const posStyle = computeExpandedPosition(state, position, isCentered);
|
|
795
|
-
styleExpandedWrapper(state, wrapper, posStyle, accentColor, isCentered);
|
|
796
|
-
// 2. Build the main row
|
|
797
|
-
const mainRow = createExpandedMainRow();
|
|
798
|
-
// 3. Status row (connection dot + info metrics + console badges)
|
|
799
|
-
const statusRow = createStatusRow(state, showMetrics, showConsoleBadges, errorCount, warningCount, infoCount);
|
|
800
|
-
mainRow.appendChild(statusRow);
|
|
801
|
-
// 4. Action buttons
|
|
802
|
-
const actionsContainer = createActionButtonsContainer(state, showScreenshot, accentColor);
|
|
803
|
-
mainRow.appendChild(actionsContainer);
|
|
804
|
-
wrapper.appendChild(mainRow);
|
|
805
|
-
// 5. Custom controls row (if any)
|
|
806
|
-
const customRow = createCustomControlsRow(customControls, accentColor);
|
|
807
|
-
if (customRow) {
|
|
808
|
-
wrapper.appendChild(customRow);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
// ============================================================================
|
|
812
|
-
// Button Creators
|
|
813
|
-
// ============================================================================
|
|
814
|
-
function createConsoleBadge(state, type, count, color) {
|
|
815
|
-
const labelMap = { error: 'error', warn: 'warning', info: 'info' };
|
|
816
|
-
const label = labelMap[type];
|
|
817
|
-
const isActive = state.consoleFilter === type;
|
|
818
|
-
const badge = document.createElement('span');
|
|
819
|
-
badge.className = 'devbar-badge';
|
|
820
|
-
Object.assign(badge.style, {
|
|
821
|
-
display: 'flex',
|
|
822
|
-
alignItems: 'center',
|
|
823
|
-
justifyContent: 'center',
|
|
824
|
-
minWidth: '18px',
|
|
825
|
-
height: '18px',
|
|
826
|
-
padding: '0 5px',
|
|
827
|
-
borderRadius: '9999px',
|
|
828
|
-
backgroundColor: isActive ? color : `${color}E6`,
|
|
829
|
-
color: '#fff',
|
|
830
|
-
fontSize: '0.625rem',
|
|
831
|
-
fontWeight: '600',
|
|
832
|
-
cursor: 'pointer',
|
|
833
|
-
boxShadow: isActive ? `0 0 8px ${color}CC` : 'none',
|
|
834
|
-
});
|
|
835
|
-
badge.textContent = count > 99 ? '99+' : String(count);
|
|
836
|
-
attachTextTooltip(state, badge, () => `${count} console ${label}${count === 1 ? '' : 's'} (click to view)`);
|
|
837
|
-
badge.onclick = () => {
|
|
838
|
-
const newFilter = state.consoleFilter === type ? null : type;
|
|
839
|
-
closeAllModals(state);
|
|
840
|
-
state.consoleFilter = newFilter;
|
|
841
|
-
state.render();
|
|
842
|
-
};
|
|
843
|
-
return badge;
|
|
844
|
-
}
|
|
845
|
-
function createScreenshotButton(state, accentColor) {
|
|
846
|
-
const btn = document.createElement('button');
|
|
847
|
-
btn.type = 'button';
|
|
848
|
-
btn.setAttribute('aria-label', 'Screenshot');
|
|
849
|
-
const hasSuccessState = state.copiedToClipboard || state.copiedPath || state.lastScreenshot;
|
|
850
|
-
const isDisabled = state.capturing;
|
|
851
|
-
const effectiveSave = resolveSaveLocation(state.options.saveLocation, state.sweetlinkConnected);
|
|
852
|
-
// Grey out only when effective save is 'local' but sweetlink not connected (explicit 'local' setting)
|
|
853
|
-
const isGreyedOut = effectiveSave === 'local' && !state.sweetlinkConnected && !hasSuccessState;
|
|
854
|
-
// Attach HTML tooltip
|
|
855
|
-
attachButtonTooltip(state, btn, accentColor, (tooltip, h) => {
|
|
856
|
-
if (state.copiedToClipboard) {
|
|
857
|
-
h.addSuccess('Copied to clipboard!');
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
if (state.copiedPath) {
|
|
861
|
-
h.addSuccess('Path copied to clipboard!');
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
if (state.lastScreenshot) {
|
|
865
|
-
const screenshotPath = state.lastScreenshot;
|
|
866
|
-
const isDownloaded = screenshotPath.endsWith('downloaded');
|
|
867
|
-
if (isDownloaded) {
|
|
868
|
-
h.addSuccess('Screenshot downloaded!');
|
|
869
|
-
}
|
|
870
|
-
else {
|
|
871
|
-
h.addSuccess('Screenshot saved!', screenshotPath);
|
|
872
|
-
const copyLink = document.createElement('div');
|
|
873
|
-
Object.assign(copyLink.style, {
|
|
874
|
-
color: accentColor,
|
|
875
|
-
cursor: 'pointer',
|
|
876
|
-
fontSize: '0.625rem',
|
|
877
|
-
marginTop: '6px',
|
|
878
|
-
opacity: '0.8',
|
|
879
|
-
transition: 'opacity 150ms',
|
|
880
|
-
});
|
|
881
|
-
copyLink.textContent = 'copy path';
|
|
882
|
-
copyLink.onmouseenter = () => {
|
|
883
|
-
copyLink.style.opacity = '1';
|
|
884
|
-
};
|
|
885
|
-
copyLink.onmouseleave = () => {
|
|
886
|
-
copyLink.style.opacity = '0.8';
|
|
887
|
-
};
|
|
888
|
-
copyLink.onclick = async (e) => {
|
|
889
|
-
e.stopPropagation();
|
|
890
|
-
try {
|
|
891
|
-
await navigator.clipboard.writeText(screenshotPath);
|
|
892
|
-
copyLink.textContent = '\u2713 copied!';
|
|
893
|
-
copyLink.style.cursor = 'default';
|
|
894
|
-
copyLink.onclick = null;
|
|
895
|
-
}
|
|
896
|
-
catch {
|
|
897
|
-
copyLink.textContent = '\u00d7 failed to copy';
|
|
898
|
-
copyLink.style.color = CSS_COLORS.error;
|
|
899
|
-
}
|
|
900
|
-
};
|
|
901
|
-
tooltip.appendChild(copyLink);
|
|
902
|
-
}
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
h.addTitle('Screenshot');
|
|
906
|
-
h.addSectionHeader('Actions');
|
|
907
|
-
if (effectiveSave === 'local' && !state.sweetlinkConnected) {
|
|
908
|
-
h.addShortcut('Shift+Click', 'Copy to clipboard');
|
|
909
|
-
h.addWarning('Sweetlink not connected. Switch save method to Auto or Download.');
|
|
910
|
-
}
|
|
911
|
-
else {
|
|
912
|
-
const saveLabel = effectiveSave === 'local' ? 'Save to file' : 'Download';
|
|
913
|
-
h.addShortcut('Click', saveLabel);
|
|
914
|
-
h.addShortcut('Shift+Click', 'Copy to clipboard');
|
|
915
|
-
h.addSectionHeader('Keyboard');
|
|
916
|
-
h.addShortcut('Cmd or Ctrl+Shift+S', saveLabel);
|
|
917
|
-
h.addShortcut('Cmd or Ctrl+Shift+C', 'Copy');
|
|
918
|
-
}
|
|
919
|
-
});
|
|
920
|
-
Object.assign(btn.style, {
|
|
921
|
-
display: 'flex',
|
|
922
|
-
alignItems: 'center',
|
|
923
|
-
justifyContent: 'center',
|
|
924
|
-
width: '22px',
|
|
925
|
-
height: '22px',
|
|
926
|
-
minWidth: '22px',
|
|
927
|
-
minHeight: '22px',
|
|
928
|
-
flexShrink: '0',
|
|
929
|
-
borderRadius: '50%',
|
|
930
|
-
border: '1px solid',
|
|
931
|
-
borderColor: hasSuccessState ? accentColor : `${accentColor}80`,
|
|
932
|
-
backgroundColor: hasSuccessState ? `${accentColor}33` : 'transparent',
|
|
933
|
-
color: hasSuccessState ? accentColor : `${accentColor}99`,
|
|
934
|
-
cursor: !isDisabled ? 'pointer' : 'not-allowed',
|
|
935
|
-
opacity: isGreyedOut ? '0.4' : '1',
|
|
936
|
-
transition: 'all 150ms',
|
|
937
|
-
});
|
|
938
|
-
btn.disabled = isDisabled;
|
|
939
|
-
btn.onclick = (e) => {
|
|
940
|
-
// If we have a saved screenshot path, clicking copies the path
|
|
941
|
-
if (state.lastScreenshot && !e.shiftKey) {
|
|
942
|
-
copyPathToClipboard(state, state.lastScreenshot);
|
|
943
|
-
}
|
|
944
|
-
else {
|
|
945
|
-
state.handleScreenshot(e.shiftKey);
|
|
946
|
-
}
|
|
947
|
-
};
|
|
948
|
-
// Button content
|
|
949
|
-
if (state.copiedToClipboard || state.copiedPath || state.lastScreenshot) {
|
|
950
|
-
btn.textContent = '\u2713';
|
|
951
|
-
btn.style.fontSize = '0.6rem';
|
|
952
|
-
}
|
|
953
|
-
else if (state.capturing) {
|
|
954
|
-
btn.textContent = '...';
|
|
955
|
-
btn.style.fontSize = '0.5rem';
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
// Camera icon SVG
|
|
959
|
-
btn.appendChild(createSvgIcon('M19.844 7.938H7.938v11.905m0 11.113v11.906h11.905m23.019-11.906v11.906H30.956m11.906-23.018V7.938H30.956', {
|
|
960
|
-
viewBox: '0 0 50.8 50.8',
|
|
961
|
-
stroke: true,
|
|
962
|
-
strokeWidth: '4',
|
|
963
|
-
children: [{ type: 'circle', cx: '25.4', cy: '25.4', r: '8.731' }],
|
|
964
|
-
}));
|
|
965
|
-
}
|
|
966
|
-
return btn;
|
|
967
|
-
}
|
|
968
|
-
function createAIReviewButton(state) {
|
|
969
|
-
const btn = document.createElement('button');
|
|
970
|
-
btn.type = 'button';
|
|
971
|
-
btn.setAttribute('aria-label', 'AI Design Review');
|
|
972
|
-
const hasError = !!state.designReviewError;
|
|
973
|
-
const isActive = state.designReviewInProgress || !!state.lastDesignReview || hasError;
|
|
974
|
-
const isDisabled = state.designReviewInProgress || !state.sweetlinkConnected;
|
|
975
|
-
// Use error color (red) when there's an error, otherwise normal review color
|
|
976
|
-
const buttonColor = hasError ? CSS_COLORS.error : BUTTON_COLORS.review;
|
|
977
|
-
// Attach HTML tooltip
|
|
978
|
-
attachButtonTooltip(state, btn, buttonColor, (_tooltip, h) => {
|
|
979
|
-
if (state.designReviewInProgress) {
|
|
980
|
-
h.addProgress('AI Design Review in progress...');
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
if (state.designReviewError) {
|
|
984
|
-
h.addError('Design review failed', state.designReviewError);
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
if (state.lastDesignReview) {
|
|
988
|
-
h.addSuccess('Design review saved!', state.lastDesignReview);
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
h.addTitle('AI Design Review');
|
|
992
|
-
h.addDescription('Captures screenshot and sends to Claude for design analysis.');
|
|
993
|
-
h.addSectionHeader('Requirements');
|
|
994
|
-
h.addShortcut('API Key', 'ANTHROPIC_API_KEY');
|
|
995
|
-
if (!state.sweetlinkConnected) {
|
|
996
|
-
h.addWarning('Sweetlink not connected');
|
|
997
|
-
}
|
|
998
|
-
});
|
|
999
|
-
Object.assign(btn.style, getButtonStyles(buttonColor, isActive, isDisabled));
|
|
1000
|
-
if (!state.sweetlinkConnected)
|
|
1001
|
-
btn.style.opacity = '0.5';
|
|
1002
|
-
btn.disabled = isDisabled;
|
|
1003
|
-
btn.onclick = () => showDesignReviewConfirmation(state);
|
|
1004
|
-
if (state.designReviewInProgress) {
|
|
1005
|
-
btn.textContent = '~';
|
|
1006
|
-
btn.style.fontSize = '0.5rem';
|
|
1007
|
-
btn.style.animation = 'pulse 1s infinite';
|
|
1008
|
-
}
|
|
1009
|
-
else if (state.designReviewError) {
|
|
1010
|
-
// Show 'x' for error state
|
|
1011
|
-
btn.textContent = '\u00D7';
|
|
1012
|
-
btn.style.fontSize = '0.875rem';
|
|
1013
|
-
btn.style.fontWeight = 'bold';
|
|
1014
|
-
}
|
|
1015
|
-
else if (state.lastDesignReview) {
|
|
1016
|
-
btn.textContent = 'v';
|
|
1017
|
-
btn.style.fontSize = '0.5rem';
|
|
1018
|
-
}
|
|
1019
|
-
else {
|
|
1020
|
-
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 }));
|
|
1021
|
-
}
|
|
1022
|
-
return btn;
|
|
1023
|
-
}
|
|
1024
|
-
function createOutlineButton(state) {
|
|
1025
|
-
const btn = document.createElement('button');
|
|
1026
|
-
btn.type = 'button';
|
|
1027
|
-
btn.setAttribute('aria-label', 'Document Outline');
|
|
1028
|
-
const isActive = state.showOutlineModal || !!state.lastOutline;
|
|
1029
|
-
// Attach HTML tooltip
|
|
1030
|
-
attachButtonTooltip(state, btn, BUTTON_COLORS.outline, (_tooltip, h) => {
|
|
1031
|
-
if (state.lastOutline) {
|
|
1032
|
-
const isDownloaded = state.lastOutline.endsWith('downloaded');
|
|
1033
|
-
h.addSuccess(isDownloaded ? 'Outline downloaded!' : 'Outline saved!', isDownloaded ? undefined : state.lastOutline);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
h.addTitle('Document Outline');
|
|
1037
|
-
h.addDescription('View page heading structure and save as markdown.');
|
|
1038
|
-
if (state.options.saveLocation === 'local' && !state.sweetlinkConnected) {
|
|
1039
|
-
h.addWarning('Sweetlink not connected. Switch save method to Auto or Download.');
|
|
1040
|
-
}
|
|
1041
|
-
});
|
|
1042
|
-
Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.outline, isActive, false));
|
|
1043
|
-
btn.onclick = () => handleDocumentOutline(state);
|
|
1044
|
-
if (state.lastOutline) {
|
|
1045
|
-
btn.textContent = 'v';
|
|
1046
|
-
btn.style.fontSize = '0.5rem';
|
|
1047
|
-
}
|
|
1048
|
-
else {
|
|
1049
|
-
btn.appendChild(createSvgIcon('M3 4h18v2H3V4zm0 7h12v2H3v-2zm0 7h18v2H3v-2z', { fill: true }));
|
|
1050
|
-
}
|
|
1051
|
-
return btn;
|
|
1052
|
-
}
|
|
1053
|
-
function createSchemaButton(state) {
|
|
1054
|
-
const btn = document.createElement('button');
|
|
1055
|
-
btn.type = 'button';
|
|
1056
|
-
btn.setAttribute('aria-label', 'Page Schema');
|
|
1057
|
-
const isActive = state.showSchemaModal || !!state.lastSchema;
|
|
1058
|
-
// Attach HTML tooltip
|
|
1059
|
-
attachButtonTooltip(state, btn, BUTTON_COLORS.schema, (_tooltip, h) => {
|
|
1060
|
-
if (state.lastSchema) {
|
|
1061
|
-
const isDownloaded = state.lastSchema.endsWith('downloaded');
|
|
1062
|
-
h.addSuccess(isDownloaded ? 'Schema downloaded!' : 'Schema saved!', isDownloaded ? undefined : state.lastSchema);
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
h.addTitle('Page Schema');
|
|
1066
|
-
h.addDescription('View JSON-LD, Open Graph, and other structured data.');
|
|
1067
|
-
if (state.options.saveLocation === 'local' && !state.sweetlinkConnected) {
|
|
1068
|
-
h.addWarning('Sweetlink not connected. Switch save method to Auto or Download.');
|
|
1069
|
-
}
|
|
1070
|
-
});
|
|
1071
|
-
Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.schema, isActive, false));
|
|
1072
|
-
btn.onclick = () => handlePageSchema(state);
|
|
1073
|
-
if (state.lastSchema) {
|
|
1074
|
-
btn.textContent = 'v';
|
|
1075
|
-
btn.style.fontSize = '0.5rem';
|
|
1076
|
-
}
|
|
1077
|
-
else {
|
|
1078
|
-
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 }));
|
|
1079
|
-
}
|
|
1080
|
-
return btn;
|
|
1081
|
-
}
|
|
1082
|
-
function createA11yButton(state) {
|
|
1083
|
-
const btn = document.createElement('button');
|
|
1084
|
-
btn.type = 'button';
|
|
1085
|
-
btn.setAttribute('aria-label', 'Accessibility Audit');
|
|
1086
|
-
const isActive = state.showA11yModal || !!state.lastA11yAudit;
|
|
1087
|
-
attachButtonTooltip(state, btn, BUTTON_COLORS.a11y, (_tooltip, h) => {
|
|
1088
|
-
if (state.lastA11yAudit) {
|
|
1089
|
-
const isDownloaded = state.lastA11yAudit.endsWith('downloaded');
|
|
1090
|
-
h.addSuccess(isDownloaded ? 'A11y report downloaded!' : 'A11y report saved!', isDownloaded ? undefined : state.lastA11yAudit);
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
h.addTitle('Accessibility Audit');
|
|
1094
|
-
h.addDescription('Run axe-core audit to check WCAG compliance.');
|
|
1095
|
-
if (state.options.saveLocation === 'local' && !state.sweetlinkConnected) {
|
|
1096
|
-
h.addWarning('Sweetlink not connected. Switch save method to Auto or Download.');
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
// Preload axe-core on hover
|
|
1100
|
-
btn.addEventListener('mouseenter', () => preloadAxe(), { once: true });
|
|
1101
|
-
Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.a11y, isActive, false));
|
|
1102
|
-
btn.onclick = () => handleA11yAudit(state);
|
|
1103
|
-
if (state.lastA11yAudit) {
|
|
1104
|
-
btn.textContent = 'v';
|
|
1105
|
-
btn.style.fontSize = '0.5rem';
|
|
1106
|
-
}
|
|
1107
|
-
else {
|
|
1108
|
-
// Accessibility/shield icon
|
|
1109
|
-
btn.appendChild(createSvgIcon('M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z', { fill: true }));
|
|
1110
|
-
}
|
|
1111
|
-
return btn;
|
|
1112
|
-
}
|
|
1113
|
-
/**
|
|
1114
|
-
* Create the settings gear button.
|
|
1115
|
-
*/
|
|
1116
|
-
function createSettingsButton(state) {
|
|
1117
|
-
const btn = document.createElement('button');
|
|
1118
|
-
btn.type = 'button';
|
|
1119
|
-
btn.setAttribute('data-testid', 'devbar-settings-button');
|
|
1120
|
-
btn.setAttribute('aria-label', 'Settings');
|
|
1121
|
-
// Attach HTML tooltip
|
|
1122
|
-
attachButtonTooltip(state, btn, CSS_COLORS.textSecondary, (_tooltip, h) => {
|
|
1123
|
-
h.addTitle('Settings');
|
|
1124
|
-
h.addSectionHeader('Keyboard');
|
|
1125
|
-
h.addShortcut('Cmd or Ctrl+Shift+M', 'Toggle compact mode');
|
|
1126
|
-
});
|
|
1127
|
-
const isActive = state.showSettingsPopover;
|
|
1128
|
-
const color = CSS_COLORS.textSecondary;
|
|
1129
|
-
Object.assign(btn.style, getButtonStyles(color, isActive, false));
|
|
1130
|
-
btn.onclick = () => {
|
|
1131
|
-
const wasOpen = state.showSettingsPopover;
|
|
1132
|
-
closeAllModals(state);
|
|
1133
|
-
state.showSettingsPopover = !wasOpen;
|
|
1134
|
-
state.render();
|
|
1135
|
-
};
|
|
1136
|
-
// Gear icon SVG
|
|
1137
|
-
btn.appendChild(createSvgIcon('M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', { stroke: true, children: [{ type: 'circle', cx: '12', cy: '12', r: '3' }] }));
|
|
1138
|
-
return btn;
|
|
1139
|
-
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Create the compact mode toggle button with chevron icon.
|
|
1142
|
-
*/
|
|
1143
|
-
function createCompactToggleButton(state) {
|
|
1144
|
-
const btn = document.createElement('button');
|
|
1145
|
-
btn.type = 'button';
|
|
1146
|
-
btn.setAttribute('aria-label', state.compactMode ? 'Switch to expanded mode' : 'Switch to compact mode');
|
|
1147
|
-
const isCompact = state.compactMode;
|
|
1148
|
-
const { accentColor } = state.options;
|
|
1149
|
-
const iconColor = CSS_COLORS.textSecondary;
|
|
1150
|
-
Object.assign(btn.style, getButtonStyles(iconColor, false, false));
|
|
1151
|
-
btn.style.borderColor = `${accentColor}60`;
|
|
1152
|
-
attachTextTooltip(state, btn, () => (isCompact ? 'Expand (Cmd or Ctrl+Shift+M)' : 'Compact (Cmd or Ctrl+Shift+M)'), {
|
|
1153
|
-
onEnter: () => {
|
|
1154
|
-
btn.style.borderColor = accentColor;
|
|
1155
|
-
btn.style.backgroundColor = `${accentColor}20`;
|
|
1156
|
-
btn.style.color = iconColor;
|
|
1157
|
-
},
|
|
1158
|
-
onLeave: () => {
|
|
1159
|
-
btn.style.borderColor = `${accentColor}60`;
|
|
1160
|
-
btn.style.backgroundColor = 'transparent';
|
|
1161
|
-
btn.style.color = `${iconColor}99`;
|
|
1162
|
-
},
|
|
1163
|
-
});
|
|
1164
|
-
btn.onclick = () => {
|
|
1165
|
-
state.toggleCompactMode();
|
|
1166
|
-
};
|
|
1167
|
-
// Chevron icon SVG - points right when expanded, left when compact
|
|
1168
|
-
const chevronPoints = isCompact ? '9 18 15 12 9 6' : '15 18 9 12 15 6';
|
|
1169
|
-
btn.appendChild(createSvgIcon('', { stroke: true, children: [{ type: 'polyline', points: chevronPoints }] }));
|
|
1170
|
-
return btn;
|
|
1171
|
-
}
|
|
1172
|
-
// ============================================================================
|
|
1173
|
-
// Console Popup
|
|
1174
|
-
// ============================================================================
|
|
1175
|
-
function renderConsolePopup(state, consoleCaptureSingleton) {
|
|
1176
|
-
const filterType = state.consoleFilter;
|
|
1177
|
-
if (!filterType)
|
|
1178
|
-
return;
|
|
1179
|
-
const logs = consoleCaptureSingleton
|
|
1180
|
-
.getLogs()
|
|
1181
|
-
.filter((log) => log.level === filterType);
|
|
1182
|
-
const colorMap = { error: BUTTON_COLORS.error, warn: BUTTON_COLORS.warning, info: BUTTON_COLORS.info };
|
|
1183
|
-
const color = colorMap[filterType];
|
|
1184
|
-
const labelMap = { error: 'Errors', warn: 'Warnings', info: 'Info' };
|
|
1185
|
-
const label = labelMap[filterType];
|
|
1186
|
-
const closeModal = () => {
|
|
1187
|
-
state.consoleFilter = null;
|
|
1188
|
-
state.render();
|
|
1189
|
-
};
|
|
1190
|
-
const overlay = createModalOverlay(closeModal);
|
|
1191
|
-
const modal = createModalBox(color);
|
|
1192
|
-
const header = createModalHeader({
|
|
1193
|
-
color,
|
|
1194
|
-
title: `Console ${label} (${logs.length})`,
|
|
1195
|
-
onClose: closeModal,
|
|
1196
|
-
onCopyMd: async () => {
|
|
1197
|
-
await navigator.clipboard.writeText(consoleLogsToMarkdown(logs));
|
|
1198
|
-
},
|
|
1199
|
-
onSave: () => handleSaveConsoleLogs(state, logs),
|
|
1200
|
-
onClear: () => state.clearConsoleLogs(),
|
|
1201
|
-
sweetlinkConnected: state.sweetlinkConnected,
|
|
1202
|
-
saveLocation: state.options.saveLocation,
|
|
1203
|
-
isSaving: state.savingConsoleLogs,
|
|
1204
|
-
savedPath: state.lastConsoleLogs,
|
|
1205
|
-
});
|
|
1206
|
-
modal.appendChild(header);
|
|
1207
|
-
const content = createModalContent();
|
|
1208
|
-
if (logs.length === 0) {
|
|
1209
|
-
content.appendChild(createEmptyMessage(`No ${filterType}s recorded`));
|
|
1210
|
-
}
|
|
1211
|
-
else {
|
|
1212
|
-
renderConsoleLogs(content, logs, color);
|
|
1213
|
-
}
|
|
1214
|
-
modal.appendChild(content);
|
|
1215
|
-
overlay.appendChild(modal);
|
|
1216
|
-
state.overlayElement = overlay;
|
|
1217
|
-
document.body.appendChild(overlay);
|
|
1218
|
-
}
|
|
1219
|
-
function renderConsoleLogs(container, logs, color) {
|
|
1220
|
-
logs.forEach((log, index) => {
|
|
1221
|
-
const logItem = document.createElement('div');
|
|
1222
|
-
Object.assign(logItem.style, {
|
|
1223
|
-
padding: '8px 14px',
|
|
1224
|
-
borderBottom: index < logs.length - 1 ? '1px solid rgba(255, 255, 255, 0.05)' : 'none',
|
|
1225
|
-
});
|
|
1226
|
-
const timestamp = document.createElement('span');
|
|
1227
|
-
Object.assign(timestamp.style, {
|
|
1228
|
-
color: CSS_COLORS.textMuted,
|
|
1229
|
-
fontSize: '0.625rem',
|
|
1230
|
-
marginRight: '8px',
|
|
1231
|
-
});
|
|
1232
|
-
timestamp.textContent = new Date(log.timestamp).toLocaleTimeString();
|
|
1233
|
-
logItem.appendChild(timestamp);
|
|
1234
|
-
const message = document.createElement('span');
|
|
1235
|
-
Object.assign(message.style, {
|
|
1236
|
-
color,
|
|
1237
|
-
fontSize: '0.6875rem',
|
|
1238
|
-
wordBreak: 'break-word',
|
|
1239
|
-
whiteSpace: 'pre-wrap',
|
|
1240
|
-
});
|
|
1241
|
-
message.textContent = log.message;
|
|
1242
|
-
logItem.appendChild(message);
|
|
1243
|
-
container.appendChild(logItem);
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
// ============================================================================
|
|
1247
|
-
// Outline / Schema Modals
|
|
1248
|
-
// ============================================================================
|
|
1249
|
-
function renderOutlineModal(state) {
|
|
1250
|
-
const outline = extractDocumentOutline();
|
|
1251
|
-
const color = BUTTON_COLORS.outline;
|
|
1252
|
-
const closeModal = () => {
|
|
1253
|
-
state.showOutlineModal = false;
|
|
1254
|
-
state.render();
|
|
1255
|
-
};
|
|
1256
|
-
const overlay = createModalOverlay(closeModal);
|
|
1257
|
-
const modal = createModalBox(color);
|
|
1258
|
-
const header = createModalHeader({
|
|
1259
|
-
color,
|
|
1260
|
-
title: 'Document Outline',
|
|
1261
|
-
onClose: closeModal,
|
|
1262
|
-
onCopyMd: async () => {
|
|
1263
|
-
const markdown = outlineToMarkdown(outline);
|
|
1264
|
-
await navigator.clipboard.writeText(markdown);
|
|
1265
|
-
},
|
|
1266
|
-
onSave: () => handleSaveOutline(state),
|
|
1267
|
-
sweetlinkConnected: state.sweetlinkConnected,
|
|
1268
|
-
saveLocation: state.options.saveLocation,
|
|
1269
|
-
isSaving: state.savingOutline,
|
|
1270
|
-
savedPath: state.lastOutline,
|
|
1271
|
-
});
|
|
1272
|
-
modal.appendChild(header);
|
|
1273
|
-
const content = createModalContent();
|
|
1274
|
-
if (outline.length === 0) {
|
|
1275
|
-
content.appendChild(createEmptyMessage('No semantic elements found in this document'));
|
|
1276
|
-
}
|
|
1277
|
-
else {
|
|
1278
|
-
renderOutlineNodes(outline, content, 0, { lastHeadingLevel: 0 });
|
|
1279
|
-
}
|
|
1280
|
-
modal.appendChild(content);
|
|
1281
|
-
overlay.appendChild(modal);
|
|
1282
|
-
state.overlayElement = overlay;
|
|
1283
|
-
document.body.appendChild(overlay);
|
|
1284
|
-
}
|
|
1285
|
-
function renderOutlineNodes(nodes, parentEl, depth, headingTracker) {
|
|
1286
|
-
for (const node of nodes) {
|
|
1287
|
-
const isHeading = node.category === 'heading' && node.level > 0;
|
|
1288
|
-
const skippedLevel = isHeading && node.level > headingTracker.lastHeadingLevel + 1;
|
|
1289
|
-
if (isHeading) {
|
|
1290
|
-
headingTracker.lastHeadingLevel = node.level;
|
|
1291
|
-
}
|
|
1292
|
-
const nodeEl = document.createElement('div');
|
|
1293
|
-
Object.assign(nodeEl.style, {
|
|
1294
|
-
padding: `4px 0 4px ${depth * 16}px`,
|
|
1295
|
-
});
|
|
1296
|
-
// Warning icon for heading hierarchy breaks
|
|
1297
|
-
if (skippedLevel) {
|
|
1298
|
-
const warn = document.createElement('span');
|
|
1299
|
-
Object.assign(warn.style, {
|
|
1300
|
-
color: CSS_COLORS.error,
|
|
1301
|
-
fontSize: '0.625rem',
|
|
1302
|
-
marginRight: '4px',
|
|
1303
|
-
});
|
|
1304
|
-
warn.textContent = '\u26A0';
|
|
1305
|
-
warn.title = `Heading level skipped (expected h${node.level - 1} or higher before h${node.level})`;
|
|
1306
|
-
nodeEl.appendChild(warn);
|
|
1307
|
-
}
|
|
1308
|
-
const tagSpan = document.createElement('span');
|
|
1309
|
-
const categoryColor = CATEGORY_COLORS[node.category || 'other'] || CATEGORY_COLORS.other;
|
|
1310
|
-
Object.assign(tagSpan.style, {
|
|
1311
|
-
color: skippedLevel ? CSS_COLORS.error : categoryColor,
|
|
1312
|
-
fontSize: '0.6875rem',
|
|
1313
|
-
fontWeight: '500',
|
|
1314
|
-
});
|
|
1315
|
-
tagSpan.textContent = `<${node.tagName}>`;
|
|
1316
|
-
nodeEl.appendChild(tagSpan);
|
|
1317
|
-
if (node.category) {
|
|
1318
|
-
const categorySpan = document.createElement('span');
|
|
1319
|
-
Object.assign(categorySpan.style, {
|
|
1320
|
-
color: CSS_COLORS.textMuted,
|
|
1321
|
-
fontSize: '0.625rem',
|
|
1322
|
-
marginLeft: '6px',
|
|
1323
|
-
});
|
|
1324
|
-
categorySpan.textContent = `[${node.category}]`;
|
|
1325
|
-
nodeEl.appendChild(categorySpan);
|
|
1326
|
-
}
|
|
1327
|
-
const textSpan = document.createElement('span');
|
|
1328
|
-
Object.assign(textSpan.style, {
|
|
1329
|
-
color: '#d1d5db',
|
|
1330
|
-
fontSize: '0.6875rem',
|
|
1331
|
-
marginLeft: '8px',
|
|
1332
|
-
});
|
|
1333
|
-
const truncatedText = node.text.length > 60 ? `${node.text.slice(0, 60)}...` : node.text;
|
|
1334
|
-
textSpan.textContent = truncatedText;
|
|
1335
|
-
nodeEl.appendChild(textSpan);
|
|
1336
|
-
if (node.id) {
|
|
1337
|
-
const idSpan = document.createElement('span');
|
|
1338
|
-
Object.assign(idSpan.style, {
|
|
1339
|
-
color: CSS_COLORS.textSecondary,
|
|
1340
|
-
fontSize: '0.625rem',
|
|
1341
|
-
marginLeft: '6px',
|
|
1342
|
-
});
|
|
1343
|
-
idSpan.textContent = `#${node.id}`;
|
|
1344
|
-
nodeEl.appendChild(idSpan);
|
|
1345
|
-
}
|
|
1346
|
-
parentEl.appendChild(nodeEl);
|
|
1347
|
-
if (node.children.length > 0) {
|
|
1348
|
-
renderOutlineNodes(node.children, parentEl, depth + 1, headingTracker);
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
function renderSchemaModal(state) {
|
|
1353
|
-
const schema = extractPageSchema();
|
|
1354
|
-
const color = BUTTON_COLORS.schema;
|
|
1355
|
-
const closeModal = () => {
|
|
1356
|
-
state.showSchemaModal = false;
|
|
1357
|
-
state.render();
|
|
1358
|
-
};
|
|
1359
|
-
const overlay = createModalOverlay(closeModal);
|
|
1360
|
-
const modal = createModalBox(color);
|
|
1361
|
-
const missingTags = checkMissingTags(schema);
|
|
1362
|
-
const favicons = extractFavicons();
|
|
1363
|
-
const header = createModalHeader({
|
|
1364
|
-
color,
|
|
1365
|
-
title: 'Page Schema',
|
|
1366
|
-
onClose: closeModal,
|
|
1367
|
-
onCopyMd: async () => {
|
|
1368
|
-
const markdown = schemaToMarkdown(schema, { missingTags, favicons });
|
|
1369
|
-
await navigator.clipboard.writeText(markdown);
|
|
1370
|
-
},
|
|
1371
|
-
onSave: () => handleSaveSchema(state),
|
|
1372
|
-
sweetlinkConnected: state.sweetlinkConnected,
|
|
1373
|
-
saveLocation: state.options.saveLocation,
|
|
1374
|
-
isSaving: state.savingSchema,
|
|
1375
|
-
savedPath: state.lastSchema,
|
|
1376
|
-
});
|
|
1377
|
-
modal.appendChild(header);
|
|
1378
|
-
const content = createModalContent();
|
|
1379
|
-
const hasContent = schema.jsonLd.length > 0 ||
|
|
1380
|
-
Object.keys(schema.openGraph).length > 0 ||
|
|
1381
|
-
Object.keys(schema.twitter).length > 0 ||
|
|
1382
|
-
Object.keys(schema.metaTags).length > 0 ||
|
|
1383
|
-
favicons.length > 0 ||
|
|
1384
|
-
missingTags.length > 0;
|
|
1385
|
-
if (!hasContent) {
|
|
1386
|
-
content.appendChild(createEmptyMessage('No structured data found on this page'));
|
|
1387
|
-
}
|
|
1388
|
-
else {
|
|
1389
|
-
if (missingTags.length > 0)
|
|
1390
|
-
renderMissingTagsSection(content, missingTags);
|
|
1391
|
-
renderSchemaSection(content, 'Open Graph', schema.openGraph, CSS_COLORS.info);
|
|
1392
|
-
renderSchemaSection(content, 'Twitter Cards', schema.twitter, CSS_COLORS.cyan);
|
|
1393
|
-
if (favicons.length > 0)
|
|
1394
|
-
renderFaviconsSection(content, favicons);
|
|
1395
|
-
renderSchemaSection(content, 'JSON-LD', schema.jsonLd, color);
|
|
1396
|
-
renderSchemaSection(content, 'Meta Tags', schema.metaTags, CSS_COLORS.textMuted);
|
|
1397
|
-
}
|
|
1398
|
-
modal.appendChild(content);
|
|
1399
|
-
overlay.appendChild(modal);
|
|
1400
|
-
state.overlayElement = overlay;
|
|
1401
|
-
document.body.appendChild(overlay);
|
|
1402
|
-
}
|
|
1403
|
-
function renderSchemaSectionHeader(section, title, color, count) {
|
|
1404
|
-
const header = document.createElement('div');
|
|
1405
|
-
Object.assign(header.style, {
|
|
1406
|
-
display: 'flex',
|
|
1407
|
-
alignItems: 'center',
|
|
1408
|
-
gap: '8px',
|
|
1409
|
-
marginBottom: '10px',
|
|
1410
|
-
paddingBottom: '6px',
|
|
1411
|
-
borderBottom: `1px solid ${color}30`,
|
|
1412
|
-
});
|
|
1413
|
-
const titleEl = document.createElement('h3');
|
|
1414
|
-
Object.assign(titleEl.style, {
|
|
1415
|
-
color,
|
|
1416
|
-
fontSize: '0.8125rem',
|
|
1417
|
-
fontWeight: '600',
|
|
1418
|
-
margin: '0',
|
|
1419
|
-
});
|
|
1420
|
-
titleEl.textContent = title;
|
|
1421
|
-
header.appendChild(titleEl);
|
|
1422
|
-
const badge = document.createElement('span');
|
|
1423
|
-
Object.assign(badge.style, {
|
|
1424
|
-
color: `${color}cc`,
|
|
1425
|
-
fontSize: '0.5625rem',
|
|
1426
|
-
backgroundColor: `${color}18`,
|
|
1427
|
-
padding: '1px 6px',
|
|
1428
|
-
borderRadius: '8px',
|
|
1429
|
-
letterSpacing: '0.03em',
|
|
1430
|
-
});
|
|
1431
|
-
badge.textContent = String(count);
|
|
1432
|
-
header.appendChild(badge);
|
|
1433
|
-
section.appendChild(header);
|
|
1434
|
-
}
|
|
1435
|
-
function renderSchemaSection(container, title, items, color) {
|
|
1436
|
-
const count = Array.isArray(items) ? items.length : Object.keys(items).length;
|
|
1437
|
-
if (count === 0)
|
|
1438
|
-
return;
|
|
1439
|
-
const section = document.createElement('div');
|
|
1440
|
-
section.style.marginBottom = '20px';
|
|
1441
|
-
renderSchemaSectionHeader(section, title, color, count);
|
|
1442
|
-
if (Array.isArray(items)) {
|
|
1443
|
-
renderJsonLdItems(section, items, color);
|
|
1444
|
-
}
|
|
1445
|
-
else {
|
|
1446
|
-
renderKeyValueItems(section, items);
|
|
1447
|
-
}
|
|
1448
|
-
container.appendChild(section);
|
|
1449
|
-
}
|
|
1450
|
-
function renderJsonLdItems(container, items, color) {
|
|
1451
|
-
items.forEach((item, i) => {
|
|
1452
|
-
const itemEl = document.createElement('div');
|
|
1453
|
-
itemEl.style.marginBottom = '10px';
|
|
1454
|
-
// Extract @type for a meaningful label
|
|
1455
|
-
const typed = item;
|
|
1456
|
-
const schemaType = typeof typed?.['@type'] === 'string' ? typed['@type'] : null;
|
|
1457
|
-
const itemHeader = document.createElement('div');
|
|
1458
|
-
Object.assign(itemHeader.style, {
|
|
1459
|
-
display: 'flex',
|
|
1460
|
-
alignItems: 'center',
|
|
1461
|
-
gap: '6px',
|
|
1462
|
-
marginBottom: '4px',
|
|
1463
|
-
});
|
|
1464
|
-
const itemTitle = document.createElement('span');
|
|
1465
|
-
Object.assign(itemTitle.style, {
|
|
1466
|
-
color: CSS_COLORS.textSecondary,
|
|
1467
|
-
fontSize: '0.6875rem',
|
|
1468
|
-
});
|
|
1469
|
-
itemTitle.textContent = `Schema ${i + 1}`;
|
|
1470
|
-
itemHeader.appendChild(itemTitle);
|
|
1471
|
-
if (schemaType) {
|
|
1472
|
-
const typeTag = document.createElement('span');
|
|
1473
|
-
Object.assign(typeTag.style, {
|
|
1474
|
-
color: `${color}cc`,
|
|
1475
|
-
fontSize: '0.5625rem',
|
|
1476
|
-
backgroundColor: `${color}15`,
|
|
1477
|
-
border: `1px solid ${color}25`,
|
|
1478
|
-
padding: '0 5px',
|
|
1479
|
-
borderRadius: '3px',
|
|
1480
|
-
});
|
|
1481
|
-
typeTag.textContent = schemaType;
|
|
1482
|
-
itemHeader.appendChild(typeTag);
|
|
1483
|
-
}
|
|
1484
|
-
itemEl.appendChild(itemHeader);
|
|
1485
|
-
const codeEl = document.createElement('pre');
|
|
1486
|
-
Object.assign(codeEl.style, {
|
|
1487
|
-
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
|
1488
|
-
borderRadius: '4px',
|
|
1489
|
-
borderLeft: `2px solid ${color}50`,
|
|
1490
|
-
padding: '10px 10px 10px 12px',
|
|
1491
|
-
fontSize: '0.625rem',
|
|
1492
|
-
margin: '0',
|
|
1493
|
-
whiteSpace: 'pre-wrap',
|
|
1494
|
-
wordBreak: 'break-word',
|
|
1495
|
-
});
|
|
1496
|
-
appendHighlightedJson(codeEl, JSON.stringify(item, null, 2));
|
|
1497
|
-
itemEl.appendChild(codeEl);
|
|
1498
|
-
container.appendChild(itemEl);
|
|
1499
|
-
});
|
|
1500
|
-
}
|
|
1501
|
-
function appendHighlightedJson(container, json) {
|
|
1502
|
-
// Color map for different token types
|
|
1503
|
-
const colors = {
|
|
1504
|
-
key: CSS_COLORS.primary, // green
|
|
1505
|
-
string: CSS_COLORS.warning, // amber/yellow
|
|
1506
|
-
number: CSS_COLORS.purple, // purple
|
|
1507
|
-
boolean: CSS_COLORS.info, // blue
|
|
1508
|
-
nullVal: CSS_COLORS.error, // red
|
|
1509
|
-
punct: CSS_COLORS.textMuted, // gray
|
|
1510
|
-
};
|
|
1511
|
-
// Simple tokenizer for JSON using matchAll for safety
|
|
1512
|
-
const tokenPattern = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)|([{}[\],])|(\s+)/g;
|
|
1513
|
-
for (const match of json.matchAll(tokenPattern)) {
|
|
1514
|
-
const [, str, colon, bool, nullToken, num, punct, whitespace] = match;
|
|
1515
|
-
if (whitespace) {
|
|
1516
|
-
container.appendChild(document.createTextNode(whitespace));
|
|
1517
|
-
}
|
|
1518
|
-
else if (str !== undefined) {
|
|
1519
|
-
const span = document.createElement('span');
|
|
1520
|
-
span.style.color = colon ? colors.key : colors.string;
|
|
1521
|
-
span.textContent = str;
|
|
1522
|
-
container.appendChild(span);
|
|
1523
|
-
if (colon) {
|
|
1524
|
-
const colonSpan = document.createElement('span');
|
|
1525
|
-
colonSpan.style.color = colors.punct;
|
|
1526
|
-
colonSpan.textContent = ':';
|
|
1527
|
-
container.appendChild(colonSpan);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
else if (bool) {
|
|
1531
|
-
const span = document.createElement('span');
|
|
1532
|
-
span.style.color = colors.boolean;
|
|
1533
|
-
span.textContent = bool;
|
|
1534
|
-
container.appendChild(span);
|
|
1535
|
-
}
|
|
1536
|
-
else if (nullToken) {
|
|
1537
|
-
const span = document.createElement('span');
|
|
1538
|
-
span.style.color = colors.nullVal;
|
|
1539
|
-
span.textContent = nullToken;
|
|
1540
|
-
container.appendChild(span);
|
|
1541
|
-
}
|
|
1542
|
-
else if (num) {
|
|
1543
|
-
const span = document.createElement('span');
|
|
1544
|
-
span.style.color = colors.number;
|
|
1545
|
-
span.textContent = num;
|
|
1546
|
-
container.appendChild(span);
|
|
1547
|
-
}
|
|
1548
|
-
else if (punct) {
|
|
1549
|
-
const span = document.createElement('span');
|
|
1550
|
-
span.style.color = colors.punct;
|
|
1551
|
-
span.textContent = punct;
|
|
1552
|
-
container.appendChild(span);
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
function renderKeyValueItems(container, items) {
|
|
1557
|
-
const entries = Object.entries(items);
|
|
1558
|
-
entries.forEach(([key, value], i) => {
|
|
1559
|
-
const isImage = isImageKey(key);
|
|
1560
|
-
const row = document.createElement('div');
|
|
1561
|
-
Object.assign(row.style, {
|
|
1562
|
-
display: 'flex',
|
|
1563
|
-
padding: isImage ? '6px 8px' : '3px 8px',
|
|
1564
|
-
alignItems: 'flex-start',
|
|
1565
|
-
borderRadius: '3px',
|
|
1566
|
-
backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
|
|
1567
|
-
});
|
|
1568
|
-
const keyEl = document.createElement('span');
|
|
1569
|
-
Object.assign(keyEl.style, {
|
|
1570
|
-
color: CSS_COLORS.textSecondary,
|
|
1571
|
-
fontSize: '0.6875rem',
|
|
1572
|
-
width: '120px',
|
|
1573
|
-
minWidth: '120px',
|
|
1574
|
-
maxWidth: '120px',
|
|
1575
|
-
flexShrink: '0',
|
|
1576
|
-
overflow: 'hidden',
|
|
1577
|
-
textOverflow: 'ellipsis',
|
|
1578
|
-
whiteSpace: 'nowrap',
|
|
1579
|
-
paddingTop: isImage ? '2px' : '0',
|
|
1580
|
-
});
|
|
1581
|
-
keyEl.textContent = key;
|
|
1582
|
-
if (key.length > 18)
|
|
1583
|
-
keyEl.title = key;
|
|
1584
|
-
row.appendChild(keyEl);
|
|
1585
|
-
if (isImage && value) {
|
|
1586
|
-
const valueCol = document.createElement('div');
|
|
1587
|
-
Object.assign(valueCol.style, { flex: '1', minWidth: '0' });
|
|
1588
|
-
// Image frame with subtle border — fixed height to prevent layout jitter
|
|
1589
|
-
const frame = document.createElement('div');
|
|
1590
|
-
Object.assign(frame.style, {
|
|
1591
|
-
display: 'inline-block',
|
|
1592
|
-
padding: '4px',
|
|
1593
|
-
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
|
1594
|
-
border: '1px solid rgba(255, 255, 255, 0.06)',
|
|
1595
|
-
borderRadius: '4px',
|
|
1596
|
-
marginBottom: '4px',
|
|
1597
|
-
minHeight: '60px',
|
|
1598
|
-
minWidth: '80px',
|
|
1599
|
-
});
|
|
1600
|
-
const thumb = document.createElement('img');
|
|
1601
|
-
Object.assign(thumb.style, {
|
|
1602
|
-
width: '200px',
|
|
1603
|
-
height: '120px',
|
|
1604
|
-
objectFit: 'contain',
|
|
1605
|
-
borderRadius: '2px',
|
|
1606
|
-
display: 'block',
|
|
1607
|
-
});
|
|
1608
|
-
thumb.src = value;
|
|
1609
|
-
thumb.alt = key;
|
|
1610
|
-
thumb.onerror = () => { frame.style.display = 'none'; };
|
|
1611
|
-
thumb.onload = () => {
|
|
1612
|
-
if (thumb.naturalWidth) {
|
|
1613
|
-
dimEl.textContent = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
|
|
1614
|
-
}
|
|
1615
|
-
};
|
|
1616
|
-
frame.appendChild(thumb);
|
|
1617
|
-
valueCol.appendChild(frame);
|
|
1618
|
-
// Reserve space for dimension text to avoid reflow
|
|
1619
|
-
const dimEl = document.createElement('div');
|
|
1620
|
-
Object.assign(dimEl.style, {
|
|
1621
|
-
color: CSS_COLORS.textMuted,
|
|
1622
|
-
fontSize: '0.5625rem',
|
|
1623
|
-
minHeight: '0.75rem',
|
|
1624
|
-
letterSpacing: '0.02em',
|
|
1625
|
-
});
|
|
1626
|
-
valueCol.appendChild(dimEl);
|
|
1627
|
-
const urlEl = document.createElement('div');
|
|
1628
|
-
Object.assign(urlEl.style, {
|
|
1629
|
-
color: CSS_COLORS.textMuted,
|
|
1630
|
-
fontSize: '0.5625rem',
|
|
1631
|
-
wordBreak: 'break-all',
|
|
1632
|
-
opacity: '0.7',
|
|
1633
|
-
});
|
|
1634
|
-
urlEl.textContent = value;
|
|
1635
|
-
valueCol.appendChild(urlEl);
|
|
1636
|
-
row.appendChild(valueCol);
|
|
1637
|
-
}
|
|
1638
|
-
else {
|
|
1639
|
-
const valueEl = document.createElement('span');
|
|
1640
|
-
Object.assign(valueEl.style, {
|
|
1641
|
-
color: CSS_COLORS.text,
|
|
1642
|
-
fontSize: '0.6875rem',
|
|
1643
|
-
flex: '1',
|
|
1644
|
-
wordBreak: 'break-word',
|
|
1645
|
-
whiteSpace: 'pre-wrap',
|
|
1646
|
-
opacity: '0.85',
|
|
1647
|
-
});
|
|
1648
|
-
valueEl.textContent = String(value);
|
|
1649
|
-
row.appendChild(valueEl);
|
|
1650
|
-
}
|
|
1651
|
-
container.appendChild(row);
|
|
1652
|
-
});
|
|
1653
|
-
}
|
|
1654
|
-
/** Derive intended device/purpose from favicon label and declared size */
|
|
1655
|
-
function faviconDevice(label, size) {
|
|
1656
|
-
const s = parseInt(size || '', 10);
|
|
1657
|
-
if (label.includes('apple'))
|
|
1658
|
-
return { text: 'Apple home screen', color: CSS_COLORS.info };
|
|
1659
|
-
if (size === 'any' || label.includes('svg'))
|
|
1660
|
-
return { text: 'Scalable (any)', color: CSS_COLORS.cyan };
|
|
1661
|
-
if (s >= 192)
|
|
1662
|
-
return { text: 'Android / PWA', color: CSS_COLORS.primary };
|
|
1663
|
-
if (s >= 48)
|
|
1664
|
-
return { text: 'Taskbar / shortcut', color: CSS_COLORS.purple };
|
|
1665
|
-
if (s > 0)
|
|
1666
|
-
return { text: 'Browser tab', color: CSS_COLORS.textSecondary };
|
|
1667
|
-
return { text: 'General', color: CSS_COLORS.textMuted };
|
|
1668
|
-
}
|
|
1669
|
-
function renderFaviconsSection(container, icons) {
|
|
1670
|
-
const color = CSS_COLORS.purple;
|
|
1671
|
-
const section = document.createElement('div');
|
|
1672
|
-
section.style.marginBottom = '20px';
|
|
1673
|
-
renderSchemaSectionHeader(section, 'Favicons', color, icons.length);
|
|
1674
|
-
icons.forEach((icon, i) => {
|
|
1675
|
-
const device = faviconDevice(icon.label, icon.size);
|
|
1676
|
-
const row = document.createElement('div');
|
|
1677
|
-
Object.assign(row.style, {
|
|
1678
|
-
display: 'flex',
|
|
1679
|
-
alignItems: 'center',
|
|
1680
|
-
padding: '6px 8px',
|
|
1681
|
-
gap: '10px',
|
|
1682
|
-
borderRadius: '3px',
|
|
1683
|
-
backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
|
|
1684
|
-
});
|
|
1685
|
-
// Thumbnail frame
|
|
1686
|
-
const frame = document.createElement('div');
|
|
1687
|
-
Object.assign(frame.style, {
|
|
1688
|
-
width: '32px',
|
|
1689
|
-
height: '32px',
|
|
1690
|
-
display: 'flex',
|
|
1691
|
-
alignItems: 'center',
|
|
1692
|
-
justifyContent: 'center',
|
|
1693
|
-
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
|
1694
|
-
border: '1px solid rgba(255, 255, 255, 0.06)',
|
|
1695
|
-
borderRadius: '4px',
|
|
1696
|
-
flexShrink: '0',
|
|
1697
|
-
});
|
|
1698
|
-
const thumb = document.createElement('img');
|
|
1699
|
-
Object.assign(thumb.style, {
|
|
1700
|
-
width: '22px',
|
|
1701
|
-
height: '22px',
|
|
1702
|
-
objectFit: 'contain',
|
|
1703
|
-
});
|
|
1704
|
-
thumb.src = icon.url;
|
|
1705
|
-
thumb.alt = icon.label;
|
|
1706
|
-
thumb.onerror = () => { frame.style.opacity = '0.3'; };
|
|
1707
|
-
frame.appendChild(thumb);
|
|
1708
|
-
row.appendChild(frame);
|
|
1709
|
-
// Info column: label, device, dimensions + URL
|
|
1710
|
-
const infoCol = document.createElement('div');
|
|
1711
|
-
Object.assign(infoCol.style, {
|
|
1712
|
-
flex: '1',
|
|
1713
|
-
minWidth: '0',
|
|
1714
|
-
display: 'flex',
|
|
1715
|
-
flexDirection: 'column',
|
|
1716
|
-
gap: '2px',
|
|
1717
|
-
});
|
|
1718
|
-
// Top row: label + device pill
|
|
1719
|
-
const topRow = document.createElement('div');
|
|
1720
|
-
Object.assign(topRow.style, {
|
|
1721
|
-
display: 'flex',
|
|
1722
|
-
alignItems: 'center',
|
|
1723
|
-
gap: '6px',
|
|
1724
|
-
});
|
|
1725
|
-
const labelEl = document.createElement('span');
|
|
1726
|
-
Object.assign(labelEl.style, {
|
|
1727
|
-
color: CSS_COLORS.text,
|
|
1728
|
-
fontSize: '0.6875rem',
|
|
1729
|
-
fontWeight: '500',
|
|
1730
|
-
overflow: 'hidden',
|
|
1731
|
-
textOverflow: 'ellipsis',
|
|
1732
|
-
whiteSpace: 'nowrap',
|
|
1733
|
-
});
|
|
1734
|
-
labelEl.textContent = icon.label;
|
|
1735
|
-
if (icon.label.length > 24)
|
|
1736
|
-
labelEl.title = icon.label;
|
|
1737
|
-
topRow.appendChild(labelEl);
|
|
1738
|
-
const devicePill = document.createElement('span');
|
|
1739
|
-
Object.assign(devicePill.style, {
|
|
1740
|
-
color: device.color,
|
|
1741
|
-
fontSize: '0.5rem',
|
|
1742
|
-
backgroundColor: `${device.color}12`,
|
|
1743
|
-
padding: '1px 6px',
|
|
1744
|
-
borderRadius: '6px',
|
|
1745
|
-
letterSpacing: '0.03em',
|
|
1746
|
-
whiteSpace: 'nowrap',
|
|
1747
|
-
flexShrink: '0',
|
|
1748
|
-
});
|
|
1749
|
-
devicePill.textContent = device.text;
|
|
1750
|
-
topRow.appendChild(devicePill);
|
|
1751
|
-
infoCol.appendChild(topRow);
|
|
1752
|
-
// Bottom row: declared size + actual dimensions + URL
|
|
1753
|
-
const bottomRow = document.createElement('div');
|
|
1754
|
-
Object.assign(bottomRow.style, {
|
|
1755
|
-
display: 'flex',
|
|
1756
|
-
alignItems: 'center',
|
|
1757
|
-
gap: '6px',
|
|
1758
|
-
fontSize: '0.5625rem',
|
|
1759
|
-
color: CSS_COLORS.textMuted,
|
|
1760
|
-
});
|
|
1761
|
-
if (icon.size) {
|
|
1762
|
-
const declaredEl = document.createElement('span');
|
|
1763
|
-
declaredEl.textContent = icon.size;
|
|
1764
|
-
declaredEl.style.opacity = '0.8';
|
|
1765
|
-
bottomRow.appendChild(declaredEl);
|
|
1766
|
-
}
|
|
1767
|
-
// Actual dimensions (populated on load)
|
|
1768
|
-
const dimEl = document.createElement('span');
|
|
1769
|
-
dimEl.style.letterSpacing = '0.02em';
|
|
1770
|
-
bottomRow.appendChild(dimEl);
|
|
1771
|
-
thumb.onload = () => {
|
|
1772
|
-
if (thumb.naturalWidth) {
|
|
1773
|
-
const actual = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
|
|
1774
|
-
if (icon.size) {
|
|
1775
|
-
dimEl.textContent = `\u2192 ${actual}`;
|
|
1776
|
-
}
|
|
1777
|
-
else {
|
|
1778
|
-
dimEl.textContent = actual;
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
};
|
|
1782
|
-
const sep = document.createElement('span');
|
|
1783
|
-
sep.textContent = '\u00b7';
|
|
1784
|
-
sep.style.opacity = '0.4';
|
|
1785
|
-
bottomRow.appendChild(sep);
|
|
1786
|
-
const urlEl = document.createElement('span');
|
|
1787
|
-
Object.assign(urlEl.style, {
|
|
1788
|
-
overflow: 'hidden',
|
|
1789
|
-
textOverflow: 'ellipsis',
|
|
1790
|
-
whiteSpace: 'nowrap',
|
|
1791
|
-
opacity: '0.6',
|
|
1792
|
-
});
|
|
1793
|
-
urlEl.textContent = icon.url;
|
|
1794
|
-
urlEl.title = icon.url;
|
|
1795
|
-
bottomRow.appendChild(urlEl);
|
|
1796
|
-
infoCol.appendChild(bottomRow);
|
|
1797
|
-
row.appendChild(infoCol);
|
|
1798
|
-
section.appendChild(row);
|
|
1799
|
-
});
|
|
1800
|
-
container.appendChild(section);
|
|
1801
|
-
}
|
|
1802
|
-
function renderMissingTagsSection(container, tags) {
|
|
1803
|
-
const section = document.createElement('div');
|
|
1804
|
-
section.style.marginBottom = '20px';
|
|
1805
|
-
const errorCount = tags.filter((t) => t.severity === 'error').length;
|
|
1806
|
-
const warnCount = tags.length - errorCount;
|
|
1807
|
-
const hasErrors = errorCount > 0;
|
|
1808
|
-
const sectionColor = hasErrors ? CSS_COLORS.error : CSS_COLORS.warning;
|
|
1809
|
-
renderSchemaSectionHeader(section, 'Missing Tags', sectionColor, tags.length);
|
|
1810
|
-
// Summary pill row
|
|
1811
|
-
if (errorCount > 0 || warnCount > 0) {
|
|
1812
|
-
const summary = document.createElement('div');
|
|
1813
|
-
Object.assign(summary.style, {
|
|
1814
|
-
display: 'flex',
|
|
1815
|
-
gap: '8px',
|
|
1816
|
-
marginBottom: '8px',
|
|
1817
|
-
});
|
|
1818
|
-
if (errorCount > 0) {
|
|
1819
|
-
const errPill = document.createElement('span');
|
|
1820
|
-
Object.assign(errPill.style, {
|
|
1821
|
-
color: CSS_COLORS.error,
|
|
1822
|
-
fontSize: '0.5625rem',
|
|
1823
|
-
backgroundColor: `${CSS_COLORS.error}15`,
|
|
1824
|
-
padding: '2px 8px',
|
|
1825
|
-
borderRadius: '8px',
|
|
1826
|
-
letterSpacing: '0.03em',
|
|
1827
|
-
});
|
|
1828
|
-
errPill.textContent = `${errorCount} error${errorCount > 1 ? 's' : ''}`;
|
|
1829
|
-
summary.appendChild(errPill);
|
|
1830
|
-
}
|
|
1831
|
-
if (warnCount > 0) {
|
|
1832
|
-
const warnPill = document.createElement('span');
|
|
1833
|
-
Object.assign(warnPill.style, {
|
|
1834
|
-
color: CSS_COLORS.warning,
|
|
1835
|
-
fontSize: '0.5625rem',
|
|
1836
|
-
backgroundColor: `${CSS_COLORS.warning}15`,
|
|
1837
|
-
padding: '2px 8px',
|
|
1838
|
-
borderRadius: '8px',
|
|
1839
|
-
letterSpacing: '0.03em',
|
|
1840
|
-
});
|
|
1841
|
-
warnPill.textContent = `${warnCount} warning${warnCount > 1 ? 's' : ''}`;
|
|
1842
|
-
summary.appendChild(warnPill);
|
|
1843
|
-
}
|
|
1844
|
-
section.appendChild(summary);
|
|
1845
|
-
}
|
|
1846
|
-
tags.forEach((tag, i) => {
|
|
1847
|
-
const isError = tag.severity === 'error';
|
|
1848
|
-
const tagColor = isError ? CSS_COLORS.error : CSS_COLORS.warning;
|
|
1849
|
-
const row = document.createElement('div');
|
|
1850
|
-
Object.assign(row.style, {
|
|
1851
|
-
display: 'flex',
|
|
1852
|
-
alignItems: 'center',
|
|
1853
|
-
padding: '4px 8px',
|
|
1854
|
-
gap: '8px',
|
|
1855
|
-
borderRadius: '3px',
|
|
1856
|
-
backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
|
|
1857
|
-
borderLeft: `2px solid ${tagColor}40`,
|
|
1858
|
-
});
|
|
1859
|
-
const icon = document.createElement('span');
|
|
1860
|
-
Object.assign(icon.style, {
|
|
1861
|
-
fontSize: '0.625rem',
|
|
1862
|
-
flexShrink: '0',
|
|
1863
|
-
width: '14px',
|
|
1864
|
-
textAlign: 'center',
|
|
1865
|
-
color: tagColor,
|
|
1866
|
-
});
|
|
1867
|
-
icon.textContent = isError ? '\u2718' : '\u26a0';
|
|
1868
|
-
row.appendChild(icon);
|
|
1869
|
-
const tagName = document.createElement('span');
|
|
1870
|
-
Object.assign(tagName.style, {
|
|
1871
|
-
color: CSS_COLORS.text,
|
|
1872
|
-
fontSize: '0.6875rem',
|
|
1873
|
-
width: '120px',
|
|
1874
|
-
minWidth: '120px',
|
|
1875
|
-
flexShrink: '0',
|
|
1876
|
-
fontWeight: '500',
|
|
1877
|
-
});
|
|
1878
|
-
tagName.textContent = tag.tag;
|
|
1879
|
-
row.appendChild(tagName);
|
|
1880
|
-
const hint = document.createElement('span');
|
|
1881
|
-
Object.assign(hint.style, {
|
|
1882
|
-
color: CSS_COLORS.textMuted,
|
|
1883
|
-
fontSize: '0.6875rem',
|
|
1884
|
-
flex: '1',
|
|
1885
|
-
opacity: '0.85',
|
|
1886
|
-
});
|
|
1887
|
-
hint.textContent = tag.hint;
|
|
1888
|
-
row.appendChild(hint);
|
|
1889
|
-
section.appendChild(row);
|
|
1890
|
-
});
|
|
1891
|
-
container.appendChild(section);
|
|
1892
|
-
}
|
|
1893
|
-
// ============================================================================
|
|
1894
|
-
// Accessibility Audit Modal
|
|
1895
|
-
// ============================================================================
|
|
1896
|
-
function clearChildren(el) {
|
|
1897
|
-
while (el.firstChild) {
|
|
1898
|
-
el.removeChild(el.firstChild);
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
function renderA11yModal(state) {
|
|
1902
|
-
const color = BUTTON_COLORS.a11y;
|
|
1903
|
-
const closeModal = () => {
|
|
1904
|
-
state.showA11yModal = false;
|
|
1905
|
-
state.render();
|
|
1906
|
-
};
|
|
1907
|
-
const overlay = createModalOverlay(closeModal);
|
|
1908
|
-
const modal = createModalBox(color);
|
|
1909
|
-
// Show loading state initially
|
|
1910
|
-
const loadingContent = createModalContent();
|
|
1911
|
-
const loadingMsg = document.createElement('div');
|
|
1912
|
-
Object.assign(loadingMsg.style, {
|
|
1913
|
-
textAlign: 'center',
|
|
1914
|
-
padding: '40px',
|
|
1915
|
-
color: CSS_COLORS.textSecondary,
|
|
1916
|
-
fontSize: '0.875rem',
|
|
1917
|
-
});
|
|
1918
|
-
loadingMsg.textContent = 'Running accessibility audit...';
|
|
1919
|
-
loadingMsg.style.animation = 'pulse 1.5s ease-in-out infinite';
|
|
1920
|
-
loadingContent.appendChild(loadingMsg);
|
|
1921
|
-
// Temporary header without save/copy (shown during loading)
|
|
1922
|
-
const loadingHeader = createModalHeader({
|
|
1923
|
-
color,
|
|
1924
|
-
title: 'Accessibility Audit',
|
|
1925
|
-
onClose: closeModal,
|
|
1926
|
-
onCopyMd: async () => { },
|
|
1927
|
-
sweetlinkConnected: state.sweetlinkConnected,
|
|
1928
|
-
saveLocation: state.options.saveLocation,
|
|
1929
|
-
});
|
|
1930
|
-
modal.appendChild(loadingHeader);
|
|
1931
|
-
modal.appendChild(loadingContent);
|
|
1932
|
-
overlay.appendChild(modal);
|
|
1933
|
-
state.overlayElement = overlay;
|
|
1934
|
-
document.body.appendChild(overlay);
|
|
1935
|
-
// Run the audit async and replace content when done
|
|
1936
|
-
runA11yAudit().then((result) => {
|
|
1937
|
-
// Check modal is still open
|
|
1938
|
-
if (!state.showA11yModal)
|
|
1939
|
-
return;
|
|
1940
|
-
const markdown = a11yToMarkdown(result);
|
|
1941
|
-
// Replace modal content
|
|
1942
|
-
clearChildren(modal);
|
|
1943
|
-
const violationCount = result.violations.length;
|
|
1944
|
-
const titleText = violationCount === 0
|
|
1945
|
-
? 'Accessibility Audit \u2014 No Issues'
|
|
1946
|
-
: `Accessibility Audit \u2014 ${violationCount} Violation${violationCount === 1 ? '' : 's'}`;
|
|
1947
|
-
const header = createModalHeader({
|
|
1948
|
-
color,
|
|
1949
|
-
title: titleText,
|
|
1950
|
-
onClose: closeModal,
|
|
1951
|
-
onCopyMd: async () => {
|
|
1952
|
-
await navigator.clipboard.writeText(markdown);
|
|
1953
|
-
},
|
|
1954
|
-
onSave: () => handleSaveA11yAudit(state, result),
|
|
1955
|
-
sweetlinkConnected: state.sweetlinkConnected,
|
|
1956
|
-
saveLocation: state.options.saveLocation,
|
|
1957
|
-
isSaving: state.savingA11yAudit,
|
|
1958
|
-
savedPath: state.lastA11yAudit,
|
|
1959
|
-
});
|
|
1960
|
-
modal.appendChild(header);
|
|
1961
|
-
const content = createModalContent();
|
|
1962
|
-
if (result.violations.length === 0) {
|
|
1963
|
-
const successMsg = document.createElement('div');
|
|
1964
|
-
Object.assign(successMsg.style, {
|
|
1965
|
-
textAlign: 'center',
|
|
1966
|
-
padding: '40px',
|
|
1967
|
-
color: CSS_COLORS.primary,
|
|
1968
|
-
fontSize: '0.875rem',
|
|
1969
|
-
});
|
|
1970
|
-
successMsg.textContent = 'No accessibility violations found!';
|
|
1971
|
-
content.appendChild(successMsg);
|
|
1972
|
-
// Show pass count
|
|
1973
|
-
if (result.passes.length > 0) {
|
|
1974
|
-
const passInfo = document.createElement('div');
|
|
1975
|
-
Object.assign(passInfo.style, {
|
|
1976
|
-
textAlign: 'center',
|
|
1977
|
-
color: CSS_COLORS.textMuted,
|
|
1978
|
-
fontSize: '0.75rem',
|
|
1979
|
-
marginTop: '8px',
|
|
1980
|
-
});
|
|
1981
|
-
passInfo.textContent = `${result.passes.length} rules passed`;
|
|
1982
|
-
content.appendChild(passInfo);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
else {
|
|
1986
|
-
// Summary bar
|
|
1987
|
-
const counts = getViolationCounts(result.violations);
|
|
1988
|
-
const summaryBar = document.createElement('div');
|
|
1989
|
-
Object.assign(summaryBar.style, {
|
|
1990
|
-
display: 'flex',
|
|
1991
|
-
gap: '12px',
|
|
1992
|
-
marginBottom: '16px',
|
|
1993
|
-
padding: '10px 12px',
|
|
1994
|
-
backgroundColor: `${color}10`,
|
|
1995
|
-
border: `1px solid ${color}30`,
|
|
1996
|
-
borderRadius: '6px',
|
|
1997
|
-
flexWrap: 'wrap',
|
|
1998
|
-
});
|
|
1999
|
-
for (const impact of ['critical', 'serious', 'moderate', 'minor']) {
|
|
2000
|
-
if (counts[impact] === 0)
|
|
2001
|
-
continue;
|
|
2002
|
-
const badge = document.createElement('span');
|
|
2003
|
-
const impactColor = getImpactColor(impact);
|
|
2004
|
-
Object.assign(badge.style, {
|
|
2005
|
-
display: 'inline-flex',
|
|
2006
|
-
alignItems: 'center',
|
|
2007
|
-
gap: '4px',
|
|
2008
|
-
fontSize: '0.6875rem',
|
|
2009
|
-
fontWeight: '600',
|
|
2010
|
-
color: impactColor,
|
|
2011
|
-
});
|
|
2012
|
-
const dot = document.createElement('span');
|
|
2013
|
-
Object.assign(dot.style, {
|
|
2014
|
-
width: '6px',
|
|
2015
|
-
height: '6px',
|
|
2016
|
-
borderRadius: '50%',
|
|
2017
|
-
backgroundColor: impactColor,
|
|
2018
|
-
});
|
|
2019
|
-
badge.appendChild(dot);
|
|
2020
|
-
badge.appendChild(document.createTextNode(`${counts[impact]} ${impact}`));
|
|
2021
|
-
summaryBar.appendChild(badge);
|
|
2022
|
-
}
|
|
2023
|
-
content.appendChild(summaryBar);
|
|
2024
|
-
// Grouped violations
|
|
2025
|
-
const grouped = groupViolationsByImpact(result.violations);
|
|
2026
|
-
for (const [impact, violations] of grouped) {
|
|
2027
|
-
if (violations.length === 0)
|
|
2028
|
-
continue;
|
|
2029
|
-
renderA11yViolationGroup(content, impact, violations);
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
modal.appendChild(content);
|
|
2033
|
-
}).catch((err) => {
|
|
2034
|
-
if (!state.showA11yModal)
|
|
2035
|
-
return;
|
|
2036
|
-
clearChildren(modal);
|
|
2037
|
-
const header = createModalHeader({
|
|
2038
|
-
color: CSS_COLORS.error,
|
|
2039
|
-
title: 'Accessibility Audit \u2014 Error',
|
|
2040
|
-
onClose: closeModal,
|
|
2041
|
-
onCopyMd: async () => { },
|
|
2042
|
-
sweetlinkConnected: state.sweetlinkConnected,
|
|
2043
|
-
saveLocation: state.options.saveLocation,
|
|
2044
|
-
});
|
|
2045
|
-
modal.appendChild(header);
|
|
2046
|
-
const content = createModalContent();
|
|
2047
|
-
content.appendChild(createInfoBox(CSS_COLORS.error, 'Audit Failed', `${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
2048
|
-
modal.appendChild(content);
|
|
2049
|
-
});
|
|
2050
|
-
}
|
|
2051
|
-
function renderA11yViolationGroup(container, impact, violations) {
|
|
2052
|
-
const impactColor = getImpactColor(impact);
|
|
2053
|
-
const section = document.createElement('div');
|
|
2054
|
-
section.style.marginBottom = '20px';
|
|
2055
|
-
// Section header
|
|
2056
|
-
const sectionTitle = document.createElement('h3');
|
|
2057
|
-
Object.assign(sectionTitle.style, {
|
|
2058
|
-
color: impactColor,
|
|
2059
|
-
fontSize: '0.8125rem',
|
|
2060
|
-
fontWeight: '600',
|
|
2061
|
-
marginBottom: '10px',
|
|
2062
|
-
borderBottom: `1px solid ${impactColor}40`,
|
|
2063
|
-
paddingBottom: '6px',
|
|
2064
|
-
textTransform: 'capitalize',
|
|
2065
|
-
});
|
|
2066
|
-
sectionTitle.textContent = `${impact} (${violations.length})`;
|
|
2067
|
-
section.appendChild(sectionTitle);
|
|
2068
|
-
for (const violation of violations) {
|
|
2069
|
-
const violationEl = document.createElement('div');
|
|
2070
|
-
Object.assign(violationEl.style, {
|
|
2071
|
-
marginBottom: '12px',
|
|
2072
|
-
padding: '10px 12px',
|
|
2073
|
-
backgroundColor: `${impactColor}08`,
|
|
2074
|
-
border: `1px solid ${impactColor}20`,
|
|
2075
|
-
borderRadius: '6px',
|
|
2076
|
-
});
|
|
2077
|
-
// Rule ID
|
|
2078
|
-
const ruleId = document.createElement('div');
|
|
2079
|
-
Object.assign(ruleId.style, {
|
|
2080
|
-
color: impactColor,
|
|
2081
|
-
fontSize: '0.6875rem',
|
|
2082
|
-
fontWeight: '600',
|
|
2083
|
-
marginBottom: '4px',
|
|
2084
|
-
});
|
|
2085
|
-
ruleId.textContent = violation.id;
|
|
2086
|
-
violationEl.appendChild(ruleId);
|
|
2087
|
-
// Help text
|
|
2088
|
-
const helpText = document.createElement('div');
|
|
2089
|
-
Object.assign(helpText.style, {
|
|
2090
|
-
color: CSS_COLORS.text,
|
|
2091
|
-
fontSize: '0.75rem',
|
|
2092
|
-
marginBottom: '4px',
|
|
2093
|
-
});
|
|
2094
|
-
helpText.textContent = violation.help;
|
|
2095
|
-
violationEl.appendChild(helpText);
|
|
2096
|
-
// Description
|
|
2097
|
-
const desc = document.createElement('div');
|
|
2098
|
-
Object.assign(desc.style, {
|
|
2099
|
-
color: CSS_COLORS.textSecondary,
|
|
2100
|
-
fontSize: '0.6875rem',
|
|
2101
|
-
marginBottom: '6px',
|
|
2102
|
-
});
|
|
2103
|
-
desc.textContent = violation.description;
|
|
2104
|
-
violationEl.appendChild(desc);
|
|
2105
|
-
// Node count
|
|
2106
|
-
const nodeCount = document.createElement('div');
|
|
2107
|
-
Object.assign(nodeCount.style, {
|
|
2108
|
-
color: CSS_COLORS.textMuted,
|
|
2109
|
-
fontSize: '0.625rem',
|
|
2110
|
-
marginBottom: '4px',
|
|
2111
|
-
});
|
|
2112
|
-
nodeCount.textContent = `${violation.nodes.length} element${violation.nodes.length === 1 ? '' : 's'} affected`;
|
|
2113
|
-
violationEl.appendChild(nodeCount);
|
|
2114
|
-
// Affected nodes (collapsed by default, show first 3)
|
|
2115
|
-
const nodesPreview = document.createElement('div');
|
|
2116
|
-
Object.assign(nodesPreview.style, {
|
|
2117
|
-
marginTop: '6px',
|
|
2118
|
-
});
|
|
2119
|
-
const visibleNodes = violation.nodes.slice(0, 3);
|
|
2120
|
-
for (const node of visibleNodes) {
|
|
2121
|
-
const nodeEl = document.createElement('div');
|
|
2122
|
-
Object.assign(nodeEl.style, {
|
|
2123
|
-
padding: '3px 6px',
|
|
2124
|
-
marginBottom: '2px',
|
|
2125
|
-
backgroundColor: 'rgba(0,0,0,0.2)',
|
|
2126
|
-
borderRadius: '3px',
|
|
2127
|
-
fontSize: '0.625rem',
|
|
2128
|
-
color: CSS_COLORS.textSecondary,
|
|
2129
|
-
fontFamily: 'monospace',
|
|
2130
|
-
whiteSpace: 'nowrap',
|
|
2131
|
-
overflow: 'hidden',
|
|
2132
|
-
textOverflow: 'ellipsis',
|
|
2133
|
-
});
|
|
2134
|
-
nodeEl.textContent = node.html.length > 100 ? `${node.html.slice(0, 100)}...` : node.html;
|
|
2135
|
-
nodeEl.title = node.html;
|
|
2136
|
-
nodesPreview.appendChild(nodeEl);
|
|
2137
|
-
}
|
|
2138
|
-
if (violation.nodes.length > 3) {
|
|
2139
|
-
const moreBtn = document.createElement('button');
|
|
2140
|
-
Object.assign(moreBtn.style, {
|
|
2141
|
-
background: 'none',
|
|
2142
|
-
border: 'none',
|
|
2143
|
-
color: impactColor,
|
|
2144
|
-
fontSize: '0.625rem',
|
|
2145
|
-
cursor: 'pointer',
|
|
2146
|
-
padding: '2px 0',
|
|
2147
|
-
fontFamily: FONT_MONO,
|
|
2148
|
-
});
|
|
2149
|
-
moreBtn.textContent = `+ ${violation.nodes.length - 3} more`;
|
|
2150
|
-
moreBtn.onclick = () => {
|
|
2151
|
-
// Show remaining nodes
|
|
2152
|
-
moreBtn.remove();
|
|
2153
|
-
for (const node of violation.nodes.slice(3)) {
|
|
2154
|
-
const nodeEl = document.createElement('div');
|
|
2155
|
-
Object.assign(nodeEl.style, {
|
|
2156
|
-
padding: '3px 6px',
|
|
2157
|
-
marginBottom: '2px',
|
|
2158
|
-
backgroundColor: 'rgba(0,0,0,0.2)',
|
|
2159
|
-
borderRadius: '3px',
|
|
2160
|
-
fontSize: '0.625rem',
|
|
2161
|
-
color: CSS_COLORS.textSecondary,
|
|
2162
|
-
fontFamily: 'monospace',
|
|
2163
|
-
whiteSpace: 'nowrap',
|
|
2164
|
-
overflow: 'hidden',
|
|
2165
|
-
textOverflow: 'ellipsis',
|
|
2166
|
-
});
|
|
2167
|
-
nodeEl.textContent = node.html.length > 100 ? `${node.html.slice(0, 100)}...` : node.html;
|
|
2168
|
-
nodeEl.title = node.html;
|
|
2169
|
-
nodesPreview.appendChild(nodeEl);
|
|
2170
|
-
}
|
|
2171
|
-
};
|
|
2172
|
-
nodesPreview.appendChild(moreBtn);
|
|
2173
|
-
}
|
|
2174
|
-
violationEl.appendChild(nodesPreview);
|
|
2175
|
-
section.appendChild(violationEl);
|
|
2176
|
-
}
|
|
2177
|
-
container.appendChild(section);
|
|
2178
|
-
}
|
|
2179
|
-
// ============================================================================
|
|
2180
|
-
// Design Review Confirmation Modal
|
|
2181
|
-
// ============================================================================
|
|
2182
|
-
function renderDesignReviewConfirmModal(state) {
|
|
2183
|
-
const color = BUTTON_COLORS.review;
|
|
2184
|
-
const closeModal = () => closeDesignReviewConfirm(state);
|
|
2185
|
-
const overlay = createModalOverlay(closeModal);
|
|
2186
|
-
const modal = createModalBox(color);
|
|
2187
|
-
modal.style.maxWidth = '450px';
|
|
2188
|
-
// Minimal header (title + close only, no Copy MD / Save)
|
|
2189
|
-
modal.appendChild(createModalHeader({ color, title: 'AI Design Review', onClose: closeModal }));
|
|
2190
|
-
// Content
|
|
2191
|
-
const content = createModalContent();
|
|
2192
|
-
Object.assign(content.style, {
|
|
2193
|
-
color: CSS_COLORS.text,
|
|
2194
|
-
fontSize: '0.8125rem',
|
|
2195
|
-
lineHeight: '1.6',
|
|
2196
|
-
});
|
|
2197
|
-
if (state.apiKeyStatus === null) {
|
|
2198
|
-
content.appendChild(createEmptyMessage('Checking API key configuration...'));
|
|
2199
|
-
}
|
|
2200
|
-
else if (!state.apiKeyStatus.configured) {
|
|
2201
|
-
content.appendChild(renderApiKeyNotConfiguredContent());
|
|
2202
|
-
}
|
|
2203
|
-
else {
|
|
2204
|
-
content.appendChild(renderApiKeyConfiguredContent(state));
|
|
2205
|
-
}
|
|
2206
|
-
modal.appendChild(content);
|
|
2207
|
-
// Footer with action button
|
|
2208
|
-
if (state.apiKeyStatus?.configured) {
|
|
2209
|
-
const footer = document.createElement('div');
|
|
2210
|
-
Object.assign(footer.style, {
|
|
2211
|
-
display: 'flex',
|
|
2212
|
-
justifyContent: 'flex-end',
|
|
2213
|
-
gap: '10px',
|
|
2214
|
-
padding: '14px 18px',
|
|
2215
|
-
borderTop: `1px solid ${CSS_COLORS.border}`,
|
|
2216
|
-
});
|
|
2217
|
-
const proceedBtn = createStyledButton({ color, text: 'Run Review', padding: '8px 16px' });
|
|
2218
|
-
proceedBtn.style.backgroundColor = `${color}20`;
|
|
2219
|
-
proceedBtn.onclick = () => proceedWithDesignReview(state);
|
|
2220
|
-
footer.appendChild(proceedBtn);
|
|
2221
|
-
modal.appendChild(footer);
|
|
2222
|
-
}
|
|
2223
|
-
overlay.appendChild(modal);
|
|
2224
|
-
state.overlayElement = overlay;
|
|
2225
|
-
document.body.appendChild(overlay);
|
|
2226
|
-
}
|
|
2227
|
-
function renderApiKeyNotConfiguredContent() {
|
|
2228
|
-
const wrapper = document.createElement('div');
|
|
2229
|
-
wrapper.appendChild(createInfoBox(CSS_COLORS.error, 'API Key Not Configured', 'The ANTHROPIC_API_KEY environment variable is not set.'));
|
|
2230
|
-
// Instructions
|
|
2231
|
-
const instructions = document.createElement('div');
|
|
2232
|
-
Object.assign(instructions.style, { marginBottom: '12px' });
|
|
2233
|
-
const instructTitle = document.createElement('div');
|
|
2234
|
-
Object.assign(instructTitle.style, {
|
|
2235
|
-
color: CSS_COLORS.textSecondary,
|
|
2236
|
-
fontWeight: '600',
|
|
2237
|
-
marginBottom: '8px',
|
|
2238
|
-
});
|
|
2239
|
-
instructTitle.textContent = 'To configure:';
|
|
2240
|
-
instructions.appendChild(instructTitle);
|
|
2241
|
-
const steps = [
|
|
2242
|
-
{ text: '1. Get an API key from console.anthropic.com', highlight: false },
|
|
2243
|
-
{ text: '2. Add to your .env file:', highlight: false },
|
|
2244
|
-
{ text: ' ANTHROPIC_API_KEY=sk-ant-...', highlight: true },
|
|
2245
|
-
{ text: '3. Restart your dev server', highlight: false },
|
|
2246
|
-
];
|
|
2247
|
-
steps.forEach(({ text, highlight }) => {
|
|
2248
|
-
const stepDiv = document.createElement('div');
|
|
2249
|
-
Object.assign(stepDiv.style, {
|
|
2250
|
-
color: highlight ? CSS_COLORS.primary : CSS_COLORS.textMuted,
|
|
2251
|
-
fontSize: '0.75rem',
|
|
2252
|
-
marginBottom: '4px',
|
|
2253
|
-
fontFamily: FONT_MONO,
|
|
2254
|
-
});
|
|
2255
|
-
stepDiv.textContent = text;
|
|
2256
|
-
instructions.appendChild(stepDiv);
|
|
2257
|
-
});
|
|
2258
|
-
wrapper.appendChild(instructions);
|
|
2259
|
-
return wrapper;
|
|
2260
|
-
}
|
|
2261
|
-
function renderApiKeyConfiguredContent(state) {
|
|
2262
|
-
const wrapper = document.createElement('div');
|
|
2263
|
-
Object.assign(wrapper.style, { marginBottom: '16px' });
|
|
2264
|
-
const desc = document.createElement('p');
|
|
2265
|
-
Object.assign(desc.style, { color: CSS_COLORS.textSecondary, marginBottom: '12px' });
|
|
2266
|
-
desc.textContent = 'This will capture a screenshot and send it to Claude for design analysis.';
|
|
2267
|
-
wrapper.appendChild(desc);
|
|
2268
|
-
// Cost estimate
|
|
2269
|
-
const estimate = calculateCostEstimate(state);
|
|
2270
|
-
if (estimate) {
|
|
2271
|
-
const costBox = createInfoBox(CSS_COLORS.primary, 'Estimated Cost', []);
|
|
2272
|
-
// Remove default margin and adjust padding
|
|
2273
|
-
costBox.style.marginBottom = '0';
|
|
2274
|
-
costBox.style.padding = '12px';
|
|
2275
|
-
const costDetails = document.createElement('div');
|
|
2276
|
-
Object.assign(costDetails.style, {
|
|
2277
|
-
display: 'flex',
|
|
2278
|
-
justifyContent: 'space-between',
|
|
2279
|
-
color: CSS_COLORS.textSecondary,
|
|
2280
|
-
fontSize: '0.75rem',
|
|
2281
|
-
});
|
|
2282
|
-
const tokensSpan = document.createElement('span');
|
|
2283
|
-
tokensSpan.textContent = `~${estimate.tokens.toLocaleString()} tokens`;
|
|
2284
|
-
costDetails.appendChild(tokensSpan);
|
|
2285
|
-
const priceSpan = document.createElement('span');
|
|
2286
|
-
Object.assign(priceSpan.style, { color: CSS_COLORS.warning, fontWeight: '600' });
|
|
2287
|
-
priceSpan.textContent = estimate.cost;
|
|
2288
|
-
costDetails.appendChild(priceSpan);
|
|
2289
|
-
costBox.appendChild(costDetails);
|
|
2290
|
-
wrapper.appendChild(costBox);
|
|
2291
|
-
}
|
|
2292
|
-
// Model info
|
|
2293
|
-
if (state.apiKeyStatus?.model) {
|
|
2294
|
-
const modelDiv = document.createElement('div');
|
|
2295
|
-
Object.assign(modelDiv.style, {
|
|
2296
|
-
color: CSS_COLORS.textMuted,
|
|
2297
|
-
fontSize: '0.6875rem',
|
|
2298
|
-
marginTop: '12px',
|
|
2299
|
-
});
|
|
2300
|
-
modelDiv.textContent = `Model: ${state.apiKeyStatus.model}`;
|
|
2301
|
-
wrapper.appendChild(modelDiv);
|
|
2302
|
-
}
|
|
2303
|
-
return wrapper;
|
|
2304
|
-
}
|
|
2305
|
-
// ============================================================================
|
|
2306
|
-
// Settings Popover
|
|
2307
|
-
// ============================================================================
|
|
2308
|
-
function renderSettingsPopover(state) {
|
|
2309
|
-
const { position, accentColor } = state.options;
|
|
2310
|
-
// Transparent overlay for click-outside-to-close (consistent with other modals)
|
|
2311
|
-
const overlay = document.createElement('div');
|
|
2312
|
-
overlay.setAttribute('data-devbar', 'true');
|
|
2313
|
-
overlay.setAttribute('data-devbar-overlay', 'true');
|
|
2314
|
-
Object.assign(overlay.style, {
|
|
2315
|
-
position: 'fixed',
|
|
2316
|
-
top: '0',
|
|
2317
|
-
left: '0',
|
|
2318
|
-
right: '0',
|
|
2319
|
-
bottom: '0',
|
|
2320
|
-
zIndex: '10003',
|
|
2321
|
-
});
|
|
2322
|
-
overlay.onclick = (e) => {
|
|
2323
|
-
if (e.target === overlay) {
|
|
2324
|
-
state.showSettingsPopover = false;
|
|
2325
|
-
state.render();
|
|
2326
|
-
}
|
|
2327
|
-
};
|
|
2328
|
-
const popover = document.createElement('div');
|
|
2329
|
-
popover.setAttribute('data-devbar', 'true');
|
|
2330
|
-
// Position: centered over the devbar on desktop, centered on screen on mobile
|
|
2331
|
-
const isTop = position.startsWith('top');
|
|
2332
|
-
const popoverWidth = 480;
|
|
2333
|
-
const edgePad = 16;
|
|
2334
|
-
let leftPx;
|
|
2335
|
-
if (state.container && window.innerWidth > 640) {
|
|
2336
|
-
const barRect = state.container.getBoundingClientRect();
|
|
2337
|
-
const barCenter = barRect.left + barRect.width / 2;
|
|
2338
|
-
leftPx = Math.max(edgePad, Math.min(barCenter - popoverWidth / 2, window.innerWidth - popoverWidth - edgePad));
|
|
2339
|
-
}
|
|
2340
|
-
else {
|
|
2341
|
-
leftPx = Math.max(edgePad, (window.innerWidth - popoverWidth) / 2);
|
|
2342
|
-
}
|
|
2343
|
-
Object.assign(popover.style, {
|
|
2344
|
-
position: 'fixed',
|
|
2345
|
-
[isTop ? 'top' : 'bottom']: '70px',
|
|
2346
|
-
left: `${leftPx}px`,
|
|
2347
|
-
zIndex: '10003',
|
|
2348
|
-
backgroundColor: 'var(--devbar-color-bg-elevated)',
|
|
2349
|
-
border: `1px solid ${accentColor}`,
|
|
2350
|
-
borderRadius: '8px',
|
|
2351
|
-
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${accentColor}33`,
|
|
2352
|
-
backdropFilter: 'blur(8px)',
|
|
2353
|
-
WebkitBackdropFilter: 'blur(8px)',
|
|
2354
|
-
width: `${popoverWidth}px`,
|
|
2355
|
-
maxWidth: 'calc(100vw - 32px)',
|
|
2356
|
-
maxHeight: 'calc(100vh - 100px)',
|
|
2357
|
-
overflowY: 'auto',
|
|
2358
|
-
fontFamily: FONT_MONO,
|
|
2359
|
-
});
|
|
2360
|
-
popover.appendChild(createSettingsHeader(state));
|
|
2361
|
-
// Two-column grid for settings sections (collapses to 1 column on mobile via CSS)
|
|
2362
|
-
const grid = document.createElement('div');
|
|
2363
|
-
grid.className = 'devbar-settings-grid';
|
|
2364
|
-
Object.assign(grid.style, {
|
|
2365
|
-
display: 'grid',
|
|
2366
|
-
gridTemplateColumns: '1fr 1fr',
|
|
2367
|
-
});
|
|
2368
|
-
// Left column: Theme + Display
|
|
2369
|
-
const color = CSS_COLORS.textSecondary;
|
|
2370
|
-
const leftCol = document.createElement('div');
|
|
2371
|
-
Object.assign(leftCol.style, { borderRight: `1px solid ${color}20` });
|
|
2372
|
-
leftCol.appendChild(createThemeSection(state));
|
|
2373
|
-
leftCol.appendChild(createDisplaySection(state));
|
|
2374
|
-
grid.appendChild(leftCol);
|
|
2375
|
-
// Right column: Features + Metrics
|
|
2376
|
-
const rightCol = document.createElement('div');
|
|
2377
|
-
rightCol.appendChild(createFeaturesSection(state));
|
|
2378
|
-
rightCol.appendChild(createMetricsSection(state));
|
|
2379
|
-
grid.appendChild(rightCol);
|
|
2380
|
-
popover.appendChild(grid);
|
|
2381
|
-
popover.appendChild(createResetSection(state));
|
|
2382
|
-
overlay.appendChild(popover);
|
|
2383
|
-
state.overlayElement = overlay;
|
|
2384
|
-
document.body.appendChild(overlay);
|
|
2385
|
-
}
|
|
2386
|
-
// ============================================================================
|
|
2387
|
-
// Settings Popover Section Builders
|
|
2388
|
-
// ============================================================================
|
|
2389
|
-
function createSettingsHeader(state) {
|
|
2390
|
-
const { accentColor } = state.options;
|
|
2391
|
-
const header = document.createElement('div');
|
|
2392
|
-
Object.assign(header.style, {
|
|
2393
|
-
display: 'flex',
|
|
2394
|
-
alignItems: 'center',
|
|
2395
|
-
justifyContent: 'space-between',
|
|
2396
|
-
padding: '10px 14px',
|
|
2397
|
-
borderBottom: `1px solid ${accentColor}30`,
|
|
2398
|
-
position: 'sticky',
|
|
2399
|
-
top: '0',
|
|
2400
|
-
backgroundColor: 'var(--devbar-color-bg-elevated)',
|
|
2401
|
-
zIndex: '1',
|
|
2402
|
-
});
|
|
2403
|
-
const title = document.createElement('span');
|
|
2404
|
-
Object.assign(title.style, { color: accentColor, fontSize: '0.75rem', fontWeight: '600' });
|
|
2405
|
-
title.textContent = 'Settings';
|
|
2406
|
-
header.appendChild(title);
|
|
2407
|
-
header.appendChild(createCloseButton(() => {
|
|
2408
|
-
state.showSettingsPopover = false;
|
|
2409
|
-
state.render();
|
|
2410
|
-
}));
|
|
2411
|
-
return header;
|
|
2412
|
-
}
|
|
2413
|
-
function createThemeSection(state) {
|
|
2414
|
-
const { accentColor } = state.options;
|
|
2415
|
-
const themeSection = createSettingsSection('Theme');
|
|
2416
|
-
const themeOptions = document.createElement('div');
|
|
2417
|
-
Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
|
|
2418
|
-
const themeModes = ['system', 'dark', 'light'];
|
|
2419
|
-
themeModes.forEach((mode) => {
|
|
2420
|
-
const btn = createSettingsRadioButton({
|
|
2421
|
-
label: mode,
|
|
2422
|
-
isActive: state.themeMode === mode,
|
|
2423
|
-
accentColor,
|
|
2424
|
-
onClick: () => setThemeMode(state, mode),
|
|
2425
|
-
});
|
|
2426
|
-
btn.style.textTransform = 'capitalize';
|
|
2427
|
-
themeOptions.appendChild(btn);
|
|
2428
|
-
});
|
|
2429
|
-
themeSection.appendChild(themeOptions);
|
|
2430
|
-
return themeSection;
|
|
2431
|
-
}
|
|
2432
|
-
function createDisplaySection(state) {
|
|
2433
|
-
const { accentColor } = state.options;
|
|
2434
|
-
const color = CSS_COLORS.textSecondary;
|
|
2435
|
-
const displaySection = createSettingsSection('Display');
|
|
2436
|
-
// Position mini-map selector
|
|
2437
|
-
const positionRow = document.createElement('div');
|
|
2438
|
-
Object.assign(positionRow.style, { marginBottom: '10px' });
|
|
2439
|
-
const posLabel = document.createElement('div');
|
|
2440
|
-
Object.assign(posLabel.style, {
|
|
2441
|
-
color: CSS_COLORS.text,
|
|
2442
|
-
fontSize: '0.6875rem',
|
|
2443
|
-
marginBottom: '6px',
|
|
2444
|
-
});
|
|
2445
|
-
posLabel.textContent = 'Position';
|
|
2446
|
-
positionRow.appendChild(posLabel);
|
|
2447
|
-
// Mini-map container (represents screen with ~16:10 aspect ratio)
|
|
2448
|
-
const miniMap = document.createElement('div');
|
|
2449
|
-
Object.assign(miniMap.style, {
|
|
2450
|
-
position: 'relative',
|
|
2451
|
-
width: '100%',
|
|
2452
|
-
height: '70px',
|
|
2453
|
-
backgroundColor: 'var(--devbar-color-bg-input)',
|
|
2454
|
-
border: `1px solid ${color}30`,
|
|
2455
|
-
borderRadius: '4px',
|
|
2456
|
-
});
|
|
2457
|
-
// Position indicator styles - rectangular bars representing DevBar
|
|
2458
|
-
const positionConfigs = [
|
|
2459
|
-
{ value: 'top-left', style: { top: '6px', left: '6px' }, title: 'Top Left' },
|
|
2460
|
-
{ value: 'top-right', style: { top: '6px', right: '6px' }, title: 'Top Right' },
|
|
2461
|
-
{ value: 'bottom-left', style: { bottom: '6px', left: '6px' }, title: 'Bottom Left' },
|
|
2462
|
-
{ value: 'bottom-right', style: { bottom: '6px', right: '6px' }, title: 'Bottom Right' },
|
|
2463
|
-
{
|
|
2464
|
-
value: 'bottom-center',
|
|
2465
|
-
style: { bottom: '6px', left: '50%', transform: 'translateX(-50%)' },
|
|
2466
|
-
title: 'Bottom Center',
|
|
2467
|
-
},
|
|
2468
|
-
];
|
|
2469
|
-
positionConfigs.forEach(({ value, style, title: posTitle }) => {
|
|
2470
|
-
const indicator = document.createElement('button');
|
|
2471
|
-
indicator.setAttribute('data-position', value);
|
|
2472
|
-
const isActive = state.options.position === value;
|
|
2473
|
-
Object.assign(indicator.style, {
|
|
2474
|
-
position: 'absolute',
|
|
2475
|
-
width: '24px',
|
|
2476
|
-
height: '6px',
|
|
2477
|
-
backgroundColor: isActive ? accentColor : CSS_COLORS.textMuted,
|
|
2478
|
-
border: `1px solid ${isActive ? accentColor : CSS_COLORS.textMuted}`,
|
|
2479
|
-
borderRadius: '2px',
|
|
2480
|
-
cursor: 'pointer',
|
|
2481
|
-
padding: '0',
|
|
2482
|
-
transition: 'all 150ms',
|
|
2483
|
-
boxShadow: isActive ? `0 0 8px ${accentColor}60` : 'none',
|
|
2484
|
-
opacity: isActive ? '1' : '0.5',
|
|
2485
|
-
...style,
|
|
2486
|
-
});
|
|
2487
|
-
indicator.title = posTitle;
|
|
2488
|
-
indicator.onclick = () => {
|
|
2489
|
-
state.options.position = value;
|
|
2490
|
-
state.settingsManager.saveSettings({ position: value });
|
|
2491
|
-
state.render();
|
|
2492
|
-
};
|
|
2493
|
-
// Hover effect
|
|
2494
|
-
indicator.onmouseenter = () => {
|
|
2495
|
-
if (!isActive) {
|
|
2496
|
-
indicator.style.backgroundColor = accentColor;
|
|
2497
|
-
indicator.style.borderColor = accentColor;
|
|
2498
|
-
indicator.style.boxShadow = `0 0 6px ${accentColor}40`;
|
|
2499
|
-
indicator.style.opacity = '1';
|
|
2500
|
-
}
|
|
2501
|
-
};
|
|
2502
|
-
indicator.onmouseleave = () => {
|
|
2503
|
-
if (!isActive) {
|
|
2504
|
-
indicator.style.backgroundColor = CSS_COLORS.textMuted;
|
|
2505
|
-
indicator.style.borderColor = CSS_COLORS.textMuted;
|
|
2506
|
-
indicator.style.boxShadow = 'none';
|
|
2507
|
-
indicator.style.opacity = '0.5';
|
|
2508
|
-
}
|
|
2509
|
-
};
|
|
2510
|
-
miniMap.appendChild(indicator);
|
|
2511
|
-
});
|
|
2512
|
-
positionRow.appendChild(miniMap);
|
|
2513
|
-
displaySection.appendChild(positionRow);
|
|
2514
|
-
// Compact mode toggle
|
|
2515
|
-
displaySection.appendChild(createToggleRow('Compact Mode', state.compactMode, accentColor, () => {
|
|
2516
|
-
state.toggleCompactMode();
|
|
2517
|
-
}));
|
|
2518
|
-
// Keyboard shortcut hint
|
|
2519
|
-
const shortcutHint = document.createElement('div');
|
|
2520
|
-
Object.assign(shortcutHint.style, {
|
|
2521
|
-
color: CSS_COLORS.textMuted,
|
|
2522
|
-
fontSize: '0.5625rem',
|
|
2523
|
-
marginTop: '2px',
|
|
2524
|
-
marginBottom: '8px',
|
|
2525
|
-
});
|
|
2526
|
-
shortcutHint.textContent = 'Keyboard: Cmd or Ctrl+Shift+M';
|
|
2527
|
-
displaySection.appendChild(shortcutHint);
|
|
2528
|
-
// Accent color
|
|
2529
|
-
const accentRow = document.createElement('div');
|
|
2530
|
-
Object.assign(accentRow.style, { marginBottom: '6px' });
|
|
2531
|
-
const accentLabel = document.createElement('div');
|
|
2532
|
-
Object.assign(accentLabel.style, {
|
|
2533
|
-
color: CSS_COLORS.text,
|
|
2534
|
-
fontSize: '0.6875rem',
|
|
2535
|
-
marginBottom: '6px',
|
|
2536
|
-
});
|
|
2537
|
-
accentLabel.textContent = 'Accent Color';
|
|
2538
|
-
accentRow.appendChild(accentLabel);
|
|
2539
|
-
const colorSwatches = document.createElement('div');
|
|
2540
|
-
Object.assign(colorSwatches.style, {
|
|
2541
|
-
display: 'flex',
|
|
2542
|
-
gap: '6px',
|
|
2543
|
-
flexWrap: 'wrap',
|
|
2544
|
-
});
|
|
2545
|
-
ACCENT_COLOR_PRESETS.forEach(({ name, value }) => {
|
|
2546
|
-
const swatch = document.createElement('button');
|
|
2547
|
-
const isActive = state.options.accentColor === value;
|
|
2548
|
-
Object.assign(swatch.style, {
|
|
2549
|
-
width: '24px',
|
|
2550
|
-
height: '24px',
|
|
2551
|
-
borderRadius: '50%',
|
|
2552
|
-
backgroundColor: value,
|
|
2553
|
-
border: isActive ? '2px solid #fff' : '2px solid transparent',
|
|
2554
|
-
cursor: 'pointer',
|
|
2555
|
-
transition: 'all 150ms',
|
|
2556
|
-
boxShadow: isActive ? `0 0 8px ${value}` : 'none',
|
|
2557
|
-
});
|
|
2558
|
-
swatch.title = name;
|
|
2559
|
-
swatch.onclick = () => {
|
|
2560
|
-
state.options.accentColor = value;
|
|
2561
|
-
state.settingsManager.saveSettings({ accentColor: value });
|
|
2562
|
-
state.render();
|
|
2563
|
-
};
|
|
2564
|
-
colorSwatches.appendChild(swatch);
|
|
2565
|
-
});
|
|
2566
|
-
accentRow.appendChild(colorSwatches);
|
|
2567
|
-
displaySection.appendChild(accentRow);
|
|
2568
|
-
// Screenshot quality slider
|
|
2569
|
-
const qualityRow = document.createElement('div');
|
|
2570
|
-
Object.assign(qualityRow.style, { marginTop: '8px' });
|
|
2571
|
-
const qualityHeader = document.createElement('div');
|
|
2572
|
-
Object.assign(qualityHeader.style, {
|
|
2573
|
-
display: 'flex',
|
|
2574
|
-
alignItems: 'center',
|
|
2575
|
-
justifyContent: 'space-between',
|
|
2576
|
-
marginBottom: '6px',
|
|
2577
|
-
});
|
|
2578
|
-
const qualityLabel = document.createElement('span');
|
|
2579
|
-
Object.assign(qualityLabel.style, {
|
|
2580
|
-
color: CSS_COLORS.text,
|
|
2581
|
-
fontSize: '0.6875rem',
|
|
2582
|
-
});
|
|
2583
|
-
qualityLabel.textContent = 'Screenshot Quality';
|
|
2584
|
-
qualityHeader.appendChild(qualityLabel);
|
|
2585
|
-
const qualityValue = document.createElement('span');
|
|
2586
|
-
Object.assign(qualityValue.style, {
|
|
2587
|
-
color: accentColor,
|
|
2588
|
-
fontSize: '0.6875rem',
|
|
2589
|
-
fontFamily: 'monospace',
|
|
2590
|
-
minWidth: '28px',
|
|
2591
|
-
textAlign: 'right',
|
|
2592
|
-
});
|
|
2593
|
-
const quality = state.options.screenshotQuality;
|
|
2594
|
-
qualityValue.textContent = quality.toFixed(2);
|
|
2595
|
-
qualityHeader.appendChild(qualityValue);
|
|
2596
|
-
qualityRow.appendChild(qualityHeader);
|
|
2597
|
-
// Wrapper: positions the visible track line behind the transparent range input
|
|
2598
|
-
const sliderWrap = document.createElement('div');
|
|
2599
|
-
Object.assign(sliderWrap.style, { position: 'relative', height: '20px' });
|
|
2600
|
-
// Visible track rail (a real div, always renders)
|
|
2601
|
-
const track = document.createElement('div');
|
|
2602
|
-
Object.assign(track.style, {
|
|
2603
|
-
position: 'absolute',
|
|
2604
|
-
top: '50%',
|
|
2605
|
-
left: '0',
|
|
2606
|
-
right: '0',
|
|
2607
|
-
height: '2px',
|
|
2608
|
-
transform: 'translateY(-50%)',
|
|
2609
|
-
borderRadius: '1px',
|
|
2610
|
-
background: `${color}40`,
|
|
2611
|
-
pointerEvents: 'none',
|
|
2612
|
-
});
|
|
2613
|
-
// Filled portion of the track
|
|
2614
|
-
const trackFill = document.createElement('div');
|
|
2615
|
-
Object.assign(trackFill.style, {
|
|
2616
|
-
height: '100%',
|
|
2617
|
-
width: `${quality * 100}%`,
|
|
2618
|
-
borderRadius: '1px',
|
|
2619
|
-
background: accentColor,
|
|
2620
|
-
});
|
|
2621
|
-
track.appendChild(trackFill);
|
|
2622
|
-
sliderWrap.appendChild(track);
|
|
2623
|
-
const qualitySlider = document.createElement('input');
|
|
2624
|
-
qualitySlider.type = 'range';
|
|
2625
|
-
qualitySlider.min = '0';
|
|
2626
|
-
qualitySlider.max = '1';
|
|
2627
|
-
qualitySlider.step = '0.01';
|
|
2628
|
-
qualitySlider.value = String(quality);
|
|
2629
|
-
Object.assign(qualitySlider.style, {
|
|
2630
|
-
position: 'absolute',
|
|
2631
|
-
top: '0',
|
|
2632
|
-
left: '0',
|
|
2633
|
-
width: '100%',
|
|
2634
|
-
height: '100%',
|
|
2635
|
-
appearance: 'none',
|
|
2636
|
-
WebkitAppearance: 'none',
|
|
2637
|
-
background: 'transparent',
|
|
2638
|
-
outline: 'none',
|
|
2639
|
-
cursor: 'pointer',
|
|
2640
|
-
margin: '0',
|
|
2641
|
-
});
|
|
2642
|
-
// Style the thumb via a scoped style element
|
|
2643
|
-
const sliderId = `devbar-quality-${Date.now()}`;
|
|
2644
|
-
qualitySlider.id = sliderId;
|
|
2645
|
-
const sliderStyle = document.createElement('style');
|
|
2646
|
-
sliderStyle.textContent = [
|
|
2647
|
-
`#${sliderId}::-webkit-slider-thumb {`,
|
|
2648
|
-
` -webkit-appearance: none;`,
|
|
2649
|
-
` width: 12px; height: 12px;`,
|
|
2650
|
-
` border-radius: 50%;`,
|
|
2651
|
-
` background: ${accentColor};`,
|
|
2652
|
-
` border: 2px solid ${CSS_COLORS.bg};`,
|
|
2653
|
-
` box-shadow: 0 0 4px ${accentColor}80;`,
|
|
2654
|
-
` cursor: grab;`,
|
|
2655
|
-
`}`,
|
|
2656
|
-
`#${sliderId}::-webkit-slider-thumb:active { cursor: grabbing; }`,
|
|
2657
|
-
`#${sliderId}::-moz-range-thumb {`,
|
|
2658
|
-
` width: 12px; height: 12px;`,
|
|
2659
|
-
` border-radius: 50%;`,
|
|
2660
|
-
` background: ${accentColor};`,
|
|
2661
|
-
` border: 2px solid ${CSS_COLORS.bg};`,
|
|
2662
|
-
` box-shadow: 0 0 4px ${accentColor}80;`,
|
|
2663
|
-
` cursor: grab;`,
|
|
2664
|
-
`}`,
|
|
2665
|
-
`#${sliderId}::-webkit-slider-runnable-track { background: transparent; }`,
|
|
2666
|
-
`#${sliderId}::-moz-range-track { background: transparent; }`,
|
|
2667
|
-
].join('\n');
|
|
2668
|
-
sliderWrap.appendChild(sliderStyle);
|
|
2669
|
-
qualitySlider.oninput = () => {
|
|
2670
|
-
const val = parseFloat(qualitySlider.value);
|
|
2671
|
-
qualityValue.textContent = val.toFixed(2);
|
|
2672
|
-
trackFill.style.width = `${val * 100}%`;
|
|
2673
|
-
state.options.screenshotQuality = val;
|
|
2674
|
-
};
|
|
2675
|
-
qualitySlider.onchange = () => {
|
|
2676
|
-
state.settingsManager.saveSettings({ screenshotQuality: state.options.screenshotQuality });
|
|
2677
|
-
};
|
|
2678
|
-
sliderWrap.appendChild(qualitySlider);
|
|
2679
|
-
qualityRow.appendChild(sliderWrap);
|
|
2680
|
-
displaySection.appendChild(qualityRow);
|
|
2681
|
-
return displaySection;
|
|
2682
|
-
}
|
|
2683
|
-
function createFeaturesSection(state) {
|
|
2684
|
-
const { accentColor } = state.options;
|
|
2685
|
-
const featuresSection = createSettingsSection('Features');
|
|
2686
|
-
featuresSection.appendChild(createToggleRow('Screenshot Button', state.options.showScreenshot, accentColor, () => {
|
|
2687
|
-
state.options.showScreenshot = !state.options.showScreenshot;
|
|
2688
|
-
state.settingsManager.saveSettings({ showScreenshot: state.options.showScreenshot });
|
|
2689
|
-
state.render();
|
|
2690
|
-
}));
|
|
2691
|
-
featuresSection.appendChild(createToggleRow('Console Badges', state.options.showConsoleBadges, accentColor, () => {
|
|
2692
|
-
state.options.showConsoleBadges = !state.options.showConsoleBadges;
|
|
2693
|
-
state.settingsManager.saveSettings({ showConsoleBadges: state.options.showConsoleBadges });
|
|
2694
|
-
state.render();
|
|
2695
|
-
}));
|
|
2696
|
-
featuresSection.appendChild(createToggleRow('Tooltips', state.options.showTooltips, accentColor, () => {
|
|
2697
|
-
state.options.showTooltips = !state.options.showTooltips;
|
|
2698
|
-
state.settingsManager.saveSettings({ showTooltips: state.options.showTooltips });
|
|
2699
|
-
state.render();
|
|
2700
|
-
}));
|
|
2701
|
-
// Save location selector
|
|
2702
|
-
const saveLocRow = document.createElement('div');
|
|
2703
|
-
Object.assign(saveLocRow.style, { marginBottom: '6px' });
|
|
2704
|
-
const saveLocLabel = document.createElement('div');
|
|
2705
|
-
Object.assign(saveLocLabel.style, {
|
|
2706
|
-
color: CSS_COLORS.text,
|
|
2707
|
-
fontSize: '0.6875rem',
|
|
2708
|
-
marginBottom: '6px',
|
|
2709
|
-
});
|
|
2710
|
-
saveLocLabel.textContent = 'Save Method';
|
|
2711
|
-
saveLocRow.appendChild(saveLocLabel);
|
|
2712
|
-
const saveLocOptions = document.createElement('div');
|
|
2713
|
-
Object.assign(saveLocOptions.style, { display: 'flex', gap: '6px' });
|
|
2714
|
-
const saveLocChoices = [
|
|
2715
|
-
{ value: 'auto', label: 'Auto' },
|
|
2716
|
-
{ value: 'download', label: 'Download' },
|
|
2717
|
-
{ value: 'local', label: 'Local' },
|
|
2718
|
-
];
|
|
2719
|
-
saveLocChoices.forEach(({ value, label }) => {
|
|
2720
|
-
const isLocalDisabled = value === 'local' && !state.sweetlinkConnected;
|
|
2721
|
-
const btn = createSettingsRadioButton({
|
|
2722
|
-
label,
|
|
2723
|
-
isActive: state.options.saveLocation === value,
|
|
2724
|
-
accentColor,
|
|
2725
|
-
disabled: isLocalDisabled,
|
|
2726
|
-
disabledTitle: 'Sweetlink not connected',
|
|
2727
|
-
onClick: () => {
|
|
2728
|
-
state.options.saveLocation = value;
|
|
2729
|
-
state.settingsManager.saveSettings({ saveLocation: value });
|
|
2730
|
-
state.render();
|
|
2731
|
-
},
|
|
2732
|
-
});
|
|
2733
|
-
saveLocOptions.appendChild(btn);
|
|
2734
|
-
});
|
|
2735
|
-
saveLocRow.appendChild(saveLocOptions);
|
|
2736
|
-
featuresSection.appendChild(saveLocRow);
|
|
2737
|
-
return featuresSection;
|
|
2738
|
-
}
|
|
2739
|
-
function createMetricsSection(state) {
|
|
2740
|
-
const { accentColor } = state.options;
|
|
2741
|
-
const metricsSection = createSettingsSection('Metrics');
|
|
2742
|
-
const metricsToggles = [
|
|
2743
|
-
{ key: 'breakpoint', label: 'Breakpoint' },
|
|
2744
|
-
{ key: 'fcp', label: 'FCP' },
|
|
2745
|
-
{ key: 'lcp', label: 'LCP' },
|
|
2746
|
-
{ key: 'cls', label: 'CLS' },
|
|
2747
|
-
{ key: 'inp', label: 'INP' },
|
|
2748
|
-
{ key: 'pageSize', label: 'Page Size' },
|
|
2749
|
-
];
|
|
2750
|
-
metricsToggles.forEach(({ key, label }) => {
|
|
2751
|
-
const currentValue = state.options.showMetrics[key] ?? true;
|
|
2752
|
-
metricsSection.appendChild(createToggleRow(label, currentValue, accentColor, () => {
|
|
2753
|
-
state.options.showMetrics[key] = !state.options.showMetrics[key];
|
|
2754
|
-
state.settingsManager.saveSettings({
|
|
2755
|
-
showMetrics: {
|
|
2756
|
-
breakpoint: state.options.showMetrics.breakpoint ?? true,
|
|
2757
|
-
fcp: state.options.showMetrics.fcp ?? true,
|
|
2758
|
-
lcp: state.options.showMetrics.lcp ?? true,
|
|
2759
|
-
cls: state.options.showMetrics.cls ?? true,
|
|
2760
|
-
inp: state.options.showMetrics.inp ?? true,
|
|
2761
|
-
pageSize: state.options.showMetrics.pageSize ?? true,
|
|
2762
|
-
},
|
|
2763
|
-
});
|
|
2764
|
-
state.render();
|
|
2765
|
-
}));
|
|
2766
|
-
});
|
|
2767
|
-
return metricsSection;
|
|
2768
|
-
}
|
|
2769
|
-
function createResetSection(state) {
|
|
2770
|
-
const color = CSS_COLORS.textSecondary;
|
|
2771
|
-
const resetSection = document.createElement('div');
|
|
2772
|
-
Object.assign(resetSection.style, {
|
|
2773
|
-
padding: '10px 14px',
|
|
2774
|
-
borderTop: `1px solid ${color}20`,
|
|
2775
|
-
});
|
|
2776
|
-
const resetBtn = createStyledButton({
|
|
2777
|
-
color: CSS_COLORS.textMuted,
|
|
2778
|
-
text: 'Reset to Defaults',
|
|
2779
|
-
padding: '6px 12px',
|
|
2780
|
-
fontSize: '0.625rem',
|
|
2781
|
-
});
|
|
2782
|
-
Object.assign(resetBtn.style, {
|
|
2783
|
-
width: '100%',
|
|
2784
|
-
justifyContent: 'center',
|
|
2785
|
-
border: `1px solid transparent`,
|
|
2786
|
-
});
|
|
2787
|
-
const resetColor = CSS_COLORS.textMuted;
|
|
2788
|
-
resetBtn.onmouseenter = () => {
|
|
2789
|
-
resetBtn.style.border = `1px solid ${resetColor}`;
|
|
2790
|
-
resetBtn.style.backgroundColor = `${resetColor}10`;
|
|
2791
|
-
};
|
|
2792
|
-
resetBtn.onmouseleave = () => {
|
|
2793
|
-
resetBtn.style.border = '1px solid transparent';
|
|
2794
|
-
resetBtn.style.backgroundColor = 'transparent';
|
|
2795
|
-
};
|
|
2796
|
-
resetBtn.onclick = () => {
|
|
2797
|
-
state.settingsManager.resetToDefaults();
|
|
2798
|
-
const defaults = DEFAULT_SETTINGS;
|
|
2799
|
-
state.applySettings(defaults);
|
|
2800
|
-
};
|
|
2801
|
-
resetSection.appendChild(resetBtn);
|
|
2802
|
-
return resetSection;
|
|
2803
|
-
}
|
|
2804
|
-
// ============================================================================
|
|
2805
|
-
// Settings UI Helpers
|
|
2806
|
-
// ============================================================================
|
|
2807
|
-
function createSettingsSection(title, hasBorder = true) {
|
|
2808
|
-
const color = CSS_COLORS.textSecondary;
|
|
2809
|
-
const section = document.createElement('div');
|
|
2810
|
-
Object.assign(section.style, {
|
|
2811
|
-
padding: '10px 14px',
|
|
2812
|
-
borderBottom: hasBorder ? `1px solid ${color}20` : 'none',
|
|
2813
|
-
});
|
|
2814
|
-
const sectionTitle = document.createElement('div');
|
|
2815
|
-
Object.assign(sectionTitle.style, {
|
|
2816
|
-
color,
|
|
2817
|
-
fontSize: '0.625rem',
|
|
2818
|
-
textTransform: 'uppercase',
|
|
2819
|
-
letterSpacing: '0.1em',
|
|
2820
|
-
marginBottom: '8px',
|
|
2821
|
-
});
|
|
2822
|
-
sectionTitle.textContent = title;
|
|
2823
|
-
section.appendChild(sectionTitle);
|
|
2824
|
-
return section;
|
|
2825
|
-
}
|
|
2826
|
-
function createSettingsRadioButton(options) {
|
|
2827
|
-
const { label, isActive, accentColor, disabled, disabledTitle, onClick } = options;
|
|
2828
|
-
const color = CSS_COLORS.textSecondary;
|
|
2829
|
-
const btn = document.createElement('button');
|
|
2830
|
-
Object.assign(btn.style, {
|
|
2831
|
-
padding: '4px 10px',
|
|
2832
|
-
backgroundColor: isActive ? `${accentColor}20` : 'transparent',
|
|
2833
|
-
border: `1px solid ${isActive ? accentColor : 'transparent'}`,
|
|
2834
|
-
borderRadius: '4px',
|
|
2835
|
-
color: isActive ? accentColor : color,
|
|
2836
|
-
fontFamily: FONT_MONO,
|
|
2837
|
-
fontSize: '0.625rem',
|
|
2838
|
-
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
2839
|
-
transition: 'all 150ms',
|
|
2840
|
-
opacity: disabled ? '0.5' : '1',
|
|
2841
|
-
});
|
|
2842
|
-
btn.textContent = label;
|
|
2843
|
-
if (disabled) {
|
|
2844
|
-
if (disabledTitle)
|
|
2845
|
-
btn.title = disabledTitle;
|
|
2846
|
-
}
|
|
2847
|
-
else if (!isActive) {
|
|
2848
|
-
btn.onmouseenter = () => {
|
|
2849
|
-
btn.style.borderColor = `${color}`;
|
|
2850
|
-
btn.style.backgroundColor = `${color}10`;
|
|
2851
|
-
};
|
|
2852
|
-
btn.onmouseleave = () => {
|
|
2853
|
-
btn.style.borderColor = 'transparent';
|
|
2854
|
-
btn.style.backgroundColor = 'transparent';
|
|
2855
|
-
};
|
|
2856
|
-
}
|
|
2857
|
-
btn.onclick = () => {
|
|
2858
|
-
if (!disabled)
|
|
2859
|
-
onClick();
|
|
2860
|
-
};
|
|
2861
|
-
return btn;
|
|
2862
|
-
}
|
|
2863
|
-
function createToggleRow(label, checked, accentColor, onChange) {
|
|
2864
|
-
const row = document.createElement('div');
|
|
2865
|
-
Object.assign(row.style, {
|
|
2866
|
-
display: 'flex',
|
|
2867
|
-
alignItems: 'center',
|
|
2868
|
-
justifyContent: 'space-between',
|
|
2869
|
-
marginBottom: '6px',
|
|
2870
|
-
});
|
|
2871
|
-
const labelEl = document.createElement('span');
|
|
2872
|
-
Object.assign(labelEl.style, { color: CSS_COLORS.text, fontSize: '0.6875rem' });
|
|
2873
|
-
labelEl.textContent = label;
|
|
2874
|
-
row.appendChild(labelEl);
|
|
2875
|
-
const toggle = document.createElement('button');
|
|
2876
|
-
Object.assign(toggle.style, {
|
|
2877
|
-
width: '32px',
|
|
2878
|
-
height: '18px',
|
|
2879
|
-
borderRadius: '9px',
|
|
2880
|
-
border: `1px solid ${checked ? accentColor : CSS_COLORS.border}`,
|
|
2881
|
-
backgroundColor: checked ? accentColor : CSS_COLORS.bgInput,
|
|
2882
|
-
position: 'relative',
|
|
2883
|
-
cursor: 'pointer',
|
|
2884
|
-
transition: 'all 150ms',
|
|
2885
|
-
flexShrink: '0',
|
|
2886
|
-
boxSizing: 'border-box',
|
|
2887
|
-
});
|
|
2888
|
-
const knob = document.createElement('span');
|
|
2889
|
-
Object.assign(knob.style, {
|
|
2890
|
-
position: 'absolute',
|
|
2891
|
-
top: '2px',
|
|
2892
|
-
left: checked ? '14px' : '2px',
|
|
2893
|
-
width: '12px',
|
|
2894
|
-
height: '12px',
|
|
2895
|
-
borderRadius: '50%',
|
|
2896
|
-
backgroundColor: checked ? '#fff' : CSS_COLORS.textMuted,
|
|
2897
|
-
boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
|
|
2898
|
-
transition: 'left 150ms, background-color 150ms',
|
|
2899
|
-
});
|
|
2900
|
-
toggle.appendChild(knob);
|
|
2901
|
-
toggle.onclick = onChange;
|
|
2902
|
-
row.appendChild(toggle);
|
|
2903
|
-
return row;
|
|
2904
|
-
}
|
|
4
|
+
* This file exists so that existing import paths
|
|
5
|
+
* import { render } from './rendering.js'
|
|
6
|
+
* continue to work without changes.
|
|
7
|
+
*
|
|
8
|
+
* The actual implementation is split across:
|
|
9
|
+
* rendering/collapsed.ts - collapsed bar rendering
|
|
10
|
+
* rendering/compact.ts - compact mode
|
|
11
|
+
* rendering/expanded.ts - expanded bar, info section, metrics, custom controls
|
|
12
|
+
* rendering/buttons.ts - button creators (screenshot, a11y, schema, etc.)
|
|
13
|
+
* rendering/console.ts - console popup
|
|
14
|
+
* rendering/modals.ts - outline, schema, a11y, design review modals
|
|
15
|
+
* rendering/settings.ts - settings popover
|
|
16
|
+
* rendering/common.ts - shared helpers (connection indicator, dot position)
|
|
17
|
+
*/
|
|
18
|
+
export { render } from './rendering/index.js';
|
|
2905
19
|
//# sourceMappingURL=rendering.js.map
|