242 lines
9.8 KiB
JavaScript
242 lines
9.8 KiB
JavaScript
;(() => {
|
|
// ---- 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 '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')
|
|
? `<div class="mt-3 h-1.5 w-full rounded-full bg-white/5 overflow-hidden">
|
|
<div class="h-full w-1/2 bg-cyan-400/50 tg-progress"></div>
|
|
</div>` : '';
|
|
|
|
const closeBtn = (o.close !== false)
|
|
? `<button class="rounded-lg bg-white/10 hover:bg-white/15 border border-white/15 p-1.5 ml-2 flex-shrink-0"
|
|
style="box-shadow:0 0 10px rgba(0,0,0,.25)" data-tg-close>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
<path d="M8 8l8 8M16 8l-8 8" stroke="rgba(229,231,235,.9)" stroke-linecap="round"/>
|
|
</svg>
|
|
</button>`
|
|
: '<div style="width:28px;height:28px"></div>';
|
|
|
|
return `
|
|
<div class="glass-card border border-glass-border/70 rounded-2xl p-4 shadow-[0_10px_30px_rgba(0,0,0,.35)] pointer-events-auto tg-card">
|
|
<div class="flex items-center justify-between gap-4">
|
|
|
|
<!-- Linke Seite -->
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
${o.badge ? `<span class="badge !bg-glass-light/50 !border-glass-border whitespace-nowrap">${String(o.badge).toUpperCase()}</span>` : ''}
|
|
${o.domain ? `<div class="text-base font-semibold text-gray-100/95 tracking-tight truncate">${o.domain}</div>` : ''}
|
|
</div>
|
|
|
|
<!-- Rechte Seite -->
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
<span class="text-[13px] text-gray-300/80">Status</span>
|
|
<span class="px-2.5 py-1 rounded-lg text-[12px] leading-none whitespace-nowrap ${st.pill}">
|
|
<i class="${st.icon} text-[14px] align-[-1px]"></i>
|
|
<span class="ml-1">${st.text}</span>
|
|
</span>
|
|
${closeBtn}
|
|
</div>
|
|
</div>
|
|
|
|
${o.message ? `<p class="mt-3 text-[15px] text-gray-200/90 leading-relaxed">${o.message}</p>` : ''}
|
|
|
|
${progress}
|
|
|
|
${o.finalNote ? `<p class="mt-3 text-[12px] text-gray-400">${o.finalNote}</p>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
function remove(wrapper) {
|
|
if (!wrapper) return;
|
|
const card = wrapper.firstElementChild;
|
|
if (!card) return wrapper.remove();
|
|
card.classList.add('tg-out');
|
|
card.addEventListener('animationend', () => {
|
|
wrapper.remove();
|
|
}, { once: true });
|
|
}
|
|
|
|
// ---- 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 };
|
|
})();
|