diff --git a/css/style.css b/css/style.css index 9f2ce5f..a08a59c 100644 --- a/css/style.css +++ b/css/style.css @@ -25,6 +25,9 @@ body { .toast { padding:0.7rem 1.2rem; border-radius:0.8rem; font-size:0.85rem; animation:slideIn 0.3s ease; color:#fff; } .toast.error { background:var(--danger); } .toast.success { background:var(--success); } +.toast { display:flex; align-items:center; gap:0.6rem; } +.toast-action { background:rgba(255,255,255,0.25); border:none; color:#fff; font-weight:700; font-size:0.75rem; padding:0.25rem 0.6rem; border-radius:1rem; cursor:pointer; white-space:nowrap; transition:0.15s; } +.toast-action:hover { background:rgba(255,255,255,0.4); } @keyframes slideIn { from { transform:translateX(100%); opacity:0; } to { transform:translateX(0); opacity:1; } } .vault { diff --git a/js/app.js b/js/app.js index 2571cba..a8f1353 100644 --- a/js/app.js +++ b/js/app.js @@ -108,7 +108,7 @@ function toggleSound() { } // ==================== TOAST ==================== -function toast(m, t) { t = t || 'success'; const c = document.getElementById('toastContainer'); const d = document.createElement('div'); d.className = 'toast ' + t; d.textContent = m; c.appendChild(d); setTimeout(() => d.remove(), 3000); } +function toast(m, t, action) { t = t || 'success'; const c = document.getElementById('toastContainer'); const d = document.createElement('div'); d.className = 'toast ' + t; d.innerHTML = '' + m + ''; if (action) { const btn = document.createElement('button'); btn.className = 'toast-action'; btn.textContent = action.label; btn.onclick = function(e) { e.stopPropagation(); action.cb(); d.remove(); }; d.appendChild(btn); c.appendChild(d); setTimeout(() => { if (!d.isConnected) return; d.remove(); if (action.onExpire) action.onExpire(); }, 6000); } else { c.appendChild(d); setTimeout(() => d.remove(), 3000); } } function showZigzagToast(elem, msg, type) { const t = document.createElement('div'); t.className = 'toast-zigzag ' + (type || 'success'); @@ -993,7 +993,7 @@ function showBatchConfirm(btn, message, callback) { document.addEventListener('keydown', keyHandler); } function attachEvents() { - document.querySelectorAll('.delete-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); if (showTrash) { permanentDelete(this.dataset.id); } else { showConfirm(this, 'Delete this entry?', id => delEntry(id)); } }); + document.querySelectorAll('.delete-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); const id = parseInt(this.dataset.id); if (showTrash) { permanentDelete(id); } else { showConfirm(this, 'Delete this entry?', async id2 => { await delEntry(id2); await loadEntries(); toast('📦 Moved to trash', 'success', { label: '↩ Undo', cb: async () => { await restoreEntry(id2, true); await loadEntries(); toast('↩ Restored'); playSound('success'); }, onExpire: null }); playSound('delete'); }); } }); document.querySelectorAll('.edit-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); openEdit(this.dataset.id); }); document.querySelectorAll('.star-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); toggleFavorite(this.dataset.id); }); document.querySelectorAll('.copy-p').forEach(b => b.onclick = async function(ev) { ev.stopPropagation(); const pw = entryPw(this.dataset.id); try { await navigator.clipboard.writeText(pw); this.textContent = '✓'; const btn = this; setTimeout(() => { btn.textContent = '📋'; }, 1000); showZigzagToast(this, '📋 Copied!', 'success'); playSound('copy'); } catch (e) { showZigzagToast(this, 'Failed', 'error'); } }); @@ -1140,13 +1140,18 @@ function hideBatchBar() { async function batchDelete() { const count = selectedIds.size; + const ids = [...selectedIds]; const btn = document.querySelector('#batchBar .btn-danger'); showBatchConfirm(btn || document.body, 'Move ' + count + ' entries to trash?', async () => { - for (const id of selectedIds) await delEntry(id, true); - toast('📦 Moved ' + count + ' entries to trash'); - playSound('delete'); + for (const id of ids) await delEntry(id, true); clearSelection(); await loadEntries(); + toast('📦 Moved ' + count + ' entries to trash', 'success', { + label: '↩ Undo', + cb: async () => { for (const id of ids) await restoreEntry(id, true); await loadEntries(); toast('↩ Restored ' + ids.length + ' entries'); playSound('success'); }, + onExpire: null + }); + playSound('delete'); }); }