/**
* GnToastProvider - Renderless-ish toast host for Vue.
*
* Renders slot content plus a single toast at the bottom-right.
* Exposes an imperative API via provide/inject and component expose.
*/
import { defineComponent, h, provide, ref, nextTick, getCurrentInstance } from "vue";
import { cx, iconNode, normalizeVariant } from "../utils.js";
import { toastKey } from "../composables/toast-context.js";
const iconByVariant = {
info: "ph-info",
success: "ph-check-circle",
warning: "ph-warning",
danger: "ph-warning-octagon",
error: "ph-warning-octagon",
primary: "ph-info",
secondary: "ph-info"
};
export default defineComponent({
name: "GnToastProvider",
props: {
lifetime: { type: Number, default: 4000 }
},
setup(props, { slots, expose }) {
const toast = ref(null);
const closing = ref(false);
const showing = ref(false);
const hovered = ref(false);
let timer = null;
let closeTimer = null;
let startedAt = 0;
let remaining = 0;
const clearTimers = () => {
window.clearTimeout(timer);
window.clearTimeout(closeTimer);
timer = null;
closeTimer = null;
};
const dismiss = () => {
clearTimers();
closing.value = true;
showing.value = false;
closeTimer = window.setTimeout(() => {
toast.value = null;
closing.value = false;
}, 300);
};
const close = () => {
clearTimers();
closing.value = false;
showing.value = false;
hovered.value = false;
toast.value = null;
remaining = 0;
};
const scheduleClose = delay => {
clearTimers();
startedAt = Date.now();
remaining = delay;
timer = window.setTimeout(() => {
remaining = 0;
dismiss();
}, delay);
};
const pause = () => {
if (!timer) return;
remaining = Math.max(0, remaining - (Date.now() - startedAt));
window.clearTimeout(timer);
timer = null;
hovered.value = true;
};
const resume = () => {
hovered.value = false;
if (!toast.value || remaining <= 0) return;
scheduleClose(remaining);
};
const show = options => {
const alone = options.alone !== undefined ? options.alone : true;
if (alone && toast.value) {
dismiss();
}
clearTimers();
closing.value = false;
showing.value = false;
hovered.value = false;
remaining = 0;
const variant = normalizeVariant(options.variant || options.type || "info", "info");
const lifetime = options.lifetime !== undefined ? options.lifetime : props.lifetime;
toast.value = {
id: Date.now(),
variant: variant === "error" ? "danger" : variant,
title: options.title || "",
text: options.text || options.message || "",
icon: options.icon || iconByVariant[variant] || iconByVariant.info,
lifetime
};
if (lifetime > 0) {
scheduleClose(lifetime);
}
nextTick(() => {
requestAnimationFrame(() => {
showing.value = true;
});
});
};
const api = {
show,
close,
info: options => show({ ...options, variant: "info" }),
success: options => show({ ...options, variant: "success" }),
warning: options => show({ ...options, variant: "warning" }),
danger: options => show({ ...options, variant: "danger" }),
error: options => show({ ...options, variant: "danger" })
};
provide(toastKey, api);
expose(api);
const instance = getCurrentInstance();
if (instance?.appContext?.app?.config?.globalProperties) {
instance.appContext.app.config.globalProperties.$vmkToasts = api;
}
const toastClass = () => {
if (closing.value) return "a-hide";
if (showing.value) return "a-show";
return "";
};
return () => [
slots.default?.(),
toast.value && h("div", {
class: cx("toast", toastClass(), `toast-${toast.value.variant}`),
role: "alert",
onMouseenter: pause,
onMouseleave: resume
}, [
h("div", { class: "toast-content" }, [
h("div", { class: "toast-header" }, [
iconNode(toast.value.icon),
toast.value.title
]),
toast.value.text && h("p", { class: "toast-text" }, toast.value.text)
]),
h("button", {
class: "btn-icon toast-close",
type: "button",
"aria-label": "Close",
onClick: close
}, [iconNode("ph-x")]),
toast.value.lifetime > 0 && h("div", { class: "toast-progress" }, [
h("div", {
class: "toast-progress-bar",
style: { animationDuration: `${toast.value.lifetime}ms` }
})
])
])
];
}
});