mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
Email filter: custom dropdown with SVG icons for each option
The All/Unread/Favorites/etc selector was a native <select>, which can't render SVG inside <option>. Replace it with a custom picker that: - Keeps the existing <select id="email-lib-filter"> as the value store (hidden via display:none). All existing 'change' listeners keep working — the picker just dispatches a change event after updating the select's value. - Renders a styled button + drop-out menu built from the select's options (preserves optgroup labels like 'Tags'). - Each option carries an SVG icon: lines for All, ringed dot for Unread, star for Favorites, empty checkbox for Undone, bell for Reminders, reply arrow for Unanswered/Reply-soon, clock for Pending, calendar-x for Stale, exclamation-triangle for Urgent, ban for Spam, newsletter and megaphone for the marketing tags. - Icons use var(--accent) so they pick up the user's theme color. - Click outside / Esc closes the menu (Esc handler is capture-phase + stopPropagation so it doesn't bubble to the modal-close listener and shut the whole email window). CSS scoped under .email-filter-picker.
This commit is contained in:
+114
-1
@@ -819,7 +819,10 @@ export function openEmailLibrary(opts = {}) {
|
||||
<select class="memory-sort-select" id="email-lib-folder" style="flex:1;min-width:0;text-overflow:ellipsis;">
|
||||
<option value="INBOX">Inbox</option>
|
||||
</select>
|
||||
<select class="memory-sort-select" id="email-lib-filter" style="flex:1;min-width:0;">
|
||||
<!-- Hidden native select kept as the source of truth — all
|
||||
existing change handlers still fire via the custom picker
|
||||
dispatching 'change' on it. -->
|
||||
<select class="memory-sort-select" id="email-lib-filter" style="display:none;">
|
||||
<option value="all">All</option>
|
||||
<option value="unread">Unread</option>
|
||||
<option value="favorites">Favorites</option>
|
||||
@@ -836,6 +839,13 @@ export function openEmailLibrary(opts = {}) {
|
||||
<option value="tag:marketing">Marketing</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<div class="email-filter-picker" id="email-filter-picker" style="flex:1;min-width:0;position:relative;">
|
||||
<button type="button" class="email-filter-btn" id="email-filter-btn" aria-haspopup="listbox" aria-expanded="false">
|
||||
<span class="email-filter-current"><span class="email-filter-icon"></span><span class="email-filter-label">All</span></span>
|
||||
<svg class="email-filter-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="email-filter-menu" id="email-filter-menu" role="listbox" hidden></div>
|
||||
</div>
|
||||
<button class="memory-toolbar-btn email-filter-select-btn" id="email-lib-select-btn">Select</button>
|
||||
<button class="memory-toolbar-btn email-filter-refresh-btn" id="email-lib-refresh-btn" title="Refresh">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;"><path d="M1 4v6h6"/><path d="M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>
|
||||
@@ -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 <select> so each row can carry an SVG icon. The hidden
|
||||
// <select id="email-lib-filter"> 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': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>',
|
||||
'unread': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>',
|
||||
'favorites': '<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
|
||||
'undone': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="3"/></svg>',
|
||||
'reminders': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>',
|
||||
'unanswered': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>',
|
||||
'pending_30d': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
|
||||
'stale_30d': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="10" y1="14" x2="14" y2="18"/><line x1="14" y1="14" x2="10" y2="18"/></svg>',
|
||||
'tag:urgent': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
'tag:reply-soon':'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/><circle cx="18" cy="6" r="2" fill="currentColor" stroke="none"/></svg>',
|
||||
'tag:spam': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>',
|
||||
'tag:newsletter':'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6z"/></svg>',
|
||||
'tag:marketing': '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg>',
|
||||
};
|
||||
|
||||
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 <select> 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 `<div class="email-filter-group">${it.group}</div>`;
|
||||
}
|
||||
return `<button type="button" role="option" class="email-filter-item" data-value="${it.value}">
|
||||
<span class="email-filter-item-icon">${_filterIcon(it.value)}</span>
|
||||
<span class="email-filter-item-label">${it.label}</span>
|
||||
</button>`;
|
||||
}).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 = '';
|
||||
|
||||
@@ -11046,6 +11046,130 @@ textarea.memory-add-input {
|
||||
border-color: var(--red);
|
||||
}
|
||||
|
||||
/* Custom email filter picker (All / Unread / Favorites / …). Replaces the
|
||||
native <select> so options can carry SVG icons. The hidden source
|
||||
<select id="email-lib-filter"> 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;
|
||||
|
||||
Reference in New Issue
Block a user