diff --git a/static/js/emailLibrary.js b/static/js/emailLibrary.js index f29f798dd..1ed8f598e 100644 --- a/static/js/emailLibrary.js +++ b/static/js/emailLibrary.js @@ -819,7 +819,10 @@ export function openEmailLibrary(opts = {}) { Inbox - + + All Unread Favorites @@ -836,6 +839,13 @@ export function openEmailLibrary(opts = {}) { Marketing + + + All + + + + Select @@ -990,7 +1000,10 @@ export function openEmailLibrary(opts = {}) { // Sync quick-toggle active states so they mirror the dropdown. document.getElementById('email-undone-btn')?.classList.toggle('active', state._libFilter === 'undone'); document.getElementById('email-reminder-btn')?.classList.toggle('active', state._libFilter === 'reminders'); + // Mirror the picker label/icon. + _renderFilterPickerCurrent(); }); + _initFilterPicker(); document.getElementById('email-attach-btn')?.addEventListener('click', () => { const btn = document.getElementById('email-attach-btn'); state._libHasAttachments = !state._libHasAttachments; @@ -1721,6 +1734,106 @@ async function _doSearch() { } } +// Custom dropdown for the email filter (All/Unread/Favorites/...). Replaces +// the native so each row can carry an SVG icon. The hidden +// stays as the value source — clicking a +// menu item updates its value and dispatches 'change', so every existing +// listener keeps working. +const _EMAIL_FILTER_ICONS = { + 'all': '', + 'unread': '', + 'favorites': '', + 'undone': '', + 'reminders': '', + 'unanswered': '', + 'pending_30d': '', + 'stale_30d': '', + 'tag:urgent': '', + 'tag:reply-soon':'', + 'tag:spam': '', + 'tag:newsletter':'', + 'tag:marketing': '', +}; + +function _filterIcon(value) { + return _EMAIL_FILTER_ICONS[value] || _EMAIL_FILTER_ICONS['all']; +} + +function _renderFilterPickerCurrent() { + const sel = document.getElementById('email-lib-filter'); + const btn = document.getElementById('email-filter-btn'); + if (!sel || !btn) return; + const value = sel.value || 'all'; + const opt = sel.querySelector(`option[value="${CSS.escape(value)}"]`); + const label = opt ? opt.textContent : value; + const iconWrap = btn.querySelector('.email-filter-icon'); + const labelEl = btn.querySelector('.email-filter-label'); + if (iconWrap) iconWrap.innerHTML = _filterIcon(value); + if (labelEl) labelEl.textContent = label; +} + +function _initFilterPicker() { + const sel = document.getElementById('email-lib-filter'); + const picker = document.getElementById('email-filter-picker'); + const btn = document.getElementById('email-filter-btn'); + const menu = document.getElementById('email-filter-menu'); + if (!sel || !picker || !btn || !menu || picker._wired) return; + picker._wired = true; + + // Build menu from the hidden contents (preserves optgroup labels). + const items = []; + for (const child of sel.children) { + if (child.tagName === 'OPTGROUP') { + items.push({ group: child.label }); + for (const o of child.children) { + items.push({ value: o.value, label: o.textContent, group: child.label }); + } + } else if (child.tagName === 'OPTION') { + items.push({ value: child.value, label: child.textContent }); + } + } + menu.innerHTML = items.map(it => { + if (!it.value) { + return `${it.group}`; + } + return ` + ${_filterIcon(it.value)} + ${it.label} + `; + }).join(''); + + const close = () => { + menu.hidden = true; + btn.setAttribute('aria-expanded', 'false'); + }; + const open = () => { + menu.hidden = false; + btn.setAttribute('aria-expanded', 'true'); + }; + btn.addEventListener('click', (e) => { + e.stopPropagation(); + if (menu.hidden) open(); else close(); + }); + menu.addEventListener('click', (e) => { + const item = e.target.closest('.email-filter-item'); + if (!item) return; + sel.value = item.dataset.value; + sel.dispatchEvent(new Event('change', { bubbles: true })); + close(); + }); + document.addEventListener('click', (e) => { + if (!menu.hidden && !picker.contains(e.target)) close(); + }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && !menu.hidden) { + e.stopPropagation(); + close(); + } + }, { capture: true }); + + _renderFilterPickerCurrent(); +} + function _renderEmailLoading(grid) { if (!grid) return null; grid.innerHTML = ''; diff --git a/static/style.css b/static/style.css index 35dc9c8ff..97b23ebaf 100644 --- a/static/style.css +++ b/static/style.css @@ -11046,6 +11046,130 @@ textarea.memory-add-input { border-color: var(--red); } +/* Custom email filter picker (All / Unread / Favorites / …). Replaces the + native so options can carry SVG icons. The hidden source + is the value store and dispatches + 'change' on selection. */ +.email-filter-picker { + position: relative; + top: 3px; +} +.email-filter-btn { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 6px; + font-family: inherit; + font-size: 11px; + height: 24px; + padding: 0 8px 0 8px; + cursor: pointer; + text-align: left; + transition: border-color 0.12s, background 0.12s; +} +.email-filter-btn:hover { border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border)); } +.email-filter-btn:focus-visible { + outline: none; + border-color: var(--accent, var(--red)); +} +.email-filter-current { + flex: 1; + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.email-filter-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 13px; + height: 13px; + color: var(--accent, var(--red)); + flex-shrink: 0; +} +.email-filter-icon svg { width: 13px; height: 13px; } +.email-filter-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.email-filter-caret { + flex-shrink: 0; + opacity: 0.6; + transition: transform 0.15s; +} +.email-filter-picker .email-filter-btn[aria-expanded="true"] .email-filter-caret { + transform: rotate(180deg); +} + +.email-filter-menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + z-index: 100; + max-height: 320px; + overflow-y: auto; + background: var(--panel, var(--bg)); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 6px 20px rgba(0,0,0,0.22); + padding: 4px; + display: flex; + flex-direction: column; + gap: 1px; +} +.email-filter-menu[hidden] { display: none; } +.email-filter-group { + font-size: 9px; + font-weight: 600; + letter-spacing: 0.6px; + text-transform: uppercase; + opacity: 0.55; + padding: 8px 9px 3px; +} +.email-filter-item { + all: unset; + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 6px 9px; + border-radius: 5px; + font-size: 12px; + cursor: pointer; + color: var(--fg); + box-sizing: border-box; +} +.email-filter-item:hover, +.email-filter-item:focus-visible { + background: color-mix(in srgb, var(--fg) 8%, transparent); +} +.email-filter-item-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + color: var(--accent, var(--red)); + flex-shrink: 0; +} +.email-filter-item-icon svg { width: 13px; height: 13px; } +.email-filter-item-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* Item metadata row */ .memory-item-meta { display: flex;