dd-timer/src/presentation/components/Toast.ts
POL Mickaël 3e485fd09b chore: normalise fins de ligne CRLF → LF dans tout le repo
Applique .gitattributes sur tous les fichiers existants.
Élimine les différences fantômes entre WSL et Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 08:55:10 +02:00

99 lines
2.5 KiB
TypeScript
Executable File

export type ToastType = 'success' | 'error';
interface ToastAction {
label: string;
callback: () => void;
}
interface ToastItem {
id: number;
type: ToastType;
message: string;
}
const ICON: Record<ToastType, string> = {
success: 'check_circle',
error: 'error',
};
const DURATION: Record<ToastType, number> = {
success: 3000,
error: 5000,
};
const DURATION_WITH_ACTION = 10_000;
const MAX_VISIBLE = 3;
let nextId = 0;
let container: HTMLElement | null = null;
const items: ToastItem[] = [];
export const Toast = {
mount(parent: HTMLElement): void {
if (container?.isConnected) return; // déjà monté
container = document.createElement('div');
container.id = 'toast-container';
parent.appendChild(container);
},
show(type: ToastType, message: string, action?: ToastAction): void {
if (!container?.isConnected) return;
const id = nextId++;
items.push({ id, type, message });
// Evincer les plus anciens si > MAX_VISIBLE
while (items.length > MAX_VISIBLE) {
const old = items.shift()!;
const oldEl = container.querySelector(`[data-toast-id="${old.id}"]`);
if (oldEl) oldEl.remove();
}
const el = document.createElement('div');
el.className = `toast toast-${type}`;
el.dataset['toastId'] = String(id);
const iconSpan = document.createElement('span');
iconSpan.className = 'toast-icon material-symbols-outlined';
iconSpan.textContent = ICON[type];
const msgSpan = document.createElement('span');
msgSpan.className = 'toast-msg';
msgSpan.textContent = message;
el.appendChild(iconSpan);
el.appendChild(msgSpan);
if (action) {
const btn = document.createElement('button');
btn.className = 'toast-action';
btn.textContent = action.label;
btn.addEventListener('click', () => {
action.callback();
cleanup();
}, { once: true });
el.appendChild(btn);
}
container.appendChild(el);
// Trigger animation
requestAnimationFrame(() => el.classList.add('toast-visible'));
const cleanup = () => {
if (!el.parentNode) return;
el.remove();
const idx = items.findIndex(i => i.id === id);
if (idx !== -1) items.splice(idx, 1);
};
const duration = action ? DURATION_WITH_ACTION : DURATION[type];
setTimeout(() => {
el.classList.remove('toast-visible');
el.classList.add('toast-exit');
el.addEventListener('animationend', cleanup, { once: true });
// Fallback si animationend ne se declenche pas
setTimeout(cleanup, 500);
}, duration);
},
};