51 lines
1.5 KiB
TypeScript
51 lines
1.5 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useRef, useState } from 'react';
|
|
|
|
// ISS-008 (v0.17.0): transient toast notifications — a bottom-right stack for action errors
|
|
// and game events derived from snapshot diffs. Each toast auto-dismisses after TOAST_TTL_MS;
|
|
// the queue is capped at MAX_TOASTS (oldest dropped) so an event burst can't fill the corner.
|
|
|
|
export type ToastKind = 'error' | 'info';
|
|
|
|
export interface Toast {
|
|
id: number;
|
|
kind: ToastKind;
|
|
message: string;
|
|
}
|
|
|
|
export const TOAST_TTL_MS = 5_000;
|
|
export const MAX_TOASTS = 4;
|
|
|
|
// Pure core (unit-tested): append a toast, dropping the oldest beyond the cap.
|
|
export function pushToast(toasts: Toast[], toast: Toast): Toast[] {
|
|
return [...toasts, toast].slice(-MAX_TOASTS);
|
|
}
|
|
|
|
export interface UseToastsResult {
|
|
toasts: Toast[];
|
|
push: (kind: ToastKind, message: string) => void;
|
|
dismiss: (id: number) => void;
|
|
}
|
|
|
|
export function useToasts(): UseToastsResult {
|
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
const nextIdRef = useRef(1);
|
|
|
|
// Functional update — a late timer for an already-dismissed/dropped id is a no-op.
|
|
const dismiss = useCallback((id: number) => {
|
|
setToasts((current) => current.filter((t) => t.id !== id));
|
|
}, []);
|
|
|
|
const push = useCallback(
|
|
(kind: ToastKind, message: string) => {
|
|
const toast: Toast = { id: nextIdRef.current++, kind, message };
|
|
setToasts((current) => pushToast(current, toast));
|
|
setTimeout(() => dismiss(toast.id), TOAST_TTL_MS);
|
|
},
|
|
[dismiss],
|
|
);
|
|
|
|
return { toasts, push, dismiss };
|
|
}
|