diff --git a/config.php b/config.php
index 8b10e1b..a073478 100644
--- a/config.php
+++ b/config.php
@@ -1,7 +1,7 @@
+
+
+
+
+
+
+
diff --git a/public/js/modules/search.js b/public/js/modules/search.js
index b9b4c3e..b27ef2f 100644
--- a/public/js/modules/search.js
+++ b/public/js/modules/search.js
@@ -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 = 'no saved searches
';
+ return;
+ }
+ savedList.innerHTML = items.map((q, i) => `
+
+
+ ${esc(q)}
+
+
+ `).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();
+});
diff --git a/public/plugins/tests/search.test.js b/public/plugins/tests/search.test.js
index 6c4c211..41c1796 100644
--- a/public/plugins/tests/search.test.js
+++ b/public/plugins/tests/search.test.js
@@ -44,3 +44,125 @@ describe('Search', () => {
expect($('#search')).toBeHidden();
});
});
+
+describe('Search: saved searches', () => {
+ const SAVED_KEY = 'hyprcrd-saved-searches';
+
+ afterEach(async () => {
+ // Close overlays and restore localStorage to avoid cross-test pollution.
+ document.querySelector('#search-saved-popup')?.classList.add('hidden');
+ await closeAll();
+ });
+
+ it('bookmark button and popup exist', () => {
+ expect($('#search-saved-btn')).toExist();
+ expect($('#search-saved-popup')).toExist();
+ expect($('#search-saved-popup').classList.contains('hidden')).toBe(true);
+ });
+
+ it('clicking bookmark opens the popup', async () => {
+ click('#search-btn');
+ await sleep(100);
+ click('#search-saved-btn');
+ await sleep(40);
+ expect($('#search-saved-popup').classList.contains('hidden')).toBe(false);
+ });
+
+ it('saves and recalls a search', async () => {
+ // Seed localStorage directly to avoid UI coupling for the save path.
+ const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
+ const probe = '__test_saved_search_probe__';
+ try {
+ localStorage.setItem(SAVED_KEY, JSON.stringify([probe, ...before]));
+
+ click('#search-btn');
+ await sleep(100);
+ click('#search-saved-btn');
+ await sleep(60);
+
+ const popup = $('#search-saved-popup');
+ const rows = popup.querySelectorAll('.search-saved-row');
+ expect(rows.length).toBeGreaterThan(0);
+ // First row should be our probe.
+ expect(rows[0].textContent).toContain(probe);
+
+ // Click the row → fills input.
+ click(rows[0]);
+ await sleep(40);
+ expect($('#search-input').value).toBe(probe);
+ } finally {
+ // Restore original saved searches.
+ localStorage.setItem(SAVED_KEY, JSON.stringify(before));
+ }
+ });
+
+ it('deletes a saved search', async () => {
+ const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
+ const probe = '__test_saved_del__';
+ try {
+ localStorage.setItem(SAVED_KEY, JSON.stringify([probe]));
+
+ click('#search-btn');
+ await sleep(100);
+ click('#search-saved-btn');
+ await sleep(60);
+
+ const delBtn = $('#search-saved-popup').querySelector('.search-saved-del');
+ expect(delBtn).toExist();
+ click(delBtn);
+ await sleep(40);
+
+ const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
+ expect(after.includes(probe)).toBe(false);
+ } finally {
+ localStorage.setItem(SAVED_KEY, JSON.stringify(before));
+ }
+ });
+
+ it('save-current button adds input value', async () => {
+ const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
+ const probe = '__test_saved_add_' + Date.now() + '__';
+ try {
+ click('#search-btn');
+ await sleep(100);
+ const inp = $('#search-input');
+ inp.value = probe;
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
+
+ click('#search-saved-btn');
+ await sleep(40);
+ click('#search-saved-add');
+ await sleep(40);
+
+ const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
+ expect(after.includes(probe)).toBe(true);
+ } finally {
+ localStorage.setItem(SAVED_KEY, JSON.stringify(before));
+ }
+ });
+
+ it('does not add duplicates', async () => {
+ const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
+ const probe = '__test_saved_dupe__';
+ try {
+ localStorage.setItem(SAVED_KEY, JSON.stringify([probe]));
+
+ click('#search-btn');
+ await sleep(100);
+ const inp = $('#search-input');
+ inp.value = probe;
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
+
+ click('#search-saved-btn');
+ await sleep(40);
+ click('#search-saved-add');
+ await sleep(40);
+
+ const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
+ const count = after.filter(q => q === probe).length;
+ expect(count).toBe(1);
+ } finally {
+ localStorage.setItem(SAVED_KEY, JSON.stringify(before));
+ }
+ });
+});