dptxa-proxy / src /lib /txa /txatoast.js
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
8.56 kB
/**
* TXAToast - Liquid Glass Toast Notification System
* Hỗ trợ đa vị trí, bypass CSP qua Trusted Types & SVG Inline
*/
class TXAToastSystem {
constructor() {
this.containers = {};
this.policy = null;
this.initPolicy();
this.injectStyles();
}
initPolicy() {
if (typeof window !== 'undefined' && window.trustedTypes) {
try {
this.policy = window.trustedTypes.createPolicy('txatoast-policy', {
createHTML: (string) => string
});
} catch (e) {
this.policy = window.trustedTypes.defaultPolicy || { createHTML: (string) => string };
}
}
}
getContainer(position = 'top-right') {
if (typeof document === 'undefined') return null;
const id = `txatoast-container-${position}`;
let container = document.getElementById(id);
if (!container) {
container = document.createElement('div');
container.id = id;
const posStyles = {
'top-right': 'top: 24px; right: 24px;',
'top-left': 'top: 24px; left: 24px;',
'bottom-right': 'bottom: 24px; right: 24px;',
'bottom-left': 'bottom: 24px; left: 24px;',
'top-center': 'top: 24px; left: 50%; transform: translateX(-50%); align-items: center;',
'bottom-center': 'bottom: 24px; left: 50%; transform: translateX(-50%); align-items: center;'
};
container.style.cssText = `
position: fixed;
z-index: 2147483647;
display: flex;
flex-direction: column;
gap: 14px;
pointer-events: none;
max-width: 380px;
width: 100%;
${posStyles[position] || posStyles['top-right']}
`;
document.body.appendChild(container);
}
return container;
}
injectStyles() {
if (typeof document === 'undefined' || document.getElementById('txatoast-styles')) return;
const style = document.createElement('style');
style.id = 'txatoast-styles';
style.textContent = `
.txa-toast {
pointer-events: auto;
position: relative;
padding: 16px 22px;
border-radius: 24px;
color: #ffffff;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.01em;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
overflow: hidden;
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5), inset 0 1px 2px rgba(255, 255, 255, 0.2);
opacity: 0;
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* Position-based Animations */
[id*="-right"] .txa-toast { transform: translateX(120%) scale(0.9); }
[id*="-left"] .txa-toast { transform: translateX(-120%) scale(0.9); }
[id*="-center"] .txa-toast { transform: translateY(50px) scale(0.9); }
[id*="top-center"] .txa-toast { transform: translateY(-50px) scale(0.9); }
.txa-toast.txa-show { transform: translate(0) scale(1); opacity: 1; }
.txa-toast.txa-hide { opacity: 0; scale: 0.85; }
.txa-toast::before {
content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.12) 0%, transparent 60%); pointer-events: none; mix-blend-mode: overlay;
}
.txa-toast-success { border-color: rgba(16, 185, 129, 0.4); box-shadow: 0 12px 35px rgba(16, 185, 129, 0.2); }
.txa-toast-success .txa-icon { color: #10b981; }
.txa-toast-error { border-color: rgba(239, 68, 68, 0.4); box-shadow: 0 12px 35px rgba(239, 68, 68, 0.2); }
.txa-toast-error .txa-icon { color: #ef4444; }
.txa-toast-info { border-color: rgba(59, 130, 246, 0.4); box-shadow: 0 12px 35px rgba(59, 130, 246, 0.2); }
.txa-toast-info .txa-icon { color: #3b82f6; }
.txa-toast-warning { border-color: rgba(245, 158, 11, 0.4); box-shadow: 0 12px 35px rgba(245, 158, 11, 0.2); }
.txa-toast-warning .txa-icon { color: #f59e0b; }
.txa-content-wrapper { display: flex; align-items: center; gap: 12px; flex-grow: 1; }
.txa-icon { display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.txa-message { line-height: 1.4; text-shadow: 0 1px 2px rgba(0,0,0,0.6); }
.txa-close {
background: transparent; border: none; color: rgba(255, 255, 255, 0.4);
cursor: pointer; padding: 6px; display: flex; align-items: center;
justify-content: center; transition: all 0.2s; border-radius: 50%;
}
.txa-close:hover { color: #ffffff; background: rgba(255,255,255,0.1); transform: rotate(90deg); }
.txa-progress-bar {
position: absolute; bottom: 0; left: 0; height: 3px; width: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent);
transform-origin: left; transform: scaleX(1);
}
`;
document.head.appendChild(style);
}
show(message, type = 'info', duration = 3500, position = 'top-right') {
const container = this.getContainer(position);
if (!container) return;
const svgIcons = {
success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>`,
error: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`,
info: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`,
warning: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`
};
const toast = document.createElement('div');
toast.className = `txa-toast txa-toast-${type}`;
const rawHTML = `
<div class="txa-content-wrapper">
<div class="txa-icon">${svgIcons[type] || svgIcons.info}</div>
<div class="txa-message">${message}</div>
</div>
<button class="txa-close" aria-label="Close">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
<div class="txa-progress-bar"></div>
`;
if (this.policy) {
toast.innerHTML = this.policy.createHTML(rawHTML);
} else {
toast.innerHTML = rawHTML;
}
container.appendChild(toast);
setTimeout(() => { toast.classList.add('txa-show'); }, 10);
const progressBar = toast.querySelector('.txa-progress-bar');
if (progressBar) {
progressBar.style.transition = `transform ${duration}ms linear`;
progressBar.style.transform = 'scaleX(0)';
}
const closeToast = () => {
toast.classList.remove('txa-show');
toast.classList.add('txa-hide');
setTimeout(() => { toast.remove(); }, 500);
};
toast.querySelector('.txa-close').onclick = closeToast;
setTimeout(closeToast, duration);
}
success(msg, duration, pos) { this.show(msg, 'success', duration, pos); }
error(msg, duration, pos) { this.show(msg, 'error', duration, pos); }
info(msg, duration, pos) { this.show(msg, 'info', duration, pos); }
warning(msg, duration, pos) { this.show(msg, 'warning', duration, pos); }
}
const txatoastInstance = new TXAToastSystem();
if (typeof window !== 'undefined') {
window.txatoast = txatoastInstance;
document.addEventListener('astro:page-load', () => {
txatoastInstance.injectStyles();
});
}