|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ToastManager {
|
|
|
constructor() {
|
|
|
this.toasts = [];
|
|
|
this.container = null;
|
|
|
this.maxToasts = 5;
|
|
|
this.defaultDuration = 5000;
|
|
|
this.init();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
|
if (!document.getElementById('toast-container')) {
|
|
|
this.container = document.createElement('div');
|
|
|
this.container.id = 'toast-container';
|
|
|
this.container.className = 'toast-container';
|
|
|
this.container.setAttribute('role', 'region');
|
|
|
this.container.setAttribute('aria-label', 'Notifications');
|
|
|
this.container.setAttribute('aria-live', 'polite');
|
|
|
document.body.appendChild(this.container);
|
|
|
} else {
|
|
|
this.container = document.getElementById('toast-container');
|
|
|
}
|
|
|
|
|
|
console.log('[Toast] Toast manager initialized');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
show(message, type = 'info', options = {}) {
|
|
|
const {
|
|
|
duration = this.defaultDuration,
|
|
|
title = null,
|
|
|
icon = null,
|
|
|
dismissible = true,
|
|
|
action = null
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
if (this.toasts.length >= this.maxToasts) {
|
|
|
this.dismiss(this.toasts[0].id);
|
|
|
}
|
|
|
|
|
|
const toast = {
|
|
|
id: this.generateId(),
|
|
|
message,
|
|
|
type,
|
|
|
title,
|
|
|
icon: icon || this.getDefaultIcon(type),
|
|
|
dismissible,
|
|
|
action,
|
|
|
duration,
|
|
|
createdAt: Date.now()
|
|
|
};
|
|
|
|
|
|
this.toasts.push(toast);
|
|
|
this.render(toast);
|
|
|
|
|
|
|
|
|
if (duration > 0) {
|
|
|
setTimeout(() => this.dismiss(toast.id), duration);
|
|
|
}
|
|
|
|
|
|
return toast.id;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
success(message, options = {}) {
|
|
|
return this.show(message, 'success', options);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
error(message, options = {}) {
|
|
|
return this.show(message, 'error', { ...options, duration: options.duration || 7000 });
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
warning(message, options = {}) {
|
|
|
return this.show(message, 'warning', options);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
info(message, options = {}) {
|
|
|
return this.show(message, 'info', options);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dismiss(toastId) {
|
|
|
const toastElement = document.getElementById(`toast-${toastId}`);
|
|
|
if (!toastElement) return;
|
|
|
|
|
|
|
|
|
toastElement.classList.add('toast-exit');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
toastElement.remove();
|
|
|
this.toasts = this.toasts.filter(t => t.id !== toastId);
|
|
|
}, 300);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dismissAll() {
|
|
|
const toastIds = this.toasts.map(t => t.id);
|
|
|
toastIds.forEach(id => this.dismiss(id));
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
render(toast) {
|
|
|
const toastElement = document.createElement('div');
|
|
|
toastElement.id = `toast-${toast.id}`;
|
|
|
toastElement.className = `toast toast-${toast.type} glass-effect`;
|
|
|
toastElement.setAttribute('role', 'alert');
|
|
|
toastElement.setAttribute('aria-atomic', 'true');
|
|
|
|
|
|
const iconHtml = window.getIcon
|
|
|
? window.getIcon(toast.icon, 24)
|
|
|
: '';
|
|
|
|
|
|
const titleHtml = toast.title
|
|
|
? `<div class="toast-title">${toast.title}</div>`
|
|
|
: '';
|
|
|
|
|
|
const actionHtml = toast.action
|
|
|
? `<button class="toast-action" onclick="${toast.action.onClick}">${toast.action.label}</button>`
|
|
|
: '';
|
|
|
|
|
|
const closeButton = toast.dismissible
|
|
|
? `<button class="toast-close" onclick="window.toastManager.dismiss('${toast.id}')" aria-label="Close notification">
|
|
|
${window.getIcon ? window.getIcon('close', 20) : '×'}
|
|
|
</button>`
|
|
|
: '';
|
|
|
|
|
|
const progressBar = toast.duration > 0
|
|
|
? `<div class="toast-progress" style="animation-duration: ${toast.duration}ms"></div>`
|
|
|
: '';
|
|
|
|
|
|
toastElement.innerHTML = `
|
|
|
<div class="toast-icon">
|
|
|
${iconHtml}
|
|
|
</div>
|
|
|
<div class="toast-content">
|
|
|
${titleHtml}
|
|
|
<div class="toast-message">${toast.message}</div>
|
|
|
${actionHtml}
|
|
|
</div>
|
|
|
${closeButton}
|
|
|
${progressBar}
|
|
|
`;
|
|
|
|
|
|
this.container.appendChild(toastElement);
|
|
|
|
|
|
|
|
|
setTimeout(() => toastElement.classList.add('toast-enter'), 10);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getDefaultIcon(type) {
|
|
|
const icons = {
|
|
|
success: 'checkCircle',
|
|
|
error: 'alertCircle',
|
|
|
warning: 'alertCircle',
|
|
|
info: 'info'
|
|
|
};
|
|
|
return icons[type] || 'info';
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generateId() {
|
|
|
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showProviderError(providerName, error) {
|
|
|
return this.error(
|
|
|
`Failed to connect to ${providerName}`,
|
|
|
{
|
|
|
title: 'Provider Error',
|
|
|
duration: 7000,
|
|
|
action: {
|
|
|
label: 'Retry',
|
|
|
onClick: `window.providerDiscovery.checkProviderHealth('${providerName}')`
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showProviderSuccess(providerName) {
|
|
|
return this.success(
|
|
|
`Successfully connected to ${providerName}`,
|
|
|
{
|
|
|
title: 'Provider Online',
|
|
|
duration: 3000
|
|
|
}
|
|
|
);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showRateLimitWarning(providerName, retryAfter) {
|
|
|
return this.warning(
|
|
|
`Rate limit reached for ${providerName}. Retry after ${retryAfter}s`,
|
|
|
{
|
|
|
title: 'Rate Limit',
|
|
|
duration: 6000
|
|
|
}
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
window.toastManager = new ToastManager();
|
|
|
|
|
|
|
|
|
window.showToast = (message, type, options) => window.toastManager.show(message, type, options);
|
|
|
window.toast = {
|
|
|
success: (msg, opts) => window.toastManager.success(msg, opts),
|
|
|
error: (msg, opts) => window.toastManager.error(msg, opts),
|
|
|
warning: (msg, opts) => window.toastManager.warning(msg, opts),
|
|
|
info: (msg, opts) => window.toastManager.info(msg, opts)
|
|
|
};
|
|
|
|
|
|
console.log('[Toast] Toast notification system ready');
|
|
|
|