From db78ad42694d1b5eaeb89d9e9db28bff5640ed2f Mon Sep 17 00:00:00 2001 From: Jonathan Martin Date: Sat, 18 Apr 2026 21:23:23 -0500 Subject: [PATCH 01/15] Refresh settings and onboarding UI --- README.md | 4 - options/controllers/import-controller.js | 12 +- options/controllers/token-controller.js | 14 +- options/options.css | 525 ++++++++++++------ options/options.html | 100 ++-- options/options.js | 14 +- options/views/repository-list-view.js | 12 +- popup/controllers/activity-controller.js | 8 +- popup/controllers/keyboard-controller.js | 2 +- popup/controllers/theme-controller.js | 22 +- popup/popup.css | 38 +- popup/popup.html | 8 +- popup/popup.js | 7 +- popup/views/onboarding-view.js | 45 +- screenshots/settings-page.png | Bin 153683 -> 0 bytes shared/error-handler.js | 16 +- shared/offline-manager.js | 10 +- shared/ui/notification-manager.js | 8 - tests/error-handler.test.js | 24 +- tests/import-controller.test.js | 26 +- tests/offline.test.js | 10 +- tests/onboarding.test.js | 40 +- tests/options-interactions.test.js | 6 +- tests/options-main.test.js | 24 +- tests/options-token-controller.test.js | 16 +- tests/phase1.test.js | 21 +- tests/popup-activity-controller-extra.test.js | 2 +- tests/popup-activity-controller.test.js | 2 +- tests/popup-bootstrap.test.js | 12 +- tests/popup-keyboard-controller.test.js | 4 +- tests/repository-list-view.test.js | 16 +- 31 files changed, 599 insertions(+), 449 deletions(-) delete mode 100644 screenshots/settings-page.png diff --git a/README.md b/README.md index 79f05fe..acd9019 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,6 @@ Settings are split into a few practical jobs: - Tune filters, notifications, refresh interval, snooze behavior, and item expiry - Change theme/color theme, export or import settings, and enable advanced repo-limit options -
- Settings page for configuring repositories -
- ## Typical Workflow Here's what using the extension looks like day-to-day: diff --git a/options/controllers/import-controller.js b/options/controllers/import-controller.js index 0cea908..f122656 100644 --- a/options/controllers/import-controller.js +++ b/options/controllers/import-controller.js @@ -84,11 +84,10 @@ export async function openImportModal(type, watchedRepos) { importModalState.previousFocusElement = document.activeElement; modal.classList.add('show'); - document.getElementById('importLoadingState').style.display = 'flex'; + document.getElementById('importLoadingState').classList.remove('hidden'); const reposList = document.getElementById('importReposList'); reposList.classList.add('hidden'); - reposList.style.display = 'none'; - document.getElementById('importErrorState').style.display = 'none'; + document.getElementById('importErrorState').classList.add('hidden'); setupModalFocusTrap(modal); @@ -113,14 +112,13 @@ export async function openImportModal(type, watchedRepos) { importModalState.filteredRepos = [...importModalState.repos]; - document.getElementById('importLoadingState').style.display = 'none'; + document.getElementById('importLoadingState').classList.add('hidden'); const reposList = document.getElementById('importReposList'); reposList.classList.remove('hidden'); - reposList.style.display = 'block'; renderImportReposList(); } catch (error) { - document.getElementById('importLoadingState').style.display = 'none'; - document.getElementById('importErrorState').style.display = 'block'; + document.getElementById('importLoadingState').classList.add('hidden'); + document.getElementById('importErrorState').classList.remove('hidden'); document.getElementById('importErrorMessage').textContent = error.message || 'Failed to fetch repositories'; } } diff --git a/options/controllers/token-controller.js b/options/controllers/token-controller.js index 2078be7..5ca198e 100644 --- a/options/controllers/token-controller.js +++ b/options/controllers/token-controller.js @@ -24,7 +24,6 @@ function setRepoAccessState(isConnected) { ? 'Add repositories to monitor (npm package, owner/repo, or GitHub URL)' : 'Connect GitHub above to start adding repositories'; importSection.classList.toggle('hidden', !isConnected); - importSection.style.display = isConnected ? 'block' : 'none'; } function setDeviceCode(userCode = '') { @@ -37,7 +36,6 @@ function setDeviceCode(userCode = '') { deviceCodeInput.value = userCode; deviceCodeSection.classList.toggle('hidden', !userCode); - deviceCodeSection.style.display = userCode ? 'flex' : 'none'; } function setHelpText(message = '') { @@ -52,14 +50,14 @@ function setHelpText(message = '') { function getHelpText({ isConnected = false, isWaiting = false } = {}) { if (isWaiting) { - return 'Approve access in the GitHub tab, then return here. If GitHub asks for a code, use the one shown below.'; + return 'Approve access in the GitHub tab, then return here. We copied the code to your clipboard in case GitHub asks for it.'; } if (isConnected) { return 'Reconnect any time if your session expires or if you want to switch accounts.'; } - return 'We\'ll open GitHub in a new tab. Approve access there, then come back here and DevWatch will finish connecting.'; + return 'We\'ll open GitHub in a new tab and copy your code to the clipboard so it\'s ready if GitHub asks for it. Then come back here and DevWatch will finish connecting.'; } function setStatus(message = '', statusClass = '') { @@ -92,7 +90,7 @@ export function applyStoredConnection(authSession, options = {}) { } if (clearBtn) { - clearBtn.style.display = isConnected ? 'block' : 'none'; + clearBtn.classList.toggle('hidden', !isConnected); } if (options.statusMessage) { @@ -149,7 +147,7 @@ export async function connectGitHub(_toastManager) { setRepoAccessState(Boolean(previousSession?.accessToken)); setStatus('Starting GitHub sign-in...', 'checking'); - setHelpText('We\'re opening GitHub so you can approve access. Come back here as soon as GitHub says the connection is ready.'); + setHelpText('We\'re opening GitHub and copying your code to the clipboard so it\'s ready if GitHub asks for it. Come back here as soon as GitHub says the connection is ready.'); try { const result = await completeGitHubDeviceAuth({ @@ -160,8 +158,8 @@ export async function connectGitHub(_toastManager) { : false; setStatus( copied - ? `Code ${userCode} copied to clipboard — paste it on the GitHub page that opens.` - : `Enter ${userCode} on GitHub to finish connecting.`, + ? `Code ${userCode} copied to clipboard in case GitHub asks for it.` + : `If GitHub asks for a code, enter ${userCode}.`, 'checking' ); setHelpText(getHelpText({ isWaiting: true })); diff --git a/options/options.css b/options/options.css index ff59892..c3d9679 100644 --- a/options/options.css +++ b/options/options.css @@ -321,22 +321,16 @@ h1 { } .section { - margin-bottom: 32px; - padding-bottom: 32px; + margin-bottom: 40px; + padding: 0 0 36px; border-bottom: 1px solid var(--border-color); scroll-margin-top: 20px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-color); - border-radius: 24px; - padding: 28px; - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08); - backdrop-filter: blur(10px); } .section:last-child { - border-bottom: 1px solid var(--border-color); + border-bottom: none; margin-bottom: 0; - padding-bottom: 32px; + padding-bottom: 0; } h2 { @@ -414,12 +408,13 @@ select:focus { } .info-box { - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border-color); - padding: 12px 16px; + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(255, 255, 255, 0.02); + border-left: 3px solid var(--border-hover); + padding: 12px 0 12px 16px; margin: 12px 0; - border-radius: 16px; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.06); + border-radius: 0 16px 16px 0; + box-shadow: none; } .info-box .help-text { @@ -574,6 +569,30 @@ button.danger:hover { border: 1px solid var(--border); } +.swatch-polar { + background: #0D9488; +} + +.swatch-graphite { + background: #6366F1; +} + +.swatch-nightfall { + background: #E87B00; +} + +.swatch-obsidian { + background: #10B981; +} + +.swatch-sand { + background: #9F1239; +} + +.swatch-terminal-ledger { + background: #D3A24B; +} + .subsection-heading { font-size: 15px; font-weight: 600; @@ -582,36 +601,184 @@ button.danger:hover { color: var(--foreground); } -/* Activity Cards Styles */ -.activity-cards { +/* Shared Settings Surfaces */ +.settings-surface { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-color); + border-radius: 18px; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, transform 0.2s ease; +} + +.settings-surface:hover { + border-color: var(--border-hover); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08); +} + +.settings-surface-header { + display: flex; + align-items: flex-start; + gap: 16px; +} + +.settings-surface-body { display: flex; flex-direction: column; - gap: 12px; - margin-top: 16px; + gap: 16px; } -.activity-card { +.settings-surface-actions { + display: flex; + gap: 24px; +} + +.settings-surface-split { display: flex; flex-direction: column; - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border-color); - border-radius: 18px; overflow: hidden; - transition: all 0.2s ease; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08); } -.activity-card:hover { - border-color: var(--link-color); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +.settings-surface-split .settings-surface-header { + gap: 12px; + padding: 16px; + background: rgba(255, 255, 255, 0.025); } -.card-header { +.settings-surface-split .settings-surface-actions { + justify-content: space-around; + padding: 12px 16px; + background: rgba(0, 0, 0, 0.04); +} + +.settings-surface-split .settings-surface-action { display: flex; - align-items: flex-start; + flex-direction: column; + align-items: center; + gap: 8px; + flex: 1; +} + +.settings-surface-inline { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 20px; +} + +.settings-surface-inline .settings-surface-header { + gap: 12px; + flex: 1; +} + +.settings-surface-inline .settings-surface-actions { + justify-content: flex-end; + padding: 0; + background: transparent; + flex-shrink: 0; +} + +.settings-surface-prominent { + display: flex; + flex-direction: column; + gap: 18px; + padding: 20px; + border-radius: 22px; + background: rgba(255, 255, 255, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 16px 28px rgba(0, 0, 0, 0.08); +} + +.settings-surface-prominent:hover { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 18px 30px rgba(0, 0, 0, 0.1); +} + +.settings-surface-nested { + display: flex; + flex-direction: column; gap: 12px; padding: 16px; +} + +.settings-surface-dashed { + border-style: dashed; + border-color: var(--border-hover); + box-shadow: none; +} + +.settings-surface-dashed:hover { + box-shadow: none; +} + +.settings-surface-danger:hover { + border-color: var(--destructive); +} + +.settings-row-group { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-color); + border-radius: 20px; + overflow: hidden; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset; +} + +.settings-row-group > .settings-row { + margin: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.settings-row-group > .settings-row:hover { background: rgba(255, 255, 255, 0.025); + transform: none; + box-shadow: none; +} + +.settings-row-group > .settings-row + .settings-row { + border-top: 1px solid var(--border-color); +} + +/* Activity Cards Styles */ +.activity-cards { + margin-top: 16px; +} + +.activity-card { + min-width: 0; +} + +.activity-cards.settings-row-group > .activity-card.settings-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 20px; + padding: 18px 20px; +} + +.activity-cards.settings-row-group .settings-surface-header, +.activity-cards.settings-row-group .settings-surface-actions { + padding: 0; + background: transparent; +} + +.activity-cards.settings-row-group .settings-surface-actions { + justify-content: flex-end; + gap: 14px; +} + +.activity-cards.settings-row-group .settings-surface-action { + min-width: 104px; + align-items: flex-start; + text-align: left; +} + +.activity-cards.settings-row-group .control-label { + text-align: left; +} + +.card-header { + min-width: 0; } .activity-icon { @@ -641,19 +808,11 @@ button.danger:hover { } .card-controls { - display: flex; - justify-content: space-around; - gap: 24px; - padding: 12px 16px; - background: rgba(0, 0, 0, 0.04); + min-width: 0; } .control-section { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - flex: 1; + min-width: 0; } .control-label { @@ -755,34 +914,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { /* Feed Management Card */ .feed-management-card { - display: flex; - justify-content: space-between; - align-items: center; - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 16px; margin-top: 16px; - transition: all 0.2s ease; -} - -.feed-management-card:hover { - border-color: var(--link-color); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.feed-management-card .card-header { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 0; - background: transparent; - flex: 1; -} - -.feed-management-card .card-controls { - padding: 0; - background: transparent; } .expiry-settings { @@ -847,11 +979,11 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { align-items: flex-start; gap: 16px; padding: 20px; - background: rgba(255, 255, 255, 0.04); + background: rgba(255, 255, 255, 0.025); border: 1px solid var(--border-color); border-radius: 18px; transition: all 0.2s ease; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08); + box-shadow: none; } .setup-step.clickable { @@ -862,8 +994,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { .setup-step:hover { border-color: var(--border-hover); - transform: translateY(-2px); - box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12); + transform: translateY(-1px); + background: rgba(255, 255, 255, 0.04); + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.06); } .step-number { @@ -903,7 +1036,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03)); border-color: var(--border-hover); padding: 24px; - box-shadow: 0 14px 34px rgba(0, 0, 0, 0.12); + box-shadow: 0 14px 30px rgba(0, 0, 0, 0.08); } .setup-step-primary:hover { @@ -1017,14 +1150,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .github-connect-panel { - display: flex; - flex-direction: column; - gap: 18px; - padding: 20px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border-color); - border-radius: 22px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 18px 34px rgba(0, 0, 0, 0.08); + min-width: 0; } .github-connect-panel-header { @@ -1045,9 +1171,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .github-connect-panel-body { - display: flex; - flex-direction: column; - gap: 16px; + min-width: 0; } .github-connect-btn { @@ -1103,13 +1227,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .device-code-card { - display: flex; - flex-direction: column; - gap: 12px; - padding: 16px; - background: rgba(255, 255, 255, 0.04); - border: 1px dashed var(--border-hover); - border-radius: 18px; + min-width: 0; } .device-code-header { @@ -1229,28 +1347,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .data-card { - display: flex; - align-items: center; - gap: 16px; - padding: 20px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border-color); - border-radius: 18px; - transition: all 0.2s ease; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08); -} - -.data-card:hover { - border-color: var(--border-hover); - box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12); -} - -.data-card.danger-card { - border-color: var(--border-color); -} - -.data-card.danger-card:hover { - border-color: var(--destructive); + min-width: 0; } .data-icon { @@ -1608,6 +1705,20 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { /* Responsive Design */ @media (max-width: 768px) { + .activity-cards.settings-row-group > .activity-card.settings-row { + grid-template-columns: 1fr; + } + + .activity-cards.settings-row-group .settings-surface-actions { + width: 100%; + justify-content: space-between; + } + + .activity-cards.settings-row-group .settings-surface-action { + min-width: 0; + flex: 1; + } + .repo-section-header { flex-wrap: wrap; } @@ -1628,29 +1739,17 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { @media (max-width: 480px) { .activity-card { - margin: 0 -8px; - border-radius: 8px; - } - - .card-header { - padding: 12px; + margin: 0; + border-radius: 0; } - .card-controls { - padding: 10px 12px; - } - - .control-section { - gap: 6px; - } - - .feed-management-card { + .settings-surface-inline { flex-direction: column; gap: 16px; align-items: flex-start; } - .feed-management-card .card-controls { + .settings-surface-inline .settings-surface-actions { width: 100%; display: flex; justify-content: flex-end; @@ -1661,6 +1760,16 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { margin-bottom: 16px; } + .repo-item { + align-items: flex-start; + flex-direction: column; + } + + .repo-actions { + width: 100%; + justify-content: flex-start; + } + .repo-section-header { flex-direction: column; gap: 12px; @@ -1720,25 +1829,30 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { .repo-list { list-style: none; margin-top: 16px; + padding: 0; } .repo-item { display: flex; justify-content: space-between; - align-items: flex-start; - padding: 16px; - border: 1px solid var(--border-color); - border-radius: 16px; - margin-bottom: 12px; - background: rgba(255, 255, 255, 0.04); + align-items: center; + padding: 18px 20px; + border: none; + border-radius: 0; + margin-bottom: 0; + background: transparent; gap: 16px; transition: all 0.15s ease; } .repo-item:hover { - border-color: var(--border-hover); - transform: translateY(-1px); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); + background: rgba(255, 255, 255, 0.025); + transform: none; + box-shadow: none; +} + +.repo-list.settings-row-group .repo-item + .repo-item { + border-top: 1px solid var(--border-color); } .repo-content { @@ -1787,13 +1901,13 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { /* Repository Section Cards */ .repo-section-card { - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border-color); + display: flex; + flex-direction: column; + gap: 18px; border-radius: 22px; padding: 24px; margin-bottom: 24px; - transition: all 0.2s ease; - box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset; } .add-repo-card { @@ -1810,11 +1924,16 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { display: flex; align-items: flex-start; gap: 16px; - margin-bottom: 20px; + margin-bottom: 0; padding-bottom: 16px; border-bottom: 1px solid var(--border-color); } +#addRepoPanel { + display: grid; + gap: 12px; +} + .repo-section-icon { width: 32px; height: 32px; @@ -1930,12 +2049,12 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { /* Collapsible Panels */ .repo-panel { - margin: 16px 0 24px 0; + margin: 0 0 16px 0; overflow: hidden; opacity: 0; max-height: 0; transition: all 0.2s ease-in-out; /* Simpler transition */ - border-radius: 18px; + border-radius: 0; } .repo-panel.show { @@ -1943,6 +2062,24 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { max-height: 300px; /* Increased max-height to accommodate import buttons */ } +.repo-toolbar.show { + padding-bottom: 14px; + border-bottom: 1px solid var(--border-color); +} + +.repo-notes { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px 20px; + margin-top: 0; + padding: 14px 0 0 16px; +} + +.repo-notes .help-text { + margin: 0; + font-size: 13px; +} + .repo-search-container { margin-top: 16px; margin-bottom: 12px; @@ -2056,7 +2193,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { display: flex; align-items: center; gap: 8px; - margin-top: 16px; + margin-top: 0; } .repo-input-label { @@ -2131,20 +2268,22 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { /* Import Repos Section */ .import-repos-section { - margin: 16px 0; - padding: 16px; - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 6px; + margin: 12px 0 0; + padding: 12px 0 0; + background: transparent; + border: none; + border-top: 1px solid var(--border-color); + border-radius: 0; } /* Import Repos Section when inside panel */ .repo-panel .import-repos-section { - margin: 16px 0 0 0; - padding: 12px 16px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 6px; + margin: 12px 0 0; + padding: 12px 0 0; + background: transparent; + border: none; + border-top: 1px solid var(--border-color); + border-radius: 0; } .import-repos-section .help-text { @@ -2511,6 +2650,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { align-items: center; gap: 8px; flex-shrink: 0; + align-self: center; + flex-wrap: wrap; + justify-content: flex-end; } .pin-btn { @@ -2622,7 +2764,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .repo-item.pinned { - border-left: 3px solid #f59e0b; + box-shadow: inset 3px 0 0 #f59e0b; background: rgba(245, 158, 11, 0.05); } @@ -2724,6 +2866,8 @@ body.dark-mode select:focus-visible { /* Current Snoozes Section */ .current-snoozes { margin-top: 24px; + padding-top: 20px; + border-top: 1px solid var(--border-color); } .current-snoozes h3 { @@ -2737,13 +2881,17 @@ body.dark-mode select:focus-visible { margin-top: 16px; } +.snoozed-repos-list.settings-row-group .snoozed-repo-item + .snoozed-repo-item { + border-top: 1px solid var(--border-color); +} + .empty-snoozes { text-align: center; - padding: 40px 20px; + padding: 28px 20px; color: var(--text-secondary); - background: var(--bg-secondary); - border: 2px dashed var(--border-color); - border-radius: 8px; + background: transparent; + border: none; + border-radius: 0; } .empty-snoozes p { @@ -2761,10 +2909,10 @@ body.dark-mode select:focus-visible { .empty-repos-card { text-align: center; padding: 48px 32px; - background: rgba(255, 255, 255, 0.04); + background: rgba(255, 255, 255, 0.02); border: 1px dashed var(--border-color); border-radius: 20px; - margin: 16px 0; + margin: 0; } .empty-repos-icon { @@ -2800,10 +2948,11 @@ body.dark-mode select:focus-visible { color: var(--text-secondary); line-height: 1.6; margin: 0; - padding: 16px; - background: rgba(255, 255, 255, 0.04); - border-radius: 16px; - border: 1px solid var(--border-color); + padding: 16px 0 0; + background: transparent; + border-radius: 0; + border: none; + border-top: 1px solid var(--border-color); text-align: left; } @@ -2812,16 +2961,15 @@ body.dark-mode select:focus-visible { align-items: center; justify-content: space-between; padding: 16px; - margin-bottom: 8px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border-color); - border-radius: 16px; + margin-bottom: 0; + background: transparent; + border: none; + border-radius: 0; transition: all 0.2s; } .snoozed-repo-item:hover { - background: var(--bg-tertiary); - border-color: var(--border-color-dark); + background: rgba(255, 255, 255, 0.025); } .snoozed-repo-info { @@ -2896,8 +3044,11 @@ body.dark-mode .snooze-expiry-warning { } .toast { - background: rgba(24, 22, 20, 0.96); - border: 1px solid var(--border-color); + position: relative; + overflow: hidden; + background: var(--card); + color: var(--foreground); + border: 1px solid var(--border); border-radius: 16px; padding: 12px 16px; box-shadow: 0 18px 32px rgba(0, 0, 0, 0.22); @@ -2958,14 +3109,14 @@ body.dark-mode .snooze-expiry-warning { .toast-message { flex: 1; - color: var(--text-primary); + color: var(--foreground); } .toast-close { flex-shrink: 0; background: none; border: none; - color: var(--text-secondary); + color: var(--muted-foreground); cursor: pointer; padding: 4px; border-radius: 10px; @@ -2978,7 +3129,25 @@ body.dark-mode .snooze-expiry-warning { .toast-close:hover { opacity: 1; - background: var(--bg-secondary); + background: var(--accent); +} + +.toast-action { + flex-shrink: 0; + background: transparent; + color: var(--foreground); + border: 1px solid var(--border); + border-radius: 10px; + padding: 6px 10px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease; +} + +.toast-action:hover { + background: var(--accent); + color: var(--accent-foreground); } /* Auto-remove toast progress bar */ @@ -2987,7 +3156,7 @@ body.dark-mode .snooze-expiry-warning { bottom: 0; left: 0; height: 3px; - background: var(--border-color); + background: var(--border); border-radius: 0 0 6px 6px; transition: width linear; } diff --git a/options/options.html b/options/options.html index 390aff8..3631d48 100644 --- a/options/options.html +++ b/options/options.html @@ -104,8 +104,8 @@

Connect GitHub

-
-
+
+

Connection status

@@ -113,11 +113,11 @@

Connect GitHub

-
+

- We'll open GitHub in a new tab. Approve access there, then come back here and DevWatch will finish connecting. + We'll open GitHub in a new tab and copy your code to the clipboard so it's ready if GitHub asks for it. Then come back here and DevWatch will finish connecting.

-
@@ -188,8 +188,8 @@

View Help & Changelog

@@ -398,19 +404,31 @@

Releases

-
-

- - Activity Filters: Enable categories to show them in your activity feed -

-

- - Notifications: Enable browser notifications (requires the category to be enabled first) -

+
+
+ How it works +

Feed visibility and browser notifications are controlled separately for each activity type.

+
+
+
+ +
+ Activity feed + Enable a category here to include new and updated items in your feed. +
+
+
+ +
+ Notifications + Browser notifications only work after the matching category is enabled in the feed first. +
+
+
From 4185cdc1a4cf7b00ff5ccfac98f8cc35c1026d6b Mon Sep 17 00:00:00 2001 From: Jonathan Martin Date: Mon, 20 Apr 2026 18:20:47 -0500 Subject: [PATCH 10/15] Make GitHub setup work before sign-in - keep GitHub auth in the current Chrome profile until disconnect/reset\n- allow public repo validation and activity checks without GitHub auth\n- reuse shared GitHub repo-source fetching across settings and onboarding --- CHANGELOG.md | 2 +- README.md | 4 +- background.js | 17 +- options/controllers/import-controller.js | 99 +-- options/controllers/token-controller.js | 69 +- options/options.css | 872 +++++++++++++++++++---- options/options.html | 125 ++-- options/options.js | 610 ++++++++++++++-- popup/popup.css | 357 ++++++---- popup/popup.html | 2 +- popup/views/activity-list-view.js | 6 +- popup/views/onboarding-view.js | 339 +++++++-- shared/github-repo-sources.js | 116 +++ shared/onboarding.js | 155 ++-- shared/repo-service.js | 31 +- shared/storage-helpers.js | 60 +- tests/background.test.js | 74 +- tests/onboarding.test.js | 258 ++++++- tests/options-interactions.test.js | 157 +++- tests/options-main.test.js | 70 +- tests/options-token-controller.test.js | 47 +- tests/options.test.js | 2 +- tests/popup-activity-list-view.test.js | 27 + tests/repo-service.test.js | 47 ++ tests/repository-validator.test.js | 24 + tests/storage-helpers.test.js | 42 +- 26 files changed, 2915 insertions(+), 697 deletions(-) create mode 100644 shared/github-repo-sources.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8340065..c64e761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Security -- GitHub sign-in sessions now stay in Chrome session storage only instead of persisting to local extension storage +- GitHub sign-in sessions now persist in local extension storage for the current Chrome profile instead of clearing at browser shutdown - Added validation for the GitHub device-flow verification URL before opening a browser tab - Tightened remote image handling for activity avatars and extension page CSP rules diff --git a/README.md b/README.md index acd9019..fd7a3b1 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Best for people who follow several repos and want one local review queue instead - Chrome or another Chromium-based browser that supports Manifest V3 extensions - A GitHub account to connect during setup - No separate hosted DevWatch account or backend service -- GitHub sign-in lasts for the current browser session and is cleared when that session ends +- GitHub sign-in persists in the current Chrome profile until you disconnect it or reset DevWatch ## Installation @@ -119,7 +119,7 @@ That said, this project has not gone through a formal accessibility audit or doc ## Privacy & Security Notes -The extension talks directly to GitHub's API and does not use a separate analytics or sync backend. It stores settings and cached activity in Chrome extension storage, while the current GitHub auth session stays in Chrome session storage so it is not persisted to disk. Legacy encrypted auth data from older builds is cleared when accessed. +The extension talks directly to GitHub's API and does not use a separate analytics or sync backend. It stores settings, cached activity, and the current GitHub auth session in Chrome extension storage for the current browser profile. Legacy encrypted auth data from older builds is cleared when accessed. - **Direct network access** - Requests go to `api.github.com` for activity checks, `github.com` for OAuth device-flow sign-in, and `registry.npmjs.org` only when you use package-name lookup - **Scoped browser permissions** - The manifest asks for `storage`, `alarms`, and `notifications` diff --git a/background.js b/background.js index d8964d3..4aafd4a 100644 --- a/background.js +++ b/background.js @@ -109,11 +109,6 @@ async function checkGitHubActivity() { 'unmutedRepos' ]); - if (!githubToken) { - console.warn('[DevWatch] No GitHub connection found. Please connect GitHub in settings.'); - return; - } - if (!watchedRepos || watchedRepos.length === 0) { console.warn('[DevWatch] No repositories being watched. Please add repos in settings.'); return; @@ -192,7 +187,9 @@ async function checkGitHubActivity() { async function fetchRepoActivity(repo, token, since, filters) { const activities = []; - const headers = createHeaders(token); + const headers = token + ? createHeaders(token) + : { 'Accept': 'application/vnd.github.v3+json' }; async function fetchWithRateLimit(url) { try { @@ -312,9 +309,13 @@ async function fetchRepoActivity(repo, token, since, filters) { if (error.message.includes('401')) { userMessage = 'GitHub sign-in expired or was revoked. Reconnect GitHub in settings.'; } else if (error.message.includes('403')) { - userMessage = 'Access denied or rate limit exceeded.'; + userMessage = token + ? 'Access denied or rate limit exceeded.' + : 'Anonymous GitHub access hit the rate limit or this repo requires GitHub sign-in.'; } else if (error.message.includes('404')) { - userMessage = 'Repository not found or access denied.'; + userMessage = token + ? 'Repository not found or access denied.' + : 'Repository not found publicly. If it is private, connect GitHub and try again.'; } else if (error.message.includes('Network error')) { userMessage = 'Network connection error. Please check your internet connection.'; } diff --git a/options/controllers/import-controller.js b/options/controllers/import-controller.js index f122656..99ef5cd 100644 --- a/options/controllers/import-controller.js +++ b/options/controllers/import-controller.js @@ -1,6 +1,9 @@ import { STORAGE_CONFIG } from '../../shared/config.js'; import { getAccessToken, getSyncItem, setWatchedRepos } from '../../shared/storage-helpers.js'; -import { createHeaders } from '../../shared/github-api.js'; +import { + fetchGitHubRepoSource, + getGitHubRepoSourceConfig +} from '../../shared/github-repo-sources.js'; import { getRepoFullName, normalizeWatchedRepoRecord } from '../../shared/repo-service.js'; import { escapeHtml, unescapeHtml } from '../../shared/sanitize.js'; import { formatDateVerbose } from '../../shared/utils.js'; @@ -72,15 +75,9 @@ export async function openImportModal(type, watchedRepos) { importModalState.type = type; const modal = document.getElementById('importModal'); const title = document.getElementById('importModalTitle'); + const sourceConfig = getGitHubRepoSourceConfig(type); - const titles = { - watched: 'Import Watched Repositories', - starred: 'Import Starred Repositories', - participating: 'Import Participating Repositories', - mine: 'Import My Repositories' - }; - - title.textContent = titles[type] || 'Import Repositories'; + title.textContent = sourceConfig.modalTitle; importModalState.previousFocusElement = document.activeElement; modal.classList.add('show'); @@ -97,7 +94,7 @@ export async function openImportModal(type, watchedRepos) { }, 100); try { - const repos = await fetchReposFromGitHub(type, token); + const repos = await fetchGitHubRepoSource(type, token); const alreadyAdded = new Set( (watchedRepos || []) @@ -123,88 +120,6 @@ export async function openImportModal(type, watchedRepos) { } } -async function fetchReposFromGitHub(type, token) { - const headers = createHeaders(token); - let allRepos = []; - let page = 1; - const perPage = 100; - const MAX_PAGES = 100; // Prevent infinite loops (10,000 repos max) - const MAX_REPOS = 10000; // Hard limit on total repos - - const endpoints = { - watched: 'https://api.github.com/user/subscriptions', - starred: 'https://api.github.com/user/starred', - participating: 'https://api.github.com/user/repos?affiliation=collaborator,organization_member&sort=pushed', - mine: 'https://api.github.com/user/repos?type=all&sort=updated' - }; - - const url = endpoints[type]; - if (!url) { - throw new Error(`Invalid import type: ${type}`); - } - - let hasMorePages = true; - let startTime = Date.now(); - const TIMEOUT_MS = 60000; // 60 second timeout - - while (hasMorePages && page <= MAX_PAGES && allRepos.length < MAX_REPOS) { - // Check for timeout to prevent hanging - if (Date.now() - startTime > TIMEOUT_MS) { - break; - } - - // Build URL safely with URLSearchParams - const urlObj = new URL(url); - urlObj.searchParams.set('per_page', perPage.toString()); - urlObj.searchParams.set('page', page.toString()); - - const response = await fetch(urlObj.toString(), { - headers - }); - - if (!response.ok) { - if (response.status === 401) { - throw new Error('GitHub sign-in expired or was revoked'); - } else if (response.status === 403) { - throw new Error('Rate limit exceeded or insufficient permissions'); - } else { - throw new Error(`GitHub API error: ${response.status}`); - } - } - - const repos = await response.json(); - if (!Array.isArray(repos) || repos.length === 0) { - hasMorePages = false; - break; - } - - const transformed = repos.map(repo => ({ - fullName: repo.full_name, - description: repo.description || 'No description provided', - language: repo.language || 'Unknown', - stars: repo.stargazers_count || 0, - forks: repo.forks_count || 0, - updatedAt: repo.updated_at || repo.pushed_at - })); - - allRepos.push(...transformed); - - // Stop if we've hit the max repos limit - if (allRepos.length >= MAX_REPOS) { - break; - } - - const linkHeader = response.headers.get('Link'); - if (!linkHeader || !linkHeader.includes('rel="next"')) { - hasMorePages = false; - } else { - page++; - } - } - - return allRepos; -} - export function closeImportModal() { const modal = document.getElementById('importModal'); modal.classList.remove('show'); diff --git a/options/controllers/token-controller.js b/options/controllers/token-controller.js index 5ca198e..2b52550 100644 --- a/options/controllers/token-controller.js +++ b/options/controllers/token-controller.js @@ -13,17 +13,19 @@ function setRepoAccessState(isConnected) { const repoInput = document.getElementById('repoInput'); const addRepoBtn = document.getElementById('addRepoBtn'); const repoHelpText = document.getElementById('repoHelpText'); - const importSection = document.getElementById('importReposSection'); + const githubImportButtons = document.querySelectorAll('.github-import-btn'); - repoInput.disabled = !isConnected; + repoInput.disabled = false; repoInput.placeholder = isConnected ? 'e.g., react, facebook/react, or GitHub URL' - : 'Connect GitHub to add repositories'; - addRepoBtn.disabled = !isConnected; + : 'e.g., react, facebook/react, GitHub URL, or public npm package'; + addRepoBtn.disabled = false; repoHelpText.textContent = isConnected ? 'Add repositories to monitor (npm package, owner/repo, or GitHub URL)' - : 'Connect GitHub above to start adding repositories'; - importSection.classList.toggle('hidden', !isConnected); + : 'Add public repositories manually now, or connect GitHub above to import your repos and access private ones.'; + githubImportButtons.forEach((button) => { + button.classList.toggle('hidden', !isConnected); + }); } function setDeviceCode(userCode = '') { @@ -48,16 +50,54 @@ function setHelpText(message = '') { helpEl.textContent = message; } +function setStatusRowVisible(isVisible) { + const statusRow = document.querySelector('.github-connect-status-row'); + + if (!statusRow) { + return; + } + + statusRow.classList.toggle('hidden', !isVisible); +} + +function setConnectionUiMode(isConnected) { + const connectCard = document.querySelector('.github-connect-card'); + const introText = document.getElementById('githubConnectIntroText'); + const connectedNote = document.getElementById('githubConnectConnectedNote'); + const panelHeading = document.getElementById('githubConnectPanelHeading'); + const panelCopy = document.getElementById('githubConnectPanelCopy'); + + connectCard?.classList.toggle('is-connected', isConnected); + + if (introText) { + introText.textContent = 'Authorize DevWatch once so it can pull activity for the repositories you monitor without asking for a personal access token.'; + } + + if (connectedNote) { + connectedNote.textContent = 'You\'re all set. DevWatch keeps this browser profile connected until you disconnect or reset it.'; + } + + if (panelHeading) { + panelHeading.textContent = isConnected + ? 'Reconnect only if GitHub asks again' + : 'Connect once, then just paste if GitHub asks'; + } + + if (panelCopy) { + panelCopy.textContent = 'DevWatch opens GitHub for you and copies the verification code automatically, so the only extra step is pasting it if GitHub prompts for one.'; + } +} + function getHelpText({ isConnected = false, isWaiting = false } = {}) { if (isWaiting) { - return 'Approve access in the GitHub tab, then return here. We copied the code to your clipboard in case GitHub asks for it.'; + return 'Approve access in the GitHub tab, then return here. If GitHub asks for a code, just paste the one DevWatch already copied for you.'; } if (isConnected) { - return 'Reconnect any time if your session expires or if you want to switch accounts.'; + return 'Your GitHub connection stays in this Chrome profile until you disconnect it or reset DevWatch. If GitHub asks for a code during reconnect, just paste the one DevWatch copies for you.'; } - return 'We\'ll open GitHub in a new tab and copy your code to the clipboard so it\'s ready if GitHub asks for it. Then come back here and DevWatch will finish connecting.'; + return 'We\'ll open GitHub in a new tab and copy the code for you. If GitHub asks for one, just paste it there, approve access, and come back here.'; } function setStatus(message = '', statusClass = '') { @@ -76,7 +116,9 @@ export function applyStoredConnection(authSession, options = {}) { const clearBtn = document.getElementById('clearTokenBtn'); const isConnected = Boolean(authSession?.accessToken); const username = authSession?.username; + const hasStatusMessage = Boolean(options.statusMessage); + setConnectionUiMode(isConnected); setRepoAccessState(isConnected); setDeviceCode(options.userCode || ''); setHelpText(options.helpText || getHelpText({ @@ -95,6 +137,7 @@ export function applyStoredConnection(authSession, options = {}) { if (options.statusMessage) { setStatus(options.statusMessage, options.statusClass); + setStatusRowVisible(true); return; } @@ -103,8 +146,10 @@ export function applyStoredConnection(authSession, options = {}) { username ? `Connected as ${username}` : 'GitHub is connected', 'valid' ); + setStatusRowVisible(true); } else { setStatus('', ''); + setStatusRowVisible(hasStatusMessage); } } @@ -145,9 +190,11 @@ export async function connectGitHub(_toastManager) { connectBtn.textContent = 'Waiting for GitHub...'; } + setConnectionUiMode(Boolean(previousSession?.accessToken)); setRepoAccessState(Boolean(previousSession?.accessToken)); setStatus('Starting GitHub sign-in...', 'checking'); - setHelpText('We\'re opening GitHub and copying your code to the clipboard so it\'s ready if GitHub asks for it. Come back here as soon as GitHub says the connection is ready.'); + setStatusRowVisible(true); + setHelpText('We\'re opening GitHub and copying the code for you. If GitHub asks for one, just paste it there, then come back here as soon as the connection is ready.'); try { const result = await completeGitHubDeviceAuth({ @@ -158,7 +205,7 @@ export async function connectGitHub(_toastManager) { : false; setStatus( copied - ? `Code ${userCode} copied to clipboard in case GitHub asks for it.` + ? `Code ${userCode} copied to your clipboard. Paste it on GitHub only if GitHub asks for it.` : `If GitHub asks for a code, enter ${userCode}.`, 'checking' ); diff --git a/options/options.css b/options/options.css index 41c6fc2..176f00b 100644 --- a/options/options.css +++ b/options/options.css @@ -193,7 +193,7 @@ h1 { font-weight: 600; color: var(--text-primary); margin: 0; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; letter-spacing: -0.03em; } @@ -222,7 +222,7 @@ h1 { transition: all 0.2s ease; text-align: left; position: relative; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -418,7 +418,6 @@ select:focus { border-radius: 18px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); } - .info-box .help-text { margin: 0; display: flex; @@ -450,6 +449,22 @@ select:focus { margin-top: 2px; } +.setup-inline-link { + color: var(--link-color); + font-weight: 600; + text-decoration: none; +} + +.setup-inline-link:hover { + text-decoration: underline; +} + +.setup-inline-link:focus-visible { + outline: 2px solid var(--link-color); + outline-offset: 3px; + border-radius: 6px; +} + button { padding: 8px 16px; border: 1px solid var(--border-color-dark); @@ -537,7 +552,7 @@ button.danger:hover { font-weight: 600; transition: all 0.2s ease; user-select: none; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -933,20 +948,32 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .expiry-settings { - margin-top: 20px; - padding: 20px; - background: var(--bg-tertiary); + display: flex; + flex-direction: column; + gap: 14px; + margin-top: 16px; + padding: 18px; + background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: 18px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +.expiry-settings .info-box { + margin: 0; +} + +.expiry-settings .input-label-styled { + margin-bottom: 0; } /* Styled Number Input */ .number-input-group { display: inline-flex; align-items: center; - background: var(--bg-primary); - border: 2px solid var(--border-color); - border-radius: 8px; + background: rgba(255, 255, 255, 0.025); + border: 1px solid var(--border-color); + border-radius: 14px; overflow: hidden; transition: all 0.2s ease; } @@ -974,7 +1001,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { .input-suffix { padding: 10px 16px; - background: var(--bg-tertiary); + background: rgba(255, 255, 255, 0.04); color: var(--text-secondary); font-size: 14px; font-weight: 500; @@ -994,11 +1021,11 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { align-items: flex-start; gap: 16px; padding: 20px; - background: rgba(255, 255, 255, 0.025); + background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-color); - border-radius: 18px; + border-radius: 20px; transition: all 0.2s ease; - box-shadow: none; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); } .setup-step.clickable { @@ -1011,7 +1038,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { border-color: var(--border-hover); transform: translateY(-1px); background: rgba(255, 255, 255, 0.04); - box-shadow: 0 10px 22px rgba(0, 0, 0, 0.06); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08); } .step-number { @@ -1027,7 +1054,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { font-size: 16px; font-weight: 700; flex-shrink: 0; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; } .step-content h3 { @@ -1088,7 +1115,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .github-step-label { - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 12px; font-weight: 600; text-transform: uppercase; @@ -1112,6 +1139,15 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { max-width: 60ch; } +.github-connect-connected-note { + display: none; + font-size: 14px; + line-height: 1.6; + color: var(--text-secondary); + margin-top: 2px; + max-width: 56ch; +} + .github-connect-facts { display: grid; gap: 12px; @@ -1153,7 +1189,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { font-weight: 700; letter-spacing: 0.01em; color: var(--text-primary); - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -1169,15 +1205,16 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .github-connect-panel-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; + display: block; +} + +.github-connect-panel-title { + min-width: 0; } .github-panel-label { margin: 0 0 10px; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 12px; font-weight: 600; text-transform: uppercase; @@ -1185,10 +1222,79 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { color: var(--text-secondary); } +.github-connect-panel-heading { + margin: 0; + color: var(--text-primary); + font-size: 22px; + line-height: 1.15; + letter-spacing: -0.04em; +} + +.github-connect-panel-copy { + margin: 10px 0 0; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.6; + max-width: 40ch; +} + .github-connect-panel-body { min-width: 0; } +.github-connect-status-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 10px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-color); +} + +.github-connect-status-hint { + display: inline-flex; + align-items: center; + width: fit-content; + min-height: 34px; + padding: 8px 14px; + border-radius: 999px; + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.03); + color: var(--text-secondary); + font-size: 12px; + line-height: 1.5; + max-width: 34ch; +} + +.github-disconnect-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 36px; + padding: 8px 14px; + border-radius: 999px; + border: 1px solid rgba(248, 113, 113, 0.2); + background: rgba(248, 113, 113, 0.06); + color: rgba(254, 202, 202, 0.9); + font-size: 13px; + font-weight: 500; + line-height: 1; + box-shadow: none; +} + +.github-disconnect-btn:hover { + background: rgba(248, 113, 113, 0.1); + border-color: rgba(248, 113, 113, 0.32); + color: #fecaca; + transform: none; +} + +.github-disconnect-btn:focus-visible { + outline: 2px solid rgba(239, 68, 68, 0.45); + outline-offset: 2px; +} + .github-connect-btn { display: inline-flex; align-items: center; @@ -1202,6 +1308,14 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { .github-connect-help { margin: 0; font-size: 13px; + line-height: 1.6; +} + +.github-connect-inline-help { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; } .github-connect-flow { @@ -1211,11 +1325,14 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .github-connect-flow-step { + display: flex; + flex-direction: column; + gap: 10px; padding: 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border-color); border-radius: 16px; - min-height: 108px; + min-height: 96px; } .github-connect-flow-step span { @@ -1225,13 +1342,12 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { width: 28px; height: 28px; border-radius: 999px; - margin-bottom: 12px; background: rgba(255, 255, 255, 0.08); border: 1px solid var(--border-color); color: var(--text-primary); font-size: 12px; font-weight: 700; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; } .github-connect-flow-step p { @@ -1241,23 +1357,73 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { line-height: 1.5; } +.github-connect-card.is-connected .github-connect-intro { + gap: 14px; +} + +.github-connect-card.is-connected #githubConnectIntroText, +.github-connect-card.is-connected .github-connect-facts, +.github-connect-card.is-connected .github-connect-flow, +.github-connect-card.is-connected .github-connect-status-hint, +.github-connect-card.is-connected #githubConnectPanelCopy { + display: none; +} + +.github-connect-card.is-connected .github-connect-connected-note { + display: block; +} + +.github-connect-card.is-connected .github-connect-panel { + gap: 14px; +} + +.github-connect-card.is-connected .github-connect-panel-body { + gap: 14px; +} + +.github-connect-card.is-connected .github-connect-panel-heading { + font-size: 20px; + max-width: 20ch; +} + .device-code-card { min-width: 0; } .device-code-header { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 12px; } +.device-code-copy { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.device-code-copy-text { + margin: 0; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.5; +} + .device-code-note { + display: inline-flex; + align-items: center; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--text-secondary); + color: var(--text-primary); + padding: 6px 10px; + border-radius: 999px; + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.04); + white-space: nowrap; } .step-header-section { @@ -1335,6 +1501,26 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { align-items: stretch; } + .github-connect-panel-header { + display: flex; + } + + .github-connect-panel-heading { + font-size: 20px; + } + + .github-connect-status-row { + align-items: stretch; + } + + .github-disconnect-btn { + width: fit-content; + } + + .github-connect-status-hint { + max-width: none; + } + .github-connect-flow { grid-template-columns: 1fr; } @@ -1410,7 +1596,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { cursor: pointer; transition: all 0.2s ease; flex-shrink: 0; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -1449,10 +1635,12 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .help-logo { - width: 64px; - height: 64px; + display: block; + width: 72px; + height: 72px; margin: 0 auto 16px; - color: var(--accent-color); + border-radius: 18px; + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); } .help-hero h2 { @@ -1545,148 +1733,378 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { transition: all 0.2s ease; } -.resource-link:hover { - border-color: var(--border-hover); - transform: translateY(-2px); - box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12); +.resource-link:hover { + border-color: var(--border-hover); + transform: translateY(-2px); + box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12); +} + +.resource-icon { + width: 32px; + height: 32px; + flex-shrink: 0; + color: var(--accent-color); +} + +.resource-content h4 { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 4px; +} + +.resource-content p { + font-size: 14px; + color: var(--text-secondary); + margin: 0; + line-height: 1.4; +} + +.help-version-changelog { + margin-top: 40px; + padding-top: 32px; + border-top: 1px solid var(--border-color); +} + +.help-version-header { + margin-bottom: 20px; +} + +.help-version-changelog h3 { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 8px; +} + +.help-version-intro { + margin: 0; + color: var(--text-secondary); + font-size: 14px; + line-height: 1.6; +} + +.version-info { + margin-bottom: 16px; +} + +.version-brief { + display: grid; + gap: 16px; + padding: 20px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.025) 100%), + var(--card); + border: 1px solid var(--border-color); + border-radius: 20px; + color: var(--text-primary); +} + +.version-brief-top { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; +} + +.version-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--link-color); + margin-bottom: 10px; +} + +.version-headline { + font-size: 24px; + font-weight: 700; + letter-spacing: -0.04em; + line-height: 1.15; + margin: 0; +} + +.version-summary { + margin: 0; + font-size: 14px; + line-height: 1.6; + color: var(--text-secondary); +} + +.release-pills { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.version-badge, +.release-pill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border-color); + border-radius: 999px; + color: var(--text-primary); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + font-size: 11px; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.version-badge svg, +.release-pill svg { + color: var(--link-color); + flex-shrink: 0; +} + +.release-highlight-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.release-highlight-card { + padding: 14px; + border-radius: 16px; + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.03); + display: grid; + gap: 6px; +} + +.release-highlight-title, +.release-chip, +.release-section-count { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; +} + +.release-highlight-title { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-primary); +} + +.release-highlight-copy { + margin: 0; + font-size: 13px; + line-height: 1.55; + color: var(--text-secondary); +} + +.changelog-content { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.035) 0%, rgba(255, 255, 255, 0.02) 100%), + var(--card); + border: 1px solid var(--border-color); + border-radius: 20px; + overflow: hidden; +} + +.release-chip-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.02); +} + +.release-chip { + appearance: none; + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.03); + color: var(--text-secondary); + border-radius: 999px; + padding: 8px 12px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; +} + +.release-chip:hover, +.release-chip:focus-visible, +.release-chip.active { + background: rgba(255, 255, 255, 0.08); + border-color: color-mix(in srgb, var(--link-color) 30%, var(--border-color)); + color: var(--text-primary); + outline: none; +} + +.release-notes-list { + display: grid; + gap: 12px; + padding: 16px 20px 20px; + max-height: 620px; + overflow-y: auto; +} + +.release-section { + padding: 16px; + border-radius: 18px; + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.03); + display: grid; + gap: 12px; + scroll-margin-top: 16px; +} + +.release-section-head { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; +} + +.release-section-title { + margin: 0; + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; +} + +.release-section-count { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-secondary); } -.resource-icon { - width: 32px; - height: 32px; - flex-shrink: 0; - color: var(--accent-color); +.release-section-body { + display: grid; + gap: 10px; } -.resource-content h4 { +.release-section-body p { + margin: 0; font-size: 14px; - font-weight: 600; - color: var(--text-primary); - margin: 0 0 4px; + line-height: 1.6; + color: var(--text-secondary); } -.resource-content p { - font-size: 14px; - color: var(--text-secondary); +.release-section-body ul { margin: 0; - line-height: 1.4; + padding-left: 20px; + display: grid; + gap: 6px; } -.help-version-changelog { - margin-top: 40px; - padding-top: 32px; - border-top: 1px solid var(--border-color); +.release-section-body li { + font-size: 14px; + line-height: 1.6; + color: var(--text-secondary); } -.help-version-changelog h3 { - font-size: 20px; - font-weight: 600; - color: var(--text-primary); - margin: 0 0 20px; - text-align: center; +.release-section-body code { + background: var(--bg-secondary); + padding: 2px 6px; + border-radius: 6px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + font-size: 13px; + color: var(--link-color); } -.version-info { - margin-bottom: 24px; - text-align: center; +.release-section-body a { + color: var(--link-color); + text-decoration: none; + font-weight: 500; } -.version-badge { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 20px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border-color); - border-radius: 16px; - color: var(--text-primary); +.release-section-body a:hover { + text-decoration: underline; } -.version-badge svg { - color: var(--link-color); - flex-shrink: 0; +.release-section-body strong { + color: var(--text-primary); + font-weight: 600; } -.changelog-content { - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border-color); - border-radius: 18px; - padding: 24px; - max-height: 600px; - overflow-y: auto; +.changelog-content > h2, +.changelog-content > h3, +.changelog-content > h4, +.changelog-content > p, +.changelog-content > ul { + margin-left: 20px; + margin-right: 20px; } -.changelog-content h2 { - font-size: 24px; +.changelog-content > h2 { + margin-top: 20px; + margin-bottom: 12px; + font-size: 22px; font-weight: 700; + letter-spacing: -0.03em; color: var(--text-primary); - margin: 0 0 16px; - padding-bottom: 12px; - border-bottom: 2px solid var(--border-color); } -.changelog-content h3 { - font-size: 18px; +.changelog-content > h3 { + margin-top: 20px; + margin-bottom: 10px; + font-size: 17px; font-weight: 600; - color: var(--link-color); - margin: 24px 0 12px; + color: var(--text-primary); } -.changelog-content h4 { +.changelog-content > h4 { + margin-top: 16px; + margin-bottom: 8px; font-size: 14px; font-weight: 600; color: var(--text-primary); - margin: 16px 0 8px; } -.changelog-content p { +.changelog-content > p, +.changelog-content > li, +.changelog-content > ul li { font-size: 14px; - color: var(--text-secondary); line-height: 1.6; - margin: 8px 0; -} - -.changelog-content ul { - margin: 12px 0; - padding-left: 24px; + color: var(--text-secondary); } -.changelog-content li { - font-size: 14px; - color: var(--text-secondary); - line-height: 1.6; - margin: 4px 0; +.changelog-content > ul { + margin-top: 0; + margin-bottom: 0; + padding-left: 40px; } -.changelog-content code { +.changelog-content > p code, +.changelog-content > ul code { background: var(--bg-secondary); padding: 2px 6px; - border-radius: 4px; - font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; - font-size: 14px; + border-radius: 6px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + font-size: 13px; color: var(--link-color); } -.changelog-content a { +.changelog-content > p a, +.changelog-content > ul a { color: var(--link-color); text-decoration: none; - font-weight: 500; } -.changelog-content a:hover { +.changelog-content > p a:hover, +.changelog-content > ul a:hover { text-decoration: underline; } -.changelog-content strong { - color: var(--text-primary); - font-weight: 600; -} - .changelog-footer { - margin-top: 20px; - padding-top: 16px; + display: flex; + justify-content: center; + padding: 16px 20px 20px; border-top: 1px solid var(--border-color); - text-align: center; + background: rgba(255, 255, 255, 0.02); } .changelog-link { @@ -1697,14 +2115,37 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { text-decoration: none; font-size: 14px; font-weight: 500; - padding: 8px 16px; - border-radius: 8px; - transition: all 0.2s ease; + padding: 10px 16px; + border-radius: 999px; + border: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.03); + transition: background-color 0.2s ease, border-color 0.2s ease; } -.changelog-link:hover { - background: var(--bg-secondary); - text-decoration: none; +.changelog-link:hover, +.changelog-link:focus-visible { + background: rgba(255, 255, 255, 0.06); + border-color: color-mix(in srgb, var(--link-color) 24%, var(--border-color)); + outline: none; +} + +.release-empty-state { + padding: 24px 20px; + color: var(--text-secondary); + font-size: 14px; + line-height: 1.6; +} + +@media (max-width: 900px) { + .version-brief-top, + .release-section-head { + flex-direction: column; + align-items: flex-start; + } + + .release-highlight-grid { + grid-template-columns: 1fr; + } } .changelog-link svg { @@ -1853,7 +2294,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { border: 1px solid rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05); color: var(--text-primary); - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 11px; font-weight: 600; letter-spacing: 0.08em; @@ -1959,7 +2400,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { .repo-name { display: flex; align-items: center; - font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 13px; font-weight: 600; color: var(--text-primary); @@ -2030,6 +2471,123 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { gap: 12px; } +.import-btn.active { + background: rgba(255, 255, 255, 0.08); + border-color: var(--border-hover); + color: var(--text-primary); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +.popular-repos-panel { + margin: 0; +} + +.popular-repos-surface { + display: grid; + gap: 14px; + padding: 18px; + background: rgba(255, 255, 255, 0.035); + border-color: var(--border-color); +} + +.popular-repos-panel-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.popular-repos-panel-header h3 { + margin: 0; + color: var(--text-primary); + font-size: 16px; +} + +.popular-repos-panel-header .help-text { + margin: 6px 0 0; +} + +.popular-repos-state { + font-size: 14px; + color: var(--text-secondary); +} + +.popular-repo-suggestions { + display: flex; + flex-direction: column; + gap: 10px; +} + +.popular-repo-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px 18px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-color); + border-radius: 18px; + transition: all 0.15s ease; +} + +.popular-repo-card:hover { + border-color: var(--border-hover); + background: rgba(255, 255, 255, 0.045); + transform: translateY(-1px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08); +} + +.popular-repo-info { + flex: 1; + min-width: 0; +} + +.popular-repo-name { + display: flex; + align-items: center; + gap: 0; + margin-bottom: 6px; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; +} + +.popular-repo-description { + margin-bottom: 8px; + font-size: 14px; + line-height: 1.45; + color: var(--text-secondary); +} + +.popular-repo-meta { + display: flex; + flex-wrap: wrap; + gap: 12px; + font-size: 12px; + color: var(--text-secondary); +} + +.popular-repo-add-btn { + width: 38px; + height: 38px; + padding: 0; + border-radius: 14px; + flex-shrink: 0; + font-size: 18px; + font-weight: 700; +} + +.popular-repo-add-btn:disabled { + opacity: 1; +} + +.popular-repo-add-btn.added { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); +} + .repo-section-icon { width: 32px; height: 32px; @@ -2077,7 +2635,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { font-size: 12px; font-weight: 700; margin-left: 4px; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; } /* Action Buttons Layout */ @@ -2125,6 +2683,10 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { max-height: 300px; /* Increased max-height to accommodate import buttons */ } +.popular-repos-panel.show { + max-height: 520px; +} + .repo-toolbar.show { padding-bottom: 14px; border-bottom: 1px solid var(--border-color); @@ -2441,7 +3003,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .modal-content { - background: rgba(24, 22, 20, 0.96); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%), + var(--card); border: 1px solid var(--border-color); border-radius: 22px; width: 90%; @@ -2477,7 +3041,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { margin: 0; font-size: 18px; color: var(--text-primary); - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; letter-spacing: -0.03em; } @@ -2578,14 +3142,14 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { } .repo-item.import-variant.selected { - border-color: var(--link-color); - box-shadow: 0 0 0 2px var(--link-color); - background: var(--bg-tertiary); + border-color: rgba(16, 185, 129, 0.48); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 0 0 1px rgba(16, 185, 129, 0.34); + background: rgba(16, 185, 129, 0.12); } .repo-item.import-variant.selected:hover { transform: translateY(-1px); - box-shadow: 0 0 0 2px var(--link-color), 0 2px 4px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 0 0 1px rgba(16, 185, 129, 0.34), 0 2px 4px rgba(0, 0, 0, 0.1); } .repo-item.import-variant.already-added { @@ -2602,7 +3166,8 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { padding: 6px 12px; background: var(--success-bg); color: var(--success-text); - border-radius: 4px; + border: 1px solid var(--success-border); + border-radius: 999px; font-size: 12px; font-weight: 500; white-space: nowrap; @@ -2654,14 +3219,14 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { color: var(--text-primary); text-transform: uppercase; letter-spacing: 0.08em; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; } #deviceCodeSection input { letter-spacing: 0.22em; font-weight: 600; text-align: center; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 20px; padding: 16px 14px; } @@ -2843,9 +3408,17 @@ body.dark-mode select:focus-visible { outline-offset: 2px; } +.repo-item.import-variant:focus { + outline: none; +} + .repo-item.import-variant:focus-visible { - outline: 2px solid var(--link-color); - outline-offset: -2px; + outline: none; + border-color: rgba(16, 185, 129, 0.6); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.03), + 0 0 0 1px rgba(16, 185, 129, 0.4), + 0 0 0 4px rgba(16, 185, 129, 0.16); } .search-clear-btn:focus-visible { @@ -2899,10 +3472,11 @@ body.dark-mode select:focus-visible { .empty-repos-card { text-align: center; padding: 48px 32px; - background: rgba(255, 255, 255, 0.02); - border: 1px dashed var(--border-color); + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-color); border-radius: 20px; margin: 0; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); } .empty-repos-icon { @@ -2969,7 +3543,7 @@ body.dark-mode select:focus-visible { .snoozed-repo-name { font-weight: 600; color: var(--text-primary); - font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 13px; margin-bottom: 4px; } @@ -3160,7 +3734,7 @@ body.dark-mode .snooze-expiry-warning { cursor: pointer; transition: all 0.2s; text-decoration: none; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -3214,7 +3788,7 @@ body.dark-mode .snooze-expiry-warning { padding: 8px 10px; border-radius: 10px; transition: background 0.2s, transform 0.2s; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -3251,7 +3825,7 @@ body.dark-mode .setup-btn:hover { font-size: 16px; margin-bottom: 12px; font-weight: 600; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -3275,7 +3849,7 @@ body.dark-mode .setup-btn:hover { font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; } .footer-column ul { @@ -3298,7 +3872,7 @@ body.dark-mode .setup-btn:hover { padding: 6px 8px; border-radius: 10px; transition: all 0.2s; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } diff --git a/options/options.html b/options/options.html index 5470ff0..4084cd5 100644 --- a/options/options.html +++ b/options/options.html @@ -4,7 +4,7 @@ GitHub DevWatch - Settings - + @@ -15,13 +15,7 @@

Settings

- - +
+
+ + +
+ -

- We'll open GitHub in a new tab and copy your code to the clipboard so it's ready if GitHub asks for it. Then come back here and DevWatch will finish connecting. -

+
+ Paste the copied code only if GitHub asks for it. +

+ We'll open GitHub in a new tab and copy the code for you. If GitHub asks for one, just paste it there, approve access, and come back here. +

+
- @@ -777,24 +808,7 @@

Import/Export Settings

-

Version & Changelog

+
+

Latest Release

+

See what shipped most recently and skim the full release notes.

+

Loading version information...

-

Loading changelog...

+

Loading latest release...

diff --git a/options/options.js b/options/options.js index d738bba..f51c6af 100644 --- a/options/options.js +++ b/options/options.js @@ -1,4 +1,5 @@ import { applyTheme, applyColorTheme, formatDateVerbose } from '../shared/utils.js'; +import { escapeHtml } from '../shared/sanitize.js'; import { clearAuthSession, getAuthSession, @@ -19,6 +20,7 @@ import { normalizeSettings } from '../shared/settings-schema.js'; import { NotificationManager } from '../shared/ui/notification-manager.js'; +import { OnboardingManager } from '../shared/onboarding.js'; // Controllers import { setupThemeListener } from './controllers/theme-controller.js'; @@ -33,9 +35,13 @@ import { renderRepoList } from './views/repository-list-view.js'; // Global toast manager instance (singleton) const toastManager = NotificationManager.getInstance(); +const onboardingManager = new OnboardingManager(); +const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/jonmartin721/devwatch-github/releases/latest'; +const CHANGELOG_GITHUB_URL = 'https://github.com/jonmartin721/devwatch-github/releases'; const state = { watchedRepos: [], + popularRepos: [], mutedRepos: [], pinnedRepos: [], currentPage: 1, @@ -144,23 +150,28 @@ function setupTabNavigation() { }); // Add click listeners to clickable setup steps - const clickableSetupSteps = document.querySelectorAll('.setup-step.clickable'); - clickableSetupSteps.forEach(step => { - step.addEventListener('click', (event) => { + const bindTabTrigger = (element) => { + element.addEventListener('click', (event) => { event.preventDefault(); - const tabName = step.dataset.tab; + const tabName = element.dataset.tab; if (tabName) { switchTab(tabName, { focusTab: true }); } }); - }); + }; + + const clickableSetupSteps = document.querySelectorAll('.setup-step.clickable'); + clickableSetupSteps.forEach(bindTabTrigger); + + const setupTabLinks = document.querySelectorAll('.setup-inline-link[data-tab]'); + setupTabLinks.forEach(bindTabTrigger); // Initialize active tab from localStorage or URL hash const hash = window.location.hash.substring(1); const savedTab = localStorage.getItem('activeTab'); - const initialTab = hash || savedTab || 'setup'; + const initialTab = hash || savedTab || 'repositories'; - const tabToActivate = validTabs.includes(initialTab) ? initialTab : 'setup'; + const tabToActivate = validTabs.includes(initialTab) ? initialTab : 'repositories'; switchTab(tabToActivate); } @@ -182,6 +193,152 @@ function handleUrlParameters() { } +function togglePopularReposPanel(forceOpen = null) { + const panel = document.getElementById('popularReposPanel'); + const button = document.getElementById('togglePopularReposBtn'); + + if (!panel || !button) { + return; + } + + const shouldOpen = forceOpen === null ? !panel.classList.contains('show') : forceOpen; + panel.classList.toggle('show', shouldOpen); + button.classList.toggle('active', shouldOpen); + button.setAttribute('aria-expanded', shouldOpen ? 'true' : 'false'); +} + +function formatCompactNumber(num) { + if (!Number.isFinite(num)) { + return '0'; + } + + if (num >= 1000000) { + return `${(num / 1000000).toFixed(1).replace(/\.0$/, '')}M`; + } + + if (num >= 1000) { + return `${(num / 1000).toFixed(1).replace(/\.0$/, '')}k`; + } + + return String(num); +} + +function getPopularRepoFullName(repo) { + return `${repo?.owner?.login || 'unknown'}/${repo?.name || 'unknown'}`; +} + +function renderPopularReposSection() { + const popularReposState = document.getElementById('popularReposState'); + const popularReposList = document.getElementById('popularReposList'); + + if (!popularReposState || !popularReposList) { + return; + } + + const watchedRepoNames = new Set(state.watchedRepos.map(repo => repo.fullName.toLowerCase())); + const visibleRepos = state.popularRepos + .filter(repo => !watchedRepoNames.has(getPopularRepoFullName(repo).toLowerCase())) + .slice(0, 3); + + if (visibleRepos.length === 0) { + popularReposState.textContent = state.popularRepos.length > 0 + ? 'All suggested repositories are already on your watchlist.' + : 'No popular repositories are available right now.'; + popularReposState.classList.remove('hidden'); + popularReposList.classList.add('hidden'); + popularReposList.innerHTML = ''; + return; + } + + popularReposState.classList.add('hidden'); + popularReposList.classList.remove('hidden'); + popularReposList.innerHTML = visibleRepos.map((repo) => { + const fullName = getPopularRepoFullName(repo); + const stars = Number.isFinite(repo?.stargazers_count) ? formatCompactNumber(repo.stargazers_count) : ''; + + return ` + + `; + }).join(''); + + popularReposList.querySelectorAll('.popular-repo-add-btn').forEach((button) => { + button.addEventListener('click', async () => { + const repoFullName = button.dataset.popularRepo; + if (!repoFullName) { + return; + } + + button.disabled = true; + + try { + const currentSettings = normalizeSettings(await getSettings()); + if (!currentSettings.allowUnlimitedRepos && state.watchedRepos.length >= STORAGE_CONFIG.MAX_WATCHED_REPOS) { + showRepoError(`Maximum of ${STORAGE_CONFIG.MAX_WATCHED_REPOS} repositories allowed. Enable "Unlimited Repositories" in Advanced settings to watch more.`); + button.disabled = false; + return; + } + + const githubToken = await getAccessToken(); + const resolution = await resolveWatchedRepoInput(repoFullName, { + githubToken, + existingRepos: state.watchedRepos + }); + + if (!resolution.valid) { + showRepoError(resolution.error || 'Repository validation failed'); + button.disabled = false; + return; + } + + state.watchedRepos.push(resolution.record); + state.currentPage = Math.ceil(state.watchedRepos.length / state.reposPerPage); + renderRepoListWrapper(); + renderPopularReposSection(); + await setWatchedRepos(state.watchedRepos); + toastManager.success(`Successfully added ${resolution.record.fullName} to watched repositories`); + } catch (error) { + console.error('Error adding repository from popular list:', error); + showRepoError('Failed to add repository. Please try again.'); + button.disabled = false; + } + }); + }); +} + +async function loadPopularRepos() { + const popularReposState = document.getElementById('popularReposState'); + const popularReposList = document.getElementById('popularReposList'); + + if (!popularReposState || !popularReposList) { + return; + } + + popularReposState.textContent = 'Loading popular repositories...'; + popularReposState.classList.remove('hidden'); + popularReposList.classList.add('hidden'); + + try { + state.popularRepos = await onboardingManager.getPopularRepos(); + renderPopularReposSection(); + } catch (error) { + console.error('Error loading popular repositories:', error); + state.popularRepos = []; + popularReposState.textContent = 'Could not load popular repositories right now.'; + popularReposState.classList.remove('hidden'); + popularReposList.classList.add('hidden'); + } +} + function syncTokenUiWithStoredCredential(hasStoredToken) { applyStoredConnection(hasStoredToken ? { accessToken: 'stored-session' } : null); } @@ -302,6 +459,13 @@ function setupEventListeners() { document.getElementById('importStarredBtn').addEventListener('click', () => openImportModal('starred', state.watchedRepos)); document.getElementById('importParticipatingBtn').addEventListener('click', () => openImportModal('participating', state.watchedRepos)); document.getElementById('importMineBtn').addEventListener('click', () => openImportModal('mine', state.watchedRepos)); + document.getElementById('togglePopularReposBtn').addEventListener('click', async () => { + togglePopularReposPanel(); + + if (state.popularRepos.length === 0) { + await loadPopularRepos(); + } + }); // Import/Export settings buttons document.getElementById('importBtn').addEventListener('click', () => { @@ -617,6 +781,7 @@ async function loadSettings() { state.pinnedRepos = settings.pinnedRepos; renderRepoListWrapper(); + await loadPopularRepos(); applySettingsToUi(settings); // Load and display current snoozes @@ -628,6 +793,7 @@ async function loadSettings() { state.mutedRepos = state.mutedRepos || []; state.pinnedRepos = state.pinnedRepos || []; renderRepoListWrapper(); + renderPopularReposSection(); } } @@ -820,6 +986,7 @@ function renderRepoListWrapper() { (repo, pin) => togglePinRepo(repo, pin, state, renderRepoListWrapper), (repo) => removeRepo(repo) ); + renderPopularReposSection(); } // Global wrapper for external calls @@ -946,59 +1113,420 @@ async function resetSettings() { * Load and display version information and changelog */ async function loadVersionAndChangelog() { + const versionInfo = document.getElementById('versionInfo'); + const changelogContent = document.getElementById('changelogContent'); + try { // Load version from manifest.json const manifestUrl = chrome.runtime.getURL('manifest.json'); const manifestResponse = await fetch(manifestUrl); const manifest = await manifestResponse.json(); - const versionInfo = document.getElementById('versionInfo'); + const release = await fetchLatestRelease(); + if (versionInfo) { - versionInfo.innerHTML = ` -
- - Current Version: v${manifest.version} -
- `; + versionInfo.innerHTML = buildVersionBriefHtml(manifest.version, release); + } + + if (changelogContent) { + changelogContent.innerHTML = buildLatestReleaseHtml(release); + bindReleaseSectionChips(changelogContent); + } + } catch (error) { + console.error('Error loading version/changelog:', error); + + if (versionInfo) { + versionInfo.innerHTML = buildFallbackVersionInfoHtml(); + } + if (changelogContent) { + changelogContent.innerHTML = await buildFallbackChangelogHtml(); + } + } +} + +async function fetchLatestRelease() { + const response = await fetch(LATEST_RELEASE_API_URL, { + headers: { + Accept: 'application/vnd.github+json' } + }); - // Load changelog from CHANGELOG.md + if (!response.ok) { + throw new Error(`Failed to fetch latest release: ${response.status}`); + } + + return response.json(); +} + +function buildVersionBriefHtml(installedVersion, release) { + const sections = parseReleaseSections(release.body, release.tag_name); + const displayTitle = extractReleaseTitle(release.name, release.tag_name); + const publishedAt = formatAbsoluteDate(release.published_at || release.created_at); + const highlightCards = sections + .filter(section => section.items.length > 0 || section.paragraphs.length > 0) + .slice(0, 3) + .map(section => ` +
+
${escapeHtml(section.title)}
+

${renderReleaseInlineMarkdown(summarizeReleaseSection(section))}

+
+ `) + .join(''); + + const releasePills = [ + buildVersionPill('Installed', `v${installedVersion}`), + buildReleasePill('Published', publishedAt) + ]; + + if (release.tag_name && release.tag_name !== `v${installedVersion}`) { + releasePills.push(buildReleasePill('Latest', release.tag_name)); + } + + return ` +
+
+
+
Latest release
+

${escapeHtml(release.tag_name || `v${installedVersion}`)}${displayTitle ? ` - ${escapeHtml(displayTitle)}` : ''}

+
+
+ ${releasePills.join('')} +
+
+

${escapeHtml(buildReleaseSummary(displayTitle, sections))}

+ ${highlightCards ? `
${highlightCards}
` : ''} +
+ `; +} + +function buildFallbackVersionInfoHtml() { + return '

Unable to load latest release information

'; +} + +async function buildFallbackChangelogHtml() { + try { const changelogUrl = chrome.runtime.getURL('CHANGELOG.md'); const changelogResponse = await fetch(changelogUrl); const changelogText = await changelogResponse.text(); - - // Parse markdown to HTML (basic conversion) const changelogHtml = parseChangelogMarkdown(changelogText); - const changelogContent = document.getElementById('changelogContent'); - if (changelogContent) { - changelogContent.innerHTML = changelogHtml + ` - + return changelogHtml + ` + + `; + } catch (fallbackError) { + console.error('Fallback changelog load failed:', fallbackError); + return ` +
+ Unable to load the latest release right now. Please try again later. +
+ + `; + } +} + +function buildLatestReleaseHtml(release) { + const sections = parseReleaseSections(release.body, release.tag_name); + const chips = sections + .map((section, index) => ` + + `) + .join(''); + + const sectionCards = sections + .map((section, index) => { + const sectionBody = [ + ...section.paragraphs.map(paragraph => `

${renderReleaseInlineMarkdown(paragraph)}

`), + section.items.length > 0 + ? `
    ${section.items.map(item => `
  • ${renderReleaseInlineMarkdown(item)}
  • `).join('')}
` + : '' + ].join(''); + + const countLabel = section.items.length > 0 + ? `${section.items.length} ${section.items.length === 1 ? 'item' : 'items'}` + : `${section.paragraphs.length} ${section.paragraphs.length === 1 ? 'note' : 'notes'}`; + + return ` +
+
+

${escapeHtml(section.title)}

+ ${escapeHtml(countLabel)} +
+
${sectionBody}
+
`; + }) + .join(''); + + return ` + ${chips ? `
${chips}
` : ''} +
+ ${sectionCards || '
No release notes were included with this version.
'} +
+ + `; +} + +function parseReleaseSections(markdown, releaseTag = '') { + const normalizedMarkdown = (markdown || '').replace(/\r\n/g, '\n'); + const lines = normalizedMarkdown.split('\n'); + const sections = []; + let currentSection = null; + + function ensureSection(title = 'Release notes') { + if (!currentSection) { + currentSection = { title, paragraphs: [], items: [] }; + sections.push(currentSection); } - } catch (error) { - console.error('Error loading version/changelog:', error); - const versionInfo = document.getElementById('versionInfo'); - const changelogContent = document.getElementById('changelogContent'); - if (versionInfo) { - versionInfo.innerHTML = '

Unable to load version information

'; + return currentSection; + } + + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line) { + continue; } - if (changelogContent) { - changelogContent.innerHTML = '

Unable to load changelog

'; + + const levelThreeHeading = line.match(/^###\s+(.+)$/); + if (levelThreeHeading) { + currentSection = { + title: cleanReleaseHeading(levelThreeHeading[1]), + paragraphs: [], + items: [] + }; + sections.push(currentSection); + continue; + } + + const levelTwoHeading = line.match(/^##\s+(.+)$/); + if (levelTwoHeading) { + const title = cleanReleaseHeading(levelTwoHeading[1]); + if (shouldSkipReleaseHeading(title, releaseTag)) { + continue; + } + + currentSection = { + title, + paragraphs: [], + items: [] + }; + sections.push(currentSection); + continue; } + + if (/^[-*]\s+/.test(line)) { + ensureSection().items.push(line.replace(/^[-*]\s+/, '').trim()); + continue; + } + + if (/^\d+\.\s+/.test(line)) { + ensureSection().items.push(line.replace(/^\d+\.\s+/, '').trim()); + continue; + } + + ensureSection().paragraphs.push(line); + } + + const nonEmptySections = sections.filter(section => section.items.length > 0 || section.paragraphs.length > 0); + if (nonEmptySections.length === 0 && normalizedMarkdown.trim()) { + return [{ + title: 'Release notes', + paragraphs: [normalizedMarkdown.trim()], + items: [] + }]; } + + return nonEmptySections; +} + +function cleanReleaseHeading(heading) { + return heading + .replace(/\s+[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]+/gu, '') + .trim(); +} + +function shouldSkipReleaseHeading(heading, releaseTag) { + const normalizedHeading = heading.toLowerCase(); + const normalizedTag = releaseTag.toLowerCase(); + + return Boolean( + normalizedHeading === 'changelog' || + (normalizedTag && normalizedHeading.startsWith(normalizedTag)) || + /^v?\d+\.\d+\.\d+\b/i.test(heading) + ); +} + +function extractReleaseTitle(name, tagName) { + const trimmedName = (name || '').trim(); + if (!trimmedName) { + return ''; + } + + if (!tagName) { + return trimmedName; + } + + const tagPattern = new RegExp(`^${escapeRegExp(tagName)}\\s*[–—:-]\\s*`, 'i'); + return trimmedName.replace(tagPattern, '').trim(); +} + +function buildReleaseSummary(displayTitle, sections) { + const sectionNames = sections.slice(0, 3).map(section => section.title); + if (!displayTitle && sectionNames.length === 0) { + return 'Read the latest release notes from GitHub.'; + } + + if (displayTitle && sectionNames.length === 0) { + return `This release focuses on ${displayTitle}.`; + } + + if (!displayTitle) { + return `This release includes updates across ${joinNaturalList(sectionNames)}.`; + } + + return `This release focuses on ${displayTitle} and includes updates across ${joinNaturalList(sectionNames)}.`; +} + +function summarizeReleaseSection(section) { + return section.items[0] || section.paragraphs[0] || 'Release notes updated.'; +} + +function buildVersionPill(label, value) { + return ` +
+ + ${escapeHtml(label)} ${escapeHtml(value)} +
+ `; +} + +function buildReleasePill(label, value) { + return ` +
+ + ${escapeHtml(label)} ${escapeHtml(value)} +
+ `; +} + +function renderReleaseInlineMarkdown(text) { + let html = escapeHtml(text || ''); + + html = html.replace( + /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, + '$1' + ); + html = html.replace(/\*\*(.+?)\*\*/g, '$1'); + html = html.replace(/`([^`]+)`/g, '$1'); + + return html; +} + +function bindReleaseSectionChips(container) { + const chips = Array.from(container.querySelectorAll('.release-chip')); + if (chips.length === 0) { + return; + } + + chips.forEach(chip => { + chip.addEventListener('click', () => { + const targetId = chip.dataset.target; + if (!targetId) { + return; + } + + const selector = typeof window !== 'undefined' && window.CSS && typeof window.CSS.escape === 'function' + ? `#${window.CSS.escape(targetId)}` + : `#${targetId}`; + const target = container.querySelector(selector); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + chips.forEach(button => { + button.classList.remove('active'); + button.setAttribute('aria-pressed', 'false'); + }); + + chip.classList.add('active'); + chip.setAttribute('aria-pressed', 'true'); + }); + }); +} + +function formatAbsoluteDate(dateString) { + if (!dateString) { + return 'Unknown date'; + } + + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) { + return 'Unknown date'; + } + + return new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric' + }).format(date); +} + +function joinNaturalList(items) { + if (items.length === 0) { + return ''; + } + + if (items.length === 1) { + return items[0]; + } + + if (items.length === 2) { + return `${items[0]} and ${items[1]}`; + } + + return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`; +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** diff --git a/popup/popup.css b/popup/popup.css index 780823a..f84b660 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -7,10 +7,14 @@ padding: 0; box-sizing: border-box; } - +html, body { width: 450px; - max-height: 600px; + height: 600px; + overflow: hidden; +} + +body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 14px; line-height: 1.5; @@ -50,6 +54,8 @@ body { display: flex; flex-direction: column; height: 100%; + min-height: 0; + overflow: hidden; background: transparent; } @@ -72,16 +78,10 @@ header { .header-stats { display: flex; flex-direction: row; - gap: 0; + gap: 10px; align-items: center; } -.header-stats .repo-count::before { - content: "\B7"; - margin: 0 6px; - color: var(--muted-foreground); -} - .header-refresh-btn { padding: 6px; background: rgba(255, 255, 255, 0.04); @@ -125,7 +125,7 @@ h1 { letter-spacing: -0.04em; line-height: 1; color: var(--foreground); - font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -279,7 +279,7 @@ h1 { font-weight: 600; color: var(--foreground); white-space: nowrap; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -482,7 +482,7 @@ h1 { .repo-group-name { font-weight: 600; color: var(--foreground); - font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 12px; letter-spacing: -0.03em; text-transform: lowercase; @@ -505,7 +505,7 @@ h1 { color: var(--primary-foreground); border-radius: 9999px; font-weight: 700; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; } .repo-snooze-btn, @@ -595,7 +595,7 @@ h1 { padding: 5px 10px; border-radius: 999px; transition: all 150ms; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -729,7 +729,6 @@ h1 { .activity-type { display: inline-flex; align-items: center; - gap: 5px; padding: 2px 7px; border-radius: 999px; font-size: 10px; @@ -744,28 +743,41 @@ h1 { flex-shrink: 0; } -.activity-type::before { - content: ""; - display: inline-block; - width: 5px; - height: 5px; - border-radius: 50%; - flex-shrink: 0; +.activity-type.pr { + border-color: rgba(34, 197, 94, 0.28); + background: rgba(34, 197, 94, 0.1); } -.activity-type.pr::before { background: #22C55E; } -.activity-type.issue::before { background: #F59E0B; } -.activity-type.release::before { background: #3B82F6; } +.activity-type.issue { + border-color: rgba(245, 158, 11, 0.28); + background: rgba(245, 158, 11, 0.1); +} + +.activity-type.release { + border-color: rgba(59, 130, 246, 0.28); + background: rgba(59, 130, 246, 0.1); +} -body.dark-mode .activity-type.pr::before { background: #4ADE80; } -body.dark-mode .activity-type.issue::before { background: #FBBF24; } -body.dark-mode .activity-type.release::before { background: #60A5FA; } +body.dark-mode .activity-type.pr { + border-color: rgba(74, 222, 128, 0.26); + background: rgba(74, 222, 128, 0.1); +} + +body.dark-mode .activity-type.issue { + border-color: rgba(251, 191, 36, 0.26); + background: rgba(251, 191, 36, 0.1); +} + +body.dark-mode .activity-type.release { + border-color: rgba(96, 165, 250, 0.26); + background: rgba(96, 165, 250, 0.1); +} .activity-repo { font-size: 11px; color: var(--muted-foreground); font-weight: 600; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -797,8 +809,7 @@ body.dark-mode .activity-type.release::before { background: #60A5FA; } /* ---- Footer ---- */ footer { - position: sticky; - bottom: 0; + flex-shrink: 0; padding: 12px 16px 16px; border-top: 1px solid var(--border); background: linear-gradient(180deg, rgba(0, 0, 0, 0), var(--background) 22%); @@ -856,7 +867,7 @@ footer { background: rgba(255, 255, 255, 0.04); border: 1px solid var(--input); border-radius: 999px; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -881,7 +892,7 @@ footer a:hover { align-items: center; font-size: 11px; color: var(--muted-foreground); - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; letter-spacing: 0.02em; } @@ -992,7 +1003,7 @@ footer a:hover { display: block; font-size: 12px; color: var(--muted-foreground); - font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; margin-top: 8px; padding: 6px 8px; background: var(--accent); @@ -1165,15 +1176,6 @@ body.dark-mode .cached-indicator { opacity: 0.9; } -.activity-item.cached::before { - content: ''; - position: absolute; - top: 8px; - right: 8px; - font-size: 12px; - opacity: 0.7; -} - /* ---- shadcn Focus Ring ---- */ .filter-btn:focus-visible, .toolbar-btn:focus-visible, @@ -1223,7 +1225,7 @@ body.dark-mode .search-box input:focus-visible { ================================================ */ .onboarding-view { flex: 1; - padding: 18px 18px 14px; + padding: 14px 18px 10px; overflow: hidden; scrollbar-gutter: stable; min-height: 0; @@ -1251,7 +1253,7 @@ body.dark-mode .search-box input:focus-visible { /* Progress — thin line */ .onboarding-progress { - margin-bottom: 18px; + margin-bottom: 14px; text-align: center; flex-shrink: 0; } @@ -1290,7 +1292,7 @@ body.dark-mode .search-box input:focus-visible { color: var(--muted-foreground); font-weight: 600; letter-spacing: 0.08em; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -1298,17 +1300,21 @@ body.dark-mode .search-box input:focus-visible { flex: 1; display: flex; flex-direction: column; - justify-content: center; + justify-content: flex-start; min-height: 0; + overflow: hidden; } .onboarding-step { + display: flex; + flex-direction: column; text-align: center; + min-height: 0; } /* Step icon — hero treatment */ .step-icon { - margin-bottom: 24px; + margin-bottom: 18px; display: flex; align-items: center; justify-content: center; @@ -1331,18 +1337,18 @@ body.dark-mode .search-box input:focus-visible { /* Headings — large, tight, confident */ .onboarding-step h2 { - font-size: 24px; + font-size: 22px; font-weight: 700; color: var(--foreground); - margin-bottom: 8px; + margin-bottom: 6px; letter-spacing: -0.04em; line-height: 1.05; } .onboarding-step p { color: var(--muted-foreground); - line-height: 1.5; - margin-bottom: 14px; + line-height: 1.45; + margin-bottom: 12px; max-width: 340px; margin-left: auto; margin-right: auto; @@ -1353,18 +1359,18 @@ body.dark-mode .search-box input:focus-visible { .feature-list { display: flex; flex-direction: column; - gap: 10px; - margin: 24px 0; + gap: 8px; + margin: 16px 0 12px; text-align: left; } .feature-item { display: flex; align-items: center; - gap: 16px; - padding: 16px; + gap: 14px; + padding: 12px 14px; background: rgba(255, 255, 255, 0.04); - border-radius: 18px; + border-radius: 16px; border: 1px solid var(--border); transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); } @@ -1380,8 +1386,8 @@ body.dark-mode .search-box input:focus-visible { display: flex; align-items: center; justify-content: center; - width: 40px; - height: 40px; + width: 36px; + height: 36px; border-radius: 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border); @@ -1389,22 +1395,22 @@ body.dark-mode .search-box input:focus-visible { } .feature-icon svg { - width: 18px; - height: 18px; + width: 16px; + height: 16px; } .feature-item span:last-child { - font-size: 14px; + font-size: 13px; font-weight: 500; color: var(--foreground); - line-height: 1.4; + line-height: 1.35; } .step-description { font-size: 13px; color: var(--muted-foreground); font-weight: 400; - margin-top: 4px; + margin-top: 2px; } /* Token Step */ @@ -1412,8 +1418,8 @@ body.dark-mode .search-box input:focus-visible { background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border); border-radius: 18px; - padding: 16px; - margin: 14px 0; + padding: 14px; + margin: 10px 0; text-align: left; } @@ -1422,7 +1428,7 @@ body.dark-mode .search-box input:focus-visible { color: var(--foreground); font-size: 12px; font-weight: 600; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.06em; } @@ -1430,8 +1436,8 @@ body.dark-mode .search-box input:focus-visible { .token-instructions ol { margin-left: 20px; color: var(--muted-foreground); - line-height: 1.5; - font-size: 14px; + line-height: 1.4; + font-size: 13px; } .token-link { @@ -1449,11 +1455,11 @@ body.dark-mode .search-box input:focus-visible { display: flex; flex-direction: column; gap: 8px; - margin: 14px 0 10px; + margin: 10px 0 8px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border); border-radius: 18px; - padding: 14px; + padding: 12px; } .token-code-stack { @@ -1485,7 +1491,7 @@ body.dark-mode .search-box input:focus-visible { border: 1px solid var(--input); border-radius: 12px; font-size: 15px; - font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; background: rgba(255, 255, 255, 0.04); color: var(--foreground); letter-spacing: 0.14em; @@ -1505,29 +1511,19 @@ body.dark-mode .search-box input:focus-visible { color: var(--muted-foreground); } -/* shadcn outline button */ -.copy-code-btn { - padding: 8px 14px; - background: rgba(255, 255, 255, 0.04); - color: var(--foreground); - border: 1px solid var(--input); - border-radius: 12px; - font-size: 12px; - font-weight: 600; - cursor: pointer; - transition: all 150ms; - font-family: 'IBM Plex Mono', ui-monospace, monospace; - text-transform: uppercase; +.token-action-row { + display: flex; + gap: 8px; } -.copy-code-btn:hover:not(:disabled) { - background: var(--accent); - color: var(--accent-foreground); +.token-action-row > * { + flex: 1; } -.copy-code-btn:disabled { - opacity: 0.5; - cursor: not-allowed; +.token-skip-link { + align-self: center; + padding: 0; + font-size: 11px; } /* Primary button */ @@ -1542,7 +1538,7 @@ body.dark-mode .search-box input:focus-visible { cursor: pointer; transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -1563,10 +1559,10 @@ body.dark-mode .search-box input:focus-visible { } @media (max-width: 560px) { - .token-input-group { + .token-input-group, + .token-action-row { flex-direction: column; } - .copy-code-btn, .validate-btn { width: 100%; } @@ -1591,17 +1587,16 @@ body.dark-mode .search-box input:focus-visible { } .security-note { - font-size: 11px; + font-size: 10px; color: var(--muted-foreground); - margin-top: 8px; + margin-top: 4px; display: flex; align-items: flex-start; - gap: 6px; - line-height: 1.4; - padding: 12px 14px; - border: 1px solid var(--border); - border-radius: 16px; - background: rgba(255, 255, 255, 0.04); + gap: 5px; + line-height: 1.35; + padding: 0; + border: none; + background: none; text-align: left; } @@ -1615,7 +1610,40 @@ body.dark-mode .search-box input:focus-visible { /* Repos Step */ .popular-repos { text-align: left; - margin: 16px 0; + margin: 12px 0 10px; +} + +.repos-step { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding-left: 4px; + padding-right: 6px; + padding-bottom: 10px; + scrollbar-gutter: stable; + overscroll-behavior: contain; +} + +.repos-step::-webkit-scrollbar { + width: 6px; +} + +.repos-step::-webkit-scrollbar-track { + background: transparent; +} + +.repos-step::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 9999px; +} + +.repos-step::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); +} + +.repo-source-panel { + margin-bottom: 8px; } .popular-repos h3 { @@ -1625,17 +1653,55 @@ body.dark-mode .search-box input:focus-visible { font-weight: 600; } +.repo-source-copy { + margin: -4px 0 10px; + max-width: none; + font-size: 12px; +} + +.repo-source-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; +} + +.repo-source-btn { + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 999px; + background: rgba(255, 255, 255, 0.04); + color: var(--muted-foreground); + font-size: 11px; + font-weight: 600; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + text-transform: uppercase; + cursor: pointer; + transition: all 150ms; +} + +.repo-source-btn:hover { + background: var(--accent); + color: var(--foreground); +} + +.repo-source-btn.active { + background: var(--primary); + color: var(--primary-foreground); + border-color: var(--primary); +} + .repo-suggestions { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; } .repo-suggestion { display: flex; align-items: center; justify-content: space-between; - padding: 10px 12px; + padding: 9px 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border); border-radius: 16px; @@ -1824,7 +1890,7 @@ body.dark-mode .search-box input:focus-visible { font-weight: 600; cursor: pointer; transition: opacity 150ms; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -1837,10 +1903,15 @@ body.dark-mode .search-box input:focus-visible { min-height: 0; } +.repos-step .step-subtitle { + margin: 0 auto 10px; + max-width: 340px; +} + /* Categories Step */ .categories-step .step-subtitle { margin-top: -8px; - margin-bottom: 20px; + margin-bottom: 16px; color: var(--muted-foreground); font-size: 14px; } @@ -1848,15 +1919,15 @@ body.dark-mode .search-box input:focus-visible { .categories-list { display: flex; flex-direction: column; - gap: 10px; - margin: 24px 0; + gap: 8px; + margin: 14px 0 10px; } .category-item { display: flex; align-items: center; - gap: 14px; - padding: 16px; + gap: 12px; + padding: 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border); border-radius: 18px; @@ -1870,8 +1941,8 @@ body.dark-mode .search-box input:focus-visible { } .category-icon { - width: 40px; - height: 40px; + width: 36px; + height: 36px; border-radius: 10px; display: flex; align-items: center; @@ -1924,7 +1995,7 @@ body.dark-mode .releases-icon { .category-controls { display: flex; - gap: 16px; + gap: 12px; align-items: center; flex-shrink: 0; } @@ -1942,7 +2013,7 @@ body.dark-mode .releases-icon { color: var(--muted-foreground); font-weight: 600; min-width: 38px; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -2108,45 +2179,49 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { font-size: 12px; color: var(--muted-foreground); font-style: italic; - margin-top: 16px; + margin-top: 10px; } /* Complete Step */ +.complete-step .step-icon { + margin-bottom: 14px; +} + .complete-step .step-icon svg { - width: 56px; - height: 56px; + width: 48px; + height: 48px; color: var(--foreground); stroke-width: 1.5; } .next-steps { - margin: 24px 0; + margin: 14px 0 10px; text-align: left; } .next-steps h3 { - margin-bottom: 14px; + margin-bottom: 10px; color: var(--muted-foreground); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; } .tips-grid { display: grid; - grid-template-columns: 1fr; + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } .tip-item { display: flex; - align-items: center; - gap: 14px; - padding: 14px 16px; + align-items: flex-start; + gap: 10px; + padding: 10px; background: rgba(255, 255, 255, 0.04); - border-radius: 18px; + border-radius: 16px; border: 1px solid var(--border); transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); } @@ -2162,17 +2237,17 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { display: flex; align-items: center; justify-content: center; - width: 36px; - height: 36px; - border-radius: 12px; + width: 32px; + height: 32px; + border-radius: 10px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border); color: var(--accent-color); } .tip-icon svg { - width: 14px; - height: 14px; + width: 13px; + height: 13px; } .tip-content { @@ -2182,7 +2257,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { .tip-content strong { display: block; color: var(--foreground); - font-size: 13px; + font-size: 12px; margin-bottom: 2px; font-weight: 600; } @@ -2190,15 +2265,15 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { .tip-content p { margin: 0; color: var(--muted-foreground); - font-size: 12px; - line-height: 1.4; + font-size: 11px; + line-height: 1.35; } .final-tips { - margin-top: 20px; - padding: 14px 16px; + margin-top: 10px; + padding: 10px 12px; background: rgba(255, 255, 255, 0.04); - border-radius: 18px; + border-radius: 16px; border: 1px solid var(--border); } @@ -2224,7 +2299,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { display: flex; justify-content: space-between; align-items: center; - padding-top: 16px; + padding-top: 12px; margin-top: auto; flex-shrink: 0; } @@ -2243,7 +2318,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider { cursor: pointer; transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); border: 1px solid transparent; - font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; text-transform: uppercase; } @@ -2329,6 +2404,10 @@ body.dark-mode .add-repo-btn:hover { max-width: none; } + .tips-grid { + grid-template-columns: 1fr; + } + .onboarding-nav { flex-direction: column; gap: 15px; diff --git a/popup/popup.html b/popup/popup.html index d5c63a4..13ae78f 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -4,7 +4,7 @@ GitHub Devwatch - + diff --git a/popup/views/activity-list-view.js b/popup/views/activity-list-view.js index 65c2064..5b4e70f 100644 --- a/popup/views/activity-list-view.js +++ b/popup/views/activity-list-view.js @@ -2,6 +2,8 @@ import { stateManager, useState } from '../../shared/state-manager.js'; import { showError } from '../../shared/error-handler.js'; import { safelyOpenUrl } from '../../shared/security.js'; +const OPTIONS_REPOSITORIES_URL = 'options/options.html?showAdd=true#repositories'; + /** * Renders the filtered activity list using ActivityListRenderer for efficient DOM updates * This is the main rendering function that delegates to optimized or legacy renderer @@ -78,7 +80,7 @@ export function renderActivities( e.preventDefault(); // Open options page with hash and query parameter - const optionsUrl = chrome.runtime.getURL('options/options.html#repositories?showAdd=true'); + const optionsUrl = chrome.runtime.getURL(OPTIONS_REPOSITORIES_URL); await chrome.tabs.create({ url: optionsUrl }); }); } @@ -299,7 +301,7 @@ function attachEventListeners( // Handle options link in empty state if (e.target.closest('#optionsLink')) { e.preventDefault(); - chrome.tabs.create({ url: chrome.runtime.getURL('options/options.html#repositories?showAdd=true') }); + chrome.tabs.create({ url: chrome.runtime.getURL(OPTIONS_REPOSITORIES_URL) }); } }; diff --git a/popup/views/onboarding-view.js b/popup/views/onboarding-view.js index 3d1b45d..1966bb6 100644 --- a/popup/views/onboarding-view.js +++ b/popup/views/onboarding-view.js @@ -6,6 +6,10 @@ import { requestGitHubDeviceCode } from '../../shared/auth.js'; import { OnboardingManager } from '../../shared/onboarding.js'; +import { + fetchGitHubRepoSource, + getGitHubRepoSourceConfig +} from '../../shared/github-repo-sources.js'; import { getAccessToken, getWatchedRepos, setAuthSession, setWatchedRepos } from '../../shared/storage-helpers.js'; import { resolveWatchedRepoInput } from '../../shared/repo-service.js'; import { CATEGORY_SETTINGS, createCategorySettings } from '../../shared/settings-schema.js'; @@ -13,11 +17,16 @@ import { escapeHtml } from '../../shared/sanitize.js'; // Create onboarding manager instance const onboardingManager = new OnboardingManager(); +const ONBOARDING_REPO_SOURCE_TYPES = ['mine', 'watched', 'starred', 'participating']; function getStatusMarkup(type, message) { return `
${escapeHtml(message)}
`; } +function getAnonymousRepoModeCopy() { + return 'You can add public repositories now and connect GitHub later for private repos and a higher API budget.'; +} + async function addWatchedRepoFromInput(rawInput) { const githubToken = await getAccessToken(); const existingRepos = await getWatchedRepos(); @@ -156,15 +165,18 @@ async function completePendingDeviceAuth(tokenData, elements, options = {}) { } function renderRepoSuggestion(repo) { - const rawOwner = repo?.owner?.login || 'unknown'; - const rawName = repo?.name || 'unknown'; + const normalizedFullName = repo?.fullName || ''; + const [fullNameOwner = '', fullNameName = ''] = normalizedFullName.split('/'); + const rawOwner = repo?.owner?.login || fullNameOwner || 'unknown'; + const rawName = repo?.name || fullNameName || 'unknown'; const owner = escapeHtml(rawOwner); const name = escapeHtml(rawName); const description = escapeHtml(repo?.description || `${repo?.language || 'Popular'} project worth watching`); const language = escapeHtml(repo?.language || ''); const repoFullName = `${rawOwner}/${rawName}`; - const stars = Number.isFinite(repo?.stargazers_count) - ? repo.stargazers_count.toLocaleString() + const starCount = Number.isFinite(repo?.stargazers_count) ? repo.stargazers_count : repo?.stars; + const stars = Number.isFinite(starCount) + ? starCount.toLocaleString() : ''; return ` @@ -335,6 +347,7 @@ function renderWelcomeStep() { async function renderTokenStep() { const tokenData = await onboardingManager.getStepData('token'); + const canSkipTokenStep = !tokenData?.validated; let statusHtml = ''; let buttonDisabled = ''; @@ -379,10 +392,12 @@ async function renderTokenStep() { value="${safeUserCode}" readonly > -

Copied automatically when possible. Click the code to select it, or use Copy.

- - +

Click the code field to copy it again.

+
+ +
+ ${canSkipTokenStep ? '' : ''}
${statusHtml}
@@ -392,31 +407,181 @@ async function renderTokenStep() { - Your GitHub session stays in Chrome session storage for the current browser session only. It's used for GitHub API access and is cleared when the browser session ends. + GitHub access stays in this Chrome profile until you disconnect it or reset DevWatch.

`; } +function renderRepoSourceButton(type, selectedSource) { + const sourceConfig = getGitHubRepoSourceConfig(type); + const isSelected = type === selectedSource; + + return ` + + `; +} + +function renderRepoSuggestionsContent(repos, { loadingMessage = 'Loading repositories...', emptyMessage = 'No repositories found.' } = {}) { + if (!Array.isArray(repos)) { + return `
${escapeHtml(loadingMessage)}
`; + } + + if (repos.length === 0) { + return `
${escapeHtml(emptyMessage)}
`; + } + + return repos.map(renderRepoSuggestion).join(''); +} + +function filterSuggestedRepos(repos, watchedRepos) { + const watchedRepoNames = new Set( + (Array.isArray(watchedRepos) ? watchedRepos : []) + .map(repo => repo?.fullName?.toLowerCase()) + .filter(Boolean) + ); + + return (Array.isArray(repos) ? repos : []).filter((repo) => { + const fullName = String(repo?.fullName || `${repo?.owner?.login || ''}/${repo?.name || ''}`).toLowerCase(); + return fullName && !watchedRepoNames.has(fullName); + }); +} + +async function getSavedRepoSourceState() { + const state = await onboardingManager.getStepData('repoSource'); + return state && typeof state === 'object' + ? state + : {}; +} + +async function saveRepoSourceState(nextState) { + await onboardingManager.saveStepData('repoSource', nextState); +} + +async function loadAuthenticatedRepoSourcePreview(sourceType, options = {}) { + const githubToken = await getAccessToken(); + if (!githubToken) { + return []; + } + + const sourceState = await getSavedRepoSourceState(); + const cachedPreview = sourceState?.previews?.[sourceType]; + if (!options.force && Array.isArray(cachedPreview)) { + return cachedPreview; + } + + const existingRepos = await getWatchedRepos(); + const previewRepos = await fetchGitHubRepoSource(sourceType, githubToken, { + perPage: 12, + maxPages: 1, + maxRepos: 12, + timeLimitMs: 10000 + }); + + const filteredPreview = previewRepos + .filter((repo) => !existingRepos.some(existing => existing?.fullName?.toLowerCase() === repo.fullName.toLowerCase())) + .slice(0, 3); + + const nextState = { + ...sourceState, + selectedSource: sourceType, + previews: { + ...(sourceState.previews || {}), + [sourceType]: filteredPreview + }, + loadedSources: Array.from(new Set([...(sourceState.loadedSources || []), sourceType])), + errors: { + ...(sourceState.errors || {}), + [sourceType]: null + } + }; + + await saveRepoSourceState(nextState); + return filteredPreview; +} + export async function renderReposStep() { - const saved = await onboardingManager.getStepData('popularRepos'); - const hasSavedRepos = Array.isArray(saved) && saved.length > 0; + const githubToken = await getAccessToken(); + const watchedRepos = await getWatchedRepos(); + const publicOnlyMode = !githubToken; + + if (!publicOnlyMode) { + const sourceState = await getSavedRepoSourceState(); + const selectedSource = ONBOARDING_REPO_SOURCE_TYPES.includes(sourceState?.selectedSource) + ? sourceState.selectedSource + : 'mine'; + const sourceConfig = getGitHubRepoSourceConfig(selectedSource); + const cachedPreview = Array.isArray(sourceState?.previews?.[selectedSource]) + ? filterSuggestedRepos(sourceState.previews[selectedSource], watchedRepos).slice(0, 3) + : null; + const isLoaded = Array.isArray(sourceState?.loadedSources) && sourceState.loadedSources.includes(selectedSource); + const sourceError = sourceState?.errors?.[selectedSource]; + + return ` +
+

Build your watchlist

+

Add repos manually, or pull from the same GitHub sources available in settings.

+ + - let popularRepos; - if (hasSavedRepos) { - popularRepos = saved; - } else { - popularRepos = await onboardingManager.getPopularRepos(); +
+

Or add one directly

+
+ + +
+
+ +
+
+ `; } + const saved = await onboardingManager.getStepData('popularRepos'); + const hasSavedRepos = Array.isArray(saved) && saved.length > 0; + const popularRepos = hasSavedRepos ? saved : await onboardingManager.getPopularRepos(); + const visibleRepos = filterSuggestedRepos(popularRepos, watchedRepos).slice(0, 3); + return `

Build your watchlist

+ ${publicOnlyMode ? `

${getAnonymousRepoModeCopy()}

` : ''}