diff --git a/.gitignore b/.gitignore
index 1b22c4f..87a5f76 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,12 @@ WINDSURF.md
.gemini/
.windsurf/
.continue/
+
+# Legacy local design / planning artifacts
+.superpowers/
+docs/superpowers/
+icon-concepts.html
+*-450.png
+v2-*.png
+onboarding-*.png
+current-page.png
diff --git a/README.md b/README.md
index 121767e..79f05fe 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
Monitor pull requests, issues, and releases across multiple GitHub repositories from a Chrome extension. It keeps a local activity feed, badge counts, and optional browser notifications without adding another hosted service to the workflow.
+Best for people who follow several repos and want one local review queue instead of GitHub email noise, browser-tab sprawl, or constantly checking each repository by hand.
+
[](https://chromewebstore.google.com/detail/github-devwatch/dbgjgcaphfcfgppicmbiafcgcabikjch)
[](LICENSE)
[](https://github.com/jonmartin721/devwatch-github/actions)
@@ -11,9 +13,11 @@ Monitor pull requests, issues, and releases across multiple GitHub repositories
- **Guided Setup** - Built-in GitHub sign-in flow and repository selection
- **Browser Notifications** - Get notified about new PRs, issues, and releases
-- **Multi-Repo Monitoring** - Watch up to 50 repositories from one interface
+- **Multi-Repo Monitoring** - Watch up to 50 repositories by default, with an advanced option to go beyond that
- **Configurable Updates** - Check every 5, 15, 30, or 60 minutes
- **Activity Filtering** - Search and filter by repo and activity type
+- **Repo Controls** - Pin favorites, mute noisy repos, or snooze them temporarily
+- **Flexible Repo Input** - Add repos with `owner/repo`, a GitHub URL, or an npm package name
- **Badge Counts** - Unread count on the extension icon
- **Direct API Access** - Talks to GitHub directly, with optional npm registry lookups only when you use package-name import
@@ -21,6 +25,13 @@ Monitor pull requests, issues, and releases across multiple GitHub repositories
+## Requirements
+
+- 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
+
## Installation
### From Chrome Web Store (Recommended)
@@ -30,7 +41,7 @@ Monitor pull requests, issues, and releases across multiple GitHub repositories
3. Grant permissions when prompted
4. Follow the guided setup wizard on first launch
-**GitHub Sign-In Permissions**: DevWatch uses GitHub OAuth device flow and requests `repo` plus `read:user` so it can monitor private repositories and show the connected account in the UI.
+**GitHub Sign-In Permissions**: DevWatch uses GitHub OAuth device flow and requests `repo` plus `read:user` so it can monitor private repositories and show the connected account in the UI. The current build uses one sign-in path for both public and private monitoring, so there is not a separate public-only permission mode yet.
### Manual Installation (For Development)
@@ -38,6 +49,7 @@ Monitor pull requests, issues, and releases across multiple GitHub repositories
```bash
git clone https://github.com/jonmartin721/devwatch-github.git
cd devwatch-github
+npm install
```
2. Load the extension in Chrome:
@@ -66,15 +78,28 @@ The built-in setup flow walks you through:
- Click the extension icon to view your activity feed
- Get browser notifications for new activity
- Badge count shows unread items at a glance
+- Use the archive to revisit read items or clear them when you're done
+- Pin, mute, or snooze repositories to control noisy feeds
- Manage repositories and preferences anytime in Settings
## How to Use
### Popup Interface
-Filter by type (All/PRs/Issues/Releases), search activities, refresh manually, or browse the archive. Click any item to open in GitHub.
+The popup is the main day-to-day view:
+- Filter by type (All/PRs/Issues/Releases)
+- Search across activity in watched repositories
+- Refresh manually, browse the archive, and open any item in GitHub
+- Pin favorite repositories or snooze noisy ones to keep the feed focused
+
+
@@ -84,12 +109,9 @@ Manage your GitHub connection, watched repositories, activity filters, check int
Here's what using the extension looks like day-to-day:
-1. You're working and get a notification: "2 new activities in your-org/api-server"
-2. Click the extension icon to see the feed
-3. See "Pull Request #145: Add OAuth2 authentication" (3 minutes ago)
-4. Click the item to open it in GitHub
-5. Review and comment on the PR
-6. When you return to the extension, it's marked as read
+1. A browser notification or badge count lets you know there is new activity.
+2. Open the popup to scan updates across your watched repositories in one place.
+3. Open the item in GitHub when you want to review it, and let DevWatch keep the rest of the queue visible locally.
The extension keeps up to 2000 items in your local history, so you can always check something you saw earlier. Badge count updates automatically as you read items.
@@ -101,9 +123,9 @@ 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, and the current build encrypts the GitHub auth session before persisting it locally while keeping a decrypted session copy available at runtime.
+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.
-- **Direct network access** - Requests go to `api.github.com`, plus `registry.npmjs.org` only when you use package-name lookup
+- **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`
- **Defensive client code** - The codebase includes URL validation, content security policy rules, and sanitization tests
- **No formal audit claim** - These measures improve the local handling of data, but they are not a substitute for securing the browser profile and GitHub account you use with the extension
@@ -125,7 +147,7 @@ Items are automatically removed when they exceed the 2000 item limit (keeping th
GitHub gives authenticated users 5,000 API requests per hour. Each repo check uses 1-3 requests, so even checking 50 repos every 15 minutes keeps you well under the limit.
-The extension defaults to checking every 15 minutes. You can change this to 5, 30, or 60 minutes in settings. The 50-repo limit is just to keep things reasonable - you won't hit GitHub's rate limits even at that level.
+The extension defaults to checking every 15 minutes. You can change this to 5, 30, or 60 minutes in settings. The default 50-repo limit is there to keep rate usage predictable, but Advanced settings also let you enable unlimited repositories if you want to trade more flexibility for more rate-limit risk.
## Development
@@ -198,7 +220,7 @@ This is an actively maintained side project. Some features under consideration:
- **Comment notifications** - Track new comments on issues and PRs
- **Mention tracking** - Get notified when you're mentioned
- **Multiple GitHub accounts** - Switch between different accounts
-- **GitHub OAuth** - Simplified authentication with one-click login
+- **Browser redirect sign-in** - Offer a less manual alternative to the current device-flow sign-in
- **Internationalization** - Support for multiple languages
- **Dashboard view** - Full-page dashboard for all activity
diff --git a/background.js b/background.js
index 24dc655..d8964d3 100644
--- a/background.js
+++ b/background.js
@@ -1,7 +1,27 @@
import { createHeaders, handleApiResponse, mapActivity, filterActivitiesByDate } from './shared/github-api.js';
-import { getSyncItems, getLocalItems, setLocalItem, getExcludedRepos, getAccessToken, getFilteringSettings, getWatchedRepos } from './shared/storage-helpers.js';
+import {
+ getSyncItems,
+ getLocalItems,
+ getSettings,
+ setLocalItem,
+ setSyncItem,
+ getExcludedRepos,
+ getAccessToken,
+ getFilteringSettings,
+ getWatchedRepos
+} from './shared/storage-helpers.js';
import { extractRepoName } from './shared/repository-utils.js';
import { safelyOpenUrl } from './shared/security.js';
+import { prepareActivitiesForStorage, countUnreadActivities } from './shared/feed-policy.js';
+import {
+ clearArchivedFeedData,
+ getAllActivityIds,
+ getRepoUnreadActivityIds,
+ markActivitiesAsRead,
+ markActivityAsUnread,
+ removeRepoFeedData,
+ upsertSnoozedRepo
+} from './shared/feed-mutations.js';
const ALARM_NAME = 'checkGitHub';
const DEFAULT_INTERVAL = 15;
@@ -46,6 +66,10 @@ function setupAlarm(intervalMinutes) {
});
}
+function isValidInterval(interval) {
+ return Number.isFinite(interval) && interval > 0;
+}
+
if (typeof chrome !== 'undefined' && chrome.alarms) {
// Listen for alarm and check GitHub
chrome.alarms.onAlarm.addListener((alarm) => {
@@ -358,16 +382,7 @@ async function storeActivities(newActivities) {
// Get list of repos to exclude
const excludedRepos = getExcludedRepos(mutedRepos, activeSnoozedRepos);
- // Merge new activities, avoiding duplicates
- const existingIds = new Set(activities.map(a => a.id));
- const uniqueNew = newActivities.filter(a => !existingIds.has(a.id));
-
- // Filter out activities from excluded repos
- const allActivities = [...uniqueNew, ...activities];
- const filtered = allActivities.filter(a => !excludedRepos.has(a.repo));
-
- // Keep up to 2000 items (same limit as state manager)
- const updated = filtered.slice(0, 2000);
+ const updated = prepareActivitiesForStorage(activities, newActivities, { excludedRepos });
// Try to store, with error handling for quota exceeded
try {
@@ -377,12 +392,12 @@ async function storeActivities(newActivities) {
if (storageError.message.includes('quota')) {
// Try to free up space by reducing to 50 items
- const reduced = filtered.slice(0, 50);
+ const reduced = updated.slice(0, 50);
try {
await setLocalItem('activities', reduced);
} catch (_retryError) {
// If still failing, try with just 25 items
- const minimal = filtered.slice(0, 25);
+ const minimal = updated.slice(0, 25);
await setLocalItem('activities', minimal);
}
} else {
@@ -399,20 +414,14 @@ async function storeActivities(newActivities) {
async function updateBadge() {
const { readItems = [], activities = [] } = await getLocalItems(['readItems', 'activities']);
const { itemExpiryHours } = await getSyncItems(['itemExpiryHours']);
-
- let filteredActivities = activities;
-
- // Apply time-based expiry filter (same logic as getFilteredActivities in state-manager)
- if (itemExpiryHours !== null && itemExpiryHours > 0) {
- const expiryThreshold = Date.now() - (itemExpiryHours * 60 * 60 * 1000);
- filteredActivities = filteredActivities.filter(activity => {
- const activityTime = new Date(activity.createdAt).getTime();
- return activityTime >= expiryThreshold;
- });
- }
-
- // Count only unread items (not in readItems)
- const unreadCount = filteredActivities.filter(a => !readItems.includes(a.id)).length;
+ const { mutedRepos, snoozedRepos } = await getFilteringSettings();
+ const activeSnoozedRepos = await cleanExpiredSnoozes(snoozedRepos);
+ const excludedRepos = getExcludedRepos(mutedRepos, activeSnoozedRepos);
+ const unreadCount = countUnreadActivities(activities, {
+ excludedRepos,
+ itemExpiryHours,
+ readItems
+ });
chrome.action.setBadgeText({ text: unreadCount > 0 ? unreadCount.toString() : '' });
chrome.action.setBadgeBackgroundColor({ color: '#0366d6' });
@@ -467,145 +476,233 @@ function showNotifications(activities, notificationSettings = {}) {
});
}
-if (typeof chrome !== 'undefined' && chrome.runtime) {
- // Allow manual check from popup
- chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
- // Validate request object
- if (!request || typeof request.action !== 'string') {
- sendResponse({ success: false, error: 'Invalid request' });
- return false;
- }
+async function getFeedStorageState() {
+ const { activities = [], readItems = [] } = await getLocalItems(['activities', 'readItems']);
+ return { activities, readItems };
+}
- // Handle different message types with consistent async/error handling
- if (request.action === 'checkNow') {
- checkGitHubActivity()
- .then(() => sendResponse({ success: true }))
- .catch((error) => {
- console.error('[DevWatch] Error in checkNow handler:', error);
- sendResponse({ success: false, error: error.message });
- });
- return true; // Required for async response
- }
+async function persistFeedStorageState({ activities, readItems }) {
+ const writes = [];
- if (request.action === 'clearBadge') {
- try {
- chrome.action.setBadgeText({ text: '' });
- sendResponse({ success: true });
- } catch (error) {
- console.error('[DevWatch] Error in clearBadge handler:', error);
- sendResponse({ success: false, error: error.message });
- }
- return true; // Keep consistent async pattern
- }
+ if (activities !== undefined) {
+ writes.push(setLocalItem('activities', activities));
+ }
- if (request.action === 'markAsRead') {
- if (!request.id) {
- sendResponse({ success: false, error: 'Missing id parameter' });
- return false;
- }
+ if (readItems !== undefined) {
+ writes.push(setLocalItem('readItems', readItems));
+ }
- chrome.storage.local.get(['readItems'], (result) => {
- try {
- const readItems = result.readItems || [];
- if (!readItems.includes(request.id)) {
- readItems.push(request.id);
- chrome.storage.local.set({ readItems }, () => {
- if (chrome.runtime.lastError) {
- console.error('[DevWatch] Storage error in markAsRead:', chrome.runtime.lastError);
- sendResponse({ success: false, error: chrome.runtime.lastError.message });
- } else {
- updateBadge().catch(err => console.error('[DevWatch] Badge update error:', err));
- sendResponse({ success: true });
- }
- });
- } else {
- sendResponse({ success: true });
- }
- } catch (error) {
- console.error('[DevWatch] Error in markAsRead handler:', error);
- sendResponse({ success: false, error: error.message });
- }
- });
- return true;
- }
+ await Promise.all(writes);
+ await updateBadge();
+}
- if (request.action === 'markAsUnread') {
- if (!request.id) {
- sendResponse({ success: false, error: 'Missing id parameter' });
- return false;
- }
+async function handleMarkAsRead(id) {
+ if (!id) {
+ throw new Error('Missing id parameter');
+ }
- chrome.storage.local.get(['readItems'], (result) => {
- try {
- const readItems = result.readItems || [];
- const updated = readItems.filter(id => id !== request.id);
- chrome.storage.local.set({ readItems: updated }, () => {
- if (chrome.runtime.lastError) {
- console.error('[DevWatch] Storage error in markAsUnread:', chrome.runtime.lastError);
- sendResponse({ success: false, error: chrome.runtime.lastError.message });
- } else {
- updateBadge().catch(err => console.error('[DevWatch] Badge update error:', err));
- sendResponse({ success: true });
- }
- });
- } catch (error) {
- console.error('[DevWatch] Error in markAsUnread handler:', error);
- sendResponse({ success: false, error: error.message });
- }
- });
- return true;
+ const { readItems } = await getFeedStorageState();
+ const updatedReadItems = markActivitiesAsRead(readItems, [id]);
+
+ if (updatedReadItems.length !== readItems.length) {
+ await persistFeedStorageState({ readItems: updatedReadItems });
+ } else {
+ await updateBadge();
+ }
+}
+
+async function handleMarkAsUnread(id) {
+ if (!id) {
+ throw new Error('Missing id parameter');
+ }
+
+ const { readItems } = await getFeedStorageState();
+ const updatedReadItems = markActivityAsUnread(readItems, id);
+ await persistFeedStorageState({ readItems: updatedReadItems });
+}
+
+async function handleMarkActivitiesAsRead(ids = []) {
+ const { readItems } = await getFeedStorageState();
+ const updatedReadItems = markActivitiesAsRead(readItems, ids);
+ await persistFeedStorageState({ readItems: updatedReadItems });
+}
+
+async function handleMarkAllAsRead() {
+ const { activities, readItems } = await getFeedStorageState();
+ const updatedReadItems = markActivitiesAsRead(readItems, getAllActivityIds(activities));
+ await persistFeedStorageState({ readItems: updatedReadItems });
+}
+
+async function handleMarkRepoAsRead(repo) {
+ if (!repo) {
+ throw new Error('Missing repo parameter');
+ }
+
+ const { activities, readItems } = await getFeedStorageState();
+ const unreadIds = getRepoUnreadActivityIds(activities, readItems, repo);
+ const updatedReadItems = markActivitiesAsRead(readItems, unreadIds);
+ await persistFeedStorageState({ readItems: updatedReadItems });
+}
+
+async function handleClearArchive() {
+ const feedState = await getFeedStorageState();
+ await persistFeedStorageState(clearArchivedFeedData(feedState));
+}
+
+async function handleSnoozeRepo(repo) {
+ if (!repo) {
+ throw new Error('Missing repo parameter');
+ }
+
+ const settings = await getSettings();
+ const snoozeHours = Number.isFinite(Number(settings.snoozeHours)) ? Number(settings.snoozeHours) : 1;
+ const expiresAt = Date.now() + (snoozeHours * 60 * 60 * 1000);
+ const updatedSnoozedRepos = upsertSnoozedRepo(settings.snoozedRepos, repo, expiresAt);
+
+ await setSyncItem('snoozedRepos', updatedSnoozedRepos);
+
+ if (settings.markReadOnSnooze) {
+ const { activities, readItems } = await getFeedStorageState();
+ const unreadIds = getRepoUnreadActivityIds(activities, readItems, repo);
+ const updatedReadItems = markActivitiesAsRead(readItems, unreadIds);
+ await persistFeedStorageState({ readItems: updatedReadItems });
+ return;
+ }
+
+ await updateBadge();
+}
+
+async function handleRemoveRepoData(repo) {
+ if (!repo) {
+ throw new Error('Missing repo parameter');
+ }
+
+ const feedState = await getFeedStorageState();
+ await persistFeedStorageState(removeRepoFeedData(feedState, repo));
+}
+
+async function runRuntimeAction(request) {
+ switch (request.action) {
+ case 'checkNow':
+ await checkGitHubActivity();
+ return { success: true };
+ case 'markAsRead':
+ await handleMarkAsRead(request.id);
+ return { success: true };
+ case 'markAsUnread':
+ await handleMarkAsUnread(request.id);
+ return { success: true };
+ case 'markActivitiesAsRead':
+ await handleMarkActivitiesAsRead(request.ids || []);
+ return { success: true };
+ case 'markAllAsRead':
+ await handleMarkAllAsRead();
+ return { success: true };
+ case 'markRepoAsRead':
+ await handleMarkRepoAsRead(request.repo);
+ return { success: true };
+ case 'clearArchive':
+ await handleClearArchive();
+ return { success: true };
+ case 'snoozeRepo':
+ await handleSnoozeRepo(request.repo);
+ return { success: true };
+ case 'removeRepoData':
+ await handleRemoveRepoData(request.repo);
+ return { success: true };
+ default:
+ return null;
+ }
+}
+
+function handleRuntimeMessage(request, sender, sendResponse) {
+ // Validate request object
+ if (!request || typeof request.action !== 'string') {
+ sendResponse({ success: false, error: 'Invalid request' });
+ return false;
+ }
+
+ if (request.action === 'clearBadge') {
+ try {
+ chrome.action.setBadgeText({ text: '' });
+ sendResponse({ success: true });
+ } catch (error) {
+ console.error('[DevWatch] Error in clearBadge handler:', error);
+ sendResponse({ success: false, error: error.message });
}
+ return true; // Keep consistent async pattern
+ }
- if (request.action === 'markAllAsRead') {
- chrome.storage.local.get(['activities'], (result) => {
- try {
- const activities = result.activities || [];
- const allIds = activities.map(a => a.id);
- chrome.storage.local.set({ readItems: allIds }, () => {
- if (chrome.runtime.lastError) {
- console.error('[DevWatch] Storage error in markAllAsRead:', chrome.runtime.lastError);
- sendResponse({ success: false, error: chrome.runtime.lastError.message });
- } else {
- updateBadge().catch(err => console.error('[DevWatch] Badge update error:', err));
- sendResponse({ success: true });
- }
- });
- } catch (error) {
- console.error('[DevWatch] Error in markAllAsRead handler:', error);
- sendResponse({ success: false, error: error.message });
- }
- });
- return true;
+ if (request.action === 'updateInterval') {
+ const interval = Number(request.interval);
+ if (!isValidInterval(interval)) {
+ sendResponse({ success: false, error: 'Invalid interval parameter' });
+ return false;
}
- // Unknown action
- sendResponse({ success: false, error: 'Unknown action' });
- return false;
- });
+ try {
+ setupAlarm(interval);
+ sendResponse({ success: true });
+ } catch (error) {
+ console.error('[DevWatch] Error in updateInterval handler:', error);
+ sendResponse({ success: false, error: error.message });
+ }
+ return true;
+ }
+
+ runRuntimeAction(request)
+ .then((result) => {
+ if (result) {
+ sendResponse(result);
+ } else {
+ sendResponse({ success: false, error: 'Unknown action' });
+ }
+ })
+ .catch((error) => {
+ console.error(`[DevWatch] Error in ${request.action} handler:`, error);
+ sendResponse({ success: false, error: error.message });
+ });
+ return true;
+
+}
+
+if (typeof chrome !== 'undefined' && chrome.runtime) {
+ // Allow manual check from popup
+ chrome.runtime.onMessage.addListener(handleRuntimeMessage);
}
// Export functions for testing
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
setupAlarm,
+ isValidInterval,
+ handleRuntimeMessage,
checkGitHubActivity,
fetchRepoActivity,
storeActivities,
updateBadge,
showNotifications,
cleanExpiredSnoozes,
- cleanupOldUnmutedEntries
+ cleanupOldUnmutedEntries,
+ getFeedStorageState,
+ persistFeedStorageState,
+ runRuntimeAction
};
}
// ES6 exports for tests
export {
setupAlarm,
+ isValidInterval,
+ handleRuntimeMessage,
checkGitHubActivity,
fetchRepoActivity,
storeActivities,
updateBadge,
showNotifications,
cleanExpiredSnoozes,
- cleanupOldUnmutedEntries
+ cleanupOldUnmutedEntries,
+ getFeedStorageState,
+ persistFeedStorageState,
+ runRuntimeAction
};
diff --git a/jsconfig.json b/jsconfig.json
index 2e4ac03..55cce57 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"target": "ES2020",
- "module": "commonjs",
+ "module": "ES2020",
"checkJs": false,
- "moduleResolution": "node",
+ "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"noEmit": true,
"skipLibCheck": true,
diff --git a/manifest.json b/manifest.json
index a85d343..76e67a3 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "GitHub Devwatch",
- "version": "1.0.2",
+ "version": "1.0.3",
"description": "Monitor pull requests, issues, and releases across GitHub repositories with notifications and a local activity feed.",
"author": "Jonathan Martin",
"permissions": [
@@ -34,6 +34,6 @@
"128": "icons/icon128.png"
},
"content_security_policy": {
- "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://api.github.com https://github.com https://registry.npmjs.org; img-src 'self' data: https://github.com https://*.github.com https://githubusercontent.com https://*.githubusercontent.com; default-src 'self'; style-src 'self'"
+ "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://api.github.com https://github.com https://registry.npmjs.org; img-src 'self' data: https://github.com https://*.github.com https://githubusercontent.com https://*.githubusercontent.com; default-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com"
}
}
diff --git a/options/controllers/export-import-controller.js b/options/controllers/export-import-controller.js
index 1267cad..419d7eb 100644
--- a/options/controllers/export-import-controller.js
+++ b/options/controllers/export-import-controller.js
@@ -1,26 +1,19 @@
import { NotificationManager } from '../../shared/ui/notification-manager.js';
-import { getWatchedRepos, setWatchedRepos } from '../../shared/storage-helpers.js';
+import { getSettings, setWatchedRepos, updateSettings } from '../../shared/storage-helpers.js';
+import { normalizeSettings, pickSyncSettings } from '../../shared/settings-schema.js';
const notifications = NotificationManager.getInstance();
export async function exportSettings() {
try {
- const syncData = await chrome.storage.sync.get(null);
- const watchedRepos = await getWatchedRepos();
+ const settings = await getSettings();
const exportData = {
version: '1.0.0',
exportedAt: new Date().toISOString(),
settings: {
- watchedRepos,
- mutedRepos: syncData.mutedRepos || [],
- pinnedRepos: syncData.pinnedRepos || [],
- filters: syncData.filters || { prs: true, issues: true, releases: true },
- notifications: syncData.notifications || { prs: true, issues: true, releases: true },
- theme: syncData.theme || 'system',
- checkInterval: syncData.checkInterval || 15,
- snoozeHours: syncData.snoozeHours || 1,
- snoozedRepos: syncData.snoozedRepos || []
+ watchedRepos: settings.watchedRepos,
+ ...pickSyncSettings(settings)
}
};
@@ -66,16 +59,7 @@ export async function handleImportFile(event, loadSettingsCallback) {
const settings = importData.settings;
await setWatchedRepos(settings.watchedRepos || []);
- await chrome.storage.sync.set({
- mutedRepos: settings.mutedRepos || [],
- pinnedRepos: settings.pinnedRepos || [],
- filters: settings.filters || { prs: true, issues: true, releases: true },
- notifications: settings.notifications || { prs: true, issues: true, releases: true },
- theme: settings.theme || 'system',
- checkInterval: settings.checkInterval || 15,
- snoozeHours: settings.snoozeHours || 1,
- snoozedRepos: settings.snoozedRepos || []
- });
+ await updateSettings(normalizeSettings(settings));
if (settings.checkInterval) {
chrome.runtime.sendMessage({
diff --git a/options/controllers/import-controller.js b/options/controllers/import-controller.js
index 4f2dd3f..0cea908 100644
--- a/options/controllers/import-controller.js
+++ b/options/controllers/import-controller.js
@@ -1,6 +1,7 @@
import { STORAGE_CONFIG } from '../../shared/config.js';
import { getAccessToken, getSyncItem, setWatchedRepos } from '../../shared/storage-helpers.js';
import { createHeaders } from '../../shared/github-api.js';
+import { getRepoFullName, normalizeWatchedRepoRecord } from '../../shared/repo-service.js';
import { escapeHtml, unescapeHtml } from '../../shared/sanitize.js';
import { formatDateVerbose } from '../../shared/utils.js';
import { STAR_ICON, createSvg } from '../../shared/icons.js';
@@ -62,25 +63,6 @@ function formatNumber(num) {
return num.toString();
}
-function getRepoFullName(repo) {
- if (typeof repo === 'string') {
- return repo;
- }
-
- return repo?.fullName || '';
-}
-
-function normalizeImportedRepo(repo) {
- return {
- fullName: repo.fullName,
- description: repo.description || 'No description provided',
- language: repo.language || 'Unknown',
- stars: repo.stars || 0,
- updatedAt: repo.updatedAt || new Date().toISOString(),
- addedAt: new Date().toISOString()
- };
-}
-
export async function openImportModal(type, watchedRepos) {
const token = await getAccessToken();
if (!token) {
@@ -364,7 +346,7 @@ export async function importSelectedRepos(watchedRepos, onReposAdded) {
const nextWatchedRepos = [
...watchedRepos,
- ...uniqueReposToImport.map(normalizeImportedRepo)
+ ...uniqueReposToImport.map(repo => normalizeWatchedRepoRecord(repo))
];
await setWatchedRepos(nextWatchedRepos);
diff --git a/options/controllers/token-controller.js b/options/controllers/token-controller.js
index 07fd82f..2078be7 100644
--- a/options/controllers/token-controller.js
+++ b/options/controllers/token-controller.js
@@ -37,7 +37,29 @@ function setDeviceCode(userCode = '') {
deviceCodeInput.value = userCode;
deviceCodeSection.classList.toggle('hidden', !userCode);
- deviceCodeSection.style.display = userCode ? 'block' : 'none';
+ deviceCodeSection.style.display = userCode ? 'flex' : 'none';
+}
+
+function setHelpText(message = '') {
+ const helpEl = document.getElementById('token-help');
+
+ if (!helpEl) {
+ return;
+ }
+
+ helpEl.textContent = 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.';
+ }
+
+ 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.';
}
function setStatus(message = '', statusClass = '') {
@@ -59,6 +81,10 @@ export function applyStoredConnection(authSession, options = {}) {
setRepoAccessState(isConnected);
setDeviceCode(options.userCode || '');
+ setHelpText(options.helpText || getHelpText({
+ isConnected,
+ isWaiting: options.statusClass === 'checking'
+ }));
if (connectBtn) {
connectBtn.disabled = false;
@@ -123,12 +149,22 @@ 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.');
try {
const result = await completeGitHubDeviceAuth({
- onCode: ({ userCode }) => {
+ onCode: async ({ userCode }) => {
setDeviceCode(userCode || '');
- setStatus(`Enter ${userCode} on GitHub to finish connecting.`, 'checking');
+ const copied = navigator.clipboard?.writeText
+ ? await navigator.clipboard.writeText(userCode).then(() => true).catch(() => false)
+ : false;
+ setStatus(
+ copied
+ ? `Code ${userCode} copied to clipboard — paste it on the GitHub page that opens.`
+ : `Enter ${userCode} on GitHub to finish connecting.`,
+ 'checking'
+ );
+ setHelpText(getHelpText({ isWaiting: true }));
}
});
diff --git a/options/options.css b/options/options.css
index e5b35a6..ff59892 100644
--- a/options/options.css
+++ b/options/options.css
@@ -1,52 +1,19 @@
+/* ================================================
+ DEVWATCH OPTIONS — Terminal Ledger Theme
+ ================================================ */
+
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
-:root {
- --bg-primary: #ffffff;
- --bg-secondary: #f6f8fa;
- --bg-tertiary: #f6f8fa;
- --border-color: #e1e4e8;
- --border-color-dark: #d0d7de;
- --text-primary: #24292e;
- --text-secondary: #586069;
- --link-color: #0366d6;
- --success-bg: #d4edda;
- --success-text: #155724;
- --success-border: #c3e6cb;
- --error-bg: #f8d7da;
- --error-text: #721c24;
- --error-border: #f5c6cb;
- --danger-bg: #dc3545;
- --danger-hover: #c82333;
-
- }
-
-body.dark-mode {
- --bg-primary: #0d1117;
- --bg-secondary: #161b22;
- --bg-tertiary: #161b22;
- --border-color: #30363d;
- --border-color-dark: #30363d;
- --text-primary: #c9d1d9;
- --text-secondary: #8b949e;
- --link-color: #58a6ff;
- --success-bg: #1b4620;
- --success-text: #7ee787;
- --success-border: #2d5a2d;
- --error-bg: #4b1e1e;
- --error-text: #f8d7da;
- --error-border: #6e2a2a;
- --danger-bg: #dc3545;
- --danger-hover: #c82333;
-}
-
body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
- font-size: 15px;
- background: var(--bg-secondary);
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
+ font-size: 14px;
+ background:
+ radial-gradient(circle at top right, var(--bg-gradient-glow), transparent 28%),
+ linear-gradient(180deg, var(--background) 0%, var(--card) 100%);
color: var(--text-primary);
padding: 0;
margin: 0;
@@ -132,7 +99,7 @@ body {
/* Warning text block */
.warning-text-block {
color: #d97706;
- font-size: 13px;
+ font-size: 14px;
margin-top: 4px;
display: flex;
align-items: center;
@@ -167,13 +134,13 @@ body {
.container {
display: flex;
min-height: 100vh;
- background: var(--bg-primary);
+ background: transparent;
}
.content-wrapper {
flex: 1;
overflow-y: auto;
- padding: 40px;
+ padding: 36px 40px 44px;
max-width: 1200px;
}
@@ -185,6 +152,7 @@ h1 {
font-size: calc(var(--font-size-base) + 9px);
margin-bottom: 8px;
color: var(--text-primary);
+ letter-spacing: -0.04em;
}
.subtitle {
@@ -196,34 +164,38 @@ h1 {
.tab-navigation {
display: flex;
flex-direction: column;
- padding: 8px;
- background: var(--bg-secondary);
+ padding: 12px;
+ background: rgba(255, 255, 255, 0.035);
border-right: 1px solid var(--border-color);
- min-width: 200px;
+ min-width: 228px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
scrollbar-width: thin;
+ backdrop-filter: blur(14px);
}
.tab-list {
display: flex;
flex-direction: column;
- gap: 4px;
+ gap: 8px;
}
.sidebar-header {
- padding: 16px 16px 12px;
- margin-bottom: 8px;
- border-bottom: 2px solid var(--border-color);
+ padding: 16px 16px 14px;
+ margin-bottom: 10px;
+ border-bottom: 1px solid var(--border-color);
}
.sidebar-header h2 {
- font-size: 18px;
+ font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
+ letter-spacing: -0.03em;
}
.tab-navigation::-webkit-scrollbar {
@@ -239,40 +211,31 @@ h1 {
display: flex;
align-items: center;
gap: 12px;
- padding: 12px 16px;
+ padding: 12px 14px;
background: transparent;
- border: none;
- border-radius: 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
color: var(--text-secondary);
cursor: pointer;
- font-size: 14px;
- font-weight: 500;
+ font-size: 13px;
+ font-weight: 600;
transition: all 0.2s ease;
text-align: left;
position: relative;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.tab-button:hover {
color: var(--text-primary);
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
}
.tab-button.active {
- color: var(--link-color);
- background: var(--bg-primary);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-
-.tab-button.active::before {
- content: '';
- position: absolute;
- left: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 3px;
- height: 60%;
- background: var(--link-color);
- border-radius: 0 2px 2px 0;
+ color: var(--text-primary);
+ background: rgba(255, 255, 255, 0.07);
+ border-color: var(--border-hover);
+ box-shadow: 0 10px 22px rgba(0, 0, 0, 0.1);
}
.tab-button:focus-visible {
@@ -338,24 +301,15 @@ h1 {
flex-direction: column;
gap: 4px;
padding: 8px 12px;
- font-size: 11px;
+ font-size: 12px;
min-width: 70px;
}
.tab-button span {
- font-size: 11px;
+ font-size: 12px;
text-align: center;
}
- .tab-button.active::before {
- left: 50%;
- top: auto;
- bottom: 0;
- transform: translateX(-50%);
- width: 60%;
- height: 3px;
- }
-
.tab-icon {
width: 20px;
height: 20px;
@@ -371,18 +325,25 @@ h1 {
padding-bottom: 32px;
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: none;
+ border-bottom: 1px solid var(--border-color);
margin-bottom: 0;
- padding-bottom: 0;
+ padding-bottom: 32px;
}
h2 {
- font-size: 18px;
- margin-bottom: 16px;
+ font-size: 20px;
+ margin-bottom: 14px;
color: var(--text-primary);
+ letter-spacing: -0.04em;
}
label {
@@ -397,11 +358,11 @@ input[type="text"],
input[type="password"],
select {
width: 100%;
- padding: 8px 12px;
+ padding: 10px 12px;
border: 1px solid var(--border-color-dark);
- background: var(--bg-primary);
+ background: rgba(255, 255, 255, 0.04);
color: var(--text-primary);
- border-radius: 6px;
+ border-radius: 12px;
font-size: 14px;
font-family: inherit;
}
@@ -411,7 +372,7 @@ input[type="password"]:focus,
select:focus {
outline: none;
border-color: var(--link-color);
- box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
+ box-shadow: 0 0 0 3px rgba(24, 24, 27, 0.08);
}
.help-text {
@@ -453,11 +414,12 @@ select:focus {
}
.info-box {
- background: var(--bg-secondary);
- border-left: 3px solid var(--link-color);
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border-color);
padding: 12px 16px;
margin: 12px 0;
- border-radius: 4px;
+ border-radius: 16px;
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.06);
}
.info-box .help-text {
@@ -465,7 +427,7 @@ select:focus {
display: flex;
align-items: flex-start;
gap: 8px;
- font-size: 13px;
+ font-size: 14px;
}
.info-box-compact {
@@ -477,7 +439,7 @@ select:focus {
display: flex;
align-items: flex-start;
gap: 8px;
- font-size: 13px;
+ font-size: 14px;
}
.info-box-compact .info-icon {
@@ -488,8 +450,8 @@ select:focus {
button {
padding: 8px 16px;
border: 1px solid var(--border-color-dark);
- border-radius: 6px;
- background: var(--bg-primary);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.04);
color: var(--text-primary);
cursor: pointer;
font-size: 14px;
@@ -499,12 +461,14 @@ button {
button:hover {
background: var(--bg-tertiary);
+ transform: translateY(-1px);
}
button.primary {
- background: var(--link-color);
- color: white;
- border-color: var(--link-color);
+ background: var(--primary);
+ color: var(--primary-foreground);
+ border-color: var(--primary);
+ font-weight: 600;
}
button.primary:hover {
@@ -562,14 +526,16 @@ button.danger:hover {
align-items: center;
gap: 8px;
padding: 10px 20px;
- background: var(--bg-tertiary);
- border: 2px solid transparent;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border-color);
border-radius: 24px;
cursor: pointer;
- font-size: 14px;
- font-weight: 500;
+ font-size: 13px;
+ font-weight: 600;
transition: all 0.2s ease;
user-select: none;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.chip label:hover {
@@ -579,9 +545,9 @@ button.danger:hover {
.chip input[type="radio"]:checked + label {
background: var(--link-color);
- color: white;
+ color: var(--background);
border-color: var(--link-color);
- box-shadow: 0 2px 8px rgba(3, 102, 214, 0.3);
+ box-shadow: 0 2px 8px rgba(24, 24, 27, 0.2);
}
.chip input[type="radio"]:focus + label {
@@ -599,6 +565,23 @@ button.danger:hover {
opacity: 1;
}
+.color-swatch {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ border: 1px solid var(--border);
+}
+
+.subsection-heading {
+ font-size: 15px;
+ font-weight: 600;
+ margin-top: 24px;
+ margin-bottom: 4px;
+ color: var(--foreground);
+}
+
/* Activity Cards Styles */
.activity-cards {
display: flex;
@@ -610,11 +593,12 @@ button.danger:hover {
.activity-card {
display: flex;
flex-direction: column;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 12px;
+ border-radius: 18px;
overflow: hidden;
transition: all 0.2s ease;
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
}
.activity-card:hover {
@@ -627,7 +611,7 @@ button.danger:hover {
align-items: flex-start;
gap: 12px;
padding: 16px;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.025);
}
.activity-icon {
@@ -650,7 +634,7 @@ button.danger:hover {
}
.activity-info p {
- font-size: 13px;
+ font-size: 14px;
color: var(--text-secondary);
margin: 0;
line-height: 1.4;
@@ -661,7 +645,7 @@ button.danger:hover {
justify-content: space-around;
gap: 24px;
padding: 12px 16px;
- background: var(--bg-primary);
+ background: rgba(0, 0, 0, 0.04);
}
.control-section {
@@ -719,11 +703,7 @@ button.danger:hover {
}
.activity-toggle input:checked + .toggle-slider {
- background-color: #10b981;
-}
-
-body.dark-mode .activity-toggle input:checked + .toggle-slider {
- background-color: #059669;
+ background-color: var(--primary);
}
.activity-toggle input:focus + .toggle-slider {
@@ -740,15 +720,15 @@ body.dark-mode .activity-toggle input:checked + .toggle-slider {
}
.notification-toggle input:checked + .toggle-slider {
- background-color: #3b82f6;
+ background-color: var(--primary);
}
body.dark-mode .notification-toggle input:checked + .toggle-slider {
- background-color: #2563eb;
+ background-color: var(--primary);
}
.notification-toggle input:focus + .toggle-slider {
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
+ box-shadow: 0 0 0 2px rgba(24, 24, 27, 0.15);
}
/* Disabled notification toggle state */
@@ -826,14 +806,14 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.number-input-group:focus-within {
border-color: var(--link-color);
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+ box-shadow: 0 0 0 3px rgba(24, 24, 27, 0.08);
}
.styled-number-input {
border: none;
background: transparent;
padding: 10px 16px;
- font-size: 15px;
+ font-size: 14px;
font-weight: 500;
color: var(--text-primary);
width: 80px;
@@ -867,10 +847,11 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
align-items: flex-start;
gap: 16px;
padding: 20px;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 12px;
+ border-radius: 18px;
transition: all 0.2s ease;
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
}
.setup-step.clickable {
@@ -880,9 +861,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.setup-step:hover {
- border-color: var(--link-color);
- transform: translateX(4px);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ border-color: var(--border-hover);
+ transform: translateY(-2px);
+ box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12);
}
.step-number {
@@ -891,12 +872,14 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
justify-content: center;
width: 40px;
height: 40px;
- background: var(--link-color);
- color: white;
- border-radius: 50%;
- font-size: 18px;
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--text-primary);
+ border-radius: 14px;
+ border: 1px solid var(--border-color);
+ font-size: 16px;
font-weight: 700;
flex-shrink: 0;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
}
.step-content h3 {
@@ -915,10 +898,12 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
/* Primary Setup Step (Step 1 with token input) */
.setup-step-primary {
- background: var(--bg-primary);
- border-color: var(--link-color);
+ background:
+ radial-gradient(circle at top right, var(--bg-gradient-glow), transparent 32%),
+ 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 1px 3px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 14px 34px rgba(0, 0, 0, 0.12);
}
.setup-step-primary:hover {
@@ -932,6 +917,214 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.step-all-content {
flex: 1;
+ width: 100%;
+}
+
+.github-connect-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr);
+ gap: 24px;
+ align-items: stretch;
+}
+
+.github-connect-intro {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.github-connect-eyebrow {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.github-step-label {
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-secondary);
+}
+
+.github-connect-copy h3 {
+ font-size: 28px;
+ line-height: 1.1;
+ margin: 0 0 10px;
+ color: var(--text-primary);
+ letter-spacing: -0.05em;
+}
+
+.github-connect-copy p {
+ font-size: 15px;
+ line-height: 1.6;
+ color: var(--text-secondary);
+ margin: 0;
+ max-width: 60ch;
+}
+
+.github-connect-facts {
+ display: grid;
+ gap: 12px;
+}
+
+.github-connect-fact {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 14px;
+ align-items: start;
+ padding: 16px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border-color);
+ border-radius: 18px;
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.06);
+}
+
+.github-connect-fact-icon {
+ width: 40px;
+ height: 40px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ flex-shrink: 0;
+}
+
+.github-connect-fact-icon svg {
+ width: 18px;
+ height: 18px;
+}
+
+.github-connect-fact-title {
+ margin: 0 0 6px;
+ font-size: 13px;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ color: var(--text-primary);
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
+}
+
+.github-connect-fact-copy {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.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);
+}
+
+.github-connect-panel-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.github-panel-label {
+ margin: 0 0 10px;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-secondary);
+}
+
+.github-connect-panel-body {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.github-connect-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ min-height: 48px;
+ font-size: 14px;
+ font-weight: 700;
+}
+
+.github-connect-help {
+ margin: 0;
+ font-size: 13px;
+}
+
+.github-connect-flow {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.github-connect-flow-step {
+ padding: 12px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border-color);
+ border-radius: 16px;
+ min-height: 108px;
+}
+
+.github-connect-flow-step span {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ 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;
+}
+
+.github-connect-flow-step p {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.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;
+}
+
+.device-code-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.device-code-note {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-secondary);
}
.step-header-section {
@@ -962,9 +1155,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.token-form-section {
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 6px;
+ border-radius: 18px;
padding: 20px;
margin-bottom: 16px;
}
@@ -992,6 +1185,37 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
gap: 12px;
}
+@media (max-width: 980px) {
+ .github-connect-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 640px) {
+ .github-connect-copy h3 {
+ font-size: 24px;
+ }
+
+ .github-connect-panel-header,
+ .device-code-header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .github-connect-flow {
+ grid-template-columns: 1fr;
+ }
+
+ .github-connect-flow-step {
+ min-height: auto;
+ }
+
+ #deviceCodeSection input {
+ font-size: 16px;
+ letter-spacing: 0.16em;
+ }
+}
+
.help-link-primary {
font-weight: 500;
}
@@ -1009,15 +1233,16 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
align-items: center;
gap: 16px;
padding: 20px;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 12px;
+ 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(--link-color);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ border-color: var(--border-hover);
+ box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12);
}
.data-card.danger-card {
@@ -1025,7 +1250,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.data-card.danger-card:hover {
- border-color: #dc3545;
+ border-color: var(--destructive);
}
.data-icon {
@@ -1036,7 +1261,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.danger-card .data-icon {
- color: #dc3545;
+ color: var(--destructive);
}
.data-info {
@@ -1051,7 +1276,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.data-info p {
- font-size: 13px;
+ font-size: 14px;
color: var(--text-secondary);
margin: 0;
line-height: 1.4;
@@ -1063,29 +1288,31 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
gap: 6px;
padding: 8px 16px;
background: var(--link-color);
- color: white;
- border: none;
- border-radius: 6px;
- font-size: 14px;
- font-weight: 500;
+ color: var(--background);
+ border: 1px solid var(--link-color);
+ border-radius: 12px;
+ font-size: 13px;
+ font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.data-action-btn:hover {
background: var(--link-hover);
transform: translateY(-1px);
- box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
+ box-shadow: 0 2px 8px rgba(24, 24, 27, 0.2);
}
.data-action-btn.danger-btn {
- background: #dc3545;
+ background: var(--destructive);
}
.data-action-btn.danger-btn:hover {
- background: #c82333;
- box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
+ background: var(--danger-hover);
+ box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
/* Help Tab Styles */
@@ -1099,11 +1326,11 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
width: 64px;
height: 64px;
margin: 0 auto 16px;
- color: var(--link-color);
+ color: var(--accent-color);
}
.help-hero h2 {
- font-size: 28px;
+ font-size: 30px;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 8px;
@@ -1127,23 +1354,23 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.feature-card {
text-align: center;
padding: 24px;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 12px;
+ border-radius: 18px;
transition: all 0.2s ease;
}
.feature-card:hover {
- border-color: var(--link-color);
+ border-color: var(--border-hover);
transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12);
}
.feature-icon {
width: 40px;
height: 40px;
margin: 0 auto 12px;
- color: var(--link-color);
+ color: var(--accent-color);
}
.feature-card h3 {
@@ -1184,36 +1411,36 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
align-items: flex-start;
gap: 16px;
padding: 20px;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 12px;
+ border-radius: 18px;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
}
.resource-link:hover {
- border-color: var(--link-color);
+ border-color: var(--border-hover);
transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12);
}
.resource-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
- color: var(--link-color);
+ color: var(--accent-color);
}
.resource-content h4 {
- font-size: 15px;
+ font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 4px;
}
.resource-content p {
- font-size: 13px;
+ font-size: 14px;
color: var(--text-secondary);
margin: 0;
line-height: 1.4;
@@ -1243,9 +1470,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
align-items: center;
gap: 8px;
padding: 12px 20px;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 8px;
+ border-radius: 16px;
color: var(--text-primary);
}
@@ -1255,16 +1482,16 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.changelog-content {
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 12px;
+ border-radius: 18px;
padding: 24px;
max-height: 600px;
overflow-y: auto;
}
.changelog-content h2 {
- font-size: 22px;
+ font-size: 24px;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 16px;
@@ -1280,7 +1507,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.changelog-content h4 {
- font-size: 15px;
+ font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin: 16px 0 8px;
@@ -1309,8 +1536,8 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: 4px;
- font-family: 'Courier New', monospace;
- font-size: 13px;
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+ font-size: 14px;
color: var(--link-color);
}
@@ -1484,7 +1711,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.subsection-title {
- font-size: 15px;
+ font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 12px;
@@ -1501,9 +1728,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
align-items: flex-start;
padding: 16px;
border: 1px solid var(--border-color);
- border-radius: 6px;
+ border-radius: 16px;
margin-bottom: 12px;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
gap: 16px;
transition: all 0.15s ease;
}
@@ -1511,7 +1738,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.repo-item:hover {
border-color: var(--border-hover);
transform: translateY(-1px);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.repo-content {
@@ -1522,15 +1749,15 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.repo-name {
display: flex;
align-items: center;
- font-family: 'Monaco', 'Courier New', monospace;
- font-size: 14px;
+ font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+ font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 6px;
}
.repo-description {
- font-size: 13px;
+ font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
line-height: 1.4;
@@ -1560,21 +1787,22 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
/* Repository Section Cards */
.repo-section-card {
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 12px;
+ border-radius: 22px;
padding: 24px;
margin-bottom: 24px;
transition: all 0.2s ease;
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08);
}
.add-repo-card {
- background: var(--bg-secondary);
+ background: rgba(255, 255, 255, 0.05);
border-color: var(--border-color);
}
.watched-repos-card {
- background: var(--bg-primary);
+ background: rgba(255, 255, 255, 0.03);
border-color: var(--border-color-dark);
}
@@ -1584,14 +1812,14 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
gap: 16px;
margin-bottom: 20px;
padding-bottom: 16px;
- border-bottom: 2px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
}
.repo-section-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
- color: var(--link-color);
+ color: var(--accent-color);
margin-top: 4px;
}
@@ -1611,7 +1839,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.repo-section-title .help-text {
margin: 0;
- font-size: 13px;
+ font-size: 14px;
}
.repo-section-actions {
@@ -1628,12 +1856,13 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
min-width: 28px;
height: 28px;
padding: 0 8px;
- background: var(--link-color);
- color: white;
+ background: var(--primary);
+ color: var(--primary-foreground);
border-radius: 14px;
- font-size: 13px;
- font-weight: 600;
+ font-size: 12px;
+ font-weight: 700;
margin-left: 4px;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
}
/* Action Buttons Layout */
@@ -1661,13 +1890,15 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
gap: 8px;
padding: 10px 16px;
border: 1px solid var(--border-color);
- border-radius: 6px;
- background: var(--background);
- color: var(--text);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--text-primary);
cursor: pointer;
- font-size: 14px;
- font-weight: 500;
+ font-size: 13px;
+ font-weight: 600;
transition: all 0.2s ease;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.action-btn:hover {
@@ -1704,6 +1935,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
opacity: 0;
max-height: 0;
transition: all 0.2s ease-in-out; /* Simpler transition */
+ border-radius: 18px;
}
.repo-panel.show {
@@ -1732,6 +1964,7 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.search-input-wrapper input {
width: 100%;
padding-right: 36px;
+ background: rgba(255, 255, 255, 0.04);
}
.search-clear-btn {
@@ -1763,15 +1996,17 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
align-items: center;
gap: 6px;
padding: 8px 12px;
- background: var(--bg-tertiary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color-dark);
- border-radius: 6px;
+ border-radius: 12px;
cursor: pointer;
- font-size: 13px;
- font-weight: 500;
+ font-size: 12px;
+ font-weight: 600;
color: var(--text-secondary);
transition: all 0.2s;
white-space: nowrap;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.hide-pinned-btn:hover {
@@ -1934,19 +2169,21 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
gap: 8px;
padding: 8px 16px;
border: 1px solid var(--border-color-dark);
- border-radius: 6px;
- background: var(--bg-primary);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.04);
color: var(--text-primary);
cursor: pointer;
- font-size: 14px;
- font-family: inherit;
+ font-size: 12px;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
transition: all 0.2s;
+ text-transform: uppercase;
+ font-weight: 600;
}
/* Import buttons when inside panel - make them smaller */
.repo-panel .import-btn {
padding: 6px 12px;
- font-size: 13px;
+ font-size: 14px;
}
.repo-panel .import-icon {
@@ -1994,15 +2231,17 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.modal-content {
- background: var(--bg-primary);
- border-radius: 8px;
+ background: rgba(24, 22, 20, 0.96);
+ border: 1px solid var(--border-color);
+ border-radius: 22px;
width: 90%;
max-width: 700px;
max-height: 80vh;
display: flex;
flex-direction: column;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+ box-shadow: 0 24px 50px rgba(0, 0, 0, 0.35);
animation: slideIn 0.3s;
+ backdrop-filter: blur(16px);
}
@keyframes slideIn {
@@ -2028,12 +2267,15 @@ 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;
+ text-transform: uppercase;
+ letter-spacing: -0.03em;
}
.close-modal {
background: none;
border: none;
- font-size: 28px;
+ font-size: 24px;
color: var(--text-secondary);
cursor: pointer;
padding: 0;
@@ -2098,8 +2340,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.import-select-all {
margin-bottom: 12px;
padding: 12px;
- background: var(--bg-tertiary);
- border-radius: 6px;
+ background: rgba(255, 255, 255, 0.04);
+ border-radius: 14px;
+ border: 1px solid var(--border-color);
}
.import-select-all label {
@@ -2211,21 +2454,33 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.device-code-label {
font-size: 12px;
font-weight: 600;
- color: var(--text-secondary);
+ color: var(--text-primary);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
}
#deviceCodeSection input {
- letter-spacing: 0.12em;
+ letter-spacing: 0.22em;
font-weight: 600;
text-align: center;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ font-size: 20px;
+ padding: 16px 14px;
}
.token-status {
- display: block;
- font-size: 12px;
- padding: 4px 8px;
- border-radius: 4px;
- margin-bottom: 4px;
+ display: inline-flex;
+ align-items: center;
+ min-height: 36px;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1.4;
+ padding: 8px 12px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--text-secondary);
width: fit-content;
}
@@ -2236,16 +2491,19 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.token-status.valid {
background: var(--success-bg);
color: var(--success-text);
+ border-color: var(--success-border);
}
.token-status.invalid {
background: var(--error-bg);
color: var(--error-text);
+ border-color: var(--error-border);
}
.token-status.checking {
- color: var(--text-secondary);
- background: transparent;
+ color: var(--text-primary);
+ background: rgba(255, 255, 255, 0.06);
+ border-color: var(--border-hover);
}
.repo-actions {
@@ -2256,9 +2514,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.pin-btn {
- background: none;
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color-dark);
- border-radius: 6px;
+ border-radius: 12px;
padding: 8px 10px;
cursor: pointer;
display: flex;
@@ -2286,9 +2544,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.mute-btn {
- background: none;
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color-dark);
- border-radius: 6px;
+ border-radius: 12px;
padding: 8px 10px;
cursor: pointer;
display: flex;
@@ -2314,9 +2572,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
}
.link-btn {
- background: none;
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color-dark);
- border-radius: 6px;
+ border-radius: 12px;
padding: 8px 10px;
cursor: pointer;
display: flex;
@@ -2381,14 +2639,16 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
align-items: center;
gap: 8px;
padding: 10px 20px;
- background: var(--bg-tertiary);
- border: 2px solid var(--border-color);
- border-radius: 6px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
cursor: pointer;
- font-size: 14px;
- font-weight: 500;
+ font-size: 12px;
+ font-weight: 600;
transition: all 0.2s ease;
color: var(--text-primary);
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.import-export-btn:hover {
@@ -2431,7 +2691,7 @@ select:focus-visible {
body.dark-mode input[type="text"]:focus-visible,
body.dark-mode input[type="password"]:focus-visible,
body.dark-mode select:focus-visible {
- box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
+ box-shadow: 0 0 0 3px rgba(212, 212, 216, 0.15);
}
.nav-links a:focus-visible {
@@ -2501,9 +2761,9 @@ body.dark-mode select:focus-visible {
.empty-repos-card {
text-align: center;
padding: 48px 32px;
- background: var(--bg-secondary);
- border: 2px dashed var(--border-color);
- border-radius: 12px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px dashed var(--border-color);
+ border-radius: 20px;
margin: 16px 0;
}
@@ -2536,13 +2796,13 @@ body.dark-mode select:focus-visible {
}
.empty-repos-tips .tip-note {
- font-size: 13px;
+ font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin: 0;
padding: 16px;
- background: var(--bg-primary);
- border-radius: 8px;
+ background: rgba(255, 255, 255, 0.04);
+ border-radius: 16px;
border: 1px solid var(--border-color);
text-align: left;
}
@@ -2553,9 +2813,9 @@ body.dark-mode select:focus-visible {
justify-content: space-between;
padding: 16px;
margin-bottom: 8px;
- background: var(--bg-secondary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 8px;
+ border-radius: 16px;
transition: all 0.2s;
}
@@ -2571,13 +2831,13 @@ body.dark-mode select:focus-visible {
.snoozed-repo-name {
font-weight: 600;
color: var(--text-primary);
- font-family: 'Monaco', 'Courier New', monospace;
- font-size: 15px;
+ font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+ font-size: 13px;
margin-bottom: 4px;
}
.snoozed-repo-time {
- font-size: 13px;
+ font-size: 14px;
color: var(--text-secondary);
}
@@ -2590,19 +2850,21 @@ body.dark-mode select:focus-visible {
.unsnooze-btn {
background: var(--danger-bg);
color: white;
- border: none;
+ border: 1px solid var(--danger-bg);
padding: 8px 16px;
- border-radius: 6px;
- font-size: 13px;
- font-weight: 500;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 600;
cursor: pointer;
transition: all 0.2s;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.unsnooze-btn:hover {
background: var(--danger-hover);
transform: translateY(-1px);
- box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
+ box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
.unsnooze-btn:focus-visible {
@@ -2634,11 +2896,11 @@ body.dark-mode .snooze-expiry-warning {
}
.toast {
- background: var(--bg-primary);
+ background: rgba(24, 22, 20, 0.96);
border: 1px solid var(--border-color);
- border-radius: 8px;
+ border-radius: 16px;
padding: 12px 16px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ box-shadow: 0 18px 32px rgba(0, 0, 0, 0.22);
display: flex;
align-items: center;
gap: 12px;
@@ -2669,7 +2931,7 @@ body.dark-mode .snooze-expiry-warning {
display: flex;
align-items: center;
justify-content: center;
- font-weight: bold;
+ font-weight: 700;
font-size: 12px;
color: white;
}
@@ -2705,8 +2967,8 @@ body.dark-mode .snooze-expiry-warning {
border: none;
color: var(--text-secondary);
cursor: pointer;
- padding: 2px;
- border-radius: 4px;
+ padding: 4px;
+ border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
@@ -2752,17 +3014,19 @@ body.dark-mode .snooze-expiry-warning {
padding: 12px 20px;
background: var(--link-color);
color: white;
- border: none;
- border-radius: 6px;
- font-size: 14px;
- font-weight: 500;
+ border: 1px solid var(--link-color);
+ border-radius: 14px;
+ font-size: 12px;
+ font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.setup-btn:hover {
- background: #0256cc;
+ background: var(--link-hover);
transform: translateY(-1px);
}
@@ -2780,9 +3044,9 @@ body.dark-mode .snooze-expiry-warning {
.help-links {
margin-top: 25px;
padding: 20px;
- background: var(--bg-secondary);
+ background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
- border-radius: 8px;
+ border-radius: 18px;
}
.help-links h3 {
@@ -2804,18 +3068,21 @@ body.dark-mode .snooze-expiry-warning {
.help-links a {
color: var(--link-color);
text-decoration: none;
- font-size: 14px;
+ font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
- padding: 8px 0;
- border-radius: 4px;
- transition: background 0.2s;
+ padding: 8px 10px;
+ border-radius: 10px;
+ transition: background 0.2s, transform 0.2s;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.help-links a:hover {
background: var(--bg-hover);
text-decoration: underline;
+ transform: translateX(2px);
}
/* Dark mode adjustments for setup section */
@@ -2824,7 +3091,7 @@ body.dark-mode .setup-btn {
}
body.dark-mode .setup-btn:hover {
- background: #1f6feb;
+ background: var(--link-hover);
}
/* Settings Footer */
@@ -2842,9 +3109,11 @@ body.dark-mode .setup-btn:hover {
.footer-section h4 {
color: var(--text-primary);
- font-size: 18px;
+ font-size: 16px;
margin-bottom: 12px;
font-weight: 600;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.footer-section p {
@@ -2862,11 +3131,12 @@ body.dark-mode .setup-btn:hover {
.footer-column h5 {
color: var(--text-primary);
- font-size: 14px;
+ font-size: 12px;
margin-bottom: 12px;
font-weight: 600;
text-transform: uppercase;
- letter-spacing: 0.5px;
+ letter-spacing: 0.08em;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
}
.footer-column ul {
@@ -2886,9 +3156,11 @@ body.dark-mode .setup-btn:hover {
display: flex;
align-items: center;
gap: 6px;
- padding: 4px 0;
- border-radius: 4px;
+ padding: 6px 8px;
+ border-radius: 10px;
transition: all 0.2s;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.footer-column a:hover {
@@ -2906,7 +3178,7 @@ body.dark-mode .setup-btn:hover {
.footer-bottom p {
color: var(--text-secondary);
- font-size: 13px;
+ font-size: 14px;
margin: 0;
}
@@ -2930,7 +3202,7 @@ body.dark-mode .setup-btn:hover {
}
.toast {
- font-size: 13px;
+ font-size: 14px;
padding: 10px 12px;
}
diff --git a/options/options.html b/options/options.html
index 570ba9d..390aff8 100644
--- a/options/options.html
+++ b/options/options.html
@@ -3,7 +3,9 @@
-
GitHub Devwatch - Settings
+
GitHub DevWatch - Settings
+
+
@@ -58,54 +60,91 @@
Settings
Getting Started
- Follow these steps to set up GitHub DevWatch
+ Set up DevWatch and shape the review queue the way you work.
-
+
-
-
-
-
-
- Click Connect GitHub , approve access on GitHub, and come back here once the browser tab says you can close it.
-
-
-
-
-
-
-
-
- Security: Your GitHub sign-in stays in Chrome session storage and is cleared when the browser session ends.
-
-
-
-
-
-
-
- Permissions: DevWatch requests repo and read:user access so it can read repository activity and identify your account.
-
-
-
+
+
+
+
+
+
Connect GitHub
+
Authorize DevWatch once so it can pull activity for the repositories you monitor without asking for a personal access token.
+
+
+
+
+
+
+
Stored for this browser session
+
Your GitHub connection stays in Chrome session storage and clears when the browser session ends.
+
+
+
+
+
+
+
Read-only account access
+
DevWatch requests repo and read:user so it can read repository activity and identify your account.
+
+
+
+
+
+
+
+
+
+
Connect GitHub
+
+
+ We'll open GitHub in a new tab. Approve access there, then come back here and DevWatch will finish connecting.
+
+
+
+
+
1
+
Start sign-in here
+
+
+
2
+
Approve access on GitHub
+
+
+
3
+
Return when GitHub is done
+
+
+
+
+
+
+
+
+
+
@@ -457,6 +496,57 @@
Appearance
+
Color Theme
+
Choose a color palette
+
+
+ Color theme
+
+
+
diff --git a/options/options.js b/options/options.js
index 5ce7340..4e7db83 100644
--- a/options/options.js
+++ b/options/options.js
@@ -1,9 +1,23 @@
-import { applyTheme, formatDateVerbose } from '../shared/utils.js';
-import { clearAuthSession, getAuthSession, getAccessToken, getLocalItems, getWatchedRepos, setLocalItem, setWatchedRepos } from '../shared/storage-helpers.js';
-import { createHeaders } from '../shared/github-api.js';
-import { STORAGE_CONFIG, VALIDATION_PATTERNS } from '../shared/config.js';
-import { validateRepository } from '../shared/repository-validator.js';
-import { fetchGitHubRepoFromNpm } from '../shared/api/npm-api.js';
+import { applyTheme, applyColorTheme, formatDateVerbose } from '../shared/utils.js';
+import {
+ clearAuthSession,
+ getAuthSession,
+ getAccessToken,
+ getSettings,
+ getWatchedRepos,
+ setLocalItem,
+ setWatchedRepos,
+ updateSettings
+} from '../shared/storage-helpers.js';
+import { STORAGE_CONFIG } from '../shared/config.js';
+import {
+ resolveWatchedRepoInput,
+ validateWatchedRepo
+} from '../shared/repo-service.js';
+import {
+ CATEGORY_SETTINGS,
+ normalizeSettings
+} from '../shared/settings-schema.js';
import { NotificationManager } from '../shared/ui/notification-manager.js';
// Controllers
@@ -38,6 +52,7 @@ if (typeof document !== 'undefined') {
await loadSettings();
setupEventListeners();
setupThemeListener();
+ setupSnoozedReposAutoRefresh();
// Handle URL parameters for enhanced navigation
handleUrlParameters();
@@ -168,31 +183,101 @@ function handleUrlParameters() {
}
function syncTokenUiWithStoredCredential(hasStoredToken) {
- const clearTokenBtn = document.getElementById('clearTokenBtn');
- const connectGitHubBtn = document.getElementById('connectGitHubBtn');
- const repoInput = document.getElementById('repoInput');
- const addRepoBtn = document.getElementById('addRepoBtn');
- const repoHelpText = document.getElementById('repoHelpText');
- const importSection = document.getElementById('importReposSection');
-
- connectGitHubBtn.textContent = hasStoredToken ? 'Reconnect GitHub' : 'Connect GitHub';
- clearTokenBtn.style.display = hasStoredToken ? 'block' : 'none';
- repoInput.disabled = !hasStoredToken;
- repoInput.placeholder = hasStoredToken
- ? 'e.g., react, facebook/react, or GitHub URL'
- : 'Connect GitHub to add repositories';
- addRepoBtn.disabled = !hasStoredToken;
- repoHelpText.textContent = hasStoredToken
- ? 'Add repositories to monitor (npm package, owner/repo, or GitHub URL)'
- : 'Connect GitHub above to start adding repositories';
- importSection.classList.toggle('hidden', !hasStoredToken);
- importSection.style.display = hasStoredToken ? 'block' : 'none';
+ applyStoredConnection(hasStoredToken ? { accessToken: 'stored-session' } : null);
}
function shouldClearStoredToken(validationResult) {
return !validationResult.isValid && validationResult.reason === 'invalid';
}
+function getThemeDisplayName(theme) {
+ const themeNames = {
+ light: 'Light Mode',
+ dark: 'Dark Mode',
+ system: 'System Theme'
+ };
+
+ return themeNames[theme] || theme;
+}
+
+function getColorThemeDisplayName(colorTheme) {
+ const themeNames = {
+ polar: 'Polar',
+ graphite: 'Graphite',
+ nightfall: 'Nightfall',
+ obsidian: 'Obsidian',
+ sand: 'Sand',
+ 'terminal-ledger': 'Terminal Ledger'
+ };
+
+ return themeNames[colorTheme] || colorTheme;
+}
+
+function getOptionCategorySettingsFromDom() {
+ const filters = {};
+ const notifications = {};
+
+ CATEGORY_SETTINGS.forEach(({ key, optionsTrackId, optionsNotifyId }) => {
+ filters[key] = document.getElementById(optionsTrackId)?.checked !== false;
+ notifications[key] = document.getElementById(optionsNotifyId)?.checked === true;
+ });
+
+ return { filters, notifications };
+}
+
+function applyCategorySettingsToOptions(settings) {
+ CATEGORY_SETTINGS.forEach(({ key, optionsTrackId, optionsNotifyId }) => {
+ const trackToggle = document.getElementById(optionsTrackId);
+ const notifyToggle = document.getElementById(optionsNotifyId);
+
+ if (trackToggle) {
+ trackToggle.checked = settings.filters[key] !== false;
+ }
+
+ if (notifyToggle) {
+ notifyToggle.checked = settings.notifications[key] !== false;
+ }
+ });
+}
+
+function applySettingsToUi(settings) {
+ applyTheme(settings.theme);
+ applyColorTheme(settings.colorTheme);
+
+ const themeRadio = document.getElementById(`theme-${settings.theme}`);
+ if (themeRadio) {
+ themeRadio.checked = true;
+ }
+
+ const colorThemeRadio = document.getElementById(`color-${settings.colorTheme}`);
+ if (colorThemeRadio) {
+ colorThemeRadio.checked = true;
+ }
+
+ const intervalRadio = document.getElementById(`interval-${settings.checkInterval}`);
+ if (intervalRadio) {
+ intervalRadio.checked = true;
+ }
+
+ const snoozeRadio = document.getElementById(`snooze-${settings.snoozeHours}`);
+ if (snoozeRadio) {
+ snoozeRadio.checked = true;
+ }
+
+ applyCategorySettingsToOptions(settings);
+
+ const itemExpiryEnabled = settings.itemExpiryHours !== null && settings.itemExpiryHours !== undefined;
+ document.getElementById('itemExpiryEnabled').checked = itemExpiryEnabled;
+ document.getElementById('itemExpiryInputRow').style.display = itemExpiryEnabled ? 'block' : 'none';
+ if (itemExpiryEnabled) {
+ document.getElementById('itemExpiryHours').value = settings.itemExpiryHours;
+ }
+
+ document.getElementById('markReadOnSnooze').checked = settings.markReadOnSnooze === true;
+ document.getElementById('allowUnlimitedRepos').checked = settings.allowUnlimitedRepos === true;
+ updateNotificationToggleStates();
+}
+
function setupEventListeners() {
// Tab navigation
setupTabNavigation();
@@ -277,50 +362,54 @@ function setupEventListeners() {
}
});
- // Auto-save theme changes
- document.querySelectorAll('input[name="theme"]').forEach(radio => {
- radio.addEventListener('change', async (e) => {
- const theme = e.target.value;
- await chrome.storage.sync.set({ theme });
- applyTheme(theme);
-
- // Show toast notification
- const themeNames = {
- 'light': 'Light Mode',
- 'dark': 'Dark Mode',
- 'system': 'System Theme'
- };
- toastManager.info(`Theme changed to ${themeNames[theme] || theme}`);
+ function bindRadioSetting(groupName, key, { formatToast, onChange } = {}) {
+ document.querySelectorAll(`input[name="${groupName}"]`).forEach(radio => {
+ radio.addEventListener('change', async (e) => {
+ const rawValue = e.target.value;
+ const value = groupName === 'theme' || groupName === 'colorTheme'
+ ? rawValue
+ : parseInt(rawValue, 10);
+
+ await updateSettings({ [key]: value });
+ await onChange?.(value);
+
+ if (formatToast) {
+ toastManager.info(formatToast(value));
+ }
+ });
});
+ }
+
+ function bindCheckboxSetting(id, key, { onChange } = {}) {
+ document.getElementById(id).addEventListener('change', async (e) => {
+ const value = e.target.checked;
+ await updateSettings({ [key]: value });
+ await onChange?.(value);
+ });
+ }
+
+ bindRadioSetting('theme', 'theme', {
+ onChange: async (theme) => applyTheme(theme),
+ formatToast: (theme) => `Theme changed to ${getThemeDisplayName(theme)}`
});
-
- // Auto-save check interval changes
- document.querySelectorAll('input[name="checkInterval"]').forEach(radio => {
- radio.addEventListener('change', async (e) => {
- const interval = parseInt(e.target.value);
- await chrome.storage.sync.set({ checkInterval: interval });
+ bindRadioSetting('colorTheme', 'colorTheme', {
+ onChange: async (colorTheme) => applyColorTheme(colorTheme),
+ formatToast: (colorTheme) => `Color theme changed to ${getColorThemeDisplayName(colorTheme)}`
+ });
- // Update alarm with new interval
+ bindRadioSetting('checkInterval', 'checkInterval', {
+ onChange: async (interval) => {
chrome.runtime.sendMessage({
action: 'updateInterval',
- interval: interval
+ interval
});
-
- // Show toast notification
- toastManager.info(`Check interval changed to ${interval} minutes`);
- });
+ },
+ formatToast: (interval) => `Check interval changed to ${interval} minutes`
});
- // Auto-save snooze duration changes
- document.querySelectorAll('input[name="snoozeHours"]').forEach(radio => {
- radio.addEventListener('change', async (e) => {
- const snoozeHours = parseInt(e.target.value);
- await chrome.storage.sync.set({ snoozeHours });
-
- // Show toast notification
- toastManager.info(`Default snooze duration changed to ${snoozeHours} hour${snoozeHours > 1 ? 's' : ''}`);
- });
+ bindRadioSetting('snoozeHours', 'snoozeHours', {
+ formatToast: (snoozeHours) => `Default snooze duration changed to ${snoozeHours} hour${snoozeHours > 1 ? 's' : ''}`
});
// Auto-save itemExpiryHours changes
@@ -334,11 +423,11 @@ function setupEventListeners() {
itemExpiryInputRow.style.display = 'block';
const hours = parseInt(itemExpiryHoursInput.value) || 24;
itemExpiryHoursInput.value = hours;
- await chrome.storage.sync.set({ itemExpiryHours: hours });
+ await updateSettings({ itemExpiryHours: hours });
toastManager.info(`Auto-removal enabled: items older than ${hours} hours will be removed`);
} else {
itemExpiryInputRow.style.display = 'none';
- await chrome.storage.sync.set({ itemExpiryHours: null });
+ await updateSettings({ itemExpiryHours: null });
toastManager.info('Auto-removal disabled');
}
});
@@ -352,69 +441,49 @@ function setupEventListeners() {
hours = 168;
e.target.value = 168;
}
- await chrome.storage.sync.set({ itemExpiryHours: hours });
+ await updateSettings({ itemExpiryHours: hours });
toastManager.info(`Auto-removal time changed to ${hours} hours`);
});
- // Auto-save markReadOnSnooze changes
- document.getElementById('markReadOnSnooze').addEventListener('change', async (e) => {
- const markReadOnSnooze = e.target.checked;
- await chrome.storage.sync.set({ markReadOnSnooze });
- toastManager.info(`Mark as read on snooze ${markReadOnSnooze ? 'enabled' : 'disabled'}`);
+ bindCheckboxSetting('markReadOnSnooze', 'markReadOnSnooze', {
+ onChange: async (markReadOnSnooze) => {
+ toastManager.info(`Mark as read on snooze ${markReadOnSnooze ? 'enabled' : 'disabled'}`);
+ }
});
- // Auto-save allowUnlimitedRepos changes
- document.getElementById('allowUnlimitedRepos').addEventListener('change', async (e) => {
- const allowUnlimitedRepos = e.target.checked;
- await chrome.storage.sync.set({ allowUnlimitedRepos });
- if (allowUnlimitedRepos) {
- toastManager.warning('Unlimited repositories enabled - watch for rate limits');
- } else {
- toastManager.info('Repository limit set to 50');
+ bindCheckboxSetting('allowUnlimitedRepos', 'allowUnlimitedRepos', {
+ onChange: async (allowUnlimitedRepos) => {
+ if (allowUnlimitedRepos) {
+ toastManager.warning('Unlimited repositories enabled - watch for rate limits');
+ } else {
+ toastManager.info('Repository limit set to 50');
+ }
}
});
- // Auto-save filter and notification changes
- ['filterPrs', 'filterIssues', 'filterReleases', 'notifyPrs', 'notifyIssues', 'notifyReleases'].forEach(id => {
- document.getElementById(id).addEventListener('change', async (e) => {
- const target = e.target;
-
- // Handle notification toggle logic
- if (target.id.startsWith('notify')) {
- const category = target.id.replace('notify', '').toLowerCase();
- const filterId = `filter${category.charAt(0).toUpperCase() + category.slice(1)}`;
- const filterToggle = document.getElementById(filterId);
-
- // If trying to enable notifications but category is disabled, disable the notification
- if (target.checked && !filterToggle.checked) {
- target.checked = false;
- return;
- }
+ CATEGORY_SETTINGS.forEach(({ key, optionsTrackId, optionsNotifyId }) => {
+ const trackToggle = document.getElementById(optionsTrackId);
+ const notifyToggle = document.getElementById(optionsNotifyId);
+
+ trackToggle?.addEventListener('change', async () => {
+ if (!trackToggle.checked && notifyToggle?.checked) {
+ notifyToggle.checked = false;
}
- // Handle filter toggle logic - disable notifications if filter is disabled
- if (target.id.startsWith('filter')) {
- const category = target.id.replace('filter', '').toLowerCase();
- const notifyId = `notify${category.charAt(0).toUpperCase() + category.slice(1)}`;
- const notifyToggle = document.getElementById(notifyId);
+ const categorySettings = getOptionCategorySettingsFromDom();
+ await updateSettings(categorySettings);
+ updateNotificationToggleStates();
+ toastManager.info(`${key === 'prs' ? 'Pull request' : key.slice(0, -1)} tracking ${trackToggle.checked ? 'enabled' : 'disabled'}`);
+ });
- // If disabling filter, also disable notifications
- if (!target.checked && notifyToggle.checked) {
- notifyToggle.checked = false;
- }
+ notifyToggle?.addEventListener('change', async () => {
+ if (notifyToggle.checked && !trackToggle?.checked) {
+ notifyToggle.checked = false;
+ return;
}
- const filters = {
- prs: document.getElementById('filterPrs').checked,
- issues: document.getElementById('filterIssues').checked,
- releases: document.getElementById('filterReleases').checked
- };
- const notifications = {
- prs: document.getElementById('notifyPrs').checked,
- issues: document.getElementById('notifyIssues').checked,
- releases: document.getElementById('notifyReleases').checked
- };
- await chrome.storage.sync.set({ filters, notifications });
+ const categorySettings = getOptionCategorySettingsFromDom();
+ await updateSettings(categorySettings);
updateNotificationToggleStates();
});
});
@@ -496,13 +565,9 @@ function toggleHidePinned() {
function updateNotificationToggleStates() {
// Update notification toggle states based on filter states
- const categories = ['prs', 'issues', 'releases'];
-
- categories.forEach(category => {
- const filterId = `filter${category.charAt(0).toUpperCase() + category.slice(1)}`;
- const notifyId = `notify${category.charAt(0).toUpperCase() + category.slice(1)}`;
- const filterToggle = document.getElementById(filterId);
- const notifyToggle = document.getElementById(notifyId);
+ CATEGORY_SETTINGS.forEach(({ optionsTrackId, optionsNotifyId }) => {
+ const filterToggle = document.getElementById(optionsTrackId);
+ const notifyToggle = document.getElementById(optionsNotifyId);
const notifyToggleLabel = notifyToggle.closest('.notification-toggle');
if (filterToggle && notifyToggle && notifyToggleLabel) {
@@ -539,36 +604,7 @@ async function loadSettings() {
}
const authSession = await getAuthSession();
-
- const settings = await chrome.storage.sync.get([
- 'mutedRepos',
- 'pinnedRepos',
- 'checkInterval',
- 'snoozeHours',
- 'filters',
- 'notifications',
- 'theme',
- 'itemExpiryHours',
- 'markReadOnSnooze',
- 'allowUnlimitedRepos'
- ]);
-
- const snoozeSettings = await chrome.storage.sync.get(['snoozedRepos']);
- const watchedRepos = await getWatchedRepos();
-
- // Safety check: if settings is undefined or null, use defaults
- if (!settings || !snoozeSettings) {
- console.warn('Storage returned undefined/null. Using default settings.');
- state.watchedRepos = [];
- state.mutedRepos = [];
- state.pinnedRepos = [];
- renderRepoListWrapper();
- return;
- }
-
- // Load and apply theme
- const theme = settings.theme || 'system';
- applyTheme(theme);
+ const settings = await getSettings();
if (authSession?.accessToken) {
applyStoredConnection(authSession);
@@ -576,65 +612,15 @@ async function loadSettings() {
syncTokenUiWithStoredCredential(false);
}
- state.watchedRepos = watchedRepos;
- state.mutedRepos = settings.mutedRepos || [];
- state.pinnedRepos = settings.pinnedRepos || [];
+ state.watchedRepos = settings.watchedRepos;
+ state.mutedRepos = settings.mutedRepos;
+ state.pinnedRepos = settings.pinnedRepos;
renderRepoListWrapper();
-
- if (settings.checkInterval) {
- const intervalRadio = document.getElementById(`interval-${settings.checkInterval}`);
- if (intervalRadio) {
- intervalRadio.checked = true;
- }
- }
-
- if (settings.snoozeHours) {
- const snoozeRadio = document.getElementById(`snooze-${settings.snoozeHours}`);
- if (snoozeRadio) {
- snoozeRadio.checked = true;
- }
- }
-
- if (settings.filters) {
- document.getElementById('filterPrs').checked = settings.filters.prs !== false;
- document.getElementById('filterIssues').checked = settings.filters.issues !== false;
- document.getElementById('filterReleases').checked = settings.filters.releases !== false;
- }
-
- if (settings.notifications) {
- document.getElementById('notifyPrs').checked = settings.notifications.prs !== false;
- document.getElementById('notifyIssues').checked = settings.notifications.issues !== false;
- document.getElementById('notifyReleases').checked = settings.notifications.releases !== false;
- }
-
- // Set theme radio button
- const themeRadio = document.getElementById(`theme-${theme}`);
- if (themeRadio) {
- themeRadio.checked = true;
- }
-
- // Load itemExpiryHours setting
- const itemExpiryEnabled = settings.itemExpiryHours !== null && settings.itemExpiryHours !== undefined;
- document.getElementById('itemExpiryEnabled').checked = itemExpiryEnabled;
- if (itemExpiryEnabled) {
- document.getElementById('itemExpiryHours').value = settings.itemExpiryHours;
- document.getElementById('itemExpiryInputRow').style.display = 'block';
- }
-
- // Load markReadOnSnooze setting (default to false - pause not dismiss)
- const markReadOnSnooze = settings.markReadOnSnooze === true;
- document.getElementById('markReadOnSnooze').checked = markReadOnSnooze;
-
- // Load allowUnlimitedRepos setting (default to false)
- const allowUnlimitedRepos = settings.allowUnlimitedRepos === true;
- document.getElementById('allowUnlimitedRepos').checked = allowUnlimitedRepos;
-
- // Update notification toggle states after loading settings
- updateNotificationToggleStates();
+ applySettingsToUi(settings);
// Load and display current snoozes
- renderSnoozedRepos(snoozeSettings.snoozedRepos || []);
+ renderSnoozedRepos(settings.snoozedRepos || []);
} catch (error) {
console.error('Error loading settings:', error);
// Set defaults even if error occurs
@@ -649,19 +635,19 @@ async function addRepo() {
const input = document.getElementById('repoInput');
const statusEl = document.getElementById('repoValidationStatus');
const errorEl = document.getElementById('repoError');
- let repo = input.value.trim();
+ const repoInput = input.value.trim();
// Clear previous error state
input.classList.remove('error');
errorEl.textContent = '';
- if (!repo) {
+ if (!repoInput) {
return;
}
// Check if we've hit the maximum repo limit (unless unlimited repos is enabled)
- const { allowUnlimitedRepos } = await chrome.storage.sync.get(['allowUnlimitedRepos']);
- if (!allowUnlimitedRepos && state.watchedRepos.length >= STORAGE_CONFIG.MAX_WATCHED_REPOS) {
+ 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.`);
statusEl.className = 'repo-validation-status error';
return;
@@ -670,78 +656,23 @@ async function addRepo() {
// Show checking indicator
statusEl.className = 'repo-validation-status checking';
- // Parse GitHub URL if provided
- const urlMatch = repo.match(/github\.com\/([^/]+\/[^/]+)/);
- if (urlMatch) {
- repo = urlMatch[1].replace(/\.git$/, '');
- input.value = repo; // Update input to show parsed format
- }
- // Check if it might be an NPM package (no slash or scoped package)
- else if (!repo.includes('/') || repo.startsWith('@')) {
- const npmResult = await fetchGitHubRepoFromNpm(repo);
- if (npmResult.success) {
- repo = npmResult.repo;
- input.value = repo; // Update input to show GitHub repo
- } else {
- showRepoError(npmResult.error);
+ try {
+ const githubToken = await getAccessToken();
+ const resolution = await resolveWatchedRepoInput(repoInput, {
+ githubToken,
+ existingRepos: state.watchedRepos
+ });
+
+ if (!resolution.valid) {
+ showRepoError(resolution.error || 'Repository validation failed');
statusEl.className = 'repo-validation-status error';
return;
}
- }
-
- // Validate owner/repo format using the same pattern as the validator
- if (!VALIDATION_PATTERNS.REPOSITORY_NAME.test(repo)) {
- showRepoError('Invalid format. Use: owner/repo, GitHub URL, or npm package');
- statusEl.className = 'repo-validation-status error';
- return;
- }
-
- if (state.watchedRepos.includes(repo)) {
- showRepoError('Repository already added');
- statusEl.className = 'repo-validation-status error';
- return;
- }
-
- // Validate repo exists on GitHub and fetch metadata
- let validationResult;
- try {
- validationResult = await validateRepo(repo);
- } catch (error) {
- console.error('Validation error:', error);
- showRepoError(`Validation error: ${error.message}`);
- statusEl.className = 'repo-validation-status error';
- return;
- }
-
- if (!validationResult) {
- showRepoError('Validation returned no result');
- statusEl.className = 'repo-validation-status error';
- return;
- }
-
- if (!validationResult.valid) {
- showRepoError(validationResult.error || 'Repository validation failed');
- statusEl.className = 'repo-validation-status error';
- return;
- }
- // Check if already added (by fullName)
- const alreadyExists = state.watchedRepos.some(r => (typeof r === 'string' ? r : r.fullName) === validationResult.fullName);
+ input.value = resolution.normalizedRepo;
+ statusEl.className = 'repo-validation-status success';
- if (alreadyExists) {
- showRepoError('Repository already added');
- statusEl.className = 'repo-validation-status error';
- return;
- }
- // Show success indicator
- statusEl.className = 'repo-validation-status success';
-
- try {
- // Add timestamp to track when repo was added (for filtering old activities)
- state.watchedRepos.push({
- ...validationResult,
- addedAt: new Date().toISOString()
- });
+ state.watchedRepos.push(resolution.record);
// Reset to last page to show the newly added repo
const totalPages = Math.ceil(state.watchedRepos.length / state.reposPerPage);
@@ -753,7 +684,7 @@ async function addRepo() {
await setWatchedRepos(state.watchedRepos);
// Show success toast
- toastManager.success(`Successfully added ${validationResult.fullName} to watched repositories`);
+ toastManager.success(`Successfully added ${resolution.record.fullName} to watched repositories`);
} catch (error) {
console.error('Error adding repository:', error);
showRepoError('Failed to add repository. Please try again.');
@@ -812,51 +743,7 @@ function showRepoError(message) {
*/
async function validateRepo(repo) {
const githubToken = await getAccessToken();
-
- if (!githubToken) {
- return { valid: false, error: 'No GitHub connection found. Connect GitHub first.' };
- }
-
- // First do basic validation
- const basicResult = await validateRepository(repo, githubToken);
-
- if (!basicResult.valid) {
- return basicResult;
- }
-
- try {
- // Try to fetch latest release for additional metadata
- const headers = createHeaders(githubToken);
- const releasesResponse = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { headers });
-
- let latestRelease = null;
- if (releasesResponse.ok) {
- const releaseData = await releasesResponse.json();
- latestRelease = {
- version: releaseData.tag_name,
- publishedAt: releaseData.published_at
- };
- }
- // If no releases, that's ok - latestRelease stays null
-
- // Return the enhanced result
- return {
- valid: true,
- metadata: {
- ...basicResult.metadata,
- latestRelease
- }
- };
- } catch (_error) {
- // If release fetch fails, return basic validation result
- return {
- valid: true,
- metadata: {
- ...basicResult.metadata,
- latestRelease: null
- }
- };
- }
+ return validateWatchedRepo(repo, githubToken);
}
async function removeRepo(repoFullName) {
@@ -882,24 +769,8 @@ async function removeRepo(repoFullName) {
async function cleanupRepoNotifications(repoFullName) {
try {
- // Get current activities and read items from local storage
- const { activities = [], readItems = [] } = await getLocalItems(['activities', 'readItems']);
-
- // Filter out activities for the removed repository
- const updatedActivities = activities.filter(activity => activity.repo !== repoFullName);
-
- // Filter out read items for activities from the removed repository
- const removedActivityIds = activities
- .filter(activity => activity.repo === repoFullName)
- .map(activity => activity.id);
-
- const updatedReadItems = readItems.filter(id => !removedActivityIds.includes(id));
-
- // Save the filtered data back to local storage
- await setLocalItem('activities', updatedActivities);
- await setLocalItem('readItems', updatedReadItems);
-
- } catch (error) {
+ await chrome.runtime.sendMessage({ action: 'removeRepoData', repo: repoFullName });
+ } catch (error) {
console.error(`[DevWatch] Error cleaning up notifications for ${repoFullName}:`, error);
// Don't show an error message to the user since this is cleanup logic
// The repo removal was successful even if cleanup failed
@@ -1182,11 +1053,50 @@ function parseChangelogMarkdown(markdown) {
return html;
}
-// Auto-refresh snoozed repos list every 5 minutes
-setInterval(async () => {
- const settings = await chrome.storage.sync.get(['snoozedRepos']);
- renderSnoozedRepos(settings.snoozedRepos || []);
-}, 300000);
+// Auto-refresh snoozed repos list:
+// - storage.onChanged: react instantly when snoozes are added/removed
+// - interval: tick down "time remaining" so expired snoozes drop off the list
+let snoozedReposRefreshIntervalId = null;
+let snoozedReposStorageListener = null;
+
+function setupSnoozedReposAutoRefresh() {
+ if (typeof chrome === 'undefined' || !chrome.storage) {
+ return;
+ }
+
+ const refresh = async () => {
+ try {
+ const settings = await chrome.storage.sync.get(['snoozedRepos']);
+ renderSnoozedRepos(settings.snoozedRepos || []);
+ } catch (error) {
+ console.error('Failed to refresh snoozed repos:', error);
+ }
+ };
+
+ snoozedReposStorageListener = (changes, areaName) => {
+ if (areaName === 'sync' && changes.snoozedRepos) {
+ renderSnoozedRepos(changes.snoozedRepos.newValue || []);
+ }
+ };
+ chrome.storage.onChanged.addListener(snoozedReposStorageListener);
+
+ snoozedReposRefreshIntervalId = setInterval(refresh, 300000);
+
+ if (typeof window !== 'undefined') {
+ window.addEventListener('beforeunload', teardownSnoozedReposAutoRefresh, { once: true });
+ }
+}
+
+function teardownSnoozedReposAutoRefresh() {
+ if (snoozedReposRefreshIntervalId !== null) {
+ clearInterval(snoozedReposRefreshIntervalId);
+ snoozedReposRefreshIntervalId = null;
+ }
+ if (snoozedReposStorageListener && chrome?.storage?.onChanged) {
+ chrome.storage.onChanged.removeListener(snoozedReposStorageListener);
+ snoozedReposStorageListener = null;
+ }
+}
// ES6 exports for tests
export {
diff --git a/package-lock.json b/package-lock.json
index 88c2784..2eb14f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "github-devwatch-chrome",
- "version": "1.0.2",
+ "version": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "github-devwatch-chrome",
- "version": "1.0.2",
+ "version": "1.0.3",
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.39.1",
diff --git a/package.json b/package.json
index 311cbcd..8236bb2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "github-devwatch-chrome",
- "version": "1.0.2",
+ "version": "1.0.3",
"description": "Chrome extension for monitoring GitHub repository activity",
"type": "module",
"scripts": {
diff --git a/popup/controllers/activity-controller.js b/popup/controllers/activity-controller.js
index 0053deb..825290b 100644
--- a/popup/controllers/activity-controller.js
+++ b/popup/controllers/activity-controller.js
@@ -9,19 +9,16 @@ import {
showCachedActivities
} from '../../shared/offline-manager.js';
import { setState } from '../../shared/state-manager.js';
+import { filterVisibleActivities } from '../../shared/feed-policy.js';
/**
* Loads activities from storage or cache, handles offline mode
* @param {Function} renderActivitiesCallback - Callback to render activities after loading
- * @param {Function} setPinnedReposCallback - Callback to update pinned repos
- * @param {Function} setCollapsedReposCallback - Callback to update collapsed repos
* @param {Object} options - Loading options
* @param {boolean} options.skipLoadingIndicator - Don't show loading state if true (for refresh)
*/
export async function loadActivities(
renderActivitiesCallback,
- setPinnedReposCallback,
- setCollapsedReposCallback,
options = {}
) {
const { skipLoadingIndicator = false } = options;
@@ -37,17 +34,18 @@ export async function loadActivities(
try {
const cachedActivities = await getCachedData('activities_cache');
const cachedReadItems = await getCachedData('readItems_cache');
- const { mutedRepos, snoozedRepos } = await getFilteringSettings();
+ const { mutedRepos, snoozedRepos, pinnedRepos } = await getFilteringSettings();
if (cachedActivities) {
- // Filter out muted and snoozed repos using shared utilities
const excludedRepos = getExcludedRepos(mutedRepos, snoozedRepos);
- const filteredActivities = cachedActivities.filter(a => !excludedRepos.has(a.repo));
+ const filteredActivities = filterVisibleActivities(cachedActivities, { excludedRepos });
// Update state with cached data
await setState({
allActivities: filteredActivities,
- readItems: cachedReadItems || []
+ readItems: cachedReadItems || [],
+ pinnedRepos,
+ collapsedRepos: new Set()
}, { persist: false }); // Don't persist cached data to storage
// Show cached indicator
@@ -89,20 +87,15 @@ export async function loadActivities(
const settings = await getFilteringSettings();
const pinnedRepos = await getSyncItem('pinnedRepos', []);
- // Update pinned repos through callback
- setPinnedReposCallback(pinnedRepos);
-
- // Load collapsed state through callback
- setCollapsedReposCallback(new Set(data.collapsedRepos || []));
-
- // Filter out muted and snoozed repos using shared utilities
const excludedRepos = getExcludedRepos(settings.mutedRepos, settings.snoozedRepos);
- const filteredActivities = (data.activities || []).filter(a => !excludedRepos.has(a.repo));
+ const filteredActivities = filterVisibleActivities(data.activities || [], { excludedRepos });
// Update state manager with loaded data
await setState({
allActivities: filteredActivities,
- readItems: data.readItems || []
+ readItems: data.readItems || [],
+ pinnedRepos,
+ collapsedRepos: new Set(data.collapsedRepos || [])
});
// Cache the loaded data for offline use
@@ -183,8 +176,8 @@ export function updateRateLimit(rateLimit) {
// Show in yellow warning when low
rateLimitInfo.innerHTML = `
-
-
+
+
${rateLimit.remaining}/${rateLimit.limit} API calls remaining
`;
diff --git a/popup/controllers/repository-controller.js b/popup/controllers/repository-controller.js
index cd7f862..d7b6fae 100644
--- a/popup/controllers/repository-controller.js
+++ b/popup/controllers/repository-controller.js
@@ -1,6 +1,15 @@
import { showError } from '../../shared/error-handler.js';
-import { useState, setState } from '../../shared/state-manager.js';
-import { groupByRepo } from '../views/activity-item-view.js';
+import { stateManager, useState, setState } from '../../shared/state-manager.js';
+import { groupByRepo } from '../../shared/feed-presentation.js';
+import {
+ clearArchivedFeedData,
+ getAllActivityIds,
+ getRepoUnreadActivityIds,
+ markActivitiesAsRead,
+ markActivityAsUnread,
+ toggleCollapsedRepo,
+ togglePinnedRepoList
+} from '../../shared/feed-mutations.js';
/**
* Toggles the collapsed state of a repository group
@@ -9,17 +18,10 @@ import { groupByRepo } from '../views/activity-item-view.js';
* @param {Function} renderActivitiesCallback - Callback to re-render activities
*/
export async function toggleRepoCollapse(repo, collapsedRepos, renderActivitiesCallback) {
- if (collapsedRepos.has(repo)) {
- collapsedRepos.delete(repo);
- } else {
- collapsedRepos.add(repo);
- }
+ const updatedCollapsedRepos = toggleCollapsedRepo(collapsedRepos, repo);
+ await setState({ collapsedRepos: updatedCollapsedRepos });
- // Save collapsed state
- await chrome.storage.local.set({ collapsedRepos: Array.from(collapsedRepos) });
-
- // Re-render
- renderActivitiesCallback();
+ renderActivitiesCallback?.();
}
/**
@@ -31,25 +33,10 @@ export async function toggleRepoCollapse(repo, collapsedRepos, renderActivitiesC
*/
export async function togglePinRepo(repo, pinnedRepos, setPinnedReposCallback, renderActivitiesCallback) {
try {
- const isCurrentlyPinned = pinnedRepos.includes(repo);
- let updatedPinnedRepos;
-
- if (isCurrentlyPinned) {
- // Unpin the repo
- updatedPinnedRepos = pinnedRepos.filter(r => r !== repo);
- } else {
- // Pin the repo
- updatedPinnedRepos = [...pinnedRepos, repo];
- }
-
- // Save to storage
- await chrome.storage.sync.set({ pinnedRepos: updatedPinnedRepos });
-
- // Update through callback
- setPinnedReposCallback(updatedPinnedRepos);
-
- // Re-render activities to show updated pin state
- renderActivitiesCallback();
+ const updatedPinnedRepos = togglePinnedRepoList(pinnedRepos, repo);
+ await setState({ pinnedRepos: updatedPinnedRepos });
+ setPinnedReposCallback?.(updatedPinnedRepos);
+ renderActivitiesCallback?.();
} catch (error) {
showError('errorMessage', error, null, { action: 'toggle pin repository', repo }, 3000);
}
@@ -62,40 +49,7 @@ export async function togglePinRepo(repo, pinnedRepos, setPinnedReposCallback, r
*/
export async function snoozeRepo(repo, loadActivitiesCallback) {
try {
- // Get snooze duration and mark-as-read setting
- const settings = await chrome.storage.sync.get(['snoozeHours', 'snoozedRepos', 'markReadOnSnooze']);
- const snoozeHours = settings.snoozeHours || 1;
- const snoozedRepos = settings.snoozedRepos || [];
- const markReadOnSnooze = settings.markReadOnSnooze === true;
-
- // Calculate expiration time
- const expiresAt = Date.now() + (snoozeHours * 60 * 60 * 1000);
-
- // Check if repo is already snoozed and update, otherwise add new
- const existingIndex = snoozedRepos.findIndex(s => s.repo === repo);
- if (existingIndex >= 0) {
- snoozedRepos[existingIndex].expiresAt = expiresAt;
- } else {
- snoozedRepos.push({ repo, expiresAt });
- }
-
- // Mark all unread items for this repo as read if setting is enabled
- if (markReadOnSnooze) {
- const { activities = [], readItems = [] } = await chrome.storage.local.get(['activities', 'readItems']);
- const unreadRepoActivityIds = activities
- .filter(activity => activity.repo === repo && !readItems.includes(activity.id))
- .map(activity => activity.id);
-
- if (unreadRepoActivityIds.length > 0) {
- const updatedReadItems = [...readItems, ...unreadRepoActivityIds];
- await chrome.storage.local.set({ readItems: updatedReadItems });
- }
- }
-
- // Save to storage
- await chrome.storage.sync.set({ snoozedRepos });
-
- // Reload activities to reflect the snooze
+ await chrome.runtime.sendMessage({ action: 'snoozeRepo', repo });
await loadActivitiesCallback();
} catch (error) {
showError('errorMessage', error, null, { action: 'snooze repository', repo }, 3000);
@@ -139,11 +93,9 @@ export async function toggleReadState(id, renderActivitiesCallback) {
try {
await chrome.runtime.sendMessage({ action, id });
if (isRead) {
- const newReadItems = state.readItems.filter(item => item !== id);
- setState({ readItems: newReadItems });
+ await setState({ readItems: markActivityAsUnread(state.readItems, id) });
} else {
- const newReadItems = [...state.readItems, id];
- setState({ readItems: newReadItems });
+ await setState({ readItems: markActivitiesAsRead(state.readItems, [id]) });
}
renderActivitiesCallback();
} catch (error) {
@@ -161,8 +113,7 @@ export async function markAsRead(id) {
try {
await chrome.runtime.sendMessage({ action: 'markAsRead', id });
- const newReadItems = [...state.readItems, id];
- setState({ readItems: newReadItems });
+ await setState({ readItems: markActivitiesAsRead(state.readItems, [id]) });
} catch (error) {
showError('errorMessage', error, null, { action: 'mark as read' }, 3000);
}
@@ -197,10 +148,10 @@ export function markAsReadWithAnimation(id, itemElement, renderActivitiesCallbac
*/
export async function handleMarkAllRead(renderActivitiesCallback) {
try {
- await chrome.runtime.sendMessage({ action: 'markAllAsRead' });
const state = useState();
- const newReadItems = state.allActivities.map(a => a.id);
- setState({ readItems: newReadItems });
+ const visibleActivityIds = getAllActivityIds(stateManager.getFilteredActivities());
+ await chrome.runtime.sendMessage({ action: 'markActivitiesAsRead', ids: visibleActivityIds });
+ await setState({ readItems: markActivitiesAsRead(state.readItems, visibleActivityIds) });
renderActivitiesCallback();
} catch (error) {
showError('errorMessage', error, null, { action: 'mark all as read' }, 3000);
@@ -214,21 +165,21 @@ export async function handleMarkAllRead(renderActivitiesCallback) {
*/
export async function handleCollapseAll(collapsedRepos, renderActivitiesCallback) {
const state = useState();
- const grouped = groupByRepo(state.allActivities);
+ const grouped = groupByRepo(state.allActivities, state.pinnedRepos);
const allRepos = Object.keys(grouped);
- const allCollapsed = collapsedRepos.size === allRepos.length;
+ const updatedCollapsedRepos = new Set(collapsedRepos);
+ const allCollapsed = updatedCollapsedRepos.size === allRepos.length;
if (allCollapsed) {
// Expand all
- collapsedRepos.clear();
+ updatedCollapsedRepos.clear();
} else {
// Collapse all
- allRepos.forEach(repo => collapsedRepos.add(repo));
+ allRepos.forEach(repo => updatedCollapsedRepos.add(repo));
}
- // Save and re-render
- await chrome.storage.local.set({ collapsedRepos: Array.from(collapsedRepos) });
- renderActivitiesCallback();
+ await setState({ collapsedRepos: updatedCollapsedRepos });
+ renderActivitiesCallback?.();
}
/**
@@ -239,24 +190,39 @@ export async function handleCollapseAll(collapsedRepos, renderActivitiesCallback
export async function markRepoAsRead(repo, renderActivitiesCallback) {
try {
const state = useState();
-
- // Find all unread items in this repo
- const repoActivities = state.allActivities.filter(activity => activity.repo === repo);
- const unreadItems = repoActivities.filter(activity => !state.readItems.includes(activity.id));
+ const unreadItems = getRepoUnreadActivityIds(state.allActivities, state.readItems, repo);
if (unreadItems.length === 0) {
return; // Nothing to mark as read
}
- // Mark all unread items in this repo as read
- for (const activity of unreadItems) {
- await markAsRead(activity.id);
- }
-
- // Re-render to update UI
+ await chrome.runtime.sendMessage({ action: 'markRepoAsRead', repo });
+ await setState({ readItems: markActivitiesAsRead(state.readItems, unreadItems) });
renderActivitiesCallback();
} catch (error) {
console.error('[markRepoAsRead] Error:', error);
showError('errorMessage', error, null, { action: 'mark repo as read' }, 3000);
}
}
+
+/**
+ * Clears archived items from storage and local UI state.
+ * @param {Function} renderActivitiesCallback - Callback to re-render activities
+ */
+export async function clearArchive(renderActivitiesCallback) {
+ try {
+ await chrome.runtime.sendMessage({ action: 'clearArchive' });
+ const state = useState();
+ const clearedState = clearArchivedFeedData({
+ activities: state.allActivities,
+ readItems: state.readItems
+ });
+ await setState({
+ allActivities: clearedState.activities,
+ readItems: clearedState.readItems
+ });
+ renderActivitiesCallback?.();
+ } catch (error) {
+ showError('errorMessage', error, null, { action: 'clear archive' }, 3000);
+ }
+}
diff --git a/popup/popup.css b/popup/popup.css
index 7793ad1..f91b146 100644
--- a/popup/popup.css
+++ b/popup/popup.css
@@ -1,48 +1,29 @@
+/* ================================================
+ DEVWATCH POPUP — Terminal Ledger Theme
+ ================================================ */
+
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
-:root {
- --bg-primary: #ffffff;
- --bg-secondary: #f6f8fa;
- --bg-hover: #f6f8fa;
- --bg-unread: #f1f8ff;
- --border-color: #e1e4e8;
- --text-primary: #24292e;
- --text-secondary: #586069;
- --link-color: #0366d6;
- --badge-bg: #0366d6;
- --success-bg: #d4edda;
- --success-text: #155724;
-
- }
-
-body.dark-mode {
- --bg-primary: #0d1117;
- --bg-secondary: #161b22;
- --bg-hover: #1c2128;
- --bg-unread: #1c2d41;
- --border-color: #30363d;
- --text-primary: #c9d1d9;
- --text-secondary: #8b949e;
- --link-color: #58a6ff;
- --badge-bg: #58a6ff;
- --success-bg: #1b4620;
- --success-text: #7ee787;
-}
-
body {
width: 450px;
max-height: 600px;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
- font-size: 15px;
- color: var(--text-primary);
- background: var(--bg-primary);
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--foreground);
+ background:
+ radial-gradient(circle at top right, var(--bg-gradient-glow), transparent 32%),
+ linear-gradient(180deg, var(--background) 0%, var(--card) 100%);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11";
}
-/* Utility classes */
+/* ---- Utilities ---- */
.hidden {
display: none !important;
}
@@ -54,85 +35,106 @@ body {
.empty-message {
text-align: center;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
padding: 20px;
}
+/* ---- Layout ---- */
.container {
display: flex;
flex-direction: column;
height: 100%;
+ background: transparent;
}
+/* ---- Header ---- */
header {
- display: none; /* Hidden by default to prevent flash on load */
+ display: none;
justify-content: space-between;
align-items: center;
- padding: 18px;
- border-bottom: 1px solid var(--border-color);
- background: var(--bg-secondary);
+ padding: 16px 16px 12px;
+ border-bottom: 1px solid var(--border);
+ background: transparent;
}
.header-info {
display: flex;
align-items: center;
- gap: 10px;
+ gap: 12px;
}
.header-stats {
display: flex;
- flex-direction: column;
- gap: 2px;
- align-items: flex-end;
+ flex-direction: row;
+ gap: 0;
+ align-items: center;
+}
+
+.header-stats .repo-count::before {
+ content: "\B7";
+ margin: 0 6px;
+ color: var(--muted-foreground);
}
.header-refresh-btn {
- padding: 4px;
- background: none;
- border: none;
- border-radius: 4px;
+ padding: 6px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
cursor: pointer;
- color: var(--text-secondary);
- transition: all 0.2s;
+ color: var(--muted-foreground);
+ transition: all 150ms;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.header-refresh-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- transform: scale(1.1);
+ background: var(--accent);
+ color: var(--accent-foreground);
+ border-color: var(--primary);
}
.header-refresh-btn:focus-visible {
- outline: 2px solid var(--link-color);
- outline-offset: 2px;
+ outline: none;
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.last-updated {
font-size: 12px;
- color: var(--text-secondary);
- font-style: italic;
+ color: var(--muted-foreground);
+ font-weight: 400;
}
.repo-count {
- font-size: 11px;
- color: var(--text-secondary);
+ font-size: 12px;
+ color: var(--muted-foreground);
}
h1 {
- font-size: 18px;
+ font-size: 16px;
font-weight: 600;
+ 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;
+ text-transform: uppercase;
}
+/* ---- Icon Buttons (shared) ---- */
.icon-btn {
- background: none;
- border: none;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
cursor: pointer;
padding: 8px;
- border-radius: 6px;
+ border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
+ transition: all 150ms;
}
.icon-btn svg {
@@ -141,7 +143,9 @@ h1 {
}
.icon-btn:hover {
- background: var(--bg-hover);
+ background: var(--accent);
+ color: var(--accent-foreground);
+ border-color: rgba(255, 255, 255, 0.16);
}
.icon-btn.spinning .refresh-icon {
@@ -149,194 +153,227 @@ h1 {
}
@keyframes spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
}
@keyframes refresh-complete {
- 0% {
- transform: scale(1);
- }
- 50% {
- transform: scale(1.2);
- background: var(--link-color);
- color: white;
- }
- 100% {
- transform: scale(1);
- }
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.15); background: var(--primary); color: var(--primary-foreground); }
+ 100% { transform: scale(1); }
}
.footer-btn.refresh-complete {
animation: refresh-complete 0.4s ease-out;
}
+/* ---- Toolbar ---- */
.toolbar {
- display: none; /* Hidden by default to prevent flash on load */
+ display: none;
justify-content: space-between;
align-items: center;
- padding: 14px 18px;
- border-bottom: 1px solid var(--border-color);
- background: var(--bg-primary);
+ padding: 8px 16px 14px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0);
+ background: transparent;
}
+/* shadcn Tabs component: filled-pill active state */
.filters {
- display: flex;
- gap: 10px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ height: auto;
}
.filter-btn {
- padding: 6px 14px;
- border: 1px solid var(--border-color);
- background: var(--bg-primary);
- color: var(--text-primary);
- border-radius: 6px;
+ padding: 0 14px;
+ height: 30px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--muted-foreground);
+ border-radius: 999px;
cursor: pointer;
- font-size: 14px;
- transition: all 0.2s;
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ transition: all 150ms;
+ white-space: nowrap;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
}
.filter-btn:hover {
- border-color: var(--link-color);
+ color: var(--foreground);
+ background: rgba(255, 255, 255, 0.04);
}
.filter-btn.active {
- background: var(--link-color);
- color: white;
- border-color: var(--link-color);
+ background: var(--primary);
+ color: var(--primary-foreground);
+ border-color: var(--primary);
+ font-weight: 600;
+ box-shadow: none;
}
.toolbar-actions {
display: flex;
- gap: 12px;
+ gap: 8px;
align-items: center;
}
.toolbar-btn {
- background: none;
- border: none;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
cursor: pointer;
- padding: 8px;
- opacity: 0.6;
- transition: opacity 0.2s;
+ padding: 7px;
+ color: var(--muted-foreground);
+ transition: all 150ms;
display: inline-flex;
align-items: center;
justify-content: center;
- border-radius: 6px;
- color: var(--text-secondary);
+ border-radius: 12px;
}
.toolbar-btn svg {
- width: 16px;
- height: 16px;
+ width: 15px;
+ height: 15px;
+ stroke-width: 1.75;
}
.toolbar-btn:hover {
- opacity: 1;
- background: var(--bg-secondary);
+ color: var(--foreground);
+ background: var(--accent);
}
.toolbar-btn.active {
- opacity: 1;
- background: var(--link-color);
- color: white;
+ color: var(--primary-foreground);
+ background: var(--primary);
+ border-color: var(--primary);
}
.toolbar-btn.active svg {
- color: white;
+ color: var(--primary-foreground);
}
+/* ---- Search ---- */
.search-box {
display: flex;
align-items: center;
gap: 10px;
- padding: 14px 18px;
- border-bottom: 1px solid var(--border-color);
- background: var(--bg-secondary);
+ margin: 0 16px 12px;
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.04);
}
.search-label {
- font-size: 14px;
- font-weight: 500;
- color: var(--text-primary);
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--foreground);
white-space: nowrap;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
+/* shadcn Input component */
.search-box input {
width: 100%;
- padding: 10px 14px;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- background: var(--bg-primary);
- color: var(--text-primary);
- font-size: 14px;
+ height: 32px;
+ padding: 0 10px;
+ border: 0;
+ border-radius: 10px;
+ background: transparent;
+ color: var(--foreground);
+ font-size: 13px;
+ transition: all 150ms;
+}
+
+.search-box input::placeholder {
+ color: var(--muted-foreground);
}
.search-box input:focus {
outline: none;
- border-color: var(--link-color);
+ box-shadow: none;
}
+/* ---- Activity List ---- */
.activity-list {
flex: 1;
overflow-y: auto;
min-height: 200px;
- display: none; /* Hidden by default to prevent flash on load */
+ display: none;
}
-/* Custom thin scrollbar styling */
+/* shadcn ScrollArea style scrollbar */
.activity-list::-webkit-scrollbar {
width: 8px;
}
.activity-list::-webkit-scrollbar-track {
background: transparent;
- margin: 4px 0;
}
.activity-list::-webkit-scrollbar-thumb {
- background: var(--border-color);
- border-radius: 4px;
+ background: var(--border);
+ border-radius: 9999px;
border: 2px solid transparent;
background-clip: content-box;
min-height: 40px;
}
.activity-list::-webkit-scrollbar-thumb:hover {
- background: var(--text-secondary);
- background-clip: content-box;
-}
-
-.activity-list::-webkit-scrollbar-thumb:active {
- background: var(--text-primary);
+ background: var(--muted-foreground);
background-clip: content-box;
}
+/* ---- Empty & Loading States ---- */
.empty-state {
- padding: 56px 18px;
+ margin: 12px 16px 18px;
+ padding: 32px 20px;
text-align: center;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
+ border: 1px solid var(--border);
+ border-radius: 20px;
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.empty-state p {
+ font-weight: 600;
+ font-size: 15px;
+ color: var(--foreground);
+ margin-bottom: 6px;
+ letter-spacing: -0.02em;
}
.empty-state small {
display: block;
- margin-top: 10px;
- font-size: 13px;
+ margin-top: 4px;
+ font-size: 14px;
+ color: var(--muted-foreground);
}
+.loading {
+ padding: 48px 24px;
+ text-align: center;
+ color: var(--muted-foreground);
+ font-size: 14px;
+}
+
+/* ---- List Header ---- */
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 10px 18px;
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--border-color);
- font-size: 13px;
- color: var(--text-secondary);
+ margin: 0 16px 10px;
+ padding: 0 0 2px;
+ background: transparent;
+ border-bottom: 0;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--muted-foreground);
position: sticky;
top: 0;
z-index: 11;
@@ -344,37 +381,42 @@ h1 {
.header-actions {
display: flex;
- gap: 12px;
+ gap: 8px;
align-items: center;
}
.time-group-header {
- padding: 10px 18px;
- font-size: 13px;
+ padding: 6px 16px;
+ font-size: 12px;
font-weight: 600;
- color: var(--text-secondary);
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--border-color);
+ color: var(--muted-foreground);
+ background: var(--muted);
+ border-bottom: 1px solid var(--border);
text-transform: uppercase;
- letter-spacing: 0.5px;
+ letter-spacing: 0.05em;
}
+/* ---- Repo Groups ---- */
.repo-group {
position: relative;
+ margin: 0 16px 12px;
+ border: 1px solid var(--border);
+ border-radius: 20px;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.035);
+ backdrop-filter: blur(10px);
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
}
.repo-group-header {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 12px 18px;
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--border-color);
- position: sticky;
- top: var(--list-header-height, 0px);
- z-index: 10;
+ padding: 12px 14px;
+ background: rgba(255, 255, 255, 0.03);
+ border-bottom: 1px solid var(--border);
cursor: pointer;
- transition: opacity 0.3s, transform 0.3s, max-height 0.3s, padding 0.3s, margin 0.3s, border-width 0.3s;
+ transition: opacity 300ms, transform 300ms, max-height 300ms, padding 300ms, margin 300ms, border-width 300ms;
}
.repo-group-header.removing {
@@ -389,18 +431,15 @@ h1 {
}
.repo-group-header:hover {
- background: var(--bg-hover);
+ background: var(--accent);
}
.repo-group-header.pinned {
- border-left: 3px solid #f59e0b;
- background: var(--bg-secondary);
- padding-left: 15px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ box-shadow: inset 3px 0 0 var(--accent-color);
}
.repo-group-header.pinned:hover {
- background: var(--bg-hover);
+ background: var(--accent);
}
.repo-group-title {
@@ -413,20 +452,20 @@ h1 {
background: none;
border: none;
cursor: pointer;
- padding: 4px;
+ padding: 2px;
display: flex;
align-items: center;
justify-content: center;
- color: var(--text-secondary);
- transition: transform 0.2s;
+ color: var(--muted-foreground);
+ transition: transform 150ms;
}
.repo-collapse-btn:hover {
- color: var(--text-primary);
+ color: var(--foreground);
}
.repo-collapse-btn .chevron {
- transition: transform 0.2s;
+ transition: transform 150ms;
}
.repo-collapse-btn .chevron.collapsed {
@@ -434,73 +473,65 @@ h1 {
}
.repo-group-name {
- font-size: 14px;
font-weight: 600;
- color: var(--text-primary);
- font-family: 'Monaco', 'Courier New', monospace;
+ color: var(--foreground);
+ font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+ font-size: 12px;
+ letter-spacing: -0.03em;
+ text-transform: lowercase;
}
.repo-group-actions {
display: flex;
align-items: center;
- gap: 8px;
+ gap: 4px;
}
+/* shadcn Badge — default variant */
.repo-unread-count {
font-size: 11px;
- padding: 2px 6px;
- background: var(--link-color);
- color: white;
- border-radius: 10px;
- font-weight: 600;
-}
-
-.repo-snooze-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 6px;
- border-radius: 6px;
- display: flex;
+ padding: 0 6px;
+ height: 22px;
+ display: inline-flex;
align-items: center;
- justify-content: center;
- color: var(--text-secondary);
- transition: all 0.2s;
-}
-
-.repo-snooze-btn svg {
- width: 16px;
- height: 16px;
-}
-
-.repo-snooze-btn:hover {
- background: var(--bg-secondary);
- color: var(--text-primary);
- transform: scale(1.1);
+ background: var(--primary);
+ color: var(--primary-foreground);
+ border-radius: 9999px;
+ font-weight: 700;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
}
-.repo-pin-btn {
+.repo-snooze-btn,
+.repo-pin-btn,
+.repo-mark-read-btn {
background: none;
border: none;
cursor: pointer;
- padding: 6px;
- border-radius: 6px;
+ padding: 4px;
+ border-radius: calc(var(--radius) - 2px);
display: flex;
align-items: center;
justify-content: center;
- color: var(--text-secondary);
- transition: all 0.2s;
+ color: var(--muted-foreground);
+ transition: all 150ms;
}
+.repo-snooze-btn svg,
.repo-pin-btn svg {
- width: 16px;
- height: 16px;
+ width: 14px;
+ height: 14px;
+}
+
+.repo-mark-read-btn svg {
+ width: 13px;
+ height: 13px;
}
-.repo-pin-btn:hover {
- background: var(--bg-secondary);
- color: var(--text-primary);
- transform: scale(1.1);
+.repo-snooze-btn:hover,
+.repo-pin-btn:hover,
+.repo-mark-read-btn:hover {
+ background: var(--accent);
+ color: var(--foreground);
}
.repo-pin-btn.pinned {
@@ -509,48 +540,25 @@ h1 {
.repo-pin-btn.pinned:hover {
color: var(--accent-color);
- filter: brightness(1.2);
-}
-
-.repo-mark-read-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 6px;
- border-radius: 6px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-secondary);
- transition: all 0.2s;
-}
-
-.repo-mark-read-btn svg {
- width: 14px;
- height: 14px;
-}
-
-.repo-mark-read-btn:hover {
- background: var(--bg-secondary);
- color: var(--text-primary);
- transform: scale(1.1);
}
.options-link {
- color: var(--link-color);
- text-decoration: none;
+ color: var(--primary);
+ text-decoration: underline;
+ text-underline-offset: 4px;
font-weight: 500;
}
.options-link:hover {
text-decoration: underline;
- color: var(--link-hover-color);
+ color: var(--primary);
+ opacity: 0.8;
}
.repo-activities {
max-height: 10000px;
overflow: hidden;
- transition: all 0.3s ease-out;
+ transition: all 300ms ease-out;
transform-origin: top;
opacity: 1;
}
@@ -569,41 +577,62 @@ h1 {
opacity: 0;
}
+/* shadcn ghost button style for text actions */
.text-btn {
- background: none;
- border: none;
- color: var(--link-color);
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ color: var(--muted-foreground);
cursor: pointer;
font-size: 12px;
- padding: 4px 8px;
+ font-weight: 600;
+ padding: 5px 10px;
+ border-radius: 999px;
+ transition: all 150ms;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.text-btn:hover {
- text-decoration: underline;
+ background: var(--accent);
+ color: var(--accent-foreground);
}
+/* ---- Activity Items ---- */
.activity-item {
display: flex;
align-items: flex-start;
- gap: 14px;
- padding: 14px 18px;
- border-bottom: 1px solid var(--border-color);
- transition: all 0.3s ease-out;
+ gap: 12px;
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--border);
+ transition: all 200ms;
transform-origin: top;
max-height: 220px;
overflow: hidden;
}
+.activity-item:last-child {
+ border-bottom: none;
+}
+
.activity-item:hover {
- background: var(--bg-hover);
+ background: var(--accent);
}
.activity-item.unread {
background: var(--bg-unread);
+ box-shadow: inset 2px 0 0 var(--accent-color);
+}
+
+.activity-item.unread:hover {
+ background: var(--accent);
}
.activity-item.read {
- opacity: 0.7;
+ opacity: 0.56;
+}
+
+.activity-item.read:hover {
+ opacity: 0.8;
}
.activity-item.removing {
@@ -617,62 +646,68 @@ h1 {
}
.activity-avatar {
- width: 36px;
- height: 36px;
- border-radius: 50%;
+ width: 32px;
+ height: 32px;
+ border-radius: 9999px;
flex-shrink: 0;
+ border: 1px solid var(--border);
}
.activity-content {
flex: 1;
cursor: pointer;
+ min-width: 0;
}
.activity-actions {
display: flex;
- gap: 4px;
+ gap: 2px;
flex-shrink: 0;
align-self: center;
+ opacity: 0;
+ transition: opacity 150ms;
+}
+
+.activity-item:hover .activity-actions {
+ opacity: 1;
}
.action-btn {
- background: none;
- border: none;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
cursor: pointer;
font-size: 18px;
- padding: 7px;
- opacity: 0.5;
- transition: all 0.2s;
+ padding: 5px;
+ color: var(--muted-foreground);
+ transition: all 150ms;
display: inline-flex;
align-items: center;
justify-content: center;
- border-radius: 4px;
+ border-radius: 10px;
}
.action-btn:hover {
- opacity: 1;
- background: var(--bg-secondary);
- transform: scale(1.1);
+ color: var(--foreground);
+ background: var(--accent);
}
.action-btn svg {
display: block;
- width: 16px;
- height: 16px;
+ width: 14px;
+ height: 14px;
}
.mark-read-btn {
- color: var(--link-color);
- opacity: 0.7;
+ color: var(--muted-foreground);
}
.mark-read-btn:hover {
- opacity: 1;
- background: var(--link-color);
+ background: var(--primary);
+ color: var(--primary-foreground);
}
.mark-read-btn:hover svg {
- color: white;
+ color: var(--primary-foreground);
}
.activity-header {
@@ -680,56 +715,84 @@ h1 {
align-items: center;
gap: 6px;
margin-bottom: 4px;
+ min-width: 0;
}
+/* shadcn Badge — zinc pill with colored dot */
.activity-type {
- display: inline-block;
- padding: 3px 7px;
- border-radius: 12px;
- font-size: 11px;
- font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 3px 8px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.03em;
+ line-height: 1.4;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ color: var(--foreground);
text-transform: uppercase;
}
-.activity-type.pr {
- background: #dcffe4;
- color: #28a745;
+.activity-type::before {
+ content: "";
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ flex-shrink: 0;
}
-.activity-type.issue {
- background: #fff5b1;
- color: #735c0f;
-}
+.activity-type.pr::before { background: #22C55E; }
+.activity-type.issue::before { background: #F59E0B; }
+.activity-type.release::before { background: #3B82F6; }
-.activity-type.release {
- background: #ddf4ff;
- color: #0969da;
-}
+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; }
.activity-repo {
- font-size: 12px;
- color: var(--text-secondary);
+ font-size: 11px;
+ color: var(--muted-foreground);
+ font-weight: 600;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.activity-title {
- font-weight: 500;
- margin-bottom: 5px;
+ font-weight: 600;
+ margin-bottom: 4px;
line-height: 1.4;
- color: var(--text-primary);
- font-size: 15px;
+ color: var(--foreground);
+ font-size: 13px;
+ letter-spacing: -0.02em;
+}
+
+.activity-description {
+ font-size: 12px;
+ color: var(--muted-foreground);
+ line-height: 1.3;
+ margin-bottom: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.activity-meta {
- font-size: 13px;
- color: var(--text-secondary);
+ font-size: 12px;
+ color: var(--muted-foreground);
}
+/* ---- Footer ---- */
footer {
position: sticky;
bottom: 0;
- padding: 10px 18px;
- border-top: 1px solid var(--border-color);
- background: var(--bg-secondary);
+ padding: 12px 16px 16px;
+ border-top: 1px solid var(--border);
+ background: linear-gradient(180deg, rgba(0, 0, 0, 0), var(--background) 22%);
z-index: 20;
}
@@ -746,110 +809,106 @@ footer {
}
.footer-btn {
- background: none;
- border: none;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
cursor: pointer;
padding: 6px;
- border-radius: 6px;
+ border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
- color: var(--text-secondary);
- transition: all 0.2s;
+ color: var(--muted-foreground);
+ transition: all 150ms;
}
.footer-btn svg {
- width: 18px;
- height: 18px;
+ width: 16px;
+ height: 16px;
+ stroke-width: 1.75;
}
-
.footer-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- transform: scale(1.1);
+ background: var(--accent);
+ color: var(--accent-foreground);
}
.footer-btn.spinning .refresh-icon {
animation: spin 1s linear infinite;
}
+/* shadcn outline button */
.footer-btn.skip-btn {
font-size: 12px;
- padding: 4px 8px;
+ font-weight: 600;
+ padding: 4px 10px;
min-width: auto;
height: auto;
- color: var(--text-secondary);
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 4px;
+ color: var(--foreground);
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--input);
+ border-radius: 999px;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.footer-btn.skip-btn:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
+ background: var(--accent);
+ color: var(--accent-foreground);
}
footer a {
- color: var(--link-color);
- text-decoration: none;
- font-size: 13px;
+ color: var(--primary);
+ text-decoration: underline;
+ text-underline-offset: 4px;
+ font-size: 12px;
}
footer a:hover {
- text-decoration: underline;
+ opacity: 0.8;
}
.rate-limit-info {
font-size: 11px;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ letter-spacing: 0.02em;
}
+/* ---- Error & Toast Messages ---- */
.error-message {
- padding: 8px;
+ padding: 8px 12px;
margin-bottom: 8px;
- background: #f8d7da;
- color: #721c24;
- border-radius: 4px;
+ background: var(--error-bg);
+ color: var(--error-text);
+ border: 1px solid var(--error-border);
+ border-radius: var(--radius);
font-size: 12px;
text-align: center;
}
-body.dark-mode .error-message {
- background: #4b1e1e;
- color: #f8d7da;
-}
-/* Toast notification positioning in bottom right */
.error-message.toast-notification {
position: fixed;
- bottom: 70px;
- right: 18px;
+ bottom: 60px;
+ right: 16px;
left: auto;
width: auto;
max-width: 380px;
margin: 0;
z-index: 1000;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- animation: slideIn 0.3s ease-out;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
+ animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
- from {
- transform: translateX(100%);
- opacity: 0;
- }
- to {
- transform: translateX(0);
- opacity: 1;
- }
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
}
-/* Toast error styling */
.error-toast {
- background: var(--bg-primary);
- border: 1px solid #dc3545;
- border-radius: 6px;
+ background: var(--background);
+ border: 1px solid var(--destructive);
+ border-radius: var(--radius);
overflow: hidden;
}
@@ -859,12 +918,12 @@ body.dark-mode .error-message {
gap: 8px;
padding: 10px 12px;
cursor: pointer;
- transition: background 0.2s;
+ transition: background 150ms;
user-select: none;
}
.error-summary:hover {
- background: var(--bg-hover);
+ background: var(--accent);
}
.error-icon {
@@ -875,18 +934,15 @@ body.dark-mode .error-message {
.error-brief {
flex: 1;
font-weight: 600;
- font-size: 13px;
- color: #dc3545;
+ font-size: 14px;
+ color: var(--destructive);
}
-body.dark-mode .error-brief {
- color: #f8d7da;
-}
.error-expand {
font-size: 20px;
- color: var(--text-secondary);
- transition: transform 0.2s;
+ color: var(--muted-foreground);
+ transition: transform 150ms;
flex-shrink: 0;
}
@@ -897,9 +953,9 @@ body.dark-mode .error-brief {
.error-details {
max-height: 0;
overflow: hidden;
- transition: max-height 0.3s ease-out;
- background: var(--bg-secondary);
- border-top: 1px solid var(--border-color);
+ transition: max-height 300ms ease-out;
+ background: var(--muted);
+ border-top: 1px solid var(--border);
}
.error-details.visible {
@@ -911,18 +967,18 @@ body.dark-mode .error-brief {
margin: 0 0 8px 0;
font-size: 12px;
line-height: 1.5;
- color: var(--text-primary);
+ color: var(--foreground);
}
.error-technical {
display: block;
- font-size: 11px;
- color: var(--text-secondary);
- font-family: 'Monaco', 'Courier New', monospace;
+ font-size: 12px;
+ color: var(--muted-foreground);
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
margin-top: 8px;
padding: 6px 8px;
- background: var(--bg-hover);
- border-radius: 4px;
+ background: var(--accent);
+ border-radius: calc(var(--radius) - 2px);
word-break: break-word;
}
@@ -933,24 +989,25 @@ body.dark-mode .error-brief {
justify-content: flex-end;
}
+/* shadcn primary button */
.error-dismiss {
- background: var(--link-color);
- color: white;
+ background: var(--primary);
+ color: var(--primary-foreground);
border: none;
- padding: 6px 12px;
- border-radius: 4px;
+ padding: 6px 14px;
+ border-radius: var(--radius);
font-size: 12px;
+ font-weight: 500;
cursor: pointer;
- transition: opacity 0.2s;
+ transition: opacity 150ms;
}
.error-dismiss:hover {
opacity: 0.9;
}
-/* Enhanced error message styles */
.error-content {
- line-height: 1.4;
+ line-height: 1.5;
}
.error-content strong {
@@ -964,23 +1021,23 @@ body.dark-mode .error-brief {
}
.error-content .error-action {
- background: var(--link-color);
- color: white;
+ background: var(--primary);
+ color: var(--primary-foreground);
border: none;
padding: 4px 8px;
- border-radius: 4px;
- font-size: 11px;
+ border-radius: var(--radius);
+ font-size: 12px;
cursor: pointer;
margin-top: 4px;
}
.error-content .error-action:hover {
- opacity: 0.8;
+ opacity: 0.9;
}
-/* Success message styles */
+/* Success message */
.success-content {
- line-height: 1.4;
+ line-height: 1.5;
color: var(--success-text);
}
@@ -994,23 +1051,18 @@ body.dark-mode .error-brief {
margin: 0;
}
-/* Success message background */
.error-message:has(.success-content) {
background: var(--success-bg);
color: var(--success-text);
+ border-color: var(--success-border);
}
-body.dark-mode .error-message:has(.success-content) {
- background: var(--success-bg);
- color: var(--success-text);
-}
-
-/* Offline status styles */
+/* ---- Offline States ---- */
.offline-message {
- background: #fff3cd;
- color: #856404;
- border: 1px solid #ffeeba;
- border-radius: 4px;
+ background: #FFFBEB;
+ color: #92400E;
+ border: 1px solid #FDE68A;
+ border-radius: var(--radius);
padding: 12px;
margin-bottom: 8px;
font-size: 12px;
@@ -1018,9 +1070,9 @@ body.dark-mode .error-message:has(.success-content) {
}
body.dark-mode .offline-message {
- background: #664d03;
- color: #ffecb5;
- border-color: #543f01;
+ background: rgba(234, 179, 8, 0.1);
+ color: #FCD34D;
+ border-color: rgba(234, 179, 8, 0.2);
}
.offline-status {
@@ -1045,7 +1097,6 @@ body.dark-mode .offline-message {
line-height: 1.3;
}
-/* Empty state when offline */
.offline-empty {
text-align: center;
padding: 32px 16px;
@@ -1061,21 +1112,21 @@ body.dark-mode .offline-message {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
- color: var(--text-primary);
+ color: var(--foreground);
}
.offline-empty small {
- color: var(--text-secondary);
+ color: var(--muted-foreground);
font-size: 12px;
}
-/* Cached data indicator */
.cached-indicator {
- background: #e7f3ff;
- color: #0366d6;
+ background: var(--muted);
+ color: var(--muted-foreground);
padding: 8px 12px;
margin-bottom: 12px;
- border-radius: 4px;
+ border-radius: var(--radius);
+ border: 1px solid var(--border);
font-size: 12px;
display: flex;
align-items: center;
@@ -1083,22 +1134,21 @@ body.dark-mode .offline-message {
}
body.dark-mode .cached-indicator {
- background: #0d2b45;
- color: #58a6ff;
+ background: var(--muted);
+ color: var(--muted-foreground);
}
.cached-icon {
font-size: 14px;
}
-/* Activity items marked as cached */
.activity-item.cached {
- border-left: 3px solid #ffc107;
+ border-left: 2px solid var(--muted-foreground);
opacity: 0.9;
}
.activity-item.cached::before {
- content: '💾';
+ content: '';
position: absolute;
top: 8px;
right: 8px;
@@ -1106,204 +1156,191 @@ body.dark-mode .cached-indicator {
opacity: 0.7;
}
-.loading {
- padding: 48px 24px;
- text-align: center;
- color: var(--text-secondary);
- font-size: 14px;
-}
-
-/* Dark mode badge colors */
-.activity-type.pr {
- background: #dcffe4;
- color: #28a745;
-}
-
-body.dark-mode .activity-type.pr {
- background: #1b4620;
- color: #7ee787;
-}
-
-.activity-type.issue {
- background: #fff5b1;
- color: #735c0f;
-}
-
-body.dark-mode .activity-type.issue {
- background: #4b3d1c;
- color: #e3b341;
-}
-
-.activity-type.release {
- background: #ddf4ff;
- color: #0969da;
-}
-
-body.dark-mode .activity-type.release {
- background: #1c3d5a;
- color: #58a6ff;
-}
-
-/* Focus indicators for accessibility */
+/* ---- shadcn Focus Ring ---- */
.filter-btn:focus-visible,
.toolbar-btn:focus-visible,
.footer-btn:focus-visible,
.icon-btn:focus-visible {
- outline: 2px solid var(--link-color);
- outline-offset: 2px;
+ outline: none;
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.search-box input:focus-visible {
- outline: 2px solid var(--link-color);
- outline-offset: -2px;
- box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
+ outline: none;
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
body.dark-mode .search-box input:focus-visible {
- box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.activity-content:focus-visible {
- outline: 2px solid var(--link-color);
+ outline: 2px solid var(--ring);
outline-offset: -2px;
- border-radius: 4px;
+ border-radius: calc(var(--radius) - 2px);
}
.action-btn:focus-visible,
.repo-collapse-btn:focus-visible,
-.repo-snooze-btn:focus-visible {
- outline: 2px solid var(--link-color);
- outline-offset: 2px;
+.repo-snooze-btn:focus-visible,
+.repo-pin-btn:focus-visible,
+.repo-mark-read-btn:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.text-btn:focus-visible {
- outline: 2px solid var(--link-color);
- outline-offset: 2px;
- border-radius: 4px;
+ outline: none;
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
+ border-radius: calc(var(--radius) - 2px);
}
.error-summary:focus-visible {
- outline: 2px solid var(--link-color);
- outline-offset: -2px;
+ outline: none;
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
-/* Onboarding Styles */
+/* ================================================
+ ONBOARDING STYLES
+ ================================================ */
.onboarding-view {
flex: 1;
- padding: 12px;
- overflow-y: auto;
+ padding: 18px 18px 14px;
+ overflow: hidden;
min-height: 0;
+ background: transparent;
+ display: flex;
+ flex-direction: column;
}
-/* Custom thin scrollbar styling for onboarding */
.onboarding-view::-webkit-scrollbar {
- width: 8px;
+ width: 6px;
}
.onboarding-view::-webkit-scrollbar-track {
background: transparent;
- margin: 4px 0;
}
.onboarding-view::-webkit-scrollbar-thumb {
- background: var(--border-color);
- border-radius: 4px;
- border: 2px solid transparent;
- background-clip: content-box;
- min-height: 40px;
+ background: var(--border);
+ border-radius: 9999px;
}
.onboarding-view::-webkit-scrollbar-thumb:hover {
- background: var(--text-secondary);
- background-clip: content-box;
-}
-
-.onboarding-view::-webkit-scrollbar-thumb:active {
- background: var(--text-primary);
- background-clip: content-box;
+ background: var(--muted-foreground);
}
+/* Progress — thin line */
.onboarding-progress {
- margin-bottom: 20px;
+ margin-bottom: 18px;
text-align: center;
+ flex-shrink: 0;
}
.progress-bar {
width: 100%;
- height: 6px;
- background: var(--bg-hover);
- border-radius: 3px;
- margin-bottom: 8px;
+ height: 4px;
+ background: var(--border);
+ border-radius: 9999px;
+ margin-bottom: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
- background: var(--link-color);
- border-radius: 3px;
- transition: width 0.3s ease;
+ background: var(--primary);
+ border-radius: 9999px;
+ transition: width 400ms cubic-bezier(0.4, 0, 0.2, 1);
}
.progress-text {
- font-size: 14px;
- color: var(--text-secondary);
- font-weight: 500;
+ font-size: 11px;
+ color: var(--muted-foreground);
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.onboarding-content {
- margin-bottom: 20px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-height: 0;
}
.onboarding-step {
text-align: center;
}
+/* Step icon — hero treatment */
.step-icon {
- font-size: 48px;
- margin-bottom: 16px;
- display: block;
+ margin-bottom: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.step-icon img {
+ border-radius: 22px;
+ box-shadow: 0 18px 36px rgba(0, 0, 0, 0.18);
+}
+
+.step-icon svg {
+ color: var(--foreground);
}
-/* Hide step icon for steps 2-4 to save space */
.token-step .step-icon,
.repos-step .step-icon,
.categories-step .step-icon {
display: none;
}
+/* Headings — large, tight, confident */
.onboarding-step h2 {
- font-size: 22px;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 10px;
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--foreground);
+ margin-bottom: 8px;
+ letter-spacing: -0.04em;
+ line-height: 1.05;
}
.onboarding-step p {
- color: var(--text-secondary);
- line-height: 1.4;
- margin-bottom: 16px;
- max-width: 350px;
+ color: var(--muted-foreground);
+ line-height: 1.5;
+ margin-bottom: 14px;
+ max-width: 340px;
margin-left: auto;
margin-right: auto;
+ font-size: 13px;
}
/* Welcome Step */
.feature-list {
display: flex;
flex-direction: column;
- gap: 8px;
- margin: 16px 0;
+ gap: 10px;
+ margin: 24px 0;
text-align: left;
}
.feature-item {
display: flex;
align-items: center;
- gap: 10px;
- padding: 10px;
- background: var(--bg-secondary);
- border-radius: 6px;
- border: 1px solid var(--border-color);
+ gap: 16px;
+ padding: 16px;
+ background: rgba(255, 255, 255, 0.04);
+ border-radius: 18px;
+ border: 1px solid var(--border);
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.feature-item:hover {
+ background: var(--accent);
+ transform: translateY(-2px);
+ box-shadow: 0 14px 24px rgba(0, 0, 0, 0.12);
}
.feature-icon {
@@ -1311,153 +1348,192 @@ body.dark-mode .search-box input:focus-visible {
display: flex;
align-items: center;
justify-content: center;
- width: 20px;
- height: 20px;
+ width: 40px;
+ height: 40px;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ color: var(--accent-color);
}
.feature-icon svg {
- width: 20px;
- height: 20px;
+ width: 18px;
+ height: 18px;
}
-.step-description {
- font-size: 15px;
- color: var(--text-primary);
+.feature-item span:last-child {
+ font-size: 14px;
font-weight: 500;
+ color: var(--foreground);
+ line-height: 1.4;
+}
+
+.step-description {
+ font-size: 13px;
+ color: var(--muted-foreground);
+ font-weight: 400;
+ margin-top: 4px;
}
/* Token Step */
.token-instructions {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 6px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ border-radius: 18px;
padding: 16px;
- margin: 16px 0;
+ margin: 14px 0;
text-align: left;
}
.token-instructions h3 {
- margin-bottom: 10px;
- color: var(--text-primary);
- font-size: 15px;
+ margin-bottom: 12px;
+ color: var(--foreground);
+ font-size: 12px;
+ font-weight: 600;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
}
.token-instructions ol {
margin-left: 20px;
- color: var(--text-secondary);
- line-height: 1.4;
+ color: var(--muted-foreground);
+ line-height: 1.5;
font-size: 14px;
}
.token-link {
- color: var(--link-color);
- text-decoration: none;
+ color: var(--primary);
+ text-decoration: underline;
+ text-underline-offset: 4px;
font-weight: 500;
}
.token-link:hover {
- text-decoration: underline;
+ opacity: 0.8;
}
.token-input-group {
display: flex;
+ flex-direction: column;
gap: 8px;
- margin: 16px 0;
- /* Match the width of the token-instructions card */
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- padding: 16px;
+ margin: 14px 0 10px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ padding: 14px;
}
.token-code-stack {
- flex: 1;
display: flex;
- flex-direction: column;
- gap: 6px;
+ align-items: center;
+ gap: 8px;
+}
+
+.token-code-stack .token-input {
+ flex: 1;
+}
+
+.token-code-stack .token-copy-hint {
+ display: none;
+}
+
+.token-input-group .copy-code-btn {
+ flex-shrink: 0;
+}
+
+.token-input-group .validate-btn {
+ width: 100%;
}
+/* shadcn input */
.token-input {
flex: 1;
- padding: 10px;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- font-size: 14px;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- background: var(--bg-primary);
- color: var(--text-primary);
- letter-spacing: 0.14em;
+ padding: 10px 12px;
+ border: 1px solid var(--input);
+ border-radius: 12px;
font-size: 15px;
- font-weight: 700;
+ font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--foreground);
+ letter-spacing: 0.14em;
+ font-weight: 600;
text-align: center;
cursor: pointer;
}
.token-input:focus {
outline: none;
- border-color: var(--link-color);
- box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.token-copy-hint {
margin: 0;
font-size: 12px;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
}
+/* shadcn outline button */
.copy-code-btn {
- padding: 10px 14px;
- background: var(--bg-primary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- font-size: 13px;
+ 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: background 0.2s, border-color 0.2s;
+ transition: all 150ms;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.copy-code-btn:hover:not(:disabled) {
- background: var(--bg-hover);
- border-color: var(--link-color);
+ background: var(--accent);
+ color: var(--accent-foreground);
}
.copy-code-btn:disabled {
- opacity: 0.6;
+ opacity: 0.5;
cursor: not-allowed;
}
+/* Primary button */
.validate-btn {
- padding: 10px 16px;
- background: var(--link-color);
- color: white;
+ padding: 10px 20px;
+ background: var(--primary);
+ color: var(--primary-foreground);
border: none;
- border-radius: 6px;
- font-size: 14px;
- font-weight: 500;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 600;
cursor: pointer;
- transition: background 0.2s;
+ 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;
+ text-transform: uppercase;
}
.validate-btn:hover {
- background: #0256cc;
+ opacity: 0.9;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.validate-btn:disabled {
- background: var(--text-secondary);
+ opacity: 0.5;
cursor: not-allowed;
}
.token-status {
- margin: 12px 0;
- min-height: 20px;
+ margin: 4px 0;
+ min-height: 0;
}
@media (max-width: 560px) {
.token-input-group {
flex-direction: column;
}
-
.copy-code-btn,
.validate-btn {
width: 100%;
@@ -1471,31 +1547,37 @@ body.dark-mode .search-box input:focus-visible {
}
.status-error {
- color: #d73a49;
+ color: var(--destructive);
font-size: 14px;
font-weight: 500;
}
.status-loading {
- color: var(--text-secondary);
+ color: var(--muted-foreground);
font-size: 14px;
font-style: italic;
}
.security-note {
- font-size: 12px;
- color: var(--text-secondary);
- margin-top: 16px;
+ font-size: 11px;
+ color: var(--muted-foreground);
+ margin-top: 8px;
display: flex;
- align-items: center;
+ 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);
+ text-align: left;
}
.info-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
- color: var(--link-color);
+ color: var(--foreground);
}
/* Repos Step */
@@ -1506,8 +1588,9 @@ body.dark-mode .search-box input:focus-visible {
.popular-repos h3 {
margin-bottom: 12px;
- color: var(--text-primary);
- font-size: 15px;
+ color: var(--foreground);
+ font-size: 14px;
+ font-weight: 600;
}
.repo-suggestions {
@@ -1520,15 +1603,17 @@ body.dark-mode .search-box input:focus-visible {
display: flex;
align-items: center;
justify-content: space-between;
- padding: 10px;
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- transition: border-color 0.2s;
+ padding: 10px 12px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.repo-suggestion:hover {
- border-color: var(--link-color);
+ background: var(--accent);
+ transform: translateY(-2px);
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
}
.repo-info {
@@ -1537,14 +1622,14 @@ body.dark-mode .search-box input:focus-visible {
.repo-name {
font-weight: 600;
- color: var(--text-primary);
+ color: var(--foreground);
margin-bottom: 2px;
- font-size: 14px;
+ font-size: 13px;
}
.repo-desc {
font-size: 12px;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
margin-bottom: 4px;
}
@@ -1552,128 +1637,114 @@ body.dark-mode .search-box input:focus-visible {
display: flex;
gap: 8px;
align-items: center;
- font-size: 11px;
+ font-size: 12px;
}
.repo-language {
- color: var(--text-secondary);
+ color: var(--muted-foreground);
font-weight: 500;
}
.repo-stars {
- color: var(--text-secondary);
- font-size: 11px;
+ color: var(--muted-foreground);
+ font-size: 12px;
}
.repo-owner {
- color: var(--link-color);
+ color: var(--foreground);
font-weight: 600;
}
.repo-name-text {
- color: var(--text-primary);
+ color: var(--foreground);
font-weight: 600;
}
.repo-loading {
padding: 20px;
text-align: center;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
font-style: italic;
+ font-size: 14px;
}
.repo-loading.repo-error {
- color: #d73a49;
+ color: var(--destructive);
font-style: normal;
}
-/* Loading spinner */
.repo-loading:not(.repo-error)::before {
content: "";
display: inline-block;
width: 14px;
height: 14px;
- border: 2px solid var(--border-color);
- border-top-color: var(--link-color);
+ border: 2px solid var(--border);
+ border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
+/* Add repo button */
.add-repo-btn {
width: 32px;
height: 32px;
padding: 0;
- background: var(--link-color);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 18px;
- font-weight: 400;
+ background: var(--primary);
+ color: var(--primary-foreground);
+ border: 1px solid var(--primary);
+ border-radius: 12px;
+ font-size: 14px;
+ font-weight: 600;
cursor: pointer;
- transition: all 0.2s ease;
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
- box-shadow: 0 1px 3px rgba(3, 102, 214, 0.2);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.add-repo-btn:hover {
- background: #0256cc;
- transform: scale(1.05);
- box-shadow: 0 2px 6px rgba(3, 102, 214, 0.3);
+ opacity: 0.9;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.add-repo-btn:active {
- transform: scale(0.95);
+ opacity: 0.8;
+ transform: translateY(0);
}
.add-repo-btn.loading {
- background: var(--text-secondary);
+ opacity: 0.5;
cursor: wait;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
- 0%, 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.6;
- }
+ 0%, 100% { opacity: 0.5; }
+ 50% { opacity: 0.3; }
}
.add-repo-btn.added {
- background: #10b981;
+ background: #16A34A;
+ color: white;
cursor: default;
- animation: successPop 0.3s ease-out;
+ animation: successPop 0.2s ease-out;
}
@keyframes successPop {
- 0% {
- transform: scale(1);
- }
- 50% {
- transform: scale(1.15);
- }
- 100% {
- transform: scale(1);
- }
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+ 100% { transform: scale(1); }
}
.add-repo-btn:disabled {
cursor: not-allowed;
opacity: 1;
transform: none;
- box-shadow: none;
}
.manual-repo {
@@ -1682,8 +1753,9 @@ body.dark-mode .search-box input:focus-visible {
.manual-repo h3 {
margin-bottom: 8px;
- color: var(--text-primary);
- font-size: 15px;
+ color: var(--foreground);
+ font-size: 14px;
+ font-weight: 600;
}
.manual-input-group {
@@ -1694,34 +1766,38 @@ body.dark-mode .search-box input:focus-visible {
.repo-input {
flex: 1;
- padding: 10px;
- border: 1px solid var(--border-color);
- border-radius: 6px;
+ height: 36px;
+ padding: 0 12px;
+ border: 1px solid var(--input);
+ border-radius: 12px;
font-size: 14px;
- background: var(--bg-primary);
- color: var(--text-primary);
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--foreground);
}
.repo-input:focus {
outline: none;
- border-color: var(--link-color);
- box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
+/* shadcn primary button */
.add-btn {
- padding: 10px 16px;
- background: var(--link-color);
- color: white;
+ padding: 0 16px;
+ height: 36px;
+ background: var(--primary);
+ color: var(--primary-foreground);
border: none;
- border-radius: 6px;
- font-size: 14px;
- font-weight: 500;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 600;
cursor: pointer;
- transition: background 0.2s;
+ transition: opacity 150ms;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.add-btn:hover {
- background: #0256cc;
+ opacity: 0.9;
}
.repo-status {
@@ -1729,11 +1805,11 @@ body.dark-mode .search-box input:focus-visible {
min-height: 0;
}
-/* Categories Step - Minimal List Design */
+/* Categories Step */
.categories-step .step-subtitle {
margin-top: -8px;
margin-bottom: 20px;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
font-size: 14px;
}
@@ -1741,46 +1817,59 @@ body.dark-mode .search-box input:focus-visible {
display: flex;
flex-direction: column;
gap: 10px;
- margin: 20px 0;
+ margin: 24px 0;
}
.category-item {
display: flex;
align-items: center;
- gap: 12px;
- padding: 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- transition: all 0.2s ease;
+ gap: 14px;
+ padding: 16px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.category-item:hover {
- border-color: var(--text-secondary);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+ background: var(--accent);
+ transform: translateY(-2px);
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
}
.category-icon {
- width: 32px;
- height: 32px;
- border-radius: 8px;
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
- color: white;
flex-shrink: 0;
+ color: white;
}
.pr-icon {
- background: #0969da;
+ background: #22C55E;
}
.issues-icon {
- background: #d1242f;
+ background: #F59E0B;
}
.releases-icon {
- background: #8250df;
+ background: #3B82F6;
+}
+
+body.dark-mode .pr-icon {
+ background: #16A34A;
+}
+
+body.dark-mode .issues-icon {
+ background: #D97706;
+}
+
+body.dark-mode .releases-icon {
+ background: #2563EB;
}
.category-info {
@@ -1791,13 +1880,13 @@ body.dark-mode .search-box input:focus-visible {
.category-info h3 {
font-size: 14px;
font-weight: 600;
- color: var(--text-primary);
+ color: var(--foreground);
margin: 0 0 2px 0;
}
.category-info p {
font-size: 12px;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
margin: 0;
}
@@ -1817,10 +1906,12 @@ body.dark-mode .search-box input:focus-visible {
}
.toggle-label > span:first-child {
- font-size: 12px;
- color: var(--text-secondary);
- font-weight: 500;
+ font-size: 11px;
+ color: var(--muted-foreground);
+ font-weight: 600;
min-width: 38px;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.toggle-checkbox {
@@ -1831,35 +1922,36 @@ body.dark-mode .search-box input:focus-visible {
pointer-events: none;
}
+/* shadcn Switch component */
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
- background-color: var(--border-color);
- border-radius: 20px;
- transition: all 0.2s ease;
+ background-color: var(--input);
+ border-radius: 9999px;
+ transition: all 150ms;
}
.toggle-switch:before {
content: "";
position: absolute;
- height: 14px;
- width: 14px;
- left: 3px;
- bottom: 3px;
- background-color: white;
- border-radius: 50%;
- transition: transform 0.2s ease;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+ height: 16px;
+ width: 16px;
+ left: 2px;
+ bottom: 2px;
+ background-color: var(--background);
+ border-radius: 9999px;
+ transition: transform 150ms;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.toggle-checkbox:checked + .toggle-switch {
- background-color: #10b981;
+ background-color: var(--primary);
}
body.dark-mode .toggle-checkbox:checked + .toggle-switch {
- background-color: #059669;
+ background-color: var(--primary);
}
.toggle-checkbox:checked + .toggle-switch:before {
@@ -1867,40 +1959,37 @@ body.dark-mode .toggle-checkbox:checked + .toggle-switch {
}
.toggle-checkbox:focus + .toggle-switch {
- box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.toggle-label:hover .toggle-switch {
- background-color: var(--text-secondary);
opacity: 0.8;
}
.toggle-label:hover .toggle-checkbox:checked + .toggle-switch {
- background-color: #0ea472;
- opacity: 1;
+ opacity: 0.9;
}
-/* Disabled state for toggle labels */
.toggle-label.disabled {
- opacity: 0.4;
+ opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.toggle-label.disabled > span:first-child {
- color: var(--text-secondary);
+ color: var(--muted-foreground);
}
.toggle-checkbox:disabled + .toggle-switch {
- opacity: 0.4;
+ opacity: 0.5;
cursor: not-allowed;
}
.toggle-checkbox:disabled + .toggle-switch:before {
- opacity: 0.6;
+ opacity: 0.5;
}
-/* Activity Toggle - Matching Settings Page Styles */
+/* Activity Toggle */
.activity-toggle {
position: relative;
display: inline-block;
@@ -1924,22 +2013,22 @@ body.dark-mode .toggle-checkbox:checked + .toggle-switch {
left: 0;
right: 0;
bottom: 0;
- background-color: var(--border-color);
- transition: all 0.3s ease;
- border-radius: 24px;
+ background-color: var(--input);
+ transition: all 150ms;
+ border-radius: 9999px;
}
.toggle-slider:before {
position: absolute;
content: "";
- height: 18px;
- width: 18px;
- left: 3px;
- bottom: 3px;
- background-color: white;
- transition: transform 0.3s ease;
- border-radius: 50%;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+ height: 20px;
+ width: 20px;
+ left: 2px;
+ bottom: 2px;
+ background-color: var(--background);
+ transition: transform 150ms;
+ border-radius: 9999px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.activity-toggle input:focus {
@@ -1951,77 +2040,107 @@ body.dark-mode .toggle-checkbox:checked + .toggle-switch {
}
.activity-toggle input:checked + .toggle-slider {
- background-color: #10b981;
+ background-color: var(--primary);
}
body.dark-mode .activity-toggle input:checked + .toggle-slider {
- background-color: #059669;
+ background-color: var(--primary);
}
.activity-toggle input:focus + .toggle-slider {
- box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.activity-toggle input:checked + .toggle-slider:before {
transform: translateX(20px);
}
-/* Notification Toggle Specific Styling */
+/* Notification Toggle */
.notification-toggle .toggle-slider {
- background-color: var(--border-color);
+ background-color: var(--input);
}
.notification-toggle input:checked + .toggle-slider {
- background-color: #3b82f6;
+ background-color: var(--primary);
}
body.dark-mode .notification-toggle input:checked + .toggle-slider {
- background-color: #2563eb;
+ background-color: var(--primary);
}
.notification-toggle input:focus + .toggle-slider {
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.step-note {
font-size: 12px;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
font-style: italic;
margin-top: 16px;
}
/* Complete Step */
+.complete-step .step-icon svg {
+ width: 56px;
+ height: 56px;
+ color: var(--foreground);
+ stroke-width: 1.5;
+}
+
.next-steps {
- margin: 16px 0;
+ margin: 24px 0;
text-align: left;
}
.next-steps h3 {
- margin-bottom: 16px;
- color: var(--text-primary);
- font-size: 15px;
+ margin-bottom: 14px;
+ 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;
}
.tips-grid {
display: grid;
grid-template-columns: 1fr;
- gap: 12px;
+ gap: 8px;
}
.tip-item {
display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 12px;
- background: var(--bg-secondary);
- border-radius: 8px;
- border: 1px solid var(--border-color);
+ align-items: center;
+ gap: 14px;
+ padding: 14px 16px;
+ background: rgba(255, 255, 255, 0.04);
+ border-radius: 18px;
+ border: 1px solid var(--border);
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.tip-item:hover {
+ background: var(--accent);
+ transform: translateY(-2px);
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
}
.tip-icon {
flex-shrink: 0;
- color: var(--link-color);
- margin-top: 2px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ color: var(--accent-color);
+}
+
+.tip-icon svg {
+ width: 14px;
+ height: 14px;
}
.tip-content {
@@ -2030,29 +2149,42 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.tip-content strong {
display: block;
- color: var(--text-primary);
+ color: var(--foreground);
font-size: 13px;
- margin-bottom: 4px;
+ margin-bottom: 2px;
+ font-weight: 600;
}
.tip-content p {
margin: 0;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
font-size: 12px;
line-height: 1.4;
}
.final-tips {
- margin-top: 16px;
- padding: 12px;
- background: var(--bg-secondary);
- border-radius: 6px;
- border: 1px solid var(--border-color);
+ margin-top: 20px;
+ padding: 14px 16px;
+ background: rgba(255, 255, 255, 0.04);
+ border-radius: 18px;
+ border: 1px solid var(--border);
}
.final-tips p {
- margin: 0;
font-size: 13px;
+ color: var(--muted-foreground);
+ margin: 0;
+ line-height: 1.5;
+}
+
+.final-tips strong {
+ color: var(--foreground);
+ font-weight: 600;
+}
+
+.final-tips p {
+ margin: 0;
+ font-size: 12px;
}
/* Navigation */
@@ -2060,8 +2192,9 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
display: flex;
justify-content: space-between;
align-items: center;
- padding-top: 12px;
- border-top: 1px solid var(--border-color);
+ padding-top: 16px;
+ margin-top: auto;
+ flex-shrink: 0;
}
.nav-center {
@@ -2069,51 +2202,55 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
text-align: center;
}
+/* Button variants */
.onboarding-btn {
- padding: 8px 16px;
- border-radius: 6px;
- font-size: 14px;
- font-weight: 500;
+ padding: 10px 20px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 600;
cursor: pointer;
- transition: all 0.2s;
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
+ text-transform: uppercase;
}
.onboarding-btn:focus {
- outline: 2px solid var(--link-color);
- outline-offset: 2px;
+ outline: none;
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
}
.onboarding-btn.primary {
- background: var(--link-color);
- color: white;
- border-color: var(--link-color);
+ background: var(--primary);
+ color: var(--primary-foreground);
+ border-color: var(--primary);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.onboarding-btn.primary:hover {
- background: #0256cc;
+ opacity: 0.9;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.onboarding-btn:disabled {
- background: var(--text-secondary) !important;
- color: white !important;
- border-color: var(--text-secondary) !important;
+ opacity: 0.5 !important;
cursor: not-allowed !important;
- opacity: 0.6 !important;
}
.onboarding-btn:disabled:hover {
- background: var(--text-secondary) !important;
+ opacity: 0.5 !important;
}
.onboarding-btn.secondary {
- background: var(--bg-secondary);
- color: var(--text-primary);
- border-color: var(--border-color);
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--foreground);
+ border-color: var(--input);
}
.onboarding-btn.secondary:hover {
- background: var(--bg-hover);
+ background: var(--accent);
+ color: var(--accent-foreground);
}
.onboarding-btn.secondary#skipBtn {
@@ -2123,31 +2260,32 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
.onboarding-btn.link {
background: none;
- color: var(--text-secondary);
+ color: var(--muted-foreground);
border: none;
- text-decoration: none;
+ text-decoration: underline;
+ text-underline-offset: 4px;
font-size: 12px;
}
.onboarding-btn.link:hover {
- color: var(--link-color);
- text-decoration: underline;
+ color: var(--foreground);
}
-/* Dark mode adjustments */
+/* Dark mode button overrides */
body.dark-mode .validate-btn,
body.dark-mode .add-btn,
body.dark-mode .add-repo-btn {
- background: var(--link-color);
+ background: var(--primary);
+ color: var(--primary-foreground);
}
body.dark-mode .validate-btn:hover,
body.dark-mode .add-btn:hover,
body.dark-mode .add-repo-btn:hover {
- background: #1f6feb;
+ opacity: 0.9;
}
-/* Responsive adjustments */
+/* Responsive */
@media (max-width: 400px) {
.onboarding-view {
padding: 15px;
diff --git a/popup/popup.html b/popup/popup.html
index 20eb913..41d0a59 100644
--- a/popup/popup.html
+++ b/popup/popup.html
@@ -4,6 +4,8 @@
GitHub Devwatch
+
+
@@ -16,9 +18,8 @@ GitHub Devwatch
Watching 0 repos
@@ -33,13 +34,13 @@
GitHub Devwatch