192 lines
6.5 KiB
JavaScript
192 lines
6.5 KiB
JavaScript
// Shared alert/notification utility for consistent Tailwind styling and Font Awesome icons
|
|
// Provides: window.alerts.show(message, type?, options?) and compatibility shims
|
|
|
|
(function () {
|
|
const TYPE_ALIASES = {
|
|
error: 'danger',
|
|
success: 'success',
|
|
warning: 'warning',
|
|
info: 'info',
|
|
danger: 'danger'
|
|
};
|
|
|
|
const TYPE_CLASSES = {
|
|
success: {
|
|
container: 'border-success-200 dark:border-success-800',
|
|
icon: 'fa-solid fa-circle-check text-success-600 dark:text-success-400'
|
|
},
|
|
danger: {
|
|
container: 'border-danger-200 dark:border-danger-800',
|
|
icon: 'fa-solid fa-triangle-exclamation text-danger-600 dark:text-danger-400'
|
|
},
|
|
warning: {
|
|
container: 'border-warning-200 dark:border-warning-800',
|
|
icon: 'fa-solid fa-triangle-exclamation text-warning-600 dark:text-warning-400'
|
|
},
|
|
info: {
|
|
container: 'border-info-200 dark:border-info-800',
|
|
icon: 'fa-solid fa-circle-info text-info-600 dark:text-info-400'
|
|
}
|
|
};
|
|
|
|
function normalizeType(type) {
|
|
const key = String(type || 'info').toLowerCase();
|
|
return TYPE_ALIASES[key] || 'info';
|
|
}
|
|
|
|
function getOrCreateContainer(containerId = 'notification-container') {
|
|
let container = document.getElementById(containerId);
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = containerId;
|
|
container.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2 p-0';
|
|
document.body.appendChild(container);
|
|
}
|
|
return container;
|
|
}
|
|
|
|
function show(message, type = 'info', options = {}) {
|
|
const tone = normalizeType(type);
|
|
const {
|
|
duration = 5000,
|
|
dismissible = true,
|
|
containerId = 'notification-container',
|
|
role = 'alert',
|
|
ariaLive = 'polite',
|
|
html = false,
|
|
title = null,
|
|
actions = [],
|
|
onClose = null,
|
|
id = null
|
|
} = options;
|
|
|
|
const container = getOrCreateContainer(containerId);
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = `alert-notification max-w-sm w-[22rem] bg-white dark:bg-neutral-800 border rounded-lg shadow-lg p-4 transition-all duration-300 translate-x-4 opacity-0 ${
|
|
(TYPE_CLASSES[tone] || TYPE_CLASSES.info).container
|
|
}`;
|
|
wrapper.setAttribute('role', role);
|
|
wrapper.setAttribute('aria-live', ariaLive);
|
|
if (id) wrapper.id = id;
|
|
|
|
const inner = document.createElement('div');
|
|
inner.className = 'flex items-start';
|
|
|
|
const iconWrap = document.createElement('div');
|
|
iconWrap.className = 'flex-shrink-0';
|
|
const icon = document.createElement('i');
|
|
icon.className = (TYPE_CLASSES[tone] || TYPE_CLASSES.info).icon;
|
|
iconWrap.appendChild(icon);
|
|
|
|
const content = document.createElement('div');
|
|
content.className = 'ml-3 flex-1';
|
|
|
|
if (title) {
|
|
const titleEl = document.createElement('p');
|
|
titleEl.className = 'text-sm font-semibold text-neutral-900 dark:text-neutral-100';
|
|
titleEl.textContent = String(title);
|
|
content.appendChild(titleEl);
|
|
}
|
|
|
|
const text = document.createElement('div');
|
|
text.className = 'text-xs mt-1 text-neutral-800 dark:text-neutral-200';
|
|
if (message instanceof Node) {
|
|
text.appendChild(message);
|
|
} else if (html) {
|
|
text.innerHTML = String(message || '');
|
|
} else {
|
|
text.textContent = String(message || '');
|
|
}
|
|
content.appendChild(text);
|
|
|
|
inner.appendChild(iconWrap);
|
|
inner.appendChild(content);
|
|
|
|
if (dismissible) {
|
|
const closeWrap = document.createElement('div');
|
|
closeWrap.className = 'ml-4 flex-shrink-0';
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.setAttribute('aria-label', 'Close');
|
|
closeBtn.className = 'text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors';
|
|
closeBtn.addEventListener('click', () => {
|
|
wrapper.remove();
|
|
if (typeof onClose === 'function') onClose();
|
|
});
|
|
const x = document.createElement('i');
|
|
x.className = 'fa-solid fa-xmark';
|
|
closeBtn.appendChild(x);
|
|
closeWrap.appendChild(closeBtn);
|
|
inner.appendChild(closeWrap);
|
|
}
|
|
|
|
// Actions (buttons)
|
|
if (Array.isArray(actions) && actions.length > 0) {
|
|
const actionsWrap = document.createElement('div');
|
|
actionsWrap.className = 'mt-2 flex gap-2 flex-wrap';
|
|
|
|
actions.forEach((action) => {
|
|
if (!action || !action.label) return;
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.textContent = String(action.label);
|
|
if (action.ariaLabel) btn.setAttribute('aria-label', action.ariaLabel);
|
|
btn.className = action.classes || 'px-3 py-1 rounded text-xs transition-colors bg-neutral-200 hover:bg-neutral-300 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-200';
|
|
btn.addEventListener('click', (ev) => {
|
|
try {
|
|
if (typeof action.onClick === 'function') {
|
|
action.onClick({ event: ev, wrapper });
|
|
}
|
|
} finally {
|
|
if (action.autoClose !== false) {
|
|
wrapper.remove();
|
|
if (typeof onClose === 'function') onClose();
|
|
}
|
|
}
|
|
});
|
|
actionsWrap.appendChild(btn);
|
|
});
|
|
|
|
content.appendChild(actionsWrap);
|
|
}
|
|
|
|
wrapper.appendChild(inner);
|
|
container.appendChild(wrapper);
|
|
|
|
// Animate in
|
|
requestAnimationFrame(() => {
|
|
wrapper.classList.remove('translate-x-4', 'opacity-0');
|
|
});
|
|
|
|
if (duration > 0) {
|
|
setTimeout(() => {
|
|
wrapper.classList.add('translate-x-4', 'opacity-0');
|
|
setTimeout(() => {
|
|
wrapper.remove();
|
|
if (typeof onClose === 'function') onClose();
|
|
}, 250);
|
|
}, duration);
|
|
}
|
|
|
|
return wrapper;
|
|
}
|
|
|
|
const alerts = {
|
|
show,
|
|
success: (message, options = {}) => show(message, 'success', options),
|
|
error: (message, options = {}) => show(message, 'danger', options),
|
|
warning: (message, options = {}) => show(message, 'warning', options),
|
|
info: (message, options = {}) => show(message, 'info', options),
|
|
getOrCreateContainer
|
|
};
|
|
|
|
// Expose globally
|
|
window.alerts = alerts;
|
|
// Backward-compatible shims
|
|
window.showAlert = (message, type = 'info', duration = 5000) => alerts.show(message, type, { duration });
|
|
window.showNotification = (message, type = 'info', duration = 5000) => alerts.show(message, type, { duration });
|
|
window.showToast = (message, type = 'info', duration = 3000) => alerts.show(message, type, { duration });
|
|
})();
|
|
|
|
|