import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { demoFunc } from './dees-toast.demo.js'; declare global { interface HTMLElementTagNameMap { 'dees-toast': DeesToast; } } export type ToastType = 'info' | 'success' | 'warning' | 'error'; export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'; export interface IToastOptions { message: string; type?: ToastType; duration?: number; position?: ToastPosition; } @customElement('dees-toast') export class DeesToast extends DeesElement { // STATIC public static demo = demoFunc; private static toastContainers = new Map(); private static getOrCreateContainer(position: ToastPosition): HTMLDivElement { if (!this.toastContainers.has(position)) { const container = document.createElement('div'); container.className = `toast-container toast-container-${position}`; container.style.cssText = ` position: fixed; z-index: 10000; pointer-events: none; padding: 16px; display: flex; flex-direction: column; gap: 8px; `; // Position the container switch (position) { case 'top-right': container.style.top = '0'; container.style.right = '0'; break; case 'top-left': container.style.top = '0'; container.style.left = '0'; break; case 'bottom-right': container.style.bottom = '0'; container.style.right = '0'; break; case 'bottom-left': container.style.bottom = '0'; container.style.left = '0'; break; case 'top-center': container.style.top = '0'; container.style.left = '50%'; container.style.transform = 'translateX(-50%)'; break; case 'bottom-center': container.style.bottom = '0'; container.style.left = '50%'; container.style.transform = 'translateX(-50%)'; break; } document.body.appendChild(container); this.toastContainers.set(position, container); } return this.toastContainers.get(position)!; } public static async show(options: IToastOptions | string) { const opts: IToastOptions = typeof options === 'string' ? { message: options } : options; const toast = new DeesToast(); toast.message = opts.message; toast.type = opts.type || 'info'; toast.duration = opts.duration || 3000; const container = this.getOrCreateContainer(opts.position || 'top-right'); container.appendChild(toast); // Trigger animation await toast.updateComplete; requestAnimationFrame(() => { toast.isVisible = true; }); // Auto dismiss if (toast.duration > 0) { setTimeout(() => { toast.dismiss(); }, toast.duration); } return toast; } // Convenience methods public static info(message: string, duration?: number) { return this.show({ message, type: 'info', duration }); } public static success(message: string, duration?: number) { return this.show({ message, type: 'success', duration }); } public static warning(message: string, duration?: number) { return this.show({ message, type: 'warning', duration }); } public static error(message: string, duration?: number) { return this.show({ message, type: 'error', duration }); } // INSTANCE @property({ type: String }) public message: string = ''; @property({ type: String }) public type: ToastType = 'info'; @property({ type: Number }) public duration: number = 3000; @property({ type: Boolean, reflect: true }) public isVisible: boolean = false; constructor() { super(); domtools.elementBasic.setup(); } public static styles = [ cssManager.defaultStyles, css` :host { display: block; pointer-events: auto; font-family: 'Geist Sans', sans-serif; opacity: 0; transform: translateY(-10px); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } :host([isvisible]) { opacity: 1; transform: translateY(0); } .toast { display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-radius: 8px; background: ${cssManager.bdTheme('#fff', '#222')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; min-width: 300px; max-width: 500px; cursor: pointer; } .toast:hover { transform: scale(1.02); } .icon { flex-shrink: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; } .icon svg { width: 100%; height: 100%; } .message { flex: 1; font-size: 14px; line-height: 1.5; color: ${cssManager.bdTheme('#333', '#fff')}; } .close { flex-shrink: 0; width: 16px; height: 16px; opacity: 0.5; cursor: pointer; transition: opacity 0.2s; } .close:hover { opacity: 1; } .close svg { width: 100%; height: 100%; fill: currentColor; } /* Type-specific styles */ :host([type="info"]) .icon { color: #0084ff; } :host([type="success"]) .icon { color: #22c55e; } :host([type="warning"]) .icon { color: #f59e0b; } :host([type="error"]) .icon { color: #ef4444; } /* Progress bar */ .progress { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: currentColor; opacity: 0.2; border-radius: 0 0 8px 8px; overflow: hidden; } .progress-bar { height: 100%; background: currentColor; opacity: 0.8; transform-origin: left; animation: progress linear forwards; } @keyframes progress { from { transform: scaleX(1); } to { transform: scaleX(0); } } ` ]; public render(): TemplateResult { const icons = { info: html` `, success: html` `, warning: html` `, error: html` ` }; return html`
${icons[this.type]}
${this.message}
${this.duration > 0 ? html`
` : ''}
`; } public async dismiss() { this.isVisible = false; await new Promise(resolve => setTimeout(resolve, 300)); this.remove(); // Clean up empty containers const container = this.parentElement; if (container && container.children.length === 0) { container.remove(); for (const [position, cont] of DeesToast.toastContainers.entries()) { if (cont === container) { DeesToast.toastContainers.delete(position); break; } } } } public firstUpdated() { // Set the type attribute for CSS this.setAttribute('type', this.type); } }