// API layer — fetch wrapper, toast, 401 redirect window.RC_API = (() => { let _on401 = null; function setOn401(fn) { _on401 = fn; } async function api(url, method = 'GET', body = undefined) { const opts = { method, credentials: 'include', headers: {}, }; if (method === 'GET' || method === 'HEAD') { opts.cache = 'no-store'; } if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); } const res = await fetch(url, opts); if (res.status === 401) { if (_on401) _on401(); return null; } if (!res.ok) { let msg = `Error ${res.status}`; try { const d = await res.json(); msg = d.detail || d.message || msg; } catch {} throw new Error(msg); } const ct = res.headers.get('content-type') || ''; if (ct.includes('application/json')) return res.json(); return null; } return { api, setOn401 }; })(); window.api = window.RC_API.api; // ── Toast system ────────────────────────────────────────────────────────────── window.RC_Toast = (() => { let container = null; function _isMobile() { return /mobile\.html$/.test(location.pathname) || (matchMedia('(pointer:coarse)').matches && innerWidth < 900); } function _ensureContainer() { if (container) return; container = document.createElement('div'); container.id = 'rc-toast-container'; const base = { position: 'fixed', zIndex: '9999', display: 'flex', flexDirection: 'column', gap: '8px', pointerEvents: 'none', }; if (_isMobile()) { // Centrado, por encima del tabbar (bottom:0 + ~64px) y respetando el home indicator. Object.assign(container.style, base, { left: '16px', right: '16px', bottom: 'calc(env(safe-area-inset-bottom, 0px) + 88px)', alignItems: 'center', }); } else { Object.assign(container.style, base, { bottom: '24px', right: '24px' }); } document.body.appendChild(container); } function showToast(msg, type = 'info') { _ensureContainer(); const el = document.createElement('div'); const colors = { info: '#3cffd0', error: '#ff4c4c', success: '#3cffd0' }; const textColors = { info: '#131313', error: '#fff', success: '#131313' }; Object.assign(el.style, { background: colors[type] || colors.info, color: textColors[type] || textColors.info, fontFamily: "'Space Grotesk', sans-serif", fontSize: '14px', fontWeight: '500', padding: '10px 16px', borderRadius: '8px', boxShadow: '0 4px 16px rgba(0,0,0,0.4)', pointerEvents: 'auto', opacity: '1', transition: 'opacity 0.3s', maxWidth: _isMobile() ? 'calc(100vw - 32px)' : '360px', width: _isMobile() ? '100%' : 'auto', boxSizing: 'border-box', }); el.textContent = msg; container.appendChild(el); setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, 3500); } return { showToast }; })(); window.showToast = window.RC_Toast.showToast; // ── Confirm modal (imperativo, funciona en desktop y móvil) ─────────────── window.RC_Confirm = (() => { let _overlay = null; function showConfirm(msg, onYes) { if (_overlay) closeConfirm(); _overlay = document.createElement('div'); Object.assign(_overlay.style, { position: 'fixed', inset: '0', zIndex: '10000', background: 'rgba(0,0,0,0.72)', display: 'flex', alignItems: 'center', justifyContent: 'center', }); const box = document.createElement('div'); const isMob = _isMobile(); Object.assign(box.style, { background: '#1c1c1c', border: '1px solid #333', borderRadius: isMob ? '20px 20px 0 0' : '16px', padding: isMob ? '24px 24px calc(max(20px, env(safe-area-inset-bottom, 0px)) + 12px)' : '24px 28px', maxWidth: isMob ? '100%' : '360px', width: isMob ? '100%' : '90%', fontFamily: "var(--f-sans, 'Space Grotesk', sans-serif)", color: '#e8e8e8', boxSizing: 'border-box', }); if (isMob) { Object.assign(_overlay.style, { alignItems: 'flex-end' }); } const text = document.createElement('p'); text.textContent = msg; Object.assign(text.style, { margin: '0 0 20px', fontSize: '15px', lineHeight: '1.5' }); const actions = document.createElement('div'); Object.assign(actions.style, { display: 'flex', gap: '10px' }); const btnBase = { minHeight: '44px', borderRadius: '20px', cursor: 'pointer', fontSize: '14px', fontWeight: '600', flex: '1', }; const btnNo = document.createElement('button'); btnNo.textContent = 'Cancelar'; Object.assign(btnNo.style, btnBase, { padding: '10px 18px', border: '1px solid #555', background: 'transparent', color: '#aaa', }); btnNo.onclick = () => closeConfirm(); const btnYes = document.createElement('button'); btnYes.textContent = 'Confirmar'; Object.assign(btnYes.style, btnBase, { padding: '10px 18px', border: 'none', background: '#3cffd0', color: '#0a0a0a', }); btnYes.onclick = onYes; actions.appendChild(btnNo); actions.appendChild(btnYes); box.appendChild(text); box.appendChild(actions); _overlay.appendChild(box); _overlay._onKey = (e) => { if (e.key === 'Escape') closeConfirm(); }; document.addEventListener('keydown', _overlay._onKey); document.body.appendChild(_overlay); } function closeConfirm() { if (!_overlay) return; document.removeEventListener('keydown', _overlay._onKey); _overlay.remove(); _overlay = null; } return { showConfirm, closeConfirm }; })(); window.showConfirm = window.RC_Confirm.showConfirm; window.closeConfirm = window.RC_Confirm.closeConfirm;