diff --git a/public/themes/dist_static/static/css/category.css b/public/themes/dist_static/static/css/category.css index c251b10..89a4663 100644 --- a/public/themes/dist_static/static/css/category.css +++ b/public/themes/dist_static/static/css/category.css @@ -162,6 +162,26 @@ display: none; } +@keyframes c-card-enter { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +.s-product-card.is-entering { + animation: c-card-enter 0.35s ease both; + animation-delay: var(--enter-delay, 0ms); +} +@media (prefers-reduced-motion: reduce) { + .s-product-card.is-entering { + animation: none; + } +} + .c-empty { display: flex; align-items: center; diff --git a/public/themes/dist_static/static/js/category.js b/public/themes/dist_static/static/js/category.js index 39b1644..6f4a23d 100644 --- a/public/themes/dist_static/static/js/category.js +++ b/public/themes/dist_static/static/js/category.js @@ -65,6 +65,20 @@ function updateProductFilter(state) { grid.hidden = visibleCount === 0; empty.hidden = visibleCount > 0; } +function animateProductCards(state) { + const panel = document.querySelector(`[data-category-panel="${state.categoryId}"]`); + if (!panel) return; + const cards = [...panel.querySelectorAll(".s-product-card:not([hidden])")]; + cards.forEach((card) => { + card.classList.remove("is-entering"); + card.style.removeProperty("--enter-delay"); + }); + void panel.offsetWidth; + cards.forEach((card, index) => { + card.style.setProperty("--enter-delay", `${index * 50}ms`); + card.classList.add("is-entering"); + }); +} function updateBreadcrumb(state) { const name = getCategoryName(state.categoryId); const breadcrumb = document.querySelector("[data-breadcrumb-current]"); @@ -75,6 +89,7 @@ function updateView(state) { updateSidebar(state); updatePanels(state); updateProductFilter(state); + animateProductCards(state); updateBreadcrumb(state); } function bindEvents(state) {