/** * admin-ui/js/components/ds-toast.js * A single toast notification component with swipe-to-dismiss support. */ class DsToast extends HTMLElement { static get observedAttributes() { return ['type', 'duration']; } constructor() { super(); this.attachShadow({ mode: 'open' }); this._duration = 5000; this._dismissTimer = null; } connectedCallback() { this.render(); this.setupAutoDismiss(); this.setupSwipeToDismiss(); this.shadowRoot.querySelector('.close-button')?.addEventListener('click', () => this.dismiss()); } disconnectedCallback() { if (this._dismissTimer) { clearTimeout(this._dismissTimer); } } attributeChangedCallback(name, oldValue, newValue) { if (name === 'duration') { this._duration = parseInt(newValue, 10); } } setupAutoDismiss() { if (this._duration > 0 && !this.hasAttribute('progress')) { this._dismissTimer = setTimeout(() => this.dismiss(), this._duration); } } dismiss() { if (this._dismissTimer) { clearTimeout(this._dismissTimer); } this.classList.add('dismissing'); this.addEventListener('animationend', () => { this.dispatchEvent(new CustomEvent('dismiss', { bubbles: true, composed: true })); this.remove(); }, { once: true }); } setupSwipeToDismiss() { let startX = 0; let currentX = 0; let isDragging = false; this.addEventListener('pointerdown', (e) => { isDragging = true; startX = e.clientX; currentX = startX; this.style.transition = 'none'; this.setPointerCapture(e.pointerId); }); this.addEventListener('pointermove', (e) => { if (!isDragging) return; currentX = e.clientX; const diff = currentX - startX; this.style.transform = `translateX(${diff}px)`; }); const onPointerUp = (e) => { if (!isDragging) return; isDragging = false; this.style.transition = 'transform 0.2s ease'; const diff = currentX - startX; const threshold = this.offsetWidth * 0.3; if (Math.abs(diff) > threshold) { this.style.transform = `translateX(${diff > 0 ? '100%' : '-100%'})`; this.dismiss(); } else { this.style.transform = 'translateX(0)'; } }; this.addEventListener('pointerup', onPointerUp); this.addEventListener('pointercancel', onPointerUp); } render() { const type = this.getAttribute('type') || 'info'; const dismissible = this.hasAttribute('dismissible'); this.shadowRoot.innerHTML = `
${dismissible ? `` : ''} `; } } customElements.define('ds-toast', DsToast);