search: saved searches via bookmark button

Small bookmark icon in the search header bar opens a popup listing
localStorage-persisted search strings. Clicking an entry fills the
input and runs the search; the x button deletes it; "save current
search" at the bottom saves the active input (no dupes). List
persists across sessions, no server round-trip.

Tests: 6 cases covering button/popup toggle, save + recall, delete,
save-current-button, and duplicate rejection. All snapshot/restore
localStorage in try/finally so they don't leak into user data or
cross-test state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-16 09:09:18 +02:00
1 parent be28080439
commit 883640e20c
5 files changed
+218 -2

No files matched your search

+85
View File
@@ -403,3 +403,88 @@ replaceAllBtn?.addEventListener('click', async () => {
search(input.value);
setTimeout(() => { replaceStatus.style.color = ''; }, 3000);
});
// ── Saved searches ──────────────────────────────────────────────
// localStorage-persisted list of search strings. The bookmark button
// toggles a popup that lets the user pick one (fills input + runs),
// save the current input value, or delete saved entries.
const SAVED_KEY = 'hyprcrd-saved-searches';
const savedBtn = $('search-saved-btn');
const savedPopup = $('search-saved-popup');
const savedList = $('search-saved-list');
const savedAdd = $('search-saved-add');
function loadSaved() {
try { return JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); } catch { return []; }
}
function storeSaved(arr) {
localStorage.setItem(SAVED_KEY, JSON.stringify(arr));
}
function renderSavedList() {
if (!savedList) return;
const items = loadSaved();
if (!items.length) {
savedList.innerHTML = '<div class="px-3 py-3 text-[10px] text-t-dim font-pixel text-center">no saved searches</div>';
return;
}
savedList.innerHTML = items.map((q, i) => `
<div class="flex items-center gap-2 px-3 py-1.5 hover:bg-t-raised cursor-pointer transition-colors search-saved-row" data-idx="${i}">
<i class="ph ph-magnifying-glass text-xs text-t-dim"></i>
<span class="flex-1 text-xs text-t-muted truncate">${esc(q)}</span>
<button class="search-saved-del text-t-dim hover:text-t-danger text-xs px-1" data-idx="${i}" title="remove"><i class="ph ph-x"></i></button>
</div>
`).join('');
savedList.querySelectorAll('.search-saved-row').forEach(row => {
row.addEventListener('click', (e) => {
if (e.target.closest('.search-saved-del')) return;
const q = items[parseInt(row.dataset.idx)];
if (q !== undefined) {
input.value = q;
search(q);
closeSavedPopup();
}
});
});
savedList.querySelectorAll('.search-saved-del').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const idx = parseInt(btn.dataset.idx);
const arr = loadSaved();
arr.splice(idx, 1);
storeSaved(arr);
renderSavedList();
});
});
}
function openSavedPopup() {
renderSavedList();
savedPopup?.classList.remove('hidden');
const close = (e) => {
if (savedPopup?.contains(e.target) || e.target === savedBtn) return;
closeSavedPopup();
document.removeEventListener('mousedown', close, true);
};
setTimeout(() => document.addEventListener('mousedown', close, true), 0);
}
function closeSavedPopup() {
savedPopup?.classList.add('hidden');
}
savedBtn?.addEventListener('click', (e) => {
e.stopPropagation();
if (savedPopup?.classList.contains('hidden')) openSavedPopup();
else closeSavedPopup();
});
savedAdd?.addEventListener('click', () => {
const q = input.value.trim();
if (!q) return;
const arr = loadSaved();
if (arr.includes(q)) return; // no dupes
arr.unshift(q);
storeSaved(arr);
renderSavedList();
});