Changeset 0.37.2 (#214)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-20 23:52:55 +00:00
committed by xcaliber
parent 8c53f61b71
commit 4f1abc6321
24 changed files with 1775 additions and 25 deletions

View 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>
`;
}