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>
99 lines
2.5 KiB
TypeScript
Executable File
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);
|
|
},
|
|
};
|