diff --git a/.gitignore b/.gitignore index 47cf36a..d66b63b 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ npm-debug.log* # Local task list TODO.md + +# Local design/spec docs +docs/superpowers/ diff --git a/README.md b/README.md index e9cc67f..649f6fd 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A browser extension for multi-account Gmail™ notifications. Get a badge counte - **Quick actions** — mark as read, star, archive, move to spam, delete, reply, or open in Gmail™, all without leaving the extension - **Conversation threads** — emails that belong to the same Gmail thread are grouped together in the popup; expand a thread inline to see each message individually, with per-message and thread-level actions (reply, star, mark all read, archive all, spam, delete all, open in Gmail™); the detail view shows a collapsible thread context panel so you can jump to any sibling message; threading can be turned off in Settings if you prefer the flat view - **Bulk selection** — enter selection mode to pick multiple messages (or whole threads) and mark them read, archive, or delete in one go +- **Search** — click the looking-glass in the toolbar to filter the active inbox by sender name, sender email, subject, or snippet; search is per inbox and clears when you switch accounts - **Notification sounds** — optional audio alert on new mail; choose the built-in sound or upload your own (WAV/MP3/OGG, max 500 KB / 5 s) with adjustable volume - **Themes** — light, dark, or auto (follows system preference) - **Privacy controls** — optionally block external images in email previews (including tracking pixels); disabled by default so images load normally diff --git a/src/popup/list.js b/src/popup/list.js index 6499740..feff303 100644 --- a/src/popup/list.js +++ b/src/popup/list.js @@ -1,8 +1,10 @@ import { groupByThread } from './thread-utils.js'; +import { filterMessages } from './search.js'; import { openInGmail, openReply, performAction, performThreadAction } from './actions.js'; import { ICONS, makeIconBtn, makeMarkReadToggleBtn, makeStarBtn, makeSvgIcon } from './icons.js'; import { openDetail } from './detail.js'; import { api, dimmedMessages, els, state } from './state.js'; +import { resetSearch, updateSearchBtn } from './search-ui.js'; import { clearNode, formatRelativeTime, sendMessage, setLoading, showError } from './utils.js'; // Injected by popup.js via initList() to avoid a circular dependency. @@ -475,6 +477,7 @@ export function renderTabs() { tab.addEventListener('click', () => { state.activeAccountId = account.id; state.pageByAccount[account.id] = 0; + resetSearch(); if (state.selectionMode) { state.selectedMessages.clear(); updateBulkBar(); @@ -486,6 +489,7 @@ export function renderTabs() { updateGmailBtn(); updateMarkAllBtn(); updateSelectBtn(); + updateSearchBtn(); }); els.tabs.appendChild(tab); } @@ -586,7 +590,7 @@ export function renderList() { } else { showError(null); } - const messages = account.messages || []; + const messages = filterMessages(account.messages || [], state.searchQuery); const threads = getThreads(messages); const perPage = state.settings?.maxMessagesPerAccount || 20; const totalPages = Math.max(1, Math.ceil(threads.length / perPage)); @@ -597,7 +601,9 @@ export function renderList() { const empty = document.createElement('li'); empty.className = 'empty-state'; const p = document.createElement('p'); - p.textContent = 'No unread messages.'; + p.textContent = state.searchQuery.trim() + ? 'No messages match your search.' + : 'No unread messages.'; empty.appendChild(p); els.list.appendChild(empty); els.pagination.hidden = true; diff --git a/src/popup/popup.css b/src/popup/popup.css index 680e1aa..57dbd90 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -997,6 +997,60 @@ body { cursor: default; } +/* ── Search bar ──────────────────────────────────────── */ +.search-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-bottom: 1px solid var(--border); + background: var(--bg-secondary); + flex-shrink: 0; +} + +.search-bar-icon { + flex: 0 0 auto; + color: var(--text-muted); +} + +.search-input { + flex: 1 1 auto; + min-width: 0; + padding: 5px 8px; + font-size: 13px; + color: var(--text-primary); + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + outline: none; +} + +.search-input:focus { + border-color: var(--accent); +} + +.search-clear-btn { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + font-size: 13px; + line-height: 1; + color: var(--text-muted); + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.search-clear-btn:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + /* ── Dark mode via media query (auto mode) ──────────── */ @media (prefers-color-scheme: dark) { html:not(.theme-light) { diff --git a/src/popup/popup.html b/src/popup/popup.html index 72fd02b..084ed7c 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -20,6 +20,20 @@ Geething