Changeset 0.37.2 (#214)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
94
src/js/sw/primitives/toast.js
Normal file
94
src/js/sw/primitives/toast.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user