This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/primitives/toast.js
gobha 4f1abc6321 Changeset 0.37.2 (#214)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-20 23:52:55 +00:00

95 lines
3.0 KiB
JavaScript

/**
* Toast primitive + ToastContainer
* Variants: success, error, info, warn
* Auto-dismiss, hover-pause, max 5 visible, stacks bottom-right
*/
const { html } = window;
const { useState, useEffect, useRef, useCallback } = hooks;
const { h, render } = preact;
// ── Singleton state ──────────────────────────
let _toasts = [];
let _id = 0;
let _setToasts = null;
export function toast(message, variant = 'info', duration) {
if (!duration) duration = variant === 'error' ? 5000 : 3000;
const id = ++_id;
const t = { id, message, variant, duration };
_toasts = [..._toasts, t];
if (_toasts.length > 5) _toasts = _toasts.slice(-5);
if (_setToasts) _setToasts([..._toasts]);
return id;
}
toast.success = (msg, dur) => toast(msg, 'success', dur);
toast.error = (msg, dur) => toast(msg, 'error', dur);
toast.info = (msg, dur) => toast(msg, 'info', dur);
toast.warn = (msg, dur) => toast(msg, 'warn', dur);
function dismiss(id) {
_toasts = _toasts.filter(t => t.id !== id);
if (_setToasts) _setToasts([..._toasts]);
}
// ── Single Toast ─────────────────────────────
function ToastItem({ id, message, variant, duration }) {
const [exiting, setExiting] = useState(false);
const timer = useRef(null);
const hovered = useRef(false);
const startTimer = useCallback(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
setExiting(true);
setTimeout(() => dismiss(id), 200);
}, duration);
}, [id, duration]);
useEffect(() => {
startTimer();
return () => clearTimeout(timer.current);
}, [startTimer]);
const onEnter = useCallback(() => {
hovered.current = true;
clearTimeout(timer.current);
}, []);
const onLeave = useCallback(() => {
hovered.current = false;
startTimer();
}, [startTimer]);
const cls = `sw-toast sw-toast--${variant} ${exiting ? 'sw-toast--exit' : 'sw-toast--enter'}`;
return html`
<div class=${cls} role="alert"
onMouseEnter=${onEnter}
onMouseLeave=${onLeave}>
<span class="sw-toast__msg">${message}</span>
<button class="sw-toast__close" onClick=${() => {
setExiting(true);
setTimeout(() => dismiss(id), 200);
}} aria-label="Dismiss">\u00d7</button>
</div>
`;
}
// ── Container ────────────────────────────────
export function ToastContainer() {
const [toasts, setToasts] = useState(_toasts);
_setToasts = setToasts;
useEffect(() => () => { _setToasts = null; }, []);
if (!toasts.length) return null;
return html`
<div class="sw-toast-container" aria-live="polite">
${toasts.map(t => html`<${ToastItem} key=${t.id} ...${t} />`)}
</div>
`;
}