@ytspar/devbar 1.0.0 → 1.1.0

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.
@@ -7,8 +7,8 @@
7
7
  import { BUTTON_COLORS, CATEGORY_COLORS, CSS_COLORS, FONT_MONO, TAILWIND_BREAKPOINTS, } from '../constants.js';
8
8
  import { extractDocumentOutline, outlineToMarkdown } from '../outline.js';
9
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';
10
+ import { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, resolveSaveLocation } from '../settings.js';
11
+ import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createCloseButton, createStyledButton, createSvgIcon, getButtonStyles, } from '../ui/index.js';
12
12
  import { getResponsiveMetricVisibility } from './performance.js';
13
13
  import { calculateCostEstimate, closeDesignReviewConfirm, copyPathToClipboard, handleDocumentOutline, handlePageSchema, handleSaveConsoleLogs, handleSaveOutline, handleSaveSchema, proceedWithDesignReview, showDesignReviewConfirmation, } from './screenshot.js';
14
14
  import { setThemeMode } from './theme.js';
@@ -25,6 +25,38 @@ function captureDotPosition(state, element) {
25
25
  bottom: window.innerHeight - (rect.top + rect.height / 2),
26
26
  };
27
27
  }
28
+ /**
29
+ * Create the connection indicator (outer wrapper + inner colored dot).
30
+ * The caller is responsible for attaching tooltip and click handlers, since
31
+ * those differ between compact and expanded modes.
32
+ */
33
+ function createConnectionIndicator(state) {
34
+ const connIndicator = document.createElement('span');
35
+ connIndicator.className = 'devbar-clickable';
36
+ Object.assign(connIndicator.style, {
37
+ width: '12px',
38
+ height: '12px',
39
+ borderRadius: '50%',
40
+ backgroundColor: 'transparent',
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ cursor: 'pointer',
45
+ flexShrink: '0',
46
+ });
47
+ const connDot = document.createElement('span');
48
+ connDot.className = 'devbar-conn-dot';
49
+ Object.assign(connDot.style, {
50
+ width: '6px',
51
+ height: '6px',
52
+ borderRadius: '50%',
53
+ backgroundColor: state.sweetlinkConnected ? CSS_COLORS.primary : CSS_COLORS.textMuted,
54
+ boxShadow: state.sweetlinkConnected ? `0 0 6px ${CSS_COLORS.primary}` : 'none',
55
+ transition: 'all 300ms',
56
+ });
57
+ connIndicator.appendChild(connDot);
58
+ return connIndicator;
59
+ }
28
60
  /**
29
61
  * Main render dispatch - creates container and delegates to appropriate renderer.
30
62
  */
@@ -66,24 +98,30 @@ function renderOverlays(state, consoleCaptureSingleton) {
66
98
  state.overlayElement.remove();
67
99
  state.overlayElement = null;
68
100
  }
69
- // Render console popup if filter is active
101
+ // Safety: only one overlay at a time. First match wins; close the rest.
70
102
  if (state.consoleFilter) {
103
+ state.showOutlineModal = false;
104
+ state.showSchemaModal = false;
105
+ state.showDesignReviewConfirm = false;
106
+ state.showSettingsPopover = false;
71
107
  renderConsolePopup(state, consoleCaptureSingleton);
72
108
  }
73
- // Render outline modal
74
- if (state.showOutlineModal) {
109
+ else if (state.showOutlineModal) {
110
+ state.showSchemaModal = false;
111
+ state.showDesignReviewConfirm = false;
112
+ state.showSettingsPopover = false;
75
113
  renderOutlineModal(state);
76
114
  }
77
- // Render schema modal
78
- if (state.showSchemaModal) {
115
+ else if (state.showSchemaModal) {
116
+ state.showDesignReviewConfirm = false;
117
+ state.showSettingsPopover = false;
79
118
  renderSchemaModal(state);
80
119
  }
81
- // Render design review confirmation modal
82
- if (state.showDesignReviewConfirm) {
120
+ else if (state.showDesignReviewConfirm) {
121
+ state.showSettingsPopover = false;
83
122
  renderDesignReviewConfirmModal(state);
84
123
  }
85
- // Render settings popover
86
- if (state.showSettingsPopover) {
124
+ else if (state.showSettingsPopover) {
87
125
  renderSettingsPopover(state);
88
126
  }
89
127
  }
@@ -107,6 +145,8 @@ function renderCollapsed(state) {
107
145
  bottom: `${state.lastDotPosition.bottom - 13}px`,
108
146
  left: `${state.lastDotPosition.left - 13}px`,
109
147
  };
148
+ // Clear after use so expand doesn't re-use stale values
149
+ state.lastDotPosition = null;
110
150
  }
111
151
  else {
112
152
  // Fallback preset positions for when no dot position was captured
@@ -244,17 +284,8 @@ function renderCompact(state) {
244
284
  fontSize: '0.6875rem',
245
285
  });
246
286
  // 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
- });
287
+ const connIndicator = createConnectionIndicator(state);
288
+ const connDot = connIndicator.querySelector('.devbar-conn-dot');
258
289
  attachTextTooltip(state, connIndicator, () => state.sweetlinkConnected ? 'Sweetlink connected' : 'Sweetlink disconnected');
259
290
  connIndicator.onclick = (e) => {
260
291
  e.stopPropagation();
@@ -263,15 +294,6 @@ function renderCompact(state) {
263
294
  state.debug.state('Collapsed DevBar from compact mode');
264
295
  state.render();
265
296
  };
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
297
  wrapper.appendChild(connIndicator);
276
298
  // Error badge
277
299
  if (errorCount > 0) {
@@ -422,19 +444,7 @@ function renderExpanded(state, customControls) {
422
444
  lineHeight: '1rem',
423
445
  });
424
446
  // 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
- });
447
+ const connIndicator = createConnectionIndicator(state);
438
448
  attachTextTooltip(state, connIndicator, () => state.sweetlinkConnected
439
449
  ? 'Sweetlink connected (click to minimize)'
440
450
  : 'Sweetlink disconnected (click to minimize)');
@@ -445,16 +455,6 @@ function renderExpanded(state, customControls) {
445
455
  state.debug.state('Collapsed DevBar (connection dot click)');
446
456
  state.render();
447
457
  };
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
458
  // Status row wrapper - keeps connection dot, info, and badges together
459
459
  const statusRow = document.createElement('div');
460
460
  statusRow.className = 'devbar-status';
@@ -719,6 +719,7 @@ function createConsoleBadge(state, type, count, color) {
719
719
  state.consoleFilter = state.consoleFilter === type ? null : type;
720
720
  state.showOutlineModal = false;
721
721
  state.showSchemaModal = false;
722
+ state.showSettingsPopover = false;
722
723
  state.render();
723
724
  };
724
725
  return badge;
@@ -728,8 +729,9 @@ function createScreenshotButton(state, accentColor) {
728
729
  btn.type = 'button';
729
730
  const hasSuccessState = state.copiedToClipboard || state.copiedPath || state.lastScreenshot;
730
731
  const isDisabled = state.capturing;
731
- // Grey out when not connected (save won't work, but clipboard still does)
732
- const isGreyedOut = !state.sweetlinkConnected && !hasSuccessState;
732
+ const effectiveSave = resolveSaveLocation(state.options.saveLocation, state.sweetlinkConnected);
733
+ // Grey out only when effective save is 'local' but sweetlink not connected (explicit 'local' setting)
734
+ const isGreyedOut = effectiveSave === 'local' && !state.sweetlinkConnected && !hasSuccessState;
733
735
  // Attach HTML tooltip
734
736
  attachButtonTooltip(state, btn, accentColor, (tooltip, h) => {
735
737
  if (state.copiedToClipboard) {
@@ -742,51 +744,57 @@ function createScreenshotButton(state, accentColor) {
742
744
  }
743
745
  if (state.lastScreenshot) {
744
746
  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);
747
+ const isDownloaded = screenshotPath.endsWith('downloaded');
748
+ if (isDownloaded) {
749
+ h.addSuccess('Screenshot downloaded!');
750
+ }
751
+ else {
752
+ h.addSuccess('Screenshot saved!', screenshotPath);
753
+ const copyLink = document.createElement('div');
754
+ Object.assign(copyLink.style, {
755
+ color: accentColor,
756
+ cursor: 'pointer',
757
+ fontSize: '0.625rem',
758
+ marginTop: '6px',
759
+ opacity: '0.8',
760
+ transition: 'opacity 150ms',
761
+ });
762
+ copyLink.textContent = 'copy path';
763
+ copyLink.onmouseenter = () => {
764
+ copyLink.style.opacity = '1';
765
+ };
766
+ copyLink.onmouseleave = () => {
767
+ copyLink.style.opacity = '0.8';
768
+ };
769
+ copyLink.onclick = async (e) => {
770
+ e.stopPropagation();
771
+ try {
772
+ await navigator.clipboard.writeText(screenshotPath);
773
+ copyLink.textContent = '\u2713 copied!';
774
+ copyLink.style.cursor = 'default';
775
+ copyLink.onclick = null;
776
+ }
777
+ catch {
778
+ copyLink.textContent = '\u00d7 failed to copy';
779
+ copyLink.style.color = CSS_COLORS.error;
780
+ }
781
+ };
782
+ tooltip.appendChild(copyLink);
783
+ }
776
784
  return;
777
785
  }
778
786
  h.addTitle('Screenshot');
779
- if (!state.sweetlinkConnected) {
780
- h.addSectionHeader('Actions');
787
+ h.addSectionHeader('Actions');
788
+ if (effectiveSave === 'local' && !state.sweetlinkConnected) {
781
789
  h.addShortcut('Shift+Click', 'Copy to clipboard');
782
- h.addWarning('Sweetlink not connected. Save to file unavailable.');
790
+ h.addWarning('Sweetlink not connected. Switch save method to Auto or Download.');
783
791
  }
784
792
  else {
785
- h.addSectionHeader('Actions');
786
- h.addShortcut('Click', 'Save to file');
793
+ const saveLabel = effectiveSave === 'local' ? 'Save to file' : 'Download';
794
+ h.addShortcut('Click', saveLabel);
787
795
  h.addShortcut('Shift+Click', 'Copy to clipboard');
788
796
  h.addSectionHeader('Keyboard');
789
- h.addShortcut('Cmd or Ctrl+Shift+S', 'Save');
797
+ h.addShortcut('Cmd or Ctrl+Shift+S', saveLabel);
790
798
  h.addShortcut('Cmd or Ctrl+Shift+C', 'Copy');
791
799
  }
792
800
  });
@@ -914,13 +922,14 @@ function createOutlineButton(state) {
914
922
  // Attach HTML tooltip
915
923
  attachButtonTooltip(state, btn, BUTTON_COLORS.outline, (_tooltip, h) => {
916
924
  if (state.lastOutline) {
917
- h.addSuccess('Outline saved!', state.lastOutline);
925
+ const isDownloaded = state.lastOutline.endsWith('downloaded');
926
+ h.addSuccess(isDownloaded ? 'Outline downloaded!' : 'Outline saved!', isDownloaded ? undefined : state.lastOutline);
918
927
  return;
919
928
  }
920
929
  h.addTitle('Document Outline');
921
930
  h.addDescription('View page heading structure and save as markdown.');
922
- if (!state.sweetlinkConnected) {
923
- h.addWarning('Sweetlink not connected. Save to file unavailable.');
931
+ if (state.options.saveLocation === 'local' && !state.sweetlinkConnected) {
932
+ h.addWarning('Sweetlink not connected. Switch save method to Auto or Download.');
924
933
  }
925
934
  });
926
935
  Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.outline, isActive, false));
@@ -941,13 +950,14 @@ function createSchemaButton(state) {
941
950
  // Attach HTML tooltip
942
951
  attachButtonTooltip(state, btn, BUTTON_COLORS.schema, (_tooltip, h) => {
943
952
  if (state.lastSchema) {
944
- h.addSuccess('Schema saved!', state.lastSchema);
953
+ const isDownloaded = state.lastSchema.endsWith('downloaded');
954
+ h.addSuccess(isDownloaded ? 'Schema downloaded!' : 'Schema saved!', isDownloaded ? undefined : state.lastSchema);
945
955
  return;
946
956
  }
947
957
  h.addTitle('Page Schema');
948
958
  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.');
959
+ if (state.options.saveLocation === 'local' && !state.sweetlinkConnected) {
960
+ h.addWarning('Sweetlink not connected. Switch save method to Auto or Download.');
951
961
  }
952
962
  });
953
963
  Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.schema, isActive, false));
@@ -1111,6 +1121,7 @@ function renderConsolePopup(state, consoleCaptureSingleton) {
1111
1121
  },
1112
1122
  onSave: () => handleSaveConsoleLogs(state, logs),
1113
1123
  sweetlinkConnected: state.sweetlinkConnected,
1124
+ saveLocation: state.options.saveLocation,
1114
1125
  isSaving: state.savingConsoleLogs,
1115
1126
  savedPath: state.lastConsoleLogs,
1116
1127
  });
@@ -1176,6 +1187,7 @@ function renderOutlineModal(state) {
1176
1187
  },
1177
1188
  onSave: () => handleSaveOutline(state),
1178
1189
  sweetlinkConnected: state.sweetlinkConnected,
1190
+ saveLocation: state.options.saveLocation,
1179
1191
  isSaving: state.savingOutline,
1180
1192
  savedPath: state.lastOutline,
1181
1193
  });
@@ -1261,6 +1273,7 @@ function renderSchemaModal(state) {
1261
1273
  },
1262
1274
  onSave: () => handleSaveSchema(state),
1263
1275
  sweetlinkConnected: state.sweetlinkConnected,
1276
+ saveLocation: state.options.saveLocation,
1264
1277
  isSaving: state.savingSchema,
1265
1278
  savedPath: state.lastSchema,
1266
1279
  });
@@ -1457,15 +1470,7 @@ function renderDesignReviewConfirmModal(state) {
1457
1470
  Object.assign(title.style, { color, fontSize: '0.875rem', fontWeight: '600' });
1458
1471
  title.textContent = 'AI Design Review';
1459
1472
  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);
1473
+ header.appendChild(createCloseButton(closeModal));
1469
1474
  modal.appendChild(header);
1470
1475
  // Content
1471
1476
  const content = document.createElement('div');
@@ -1494,13 +1499,7 @@ function renderDesignReviewConfirmModal(state) {
1494
1499
  padding: '14px 18px',
1495
1500
  borderTop: `1px solid ${CSS_COLORS.border}`,
1496
1501
  });
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);
1502
+ footer.appendChild(createCloseButton(closeModal, 'Cancel'));
1504
1503
  if (state.apiKeyStatus?.configured) {
1505
1504
  const proceedBtn = createStyledButton({ color, text: 'Run Review', padding: '8px 16px' });
1506
1505
  proceedBtn.style.backgroundColor = `${color}20`;
@@ -1509,6 +1508,7 @@ function renderDesignReviewConfirmModal(state) {
1509
1508
  }
1510
1509
  modal.appendChild(footer);
1511
1510
  overlay.appendChild(modal);
1511
+ state.overlayElement = overlay;
1512
1512
  document.body.appendChild(overlay);
1513
1513
  }
1514
1514
  function renderApiKeyNotConfiguredContent() {
@@ -1594,16 +1594,26 @@ function renderApiKeyConfiguredContent(state) {
1594
1594
  // ============================================================================
1595
1595
  function renderSettingsPopover(state) {
1596
1596
  const { position, accentColor } = state.options;
1597
- const color = CSS_COLORS.textSecondary;
1598
1597
  const popover = document.createElement('div');
1599
1598
  popover.setAttribute('data-devbar', 'true');
1600
- // Position based on devbar position
1599
+ popover.setAttribute('data-devbar-overlay', 'true');
1600
+ // Position: centered over the devbar on desktop, centered on screen on mobile
1601
1601
  const isTop = position.startsWith('top');
1602
- const isRight = position.includes('right');
1602
+ const popoverWidth = 480;
1603
+ const edgePad = 16;
1604
+ let leftPx;
1605
+ if (state.container && window.innerWidth > 640) {
1606
+ const barRect = state.container.getBoundingClientRect();
1607
+ const barCenter = barRect.left + barRect.width / 2;
1608
+ leftPx = Math.max(edgePad, Math.min(barCenter - popoverWidth / 2, window.innerWidth - popoverWidth - edgePad));
1609
+ }
1610
+ else {
1611
+ leftPx = Math.max(edgePad, (window.innerWidth - popoverWidth) / 2);
1612
+ }
1603
1613
  Object.assign(popover.style, {
1604
1614
  position: 'fixed',
1605
1615
  [isTop ? 'top' : 'bottom']: '70px',
1606
- [isRight ? 'right' : 'left']: isRight ? '16px' : '80px',
1616
+ left: `${leftPx}px`,
1607
1617
  zIndex: '10003',
1608
1618
  backgroundColor: 'var(--devbar-color-bg-elevated)',
1609
1619
  border: `1px solid ${accentColor}`,
@@ -1611,13 +1621,42 @@ function renderSettingsPopover(state) {
1611
1621
  boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${accentColor}33`,
1612
1622
  backdropFilter: 'blur(8px)',
1613
1623
  WebkitBackdropFilter: 'blur(8px)',
1614
- minWidth: '240px',
1615
- maxWidth: '280px',
1624
+ width: `${popoverWidth}px`,
1625
+ maxWidth: 'calc(100vw - 32px)',
1616
1626
  maxHeight: 'calc(100vh - 100px)',
1617
1627
  overflowY: 'auto',
1618
1628
  fontFamily: FONT_MONO,
1619
1629
  });
1620
- // Header
1630
+ popover.appendChild(createSettingsHeader(state));
1631
+ // Two-column grid for settings sections (collapses to 1 column on mobile via CSS)
1632
+ const grid = document.createElement('div');
1633
+ grid.className = 'devbar-settings-grid';
1634
+ Object.assign(grid.style, {
1635
+ display: 'grid',
1636
+ gridTemplateColumns: '1fr 1fr',
1637
+ });
1638
+ // Left column: Theme + Display
1639
+ const color = CSS_COLORS.textSecondary;
1640
+ const leftCol = document.createElement('div');
1641
+ Object.assign(leftCol.style, { borderRight: `1px solid ${color}20` });
1642
+ leftCol.appendChild(createThemeSection(state));
1643
+ leftCol.appendChild(createDisplaySection(state));
1644
+ grid.appendChild(leftCol);
1645
+ // Right column: Features + Metrics
1646
+ const rightCol = document.createElement('div');
1647
+ rightCol.appendChild(createFeaturesSection(state));
1648
+ rightCol.appendChild(createMetricsSection(state));
1649
+ grid.appendChild(rightCol);
1650
+ popover.appendChild(grid);
1651
+ popover.appendChild(createResetSection(state));
1652
+ state.overlayElement = popover;
1653
+ document.body.appendChild(popover);
1654
+ }
1655
+ // ============================================================================
1656
+ // Settings Popover Section Builders
1657
+ // ============================================================================
1658
+ function createSettingsHeader(state) {
1659
+ const { accentColor } = state.options;
1621
1660
  const header = document.createElement('div');
1622
1661
  Object.assign(header.style, {
1623
1662
  display: 'flex',
@@ -1634,20 +1673,15 @@ function renderSettingsPopover(state) {
1634
1673
  Object.assign(title.style, { color: accentColor, fontSize: '0.75rem', fontWeight: '600' });
1635
1674
  title.textContent = 'Settings';
1636
1675
  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 = () => {
1676
+ header.appendChild(createCloseButton(() => {
1645
1677
  state.showSettingsPopover = false;
1646
1678
  state.render();
1647
- };
1648
- header.appendChild(closeBtn);
1649
- popover.appendChild(header);
1650
- // ========== THEME SECTION ==========
1679
+ }));
1680
+ return header;
1681
+ }
1682
+ function createThemeSection(state) {
1683
+ const { accentColor } = state.options;
1684
+ const color = CSS_COLORS.textSecondary;
1651
1685
  const themeSection = createSettingsSection('Theme');
1652
1686
  const themeOptions = document.createElement('div');
1653
1687
  Object.assign(themeOptions.style, { display: 'flex', gap: '6px' });
@@ -1673,8 +1707,11 @@ function renderSettingsPopover(state) {
1673
1707
  themeOptions.appendChild(btn);
1674
1708
  });
1675
1709
  themeSection.appendChild(themeOptions);
1676
- popover.appendChild(themeSection);
1677
- // ========== DISPLAY SECTION ==========
1710
+ return themeSection;
1711
+ }
1712
+ function createDisplaySection(state) {
1713
+ const { accentColor } = state.options;
1714
+ const color = CSS_COLORS.textSecondary;
1678
1715
  const displaySection = createSettingsSection('Display');
1679
1716
  // Position mini-map selector
1680
1717
  const positionRow = document.createElement('div');
@@ -1697,6 +1734,7 @@ function renderSettingsPopover(state) {
1697
1734
  border: `1px solid ${color}30`,
1698
1735
  borderRadius: '4px',
1699
1736
  });
1737
+ // Position indicator styles - rectangular bars representing DevBar
1700
1738
  const positionConfigs = [
1701
1739
  { value: 'top-left', style: { top: '6px', left: '6px' }, title: 'Top Left' },
1702
1740
  { value: 'top-right', style: { top: '6px', right: '6px' }, title: 'Top Right' },
@@ -1807,8 +1845,11 @@ function renderSettingsPopover(state) {
1807
1845
  });
1808
1846
  accentRow.appendChild(colorSwatches);
1809
1847
  displaySection.appendChild(accentRow);
1810
- popover.appendChild(displaySection);
1811
- // ========== FEATURES SECTION ==========
1848
+ return displaySection;
1849
+ }
1850
+ function createFeaturesSection(state) {
1851
+ const { accentColor } = state.options;
1852
+ const color = CSS_COLORS.textSecondary;
1812
1853
  const featuresSection = createSettingsSection('Features');
1813
1854
  featuresSection.appendChild(createToggleRow('Screenshot Button', state.options.showScreenshot, accentColor, () => {
1814
1855
  state.options.showScreenshot = !state.options.showScreenshot;
@@ -1825,8 +1866,58 @@ function renderSettingsPopover(state) {
1825
1866
  state.settingsManager.saveSettings({ showTooltips: state.options.showTooltips });
1826
1867
  state.render();
1827
1868
  }));
1828
- popover.appendChild(featuresSection);
1829
- // ========== METRICS SECTION ==========
1869
+ // Save location selector
1870
+ const saveLocRow = document.createElement('div');
1871
+ Object.assign(saveLocRow.style, { marginBottom: '6px' });
1872
+ const saveLocLabel = document.createElement('div');
1873
+ Object.assign(saveLocLabel.style, {
1874
+ color: CSS_COLORS.text,
1875
+ fontSize: '0.6875rem',
1876
+ marginBottom: '6px',
1877
+ });
1878
+ saveLocLabel.textContent = 'Save Method';
1879
+ saveLocRow.appendChild(saveLocLabel);
1880
+ const saveLocOptions = document.createElement('div');
1881
+ Object.assign(saveLocOptions.style, { display: 'flex', gap: '6px' });
1882
+ const saveLocChoices = [
1883
+ { value: 'auto', label: 'Auto' },
1884
+ { value: 'download', label: 'Download' },
1885
+ { value: 'local', label: 'Local' },
1886
+ ];
1887
+ saveLocChoices.forEach(({ value, label }) => {
1888
+ const btn = document.createElement('button');
1889
+ const isActive = state.options.saveLocation === value;
1890
+ const isLocalDisabled = value === 'local' && !state.sweetlinkConnected;
1891
+ Object.assign(btn.style, {
1892
+ padding: '4px 10px',
1893
+ backgroundColor: isActive ? `${accentColor}20` : 'transparent',
1894
+ border: `1px solid ${isActive ? accentColor : `${color}40`}`,
1895
+ borderRadius: '4px',
1896
+ color: isActive ? accentColor : color,
1897
+ fontSize: '0.625rem',
1898
+ cursor: isLocalDisabled ? 'not-allowed' : 'pointer',
1899
+ transition: 'all 150ms',
1900
+ opacity: isLocalDisabled ? '0.5' : '1',
1901
+ });
1902
+ btn.textContent = label;
1903
+ if (isLocalDisabled) {
1904
+ btn.title = 'Sweetlink not connected';
1905
+ }
1906
+ btn.onclick = () => {
1907
+ if (isLocalDisabled)
1908
+ return;
1909
+ state.options.saveLocation = value;
1910
+ state.settingsManager.saveSettings({ saveLocation: value });
1911
+ state.render();
1912
+ };
1913
+ saveLocOptions.appendChild(btn);
1914
+ });
1915
+ saveLocRow.appendChild(saveLocOptions);
1916
+ featuresSection.appendChild(saveLocRow);
1917
+ return featuresSection;
1918
+ }
1919
+ function createMetricsSection(state) {
1920
+ const { accentColor } = state.options;
1830
1921
  const metricsSection = createSettingsSection('Metrics');
1831
1922
  const metricsToggles = [
1832
1923
  { key: 'breakpoint', label: 'Breakpoint' },
@@ -1853,8 +1944,10 @@ function renderSettingsPopover(state) {
1853
1944
  state.render();
1854
1945
  }));
1855
1946
  });
1856
- popover.appendChild(metricsSection);
1857
- // ========== RESET SECTION ==========
1947
+ return metricsSection;
1948
+ }
1949
+ function createResetSection(state) {
1950
+ const color = CSS_COLORS.textSecondary;
1858
1951
  const resetSection = document.createElement('div');
1859
1952
  Object.assign(resetSection.style, {
1860
1953
  padding: '10px 14px',
@@ -1876,9 +1969,7 @@ function renderSettingsPopover(state) {
1876
1969
  state.applySettings(defaults);
1877
1970
  };
1878
1971
  resetSection.appendChild(resetBtn);
1879
- popover.appendChild(resetSection);
1880
- state.overlayElement = popover;
1881
- document.body.appendChild(popover);
1972
+ return resetSection;
1882
1973
  }
1883
1974
  // ============================================================================
1884
1975
  // Settings UI Helpers