// ── api.js — fetch wrapper, toast, clipboard ───────────── import { getNavSignal } from './nav-signal.js'; export async function api(path, opts) { opts = opts || {}; const signal = opts.signal !== undefined ? opts.signal : getNavSignal(); let resp; try { resp = await fetch('/api' + path, { signal }); } catch (err) { // Route changed or caller aborted — swallow silently, no toast. if (err && err.name === 'AbortError') throw err; showToast('Network error: ' + (err && err.message ? err.message : 'fetch failed'), 'error'); throw err; } if (!resp.ok) { const body = await resp.json().catch(() => ({})); const msg = body.error || 'Request failed (' + resp.status + ')'; showToast(msg, 'error'); throw new Error(msg); } return resp.json(); } export function showToast(message, type) { // Limit to 3 stacked toasts const existing = document.querySelectorAll('.toast'); if (existing.length >= 3) existing[0].remove(); const toast = document.createElement('div'); toast.className = 'toast toast-' + (type || 'info'); toast.textContent = message; document.body.appendChild(toast); // Stack: offset each toast by its position const stackToasts = () => { document.querySelectorAll('.toast').forEach((t, i) => { t.style.bottom = (2 + i * 3.5) + 'rem'; }); }; stackToasts(); requestAnimationFrame(() => toast.classList.add('visible')); setTimeout(() => { toast.classList.remove('visible'); setTimeout(() => { toast.remove(); stackToasts(); }, 300); }, 4000); } export function copyToClipboard(text, el) { if (!text) return; navigator.clipboard.writeText(text).then(() => { showToast('Copied to clipboard', 'success'); if (el) { const originalText = el.textContent; el.textContent = 'Copied!'; setTimeout(() => { el.textContent = originalText; }, 1500); } }).catch(err => { console.error('Failed to copy:', err); showToast('Copy failed', 'error'); }); }