// resources/js/plugins/GlassToastra/toastra.glass.js (() => { // ---- Config -------------------------------------------------------------- const MAX_PER_POSITION = 3; // wie viele Toasts pro Ecke sichtbar const ROOT_PREFIX = 'toastra-root-'; // pro Position eigener Container const POS_STYLES = { 'top-left' : ['top:1.5rem','left:1.5rem'], 'top-center' : ['top:1.5rem','left:50%','transform:translateX(-50%)'], 'top-right' : ['top:1.5rem','right:1.5rem'], 'bottom-left' : ['bottom:1.5rem','left:1.5rem'], 'bottom-center': ['bottom:1.5rem','left:50%','transform:translateX(-50%)'], 'bottom-right' : ['bottom:1.5rem','right:1.5rem'], }; // ---- One-time style injection ------------------------------------------- let styleInjected = false; function injectStyleOnce() { if (styleInjected) return; styleInjected = true; const css = ` /* Mount/Unmount Animations */ .tg-card { opacity: 0; transform: translateY(6px); will-change: transform, opacity; } .tg-in { animation: tgIn .28s ease-out forwards; } .tg-out { animation: tgOut .28s ease-in forwards; } @keyframes tgIn { from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none} } @keyframes tgOut { from{opacity:1; transform:none} to{opacity:0; transform:translateY(6px)} } /* kleine Progress-Move-Anim */ @keyframes tgProgress { 0% { transform: translateX(-60%) } 50%{ transform: translateX(10%) } 100%{ transform: translateX(120%) } } /* Badge im Title (Fallback, falls du kein Tailwind-Badge willst) */ .tg-badge { display:inline-flex; align-items:center; gap:.25rem; padding:.25rem .5rem; border-radius:.5rem; font-size:.7rem; font-weight:700; letter-spacing:.5px; background: rgba(255,255,255,.08); color:#cbd5e1; border:1px solid rgba(255,255,255,.12); } /* Close-Hitbox */ .tg-close { display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:.6rem; background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.12); box-shadow: 0 0 10px rgba(0,0,0,.25); transition: background .15s ease; } .tg-close:hover { background: rgba(255,255,255,.12) } .tg-close svg path { stroke: rgba(229,231,235,.9) } /* Stack-Abstand */ [data-toastra-stack] > div { margin-bottom: .75rem } `; const el = document.createElement('style'); el.textContent = css; document.head.appendChild(el); } // ---- Helpers ------------------------------------------------------------- function ensureRoot(position) { injectStyleOnce(); const pos = POS_STYLES[position] ? position : 'bottom-right'; const id = ROOT_PREFIX + pos; let root = document.getElementById(id); if (!root) { root = document.createElement('div'); root.id = id; root.setAttribute('data-toastra-stack', pos); root.style.position = 'fixed'; root.style.zIndex = '99999'; root.style.pointerEvents = 'none'; root.style.width = 'clamp(260px, 92vw, 480px)'; document.body.appendChild(root); } root.style.cssText = [ 'position:fixed','z-index:99999','pointer-events:none','width:clamp(260px,92vw,480px)' ].concat(POS_STYLES[pos]).join(';'); return root; } // function statusMap(state) { // switch (state) { // case 'done': // return { text: 'Erledigt', pill: 'bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30', icon: 'ph ph-check-circle' }; // case 'failed': // return { text: 'Fehlgeschlagen', pill: 'bg-rose-500/15 text-rose-300 ring-1 ring-rose-500/30', icon: 'ph ph-x-circle' }; // case 'forbidden': // return { text: 'Verboten', pill: 'bg-fuchsia-500/15 text-fuchsia-300 ring-1 ring-fuchsia-500/30', icon: 'ph ph-shield-slash' }; // case 'running': // return { text: 'Läuft…', pill: 'bg-cyan-500/15 text-cyan-300 ring-1 ring-cyan-500/30', icon: 'ph ph-arrow-clockwise animate-spin' }; // default: // return { text: 'Wartet…', pill: 'bg-amber-500/15 text-amber-300 ring-1 ring-amber-500/30', icon: 'ph ph-pause-circle' }; // } // } function statusMap(state) { switch (state) { case 'done': return { text: 'Erledigt', pill: 'bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30', icon: 'ph ph-check-circle' }; case 'failed': return { text: 'Fehlgeschlagen', pill: 'bg-rose-500/15 text-rose-300 ring-1 ring-rose-500/30', icon: 'ph ph-x-circle' }; case 'forbidden': return { text: 'Verboten', pill: 'bg-fuchsia-500/15 text-fuchsia-300 ring-1 ring-fuchsia-500/30', icon: 'ph ph-shield-slash' }; case 'running': return { text: 'Läuft…', pill: 'bg-cyan-500/15 text-cyan-300 ring-1 ring-cyan-500/30', icon: 'ph ph-arrow-clockwise animate-spin' }; default: return { text: 'Wartet…', pill: 'bg-amber-500/15 text-amber-300 ring-1 ring-amber-500/30', icon: 'ph ph-pause-circle' }; } } function buildHTML(o) { const st = statusMap(o.state); const progress = (o.state === 'running' || o.state === 'queued') ? `
` : ''; const closeBtn = (o.close !== false) ? `` : '
'; return `
${o.badge ? ` ${String(o.badge).toUpperCase()} ` : ''} ${o.domain ? `
${o.domain}
` : ''}
Status ${st.text} ${closeBtn}
${o.message ? `

${o.message}

` : ''} ${progress} ${o.finalNote ? `

${o.finalNote}

` : ''}
`; } // function buildHTML(o){ // const st = statusMap(o.state); // const progress = (o.state === 'running' || o.state === 'queued') // ? `
//
//
` : ''; // // const closeBtn = (o.close !== false) // ? `` // : '
'; // // return ` //
//
// //
// ${o.badge ? `${String(o.badge).toUpperCase()}` : ''} // ${o.domain ? `
${o.domain}
` : ''} //
// // //
// Status // // // ${st.text} // // ${closeBtn} //
//
// // ${o.message ? `

${o.message}

` : ''} // // ${progress} // // ${o.finalNote ? `

${o.finalNote}

` : ''} //
// `; // // return ` // //
// //
// // // // // //
// // ${o.badge ? `${String(o.badge).toUpperCase()}` : ''} // // ${o.domain ? `
${o.domain}
` : ''} // //
// // // // // //
// // Status // // // // // // ${st.text} // // // // ${closeBtn} // //
// //
// // // // ${o.message ? `

${o.message}

` : ''} // // // // ${progress} // // // // ${o.finalNote ? `

${o.finalNote}

` : ''} // //
`; // } function remove(wrapper) { if (!wrapper) return; // wir animieren das eigentliche Card-Element (erstes Kind) const card = wrapper.firstElementChild || wrapper; // Wenn schon im Ausblenden, doppelt nicht starten if (card.classList.contains('tg-out')) return; // Ausblend-Animation starten card.classList.remove('tg-in'); card.classList.add('tg-out'); // Sicher entfernen, wenn die Animation fertig ist let cleaned = false; const cleanup = () => { if (cleaned) return; cleaned = true; // kompletten Wrapper aus dem DOM wrapper.remove(); }; // Normalfall: nach Animation card.addEventListener('animationend', cleanup, { once: true }); // Fallback: falls animationend aus irgendeinem Grund nicht feuert setTimeout(cleanup, 600); // > .28s; gib etwas Puffer } // ---- Public API ---------------------------------------------------------- function show(options) { const o = Object.assign({ id: 'toast-' + Date.now(), // gleicher id => ersetzt state: 'queued', // queued|running|done|failed badge: null, domain: '', message: '', finalNote: '', position: 'bottom-right', duration: 0, // 0 => stehenlassen; bei done/failed wird (falls 0) 6000ms genommen close: true, }, options || {}); const root = ensureRoot(o.position); // Stack-Limit pro Position const current = Array.from(root.querySelectorAll('[data-tg-id]')); if (!current.find(n => n.dataset.tgId === o.id) && current.length >= MAX_PER_POSITION) { remove(current[ current.length - 1 ]); // ältesten unten entfernen } const wrapper = document.createElement('div'); wrapper.setAttribute('data-tg-id', o.id); wrapper.innerHTML = buildHTML(o); const prev = root.querySelector(`[data-tg-id="${o.id}"]`); if (prev) { prev.replaceWith(wrapper); } else { root.prepend(wrapper); } // Progress anim const prog = wrapper.querySelector('.tg-progress'); if (prog) { prog.style.animation = 'tgProgress 1.6s ease-in-out infinite'; } // Close (Maus & Keyboard) const btn = wrapper.querySelector('[data-tg-close]'); if (btn) { btn.addEventListener('click', () => remove(wrapper)); btn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); remove(wrapper); } }); btn.tabIndex = 0; } // Mount anim requestAnimationFrame(() => wrapper.firstElementChild.classList.add('tg-in')); // Auto-Close const autoMs = (o.state === 'done' || o.state === 'failed') ? Number(o.duration || 6000) : Number(o.duration || 0); if (autoMs > 0) setTimeout(() => remove(wrapper), autoMs); return o.id; } function update(id, options) { // Einfach erneut show mit gleicher id => ersetzt in-place if (!id) return; const root = document.body.querySelector(`[id^="${ROOT_PREFIX}"]`); // irgendeine Ecke const found = root && document.querySelector(`[data-tg-id="${id}"]`); const currentPos = found ? found.parentElement.getAttribute('data-toastra-stack') : 'bottom-right'; show(Object.assign({ id, position: currentPos }, options || {})); } function dismiss(id) { const el = id ? document.querySelector(`[data-tg-id="${id}"]`) : null; if (el) remove(el); } function clear(position) { const root = position ? document.getElementById(ROOT_PREFIX + position) : null; (root ? Array.from(root.children) : Array.from(document.querySelectorAll(`[id^="${ROOT_PREFIX}"] > div`))) .forEach(remove); } window.toastraGlass = { show, update, dismiss, clear }; })();