Files
dees-catalog/ts_web/elements/dees-toast.ts
Juergen Kunz 987f557c60 Enhance DeesToast component with new features and improved demo
- Updated README to reflect new toast positions and convenience methods.
- Expanded demo functionality to showcase various toast types, positions, and durations.
- Added programmatic control for toast dismissal and multiple toast notifications.
- Introduced new toast positions: top-center and bottom-center.
- Implemented a progress bar for auto-dismiss functionality.
- Improved styling and animations for better user experience.
2025-06-12 09:33:46 +00:00

325 lines
9.0 KiB
TypeScript

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<ToastPosition, HTMLDivElement>();
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`<svg viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd"/>
</svg>`,
success: html`<svg viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>`,
warning: html`<svg viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>`,
error: html`<svg viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>`
};
return html`
<div class="toast" @click=${this.dismiss}>
<div class="icon">
${icons[this.type]}
</div>
<div class="message">${this.message}</div>
<div class="close">
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
</div>
${this.duration > 0 ? html`
<div class="progress">
<div class="progress-bar" style="animation-duration: ${this.duration}ms"></div>
</div>
` : ''}
</div>
`;
}
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);
}
}