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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +4 -0
  2. package/dist/GlobalDevBar.d.ts +111 -197
  3. package/dist/GlobalDevBar.d.ts.map +1 -0
  4. package/dist/GlobalDevBar.js +165 -2657
  5. package/dist/GlobalDevBar.js.map +1 -0
  6. package/dist/accessibility.d.ts +1 -0
  7. package/dist/accessibility.d.ts.map +1 -0
  8. package/dist/accessibility.js +1 -0
  9. package/dist/accessibility.js.map +1 -0
  10. package/dist/constants.d.ts +44 -12
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +104 -121
  13. package/dist/constants.js.map +1 -0
  14. package/dist/debug.d.ts +4 -3
  15. package/dist/debug.d.ts.map +1 -0
  16. package/dist/debug.js +6 -4
  17. package/dist/debug.js.map +1 -0
  18. package/dist/earlyConsoleCapture.d.ts +4 -31
  19. package/dist/earlyConsoleCapture.d.ts.map +1 -0
  20. package/dist/earlyConsoleCapture.js +4 -74
  21. package/dist/earlyConsoleCapture.js.map +1 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +6 -1
  25. package/dist/index.js.map +1 -0
  26. package/dist/lazy/index.d.ts +1 -0
  27. package/dist/lazy/index.d.ts.map +1 -0
  28. package/dist/lazy/index.js +1 -0
  29. package/dist/lazy/index.js.map +1 -0
  30. package/dist/lazy/lazyHtml2Canvas.d.ts +5 -0
  31. package/dist/lazy/lazyHtml2Canvas.d.ts.map +1 -0
  32. package/dist/lazy/lazyHtml2Canvas.js +1 -0
  33. package/dist/lazy/lazyHtml2Canvas.js.map +1 -0
  34. package/dist/modules/index.d.ts +15 -0
  35. package/dist/modules/index.d.ts.map +1 -0
  36. package/dist/modules/index.js +14 -0
  37. package/dist/modules/index.js.map +1 -0
  38. package/dist/modules/keyboard.d.ts +15 -0
  39. package/dist/modules/keyboard.d.ts.map +1 -0
  40. package/dist/modules/keyboard.js +60 -0
  41. package/dist/modules/keyboard.js.map +1 -0
  42. package/dist/modules/performance.d.ts +26 -0
  43. package/dist/modules/performance.d.ts.map +1 -0
  44. package/dist/modules/performance.js +197 -0
  45. package/dist/modules/performance.js.map +1 -0
  46. package/dist/modules/rendering.d.ts +20 -0
  47. package/dist/modules/rendering.d.ts.map +1 -0
  48. package/dist/modules/rendering.js +1947 -0
  49. package/dist/modules/rendering.js.map +1 -0
  50. package/dist/modules/screenshot.d.ts +62 -0
  51. package/dist/modules/screenshot.d.ts.map +1 -0
  52. package/dist/modules/screenshot.js +325 -0
  53. package/dist/modules/screenshot.js.map +1 -0
  54. package/dist/modules/theme.d.ts +20 -0
  55. package/dist/modules/theme.d.ts.map +1 -0
  56. package/dist/modules/theme.js +60 -0
  57. package/dist/modules/theme.js.map +1 -0
  58. package/dist/modules/tooltips.d.ts +74 -0
  59. package/dist/modules/tooltips.d.ts.map +1 -0
  60. package/dist/modules/tooltips.js +553 -0
  61. package/dist/modules/tooltips.js.map +1 -0
  62. package/dist/modules/types.d.ts +118 -0
  63. package/dist/modules/types.d.ts.map +1 -0
  64. package/dist/modules/types.js +9 -0
  65. package/dist/modules/types.js.map +1 -0
  66. package/dist/modules/websocket.d.ts +16 -0
  67. package/dist/modules/websocket.d.ts.map +1 -0
  68. package/dist/modules/websocket.js +314 -0
  69. package/dist/modules/websocket.js.map +1 -0
  70. package/dist/network.d.ts +1 -0
  71. package/dist/network.d.ts.map +1 -0
  72. package/dist/network.js +5 -6
  73. package/dist/network.js.map +1 -0
  74. package/dist/outline.d.ts +1 -0
  75. package/dist/outline.d.ts.map +1 -0
  76. package/dist/outline.js +1 -0
  77. package/dist/outline.js.map +1 -0
  78. package/dist/presets.d.ts +9 -8
  79. package/dist/presets.d.ts.map +1 -0
  80. package/dist/presets.js +9 -8
  81. package/dist/presets.js.map +1 -0
  82. package/dist/schema.d.ts +1 -0
  83. package/dist/schema.d.ts.map +1 -0
  84. package/dist/schema.js +3 -3
  85. package/dist/schema.js.map +1 -0
  86. package/dist/settings.d.ts +7 -8
  87. package/dist/settings.d.ts.map +1 -0
  88. package/dist/settings.js +21 -17
  89. package/dist/settings.js.map +1 -0
  90. package/dist/storage.d.ts +1 -0
  91. package/dist/storage.d.ts.map +1 -0
  92. package/dist/storage.js +1 -0
  93. package/dist/storage.js.map +1 -0
  94. package/dist/types.d.ts +5 -4
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/types.js +3 -2
  97. package/dist/types.js.map +1 -0
  98. package/dist/ui/buttons.d.ts +3 -2
  99. package/dist/ui/buttons.d.ts.map +1 -0
  100. package/dist/ui/buttons.js +3 -2
  101. package/dist/ui/buttons.js.map +1 -0
  102. package/dist/ui/cards.d.ts +37 -0
  103. package/dist/ui/cards.d.ts.map +1 -0
  104. package/dist/ui/cards.js +134 -0
  105. package/dist/ui/cards.js.map +1 -0
  106. package/dist/ui/icons.d.ts +71 -2
  107. package/dist/ui/icons.d.ts.map +1 -0
  108. package/dist/ui/icons.js +148 -2
  109. package/dist/ui/icons.js.map +1 -0
  110. package/dist/ui/index.d.ts +3 -1
  111. package/dist/ui/index.d.ts.map +1 -0
  112. package/dist/ui/index.js +3 -1
  113. package/dist/ui/index.js.map +1 -0
  114. package/dist/ui/modals.d.ts +7 -2
  115. package/dist/ui/modals.d.ts.map +1 -0
  116. package/dist/ui/modals.js +51 -6
  117. package/dist/ui/modals.js.map +1 -0
  118. package/dist/utils.d.ts +3 -2
  119. package/dist/utils.d.ts.map +1 -0
  120. package/dist/utils.js +3 -2
  121. package/dist/utils.js.map +1 -0
  122. package/package.json +17 -14
@@ -0,0 +1,1947 @@
1
+ /**
2
+ * Rendering: renderCollapsed, renderCompact, renderExpanded, renderOverlays,
3
+ * console popups, modals, settings popover, and all DOM-creation UI code.
4
+ *
5
+ * Extracted from GlobalDevBar to reduce file size.
6
+ */
7
+ import { BUTTON_COLORS, CATEGORY_COLORS, CSS_COLORS, FONT_MONO, TAILWIND_BREAKPOINTS, } from '../constants.js';
8
+ import { extractDocumentOutline, outlineToMarkdown } from '../outline.js';
9
+ import { extractPageSchema, schemaToMarkdown } from '../schema.js';
10
+ import { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS } from '../settings.js';
11
+ import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createStyledButton, createSvgIcon, getButtonStyles, } from '../ui/index.js';
12
+ import { getResponsiveMetricVisibility } from './performance.js';
13
+ import { calculateCostEstimate, closeDesignReviewConfirm, copyPathToClipboard, handleDocumentOutline, handlePageSchema, handleSaveConsoleLogs, handleSaveOutline, handleSaveSchema, proceedWithDesignReview, showDesignReviewConfirmation, } from './screenshot.js';
14
+ import { setThemeMode } from './theme.js';
15
+ import { addTooltipTitle, attachBreakpointTooltip, attachButtonTooltip, attachClickToggleTooltip, attachInfoTooltip, attachMetricTooltip, attachTextTooltip, clearAllTooltips, } from './tooltips.js';
16
+ /**
17
+ * Capture the center of an element's bounding rect as a dot position.
18
+ * Used to animate the collapsed circle to the same spot as the connection dot.
19
+ */
20
+ function captureDotPosition(state, element) {
21
+ const rect = element.getBoundingClientRect();
22
+ state.lastDotPosition = {
23
+ left: rect.left + rect.width / 2,
24
+ top: rect.top + rect.height / 2,
25
+ bottom: window.innerHeight - (rect.top + rect.height / 2),
26
+ };
27
+ }
28
+ /**
29
+ * Main render dispatch - creates container and delegates to appropriate renderer.
30
+ */
31
+ export function render(state, consoleCaptureSingleton, customControls) {
32
+ if (state.destroyed)
33
+ return;
34
+ if (typeof document === 'undefined')
35
+ return;
36
+ // Clear any orphaned tooltips from previous render
37
+ clearAllTooltips(state);
38
+ // Remove existing overlay if any (modals append to body, need explicit cleanup)
39
+ if (state.overlayElement) {
40
+ state.overlayElement.remove();
41
+ state.overlayElement = null;
42
+ }
43
+ // Remove existing container if any
44
+ if (state.container) {
45
+ state.container.remove();
46
+ }
47
+ // Create new container
48
+ state.container = document.createElement('div');
49
+ state.container.setAttribute('data-devbar', 'true');
50
+ if (state.collapsed) {
51
+ renderCollapsed(state);
52
+ }
53
+ else if (state.compactMode) {
54
+ renderCompact(state);
55
+ }
56
+ else {
57
+ renderExpanded(state, customControls);
58
+ }
59
+ document.body.appendChild(state.container);
60
+ // Render overlays/modals
61
+ renderOverlays(state, consoleCaptureSingleton);
62
+ }
63
+ function renderOverlays(state, consoleCaptureSingleton) {
64
+ // Remove existing overlay
65
+ if (state.overlayElement) {
66
+ state.overlayElement.remove();
67
+ state.overlayElement = null;
68
+ }
69
+ // Render console popup if filter is active
70
+ if (state.consoleFilter) {
71
+ renderConsolePopup(state, consoleCaptureSingleton);
72
+ }
73
+ // Render outline modal
74
+ if (state.showOutlineModal) {
75
+ renderOutlineModal(state);
76
+ }
77
+ // Render schema modal
78
+ if (state.showSchemaModal) {
79
+ renderSchemaModal(state);
80
+ }
81
+ // Render design review confirmation modal
82
+ if (state.showDesignReviewConfirm) {
83
+ renderDesignReviewConfirmModal(state);
84
+ }
85
+ // Render settings popover
86
+ if (state.showSettingsPopover) {
87
+ renderSettingsPopover(state);
88
+ }
89
+ }
90
+ // ============================================================================
91
+ // Collapsed State
92
+ // ============================================================================
93
+ function renderCollapsed(state) {
94
+ if (!state.container)
95
+ return;
96
+ const { position, accentColor } = state.options;
97
+ const { errorCount, warningCount } = state.getLogCounts();
98
+ // Use captured dot position if available, otherwise fall back to preset positions
99
+ // The 13px offset accounts for half the collapsed circle diameter (26px / 2)
100
+ let posStyle;
101
+ if (state.lastDotPosition) {
102
+ // Position based on where the dot actually was
103
+ const isTop = position.startsWith('top');
104
+ posStyle = isTop
105
+ ? { top: `${state.lastDotPosition.top - 13}px`, left: `${state.lastDotPosition.left - 13}px` }
106
+ : {
107
+ bottom: `${state.lastDotPosition.bottom - 13}px`,
108
+ left: `${state.lastDotPosition.left - 13}px`,
109
+ };
110
+ }
111
+ else {
112
+ // Fallback preset positions for when no dot position was captured
113
+ const collapsedPositions = {
114
+ 'bottom-left': { bottom: '27px', left: '86px' },
115
+ 'bottom-right': { bottom: '27px', right: '29px' },
116
+ 'top-left': { top: '27px', left: '86px' },
117
+ 'top-right': { top: '27px', right: '29px' },
118
+ 'bottom-center': { bottom: '19px', left: '50%', transform: 'translateX(-50%)' },
119
+ };
120
+ posStyle = collapsedPositions[position] ?? collapsedPositions['bottom-left'];
121
+ }
122
+ const wrapper = state.container;
123
+ wrapper.className = 'devbar-collapse';
124
+ state.resetPositionStyles(wrapper);
125
+ // Set CSS variable for accent color (used by pulse animation)
126
+ wrapper.style.setProperty('--devbar-color-accent', accentColor);
127
+ Object.assign(wrapper.style, {
128
+ position: 'fixed',
129
+ ...posStyle,
130
+ zIndex: '9999',
131
+ backgroundColor: 'var(--devbar-color-bg-card)',
132
+ border: `1px solid ${accentColor}`,
133
+ borderRadius: '50%',
134
+ color: accentColor,
135
+ boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
136
+ backdropFilter: 'blur(8px)',
137
+ WebkitBackdropFilter: 'blur(8px)',
138
+ cursor: 'pointer',
139
+ display: 'flex',
140
+ alignItems: 'center',
141
+ justifyContent: 'center',
142
+ width: '26px',
143
+ height: '26px',
144
+ boxSizing: 'border-box',
145
+ animation: 'devbar-collapse 150ms ease-out, devbar-collapsed-pulse 2s ease-in-out 0.2s 3',
146
+ });
147
+ wrapper.onclick = () => {
148
+ state.collapsed = false;
149
+ state.debug.state('Expanded DevBar');
150
+ state.render();
151
+ };
152
+ // Create inner container for dot + chevron
153
+ const innerContainer = document.createElement('span');
154
+ Object.assign(innerContainer.style, {
155
+ display: 'flex',
156
+ alignItems: 'center',
157
+ justifyContent: 'center',
158
+ position: 'relative',
159
+ });
160
+ // Connection indicator dot (same size as in expanded state)
161
+ const dot = document.createElement('span');
162
+ Object.assign(dot.style, {
163
+ width: '6px',
164
+ height: '6px',
165
+ borderRadius: '50%',
166
+ backgroundColor: state.sweetlinkConnected ? CSS_COLORS.primary : CSS_COLORS.textMuted,
167
+ boxShadow: state.sweetlinkConnected ? `0 0 6px ${CSS_COLORS.primary}` : 'none',
168
+ transition: 'transform 150ms ease-out, opacity 150ms ease-out',
169
+ });
170
+ innerContainer.appendChild(dot);
171
+ // Expand chevron indicator (appears on hover)
172
+ const chevron = document.createElement('span');
173
+ Object.assign(chevron.style, {
174
+ position: 'absolute',
175
+ width: '100%',
176
+ height: '100%',
177
+ display: 'flex',
178
+ alignItems: 'center',
179
+ justifyContent: 'center',
180
+ opacity: '0',
181
+ transition: 'opacity 150ms ease-out',
182
+ fontSize: '10px',
183
+ color: accentColor,
184
+ });
185
+ chevron.textContent = '\u2197';
186
+ innerContainer.appendChild(chevron);
187
+ attachTextTooltip(state, wrapper, () => `Click to expand DevBar${state.sweetlinkConnected ? ' (Sweetlink connected)' : ' (Sweetlink not connected)'}${errorCount > 0 ? `\n${errorCount} console error${errorCount === 1 ? '' : 's'}` : ''}`, {
188
+ onEnter: () => {
189
+ dot.style.opacity = '0';
190
+ dot.style.transform = 'scale(0)';
191
+ chevron.style.opacity = '1';
192
+ },
193
+ onLeave: () => {
194
+ dot.style.opacity = '1';
195
+ dot.style.transform = 'scale(1)';
196
+ chevron.style.opacity = '0';
197
+ },
198
+ });
199
+ wrapper.appendChild(innerContainer);
200
+ // Error badge (absolute, top-right of circle, shifted left if warning badge exists)
201
+ if (errorCount > 0) {
202
+ wrapper.appendChild(state.createCollapsedBadge(errorCount, 'rgba(239, 68, 68, 0.95)', warningCount > 0 ? '12px' : '-6px'));
203
+ }
204
+ // Warning badge (absolute, top-right)
205
+ if (warningCount > 0) {
206
+ wrapper.appendChild(state.createCollapsedBadge(warningCount, 'rgba(245, 158, 11, 0.95)', '-6px'));
207
+ }
208
+ }
209
+ // ============================================================================
210
+ // Compact State
211
+ // ============================================================================
212
+ function renderCompact(state) {
213
+ if (!state.container)
214
+ return;
215
+ const { position, accentColor } = state.options;
216
+ const { errorCount, warningCount, infoCount } = state.getLogCounts();
217
+ // Simple position styles - same anchor points as expanded mode
218
+ const positionStyles = {
219
+ 'bottom-left': { bottom: '20px', left: '80px' },
220
+ 'bottom-right': { bottom: '20px', right: '16px' },
221
+ 'top-left': { top: '20px', left: '80px' },
222
+ 'top-right': { top: '20px', right: '16px' },
223
+ 'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
224
+ };
225
+ const posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
226
+ const wrapper = state.container;
227
+ state.resetPositionStyles(wrapper);
228
+ Object.assign(wrapper.style, {
229
+ position: 'fixed',
230
+ ...posStyle,
231
+ zIndex: '9999',
232
+ backgroundColor: 'var(--devbar-color-bg-card)',
233
+ border: `1px solid ${accentColor}`,
234
+ borderRadius: '20px',
235
+ color: accentColor,
236
+ boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
237
+ backdropFilter: 'blur(8px)',
238
+ WebkitBackdropFilter: 'blur(8px)',
239
+ padding: '6px 10px',
240
+ display: 'flex',
241
+ alignItems: 'center',
242
+ gap: '8px',
243
+ fontFamily: FONT_MONO,
244
+ fontSize: '0.6875rem',
245
+ });
246
+ // Connection indicator
247
+ const connIndicator = document.createElement('span');
248
+ connIndicator.className = 'devbar-clickable';
249
+ Object.assign(connIndicator.style, {
250
+ width: '12px',
251
+ height: '12px',
252
+ borderRadius: '50%',
253
+ display: 'flex',
254
+ alignItems: 'center',
255
+ justifyContent: 'center',
256
+ cursor: 'pointer',
257
+ });
258
+ attachTextTooltip(state, connIndicator, () => state.sweetlinkConnected ? 'Sweetlink connected' : 'Sweetlink disconnected');
259
+ connIndicator.onclick = (e) => {
260
+ e.stopPropagation();
261
+ captureDotPosition(state, connDot);
262
+ state.collapsed = true;
263
+ state.debug.state('Collapsed DevBar from compact mode');
264
+ state.render();
265
+ };
266
+ const connDot = document.createElement('span');
267
+ Object.assign(connDot.style, {
268
+ width: '6px',
269
+ height: '6px',
270
+ borderRadius: '50%',
271
+ backgroundColor: state.sweetlinkConnected ? CSS_COLORS.primary : CSS_COLORS.textMuted,
272
+ boxShadow: state.sweetlinkConnected ? `0 0 6px ${CSS_COLORS.primary}` : 'none',
273
+ });
274
+ connIndicator.appendChild(connDot);
275
+ wrapper.appendChild(connIndicator);
276
+ // Error badge
277
+ if (errorCount > 0) {
278
+ wrapper.appendChild(createConsoleBadge(state, 'error', errorCount, BUTTON_COLORS.error));
279
+ }
280
+ // Warning badge
281
+ if (warningCount > 0) {
282
+ wrapper.appendChild(createConsoleBadge(state, 'warn', warningCount, BUTTON_COLORS.warning));
283
+ }
284
+ // Info badge
285
+ if (infoCount > 0) {
286
+ wrapper.appendChild(createConsoleBadge(state, 'info', infoCount, BUTTON_COLORS.info));
287
+ }
288
+ // Screenshot button (if enabled)
289
+ if (state.options.showScreenshot) {
290
+ wrapper.appendChild(createScreenshotButton(state, accentColor));
291
+ }
292
+ // Settings gear button
293
+ wrapper.appendChild(createSettingsButton(state));
294
+ // Expand button (double-arrow)
295
+ const expandBtn = document.createElement('button');
296
+ expandBtn.type = 'button';
297
+ Object.assign(expandBtn.style, {
298
+ display: 'flex',
299
+ alignItems: 'center',
300
+ justifyContent: 'center',
301
+ width: '18px',
302
+ height: '18px',
303
+ borderRadius: '50%',
304
+ border: `1px solid ${accentColor}60`,
305
+ backgroundColor: 'transparent',
306
+ color: `${accentColor}99`,
307
+ cursor: 'pointer',
308
+ fontSize: '0.5rem',
309
+ transition: 'all 150ms',
310
+ });
311
+ expandBtn.textContent = '\u27EB';
312
+ attachTextTooltip(state, expandBtn, () => 'Expand DevBar', {
313
+ onEnter: () => {
314
+ expandBtn.style.backgroundColor = `${accentColor}20`;
315
+ expandBtn.style.borderColor = accentColor;
316
+ expandBtn.style.color = accentColor;
317
+ },
318
+ onLeave: () => {
319
+ expandBtn.style.backgroundColor = 'transparent';
320
+ expandBtn.style.borderColor = `${accentColor}60`;
321
+ expandBtn.style.color = `${accentColor}99`;
322
+ },
323
+ });
324
+ expandBtn.onclick = () => {
325
+ state.toggleCompactMode();
326
+ };
327
+ wrapper.appendChild(expandBtn);
328
+ }
329
+ // ============================================================================
330
+ // Expanded State
331
+ // ============================================================================
332
+ function renderExpanded(state, customControls) {
333
+ if (!state.container)
334
+ return;
335
+ const { position, accentColor, showMetrics, showScreenshot, showConsoleBadges } = state.options;
336
+ const { errorCount, warningCount, infoCount } = state.getLogCounts();
337
+ // Dot offset from container edge in expanded mode:
338
+ // border (1px) + padding (12px) + half indicator (6px) = 19px from left
339
+ // border (1px) + padding (8px) + half indicator (6px) = 15px from top
340
+ const DOT_OFFSET_LEFT = 19;
341
+ const DOT_OFFSET_TOP = 15;
342
+ const isCentered = position === 'bottom-center';
343
+ let posStyle;
344
+ // Use captured dot position to align the expanded bar's dot with where it was
345
+ // Always use top/left positioning for precise alignment
346
+ if (state.lastDotPosition && !isCentered) {
347
+ const isRight = position.endsWith('right');
348
+ if (isRight) {
349
+ // For right-aligned, fall back to default
350
+ const isTop = position.startsWith('top');
351
+ posStyle = isTop ? { top: '20px', right: '16px' } : { bottom: '20px', right: '16px' };
352
+ }
353
+ else {
354
+ // Use top positioning for precise dot alignment
355
+ posStyle = {
356
+ top: `${state.lastDotPosition.top - DOT_OFFSET_TOP}px`,
357
+ left: `${state.lastDotPosition.left - DOT_OFFSET_LEFT}px`,
358
+ };
359
+ }
360
+ // Clear the position after using it
361
+ state.lastDotPosition = null;
362
+ }
363
+ else {
364
+ const positionStyles = {
365
+ 'bottom-left': { bottom: '20px', left: '80px' },
366
+ 'bottom-right': { bottom: '20px', right: '16px' },
367
+ 'top-left': { top: '20px', left: '80px' },
368
+ 'top-right': { top: '20px', right: '16px' },
369
+ 'bottom-center': { bottom: '12px', left: '50%', transform: 'translateX(-50%)' },
370
+ };
371
+ posStyle = positionStyles[position] ?? positionStyles['bottom-left'];
372
+ }
373
+ const sizeOverrides = state.options.sizeOverrides;
374
+ const wrapper = state.container;
375
+ state.resetPositionStyles(wrapper);
376
+ // Calculate size values with overrides or defaults
377
+ // Use fit-content so DevBar only takes space it needs, but allow expansion up to max
378
+ // Centered: 16px margin each side. Left/right: 80px for Next.js bar + 16px margin
379
+ const defaultWidth = 'fit-content';
380
+ const defaultMinWidth = 'auto';
381
+ const defaultMaxWidth = isCentered ? 'calc(100vw - 32px)' : 'calc(100vw - 96px)';
382
+ Object.assign(wrapper.style, {
383
+ position: 'fixed',
384
+ ...posStyle,
385
+ zIndex: '9999',
386
+ backgroundColor: 'var(--devbar-color-bg-card)',
387
+ border: `1px solid ${accentColor}`,
388
+ borderRadius: '12px',
389
+ color: accentColor,
390
+ boxShadow: `0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px ${accentColor}1A`,
391
+ backdropFilter: 'blur(8px)',
392
+ WebkitBackdropFilter: 'blur(8px)',
393
+ boxSizing: 'border-box',
394
+ width: sizeOverrides?.width ?? defaultWidth,
395
+ maxWidth: sizeOverrides?.maxWidth ?? defaultMaxWidth,
396
+ minWidth: sizeOverrides?.minWidth ?? defaultMinWidth,
397
+ cursor: 'default',
398
+ });
399
+ wrapper.ondblclick = () => {
400
+ const dotEl = wrapper.querySelector('.devbar-status span span');
401
+ if (dotEl) {
402
+ captureDotPosition(state, dotEl);
403
+ }
404
+ state.collapsed = true;
405
+ state.debug.state('Collapsed DevBar (double-click)');
406
+ state.render();
407
+ };
408
+ // Main row - wrapping controlled by CSS media query
409
+ const mainRow = document.createElement('div');
410
+ mainRow.className = 'devbar-main';
411
+ Object.assign(mainRow.style, {
412
+ display: 'flex',
413
+ alignItems: 'center',
414
+ alignContent: 'flex-start',
415
+ justifyContent: 'flex-start',
416
+ gap: '0.5rem',
417
+ padding: '0.5rem 0.75rem',
418
+ minWidth: '0',
419
+ boxSizing: 'border-box',
420
+ fontFamily: FONT_MONO,
421
+ fontSize: '0.6875rem',
422
+ lineHeight: '1rem',
423
+ });
424
+ // Connection indicator (click to collapse)
425
+ const connIndicator = document.createElement('span');
426
+ connIndicator.className = 'devbar-clickable';
427
+ Object.assign(connIndicator.style, {
428
+ width: '12px',
429
+ height: '12px',
430
+ borderRadius: '50%',
431
+ backgroundColor: 'transparent',
432
+ display: 'flex',
433
+ alignItems: 'center',
434
+ justifyContent: 'center',
435
+ cursor: 'pointer',
436
+ flexShrink: '0',
437
+ });
438
+ attachTextTooltip(state, connIndicator, () => state.sweetlinkConnected
439
+ ? 'Sweetlink connected (click to minimize)'
440
+ : 'Sweetlink disconnected (click to minimize)');
441
+ connIndicator.onclick = (e) => {
442
+ e.stopPropagation();
443
+ captureDotPosition(state, connIndicator);
444
+ state.collapsed = true;
445
+ state.debug.state('Collapsed DevBar (connection dot click)');
446
+ state.render();
447
+ };
448
+ const connDot = document.createElement('span');
449
+ Object.assign(connDot.style, {
450
+ width: '6px',
451
+ height: '6px',
452
+ borderRadius: '50%',
453
+ backgroundColor: state.sweetlinkConnected ? CSS_COLORS.primary : CSS_COLORS.textMuted,
454
+ boxShadow: state.sweetlinkConnected ? `0 0 6px ${CSS_COLORS.primary}` : 'none',
455
+ transition: 'all 300ms',
456
+ });
457
+ connIndicator.appendChild(connDot);
458
+ // Status row wrapper - keeps connection dot, info, and badges together
459
+ const statusRow = document.createElement('div');
460
+ statusRow.className = 'devbar-status';
461
+ Object.assign(statusRow.style, {
462
+ display: 'flex',
463
+ alignItems: 'center',
464
+ gap: '0.5rem',
465
+ flexWrap: 'nowrap',
466
+ flexShrink: '0',
467
+ });
468
+ statusRow.appendChild(connIndicator);
469
+ // Info section
470
+ const infoSection = document.createElement('div');
471
+ infoSection.className = 'devbar-info';
472
+ Object.assign(infoSection.style, {
473
+ display: 'flex',
474
+ alignItems: 'center',
475
+ gap: '0.5rem',
476
+ textTransform: 'uppercase',
477
+ letterSpacing: '0.05em',
478
+ flexShrink: '1',
479
+ minWidth: '0',
480
+ overflow: 'visible',
481
+ });
482
+ // Breakpoint info
483
+ if (showMetrics.breakpoint && state.breakpointInfo) {
484
+ const bp = state.breakpointInfo.tailwindBreakpoint;
485
+ const breakpointData = TAILWIND_BREAKPOINTS[bp];
486
+ const bpSpan = document.createElement('span');
487
+ bpSpan.className = 'devbar-item';
488
+ Object.assign(bpSpan.style, { opacity: '0.9', cursor: 'default' });
489
+ // Use HTML tooltip for breakpoint info
490
+ attachBreakpointTooltip(state, bpSpan, bp, state.breakpointInfo.dimensions, breakpointData?.label || '');
491
+ let bpText = bp;
492
+ if (bp !== 'base') {
493
+ bpText =
494
+ bp === 'sm'
495
+ ? `${bp} - ${state.breakpointInfo.dimensions.split('x')[0]}`
496
+ : `${bp} - ${state.breakpointInfo.dimensions}`;
497
+ }
498
+ bpSpan.textContent = bpText;
499
+ infoSection.appendChild(bpSpan);
500
+ }
501
+ // Performance stats with responsive visibility
502
+ if (state.perfStats) {
503
+ const { visible, hidden } = getResponsiveMetricVisibility(state);
504
+ const addSeparator = () => {
505
+ const sep = document.createElement('span');
506
+ sep.style.opacity = '0.4';
507
+ sep.textContent = '|';
508
+ infoSection.appendChild(sep);
509
+ };
510
+ // Metric configurations for reuse
511
+ const metricConfigs = {
512
+ fcp: {
513
+ label: 'FCP',
514
+ value: state.perfStats.fcp,
515
+ title: 'First Contentful Paint (FCP)',
516
+ description: 'Time until the first text or image renders on screen.',
517
+ thresholds: { good: '<1.8s', needsWork: '1.8-3s', poor: '>3s' },
518
+ },
519
+ lcp: {
520
+ label: 'LCP',
521
+ value: state.perfStats.lcp,
522
+ title: 'Largest Contentful Paint (LCP)',
523
+ description: 'Time until the largest visible element renders on screen.',
524
+ thresholds: { good: '<2.5s', needsWork: '2.5-4s', poor: '>4s' },
525
+ },
526
+ cls: {
527
+ label: 'CLS',
528
+ value: state.perfStats.cls,
529
+ title: 'Cumulative Layout Shift (CLS)',
530
+ description: 'Visual stability score. Higher values mean more unexpected layout shifts.',
531
+ thresholds: { good: '<0.1', needsWork: '0.1-0.25', poor: '>0.25' },
532
+ },
533
+ inp: {
534
+ label: 'INP',
535
+ value: state.perfStats.inp,
536
+ title: 'Interaction to Next Paint (INP)',
537
+ description: 'Responsiveness to user input. Measures the longest interaction delay.',
538
+ thresholds: { good: '<200ms', needsWork: '200-500ms', poor: '>500ms' },
539
+ },
540
+ pageSize: {
541
+ label: '',
542
+ value: state.perfStats.totalSize,
543
+ title: 'Total Page Size',
544
+ description: 'Compressed/transferred size including HTML, CSS, JS, images, and other resources.',
545
+ },
546
+ };
547
+ // Render visible metrics
548
+ for (const metric of visible) {
549
+ if (!showMetrics[metric])
550
+ continue;
551
+ const config = metricConfigs[metric];
552
+ addSeparator();
553
+ const span = document.createElement('span');
554
+ span.className = 'devbar-item';
555
+ Object.assign(span.style, {
556
+ opacity: metric === 'pageSize' ? '0.7' : '0.85',
557
+ cursor: 'default',
558
+ });
559
+ span.textContent = config.label ? `${config.label} ${config.value}` : config.value;
560
+ if (config.thresholds) {
561
+ attachMetricTooltip(state, span, config.title, config.description, config.thresholds);
562
+ }
563
+ else {
564
+ attachInfoTooltip(state, span, config.title, config.description);
565
+ }
566
+ infoSection.appendChild(span);
567
+ }
568
+ // Render ellipsis button for hidden metrics
569
+ const hiddenMetricsEnabled = hidden.filter((m) => showMetrics[m]);
570
+ if (hiddenMetricsEnabled.length > 0) {
571
+ addSeparator();
572
+ const ellipsisBtn = document.createElement('span');
573
+ ellipsisBtn.className = 'devbar-item devbar-clickable';
574
+ Object.assign(ellipsisBtn.style, {
575
+ opacity: '0.7',
576
+ cursor: 'pointer',
577
+ padding: '0 2px',
578
+ });
579
+ ellipsisBtn.textContent = '\u00B7\u00B7\u00B7';
580
+ // Attach click-toggle tooltip showing hidden metrics (for mobile support)
581
+ attachClickToggleTooltip(state, ellipsisBtn, (tooltip) => {
582
+ addTooltipTitle(state, tooltip, 'More Metrics');
583
+ const metricsContainer = document.createElement('div');
584
+ Object.assign(metricsContainer.style, {
585
+ display: 'flex',
586
+ flexDirection: 'column',
587
+ gap: '6px',
588
+ marginTop: '8px',
589
+ });
590
+ for (const metric of hiddenMetricsEnabled) {
591
+ const config = metricConfigs[metric];
592
+ const row = document.createElement('div');
593
+ Object.assign(row.style, {
594
+ display: 'flex',
595
+ justifyContent: 'space-between',
596
+ gap: '12px',
597
+ });
598
+ const labelSpan = document.createElement('span');
599
+ Object.assign(labelSpan.style, { color: CSS_COLORS.textMuted });
600
+ labelSpan.textContent = config.title.split('(')[0].trim();
601
+ const valueSpan = document.createElement('span');
602
+ Object.assign(valueSpan.style, { color: CSS_COLORS.text, fontWeight: '500' });
603
+ valueSpan.textContent = config.value;
604
+ row.appendChild(labelSpan);
605
+ row.appendChild(valueSpan);
606
+ metricsContainer.appendChild(row);
607
+ }
608
+ tooltip.appendChild(metricsContainer);
609
+ });
610
+ infoSection.appendChild(ellipsisBtn);
611
+ }
612
+ }
613
+ statusRow.appendChild(infoSection);
614
+ // Console badges - add to status row so they stay with info
615
+ if (showConsoleBadges) {
616
+ if (errorCount > 0) {
617
+ statusRow.appendChild(createConsoleBadge(state, 'error', errorCount, BUTTON_COLORS.error));
618
+ }
619
+ if (warningCount > 0) {
620
+ statusRow.appendChild(createConsoleBadge(state, 'warn', warningCount, BUTTON_COLORS.warning));
621
+ }
622
+ if (infoCount > 0) {
623
+ statusRow.appendChild(createConsoleBadge(state, 'info', infoCount, BUTTON_COLORS.info));
624
+ }
625
+ }
626
+ mainRow.appendChild(statusRow);
627
+ // Action buttons - always render container for consistent height
628
+ const actionsContainer = document.createElement('div');
629
+ actionsContainer.className = 'devbar-actions';
630
+ if (showScreenshot) {
631
+ actionsContainer.appendChild(createScreenshotButton(state, accentColor));
632
+ }
633
+ actionsContainer.appendChild(createAIReviewButton(state));
634
+ actionsContainer.appendChild(createOutlineButton(state));
635
+ actionsContainer.appendChild(createSchemaButton(state));
636
+ actionsContainer.appendChild(createSettingsButton(state));
637
+ actionsContainer.appendChild(createCompactToggleButton(state));
638
+ mainRow.appendChild(actionsContainer);
639
+ wrapper.appendChild(mainRow);
640
+ // Render custom controls row if there are any
641
+ if (customControls.length > 0) {
642
+ const customRow = document.createElement('div');
643
+ Object.assign(customRow.style, {
644
+ display: 'flex',
645
+ flexWrap: 'wrap',
646
+ alignItems: 'center',
647
+ gap: '0.5rem',
648
+ padding: '0 0.75rem 0.5rem 0.75rem',
649
+ borderTop: `1px solid ${accentColor}30`,
650
+ marginTop: '0',
651
+ paddingTop: '0.5rem',
652
+ fontFamily: FONT_MONO,
653
+ fontSize: '0.6875rem',
654
+ });
655
+ customControls.forEach((control) => {
656
+ const btn = document.createElement('button');
657
+ btn.type = 'button';
658
+ const color = control.variant === 'warning' ? BUTTON_COLORS.warning : accentColor;
659
+ const isActive = control.active ?? false;
660
+ const isDisabled = control.disabled ?? false;
661
+ Object.assign(btn.style, {
662
+ padding: '4px 10px',
663
+ backgroundColor: isActive ? `${color}33` : 'transparent',
664
+ border: `1px solid ${isActive ? color : `${color}60`}`,
665
+ borderRadius: '6px',
666
+ color: isActive ? color : `${color}99`,
667
+ fontSize: '0.625rem',
668
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
669
+ opacity: isDisabled ? '0.5' : '1',
670
+ transition: 'all 150ms',
671
+ });
672
+ btn.textContent = control.label;
673
+ btn.disabled = isDisabled;
674
+ if (!isDisabled) {
675
+ btn.onmouseenter = () => {
676
+ btn.style.backgroundColor = `${color}20`;
677
+ btn.style.borderColor = color;
678
+ btn.style.color = color;
679
+ };
680
+ btn.onmouseleave = () => {
681
+ btn.style.backgroundColor = isActive ? `${color}33` : 'transparent';
682
+ btn.style.borderColor = isActive ? color : `${color}60`;
683
+ btn.style.color = isActive ? color : `${color}99`;
684
+ };
685
+ btn.onclick = () => control.onClick();
686
+ }
687
+ customRow.appendChild(btn);
688
+ });
689
+ wrapper.appendChild(customRow);
690
+ }
691
+ }
692
+ // ============================================================================
693
+ // Button Creators
694
+ // ============================================================================
695
+ function createConsoleBadge(state, type, count, color) {
696
+ const labelMap = { error: 'error', warn: 'warning', info: 'info' };
697
+ const label = labelMap[type];
698
+ const isActive = state.consoleFilter === type;
699
+ const badge = document.createElement('span');
700
+ badge.className = 'devbar-badge';
701
+ Object.assign(badge.style, {
702
+ display: 'flex',
703
+ alignItems: 'center',
704
+ justifyContent: 'center',
705
+ minWidth: '18px',
706
+ height: '18px',
707
+ padding: '0 5px',
708
+ borderRadius: '9999px',
709
+ backgroundColor: isActive ? color : `${color}E6`,
710
+ color: '#fff',
711
+ fontSize: '0.625rem',
712
+ fontWeight: '600',
713
+ cursor: 'pointer',
714
+ boxShadow: isActive ? `0 0 8px ${color}CC` : 'none',
715
+ });
716
+ badge.textContent = count > 99 ? '99+' : String(count);
717
+ attachTextTooltip(state, badge, () => `${count} console ${label}${count === 1 ? '' : 's'} (click to view)`);
718
+ badge.onclick = () => {
719
+ state.consoleFilter = state.consoleFilter === type ? null : type;
720
+ state.showOutlineModal = false;
721
+ state.showSchemaModal = false;
722
+ state.render();
723
+ };
724
+ return badge;
725
+ }
726
+ function createScreenshotButton(state, accentColor) {
727
+ const btn = document.createElement('button');
728
+ btn.type = 'button';
729
+ const hasSuccessState = state.copiedToClipboard || state.copiedPath || state.lastScreenshot;
730
+ const isDisabled = state.capturing;
731
+ // Grey out when not connected (save won't work, but clipboard still does)
732
+ const isGreyedOut = !state.sweetlinkConnected && !hasSuccessState;
733
+ // Attach HTML tooltip
734
+ attachButtonTooltip(state, btn, accentColor, (tooltip, h) => {
735
+ if (state.copiedToClipboard) {
736
+ h.addSuccess('Copied to clipboard!');
737
+ return;
738
+ }
739
+ if (state.copiedPath) {
740
+ h.addSuccess('Path copied to clipboard!');
741
+ return;
742
+ }
743
+ if (state.lastScreenshot) {
744
+ const screenshotPath = state.lastScreenshot;
745
+ h.addSuccess('Screenshot saved!', screenshotPath);
746
+ const copyLink = document.createElement('div');
747
+ Object.assign(copyLink.style, {
748
+ color: accentColor,
749
+ cursor: 'pointer',
750
+ fontSize: '0.625rem',
751
+ marginTop: '6px',
752
+ opacity: '0.8',
753
+ transition: 'opacity 150ms',
754
+ });
755
+ copyLink.textContent = 'copy path';
756
+ copyLink.onmouseenter = () => {
757
+ copyLink.style.opacity = '1';
758
+ };
759
+ copyLink.onmouseleave = () => {
760
+ copyLink.style.opacity = '0.8';
761
+ };
762
+ copyLink.onclick = async (e) => {
763
+ e.stopPropagation();
764
+ try {
765
+ await navigator.clipboard.writeText(screenshotPath);
766
+ copyLink.textContent = '\u2713 copied!';
767
+ copyLink.style.cursor = 'default';
768
+ copyLink.onclick = null;
769
+ }
770
+ catch {
771
+ copyLink.textContent = '\u00d7 failed to copy';
772
+ copyLink.style.color = CSS_COLORS.error;
773
+ }
774
+ };
775
+ tooltip.appendChild(copyLink);
776
+ return;
777
+ }
778
+ h.addTitle('Screenshot');
779
+ if (!state.sweetlinkConnected) {
780
+ h.addSectionHeader('Actions');
781
+ h.addShortcut('Shift+Click', 'Copy to clipboard');
782
+ h.addWarning('Sweetlink not connected. Save to file unavailable.');
783
+ }
784
+ else {
785
+ h.addSectionHeader('Actions');
786
+ h.addShortcut('Click', 'Save to file');
787
+ h.addShortcut('Shift+Click', 'Copy to clipboard');
788
+ h.addSectionHeader('Keyboard');
789
+ h.addShortcut('Cmd or Ctrl+Shift+S', 'Save');
790
+ h.addShortcut('Cmd or Ctrl+Shift+C', 'Copy');
791
+ }
792
+ });
793
+ Object.assign(btn.style, {
794
+ display: 'flex',
795
+ alignItems: 'center',
796
+ justifyContent: 'center',
797
+ width: '22px',
798
+ height: '22px',
799
+ minWidth: '22px',
800
+ minHeight: '22px',
801
+ flexShrink: '0',
802
+ borderRadius: '50%',
803
+ border: '1px solid',
804
+ borderColor: hasSuccessState ? accentColor : `${accentColor}80`,
805
+ backgroundColor: hasSuccessState ? `${accentColor}33` : 'transparent',
806
+ color: hasSuccessState ? accentColor : `${accentColor}99`,
807
+ cursor: !isDisabled ? 'pointer' : 'not-allowed',
808
+ opacity: isGreyedOut ? '0.4' : '1',
809
+ transition: 'all 150ms',
810
+ });
811
+ btn.disabled = isDisabled;
812
+ btn.onclick = (e) => {
813
+ // If we have a saved screenshot path, clicking copies the path
814
+ if (state.lastScreenshot && !e.shiftKey) {
815
+ copyPathToClipboard(state, state.lastScreenshot);
816
+ }
817
+ else {
818
+ state.handleScreenshot(e.shiftKey);
819
+ }
820
+ };
821
+ // Button content
822
+ if (state.copiedToClipboard || state.copiedPath || state.lastScreenshot) {
823
+ btn.textContent = '\u2713';
824
+ btn.style.fontSize = '0.6rem';
825
+ }
826
+ else if (state.capturing) {
827
+ btn.textContent = '...';
828
+ btn.style.fontSize = '0.5rem';
829
+ }
830
+ else {
831
+ // Camera icon SVG
832
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
833
+ svg.setAttribute('width', '12');
834
+ svg.setAttribute('height', '12');
835
+ svg.setAttribute('viewBox', '0 0 50.8 50.8');
836
+ svg.style.stroke = 'currentColor';
837
+ svg.style.fill = 'none';
838
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
839
+ g.setAttribute('stroke-linecap', 'round');
840
+ g.setAttribute('stroke-linejoin', 'round');
841
+ g.setAttribute('stroke-width', '4');
842
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
843
+ path.setAttribute('d', 'M19.844 7.938H7.938v11.905m0 11.113v11.906h11.905m23.019-11.906v11.906H30.956m11.906-23.018V7.938H30.956');
844
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
845
+ circle.setAttribute('cx', '25.4');
846
+ circle.setAttribute('cy', '25.4');
847
+ circle.setAttribute('r', '8.731');
848
+ g.appendChild(path);
849
+ g.appendChild(circle);
850
+ svg.appendChild(g);
851
+ btn.appendChild(svg);
852
+ }
853
+ return btn;
854
+ }
855
+ function createAIReviewButton(state) {
856
+ const btn = document.createElement('button');
857
+ btn.type = 'button';
858
+ const hasError = !!state.designReviewError;
859
+ const isActive = state.designReviewInProgress || !!state.lastDesignReview || hasError;
860
+ const isDisabled = state.designReviewInProgress || !state.sweetlinkConnected;
861
+ // Use error color (red) when there's an error, otherwise normal review color
862
+ const buttonColor = hasError ? CSS_COLORS.error : BUTTON_COLORS.review;
863
+ // Attach HTML tooltip
864
+ attachButtonTooltip(state, btn, buttonColor, (_tooltip, h) => {
865
+ if (state.designReviewInProgress) {
866
+ h.addProgress('AI Design Review in progress...');
867
+ return;
868
+ }
869
+ if (state.designReviewError) {
870
+ h.addError('Design review failed', state.designReviewError);
871
+ return;
872
+ }
873
+ if (state.lastDesignReview) {
874
+ h.addSuccess('Design review saved!', state.lastDesignReview);
875
+ return;
876
+ }
877
+ h.addTitle('AI Design Review');
878
+ h.addDescription('Captures screenshot and sends to Claude for design analysis.');
879
+ h.addSectionHeader('Requirements');
880
+ h.addShortcut('API Key', 'ANTHROPIC_API_KEY');
881
+ if (!state.sweetlinkConnected) {
882
+ h.addWarning('Sweetlink not connected');
883
+ }
884
+ });
885
+ Object.assign(btn.style, getButtonStyles(buttonColor, isActive, isDisabled));
886
+ if (!state.sweetlinkConnected)
887
+ btn.style.opacity = '0.5';
888
+ btn.disabled = isDisabled;
889
+ btn.onclick = () => showDesignReviewConfirmation(state);
890
+ if (state.designReviewInProgress) {
891
+ btn.textContent = '~';
892
+ btn.style.fontSize = '0.5rem';
893
+ btn.style.animation = 'pulse 1s infinite';
894
+ }
895
+ else if (state.designReviewError) {
896
+ // Show 'x' for error state
897
+ btn.textContent = '\u00D7';
898
+ btn.style.fontSize = '0.875rem';
899
+ btn.style.fontWeight = 'bold';
900
+ }
901
+ else if (state.lastDesignReview) {
902
+ btn.textContent = 'v';
903
+ btn.style.fontSize = '0.5rem';
904
+ }
905
+ else {
906
+ 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 }));
907
+ }
908
+ return btn;
909
+ }
910
+ function createOutlineButton(state) {
911
+ const btn = document.createElement('button');
912
+ btn.type = 'button';
913
+ const isActive = state.showOutlineModal || !!state.lastOutline;
914
+ // Attach HTML tooltip
915
+ attachButtonTooltip(state, btn, BUTTON_COLORS.outline, (_tooltip, h) => {
916
+ if (state.lastOutline) {
917
+ h.addSuccess('Outline saved!', state.lastOutline);
918
+ return;
919
+ }
920
+ h.addTitle('Document Outline');
921
+ h.addDescription('View page heading structure and save as markdown.');
922
+ if (!state.sweetlinkConnected) {
923
+ h.addWarning('Sweetlink not connected. Save to file unavailable.');
924
+ }
925
+ });
926
+ Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.outline, isActive, false));
927
+ btn.onclick = () => handleDocumentOutline(state);
928
+ if (state.lastOutline) {
929
+ btn.textContent = 'v';
930
+ btn.style.fontSize = '0.5rem';
931
+ }
932
+ else {
933
+ btn.appendChild(createSvgIcon('M3 4h18v2H3V4zm0 7h12v2H3v-2zm0 7h18v2H3v-2z', { fill: true }));
934
+ }
935
+ return btn;
936
+ }
937
+ function createSchemaButton(state) {
938
+ const btn = document.createElement('button');
939
+ btn.type = 'button';
940
+ const isActive = state.showSchemaModal || !!state.lastSchema;
941
+ // Attach HTML tooltip
942
+ attachButtonTooltip(state, btn, BUTTON_COLORS.schema, (_tooltip, h) => {
943
+ if (state.lastSchema) {
944
+ h.addSuccess('Schema saved!', state.lastSchema);
945
+ return;
946
+ }
947
+ h.addTitle('Page Schema');
948
+ h.addDescription('View JSON-LD, Open Graph, and other structured data.');
949
+ if (!state.sweetlinkConnected) {
950
+ h.addWarning('Sweetlink not connected. Save to file unavailable.');
951
+ }
952
+ });
953
+ Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.schema, isActive, false));
954
+ btn.onclick = () => handlePageSchema(state);
955
+ if (state.lastSchema) {
956
+ btn.textContent = 'v';
957
+ btn.style.fontSize = '0.5rem';
958
+ }
959
+ else {
960
+ 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 }));
961
+ }
962
+ return btn;
963
+ }
964
+ /**
965
+ * Create the settings gear button.
966
+ */
967
+ function createSettingsButton(state) {
968
+ const btn = document.createElement('button');
969
+ btn.type = 'button';
970
+ btn.setAttribute('data-testid', 'devbar-settings-button');
971
+ // Attach HTML tooltip
972
+ attachButtonTooltip(state, btn, CSS_COLORS.textSecondary, (_tooltip, h) => {
973
+ h.addTitle('Settings');
974
+ h.addSectionHeader('Keyboard');
975
+ h.addShortcut('Cmd or Ctrl+Shift+M', 'Toggle compact mode');
976
+ });
977
+ const isActive = state.showSettingsPopover;
978
+ const color = CSS_COLORS.textSecondary;
979
+ Object.assign(btn.style, {
980
+ display: 'flex',
981
+ alignItems: 'center',
982
+ justifyContent: 'center',
983
+ width: '22px',
984
+ height: '22px',
985
+ minWidth: '22px',
986
+ minHeight: '22px',
987
+ flexShrink: '0',
988
+ borderRadius: '50%',
989
+ border: `1px solid ${isActive ? color : `${color}60`}`,
990
+ backgroundColor: isActive ? `${color}20` : 'transparent',
991
+ color: isActive ? color : `${color}99`,
992
+ cursor: 'pointer',
993
+ transition: 'all 150ms',
994
+ });
995
+ btn.onclick = () => {
996
+ state.showSettingsPopover = !state.showSettingsPopover;
997
+ state.consoleFilter = null;
998
+ state.showOutlineModal = false;
999
+ state.showSchemaModal = false;
1000
+ state.showDesignReviewConfirm = false;
1001
+ state.render();
1002
+ };
1003
+ // Gear icon SVG
1004
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1005
+ svg.setAttribute('width', '12');
1006
+ svg.setAttribute('height', '12');
1007
+ svg.setAttribute('viewBox', '0 0 24 24');
1008
+ svg.setAttribute('fill', 'none');
1009
+ svg.setAttribute('stroke', 'currentColor');
1010
+ svg.setAttribute('stroke-width', '2');
1011
+ svg.setAttribute('stroke-linecap', 'round');
1012
+ svg.setAttribute('stroke-linejoin', 'round');
1013
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1014
+ path.setAttribute('d', 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z');
1015
+ svg.appendChild(path);
1016
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1017
+ circle.setAttribute('cx', '12');
1018
+ circle.setAttribute('cy', '12');
1019
+ circle.setAttribute('r', '3');
1020
+ svg.appendChild(circle);
1021
+ btn.appendChild(svg);
1022
+ return btn;
1023
+ }
1024
+ /**
1025
+ * Create the compact mode toggle button with chevron icon.
1026
+ */
1027
+ function createCompactToggleButton(state) {
1028
+ const btn = document.createElement('button');
1029
+ btn.type = 'button';
1030
+ const isCompact = state.compactMode;
1031
+ const { accentColor } = state.options;
1032
+ const iconColor = CSS_COLORS.textSecondary;
1033
+ Object.assign(btn.style, {
1034
+ display: 'flex',
1035
+ alignItems: 'center',
1036
+ justifyContent: 'center',
1037
+ width: '22px',
1038
+ height: '22px',
1039
+ minWidth: '22px',
1040
+ minHeight: '22px',
1041
+ flexShrink: '0',
1042
+ borderRadius: '50%',
1043
+ border: `1px solid ${accentColor}60`,
1044
+ backgroundColor: 'transparent',
1045
+ color: `${iconColor}99`,
1046
+ cursor: 'pointer',
1047
+ transition: 'all 150ms',
1048
+ });
1049
+ attachTextTooltip(state, btn, () => (isCompact ? 'Expand (Cmd or Ctrl+Shift+M)' : 'Compact (Cmd or Ctrl+Shift+M)'), {
1050
+ onEnter: () => {
1051
+ btn.style.borderColor = accentColor;
1052
+ btn.style.backgroundColor = `${accentColor}20`;
1053
+ btn.style.color = iconColor;
1054
+ },
1055
+ onLeave: () => {
1056
+ btn.style.borderColor = `${accentColor}60`;
1057
+ btn.style.backgroundColor = 'transparent';
1058
+ btn.style.color = `${iconColor}99`;
1059
+ },
1060
+ });
1061
+ btn.onclick = () => {
1062
+ state.toggleCompactMode();
1063
+ };
1064
+ // Chevron icon SVG - points right when expanded, left when compact
1065
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1066
+ svg.setAttribute('width', '12');
1067
+ svg.setAttribute('height', '12');
1068
+ svg.setAttribute('viewBox', '0 0 24 24');
1069
+ svg.setAttribute('fill', 'none');
1070
+ svg.setAttribute('stroke', 'currentColor');
1071
+ svg.setAttribute('stroke-width', '2');
1072
+ svg.setAttribute('stroke-linecap', 'round');
1073
+ svg.setAttribute('stroke-linejoin', 'round');
1074
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
1075
+ // Left chevron (<) when expanded to shrink, right chevron (>) when compact to expand
1076
+ path.setAttribute('points', isCompact ? '9 18 15 12 9 6' : '15 18 9 12 15 6');
1077
+ svg.appendChild(path);
1078
+ btn.appendChild(svg);
1079
+ return btn;
1080
+ }
1081
+ // ============================================================================
1082
+ // Console Popup
1083
+ // ============================================================================
1084
+ function renderConsolePopup(state, consoleCaptureSingleton) {
1085
+ const filterType = state.consoleFilter;
1086
+ if (!filterType)
1087
+ return;
1088
+ const logs = consoleCaptureSingleton
1089
+ .getLogs()
1090
+ .filter((log) => log.level === filterType);
1091
+ const colorMap = { error: BUTTON_COLORS.error, warn: BUTTON_COLORS.warning, info: BUTTON_COLORS.info };
1092
+ const color = colorMap[filterType];
1093
+ const labelMap = { error: 'Errors', warn: 'Warnings', info: 'Info' };
1094
+ const label = labelMap[filterType];
1095
+ const closeModal = () => {
1096
+ state.consoleFilter = null;
1097
+ state.render();
1098
+ };
1099
+ const overlay = createModalOverlay(closeModal);
1100
+ const modal = createModalBox(color);
1101
+ const header = createModalHeader({
1102
+ color,
1103
+ title: `Console ${label} (${logs.length})`,
1104
+ onClose: closeModal,
1105
+ onCopyMd: async () => {
1106
+ const lines = logs.map((log) => {
1107
+ const time = new Date(log.timestamp).toLocaleTimeString();
1108
+ return `[${time}] ${log.level}: ${log.message}`;
1109
+ });
1110
+ await navigator.clipboard.writeText(lines.join('\n'));
1111
+ },
1112
+ onSave: () => handleSaveConsoleLogs(state, logs),
1113
+ sweetlinkConnected: state.sweetlinkConnected,
1114
+ isSaving: state.savingConsoleLogs,
1115
+ savedPath: state.lastConsoleLogs,
1116
+ });
1117
+ modal.appendChild(header);
1118
+ const content = createModalContent();
1119
+ if (logs.length === 0) {
1120
+ content.appendChild(createEmptyMessage(`No ${filterType}s recorded`));
1121
+ }
1122
+ else {
1123
+ renderConsoleLogs(content, logs, color);
1124
+ }
1125
+ modal.appendChild(content);
1126
+ overlay.appendChild(modal);
1127
+ state.overlayElement = overlay;
1128
+ document.body.appendChild(overlay);
1129
+ }
1130
+ function renderConsoleLogs(container, logs, color) {
1131
+ logs.forEach((log, index) => {
1132
+ const logItem = document.createElement('div');
1133
+ Object.assign(logItem.style, {
1134
+ padding: '8px 14px',
1135
+ borderBottom: index < logs.length - 1 ? '1px solid rgba(255, 255, 255, 0.05)' : 'none',
1136
+ });
1137
+ const timestamp = document.createElement('span');
1138
+ Object.assign(timestamp.style, {
1139
+ color: CSS_COLORS.textMuted,
1140
+ fontSize: '0.625rem',
1141
+ marginRight: '8px',
1142
+ });
1143
+ timestamp.textContent = new Date(log.timestamp).toLocaleTimeString();
1144
+ logItem.appendChild(timestamp);
1145
+ const message = document.createElement('span');
1146
+ Object.assign(message.style, {
1147
+ color,
1148
+ fontSize: '0.6875rem',
1149
+ wordBreak: 'break-word',
1150
+ whiteSpace: 'pre-wrap',
1151
+ });
1152
+ message.textContent = log.message;
1153
+ logItem.appendChild(message);
1154
+ container.appendChild(logItem);
1155
+ });
1156
+ }
1157
+ // ============================================================================
1158
+ // Outline / Schema Modals
1159
+ // ============================================================================
1160
+ function renderOutlineModal(state) {
1161
+ const outline = extractDocumentOutline();
1162
+ const color = BUTTON_COLORS.outline;
1163
+ const closeModal = () => {
1164
+ state.showOutlineModal = false;
1165
+ state.render();
1166
+ };
1167
+ const overlay = createModalOverlay(closeModal);
1168
+ const modal = createModalBox(color);
1169
+ const header = createModalHeader({
1170
+ color,
1171
+ title: 'Document Outline',
1172
+ onClose: closeModal,
1173
+ onCopyMd: async () => {
1174
+ const markdown = outlineToMarkdown(outline);
1175
+ await navigator.clipboard.writeText(markdown);
1176
+ },
1177
+ onSave: () => handleSaveOutline(state),
1178
+ sweetlinkConnected: state.sweetlinkConnected,
1179
+ isSaving: state.savingOutline,
1180
+ savedPath: state.lastOutline,
1181
+ });
1182
+ modal.appendChild(header);
1183
+ const content = createModalContent();
1184
+ if (outline.length === 0) {
1185
+ content.appendChild(createEmptyMessage('No semantic elements found in this document'));
1186
+ }
1187
+ else {
1188
+ renderOutlineNodes(outline, content, 0);
1189
+ }
1190
+ modal.appendChild(content);
1191
+ overlay.appendChild(modal);
1192
+ state.overlayElement = overlay;
1193
+ document.body.appendChild(overlay);
1194
+ }
1195
+ function renderOutlineNodes(nodes, parentEl, depth) {
1196
+ for (const node of nodes) {
1197
+ const nodeEl = document.createElement('div');
1198
+ Object.assign(nodeEl.style, {
1199
+ padding: `4px 0 4px ${depth * 16}px`,
1200
+ });
1201
+ const tagSpan = document.createElement('span');
1202
+ const categoryColor = CATEGORY_COLORS[node.category || 'other'] || CATEGORY_COLORS.other;
1203
+ Object.assign(tagSpan.style, {
1204
+ color: categoryColor,
1205
+ fontSize: '0.6875rem',
1206
+ fontWeight: '500',
1207
+ });
1208
+ tagSpan.textContent = `<${node.tagName}>`;
1209
+ nodeEl.appendChild(tagSpan);
1210
+ if (node.category) {
1211
+ const categorySpan = document.createElement('span');
1212
+ Object.assign(categorySpan.style, {
1213
+ color: CSS_COLORS.textMuted,
1214
+ fontSize: '0.625rem',
1215
+ marginLeft: '6px',
1216
+ });
1217
+ categorySpan.textContent = `[${node.category}]`;
1218
+ nodeEl.appendChild(categorySpan);
1219
+ }
1220
+ const textSpan = document.createElement('span');
1221
+ Object.assign(textSpan.style, {
1222
+ color: '#d1d5db',
1223
+ fontSize: '0.6875rem',
1224
+ marginLeft: '8px',
1225
+ });
1226
+ const truncatedText = node.text.length > 60 ? `${node.text.slice(0, 60)}...` : node.text;
1227
+ textSpan.textContent = truncatedText;
1228
+ nodeEl.appendChild(textSpan);
1229
+ if (node.id) {
1230
+ const idSpan = document.createElement('span');
1231
+ Object.assign(idSpan.style, {
1232
+ color: '#9ca3af',
1233
+ fontSize: '0.625rem',
1234
+ marginLeft: '6px',
1235
+ });
1236
+ idSpan.textContent = `#${node.id}`;
1237
+ nodeEl.appendChild(idSpan);
1238
+ }
1239
+ parentEl.appendChild(nodeEl);
1240
+ if (node.children.length > 0) {
1241
+ renderOutlineNodes(node.children, parentEl, depth + 1);
1242
+ }
1243
+ }
1244
+ }
1245
+ function renderSchemaModal(state) {
1246
+ const schema = extractPageSchema();
1247
+ const color = BUTTON_COLORS.schema;
1248
+ const closeModal = () => {
1249
+ state.showSchemaModal = false;
1250
+ state.render();
1251
+ };
1252
+ const overlay = createModalOverlay(closeModal);
1253
+ const modal = createModalBox(color);
1254
+ const header = createModalHeader({
1255
+ color,
1256
+ title: 'Page Schema',
1257
+ onClose: closeModal,
1258
+ onCopyMd: async () => {
1259
+ const markdown = schemaToMarkdown(schema);
1260
+ await navigator.clipboard.writeText(markdown);
1261
+ },
1262
+ onSave: () => handleSaveSchema(state),
1263
+ sweetlinkConnected: state.sweetlinkConnected,
1264
+ isSaving: state.savingSchema,
1265
+ savedPath: state.lastSchema,
1266
+ });
1267
+ modal.appendChild(header);
1268
+ const content = createModalContent();
1269
+ const hasContent = schema.jsonLd.length > 0 ||
1270
+ Object.keys(schema.openGraph).length > 0 ||
1271
+ Object.keys(schema.twitter).length > 0 ||
1272
+ Object.keys(schema.metaTags).length > 0;
1273
+ if (!hasContent) {
1274
+ content.appendChild(createEmptyMessage('No structured data found on this page'));
1275
+ }
1276
+ else {
1277
+ renderSchemaSection(content, 'JSON-LD', schema.jsonLd, color);
1278
+ renderSchemaSection(content, 'Open Graph', schema.openGraph, CSS_COLORS.info);
1279
+ renderSchemaSection(content, 'Twitter Cards', schema.twitter, CSS_COLORS.cyan);
1280
+ renderSchemaSection(content, 'Meta Tags', schema.metaTags, CSS_COLORS.textMuted);
1281
+ }
1282
+ modal.appendChild(content);
1283
+ overlay.appendChild(modal);
1284
+ state.overlayElement = overlay;
1285
+ document.body.appendChild(overlay);
1286
+ }
1287
+ function renderSchemaSection(container, title, items, color) {
1288
+ const isEmpty = Array.isArray(items) ? items.length === 0 : Object.keys(items).length === 0;
1289
+ if (isEmpty)
1290
+ return;
1291
+ const section = document.createElement('div');
1292
+ section.style.marginBottom = '20px';
1293
+ const sectionTitle = document.createElement('h3');
1294
+ Object.assign(sectionTitle.style, {
1295
+ color,
1296
+ fontSize: '0.8125rem',
1297
+ fontWeight: '600',
1298
+ marginBottom: '10px',
1299
+ borderBottom: `1px solid ${color}40`,
1300
+ paddingBottom: '6px',
1301
+ });
1302
+ sectionTitle.textContent = title;
1303
+ section.appendChild(sectionTitle);
1304
+ if (Array.isArray(items)) {
1305
+ renderJsonLdItems(section, items);
1306
+ }
1307
+ else {
1308
+ renderKeyValueItems(section, items);
1309
+ }
1310
+ container.appendChild(section);
1311
+ }
1312
+ function renderJsonLdItems(container, items) {
1313
+ items.forEach((item, i) => {
1314
+ const itemEl = document.createElement('div');
1315
+ itemEl.style.marginBottom = '10px';
1316
+ const itemTitle = document.createElement('div');
1317
+ Object.assign(itemTitle.style, {
1318
+ color: '#9ca3af',
1319
+ fontSize: '0.6875rem',
1320
+ marginBottom: '4px',
1321
+ });
1322
+ itemTitle.textContent = `Schema ${i + 1}`;
1323
+ itemEl.appendChild(itemTitle);
1324
+ const codeEl = document.createElement('pre');
1325
+ Object.assign(codeEl.style, {
1326
+ backgroundColor: 'rgba(0, 0, 0, 0.3)',
1327
+ borderRadius: '4px',
1328
+ padding: '10px',
1329
+ overflow: 'auto',
1330
+ fontSize: '0.625rem',
1331
+ margin: '0',
1332
+ maxHeight: '300px',
1333
+ });
1334
+ // Syntax highlight the JSON using DOM methods for safety
1335
+ appendHighlightedJson(codeEl, JSON.stringify(item, null, 2));
1336
+ itemEl.appendChild(codeEl);
1337
+ container.appendChild(itemEl);
1338
+ });
1339
+ }
1340
+ function appendHighlightedJson(container, json) {
1341
+ // Color map for different token types
1342
+ const colors = {
1343
+ key: CSS_COLORS.primary, // green
1344
+ string: CSS_COLORS.warning, // amber/yellow
1345
+ number: CSS_COLORS.purple, // purple
1346
+ boolean: CSS_COLORS.info, // blue
1347
+ nullVal: CSS_COLORS.error, // red
1348
+ punct: CSS_COLORS.textMuted, // gray
1349
+ };
1350
+ // Simple tokenizer for JSON using matchAll for safety
1351
+ const tokenPattern = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)|([{}[\],])|(\s+)/g;
1352
+ for (const match of json.matchAll(tokenPattern)) {
1353
+ const [, str, colon, bool, nullToken, num, punct, whitespace] = match;
1354
+ if (whitespace) {
1355
+ container.appendChild(document.createTextNode(whitespace));
1356
+ }
1357
+ else if (str !== undefined) {
1358
+ const span = document.createElement('span');
1359
+ span.style.color = colon ? colors.key : colors.string;
1360
+ span.textContent = str;
1361
+ container.appendChild(span);
1362
+ if (colon) {
1363
+ const colonSpan = document.createElement('span');
1364
+ colonSpan.style.color = colors.punct;
1365
+ colonSpan.textContent = ':';
1366
+ container.appendChild(colonSpan);
1367
+ }
1368
+ }
1369
+ else if (bool) {
1370
+ const span = document.createElement('span');
1371
+ span.style.color = colors.boolean;
1372
+ span.textContent = bool;
1373
+ container.appendChild(span);
1374
+ }
1375
+ else if (nullToken) {
1376
+ const span = document.createElement('span');
1377
+ span.style.color = colors.nullVal;
1378
+ span.textContent = nullToken;
1379
+ container.appendChild(span);
1380
+ }
1381
+ else if (num) {
1382
+ const span = document.createElement('span');
1383
+ span.style.color = colors.number;
1384
+ span.textContent = num;
1385
+ container.appendChild(span);
1386
+ }
1387
+ else if (punct) {
1388
+ const span = document.createElement('span');
1389
+ span.style.color = colors.punct;
1390
+ span.textContent = punct;
1391
+ container.appendChild(span);
1392
+ }
1393
+ }
1394
+ }
1395
+ function renderKeyValueItems(container, items) {
1396
+ for (const [key, value] of Object.entries(items)) {
1397
+ const row = document.createElement('div');
1398
+ Object.assign(row.style, {
1399
+ display: 'flex',
1400
+ marginBottom: '4px',
1401
+ alignItems: 'flex-start',
1402
+ });
1403
+ const keyEl = document.createElement('span');
1404
+ Object.assign(keyEl.style, {
1405
+ color: '#9ca3af',
1406
+ fontSize: '0.6875rem',
1407
+ width: '120px',
1408
+ minWidth: '120px',
1409
+ maxWidth: '120px',
1410
+ flexShrink: '0',
1411
+ overflow: 'hidden',
1412
+ textOverflow: 'ellipsis',
1413
+ whiteSpace: 'nowrap',
1414
+ });
1415
+ keyEl.textContent = key;
1416
+ // Show full key on hover if it might be truncated
1417
+ if (key.length > 18) {
1418
+ keyEl.title = key;
1419
+ }
1420
+ row.appendChild(keyEl);
1421
+ const valueEl = document.createElement('span');
1422
+ const strValue = String(value);
1423
+ Object.assign(valueEl.style, {
1424
+ color: '#d1d5db',
1425
+ fontSize: '0.6875rem',
1426
+ flex: '1',
1427
+ wordBreak: 'break-word',
1428
+ whiteSpace: 'pre-wrap',
1429
+ });
1430
+ valueEl.textContent = strValue;
1431
+ row.appendChild(valueEl);
1432
+ container.appendChild(row);
1433
+ }
1434
+ }
1435
+ // ============================================================================
1436
+ // Design Review Confirmation Modal
1437
+ // ============================================================================
1438
+ function renderDesignReviewConfirmModal(state) {
1439
+ const color = BUTTON_COLORS.review;
1440
+ const closeModal = () => closeDesignReviewConfirm(state);
1441
+ const overlay = createModalOverlay(closeModal);
1442
+ // Override z-index for this modal to be above others
1443
+ overlay.style.zIndex = '10003';
1444
+ const modal = createModalBox(color);
1445
+ modal.style.maxWidth = '450px';
1446
+ // Header with title and close button
1447
+ const header = document.createElement('div');
1448
+ Object.assign(header.style, {
1449
+ display: 'flex',
1450
+ alignItems: 'center',
1451
+ justifyContent: 'space-between',
1452
+ padding: '14px 18px',
1453
+ borderBottom: `1px solid ${color}40`,
1454
+ backgroundColor: `${color}15`,
1455
+ });
1456
+ const title = document.createElement('span');
1457
+ Object.assign(title.style, { color, fontSize: '0.875rem', fontWeight: '600' });
1458
+ title.textContent = 'AI Design Review';
1459
+ header.appendChild(title);
1460
+ const closeBtn = createStyledButton({
1461
+ color: CSS_COLORS.textMuted,
1462
+ text: '\u00D7',
1463
+ padding: '0',
1464
+ fontSize: '1.25rem',
1465
+ });
1466
+ closeBtn.style.border = 'none';
1467
+ closeBtn.onclick = closeModal;
1468
+ header.appendChild(closeBtn);
1469
+ modal.appendChild(header);
1470
+ // Content
1471
+ const content = document.createElement('div');
1472
+ Object.assign(content.style, {
1473
+ padding: '18px',
1474
+ color: CSS_COLORS.text,
1475
+ fontSize: '0.8125rem',
1476
+ lineHeight: '1.6',
1477
+ });
1478
+ if (state.apiKeyStatus === null) {
1479
+ content.appendChild(createEmptyMessage('Checking API key configuration...'));
1480
+ }
1481
+ else if (!state.apiKeyStatus.configured) {
1482
+ content.appendChild(renderApiKeyNotConfiguredContent());
1483
+ }
1484
+ else {
1485
+ content.appendChild(renderApiKeyConfiguredContent(state));
1486
+ }
1487
+ modal.appendChild(content);
1488
+ // Footer with buttons
1489
+ const footer = document.createElement('div');
1490
+ Object.assign(footer.style, {
1491
+ display: 'flex',
1492
+ justifyContent: 'flex-end',
1493
+ gap: '10px',
1494
+ padding: '14px 18px',
1495
+ borderTop: `1px solid ${CSS_COLORS.border}`,
1496
+ });
1497
+ const cancelBtn = createStyledButton({
1498
+ color: CSS_COLORS.textMuted,
1499
+ text: 'Cancel',
1500
+ padding: '8px 16px',
1501
+ });
1502
+ cancelBtn.onclick = closeModal;
1503
+ footer.appendChild(cancelBtn);
1504
+ if (state.apiKeyStatus?.configured) {
1505
+ const proceedBtn = createStyledButton({ color, text: 'Run Review', padding: '8px 16px' });
1506
+ proceedBtn.style.backgroundColor = `${color}20`;
1507
+ proceedBtn.onclick = () => proceedWithDesignReview(state);
1508
+ footer.appendChild(proceedBtn);
1509
+ }
1510
+ modal.appendChild(footer);
1511
+ overlay.appendChild(modal);
1512
+ document.body.appendChild(overlay);
1513
+ }
1514
+ function renderApiKeyNotConfiguredContent() {
1515
+ const wrapper = document.createElement('div');
1516
+ wrapper.appendChild(createInfoBox(CSS_COLORS.error, 'API Key Not Configured', 'The ANTHROPIC_API_KEY environment variable is not set.'));
1517
+ // Instructions
1518
+ const instructions = document.createElement('div');
1519
+ Object.assign(instructions.style, { marginBottom: '12px' });
1520
+ const instructTitle = document.createElement('div');
1521
+ Object.assign(instructTitle.style, {
1522
+ color: CSS_COLORS.textSecondary,
1523
+ fontWeight: '600',
1524
+ marginBottom: '8px',
1525
+ });
1526
+ instructTitle.textContent = 'To configure:';
1527
+ instructions.appendChild(instructTitle);
1528
+ const steps = [
1529
+ { text: '1. Get an API key from console.anthropic.com', highlight: false },
1530
+ { text: '2. Add to your .env file:', highlight: false },
1531
+ { text: ' ANTHROPIC_API_KEY=sk-ant-...', highlight: true },
1532
+ { text: '3. Restart your dev server', highlight: false },
1533
+ ];
1534
+ steps.forEach(({ text, highlight }) => {
1535
+ const stepDiv = document.createElement('div');
1536
+ Object.assign(stepDiv.style, {
1537
+ color: highlight ? CSS_COLORS.primary : CSS_COLORS.textMuted,
1538
+ fontSize: '0.75rem',
1539
+ marginBottom: '4px',
1540
+ fontFamily: FONT_MONO,
1541
+ });
1542
+ stepDiv.textContent = text;
1543
+ instructions.appendChild(stepDiv);
1544
+ });
1545
+ wrapper.appendChild(instructions);
1546
+ return wrapper;
1547
+ }
1548
+ function renderApiKeyConfiguredContent(state) {
1549
+ const wrapper = document.createElement('div');
1550
+ Object.assign(wrapper.style, { marginBottom: '16px' });
1551
+ const desc = document.createElement('p');
1552
+ Object.assign(desc.style, { color: CSS_COLORS.textSecondary, marginBottom: '12px' });
1553
+ desc.textContent = 'This will capture a screenshot and send it to Claude for design analysis.';
1554
+ wrapper.appendChild(desc);
1555
+ // Cost estimate
1556
+ const estimate = calculateCostEstimate(state);
1557
+ if (estimate) {
1558
+ const costBox = createInfoBox(CSS_COLORS.primary, 'Estimated Cost', []);
1559
+ // Remove default margin and adjust padding
1560
+ costBox.style.marginBottom = '0';
1561
+ costBox.style.padding = '12px';
1562
+ const costDetails = document.createElement('div');
1563
+ Object.assign(costDetails.style, {
1564
+ display: 'flex',
1565
+ justifyContent: 'space-between',
1566
+ color: CSS_COLORS.textSecondary,
1567
+ fontSize: '0.75rem',
1568
+ });
1569
+ const tokensSpan = document.createElement('span');
1570
+ tokensSpan.textContent = `~${estimate.tokens.toLocaleString()} tokens`;
1571
+ costDetails.appendChild(tokensSpan);
1572
+ const priceSpan = document.createElement('span');
1573
+ Object.assign(priceSpan.style, { color: CSS_COLORS.warning, fontWeight: '600' });
1574
+ priceSpan.textContent = estimate.cost;
1575
+ costDetails.appendChild(priceSpan);
1576
+ costBox.appendChild(costDetails);
1577
+ wrapper.appendChild(costBox);
1578
+ }
1579
+ // Model info
1580
+ if (state.apiKeyStatus?.model) {
1581
+ const modelDiv = document.createElement('div');
1582
+ Object.assign(modelDiv.style, {
1583
+ color: CSS_COLORS.textMuted,
1584
+ fontSize: '0.6875rem',
1585
+ marginTop: '12px',
1586
+ });
1587
+ modelDiv.textContent = `Model: ${state.apiKeyStatus.model}`;
1588
+ wrapper.appendChild(modelDiv);
1589
+ }
1590
+ return wrapper;
1591
+ }
1592
+ // ============================================================================
1593
+ // Settings Popover
1594
+ // ============================================================================
1595
+ function renderSettingsPopover(state) {
1596
+ const { position, accentColor } = state.options;
1597
+ const color = CSS_COLORS.textSecondary;
1598
+ const popover = document.createElement('div');
1599
+ popover.setAttribute('data-devbar', 'true');
1600
+ // Position based on devbar position
1601
+ const isTop = position.startsWith('top');
1602
+ const isRight = position.includes('right');
1603
+ Object.assign(popover.style, {
1604
+ position: 'fixed',
1605
+ [isTop ? 'top' : 'bottom']: '70px',
1606
+ [isRight ? 'right' : 'left']: isRight ? '16px' : '80px',
1607
+ zIndex: '10003',
1608
+ backgroundColor: 'var(--devbar-color-bg-elevated)',
1609
+ border: `1px solid ${accentColor}`,
1610
+ borderRadius: '8px',
1611
+ boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${accentColor}33`,
1612
+ backdropFilter: 'blur(8px)',
1613
+ WebkitBackdropFilter: 'blur(8px)',
1614
+ minWidth: '240px',
1615
+ maxWidth: '280px',
1616
+ maxHeight: 'calc(100vh - 100px)',
1617
+ overflowY: 'auto',
1618
+ fontFamily: FONT_MONO,
1619
+ });
1620
+ // Header
1621
+ const header = document.createElement('div');
1622
+ Object.assign(header.style, {
1623
+ display: 'flex',
1624
+ alignItems: 'center',
1625
+ justifyContent: 'space-between',
1626
+ padding: '10px 14px',
1627
+ borderBottom: `1px solid ${accentColor}30`,
1628
+ position: 'sticky',
1629
+ top: '0',
1630
+ backgroundColor: 'var(--devbar-color-bg-elevated)',
1631
+ zIndex: '1',
1632
+ });
1633
+ const title = document.createElement('span');
1634
+ Object.assign(title.style, { color: accentColor, fontSize: '0.75rem', fontWeight: '600' });
1635
+ title.textContent = 'Settings';
1636
+ header.appendChild(title);
1637
+ const closeBtn = createStyledButton({
1638
+ color: CSS_COLORS.textMuted,
1639
+ text: '\u00D7',
1640
+ padding: '2px 6px',
1641
+ fontSize: '0.875rem',
1642
+ });
1643
+ closeBtn.style.border = 'none';
1644
+ closeBtn.onclick = () => {
1645
+ state.showSettingsPopover = false;
1646
+ state.render();
1647
+ };
1648
+ header.appendChild(closeBtn);
1649
+ popover.appendChild(header);
1650
+ // ========== THEME SECTION ==========
1651
+ const themeSection = createSettingsSection('Theme');
1652
+ const themeOptions = document.createElement('div');
1653
+ Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
1654
+ const themeModes = ['system', 'dark', 'light'];
1655
+ themeModes.forEach((mode) => {
1656
+ const btn = document.createElement('button');
1657
+ const isActive = state.themeMode === mode;
1658
+ Object.assign(btn.style, {
1659
+ padding: '4px 10px',
1660
+ backgroundColor: isActive ? `${accentColor}20` : 'transparent',
1661
+ border: `1px solid ${isActive ? accentColor : `${color}40`}`,
1662
+ borderRadius: '4px',
1663
+ color: isActive ? accentColor : color,
1664
+ fontSize: '0.625rem',
1665
+ cursor: 'pointer',
1666
+ textTransform: 'capitalize',
1667
+ transition: 'all 150ms',
1668
+ });
1669
+ btn.textContent = mode;
1670
+ btn.onclick = () => {
1671
+ setThemeMode(state, mode);
1672
+ };
1673
+ themeOptions.appendChild(btn);
1674
+ });
1675
+ themeSection.appendChild(themeOptions);
1676
+ popover.appendChild(themeSection);
1677
+ // ========== DISPLAY SECTION ==========
1678
+ const displaySection = createSettingsSection('Display');
1679
+ // Position mini-map selector
1680
+ const positionRow = document.createElement('div');
1681
+ Object.assign(positionRow.style, { marginBottom: '10px' });
1682
+ const posLabel = document.createElement('div');
1683
+ Object.assign(posLabel.style, {
1684
+ color: CSS_COLORS.text,
1685
+ fontSize: '0.6875rem',
1686
+ marginBottom: '6px',
1687
+ });
1688
+ posLabel.textContent = 'Position';
1689
+ positionRow.appendChild(posLabel);
1690
+ // Mini-map container (represents screen with ~16:10 aspect ratio)
1691
+ const miniMap = document.createElement('div');
1692
+ Object.assign(miniMap.style, {
1693
+ position: 'relative',
1694
+ width: '100%',
1695
+ height: '70px',
1696
+ backgroundColor: 'var(--devbar-color-bg-input)',
1697
+ border: `1px solid ${color}30`,
1698
+ borderRadius: '4px',
1699
+ });
1700
+ const positionConfigs = [
1701
+ { value: 'top-left', style: { top: '6px', left: '6px' }, title: 'Top Left' },
1702
+ { value: 'top-right', style: { top: '6px', right: '6px' }, title: 'Top Right' },
1703
+ { value: 'bottom-left', style: { bottom: '6px', left: '6px' }, title: 'Bottom Left' },
1704
+ { value: 'bottom-right', style: { bottom: '6px', right: '6px' }, title: 'Bottom Right' },
1705
+ {
1706
+ value: 'bottom-center',
1707
+ style: { bottom: '6px', left: '50%', transform: 'translateX(-50%)' },
1708
+ title: 'Bottom Center',
1709
+ },
1710
+ ];
1711
+ positionConfigs.forEach(({ value, style, title: posTitle }) => {
1712
+ const indicator = document.createElement('button');
1713
+ indicator.setAttribute('data-position', value);
1714
+ const isActive = state.options.position === value;
1715
+ Object.assign(indicator.style, {
1716
+ position: 'absolute',
1717
+ width: '24px',
1718
+ height: '6px',
1719
+ backgroundColor: isActive ? accentColor : CSS_COLORS.textMuted,
1720
+ border: `1px solid ${isActive ? accentColor : CSS_COLORS.textMuted}`,
1721
+ borderRadius: '2px',
1722
+ cursor: 'pointer',
1723
+ padding: '0',
1724
+ transition: 'all 150ms',
1725
+ boxShadow: isActive ? `0 0 8px ${accentColor}60` : 'none',
1726
+ opacity: isActive ? '1' : '0.5',
1727
+ ...style,
1728
+ });
1729
+ indicator.title = posTitle;
1730
+ indicator.onclick = () => {
1731
+ state.options.position = value;
1732
+ state.settingsManager.saveSettings({ position: value });
1733
+ state.render();
1734
+ };
1735
+ // Hover effect
1736
+ indicator.onmouseenter = () => {
1737
+ if (!isActive) {
1738
+ indicator.style.backgroundColor = accentColor;
1739
+ indicator.style.borderColor = accentColor;
1740
+ indicator.style.boxShadow = `0 0 6px ${accentColor}40`;
1741
+ indicator.style.opacity = '1';
1742
+ }
1743
+ };
1744
+ indicator.onmouseleave = () => {
1745
+ if (!isActive) {
1746
+ indicator.style.backgroundColor = CSS_COLORS.textMuted;
1747
+ indicator.style.borderColor = CSS_COLORS.textMuted;
1748
+ indicator.style.boxShadow = 'none';
1749
+ indicator.style.opacity = '0.5';
1750
+ }
1751
+ };
1752
+ miniMap.appendChild(indicator);
1753
+ });
1754
+ positionRow.appendChild(miniMap);
1755
+ displaySection.appendChild(positionRow);
1756
+ // Compact mode toggle
1757
+ displaySection.appendChild(createToggleRow('Compact Mode', state.compactMode, accentColor, () => {
1758
+ state.toggleCompactMode();
1759
+ }));
1760
+ // Keyboard shortcut hint
1761
+ const shortcutHint = document.createElement('div');
1762
+ Object.assign(shortcutHint.style, {
1763
+ color: CSS_COLORS.textMuted,
1764
+ fontSize: '0.5625rem',
1765
+ marginTop: '2px',
1766
+ marginBottom: '8px',
1767
+ });
1768
+ shortcutHint.textContent = 'Keyboard: Cmd or Ctrl+Shift+M';
1769
+ displaySection.appendChild(shortcutHint);
1770
+ // Accent color
1771
+ const accentRow = document.createElement('div');
1772
+ Object.assign(accentRow.style, { marginBottom: '6px' });
1773
+ const accentLabel = document.createElement('div');
1774
+ Object.assign(accentLabel.style, {
1775
+ color: CSS_COLORS.text,
1776
+ fontSize: '0.6875rem',
1777
+ marginBottom: '6px',
1778
+ });
1779
+ accentLabel.textContent = 'Accent Color';
1780
+ accentRow.appendChild(accentLabel);
1781
+ const colorSwatches = document.createElement('div');
1782
+ Object.assign(colorSwatches.style, {
1783
+ display: 'flex',
1784
+ gap: '6px',
1785
+ flexWrap: 'wrap',
1786
+ });
1787
+ ACCENT_COLOR_PRESETS.forEach(({ name, value }) => {
1788
+ const swatch = document.createElement('button');
1789
+ const isActive = state.options.accentColor === value;
1790
+ Object.assign(swatch.style, {
1791
+ width: '24px',
1792
+ height: '24px',
1793
+ borderRadius: '50%',
1794
+ backgroundColor: value,
1795
+ border: isActive ? '2px solid #fff' : '2px solid transparent',
1796
+ cursor: 'pointer',
1797
+ transition: 'all 150ms',
1798
+ boxShadow: isActive ? `0 0 8px ${value}` : 'none',
1799
+ });
1800
+ swatch.title = name;
1801
+ swatch.onclick = () => {
1802
+ state.options.accentColor = value;
1803
+ state.settingsManager.saveSettings({ accentColor: value });
1804
+ state.render();
1805
+ };
1806
+ colorSwatches.appendChild(swatch);
1807
+ });
1808
+ accentRow.appendChild(colorSwatches);
1809
+ displaySection.appendChild(accentRow);
1810
+ popover.appendChild(displaySection);
1811
+ // ========== FEATURES SECTION ==========
1812
+ const featuresSection = createSettingsSection('Features');
1813
+ featuresSection.appendChild(createToggleRow('Screenshot Button', state.options.showScreenshot, accentColor, () => {
1814
+ state.options.showScreenshot = !state.options.showScreenshot;
1815
+ state.settingsManager.saveSettings({ showScreenshot: state.options.showScreenshot });
1816
+ state.render();
1817
+ }));
1818
+ featuresSection.appendChild(createToggleRow('Console Badges', state.options.showConsoleBadges, accentColor, () => {
1819
+ state.options.showConsoleBadges = !state.options.showConsoleBadges;
1820
+ state.settingsManager.saveSettings({ showConsoleBadges: state.options.showConsoleBadges });
1821
+ state.render();
1822
+ }));
1823
+ featuresSection.appendChild(createToggleRow('Tooltips', state.options.showTooltips, accentColor, () => {
1824
+ state.options.showTooltips = !state.options.showTooltips;
1825
+ state.settingsManager.saveSettings({ showTooltips: state.options.showTooltips });
1826
+ state.render();
1827
+ }));
1828
+ popover.appendChild(featuresSection);
1829
+ // ========== METRICS SECTION ==========
1830
+ const metricsSection = createSettingsSection('Metrics');
1831
+ const metricsToggles = [
1832
+ { key: 'breakpoint', label: 'Breakpoint' },
1833
+ { key: 'fcp', label: 'FCP' },
1834
+ { key: 'lcp', label: 'LCP' },
1835
+ { key: 'cls', label: 'CLS' },
1836
+ { key: 'inp', label: 'INP' },
1837
+ { key: 'pageSize', label: 'Page Size' },
1838
+ ];
1839
+ metricsToggles.forEach(({ key, label }) => {
1840
+ const currentValue = state.options.showMetrics[key] ?? true;
1841
+ metricsSection.appendChild(createToggleRow(label, currentValue, accentColor, () => {
1842
+ state.options.showMetrics[key] = !state.options.showMetrics[key];
1843
+ state.settingsManager.saveSettings({
1844
+ showMetrics: {
1845
+ breakpoint: state.options.showMetrics.breakpoint ?? true,
1846
+ fcp: state.options.showMetrics.fcp ?? true,
1847
+ lcp: state.options.showMetrics.lcp ?? true,
1848
+ cls: state.options.showMetrics.cls ?? true,
1849
+ inp: state.options.showMetrics.inp ?? true,
1850
+ pageSize: state.options.showMetrics.pageSize ?? true,
1851
+ },
1852
+ });
1853
+ state.render();
1854
+ }));
1855
+ });
1856
+ popover.appendChild(metricsSection);
1857
+ // ========== RESET SECTION ==========
1858
+ const resetSection = document.createElement('div');
1859
+ Object.assign(resetSection.style, {
1860
+ padding: '10px 14px',
1861
+ borderTop: `1px solid ${color}20`,
1862
+ });
1863
+ const resetBtn = createStyledButton({
1864
+ color: CSS_COLORS.textMuted,
1865
+ text: 'Reset to Defaults',
1866
+ padding: '6px 12px',
1867
+ fontSize: '0.625rem',
1868
+ });
1869
+ Object.assign(resetBtn.style, {
1870
+ width: '100%',
1871
+ justifyContent: 'center',
1872
+ });
1873
+ resetBtn.onclick = () => {
1874
+ state.settingsManager.resetToDefaults();
1875
+ const defaults = DEFAULT_SETTINGS;
1876
+ state.applySettings(defaults);
1877
+ };
1878
+ resetSection.appendChild(resetBtn);
1879
+ popover.appendChild(resetSection);
1880
+ state.overlayElement = popover;
1881
+ document.body.appendChild(popover);
1882
+ }
1883
+ // ============================================================================
1884
+ // Settings UI Helpers
1885
+ // ============================================================================
1886
+ function createSettingsSection(title, hasBorder = true) {
1887
+ const color = CSS_COLORS.textSecondary;
1888
+ const section = document.createElement('div');
1889
+ Object.assign(section.style, {
1890
+ padding: '10px 14px',
1891
+ borderBottom: hasBorder ? `1px solid ${color}20` : 'none',
1892
+ });
1893
+ const sectionTitle = document.createElement('div');
1894
+ Object.assign(sectionTitle.style, {
1895
+ color,
1896
+ fontSize: '0.625rem',
1897
+ textTransform: 'uppercase',
1898
+ letterSpacing: '0.1em',
1899
+ marginBottom: '8px',
1900
+ });
1901
+ sectionTitle.textContent = title;
1902
+ section.appendChild(sectionTitle);
1903
+ return section;
1904
+ }
1905
+ function createToggleRow(label, checked, accentColor, onChange) {
1906
+ const row = document.createElement('div');
1907
+ Object.assign(row.style, {
1908
+ display: 'flex',
1909
+ alignItems: 'center',
1910
+ justifyContent: 'space-between',
1911
+ marginBottom: '6px',
1912
+ });
1913
+ const labelEl = document.createElement('span');
1914
+ Object.assign(labelEl.style, { color: CSS_COLORS.text, fontSize: '0.6875rem' });
1915
+ labelEl.textContent = label;
1916
+ row.appendChild(labelEl);
1917
+ const toggle = document.createElement('button');
1918
+ Object.assign(toggle.style, {
1919
+ width: '32px',
1920
+ height: '18px',
1921
+ borderRadius: '9px',
1922
+ border: `1px solid ${checked ? accentColor : CSS_COLORS.border}`,
1923
+ backgroundColor: checked ? accentColor : CSS_COLORS.bgInput,
1924
+ position: 'relative',
1925
+ cursor: 'pointer',
1926
+ transition: 'all 150ms',
1927
+ flexShrink: '0',
1928
+ boxSizing: 'border-box',
1929
+ });
1930
+ const knob = document.createElement('span');
1931
+ Object.assign(knob.style, {
1932
+ position: 'absolute',
1933
+ top: '2px',
1934
+ left: checked ? '14px' : '2px',
1935
+ width: '12px',
1936
+ height: '12px',
1937
+ borderRadius: '50%',
1938
+ backgroundColor: checked ? '#fff' : CSS_COLORS.textMuted,
1939
+ boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
1940
+ transition: 'left 150ms, background-color 150ms',
1941
+ });
1942
+ toggle.appendChild(knob);
1943
+ toggle.onclick = onChange;
1944
+ row.appendChild(toggle);
1945
+ return row;
1946
+ }
1947
+ //# sourceMappingURL=rendering.js.map