first deployment of kikomiko with url photos

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shayan khoshrou
2026-10-19 19:10:57 +02:00
co-authored by Cursor
parent 8d98304137
commit ae3ed7c7b7
159 changed files with 59509 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
/**
* Horizontal product rails with under-rail dots + 3s autoplay ([data-product-slider]).
* RTL-safe scrollLeft (Chrome negative vs Firefox positive).
*/
export function initProductSliders() {
document.querySelectorAll("[data-product-slider]").forEach((root) => {
const rail = root.querySelector("[data-slider-rail]");
const dotsHost = root.querySelector("[data-slider-dots]");
if (!(rail instanceof HTMLElement)) return;
const rtl =
getComputedStyle(rail).direction === "rtl" ||
document.documentElement.dir === "rtl";
const AUTO_MS = 3000;
// Chrome/Safari RTL: scrollLeft goes negative; Firefox RTL: positive.
let rtlNegative = false;
if (rtl) {
const prevLeft = rail.scrollLeft;
rail.scrollLeft = -1;
rtlNegative = rail.scrollLeft < 0;
rail.scrollLeft = prevLeft;
}
const step = () => {
const card = rail.querySelector(".product-card");
const w = card instanceof HTMLElement ? card.getBoundingClientRect().width : 200;
const styles = getComputedStyle(rail);
const gap = Number.parseFloat(styles.columnGap || styles.gap || "1") || 1;
return Math.max(w + gap, 120);
};
function maxScroll() {
return Math.max(0, rail.scrollWidth - rail.clientWidth);
}
function canScroll() {
return maxScroll() > 4;
}
/** Absolute distance from the logical start (0…max), independent of engine sign. */
function scrollPos() {
return Math.abs(rail.scrollLeft);
}
function scrollToPos(pos, smooth) {
const max = maxScroll();
const target = Math.min(max, Math.max(0, pos));
let left = target;
if (rtl) left = rtlNegative ? -target : target;
rail.scrollTo({ left, behavior: smooth ? "smooth" : "auto" });
}
function pageCount() {
const max = maxScroll();
if (max <= 4) return 1;
return Math.floor(max / step()) + 1;
}
function currentPage() {
const pages = pageCount();
if (pages <= 1) return 0;
const s = step();
return Math.min(pages - 1, Math.round(scrollPos() / s));
}
function goToPage(index, smooth = true) {
const pages = pageCount();
const i = Math.max(0, Math.min(pages - 1, index));
const max = maxScroll();
if (i >= pages - 1) scrollToPos(max, smooth);
else scrollToPos(i * step(), smooth);
}
function goNext() {
const max = maxScroll();
if (max <= 4) return;
const pages = pageCount();
const next = currentPage() + 1;
if (next >= pages) goToPage(0, true);
else goToPage(next, true);
}
function renderDots() {
if (!(dotsHost instanceof HTMLElement)) return;
const pages = pageCount();
if (pages <= 1) {
dotsHost.hidden = true;
dotsHost.replaceChildren();
return;
}
dotsHost.hidden = false;
const active = currentPage();
if (dotsHost.childElementCount !== pages) {
const frag = document.createDocumentFragment();
for (let i = 0; i < pages; i++) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "slider-dot";
btn.setAttribute("role", "tab");
btn.setAttribute("aria-label", `صفحه ${i + 1}`);
btn.addEventListener("click", (e) => {
e.preventDefault();
goToPage(i, true);
startAuto();
});
frag.appendChild(btn);
}
dotsHost.replaceChildren(frag);
}
Array.from(dotsHost.children).forEach((el, i) => {
if (!(el instanceof HTMLElement)) return;
const on = i === active;
el.classList.toggle("is-active", on);
el.setAttribute("aria-selected", on ? "true" : "false");
if (on) el.setAttribute("aria-current", "true");
else el.removeAttribute("aria-current");
});
}
let timer = 0;
function stopAuto() {
window.clearInterval(timer);
timer = 0;
}
function startAuto() {
stopAuto();
if (!canScroll()) return;
timer = window.setInterval(goNext, AUTO_MS);
}
root.addEventListener("mouseenter", stopAuto);
root.addEventListener("mouseleave", startAuto);
root.addEventListener(
"focusin",
(e) => {
if (e.target instanceof Element && e.target.closest(".slider-dot")) {
/* keep running */
} else {
stopAuto();
}
},
true
);
root.addEventListener("focusout", () => {
window.setTimeout(() => {
if (!root.contains(document.activeElement)) startAuto();
}, 0);
});
rail.addEventListener(
"pointerdown",
() => {
stopAuto();
},
{ passive: true }
);
rail.addEventListener(
"pointerup",
() => {
startAuto();
},
{ passive: true }
);
rail.addEventListener(
"scroll",
() => {
renderDots();
},
{ passive: true }
);
window.addEventListener(
"resize",
() => {
renderDots();
startAuto();
},
{ passive: true }
);
// Layout may not be final on first paint (images / fonts).
const kick = () => {
renderDots();
startAuto();
};
kick();
window.requestAnimationFrame(kick);
window.setTimeout(kick, 400);
window.addEventListener("load", kick, { once: true });
});
}