chore: prep 0.1.2 release, tidy repo
This commit is contained in:
258
pikit-web/assets/toast.js
Normal file
258
pikit-web/assets/toast.js
Normal file
@@ -0,0 +1,258 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user