// Tiny API client: JSON + CSRF double-submit header + error envelope. export function csrfToken() { const m = document.cookie.match(/(?:^|;\s*)bb_csrf=([^;]+)/); return m ? decodeURIComponent(m[1]) : ''; } export class ApiError extends Error { constructor(status, code, message) { super(message); this.status = status; this.code = code; } } export async function api(method, path, body) { const opts = { method, headers: {} }; if (method !== 'GET' && method !== 'HEAD') { opts.headers['X-CSRF-Token'] = csrfToken(); } if (body !== undefined) { if (body instanceof FormData) { opts.body = body; } else { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); } } const resp = await fetch(path, opts); if (resp.status === 204) return null; let data = null; try { data = await resp.json(); } catch (e) { /* non-JSON */ } if (!resp.ok) { const err = (data && data.error) || {}; throw new ApiError(resp.status, err.code || 'unknown', err.message || resp.statusText); } return data; } export function toast(message, kind) { let host = document.getElementById('toasts'); if (!host) { host = document.createElement('div'); host.id = 'toasts'; document.body.appendChild(host); } const t = document.createElement('div'); t.className = 'toast' + (kind ? ' ' + kind : ''); t.setAttribute('role', 'status'); t.textContent = message; host.appendChild(t); setTimeout(() => t.remove(), 6000); }