const TOAST_POS_KEY = "pikit-toast-pos"; const TOAST_ANIM_KEY = "pikit-toast-anim"; const TOAST_SPEED_KEY = "pikit-toast-speed"; const TOAST_DURATION_KEY = "pikit-toast-duration"; const FONT_KEY = "pikit-font"; export const ALLOWED_TOAST_POS = [ "bottom-center", "bottom-right", "bottom-left", "top-right", "top-left", "top-center", ]; export const ALLOWED_TOAST_ANIM = ["slide-in", "fade", "pop", "bounce", "drop", "grow"]; export const ALLOWED_FONTS = ["redhat", "space", "manrope", "dmsans", "sora", "chivo", "atkinson", "plex"]; const clamp = (val, min, max) => Math.min(max, Math.max(min, val)); export function createToastManager({ container, posSelect, animSelect, speedInput, durationInput, fontSelect, testBtn, } = {}) { const state = { position: "bottom-center", animation: "slide-in", durationMs: 5000, speedMs: 300, font: "redhat", }; function applyToastSettings() { if (!container) return; container.className = `toast-container pos-${state.position}`; document.documentElement.style.setProperty("--toast-speed", `${state.speedMs}ms`); const dir = state.position.startsWith("top") ? -1 : 1; const isLeft = state.position.includes("left"); const isRight = state.position.includes("right"); const slideX = isLeft ? -26 : isRight ? 26 : 0; const slideY = isLeft || isRight ? 0 : dir * 24; document.documentElement.style.setProperty("--toast-slide-offset", `${dir * 24}px`); document.documentElement.style.setProperty("--toast-dir", `${dir}`); document.documentElement.style.setProperty("--toast-slide-x", `${slideX}px`); document.documentElement.style.setProperty("--toast-slide-y", `${slideY}px`); if (durationInput) durationInput.value = state.durationMs; } function applyFontSetting() { document.documentElement.setAttribute("data-font", state.font); if (fontSelect) fontSelect.value = state.font; } function persistSettings() { try { localStorage.setItem(TOAST_POS_KEY, state.position); localStorage.setItem(TOAST_ANIM_KEY, state.animation); localStorage.setItem(TOAST_SPEED_KEY, String(state.speedMs)); localStorage.setItem(TOAST_DURATION_KEY, String(state.durationMs)); localStorage.setItem(FONT_KEY, state.font); } catch (e) { console.warn("Toast settings save failed", e); } } function loadFromStorage() { try { const posSaved = localStorage.getItem(TOAST_POS_KEY); if (ALLOWED_TOAST_POS.includes(posSaved)) state.position = posSaved; const animSaved = localStorage.getItem(TOAST_ANIM_KEY); const migrated = animSaved === "slide-up" || animSaved === "slide-left" || animSaved === "slide-right" ? "slide-in" : animSaved; if (migrated && ALLOWED_TOAST_ANIM.includes(migrated)) state.animation = migrated; const savedSpeed = Number(localStorage.getItem(TOAST_SPEED_KEY)); if (!Number.isNaN(savedSpeed) && savedSpeed >= 100 && savedSpeed <= 3000) { state.speedMs = savedSpeed; } const savedDur = Number(localStorage.getItem(TOAST_DURATION_KEY)); if (!Number.isNaN(savedDur) && savedDur >= 1000 && savedDur <= 15000) { state.durationMs = savedDur; } const savedFont = localStorage.getItem(FONT_KEY); if (ALLOWED_FONTS.includes(savedFont)) state.font = savedFont; } catch (e) { console.warn("Toast settings load failed", e); } if (posSelect) posSelect.value = state.position; if (animSelect) animSelect.value = state.animation; if (speedInput) speedInput.value = state.speedMs; if (durationInput) durationInput.value = state.durationMs; if (fontSelect) fontSelect.value = state.font; applyToastSettings(); applyFontSetting(); } function showToast(message, type = "info") { if (!container || !message) return; const t = document.createElement("div"); t.className = `toast ${type} anim-${state.animation}`; t.textContent = message; container.appendChild(t); const animOn = document.documentElement.getAttribute("data-anim") !== "off"; if (!animOn) { t.classList.add("show"); } else { requestAnimationFrame(() => t.classList.add("show")); } const duration = state.durationMs; setTimeout(() => { const all = Array.from(container.querySelectorAll(".toast")); const others = all.filter((el) => el !== t && !el.classList.contains("leaving")); const first = new Map( others.map((el) => [el, el.getBoundingClientRect()]), ); t.classList.add("leaving"); void t.offsetHeight; requestAnimationFrame(() => { const second = new Map( others.map((el) => [el, el.getBoundingClientRect()]), ); others.forEach((el) => { const dy = first.get(el).top - second.get(el).top; if (Math.abs(dy) > 0.5) { el.style.transition = "transform var(--toast-speed, 0.28s) ease"; el.style.transform = `translateY(${dy}px)`; requestAnimationFrame(() => { el.style.transform = ""; }); } }); }); const removeDelay = animOn ? state.speedMs : 0; setTimeout(() => { t.classList.remove("show"); t.remove(); others.forEach((el) => (el.style.transition = "")); }, removeDelay); }, duration); } function wireControls() { posSelect?.addEventListener("change", () => { const val = posSelect.value; if (ALLOWED_TOAST_POS.includes(val)) { state.position = val; applyToastSettings(); persistSettings(); } else { posSelect.value = state.position; showToast("Invalid toast position", "error"); } }); animSelect?.addEventListener("change", () => { let val = animSelect.value; if (val === "slide-up" || val === "slide-left" || val === "slide-right") val = "slide-in"; if (ALLOWED_TOAST_ANIM.includes(val)) { state.animation = val; persistSettings(); } else { animSelect.value = state.animation; showToast("Invalid toast animation", "error"); } }); const clampSpeed = (val) => clamp(val, 100, 3000); const clampDuration = (val) => clamp(val, 1000, 15000); speedInput?.addEventListener("input", () => { const raw = speedInput.value; if (raw === "") return; const val = Number(raw); if (Number.isNaN(val) || val < 100 || val > 3000) return; state.speedMs = val; applyToastSettings(); persistSettings(); }); speedInput?.addEventListener("blur", () => { const raw = speedInput.value; if (raw === "") { speedInput.value = state.speedMs; return; } const val = Number(raw); if (Number.isNaN(val) || val < 100 || val > 3000) { state.speedMs = clampSpeed(state.speedMs); speedInput.value = state.speedMs; showToast("Toast speed must be 100-3000 ms", "error"); return; } state.speedMs = val; speedInput.value = state.speedMs; applyToastSettings(); persistSettings(); }); durationInput?.addEventListener("input", () => { const raw = durationInput.value; if (raw === "") return; const val = Number(raw); if (Number.isNaN(val) || val < 1000 || val > 15000) return; state.durationMs = val; persistSettings(); }); durationInput?.addEventListener("blur", () => { const raw = durationInput.value; if (raw === "") { durationInput.value = state.durationMs; return; } const val = Number(raw); if (Number.isNaN(val) || val < 1000 || val > 15000) { state.durationMs = clampDuration(state.durationMs); durationInput.value = state.durationMs; showToast("Toast duration must be 1000-15000 ms", "error"); return; } state.durationMs = val; durationInput.value = state.durationMs; persistSettings(); }); fontSelect?.addEventListener("change", () => { const val = fontSelect.value; if (!ALLOWED_FONTS.includes(val)) { fontSelect.value = state.font; showToast("Invalid font choice", "error"); return; } state.font = val; applyFontSetting(); persistSettings(); }); testBtn?.addEventListener("click", () => showToast("This is a test toast", "info")); } loadFromStorage(); wireControls(); return { state, showToast, applyToastSettings, applyFontSetting, persistSettings, loadFromStorage, }; }